Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-09-07 09:12:12 +00:00
parent 913224e81c
commit b9bc4d88ea
49 changed files with 654 additions and 408 deletions

View File

@ -3666,7 +3666,6 @@ Layout/LineLength:
- 'qa/qa/ee/page/project/secure/security_dashboard.rb'
- 'qa/qa/ee/page/project/secure/show.rb'
- 'qa/qa/ee/resource/license.rb'
- 'qa/qa/fixtures/auto_devops_rack/config.ru'
- 'qa/qa/flow/sign_up.rb'
- 'qa/qa/git/repository.rb'
- 'qa/qa/page/base.rb'

View File

@ -217,7 +217,6 @@ Style/Lambda:
- 'lib/gitlab/sidekiq_signals.rb'
- 'lib/gitlab/utils/measuring.rb'
- 'lib/gitlab/visibility_level.rb'
- 'qa/qa/fixtures/auto_devops_rack/config.ru'
- 'rubocop/cop/rspec/modify_sidekiq_middleware.rb'
- 'rubocop/cop/rspec/timecop_freeze.rb'
- 'rubocop/cop/rspec/timecop_travel.rb'

View File

@ -1281,7 +1281,7 @@ GEM
shellany (0.0.1)
shoulda-matchers (5.1.0)
activesupport (>= 5.2.0)
sidekiq (6.4.0)
sidekiq (6.4.2)
connection_pool (>= 2.2.2)
rack (~> 2.0)
redis (>= 4.2.0)

View File

@ -17,11 +17,6 @@ export default {
GlLoadingIcon,
EmptyState,
},
inject: {
renderEmptyState: {
default: false,
},
},
props: {
action: {
type: String,
@ -45,6 +40,11 @@ export default {
type: Boolean,
required: true,
},
renderEmptyState: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
@ -224,6 +224,9 @@ export default {
},
showLegacyEmptyState() {
const { containerEl } = this;
if (!containerEl) return;
const contentListEl = containerEl.querySelector(CONTENT_LIST_CLASS);
const emptyStateEl = containerEl.querySelector('.empty-state');

View File

@ -0,0 +1,80 @@
<script>
import { GlTabs, GlTab } from '@gitlab/ui';
import { __ } from '~/locale';
import GroupsStore from '../store/groups_store';
import GroupsService from '../service/groups_service';
import {
ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
ACTIVE_TAB_SHARED,
ACTIVE_TAB_ARCHIVED,
} from '../constants';
import GroupsApp from './app.vue';
export default {
components: { GlTabs, GlTab, GroupsApp },
inject: ['endpoints'],
data() {
return {
tabs: [
{
title: this.$options.i18n.subgroupsAndProjects,
key: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
renderEmptyState: true,
lazy: false,
service: new GroupsService(this.endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]),
store: new GroupsStore({ showSchemaMarkup: true }),
},
{
title: this.$options.i18n.sharedProjects,
key: ACTIVE_TAB_SHARED,
renderEmptyState: false,
lazy: true,
service: new GroupsService(this.endpoints[ACTIVE_TAB_SHARED]),
store: new GroupsStore(),
},
{
title: this.$options.i18n.archivedProjects,
key: ACTIVE_TAB_ARCHIVED,
renderEmptyState: false,
lazy: true,
service: new GroupsService(this.endpoints[ACTIVE_TAB_ARCHIVED]),
store: new GroupsStore(),
},
],
activeTabIndex: 0,
};
},
methods: {
handleTabInput(tabIndex) {
this.activeTabIndex = tabIndex;
const tab = this.tabs[tabIndex];
tab.lazy = false;
},
},
i18n: {
subgroupsAndProjects: __('Subgroups and projects'),
sharedProjects: __('Shared projects'),
archivedProjects: __('Archived projects'),
},
};
</script>
<template>
<gl-tabs content-class="gl-pt-0" :value="activeTabIndex" @input="handleTabInput">
<gl-tab
v-for="{ key, title, renderEmptyState, lazy, service, store } in tabs"
:key="key"
:title="title"
:lazy="lazy"
>
<groups-app
:action="key"
:service="service"
:store="store"
:hide-projects="false"
:render-empty-state="renderEmptyState"
/>
</gl-tab>
</gl-tabs>
</template>

View File

@ -52,7 +52,6 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
newSubgroupIllustration,
newProjectIllustration,
emptySubgroupIllustration,
renderEmptyState,
canCreateSubgroups,
canCreateProjects,
currentGroupVisibility,
@ -65,7 +64,6 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
newSubgroupIllustration,
newProjectIllustration,
emptySubgroupIllustration,
renderEmptyState: parseBoolean(renderEmptyState),
canCreateSubgroups: parseBoolean(canCreateSubgroups),
canCreateProjects: parseBoolean(canCreateProjects),
currentGroupVisibility,
@ -75,6 +73,7 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
const { dataset } = dataEl || this.$options.el;
const hideProjects = parseBoolean(dataset.hideProjects);
const showSchemaMarkup = parseBoolean(dataset.showSchemaMarkup);
const renderEmptyState = parseBoolean(dataset.renderEmptyState);
const service = new GroupsService(endpoint || dataset.endpoint);
const store = new GroupsStore({ hideProjects, showSchemaMarkup });
@ -83,6 +82,7 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
store,
service,
hideProjects,
renderEmptyState,
loading: true,
containerId,
};
@ -119,6 +119,7 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
store: this.store,
service: this.service,
hideProjects: this.hideProjects,
renderEmptyState: this.renderEmptyState,
containerId: this.containerId,
},
});

View File

@ -0,0 +1,57 @@
import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
import { parseBoolean } from '~/lib/utils/common_utils';
import GroupFolder from './components/group_folder.vue';
import GroupItem from './components/group_item.vue';
import {
ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
ACTIVE_TAB_SHARED,
ACTIVE_TAB_ARCHIVED,
} from './constants';
import OverviewTabs from './components/overview_tabs.vue';
export const initGroupOverviewTabs = () => {
const el = document.getElementById('js-group-overview-tabs');
if (!el) return false;
Vue.component('GroupFolder', GroupFolder);
Vue.component('GroupItem', GroupItem);
Vue.use(GlToast);
const {
newSubgroupPath,
newProjectPath,
newSubgroupIllustration,
newProjectIllustration,
emptySubgroupIllustration,
canCreateSubgroups,
canCreateProjects,
currentGroupVisibility,
subgroupsAndProjectsEndpoint,
sharedProjectsEndpoint,
archivedProjectsEndpoint,
} = el.dataset;
return new Vue({
el,
provide: {
newSubgroupPath,
newProjectPath,
newSubgroupIllustration,
newProjectIllustration,
emptySubgroupIllustration,
canCreateSubgroups: parseBoolean(canCreateSubgroups),
canCreateProjects: parseBoolean(canCreateProjects),
currentGroupVisibility,
endpoints: {
[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]: subgroupsAndProjectsEndpoint,
[ACTIVE_TAB_SHARED]: sharedProjectsEndpoint,
[ACTIVE_TAB_ARCHIVED]: archivedProjectsEndpoint,
},
},
render(createElement) {
return createElement(OverviewTabs);
},
});
};

View File

@ -39,6 +39,11 @@ function format(searchTerm, isFallbackKey = false) {
return formattedQuery;
}
function getSearchTerm(newIssuePath) {
const { search, pathname } = document.location;
return newIssuePath === pathname ? '' : format(search);
}
function getFallbackKey() {
const searchTerm = format(document.location.search, true);
return ['autosave', document.location.pathname, searchTerm].join('/');
@ -72,7 +77,8 @@ export default class IssuableForm {
this.reviewersSelect = new UsersSelect(undefined, '.js-reviewer-search');
this.zenMode = new ZenMode();
this.newIssuePath = form[0].getAttribute(DATA_ISSUES_NEW_PATH);
this.searchTerm = getSearchTerm(form[0].getAttribute(DATA_ISSUES_NEW_PATH));
this.fallbackKey = getFallbackKey();
this.titleField = this.form.find('input[name*="[title]"]');
this.descriptionField = this.form.find('textarea[name*="[description]"]');
if (!(this.titleField.length && this.descriptionField.length)) {
@ -109,20 +115,16 @@ export default class IssuableForm {
}
initAutosave() {
const { search, pathname } = document.location;
const searchTerm = this.newIssuePath === pathname ? '' : format(search);
const fallbackKey = getFallbackKey();
this.autosave = new Autosave(
this.autosaveTitle = new Autosave(
this.titleField,
[document.location.pathname, searchTerm, 'title'],
`${fallbackKey}=title`,
[document.location.pathname, this.searchTerm, 'title'],
`${this.fallbackKey}=title`,
);
return new Autosave(
this.autosaveDescription = new Autosave(
this.descriptionField,
[document.location.pathname, searchTerm, 'description'],
`${fallbackKey}=description`,
[document.location.pathname, this.searchTerm, 'description'],
`${this.fallbackKey}=description`,
);
}
@ -131,8 +133,8 @@ export default class IssuableForm {
}
resetAutosave() {
this.titleField.data('autosave').reset();
return this.descriptionField.data('autosave').reset();
this.autosaveTitle.reset();
this.autosaveDescription.reset();
}
initWip() {

View File

@ -1,3 +1,5 @@
import { initGroupOverviewTabs } from '~/groups/init_overview_tabs';
import initGroupDetails from '../shared/group_details';
initGroupDetails('details');
initGroupOverviewTabs();

View File

@ -1,5 +1,7 @@
import leaveByUrl from '~/namespaces/leave_by_url';
import { initGroupOverviewTabs } from '~/groups/init_overview_tabs';
import initGroupDetails from '../shared/group_details';
leaveByUrl('group');
initGroupDetails();
initGroupOverviewTabs();

View File

@ -13,6 +13,7 @@ import Poll from '~/lib/utils/poll';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import { EXTENSION_ICON_CLASS, EXTENSION_ICONS } from '../../constants';
import Actions from '../action_buttons.vue';
import StateContainer from '../state_container.vue';
import StatusIcon from './status_icon.vue';
import ChildContent from './child_content.vue';
import { createTelemetryHub } from './telemetry';
@ -36,6 +37,7 @@ export default {
ChildContent,
DynamicScroller,
DynamicScrollerItem,
StateContainer,
},
directives: {
SafeHtml: GlSafeHtmlDirective,
@ -312,18 +314,15 @@ export default {
data-testid="widget-extension"
data-qa-selector="mr_widget_extension"
>
<div
<state-container
:mr="mr"
:status="statusIconName"
:is-loading="isLoadingSummary"
:class="{ 'gl-cursor-pointer': isCollapsible }"
class="media gl-p-5"
class="gl-p-5"
@mousedown="onRowMouseDown"
@mouseup="onRowMouseUp"
>
<status-icon
:level="1"
:name="$options.label || $options.name"
:is-loading="isLoadingSummary"
:icon-name="statusIconName"
/>
<div
class="media-body gl-display-flex gl-flex-direction-row! gl-align-self-center"
data-testid="widget-extension-top-level"
@ -362,7 +361,7 @@ export default {
/>
</div>
</div>
</div>
</state-container>
<div
v-if="!isCollapsed"
class="mr-widget-grouped-section gl-relative"

View File

@ -62,7 +62,9 @@ export default {
<strong v-else v-safe-html="generateText(data.header)"></strong>
</div>
<div class="gl-display-flex">
<status-icon v-if="data.icon" :icon-name="data.icon.name" :size="12" class="gl-pl-0" />
<div v-if="data.icon" class="report-block-child-icon gl-display-flex">
<status-icon :icon-name="data.icon.name" :size="12" class="gl-m-auto" />
</div>
<div class="gl-w-full">
<div class="gl-display-flex gl-flex-nowrap">
<div class="gl-flex-wrap gl-display-flex gl-w-full">

View File

@ -44,7 +44,14 @@ export default {
<template>
<div class="mr-widget-body media">
<div v-if="isLoading" class="gl-w-full mr-conflict-loader">
<slot name="loading"></slot>
<slot name="loading">
<div class="gl-display-flex">
<status-icon status="loading" />
<div class="media-body">
<slot></slot>
</div>
</div>
</slot>
</div>
<template v-else>
<slot name="icon">

View File

@ -11,7 +11,7 @@ export default {
type: String,
default: '',
required: false,
validator: (value) => value === '' || Object.keys(EXTENSION_ICONS).indexOf(value) > -1,
validator: (value) => value === '' || Object.keys(EXTENSION_ICONS).includes(value),
},
widgetName: {
type: String,

View File

@ -21,7 +21,6 @@
@import './pages/notifications';
@import './pages/pipelines';
@import './pages/profile';
@import './pages/profiles/preferences';
@import './pages/projects';
@import './pages/prometheus';
@import './pages/registry';

View File

@ -1,3 +1,5 @@
@import 'page_bundles/mixins_and_variables_and_functions';
.application-theme {
$ui-gray-bg: #303030;
$ui-light-gray-bg: #f0f0f0;

View File

@ -16,6 +16,10 @@
line-height: 20px;
}
.report-block-child-icon {
height: 20px;
}
.report-block-list {
list-style: none;
padding: 0 1px;

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
class Admin::SpamLogsController < Admin::ApplicationController
feature_category :not_owned # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned
feature_category :instance_resiliency
# rubocop: disable CodeReuse/ActiveRecord
def index

View File

@ -172,6 +172,15 @@ module GroupsHelper
}
end
def group_overview_tabs_app_data(group)
{
subgroups_and_projects_endpoint: group_children_path(group, format: :json),
shared_projects_endpoint: group_shared_projects_path(group, format: :json),
archived_projects_endpoint: group_children_path(group, format: :json, archived: 'only'),
current_group_visibility: group.visibility
}.merge(subgroups_and_projects_list_app_data(group))
end
def enabled_git_access_protocol_options_for_group
case ::Gitlab::CurrentSettings.enabled_git_access_protocol
when nil, ""

View File

@ -33,33 +33,36 @@
= render_if_exists 'groups/group_activity_analytics', group: @group
.groups-listing{ data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } }
.top-area.group-nav-container.justify-content-between
.scrolling-tabs-container.inner-page-scroll-tabs
.fade-left= sprite_icon('chevron-lg-left', size: 12)
.fade-right= sprite_icon('chevron-lg-right', size: 12)
-# `item_active` is set to `false` as the active state is set by `app/assets/javascripts/pages/groups/shared/group_details.js`
-# TODO: Replace this approach in https://gitlab.com/gitlab-org/gitlab/-/issues/23466
= gl_tabs_nav({ class: 'nav-links scrolling-tabs gl-display-flex gl-flex-grow-1 gl-flex-nowrap gl-border-0' }) do
= gl_tab_link_to group_path, item_active: false, tab_class: 'js-subgroups_and_projects-tab', data: { target: 'div#subgroups_and_projects', action: 'subgroups_and_projects', toggle: 'tab' } do
= _("Subgroups and projects")
= gl_tab_link_to group_shared_path, item_active: false, tab_class: 'js-shared-tab', data: { target: 'div#shared', action: 'shared', toggle: 'tab' } do
= _("Shared projects")
= gl_tab_link_to group_archived_path, item_active: false, tab_class: 'js-archived-tab', data: { target: 'div#archived', action: 'archived', toggle: 'tab' } do
= _("Archived projects")
- if Feature.enabled?(:group_overview_tabs_vue, @group)
#js-group-overview-tabs{ data: group_overview_tabs_app_data(@group) }
- else
.groups-listing{ data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } }
.top-area.group-nav-container.justify-content-between
.scrolling-tabs-container.inner-page-scroll-tabs
.fade-left= sprite_icon('chevron-lg-left', size: 12)
.fade-right= sprite_icon('chevron-lg-right', size: 12)
-# `item_active` is set to `false` as the active state is set by `app/assets/javascripts/pages/groups/shared/group_details.js`
-# TODO: Replace this approach in https://gitlab.com/gitlab-org/gitlab/-/issues/23466
= gl_tabs_nav({ class: 'nav-links scrolling-tabs gl-display-flex gl-flex-grow-1 gl-flex-nowrap gl-border-0' }) do
= gl_tab_link_to group_path, item_active: false, tab_class: 'js-subgroups_and_projects-tab', data: { target: 'div#subgroups_and_projects', action: 'subgroups_and_projects', toggle: 'tab' } do
= _("Subgroups and projects")
= gl_tab_link_to group_shared_path, item_active: false, tab_class: 'js-shared-tab', data: { target: 'div#shared', action: 'shared', toggle: 'tab' } do
= _("Shared projects")
= gl_tab_link_to group_archived_path, item_active: false, tab_class: 'js-archived-tab', data: { target: 'div#archived', action: 'archived', toggle: 'tab' } do
= _("Archived projects")
.nav-controls.d-block.d-md-flex
.group-search
= render "shared/groups/search_form"
.nav-controls.d-block.d-md-flex
.group-search
= render "shared/groups/search_form"
= render "shared/groups/dropdown", options_hash: subgroups_sort_options_hash
= render "shared/groups/dropdown", options_hash: subgroups_sort_options_hash
.tab-content
#subgroups_and_projects.tab-pane
= render "subgroups_and_projects", group: @group
.tab-content
#subgroups_and_projects.tab-pane
= render "subgroups_and_projects", group: @group
#shared.tab-pane
= render "shared_projects", group: @group
#shared.tab-pane
= render "shared_projects", group: @group
#archived.tab-pane
= render "archived_projects", group: @group
#archived.tab-pane
= render "archived_projects", group: @group

View File

@ -1,2 +1,2 @@
%p
= s_('Notify|All discussions on merge request %{mr_link} were resolved by %{name}') %{mr_link: sanitize(merge_request_reference_link(@merge_request)), name: sanitize_name(@resolved_by.name)}
= s_('Notify|All discussions on merge request %{mr_link} were resolved by %{name}').html_safe % { mr_link: merge_request_reference_link(@merge_request), name: sanitize_name(@resolved_by.name) }

View File

@ -1,4 +1,5 @@
- page_title _('Preferences')
- add_page_specific_style 'page_bundles/profiles/preferences'
- @content_class = "limit-container-width" unless fluid_layout
- user_theme_id = Gitlab::Themes.for_user(@user).id
- user_color_schema_id = Gitlab::ColorSchemes.for_user(@user).id

View File

@ -291,10 +291,12 @@ module Gitlab
config.assets.precompile << "page_bundles/productivity_analytics.css"
config.assets.precompile << "page_bundles/profile.css"
config.assets.precompile << "page_bundles/profile_two_factor_auth.css"
config.assets.precompile << "page_bundles/profiles/preferences.css"
config.assets.precompile << "page_bundles/project.css"
config.assets.precompile << "page_bundles/projects_edit.css"
config.assets.precompile << "page_bundles/reports.css"
config.assets.precompile << "page_bundles/roadmap.css"
config.assets.precompile << "page_bundles/requirements.css"
config.assets.precompile << "page_bundles/runner_details.css"
config.assets.precompile << "page_bundles/security_dashboard.css"
config.assets.precompile << "page_bundles/security_discover.css"

View File

@ -0,0 +1,8 @@
---
name: group_overview_tabs_vue
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95850
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/370872
milestone: '15.4'
type: development
group: group::workspace
default_enabled: false

View File

@ -9,5 +9,5 @@
require 'sidekiq/web'
if Rails.env.development?
Sidekiq.default_worker_options[:backtrace] = true
Sidekiq.default_job_options[:backtrace] = true
end

View File

@ -71,7 +71,7 @@ Migration file for adding `NOT VALID` foreign key:
```ruby
class AddNotValidForeignKeyToEmailsUser < Gitlab::Database::Migration[2.0]
def up
add_concurrent_foreign_key :emails, :users, on_delete: :cascade, validate: false
add_concurrent_foreign_key :emails, :users, column: :user_id, on_delete: :cascade, validate: false
end
def down

View File

@ -144,14 +144,14 @@ graph LR
### Manually deploy to production
GitLab Docs is deployed to production whenever the `Build docs.gitlab.com every 4 hours` scheduled pipeline runs. By
default, this pipeline runs every four hours.
GitLab Docs is deployed to production whenever the `Build docs.gitlab.com every hour` scheduled pipeline runs. By
default, this pipeline runs every hour.
Maintainers can [manually](../../../ci/pipelines/schedules.md#run-manually) run this pipeline to force a deployment to
production:
1. Go to the [scheduled pipelines](https://gitlab.com/gitlab-org/gitlab-docs/-/pipeline_schedules) for `gitlab-docs`.
1. Next to `Build docs.gitlab.com every 4 hours`, select **Play** (**{play}**).
1. Next to `Build docs.gitlab.com every hour`, select **Play** (**{play}**).
The updated documentation is available in production after the `pages` and `pages:deploy` jobs
complete in the new pipeline.

View File

@ -190,7 +190,7 @@ To update the linting images:
1. In `gitlab-docs`, open a merge request to update `.gitlab-ci.yml` to use the new tooling
version. ([Example MR](https://gitlab.com/gitlab-org/gitlab-docs/-/merge_requests/2571))
1. When merged, start a `Build docs.gitlab.com every 4 hours` [scheduled pipeline](https://gitlab.com/gitlab-org/gitlab-docs/-/pipeline_schedules).
1. When merged, start a `Build docs.gitlab.com every hour` [scheduled pipeline](https://gitlab.com/gitlab-org/gitlab-docs/-/pipeline_schedules).
1. Go the pipeline you started, and manually run the relevant build-images job,
for example, `image:docs-lint-markdown`.
1. In the job output, get the name of the new image.

View File

@ -49,3 +49,16 @@ Enable the Google Chat integration in GitLab:
To test the integration, make a change based on the events you selected and
see the notification in your Google Chat room.
### Enable threads in Google Chat
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27823) in GitLab 15.4.
To enable threaded notifications for the same GitLab object (for example, an issue or merge request):
1. Go to [Google Chat](https://chat.google.com/).
1. In **Spaces**, select **+ > Create space**.
1. Enter the space name and (optionally) other details, and select **Use threaded replies**.
1. Select **Create**.
You cannot enable threaded replies for existing Google Chat spaces.

View File

@ -1,9 +0,0 @@
FROM ruby:2.6.5-alpine
ADD ./ /app/
WORKDIR /app
ENV RACK_ENV production
ENV PORT 5000
EXPOSE 5000
RUN bundle install
CMD ["bundle","exec", "rackup", "-p", "5000"]

View File

@ -1,5 +0,0 @@
# frozen_string_literal: true
source 'https://rubygems.org'
gem 'rack'
gem 'rake'

View File

@ -1,15 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
rack (2.2.3)
rake (12.3.3)
PLATFORMS
ruby
DEPENDENCIES
rack
rake
BUNDLED WITH
1.17.3

View File

@ -1,9 +0,0 @@
# frozen_string_literal: true
require 'rake/testtask'
task default: %w[test]
task :test do
puts "ok"
end

View File

@ -1,3 +0,0 @@
# frozen_string_literal: true
run lambda { |env| [200, { 'Content-Type' => 'text/plain' }, StringIO.new("Hello World! #{ENV['OPTIONAL_MESSAGE']}\n")] }

View File

@ -10,18 +10,13 @@ module QA
element :clusters_actions_button
end
def connect_existing_cluster
within_element(:clusters_actions_button) { click_button(class: 'dropdown-toggle-split') }
click_link 'Connect a cluster (certificate - deprecated)'
def connect_cluster
click_element(:clusters_actions_button)
end
def has_cluster?(cluster)
has_element?(:cluster, cluster_name: cluster.to_s)
end
def click_on_cluster(cluster)
click_on cluster.cluster_name
end
end
end
end

View File

@ -26,25 +26,18 @@ module QA
end
def api_get_path
"gid://gitlab/Clusters::Agent/#{id}"
"/projects/#{project.id}/cluster_agents/#{id}"
end
def api_post_path
"/graphql"
"/projects/#{project.id}/cluster_agents"
end
def api_post_body
<<~GQL
mutation createAgent {
createClusterAgent(input: { projectPath: "#{project.full_path}", name: "#{@name}" }) {
clusterAgent {
id
name
}
errors
}
{
id: project.id,
name: name
}
GQL
end
end
end

View File

@ -5,7 +5,7 @@ module QA
module Clusters
class AgentToken < QA::Resource::Base
attribute :id
attribute :secret
attribute :token
attribute :agent do
QA::Resource::Clusters::Agent.fabricate_via_api!
end
@ -20,26 +20,19 @@ module QA
end
def api_get_path
"gid://gitlab/Clusters::AgentToken/#{id}"
"/projects/#{agent.project.id}/cluster_agents/#{agent.id}/tokens/#{id}"
end
def api_post_path
"/graphql"
"/projects/#{agent.project.id}/cluster_agents/#{agent.id}/tokens"
end
def api_post_body
<<~GQL
mutation createToken {
clusterAgentTokenCreate(input: { clusterAgentId: "gid://gitlab/Clusters::Agent/#{agent.id}" name: "token-#{agent.id}" }) {
secret # This is the value you need to use on the next step
token {
createdAt
id
}
errors
}
{
id: agent.project.id,
agent_id: agent.id,
name: agent.name
}
GQL
end
end
end

View File

@ -1,50 +0,0 @@
# frozen_string_literal: true
module QA
module Resource
module KubernetesCluster
# TODO: This resource is currently broken, since one-click apps have been removed.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/333818
class ProjectCluster < Base
attr_writer :cluster,
:install_ingress, :install_prometheus, :install_runner, :domain
attribute :project do
Resource::Project.fabricate!
end
attribute :ingress_ip do
@cluster.fetch_external_ip_for_ingress
end
def fabricate!
project.visit!
Page::Project::Menu.perform(
&:go_to_infrastructure_kubernetes)
Page::Project::Infrastructure::Kubernetes::Index.perform(
&:connect_existing_cluster)
Page::Project::Infrastructure::Kubernetes::AddExisting.perform do |cluster_page|
cluster_page.set_cluster_name(@cluster.cluster_name)
cluster_page.set_api_url(@cluster.api_url)
cluster_page.set_ca_certificate(@cluster.ca_certificate)
cluster_page.set_token(@cluster.token)
cluster_page.uncheck_rbac! unless @cluster.rbac
cluster_page.add_cluster!
end
Page::Project::Infrastructure::Kubernetes::Show.perform do |show|
if @install_ingress
ingress_ip
show.set_domain("#{@ingress_ip}.nip.io")
show.save_domain
end
end
end
end
end
end
end

View File

@ -33,14 +33,32 @@ module QA
delete_cluster
end
def install_ingress
QA::Runtime::Logger.info "Attempting to install Ingress on cluster #{cluster_name}"
shell 'kubectl create -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-0.31.0/deploy/static/provider/cloud/deploy.yaml'
wait_for_ingress
# kas is hardcoded to staging since this test should only run in staging for now
def install_kubernetes_agent(agent_token)
install_helm
shell <<~CMD.tr("\n", ' ')
helm repo add gitlab https://charts.gitlab.io &&
helm repo update &&
helm upgrade --install test gitlab/gitlab-agent
--namespace gitlab-agent
--create-namespace
--set image.tag=#{Runtime::Env.gitlab_agentk_version}
--set config.token=#{agent_token}
--set config.kasAddress=wss://kas.staging.gitlab.com
CMD
end
private
def install_helm
shell <<~CMD.tr("\n", ' ')
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 &&
chmod 700 get_helm.sh &&
./get_helm.sh
CMD
end
def login_if_not_already_logged_in
if Runtime::Env.has_gcloud_credentials?
attempt_login_with_env_vars
@ -104,18 +122,6 @@ module QA
def get_region
Runtime::Env.gcloud_region || @available_regions.delete(@available_regions.sample)
end
def wait_for_ingress
QA::Runtime::Logger.info 'Waiting for Ingress controller pod to be initialized'
Support::Retrier.retry_until(max_attempts: 60, sleep_interval: 1) do
service_available?('kubectl get pods --all-namespaces -l app.kubernetes.io/component=controller | grep -o "ingress-nginx-controller.*1/1"')
end
end
def service_available?(command)
system("#{command} > /dev/null 2>&1")
end
end
end
end

View File

@ -41,8 +41,8 @@ module QA
cluster_name
end
def install_ingress
@provider.install_ingress
def install_kubernetes_agent(agent_token)
@provider.install_kubernetes_agent(agent_token)
end
def create_secret(secret, secret_name)
@ -73,16 +73,6 @@ module QA
shell('kubectl apply -f -', stdin_data: network_policy)
end
def fetch_external_ip_for_ingress
install_ingress
# need to wait since the ingress-nginx service has an initial delay set of 10 seconds
sleep 12
ingress_ip = `kubectl get svc --all-namespaces --no-headers=true -l app.kubernetes.io/name=ingress-nginx -o custom-columns=:'status.loadBalancer.ingress[0].ip' | grep -v 'none'`
QA::Runtime::Logger.debug "Has ingress address set to: #{ingress_ip}"
ingress_ip
end
private
def fetch_api_url

View File

@ -1,78 +1,67 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Configure',
only: { subdomain: %i[staging staging-canary] },
quarantine: {
issue: 'https://gitlab.com/gitlab-org/quality/team-tasks/-/issues/1198',
type: :waiting_on
} do
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'autodevops-project'
project.auto_devops_enabled = true
RSpec.describe 'Configure', only: { subdomain: %i[staging staging-canary] } do
describe 'Auto DevOps with a Kubernetes Agent' do
let!(:app_project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'autodevops-app-project'
project.template_name = 'express'
project.auto_devops_enabled = true
end
end
end
before do
set_kube_ingress_base_domain(project)
disable_optional_jobs(project)
end
let!(:cluster) { Service::KubernetesCluster.new(provider_class: Service::ClusterProvider::Gcloud).create! }
describe 'Auto DevOps support' do
context 'when rbac is enabled' do
let(:cluster) { Service::KubernetesCluster.new.create! }
let!(:kubernetes_agent) do
Resource::Clusters::Agent.fabricate_via_api! do |agent|
agent.name = 'agent1'
agent.project = app_project
end
end
after do
cluster&.remove!
project.remove_via_api!
let!(:agent_token) do
Resource::Clusters::AgentToken.fabricate_via_api! do |token|
token.agent = kubernetes_agent
end
end
before do
cluster.install_kubernetes_agent(agent_token.token)
upload_agent_config(app_project, kubernetes_agent.name)
set_kube_ingress_base_domain(app_project)
set_kube_context(app_project)
disable_optional_jobs(app_project)
end
after do
cluster&.remove!
end
it 'runs auto devops', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348061' do
Flow::Login.sign_in
app_project.visit!
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button)
Page::Project::Pipeline::New.perform(&:click_run_pipeline_button)
Page::Project::Pipeline::Show.perform do |pipeline|
pipeline.click_job('build')
end
Page::Project::Job::Show.perform do |job|
expect(job).to be_successful(timeout: 600)
job.click_element(:pipeline_path)
end
it 'runs auto devops', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348061' do
Flow::Login.sign_in
Resource::KubernetesCluster::ProjectCluster.fabricate! do |k8s_cluster|
k8s_cluster.project = project
k8s_cluster.cluster = cluster
k8s_cluster.install_ingress = true
end
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.directory = Pathname
.new(__dir__)
.join('../../../../../fixtures/auto_devops_rack')
push.commit_message = 'Create Auto DevOps compatible rack application'
end
Flow::Pipeline.visit_latest_pipeline
Page::Project::Pipeline::Show.perform do |pipeline|
pipeline.click_job('build')
end
Page::Project::Job::Show.perform do |job|
expect(job).to be_successful(timeout: 600)
job.click_element(:pipeline_path)
end
Page::Project::Pipeline::Show.perform do |pipeline|
pipeline.click_job('test')
end
Page::Project::Job::Show.perform do |job|
expect(job).to be_successful(timeout: 600)
job.click_element(:pipeline_path)
end
Page::Project::Pipeline::Show.perform do |pipeline|
pipeline.click_job('production')
end
Page::Project::Job::Show.perform do |job|
expect(job).to be_successful(timeout: 1200)
job.click_element(:pipeline_path)
end
Page::Project::Pipeline::Show.perform do |pipeline|
pipeline.click_job('production')
end
Page::Project::Job::Show.perform do |job|
expect(job).to be_successful(timeout: 600)
end
end
end
@ -88,12 +77,43 @@ module QA
end
end
def set_kube_context(project)
Resource::CiVariable.fabricate_via_api! do |resource|
resource.project = project
resource.key = 'KUBE_CONTEXT'
resource.value = "#{project.path_with_namespace}:#{kubernetes_agent.name}"
resource.masked = false
end
end
def upload_agent_config(project, agent)
Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = project
commit.commit_message = 'Add kubernetes agent configuration'
commit.add_files(
[
{
file_path: ".gitlab/agents/#{agent}/config.yaml",
content: <<~YAML
ci_access:
projects:
- id: #{project.path_with_namespace}
YAML
}
]
)
end
end
end
def disable_optional_jobs(project)
%w[
CODE_QUALITY_DISABLED LICENSE_MANAGEMENT_DISABLED
SAST_DISABLED DAST_DISABLED DEPENDENCY_SCANNING_DISABLED
CONTAINER_SCANNING_DISABLED BROWSER_PERFORMANCE_DISABLED
SECRET_DETECTION_DISABLED
TEST_DISABLED CODE_QUALITY_DISABLED LICENSE_MANAGEMENT_DISABLED
BROWSER_PERFORMANCE_DISABLED LOAD_PERFORMANCE_DISABLED
SAST_DISABLED SECRET_DETECTION_DISABLED DEPENDENCY_SCANNING_DISABLED
CONTAINER_SCANNING_DISABLED DAST_DISABLED REVIEW_DISABLED
CODE_INTELLIGENCE_DISABLED CLUSTER_IMAGE_SCANNING_DISABLED
].each do |key|
Resource::CiVariable.fabricate_via_api! do |resource|
resource.project = project

View File

@ -1,38 +0,0 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Configure', except: { job: 'review-qa-*' } do
describe 'Kubernetes Cluster Integration', :orchestrated, :requires_admin, :skip_live_env do
context 'Project Clusters' do
let!(:cluster) { Service::KubernetesCluster.new(provider_class: Service::ClusterProvider::K3s).create! }
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'project-with-k8s'
project.description = 'Project with Kubernetes cluster integration'
end
end
before do
Flow::Login.sign_in_as_admin
end
after do
cluster.remove!
end
it 'can create and associate a project cluster', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348062' do
Resource::KubernetesCluster::ProjectCluster.fabricate_via_browser_ui! do |k8s_cluster|
k8s_cluster.project = project
k8s_cluster.cluster = cluster
end.project.visit!
Page::Project::Menu.perform(&:go_to_infrastructure_kubernetes)
Page::Project::Infrastructure::Kubernetes::Index.perform do |index|
expect(index).to have_cluster(cluster)
end
end
end
end
end
end

View File

@ -331,6 +331,7 @@ RSpec.describe 'Group show page' do
end
it 'does not include structured markup in shared projects tab', :aggregate_failures, :js do
stub_feature_flags(group_overview_tabs_vue: false)
other_project = create(:project, :public)
other_project.project_group_links.create!(group: group)
@ -342,6 +343,7 @@ RSpec.describe 'Group show page' do
end
it 'does not include structured markup in archived projects tab', :aggregate_failures, :js do
stub_feature_flags(group_overview_tabs_vue: false)
project.update!(archived: true)
visit group_archived_path(group)

View File

@ -24,6 +24,7 @@ RSpec.describe 'User sorts projects and order persists' do
end
it "is set on the group_canonical_path" do
stub_feature_flags(group_overview_tabs_vue: false)
visit(group_canonical_path(group))
within '[data-testid=group_sort_by_dropdown]' do
@ -32,6 +33,7 @@ RSpec.describe 'User sorts projects and order persists' do
end
it "is set on the details_group_path" do
stub_feature_flags(group_overview_tabs_vue: false)
visit(details_group_path(group))
within '[data-testid=group_sort_by_dropdown]' do
@ -64,6 +66,7 @@ RSpec.describe 'User sorts projects and order persists' do
context 'from group homepage', :js do
before do
stub_feature_flags(group_overview_tabs_vue: false)
sign_in(user)
visit(group_canonical_path(group))
within '[data-testid=group_sort_by_dropdown]' do
@ -77,6 +80,7 @@ RSpec.describe 'User sorts projects and order persists' do
context 'from group details', :js do
before do
stub_feature_flags(group_overview_tabs_vue: false)
sign_in(user)
visit(details_group_path(group))
within '[data-testid=group_sort_by_dropdown]' do

View File

@ -40,7 +40,7 @@ describe('AppComponent', () => {
const store = new GroupsStore({ hideProjects: false });
const service = new GroupsService(mockEndpoint);
const createShallowComponent = ({ propsData = {}, provide = {} } = {}) => {
const createShallowComponent = ({ propsData = {} } = {}) => {
store.state.pageInfo = mockPageInfo;
wrapper = shallowMount(appComponent, {
propsData: {
@ -53,10 +53,6 @@ describe('AppComponent', () => {
mocks: {
$toast,
},
provide: {
renderEmptyState: false,
...provide,
},
});
vm = wrapper.vm;
};
@ -402,8 +398,7 @@ describe('AppComponent', () => {
({ action, groups, fromSearch, renderEmptyState, expected }) => {
it(expected ? 'renders empty state' : 'does not render empty state', async () => {
createShallowComponent({
propsData: { action },
provide: { renderEmptyState },
propsData: { action, renderEmptyState },
});
vm.updateGroups(groups, fromSearch);
@ -420,7 +415,6 @@ describe('AppComponent', () => {
it('renders legacy empty state', async () => {
createShallowComponent({
propsData: { action: 'subgroups_and_projects' },
provide: { renderEmptyState: false },
});
vm.updateGroups([], false);

View File

@ -0,0 +1,106 @@
import { GlTab } from '@gitlab/ui';
import { nextTick } from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import OverviewTabs from '~/groups/components/overview_tabs.vue';
import GroupsApp from '~/groups/components/app.vue';
import GroupsStore from '~/groups/store/groups_store';
import GroupsService from '~/groups/service/groups_service';
import {
ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
ACTIVE_TAB_SHARED,
ACTIVE_TAB_ARCHIVED,
} from '~/groups/constants';
import axios from '~/lib/utils/axios_utils';
describe('OverviewTabs', () => {
let wrapper;
const endpoints = {
subgroups_and_projects: '/groups/foobar/-/children.json',
shared: '/groups/foobar/-/shared_projects.json',
archived: '/groups/foobar/-/children.json?archived=only',
};
const createComponent = async () => {
wrapper = mountExtended(OverviewTabs, {
provide: {
endpoints,
},
});
await nextTick();
};
const findTabPanels = () => wrapper.findAllComponents(GlTab);
const findTab = (name) => wrapper.findByRole('tab', { name });
afterEach(() => {
wrapper.destroy();
});
beforeEach(async () => {
// eslint-disable-next-line no-new
new AxiosMockAdapter(axios);
await createComponent();
});
it('renders `Subgroups and projects` tab with `GroupsApp` component', async () => {
const tabPanel = findTabPanels().at(0);
expect(tabPanel.vm.$attrs).toMatchObject({
title: OverviewTabs.i18n.subgroupsAndProjects,
lazy: false,
});
expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
action: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
store: new GroupsStore({ showSchemaMarkup: true }),
service: new GroupsService(endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]),
hideProjects: false,
renderEmptyState: true,
});
});
it('renders `Shared projects` tab and renders `GroupsApp` component after clicking tab', async () => {
const tabPanel = findTabPanels().at(1);
expect(tabPanel.vm.$attrs).toMatchObject({
title: OverviewTabs.i18n.sharedProjects,
lazy: true,
});
await findTab(OverviewTabs.i18n.sharedProjects).trigger('click');
expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
action: ACTIVE_TAB_SHARED,
store: new GroupsStore(),
service: new GroupsService(endpoints[ACTIVE_TAB_SHARED]),
hideProjects: false,
renderEmptyState: false,
});
expect(tabPanel.vm.$attrs.lazy).toBe(false);
});
it('renders `Archived projects` tab and renders `GroupsApp` component after clicking tab', async () => {
const tabPanel = findTabPanels().at(2);
expect(tabPanel.vm.$attrs).toMatchObject({
title: OverviewTabs.i18n.archivedProjects,
lazy: true,
});
await findTab(OverviewTabs.i18n.archivedProjects).trigger('click');
expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
action: ACTIVE_TAB_ARCHIVED,
store: new GroupsStore(),
service: new GroupsService(endpoints[ACTIVE_TAB_ARCHIVED]),
hideProjects: false,
renderEmptyState: false,
});
expect(tabPanel.vm.$attrs.lazy).toBe(false);
});
});

View File

@ -1,111 +1,168 @@
import $ from 'jquery';
import Autosave from '~/autosave';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import IssuableForm from '~/issuable/issuable_form';
import setWindowLocation from 'helpers/set_window_location_helper';
describe('IssuableForm', () => {
let instance;
jest.mock('~/autosave');
const createIssuable = (form) => {
instance = new IssuableForm(form);
};
const createIssuable = (form) => {
return new IssuableForm(form);
};
describe('IssuableForm', () => {
let $form;
let instance;
beforeEach(() => {
setHTMLFixture(`
<form>
<input name="[title]" />
<textarea name="[description]"></textarea>
</form>
`);
createIssuable($('form'));
$form = $('form');
});
afterEach(() => {
resetHTMLFixture();
$form = null;
instance = null;
});
describe('initAutosave', () => {
it('creates autosave with the searchTerm included', () => {
setWindowLocation('https://gitlab.test/foo?bar=true');
const autosave = instance.initAutosave();
describe('autosave', () => {
let $title;
let $description;
expect(autosave.key.includes('bar=true')).toBe(true);
beforeEach(() => {
$title = $form.find('input[name*="[title]"]');
$description = $form.find('textarea[name*="[description]"]');
});
it("creates autosave fields without the searchTerm if it's an issue new form", () => {
setHTMLFixture(`
<form data-new-issue-path="/issues/new">
<input name="[title]" />
</form>
`);
createIssuable($('form'));
afterEach(() => {
$title = null;
$description = null;
});
setWindowLocation('https://gitlab.test/issues/new?bar=true');
describe('initAutosave', () => {
it('calls initAutosave', () => {
const initAutosave = jest.spyOn(IssuableForm.prototype, 'initAutosave');
createIssuable($form);
const autosave = instance.initAutosave();
expect(initAutosave).toHaveBeenCalledTimes(1);
});
expect(autosave.key.includes('bar=true')).toBe(false);
it('creates autosave with the searchTerm included', () => {
setWindowLocation('https://gitlab.test/foo?bar=true');
createIssuable($form);
expect(Autosave).toHaveBeenCalledWith(
$title,
['/foo', 'bar=true', 'title'],
'autosave//foo/bar=true=title',
);
expect(Autosave).toHaveBeenCalledWith(
$description,
['/foo', 'bar=true', 'description'],
'autosave//foo/bar=true=description',
);
});
it("creates autosave fields without the searchTerm if it's an issue new form", () => {
setWindowLocation('https://gitlab.test/issues/new?bar=true');
$form.attr('data-new-issue-path', '/issues/new');
createIssuable($form);
expect(Autosave).toHaveBeenCalledWith(
$title,
['/issues/new', '', 'title'],
'autosave//issues/new/bar=true=title',
);
expect(Autosave).toHaveBeenCalledWith(
$description,
['/issues/new', '', 'description'],
'autosave//issues/new/bar=true=description',
);
});
});
describe('resetAutosave', () => {
it('calls reset on title and description', () => {
instance = createIssuable($form);
instance.resetAutosave();
expect(instance.autosaveTitle.reset).toHaveBeenCalledTimes(1);
expect(instance.autosaveDescription.reset).toHaveBeenCalledTimes(1);
});
it('resets autosave when submit', () => {
const resetAutosave = jest.spyOn(IssuableForm.prototype, 'resetAutosave');
createIssuable($form);
$form.submit();
expect(resetAutosave).toHaveBeenCalledTimes(1);
});
it('resets autosave on elements with the .js-reset-autosave class', () => {
const resetAutosave = jest.spyOn(IssuableForm.prototype, 'resetAutosave');
$form.append('<a class="js-reset-autosave">Cancel</a>');
createIssuable($form);
$form.find('.js-reset-autosave').trigger('click');
expect(resetAutosave).toHaveBeenCalledTimes(1);
});
});
});
describe('resetAutosave', () => {
it('resets autosave on elements with the .js-reset-autosave class', () => {
setHTMLFixture(`
<form>
<input name="[title]" />
<textarea name="[description]"></textarea>
<a class="js-reset-autosave">Cancel</a>
</form>
`);
const $form = $('form');
const resetAutosave = jest.spyOn(IssuableForm.prototype, 'resetAutosave');
createIssuable($form);
$form.find('.js-reset-autosave').trigger('click');
expect(resetAutosave).toHaveBeenCalled();
describe('wip', () => {
beforeEach(() => {
instance = createIssuable($form);
});
});
describe('removeWip', () => {
it.each`
prefix
${'draFT: '}
${' [DRaft] '}
${'drAft:'}
${'[draFT]'}
${'(draft) '}
${' (DrafT)'}
${'draft: [draft] (draft)'}
`('removes "$prefix" from the beginning of the title', ({ prefix }) => {
instance.titleField.val(`${prefix}The Issuable's Title Value`);
describe('removeWip', () => {
it.each`
prefix
${'draFT: '}
${' [DRaft] '}
${'drAft:'}
${'[draFT]'}
${'(draft) '}
${' (DrafT)'}
${'draft: [draft] (draft)'}
`('removes "$prefix" from the beginning of the title', ({ prefix }) => {
instance.titleField.val(`${prefix}The Issuable's Title Value`);
instance.removeWip();
instance.removeWip();
expect(instance.titleField.val()).toBe("The Issuable's Title Value");
expect(instance.titleField.val()).toBe("The Issuable's Title Value");
});
});
});
describe('addWip', () => {
it("properly adds the work in progress prefix to the Issuable's title", () => {
instance.titleField.val("The Issuable's Title Value");
describe('addWip', () => {
it("properly adds the work in progress prefix to the Issuable's title", () => {
instance.titleField.val("The Issuable's Title Value");
instance.addWip();
instance.addWip();
expect(instance.titleField.val()).toBe("Draft: The Issuable's Title Value");
expect(instance.titleField.val()).toBe("Draft: The Issuable's Title Value");
});
});
});
describe('workInProgress', () => {
it.each`
title | expected
${'draFT: something is happening'} | ${true}
${'draft something is happening'} | ${false}
${'something is happening to drafts'} | ${false}
${'something is happening'} | ${false}
`('returns $expected with "$title"', ({ title, expected }) => {
instance.titleField.val(title);
describe('workInProgress', () => {
it.each`
title | expected
${'draFT: something is happening'} | ${true}
${'draft something is happening'} | ${false}
${'something is happening to drafts'} | ${false}
${'something is happening'} | ${false}
`('returns $expected with "$title"', ({ title, expected }) => {
instance.titleField.val(title);
expect(instance.workInProgress()).toBe(expected);
expect(instance.workInProgress()).toBe(expected);
});
});
});
});

View File

@ -520,6 +520,29 @@ RSpec.describe GroupsHelper do
end
end
describe '#group_overview_tabs_app_data' do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
before do
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:can?).with(user, :create_subgroup, group) { true }
allow(helper).to receive(:can?).with(user, :create_projects, group) { true }
end
it 'returns expected hash' do
expect(helper.group_overview_tabs_app_data(group)).to match(
{
subgroups_and_projects_endpoint: including("/groups/#{group.path}/-/children.json"),
shared_projects_endpoint: including("/groups/#{group.path}/-/shared_projects.json"),
archived_projects_endpoint: including("/groups/#{group.path}/-/children.json?archived=only"),
current_group_visibility: group.visibility
}.merge(helper.group_overview_tabs_app_data(group))
)
end
end
describe "#enabled_git_access_protocol_options_for_group" do
subject { helper.enabled_git_access_protocol_options_for_group }

View File

@ -289,7 +289,6 @@ RSpec.describe ApplicationWorker do
perform_action
expect(worker.jobs.count).to eq args.count
expect(worker.jobs).to all(include('enqueued_at'))
end
end
@ -302,7 +301,6 @@ RSpec.describe ApplicationWorker do
perform_action
expect(worker.jobs.count).to eq args.count
expect(worker.jobs).to all(include('enqueued_at'))
end
end