Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-01-25 15:12:32 +00:00
parent 868c8c35fb
commit 7d8d5a3dab
240 changed files with 3338 additions and 2529 deletions

View File

@ -1,18 +1,31 @@
<script>
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import {
GlSafeHtmlDirective as SafeHtml,
GlModal,
GlModalDirective,
GlPopover,
GlButton,
} from '@gitlab/ui';
import $ from 'jquery';
import createFlash from '~/flash';
import { __, sprintf } from '~/locale';
import TaskList from '~/task_list';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import animateMixin from '../mixins/animate';
export default {
directives: {
SafeHtml,
GlModal: GlModalDirective,
},
mixins: [animateMixin],
components: {
GlModal,
GlPopover,
CreateWorkItem,
GlButton,
},
mixins: [animateMixin, glFeatureFlagMixin()],
props: {
canUpdate: {
type: Boolean,
@ -53,8 +66,15 @@ export default {
preAnimation: false,
pulseAnimation: false,
initialUpdate: true,
taskButtons: [],
activeTask: {},
};
},
computed: {
workItemsEnabled() {
return this.glFeatures.workItems;
},
},
watch: {
descriptionHtml(newDescription, oldDescription) {
if (!this.initialUpdate && newDescription !== oldDescription) {
@ -74,6 +94,10 @@ export default {
mounted() {
this.renderGFM();
this.updateTaskStatusText();
if (this.workItemsEnabled) {
this.renderTaskActions();
}
},
methods: {
renderGFM() {
@ -132,6 +156,55 @@ export default {
$tasksShort.text('');
}
},
renderTaskActions() {
const taskListFields = this.$el.querySelectorAll('.task-list-item');
taskListFields.forEach((item, index) => {
const button = document.createElement('button');
button.classList.add(
'btn',
'btn-default',
'btn-md',
'gl-button',
'btn-default-tertiary',
'gl-left-0',
'gl-p-0!',
'gl-top-2',
'gl-absolute',
'js-add-task',
);
button.id = `js-task-button-${index}`;
this.taskButtons.push(button.id);
button.innerHTML =
'<svg data-testid="ellipsis_v-icon" role="img" aria-hidden="true" class="dropdown-icon gl-icon s14"><use href="/assets/icons-7f1680a3670112fe4c8ef57b9dfb93f0f61b43a2a479d7abd6c83bcb724b9201.svg#ellipsis_v"></use></svg>';
item.prepend(button);
});
},
openCreateTaskModal(id) {
this.activeTask = { id, title: this.$el.querySelector(`#${id}`).parentElement.innerText };
this.$refs.modal.show();
},
closeCreateTaskModal() {
this.$refs.modal.hide();
},
handleCreateTask(title) {
const listItem = this.$el.querySelector(`#${this.activeTask.id}`).parentElement;
const taskBadge = document.createElement('span');
taskBadge.innerHTML = `
<svg data-testid="issue-open-m-icon" role="img" aria-hidden="true" class="gl-icon gl-fill-green-500 s12">
<use href="/assets/icons-7f1680a3670112fe4c8ef57b9dfb93f0f61b43a2a479d7abd6c83bcb724b9201.svg#issue-open-m"></use>
</svg>
<span class="badge badge-info badge-pill gl-badge sm gl-mr-1">
${__('Task')}
</span>
<a href="#">${title}</a>
`;
listItem.insertBefore(taskBadge, listItem.lastChild);
listItem.removeChild(listItem.lastChild);
this.closeCreateTaskModal();
},
focusButton() {
this.$refs.convertButton[0].$el.focus();
},
},
safeHtmlConfig: { ADD_TAGS: ['gl-emoji', 'copy-code'] },
};
@ -142,12 +215,14 @@ export default {
v-if="descriptionHtml"
:class="{
'js-task-list-container': canUpdate,
'work-items-enabled': workItemsEnabled,
}"
class="description"
>
<div
ref="gfm-content"
v-safe-html:[$options.safeHtmlConfig]="descriptionHtml"
data-testid="gfm-content"
:class="{
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation,
@ -157,13 +232,46 @@ export default {
<!-- eslint-disable vue/no-mutating-props -->
<textarea
v-if="descriptionText"
ref="textarea"
v-model="descriptionText"
:data-update-url="updateUrl"
class="hidden js-task-list-field"
dir="auto"
data-testid="textarea"
>
</textarea>
<!-- eslint-enable vue/no-mutating-props -->
<gl-modal
ref="modal"
modal-id="create-task-modal"
:title="s__('WorkItem|New Task')"
hide-footer
body-class="gl-py-0!"
>
<create-work-item
:is-modal="true"
:initial-title="activeTask.title"
@closeModal="closeCreateTaskModal"
@onCreate="handleCreateTask"
/>
</gl-modal>
<template v-if="workItemsEnabled">
<gl-popover
v-for="item in taskButtons"
:key="item"
:target="item"
placement="top"
triggers="focus"
@shown="focusButton"
>
<gl-button
ref="convertButton"
variant="link"
data-testid="convert-to-task"
class="gl-text-gray-900! gl-text-decoration-none! gl-outline-0!"
@click="openCreateTaskModal(item)"
>{{ s__('WorkItem|Convert to work item') }}</gl-button
>
</gl-popover>
</template>
</div>
</template>

View File

@ -2,6 +2,7 @@ import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import produce from 'immer';
import VueApollo from 'vue-apollo';
import getIssueStateQuery from '~/issues/show/queries/get_issue_state.query.graphql';
import { resolvers as workItemResolvers } from '~/work_items/graphql/resolvers';
import createDefaultClient from '~/lib/graphql';
import introspectionQueryResultData from './fragmentTypes.json';
@ -10,6 +11,7 @@ const fragmentMatcher = new IntrospectionFragmentMatcher({
});
const resolvers = {
...workItemResolvers,
Mutation: {
updateIssueState: (_, { issueType = undefined, isDirty = false }, { cache }) => {
const sourceData = cache.readQuery({ query: getIssueStateQuery });
@ -18,6 +20,7 @@ const resolvers = {
});
cache.writeQuery({ query: getIssueStateQuery, data });
},
...workItemResolvers.Mutation,
},
};

View File

@ -10,9 +10,21 @@ export default {
GlAlert,
ItemTitle,
},
props: {
isModal: {
type: Boolean,
required: false,
default: false,
},
initialTitle: {
type: String,
required: false,
default: '',
},
},
data() {
return {
title: '',
title: this.initialTitle,
error: false,
};
},
@ -35,7 +47,11 @@ export default {
},
},
} = response;
this.$router.push({ name: 'workItem', params: { id } });
if (!this.isModal) {
this.$router.push({ name: 'workItem', params: { id } });
} else {
this.$emit('onCreate', this.title);
}
} catch {
this.error = true;
}
@ -43,6 +59,13 @@ export default {
handleTitleInput(title) {
this.title = title;
},
handleCancelClick() {
if (!this.isModal) {
this.$router.go(-1);
return;
}
this.$emit('closeModal');
},
},
};
</script>
@ -52,18 +75,27 @@ export default {
<gl-alert v-if="error" variant="danger" @dismiss="error = false">{{
__('Something went wrong when creating a work item. Please try again')
}}</gl-alert>
<item-title data-testid="title-input" @title-input="handleTitleInput" />
<div class="gl-bg-gray-10 gl-py-5 gl-px-6">
<item-title :initial-title="title" data-testid="title-input" @title-input="handleTitleInput" />
<div
class="gl-bg-gray-10 gl-py-5 gl-px-6"
:class="{ 'gl-display-flex gl-justify-content-end': isModal }"
>
<gl-button
variant="confirm"
:disabled="title.length === 0"
class="gl-mr-3"
:class="{ 'gl-mr-3': !isModal }"
data-testid="create-button"
type="submit"
>
{{ __('Create') }}
{{ s__('WorkItem|Create work item') }}
</gl-button>
<gl-button type="button" data-testid="cancel-button" @click="$router.go(-1)">
<gl-button
type="button"
data-testid="cancel-button"
class="gl-order-n1"
:class="{ 'gl-mr-3': isModal }"
@click="handleCancelClick"
>
{{ __('Cancel') }}
</gl-button>
</div>

View File

@ -305,3 +305,32 @@ ul.related-merge-requests > li gl-emoji {
.issuable-header-slide-leave-to {
transform: translateY(-100%);
}
.description.work-items-enabled {
ul.task-list {
> li.task-list-item {
padding-inline-start: 2.25rem;
.js-add-task {
svg {
visibility: hidden;
}
&:focus svg {
visibility: visible;
}
}
> input.task-list-item-checkbox {
left: 0.875rem;
}
&:hover,
&:focus-within {
.js-add-task svg {
visibility: visible;
}
}
}
}
}

View File

@ -54,6 +54,7 @@ class Projects::IssuesController < Projects::ApplicationController
push_frontend_feature_flag(:issue_assignees_widget, @project, default_enabled: :yaml)
push_frontend_feature_flag(:paginated_issue_discussions, @project, default_enabled: :yaml)
push_frontend_feature_flag(:fix_comment_scroll, @project, default_enabled: :yaml)
push_frontend_feature_flag(:work_items, project, default_enabled: :yaml)
end
around_action :allow_gitaly_ref_name_caching, only: [:discussions]

View File

@ -35,6 +35,7 @@ class RegistrationsController < Devise::RegistrationsController
persist_accepted_terms_if_required(new_user)
set_role_required(new_user)
track_experiment_event(new_user)
if pending_approval?
NotificationService.new.new_instance_access_request(new_user)
@ -223,6 +224,14 @@ class RegistrationsController < Devise::RegistrationsController
def context_user
current_user
end
def track_experiment_event(new_user)
# Track signed up event to relate it with click "Sign up" button events from
# the experimental logged out header with marketing links. This allows us to
# have a funnel of visitors clicking on the header and those visitors
# signing up and becoming users
experiment(:logged_out_marketing_header, actor: new_user).track(:signed_up) if new_user.persisted?
end
end
RegistrationsController.prepend_mod_with('RegistrationsController')

View File

@ -7,12 +7,20 @@ module Types
value 'ACTIVE',
description: 'Runner that is not paused.',
deprecated: { reason: 'Use CiRunnerType.active instead', milestone: '14.6' },
deprecated: {
reason: :renamed,
replacement: 'CiRunner.paused',
milestone: '14.6'
},
value: :active
value 'PAUSED',
description: 'Runner that is paused.',
deprecated: { reason: 'Use CiRunnerType.active instead', milestone: '14.6' },
deprecated: {
reason: :renamed,
replacement: 'CiRunner.paused',
milestone: '14.6'
},
value: :paused
value 'ONLINE',

View File

@ -29,7 +29,10 @@ module Types
field :access_level, ::Types::Ci::RunnerAccessLevelEnum, null: false,
description: 'Access level of the runner.'
field :active, GraphQL::Types::Boolean, null: false,
description: 'Indicates the runner is allowed to receive jobs.'
description: 'Indicates the runner is allowed to receive jobs.',
deprecated: { reason: 'Use paused', milestone: '14.8' }
field :paused, GraphQL::Types::Boolean, null: false,
description: 'Indicates the runner is paused and not available to run jobs.'
field :status,
Types::Ci::RunnerStatusEnum,
null: false,

View File

@ -6,7 +6,7 @@ module Ci
class NamespaceMirror < ApplicationRecord
belongs_to :namespace
scope :contains_namespace, -> (id) do
scope :by_group_and_descendants, -> (id) do
where('traversal_ids @> ARRAY[?]::int[]', id)
end
@ -32,7 +32,7 @@ module Ci
private
def sync_children_namespaces!(namespace_id, traversal_ids)
contains_namespace(namespace_id)
by_group_and_descendants(namespace_id)
.where.not(namespace_id: namespace_id)
.update_all(
"traversal_ids = ARRAY[#{sanitize_sql(traversal_ids.join(','))}]::int[] || traversal_ids[array_position(traversal_ids, #{sanitize_sql(namespace_id)}) + 1:]"

View File

@ -1163,7 +1163,11 @@ module Ci
end
def merge_request?
merge_request_id.present?
if Feature.enabled?(:ci_pipeline_merge_request_presence_check, default_enabled: :yaml)
merge_request_id.present? && merge_request
else
merge_request_id.present?
end
end
def external_pull_request?

View File

@ -101,7 +101,7 @@ module Ci
}
scope :belonging_to_group_or_project_descendants, -> (group_id) {
group_ids = Ci::NamespaceMirror.contains_namespace(group_id).select(:namespace_id)
group_ids = Ci::NamespaceMirror.by_group_and_descendants(group_id).select(:namespace_id)
project_ids = Ci::ProjectMirror.by_namespace_id(group_ids).select(:project_id)
group_runners = joins(:runner_namespaces).where(ci_runner_namespaces: { namespace_id: group_ids })

View File

@ -1519,6 +1519,10 @@ class Project < ApplicationRecord
group || namespace.try(:owner)
end
def owners
Array.wrap(owner)
end
def first_owner
obj = owner

View File

@ -15,5 +15,9 @@ module Ci
def executor_name
Ci::Runner::EXECUTOR_TYPE_TO_NAMES[executor_type&.to_sym]
end
def paused
!active
end
end
end

View File

@ -61,6 +61,8 @@ module Groups
delay = Namespaces::InviteTeamEmailService::DELIVERY_DELAY_IN_MINUTES
Namespaces::InviteTeamEmailWorker.perform_in(delay, group.id, current_user.id)
end
track_experiment_event
end
def remove_unallowed_params
@ -112,6 +114,15 @@ module Groups
@group.shared_runners_enabled = @group.parent.shared_runners_enabled
@group.allow_descendants_override_disabled_shared_runners = @group.parent.allow_descendants_override_disabled_shared_runners
end
def track_experiment_event
return unless group.persisted?
# Track namespace created events to relate them with signed up events for
# the same experiment. This will let us associate created namespaces to
# users that signed up from the experimental logged out header.
experiment(:logged_out_marketing_header, actor: current_user).track(:namespace_created, namespace: group)
end
end
end

View File

@ -16,6 +16,7 @@
= render "shared/service_ping_consent"
= render_two_factor_auth_recovery_settings_check
= render_if_exists "layouts/header/ee_subscribable_banner"
= render_if_exists "layouts/header/seats_count_alert"
= render_if_exists "shared/namespace_storage_limit_alert"
= render_if_exists "shared/namespace_user_cap_reached_alert"
= render_if_exists "shared/new_user_signups_cap_reached_alert"

View File

@ -139,15 +139,15 @@
- experiment(:logged_out_marketing_header, actor: nil) do |e|
- e.candidate do
%li.nav-item.gl-display-none.gl-sm-display-block
= link_to _('Sign up now'), new_user_registration_path, class: 'gl-button btn btn-default btn-sign-in'
= link_to _('Sign up now'), new_user_registration_path, class: 'gl-button btn btn-default btn-sign-in', data: { track_action: 'click_button', track_experiment: e.name, track_label: 'sign_up_now' }
%li.nav-item.gl-display-none.gl-sm-display-block
= link_to _('Login'), new_session_path(:user, redirect_to_referer: 'yes')
= render 'layouts/header/sign_in_register_button', class: 'gl-sm-display-none'
- e.try(:trial_focused) do
%li.nav-item.gl-display-none.gl-sm-display-block
= link_to _('Get a free trial'), 'https://about.gitlab.com/free-trial/', class: 'gl-button btn btn-default btn-sign-in'
= link_to _('Get a free trial'), 'https://about.gitlab.com/free-trial/', class: 'gl-button btn btn-default btn-sign-in', data: { track_action: 'click_button', track_experiment: e.name, track_label: 'get_a_free_trial' }
%li.nav-item.gl-display-none.gl-sm-display-block
= link_to _('Sign up'), new_user_registration_path
= link_to _('Sign up'), new_user_registration_path, data: { track_action: 'click_button', track_experiment: e.name, track_label: 'sign_up' }
%li.nav-item.gl-display-none.gl-sm-display-block
= link_to _('Login'), new_session_path(:user, redirect_to_referer: 'yes')
= render 'layouts/header/sign_in_register_button', class: 'gl-sm-display-none'

View File

@ -0,0 +1,8 @@
---
name: ci_pipeline_merge_request_presence_check
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78574
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350734
milestone: '14.8'
type: development
group: group::pipeline execution
default_enabled: true

View File

@ -12,7 +12,6 @@ milestone: "14.5"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73076
time_frame: 7d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
performance_indicator_type: []
distribution:

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
class BackfillProjectNamespacesForGroup < Gitlab::Database::Migration[1.0]
MIGRATION = 'ProjectNamespaces::BackfillProjectNamespaces'
DELAY_INTERVAL = 2.minutes
GROUP_ID = 9970 # picking gitlab-org group.
disable_ddl_transaction!
def up
return unless Gitlab.com? || Gitlab.staging?
projects_table = ::Gitlab::BackgroundMigration::ProjectNamespaces::Models::Project.arel_table
hierarchy_cte_sql = Arel.sql(::Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNamespaces.hierarchy_cte(GROUP_ID))
group_projects = ::Gitlab::BackgroundMigration::ProjectNamespaces::Models::Project.where(projects_table[:namespace_id].in(hierarchy_cte_sql))
min_id = group_projects.minimum(:id)
max_id = group_projects.maximum(:id)
return if min_id.blank? || max_id.blank?
queue_batched_background_migration(
MIGRATION,
:projects,
:id,
GROUP_ID,
'up',
job_interval: DELAY_INTERVAL,
batch_min_value: min_id,
batch_max_value: max_id,
sub_batch_size: 25,
batch_class_name: 'BackfillProjectNamespacePerGroupBatchingStrategy'
)
end
def down
return unless Gitlab.com? || Gitlab.staging?
Gitlab::Database::BackgroundMigration::BatchedMigration
.for_configuration(MIGRATION, :projects, :id, [GROUP_ID, 'up']).delete_all
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class RemoveMergeRequestsCiPipelinesMergeRequestIdFk < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
return unless foreign_key_exists?(:ci_pipelines, :merge_requests, name: "fk_a23be95014")
with_lock_retries do
execute('LOCK merge_requests, ci_pipelines IN ACCESS EXCLUSIVE MODE') if transaction_open?
remove_foreign_key_if_exists(:ci_pipelines, :merge_requests, name: "fk_a23be95014")
end
end
def down
add_concurrent_foreign_key(:ci_pipelines, :merge_requests, name: "fk_a23be95014", column: :merge_request_id, target_column: :id, on_delete: :cascade)
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class RemoveProjectsCiVariablesProjectIdFk < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
return unless foreign_key_exists?(:ci_variables, :projects, name: "fk_ada5eb64b3")
with_lock_retries do
execute('LOCK projects, ci_variables IN ACCESS EXCLUSIVE MODE') if transaction_open?
remove_foreign_key_if_exists(:ci_variables, :projects, name: "fk_ada5eb64b3")
end
end
def down
add_concurrent_foreign_key(:ci_variables, :projects, name: "fk_ada5eb64b3", column: :project_id, target_column: :id, on_delete: :cascade)
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class RemoveProjectsCiSourcesPipelinesProjectIdFk < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
return unless foreign_key_exists?(:ci_sources_pipelines, :projects, name: "fk_1e53c97c0a")
with_lock_retries do
execute('LOCK projects, ci_sources_pipelines IN ACCESS EXCLUSIVE MODE') if transaction_open?
remove_foreign_key_if_exists(:ci_sources_pipelines, :projects, name: "fk_1e53c97c0a")
end
end
def down
add_concurrent_foreign_key(:ci_sources_pipelines, :projects, name: "fk_1e53c97c0a", column: :project_id, target_column: :id, on_delete: :cascade)
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class RemoveProjectsCiRefsProjectIdFk < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
return unless foreign_key_exists?(:ci_refs, :projects, name: "fk_rails_4249db8cc3")
with_lock_retries do
execute('LOCK projects, ci_refs IN ACCESS EXCLUSIVE MODE') if transaction_open?
remove_foreign_key_if_exists(:ci_refs, :projects, name: "fk_rails_4249db8cc3")
end
end
def down
add_concurrent_foreign_key(:ci_refs, :projects, name: "fk_rails_4249db8cc3", column: :project_id, target_column: :id, on_delete: :cascade)
end
end

View File

@ -0,0 +1 @@
d2d270a335b3a2441a20673bf19d47553f607533d4503e3a01bc3d6d108bcdb3

View File

@ -0,0 +1 @@
7865f26c43c79681f37ceb6e4fecf6153282856907ddfd8211d6d1d57d1fb7f3

View File

@ -0,0 +1 @@
a1681c1c619db7f4e7e5a760cee7d06a931aa1f02dccfce46be81d75a03ce8ac

View File

@ -0,0 +1 @@
90115936ede32bbf9a299582409cb0686e8072c204c4f91364dfb13195155929

View File

@ -0,0 +1 @@
af60c6df0fb178a4820ea8cb40b402178da7fb4b6ebeabb8739dc45b96225f89

View File

@ -29240,9 +29240,6 @@ ALTER TABLE ONLY agent_project_authorizations
ALTER TABLE ONLY vulnerabilities
ADD CONSTRAINT fk_1d37cddf91 FOREIGN KEY (epic_id) REFERENCES epics(id) ON DELETE SET NULL;
ALTER TABLE ONLY ci_sources_pipelines
ADD CONSTRAINT fk_1e53c97c0a FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY boards
ADD CONSTRAINT fk_1e9a074a35 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
@ -29657,9 +29654,6 @@ ALTER TABLE ONLY issues
ALTER TABLE ONLY ci_builds
ADD CONSTRAINT fk_a2141b1522 FOREIGN KEY (auto_canceled_by_id) REFERENCES ci_pipelines(id) ON DELETE SET NULL;
ALTER TABLE ONLY ci_pipelines
ADD CONSTRAINT fk_a23be95014 FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;
ALTER TABLE ONLY bulk_import_entities
ADD CONSTRAINT fk_a44ff95be5 FOREIGN KEY (parent_id) REFERENCES bulk_import_entities(id) ON DELETE CASCADE;
@ -29696,9 +29690,6 @@ ALTER TABLE ONLY member_tasks
ALTER TABLE ONLY merge_requests
ADD CONSTRAINT fk_ad525e1f87 FOREIGN KEY (merge_user_id) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE ONLY ci_variables
ADD CONSTRAINT fk_ada5eb64b3 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY merge_request_metrics
ADD CONSTRAINT fk_ae440388cc FOREIGN KEY (latest_closed_by_id) REFERENCES users(id) ON DELETE SET NULL;
@ -30419,9 +30410,6 @@ ALTER TABLE ONLY geo_node_namespace_links
ALTER TABLE ONLY epic_issues
ADD CONSTRAINT fk_rails_4209981af6 FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_refs
ADD CONSTRAINT fk_rails_4249db8cc3 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_resources
ADD CONSTRAINT fk_rails_430336af2d FOREIGN KEY (resource_group_id) REFERENCES ci_resource_groups(id) ON DELETE CASCADE;

View File

@ -9015,7 +9015,7 @@ Represents the total number of issues and their weights for a particular day.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="cirunneraccesslevel"></a>`accessLevel` | [`CiRunnerAccessLevel!`](#cirunneraccesslevel) | Access level of the runner. |
| <a id="cirunneractive"></a>`active` | [`Boolean!`](#boolean) | Indicates the runner is allowed to receive jobs. |
| <a id="cirunneractive"></a>`active` **{warning-solid}** | [`Boolean!`](#boolean) | **Deprecated** in 14.8. Use paused. |
| <a id="cirunneradminurl"></a>`adminUrl` | [`String`](#string) | Admin URL of the runner. Only available for administrators. |
| <a id="cirunnercontactedat"></a>`contactedAt` | [`Time`](#time) | Timestamp of last contact from this runner. |
| <a id="cirunnercreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp of creation of this runner. |
@ -9028,6 +9028,7 @@ Represents the total number of issues and their weights for a particular day.
| <a id="cirunnerjobcount"></a>`jobCount` | [`Int`](#int) | Number of jobs processed by the runner (limited to 1000, plus one to indicate that more items exist). |
| <a id="cirunnerlocked"></a>`locked` | [`Boolean`](#boolean) | Indicates the runner is locked. |
| <a id="cirunnermaximumtimeout"></a>`maximumTimeout` | [`Int`](#int) | Maximum timeout (in seconds) for jobs processed by the runner. |
| <a id="cirunnerpaused"></a>`paused` | [`Boolean!`](#boolean) | Indicates the runner is paused and not available to run jobs. |
| <a id="cirunnerprivateprojectsminutescostfactor"></a>`privateProjectsMinutesCostFactor` | [`Float`](#float) | Private projects' "minutes cost factor" associated with the runner (GitLab.com only). |
| <a id="cirunnerprojectcount"></a>`projectCount` | [`Int`](#int) | Number of projects that the runner is associated with. |
| <a id="cirunnerprojects"></a>`projects` | [`ProjectConnection`](#projectconnection) | Projects the runner is associated with. For project runners only. (see [Connections](#connections)) |
@ -16491,12 +16492,12 @@ Values for sorting runners.
| Value | Description |
| ----- | ----------- |
| <a id="cirunnerstatusactive"></a>`ACTIVE` **{warning-solid}** | **Deprecated** in 14.6. Use CiRunnerType.active instead. |
| <a id="cirunnerstatusactive"></a>`ACTIVE` **{warning-solid}** | **Deprecated** in 14.6. This was renamed. Use: [`CiRunner.paused`](#cirunnerpaused). |
| <a id="cirunnerstatusnever_contacted"></a>`NEVER_CONTACTED` | Runner that has never contacted this instance. Set legacyMode to null to utilize this value. Will replace NOT_CONNECTED starting in 15.0. |
| <a id="cirunnerstatusnot_connected"></a>`NOT_CONNECTED` **{warning-solid}** | **Deprecated** in 14.6. Use NEVER_CONTACTED instead. NEVER_CONTACTED will have a slightly different scope starting in 15.0, with STALE being returned instead after 3 months of no contact. |
| <a id="cirunnerstatusoffline"></a>`OFFLINE` **{warning-solid}** | **Deprecated** in 14.6. This field will have a slightly different scope starting in 15.0, with STALE being returned after a certain period offline. |
| <a id="cirunnerstatusonline"></a>`ONLINE` | Runner that contacted this instance within the last 2 hours. |
| <a id="cirunnerstatuspaused"></a>`PAUSED` **{warning-solid}** | **Deprecated** in 14.6. Use CiRunnerType.active instead. |
| <a id="cirunnerstatuspaused"></a>`PAUSED` **{warning-solid}** | **Deprecated** in 14.6. This was renamed. Use: [`CiRunner.paused`](#cirunnerpaused). |
| <a id="cirunnerstatusstale"></a>`STALE` | Runner that has not contacted this instance within the last 3 months. Only available if legacyMode is null. Will be a possible return value starting in 15.0. |
### `CiRunnerType`

View File

@ -370,6 +370,7 @@ listed in the descriptions of the relevant settings.
| `push_event_hooks_limit` | integer | no | Number of changes (branches or tags) in a single push to determine whether webhooks and services fire or not. Webhooks and services aren't submitted if it surpasses that value. |
| `rate_limiting_response_text` | string | no | When rate limiting is enabled via the `throttle_*` settings, send this plain text response when a rate limit is exceeded. 'Retry later' is sent if this is blank. |
| `raw_blob_request_limit` | integer | no | Max number of requests per minute for each raw path. Default: 300. To disable throttling set to 0.|
| `user_email_lookup_limit` | integer | no | Max number of requests per minute for email lookup. Default: 60. To disable throttling set to 0.|
| `recaptcha_enabled` | boolean | no | (**If enabled, requires:** `recaptcha_private_key` and `recaptcha_site_key`) Enable reCAPTCHA. |
| `recaptcha_private_key` | string | required by: `recaptcha_enabled` | Private key for reCAPTCHA. |
| `recaptcha_site_key` | string | required by: `recaptcha_enabled` | Site key for reCAPTCHA. |

View File

@ -18,6 +18,7 @@ Before submitting a merge request with a new or updated CI/CD template, you must
- Name the template following the `*.gitlab-ci.yml` format.
- Use valid [`.gitlab-ci.yml` syntax](../../ci/yaml/index.md). Verify it's valid
with the [CI/CD lint tool](../../ci/lint.md).
- [Add template metrics](#add-metrics).
- Include [a changelog](../changelog.md) if the merge request introduces a user-facing change.
- Follow the [template review process](#contribute-cicd-template-merge-requests).
- (Optional but highly recommended) Test the template in an example GitLab project
@ -382,6 +383,77 @@ you must:
This information is important for users when [a stable template](#stable-version)
is updated in a major version GitLab release.
### Add metrics
Every CI/CD template must also have metrics defined to track their use.
To add a metric definition for a new template:
1. Install and start the [GitLab GDK](https://gitlab.com/gitlab-org/gitlab-development-kit#installation).
1. In the `gitlab` directory in your GDK, check out the branch that contains the new template.
1. [Add the template inclusion event](../service_ping/implement.md#add-new-events)
with this Rake task:
```shell
bin/rake gitlab:usage_data:generate_ci_template_events
```
The task adds a section like the following to [`lib/gitlab/usage_data_counters/known_events/ci_templates.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data_counters/known_events/ci_templates.yml):
```yaml
- name: p_ci_templates_my_template_name
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
```
1. Copy the value of `name` from the new YAML section, and add it to the weekly
and monthly CI/CD template total count metrics:
- [`config/metrics/counts_7d/20210216184557_ci_templates_total_unique_counts_weekly.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_7d/20210216184557_ci_templates_total_unique_counts_weekly.yml)
- [`config/metrics/counts_28d/20210216184559_ci_templates_total_unique_counts_monthly.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_28d/20210216184559_ci_templates_total_unique_counts_monthly.yml)
1. Use the same `name` as above as the last argument in the following command to
[add new metric definitions](../service_ping/metrics_dictionary.md#metrics-added-dynamic-to-service-ping-payload):
```shell
bundle exec rails generate gitlab:usage_metric_definition:redis_hll ci_templates <template_metric_event_name>
```
The output should look like:
```shell
$ bundle exec rails generate gitlab:usage_metric_definition:redis_hll ci_templates p_ci_templates_my_template_name
create config/metrics/counts_7d/20220120073740_p_ci_templates_my_template_name_weekly.yml
create config/metrics/counts_28d/20220120073746_p_ci_templates_my_template_name_monthly.yml
```
1. Edit both newly generated files as follows:
- `name:` and `performance_indicator_type:`: Delete (not needed).
- `introduced_by_url:`: The URL of the MR adding the template.
- `data_source:`: Set to `redis_hll`.
- All other fields that have no values: Set to empty strings (`''`).
- Add the following to the end of each file:
```yaml
options:
events:
- p_ci_templates_my_template_name
```
1. Commit and push the changes.
For example, these are the metrics configuration files for the
[5 Minute Production App template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/5-Minute-Production-App.gitlab-ci.yml):
- The template inclusion event: [`lib/gitlab/usage_data_counters/known_events/ci_templates.yml#L438-L441`](https://gitlab.com/gitlab-org/gitlab/-/blob/dcddbf83c29d1ad0839d55362c1b43888304f453/lib/gitlab/usage_data_counters/known_events/ci_templates.yml#L438-L441)
- The weekly and monthly metrics definitions:
- [`config/metrics/counts_7d/20210901223501_p_ci_templates_5_minute_production_app_weekly.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/1a6eceff3914f240864b2ca15ae2dc076ea67bf6/config/metrics/counts_7d/20210216184515_p_ci_templates_5_min_production_app_weekly.yml)
- [`config/metrics/counts_28d/20210901223505_p_ci_templates_5_minute_production_app_monthly.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_28d/20210216184517_p_ci_templates_5_min_production_app_monthly.yml)
- The metrics count totals:
- [`config/metrics/counts_7d/20210216184557_ci_templates_total_unique_counts_weekly.yml#L19`](https://gitlab.com/gitlab-org/gitlab/-/blob/4e01ef2b094763943348655ef77008aba7a052ae/config/metrics/counts_7d/20210216184557_ci_templates_total_unique_counts_weekly.yml#L19)
- [`config/metrics/counts_28d/20210216184559_ci_templates_total_unique_counts_monthly.yml#L19`](https://gitlab.com/gitlab-org/gitlab/-/blob/4e01ef2b094763943348655ef77008aba7a052ae/config/metrics/counts_28d/20210216184559_ci_templates_total_unique_counts_monthly.yml#L19)
## Security
A template could contain malicious code. For example, a template that contains the `export` shell command in a job

View File

@ -557,6 +557,10 @@ skip_db_write:
ServicePing::SubmitService.new(skip_db_write: true).execute
```
## Monitoring
Service Ping reporting process state is monitored with [internal SiSense dashboard](https://app.periscopedata.com/app/gitlab/968489/Product-Intelligence---Service-Ping-Health).
## Troubleshooting
### Cannot disable Service Ping using the configuration file

View File

@ -445,3 +445,15 @@ This happens because the value of `$CI_JOB_TOKEN` is only valid for the duration
As a workaround, use [http backend configuration variables](https://www.terraform.io/docs/language/settings/backends/http.html#configuration-variables) in your CI job,
which is what happens behind the scenes when following the
[Get started using GitLab CI](#get-started-using-gitlab-ci) instructions.
### Error: "address": required field is not set
By default, we set `TF_ADDRESS` to `${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${TF_STATE_NAME}`.
If you don't set `TF_STATE_NAME` or `TF_ADDRESS` in your job, the job fails with the error message
`Error: "address": required field is not set`.
To resolve this, ensure that either `TF_ADDRESS` or `TF_STATE_NAME` is accessible in the
job that returned the error:
1. Configure the [CI/CD environment scope](../../../ci/variables/#add-a-cicd-variable-to-a-project) for the job.
1. Set the job's [environment](../../../ci/yaml/#environment), matching the environment scope from the previous step.

View File

@ -140,3 +140,28 @@ its performance:
| Issues | `global_search_issues_tab` | When enabled, the global search includes issues as part of the search. |
| Merge Requests | `global_search_merge_requests_tab` | When enabled, the global search includes merge requests as part of the search. |
| Wiki | `global_search_wiki_tab` | When enabled, the global search includes wiki as part of the search. [Group wikis](../project/wiki/group.md) are not included. |
## Global Search validation
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/346263) in GitLab 14.6 [with a flag](../../administration/feature_flags.md) named `prevent_abusive_searches`. Disabled by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available,
ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `prevent_abusive_searches`.
The feature is not ready for production use.
To prevent abusive searches, such as searches that may result in a Distributed Denial of Service (DDoS), Global Search ignores, logs, and
doesn't return any results for searches considered abusive according to the following criteria, if `prevent_abusive_searches` feature flag is enabled:
- Searches with less than 2 characters.
- Searches with any term greater than 100 characters. URL search terms have a maximum of 200 characters.
- Searches with a stop word as the only term (ie: "the", "and", "if", etc.).
- Searches with a `group_id` or `project_id` parameter that is not completely numeric.
- Searches with a `repository_ref` or `project_ref` parameter that has special characters not allowed by [Git refname](https://git-scm.com/docs/git-check-ref-format).
- Searches with a `scope` that is unknown.
Regardless of the status of the `prevent_abusive_searches` feature flag, searches that don't
comply with the criteria described below aren't logged as abusive but are flagged with an error:
- Searches with more than 4096 characters.
- Searches with more than 64 terms.

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
module BatchingStrategies
# Batching class to use for back-filling project namespaces for a single group.
# Batches over the projects table and id column combination, scoped to a given group returning the MIN() and MAX()
# values for the next batch as an array.
#
# If no more batches exist in the table, returns nil.
class BackfillProjectNamespacePerGroupBatchingStrategy < PrimaryKeyBatchingStrategy
# Finds and returns the next batch in the table.
#
# table_name - The table to batch over
# column_name - The column to batch over
# batch_min_value - The minimum value which the next batch will start at
# batch_size - The size of the next batch
# job_arguments - The migration job arguments
def next_batch(table_name, column_name, batch_min_value:, batch_size:, job_arguments:)
next_batch_bounds = nil
model_class = ::Gitlab::BackgroundMigration::ProjectNamespaces::Models::Project
quoted_column_name = model_class.connection.quote_column_name(column_name)
projects_table = model_class.arel_table
hierarchy_cte_sql = Arel::Nodes::SqlLiteral.new(::Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNamespaces.hierarchy_cte(job_arguments.first))
relation = model_class.where(projects_table[:namespace_id].in(hierarchy_cte_sql)).where("#{quoted_column_name} >= ?", batch_min_value)
relation.each_batch(of: batch_size, column: column_name) do |batch| # rubocop:disable Lint/UnreachableLoop
next_batch_bounds = batch.pluck(Arel.sql("MIN(#{quoted_column_name}), MAX(#{quoted_column_name})")).first
break
end
next_batch_bounds
end
end
end
end
end

View File

@ -17,7 +17,8 @@ module Gitlab
# column_name - The column to batch over
# batch_min_value - The minimum value which the next batch will start at
# batch_size - The size of the next batch
def next_batch(table_name, column_name, batch_min_value:, batch_size:)
# job_arguments - The migration job arguments
def next_batch(table_name, column_name, batch_min_value:, batch_size:, job_arguments:)
model_class = define_batchable_model(table_name)
quoted_column_name = model_class.connection.quote_column_name(column_name)

View File

@ -5,19 +5,15 @@ module Gitlab
module ProjectNamespaces
# Back-fill project namespaces for projects that do not yet have a namespace.
#
# TODO: remove this comment when an actuall backfill migration is added.
#
# This is first being added without an actual migration as we need to initially test
# if backfilling project namespaces affects performance in any significant way.
# rubocop: disable Metrics/ClassLength
class BackfillProjectNamespaces
BATCH_SIZE = 100
DELETE_BATCH_SIZE = 10
SUB_BATCH_SIZE = 25
PROJECT_NAMESPACE_STI_NAME = 'Project'
IsolatedModels = ::Gitlab::BackgroundMigration::ProjectNamespaces::Models
def perform(start_id, end_id, namespace_id, migration_type = 'up')
def perform(start_id, end_id, migration_table_name, migration_column_name, sub_batch_size, pause_ms, namespace_id, migration_type = 'up')
@sub_batch_size = sub_batch_size || SUB_BATCH_SIZE
load_project_ids(start_id, end_id, namespace_id)
case migration_type
@ -34,10 +30,13 @@ module Gitlab
private
attr_accessor :project_ids
attr_accessor :project_ids, :sub_batch_size
def backfill_project_namespaces(namespace_id)
project_ids.each_slice(BATCH_SIZE) do |project_ids|
project_ids.each_slice(sub_batch_size) do |project_ids|
ActiveRecord::Base.connection.execute("select gin_clean_pending_list('index_namespaces_on_name_trigram')")
ActiveRecord::Base.connection.execute("select gin_clean_pending_list('index_namespaces_on_path_trigram')")
# We need to lock these project records for the period when we create project namespaces
# and link them to projects so that if a project is modified in the time between creating
# project namespaces `batch_insert_namespaces` and linking them to projects `batch_update_projects`
@ -45,18 +44,17 @@ module Gitlab
#
# see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72527#note_730679469
Project.transaction do
Project.where(id: project_ids).select(:id).lock!('FOR UPDATE')
Project.where(id: project_ids).select(:id).lock!('FOR UPDATE').load
batch_insert_namespaces(project_ids)
batch_update_projects(project_ids)
batch_update_project_namespaces_traversal_ids(project_ids)
end
batch_update_project_namespaces_traversal_ids(project_ids)
end
end
def cleanup_backfilled_project_namespaces(namespace_id)
project_ids.each_slice(BATCH_SIZE) do |project_ids|
project_ids.each_slice(sub_batch_size) do |project_ids|
# IMPORTANT: first nullify project_namespace_id in projects table to avoid removing projects when records
# from namespaces are deleted due to FK/triggers
nullify_project_namespaces_in_projects(project_ids)
@ -109,7 +107,10 @@ module Gitlab
end
def delete_project_namespace_records(project_ids)
project_ids.each_slice(DELETE_BATCH_SIZE) do |p_ids|
# keep the deletes a 10x smaller batch as deletes seem to be much more expensive
delete_batch_size = (sub_batch_size / 10).to_i + 1
project_ids.each_slice(delete_batch_size) do |p_ids|
IsolatedModels::Namespace.where(type: PROJECT_NAMESPACE_STI_NAME).where(tmp_project_id: p_ids).delete_all
end
end
@ -117,7 +118,7 @@ module Gitlab
def load_project_ids(start_id, end_id, namespace_id)
projects = IsolatedModels::Project.arel_table
relation = IsolatedModels::Project.where(projects[:id].between(start_id..end_id))
relation = relation.where(projects[:namespace_id].in(Arel::Nodes::SqlLiteral.new(hierarchy_cte(namespace_id)))) if namespace_id
relation = relation.where(projects[:namespace_id].in(Arel::Nodes::SqlLiteral.new(self.class.hierarchy_cte(namespace_id)))) if namespace_id
@project_ids = relation.pluck(:id)
end
@ -126,7 +127,7 @@ module Gitlab
::Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded('BackfillProjectNamespaces', arguments)
end
def hierarchy_cte(root_namespace_id)
def self.hierarchy_cte(root_namespace_id)
<<-SQL
WITH RECURSIVE "base_and_descendants" AS (
(

View File

@ -95,7 +95,8 @@ module Gitlab
active_migration.table_name,
active_migration.column_name,
batch_min_value: batch_min_value,
batch_size: active_migration.batch_size)
batch_size: active_migration.batch_size,
job_arguments: active_migration.job_arguments)
return if next_batch_bounds.nil?

View File

@ -27,6 +27,10 @@ clusters_applications_runners:
- table: ci_runners
column: runner_id
on_delete: async_nullify
ci_variables:
- table: projects
column: project_id
on_delete: async_delete
ci_job_token_project_scope_links:
- table: users
column: added_by_id
@ -83,6 +87,9 @@ ci_pipelines:
- table: users
column: user_id
on_delete: async_nullify
- table: merge_requests
column: merge_request_id
on_delete: async_delete
ci_project_mirrors:
- table: projects
column: project_id
@ -138,6 +145,10 @@ merge_trains:
- table: ci_pipelines
column: pipeline_id
on_delete: async_nullify
ci_refs:
- table: projects
column: project_id
on_delete: async_delete
ci_group_variables:
- table: namespaces
column: group_id
@ -166,6 +177,9 @@ ci_sources_pipelines:
- table: projects
column: source_project_id
on_delete: async_delete
- table: projects
column: project_id
on_delete: async_delete
ci_triggers:
- table: users
column: owner_id

View File

@ -42,10 +42,6 @@ module Gitlab
"#{self.subscriptions_url}/subscriptions"
end
def self.subscriptions_plans_url
Gitlab::Saas.about_pricing_url
end
def self.subscriptions_gitlab_plans_url
"#{self.subscriptions_url}/gitlab_plans"
end

View File

@ -632,6 +632,9 @@ msgstr ""
msgid "%{group_name} group members"
msgstr ""
msgid "%{group_name} is approaching the limit of available seats"
msgstr ""
msgid "%{group_name} uses group managed accounts. You need to create a new GitLab account which will be managed by %{group_name}."
msgstr ""
@ -5421,9 +5424,6 @@ msgstr ""
msgid "BillingPlans|@%{user_name} you are currently using the %{plan_name}."
msgstr ""
msgid "BillingPlans|Compare all plans"
msgstr ""
msgid "BillingPlans|Congratulations, your free trial is activated."
msgstr ""
@ -5460,9 +5460,6 @@ msgstr ""
msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
msgstr ""
msgid "BillingPlans|Upgrade to GitLab %{planNameForUpgrade}"
msgstr ""
msgid "BillingPlans|While GitLab is ending availability of the Bronze plan, you can still renew your Bronze subscription one additional time before %{eoa_bronze_plan_end_date}. We are also offering a limited time free upgrade to our Premium Plan (up to 25 users)! Learn more about the changes and offers in our %{announcement_link}."
msgstr ""
@ -15141,14 +15138,6 @@ msgstr ""
msgid "FeatureFlag|User List"
msgstr ""
msgid "FeatureHighlight|%{daysRemaining} day remaining to enjoy %{featureName}"
msgid_plural "FeatureHighlight|%{daysRemaining} days remaining to enjoy %{featureName}"
msgstr[0] ""
msgstr[1] ""
msgid "FeatureHighlight|Enjoying your GitLab %{planNameForTrial} trial? To continue using %{featureName} after your trial ends, upgrade to GitLab %{planNameForUpgrade}."
msgstr ""
msgid "Feb"
msgstr ""
@ -31127,9 +31116,6 @@ msgstr ""
msgid "SSL verification"
msgstr ""
msgid "SVG illustration"
msgstr ""
msgid "Satisfied"
msgstr ""
@ -35058,6 +35044,9 @@ msgstr ""
msgid "Target-Branch"
msgstr ""
msgid "Task"
msgstr ""
msgid "Task ID: %{elastic_task}"
msgstr ""
@ -35475,9 +35464,6 @@ msgstr ""
msgid "The \"%{group_path}\" group allows you to sign in with your Single Sign-On Account"
msgstr ""
msgid "The %{featureName} feature is part of your GitLab Ultimate trial."
msgstr ""
msgid "The %{link_start}true-up model%{link_end} allows having more users, and additional users will incur a retroactive charge on renewal."
msgstr ""
@ -36481,9 +36467,6 @@ msgstr ""
msgid "This epic does not exist or you don't have sufficient permission."
msgstr ""
msgid "This feature is part of your GitLab Ultimate trial."
msgstr ""
msgid "This feature requires local storage to be enabled"
msgstr ""
@ -39529,6 +39512,9 @@ msgstr[1] ""
msgid "View replaced file @ "
msgstr ""
msgid "View seat usage"
msgstr ""
msgid "View setting"
msgstr ""
@ -39865,6 +39851,9 @@ msgstr ""
msgid "Vulnerability|Scanner Provider"
msgstr ""
msgid "Vulnerability|Secure Code Warrior"
msgstr ""
msgid "Vulnerability|Security Audit"
msgstr ""
@ -39898,6 +39887,9 @@ msgstr ""
msgid "Vulnerability|Unmodified Response"
msgstr ""
msgid "Vulnerability|View training"
msgstr ""
msgid "WARNING:"
msgstr ""
@ -40617,6 +40609,15 @@ msgstr ""
msgid "Work in progress Limit"
msgstr ""
msgid "WorkItem|Convert to work item"
msgstr ""
msgid "WorkItem|Create work item"
msgstr ""
msgid "WorkItem|New Task"
msgstr ""
msgid "WorkItem|Work Items"
msgstr ""
@ -41588,6 +41589,9 @@ msgstr ""
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has %{remaining_seats_count} out of %{total_seats_count} seats remaining. Even if you reach the number of seats in your subscription, you can continue to add users, and GitLab will bill you for the overage."
msgstr ""
msgid "Your subscription is now expired. To renew, export your license usage file and email it to %{renewal_service_email}. A new license will be emailed to the email address registered in the %{customers_dot}. You can upload this license to your instance. To use Free tier, remove your current license."
msgstr ""

View File

@ -0,0 +1,405 @@
#!/usr/bin/env -S ENABLE_SPRING=0 bin/rails runner -e test
# This is helper script to swap foreign key to loose foreign key
# using DB schema
require 'optparse'
$options = {
milestone: "#{Gitlab.version_info.major}.#{Gitlab.version_info.minor}",
cross_schema: false,
dry_run: false,
branch: true,
rspec: true
}
OptionParser.new do |opts|
opts.banner = "Usage: #{$0} [options] <filters...>"
opts.on("-c", "--cross-schema", "Show only cross-schema foreign keys") do |v|
$options[:cross_schema] = v
end
opts.on("-n", "--dry-run", "Do not execute any commands (dry run)") do |v|
$options[:dry_run] = v
end
opts.on("-b", "--[no-]branch", "Create or not a new branch") do |v|
$options[:branch] = v
end
opts.on("-r", "--[no-]rspec", "Create or not a rspecs automatically") do |v|
$options[:rspec] = v
end
opts.on("-m", "--milestone MILESTONE", "Specify custom milestone (current: #{$options[:milestone]})") do |v|
$options[:milestone] = v
end
opts.on("-h", "--help", "Prints this help") do
puts opts
exit
end
end.parse!
unless system("git diff --quiet db/structure.sql")
raise "The db/structure.sql is changed. Reset branch or commit changes."
end
unless system("git diff --quiet")
raise "There are uncommitted changes. Commit to continue."
end
if Gitlab::Database.database_base_models.many?
raise 'Cannot run in multiple-databases mode. Use only `main:` in `config/database.yml`.'
end
puts "Re-creating current test database"
ActiveRecord::Tasks::DatabaseTasks.drop_current
ActiveRecord::Tasks::DatabaseTasks.create_current
ActiveRecord::Tasks::DatabaseTasks.load_schema_current
ActiveRecord::Tasks::DatabaseTasks.migrate
ActiveRecord::Migration.check_pending!
ActiveRecord::Base.connection_pool.disconnect!
puts
def exec_cmd(*args, fail: nil)
# output full command
if $options[:dry_run]
puts ">> #{args.shelljoin}"
return true
end
# truncate up-to 60 chars or first line
command = args.shelljoin
truncated_command = command.truncate([command.lines.first.length+3, 120].min)
puts ">> #{truncated_command}"
return true if system(*args)
raise fail if fail
puts "--------------------------------------------------"
puts "This command failed:"
puts ">> #{command}"
puts "--------------------------------------------------"
false
end
def has_lfk?(definition)
Gitlab::Database::LooseForeignKeys.definitions.any? do |lfk_definition|
lfk_definition.from_table == definition.from_table &&
lfk_definition.to_table == definition.to_table &&
lfk_definition.column == definition.column
end
end
def matching_filter?(definition, filters)
filters.all? do |filter|
definition.from_table.include?(filter) ||
definition.to_table.include?(filter) ||
definition.column.include?(filter)
end
end
def columns(*args)
puts("%5s | %7s | %40s | %20s | %30s | %15s " % args)
end
def add_definition_to_yaml(definition)
content = YAML.load_file(Rails.root.join('lib/gitlab/database/gitlab_loose_foreign_keys.yml'))
table_definitions = content[definition.from_table]
# insert new entry at random place to avoid conflicts
unless table_definitions
table_definitions = []
insert_idx = rand(content.count+1)
# insert at a given index in ordered hash
content = content.to_a
content.insert(insert_idx, [definition.from_table, table_definitions])
content = content.to_h
end
on_delete =
case definition.on_delete
when :cascade
'async_delete'
when :nullify
'async_nullify'
else
raise "Unsupported on_delete behavior: #{definition.on_delete}"
end
yaml_definition = {
"table" => definition.to_table,
"column" => definition.column,
"on_delete" => on_delete
}
# match and update by "table", "column"
if existing = table_definitions.pluck("table", "column").index([definition.to_table, definition.column])
puts "Updated existing definition from #{table_definitions[existing]} to #{yaml_definition}."
table_definitions[existing] = yaml_definition
else
puts "Add new definition for #{yaml_definition}."
table_definitions.append(yaml_definition)
end
# emulate existing formatting
File.write(
Rails.root.join('lib/gitlab/database/gitlab_loose_foreign_keys.yml'),
content.to_yaml.gsub(/^([- ] )/, ' \1')
)
exec_cmd("git", "add", "lib/gitlab/database/gitlab_loose_foreign_keys.yml")
end
def generate_migration(definition)
timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S")
# db/post_migrate/20220111221516_remove_projects_ci_pending_builds_fk.rb
migration_name = "db/post_migrate/#{timestamp}_remove_#{definition.to_table}_#{definition.from_table}_#{definition.column}_fk.rb"
puts "Writing #{migration_name}"
content = <<-EOF.strip_heredoc
# frozen_string_literal: true
class Remove#{definition.to_table.camelcase}#{definition.from_table.camelcase}#{definition.column.camelcase}Fk < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
return unless foreign_key_exists?(:#{definition.from_table}, :#{definition.to_table}, name: "#{definition.name}")
with_lock_retries do
execute('LOCK #{definition.to_table}, #{definition.from_table} IN ACCESS EXCLUSIVE MODE') if transaction_open?
remove_foreign_key_if_exists(:#{definition.from_table}, :#{definition.to_table}, name: "#{definition.name}")
end
end
def down
add_concurrent_foreign_key(:#{definition.from_table}, :#{definition.to_table}, name: "#{definition.name}", column: :#{definition.column}, target_column: :#{definition.primary_key}, on_delete: :#{definition.on_delete})
end
end
EOF
File.write(migration_name, content)
exec_cmd("git", "add", migration_name, fail: "Failed to add migration file.")
exec_cmd("bin/rails", "db:migrate", fail: "Failed to run db:migrate.")
exec_cmd("git", "add", "db/schema_migrations/#{timestamp}", "db/structure.sql", fail: "There are uncommitted changes. We should not have any.")
exec_cmd("git diff --exit-code --name-only", fail: "There are uncommitted changes. We should not have any.")
end
def class_by_table_name
@index_by_table_name ||= ActiveRecord::Base
.descendants
.reject(&:abstract_class)
.map(&:base_class)
.index_by(&:table_name)
end
def spec_from_clazz(clazz, definition)
%w[spec/models ee/spec/models].each do |specs_path|
path = File.join(specs_path, clazz.underscore + "_spec.rb")
return path if File.exist?(path)
end
raise "Cannot find specs for #{clazz} (#{definition.from_table})"
end
def add_test_to_specs(definition)
return unless $options[:rspec]
clazz = class_by_table_name[definition.from_table]
raise "Cannot map #{definition.from_table} to clazz" unless clazz
spec_path = spec_from_clazz(clazz, definition)
puts "Adding test to #{spec_path}..."
spec_test = <<-EOF.strip_heredoc.indent(2)
context 'loose foreign key on #{definition.from_table}.#{definition.column}' do
it_behaves_like 'cleanup by a loose foreign key' do
let!(:parent) { create(:#{definition.to_table.singularize}) }
let!(:model) { create(:#{definition.from_table.singularize}, #{definition.column.delete_suffix("_id").singularize}: parent) }
end
end
EOF
# append to end of file with empty line before
lines = File.readlines(spec_path)
insert_line = lines.count - 1
lines.insert(insert_line, "\n", *spec_test.lines)
File.write(spec_path, lines.join(""))
# find a matching line
test_lines = (1..lines.count).select do |line|
lines[line-1].include?("it_behaves_like 'cleanup by a loose foreign key' do")
end.join(":")
loop do
if system("bin/rspec", "#{spec_path}:#{test_lines}")
puts "Test seems fine?"
break
end
puts "--------------------------------------------------"
puts "Test failed:"
puts "Edit: vim #{spec_path} (lines #{test_lines})"
puts "Re-run: bin/rspec #{spec_path}:#{test_lines}"
puts "--------------------------------------------------"
puts "Running bash. To exit do 'Ctrl-D' to re-run, or do 'Ctrl-C' to break (and ignore failure)."
puts
unless exec_cmd("bash")
break
end
end
exec_cmd("git", "add", spec_path, fail: "There are uncommitted changes. We should not have any.")
end
def update_no_cross_db_foreign_keys_spec(definition)
from_column = "#{definition.from_table}.#{definition.column}"
spec_path = "spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb"
puts "Updating #{spec_path}..."
lines = File.readlines(spec_path)
updated = lines.reject { |line| line.strip == from_column }
if lines.count == updated.count
puts "Nothing changed."
return
end
File.write(spec_path, updated.join(""))
exec_cmd("git", "add", spec_path, fail: "Failed to add changes from #{spec_path}")
end
def commit_changes(definition)
branch_name = "remove-#{definition.to_table}_#{definition.from_table}_#{definition.column}-fk"
commit_title = "Swap FK #{definition.from_table} to #{definition.to_table} for LFK"
mr_title = "Swap FK #{definition.from_table}.#{definition.column} to #{definition.to_table} for LFK"
description = <<-EOF.strip_heredoc
Swaps FK for #{definition.from_table}.#{definition.column} to #{definition.to_table}
Changelog: changed
EOF
commit_message = "#{commit_title}\n\n#{description}"
existing_branch = %x[git rev-parse --abbrev-ref HEAD].strip
if $options[:branch]
unless exec_cmd("git", "checkout", "-b", branch_name)
raise "Failed to create branch: #{branch_name}"
end
end
unless exec_cmd("git", "commit", "-m", commit_message)
raise "Failed to commit changes."
end
if $options[:branch]
exec_cmd("git", "push", "origin", "-u", "HEAD",
"-o", "merge_request.create",
"-o", "merge_request.target=#{existing_branch}",
"-o", "merge_request.milestone=#{$options[:milestone]}",
"-o", "merge_request.title=#{mr_title}"
)
puts
puts "--------------------------------------------------"
puts "Put this as MR description:"
puts "--------------------------------------------------"
puts <<-EOF.strip_heredoc
## What does this MR do and why?
Per https://gitlab.com/groups/gitlab-org/-/epics/7249
As part of our CI "decomposition" efforts we need to remove all foreign keys that are cross-database (ie. between the planned \`main\` and \`ci\` databases). We are going to replace them all with ["loose foreign keys"](https://docs.gitlab.com/ee/development/database/loose_foreign_keys.html).
Related: <DETAIL>
## Validations
- **Best team to review (check off when reviewed):** TBD
- [ ] No way for user to access once parent is deleted. Please explain: <DETAIL>
- [ ] Possible to access once parent deleted but low user impact. Please explain: <DETAIL>
- [ ] Possible Sidekiq workers that may load directly and possibly lead to exceptions. Please explain: <DETAIL>
- [ ] Possible user impact to be evaluated or mitigated. Please explain: <DETAIL>
- [ ] Is this FK safe to be removed to avoid LOCKing problems? (Explanation: https://gitlab.com/groups/gitlab-org/-/epics/7249#note_819662046). Please explain: <DETAIL>
## MR acceptance checklist
This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.
* [ ] I have evaluated the [MR acceptance checklist](https://docs.gitlab.com/ee/development/code_review.html#acceptance-checklist) for this MR.
/label ~"ci-decomposition::phase4" ~"database::review pending" ~"devops::enablement" ~"group::sharding" ~"section::enablement" ~"sharding::active" ~"type::feature" ~"workflow::in dev" ~backend ~"ci-decomposition" ~database ~"Category:Sharding"
/milestone %"#{$options[:milestone]}"
/assign_reviewer @ahegyi
EOF
puts "--------------------------------------------------"
end
end
all_foreign_keys = ActiveRecord::Base.connection.tables.flat_map do |table|
ActiveRecord::Base.connection.foreign_keys(table)
end
# Show only cross-schema foreign keys
if $options[:cross_schema]
all_foreign_keys.select! do |definition|
Gitlab::Database::GitlabSchema.table_schema(definition.from_table) != Gitlab::Database::GitlabSchema.table_schema(definition.to_table)
end
end
if $options[:cross_schema]
puts "Showing cross-schema foreign keys (#{all_foreign_keys.count}):"
else
puts "Showing all foreign keys (#{all_foreign_keys.count}):"
puts "Did you meant `#{$0} --cross-schema ...`?"
end
columns("ID", "HAS_LFK", "FROM", "TO", "COLUMN", "ON_DELETE")
all_foreign_keys.each_with_index do |definition, idx|
columns(idx, has_lfk?(definition) ? 'Y' : 'N', definition.from_table, definition.to_table, definition.column, definition.on_delete)
end
puts
puts "To match FK write one or many filters to match against FROM/TO/COLUMN:"
puts "- #{$0} <filter(s)...>"
puts "- #{$0} ci_job_artifacts project_id"
puts "- #{$0} dast_site_profiles_pipelines"
puts
return if ARGV.empty?
puts "Loading all models..."
# Fix bug with loading `app/models/identity/uniqueness_scopes.rb`
require_relative Rails.root.join('app/models/identity.rb')
%w[app/models/**/*.rb ee/app/models/**/*.rb].each do |filter|
Dir.glob(filter).each do |path|
require_relative Rails.root.join(path)
end
end
puts
puts "Generating Loose Foreign Key for given filters: #{ARGV}"
all_foreign_keys.each_with_index do |definition, idx|
next unless matching_filter?(definition, ARGV)
puts "Matched: #{idx} (#{definition.from_table}, #{definition.to_table}, #{definition.column})"
add_definition_to_yaml(definition)
generate_migration(definition)
add_test_to_specs(definition)
update_no_cross_db_foreign_keys_spec(definition)
commit_changes(definition)
end
puts

View File

@ -456,6 +456,28 @@ RSpec.describe RegistrationsController do
subject
end
describe 'logged_out_marketing_header experiment', :experiment do
before do
stub_experiments(logged_out_marketing_header: :candidate)
end
it 'tracks signed_up event' do
expect(experiment(:logged_out_marketing_header)).to track(:signed_up).on_next_instance
subject
end
context 'when registration fails' do
let_it_be(:user_params) { { user: base_user_params.merge({ username: '' }) } }
it 'does not track signed_up event' do
expect(experiment(:logged_out_marketing_header)).not_to track(:signed_up)
subject
end
end
end
end
describe '#destroy' do

View File

@ -495,7 +495,7 @@ RSpec.describe "Admin Runners" do
description: 'runner-foo',
version: '14.0',
ip_address: '127.0.0.1',
tag_list: %w(tag1 tag2)
tag_list: ['tag1']
)
end
@ -519,7 +519,7 @@ RSpec.describe "Admin Runners" do
expect(page).to have_content 'IP Address 127.0.0.1'
expect(page).to have_content 'Configuration Runs untagged jobs'
expect(page).to have_content 'Maximum job timeout None'
expect(page).to have_content 'Tags tag1 tag2'
expect(page).to have_content 'Tags tag1'
end
end
end

View File

@ -24,23 +24,6 @@ RSpec.describe 'Dropdown base', :js do
visit project_issues_path(project)
end
describe 'behavior' do
it 'shows loading indicator when opened' do
slow_requests do
# We aren't using `input_filtered_search` because we want to see the loading indicator
filtered_search.set('assignee:=')
expect(page).to have_css("#{js_dropdown_assignee} .filter-dropdown-loading", visible: true)
end
end
it 'hides loading indicator when loaded' do
input_filtered_search('assignee:=', submit: false, extra_space: false)
expect(find(js_dropdown_assignee)).not_to have_css('.filter-dropdown-loading')
end
end
describe 'caching requests' do
it 'caches requests after the first load' do
input_filtered_search('assignee:=', submit: false, extra_space: false)

View File

@ -145,7 +145,7 @@ RSpec.describe "Issues > User edits issue", :js do
fill_in 'Comment', with: '/label ~syzygy'
click_button 'Comment'
expect(page).to have_text('added syzygy label just now', wait: 300)
expect(page).to have_text('added syzygy label just now')
page.within '.block.labels' do
# Remove `verisimilitude` label

View File

@ -1,6 +1,6 @@
import { GlModal, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import getDiffWithCommit from 'test_fixtures/merge_request_diffs/with_commit.json';
import AddReviewItemsModal from '~/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue';
@ -84,11 +84,10 @@ describe('AddContextCommitsModal', () => {
expect(findModal().attributes('ok-disabled')).toBe('true');
});
it('enabled ok button when atleast one row is selected', () => {
it('enabled ok button when atleast one row is selected', async () => {
wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
return wrapper.vm.$nextTick().then(() => {
expect(findModal().attributes('ok-disabled')).toBeFalsy();
});
await nextTick();
expect(findModal().attributes('ok-disabled')).toBeFalsy();
});
});
@ -100,11 +99,10 @@ describe('AddContextCommitsModal', () => {
expect(findModal().attributes('ok-disabled')).toBe('true');
});
it('an enabled ok button when atleast one row is selected', () => {
it('an enabled ok button when atleast one row is selected', async () => {
wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
return wrapper.vm.$nextTick().then(() => {
expect(findModal().attributes('ok-disabled')).toBeFalsy();
});
await nextTick();
expect(findModal().attributes('ok-disabled')).toBeFalsy();
});
it('a disabled ok button in first tab, when row is selected in second tab', () => {
@ -114,33 +112,30 @@ describe('AddContextCommitsModal', () => {
});
describe('has an ok button when clicked calls action', () => {
it('"createContextCommits" when only new commits to be added ', () => {
it('"createContextCommits" when only new commits to be added ', async () => {
wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
findModal().vm.$emit('ok');
return wrapper.vm.$nextTick().then(() => {
expect(createContextCommits).toHaveBeenCalledWith(expect.anything(), {
commits: [{ ...commit, isSelected: true }],
forceReload: true,
});
await nextTick();
expect(createContextCommits).toHaveBeenCalledWith(expect.anything(), {
commits: [{ ...commit, isSelected: true }],
forceReload: true,
});
});
it('"removeContextCommits" when only added commits are to be removed ', () => {
it('"removeContextCommits" when only added commits are to be removed ', async () => {
wrapper.vm.$store.state.toRemoveCommits = [commit.short_id];
findModal().vm.$emit('ok');
return wrapper.vm.$nextTick().then(() => {
expect(removeContextCommits).toHaveBeenCalledWith(expect.anything(), true);
});
await nextTick();
expect(removeContextCommits).toHaveBeenCalledWith(expect.anything(), true);
});
it('"createContextCommits" and "removeContextCommits" when new commits are to be added and old commits are to be removed', () => {
it('"createContextCommits" and "removeContextCommits" when new commits are to be added and old commits are to be removed', async () => {
wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
wrapper.vm.$store.state.toRemoveCommits = [commit.short_id];
findModal().vm.$emit('ok');
return wrapper.vm.$nextTick().then(() => {
expect(createContextCommits).toHaveBeenCalledWith(expect.anything(), {
commits: [{ ...commit, isSelected: true }],
});
expect(removeContextCommits).toHaveBeenCalledWith(expect.anything(), undefined);
await nextTick();
expect(createContextCommits).toHaveBeenCalledWith(expect.anything(), {
commits: [{ ...commit, isSelected: true }],
});
expect(removeContextCommits).toHaveBeenCalledWith(expect.anything(), undefined);
});
});

View File

@ -1,5 +1,6 @@
import { GlButton, GlFormInput, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import DeleteUserModal from '~/admin/users/components/modals/delete_user_modal.vue';
import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue';
import ModalStub from './stubs/modal_stub';
@ -82,11 +83,11 @@ describe('User Operation confirmation modal', () => {
});
describe('with incorrect username', () => {
beforeEach(() => {
beforeEach(async () => {
createComponent();
setUsername(badUsername);
return wrapper.vm.$nextTick();
await nextTick();
});
it('shows incorrect username', () => {
@ -100,11 +101,11 @@ describe('User Operation confirmation modal', () => {
});
describe('with correct username', () => {
beforeEach(() => {
beforeEach(async () => {
createComponent();
setUsername(username);
return wrapper.vm.$nextTick();
await nextTick();
});
it('shows correct username', () => {
@ -117,10 +118,10 @@ describe('User Operation confirmation modal', () => {
});
describe('when primary action is submitted', () => {
beforeEach(() => {
beforeEach(async () => {
findPrimaryButton().vm.$emit('click');
return wrapper.vm.$nextTick();
await nextTick();
});
it('clears the input', () => {
@ -136,10 +137,10 @@ describe('User Operation confirmation modal', () => {
});
describe('when secondary action is submitted', () => {
beforeEach(() => {
beforeEach(async () => {
findSecondaryButton().vm.$emit('click');
return wrapper.vm.$nextTick();
await nextTick();
});
it('has correct form attributes and calls submit', () => {
@ -168,7 +169,7 @@ describe('User Operation confirmation modal', () => {
it("shows enabled buttons when user's name is entered without whitespace", async () => {
setUsername('John Smith');
await wrapper.vm.$nextTick();
await nextTick();
expect(findPrimaryButton().attributes('disabled')).toBeUndefined();
expect(findSecondaryButton().attributes('disabled')).toBeUndefined();

View File

@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import UserModalManager from '~/admin/users/components/modals/user_modal_manager.vue';
import ModalStub from './stubs/modal_stub';
@ -50,20 +51,19 @@ describe('Users admin page Modal Manager', () => {
expect(() => wrapper.vm.show({ glModalAction: 'action1' })).toThrow();
});
it('renders modal with expected props when valid configuration is passed', () => {
it('renders modal with expected props when valid configuration is passed', async () => {
createComponent();
wrapper.vm.show({
glModalAction: 'action1',
extraProp: 'extraPropValue',
});
return wrapper.vm.$nextTick().then(() => {
const modal = findModal();
expect(modal.exists()).toBeTruthy();
expect(modal.vm.$attrs.csrfToken).toEqual('dummyCSRF');
expect(modal.vm.$attrs.extraProp).toEqual('extraPropValue');
expect(modal.vm.showWasCalled).toBeTruthy();
});
await nextTick();
const modal = findModal();
expect(modal.exists()).toBeTruthy();
expect(modal.vm.$attrs.csrfToken).toEqual('dummyCSRF');
expect(modal.vm.$attrs.extraProp).toEqual('extraPropValue');
expect(modal.vm.showWasCalled).toBeTruthy();
});
});
@ -101,7 +101,7 @@ describe('Users admin page Modal Manager', () => {
it('renders the modal when the button is clicked', async () => {
button.click();
await wrapper.vm.$nextTick();
await nextTick();
expect(findModal().exists()).toBe(true);
});
@ -110,7 +110,7 @@ describe('Users admin page Modal Manager', () => {
button.removeAttribute('data-gl-modal-action');
button.click();
await wrapper.vm.$nextTick();
await nextTick();
expect(findModal().exists()).toBe(false);
});
@ -118,7 +118,7 @@ describe('Users admin page Modal Manager', () => {
it('does not render the modal when a button without the selector class is clicked', async () => {
button2.click();
await wrapper.vm.$nextTick();
await nextTick();
expect(findModal().exists()).toBe(false);
});

View File

@ -2,6 +2,7 @@ import { GlTable, GlAlert, GlLoadingIcon, GlDropdown, GlIcon, GlAvatar } from '@
import { mount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import mockAlerts from 'jest/vue_shared/alert_details/mocks/alerts.json';
@ -169,7 +170,7 @@ describe('AlertManagementTable', () => {
loading: false,
});
await wrapper.vm.$nextTick();
await nextTick();
expect(wrapper.find(GlTable).exists()).toBe(true);
expect(findAlertsTable().find(GlIcon).classes('icon-critical')).toBe(true);

View File

@ -1,7 +1,7 @@
import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql';
import updateHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql';
@ -70,7 +70,7 @@ describe('AlertsSettingsWrapper', () => {
async function destroyHttpIntegration(localWrapper) {
await jest.runOnlyPendingTimers();
await localWrapper.vm.$nextTick();
await nextTick();
localWrapper
.find(IntegrationsList)

View File

@ -1,5 +1,6 @@
import { GlDaterangePicker } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { useFakeDate } from 'helpers/fake_date';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import Daterange from '~/analytics/shared/components/daterange.vue';
@ -48,7 +49,7 @@ describe('Daterange component', () => {
});
describe('with a minDate being set', () => {
it('emits the change event with the minDate when the user enters a start date before the minDate', () => {
it('emits the change event with the minDate when the user enters a start date before the minDate', async () => {
const startDate = new Date('2019-09-01');
const endDate = new Date('2019-09-30');
const minDate = new Date('2019-06-01');
@ -60,9 +61,8 @@ describe('Daterange component', () => {
input.setValue('2019-01-01');
input.trigger('change');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted().change).toEqual([[{ startDate: minDate, endDate }]]);
});
await nextTick();
expect(wrapper.emitted().change).toEqual([[{ startDate: minDate, endDate }]]);
});
});

View File

@ -1,4 +1,5 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import { TEST_HOST } from 'helpers/test_constants';
@ -99,9 +100,9 @@ describe('ProjectsDropdownFilter component', () => {
const findDropdownFullPathAtIndex = (index) =>
findDropdownAtIndex(index).find('[data-testid="project-full-path"]');
const selectDropdownItemAtIndex = (index) => {
const selectDropdownItemAtIndex = async (index) => {
findDropdownAtIndex(index).find('button').trigger('click');
return wrapper.vm.$nextTick();
await nextTick();
};
// NOTE: Selected items are now visually separated from unselected items
@ -132,16 +133,15 @@ describe('ProjectsDropdownFilter component', () => {
expect(spyQuery).toHaveBeenCalledTimes(1);
await wrapper.vm.$nextTick(() => {
expect(spyQuery).toHaveBeenCalledWith({
query: getProjects,
variables: {
search: 'gitlab',
groupFullPath: wrapper.vm.groupNamespace,
first: 50,
includeSubgroups: true,
},
});
await nextTick();
expect(spyQuery).toHaveBeenCalledWith({
query: getProjects,
variables: {
search: 'gitlab',
groupFullPath: wrapper.vm.groupNamespace,
first: 50,
includeSubgroups: true,
},
});
});
});
@ -193,7 +193,7 @@ describe('ProjectsDropdownFilter component', () => {
expect(wrapper.text()).toContain('2 projects selected');
findClearAllButton().trigger('click');
await wrapper.vm.$nextTick();
await nextTick();
expect(wrapper.text()).not.toContain('2 projects selected');
expect(wrapper.text()).toContain('Select projects');
@ -366,9 +366,8 @@ describe('ProjectsDropdownFilter component', () => {
selectDropdownItemAtIndex(0);
selectDropdownItemAtIndex(1);
await wrapper.vm.$nextTick().then(() => {
expect(findDropdownButton().text()).toBe('2 projects selected');
});
await nextTick();
expect(findDropdownButton().text()).toBe('2 projects selected');
});
});
});

View File

@ -1,7 +1,7 @@
import { GlAlert } from '@gitlab/ui';
import { GlLineChart } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@ -164,7 +164,7 @@ describe('UsageTrendsCountChart', () => {
.spyOn(wrapper.vm.$apollo.queries[identifier], 'fetchMore')
.mockImplementation(jest.fn().mockRejectedValue());
await wrapper.vm.$nextTick();
await nextTick();
});
it('calls fetchMore', () => {

View File

@ -1,7 +1,7 @@
import { GlAlert } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import UsersChart from '~/analytics/usage_trends/components/users_chart.vue';
@ -67,7 +67,7 @@ describe('UsersChart', () => {
describe('without data', () => {
beforeEach(async () => {
wrapper = createComponent({ users: [] });
await wrapper.vm.$nextTick();
await nextTick();
});
it('renders an no data message', () => {
@ -86,7 +86,7 @@ describe('UsersChart', () => {
describe('with data', () => {
beforeEach(async () => {
wrapper = createComponent({ users: mockCountsData2 });
await wrapper.vm.$nextTick();
await nextTick();
});
it('hides the skeleton loader', () => {
@ -107,7 +107,7 @@ describe('UsersChart', () => {
describe('with errors', () => {
beforeEach(async () => {
wrapper = createComponent({ loadingError: true });
await wrapper.vm.$nextTick();
await nextTick();
});
it('renders an error message', () => {
@ -134,7 +134,7 @@ describe('UsersChart', () => {
});
jest.spyOn(wrapper.vm.$apollo.queries.users, 'fetchMore');
await wrapper.vm.$nextTick();
await nextTick();
});
it('requests data twice', () => {
@ -147,7 +147,7 @@ describe('UsersChart', () => {
});
describe('when the fetchMore query throws an error', () => {
beforeEach(() => {
beforeEach(async () => {
wrapper = createComponent({
users: mockCountsData2,
additionalData: mockCountsData1,
@ -156,7 +156,7 @@ describe('UsersChart', () => {
jest
.spyOn(wrapper.vm.$apollo.queries.users, 'fetchMore')
.mockImplementation(jest.fn().mockRejectedValue());
return wrapper.vm.$nextTick();
await nextTick();
});
it('calls fetchMore', () => {

View File

@ -1,6 +1,6 @@
import { GlFormCheckbox, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import UpdateKeepLatestArtifactProjectSetting from '~/artifacts_settings/graphql/mutations/update_keep_latest_artifact_project_setting.mutation.graphql';
@ -109,13 +109,13 @@ describe('Keep latest artifact checkbox', () => {
});
it('sets correct setting value in checkbox with query result', async () => {
await wrapper.vm.$nextTick();
await nextTick();
expect(wrapper.element).toMatchSnapshot();
});
it('checkbox is enabled when application setting is enabled', async () => {
await wrapper.vm.$nextTick();
await nextTick();
expect(findCheckbox().attributes('disabled')).toBeUndefined();
});

View File

@ -1,6 +1,6 @@
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import BadgeList from '~/badges/components/badge_list.vue';
import BadgeListRow from '~/badges/components/badge_list_row.vue';
@ -40,7 +40,7 @@ describe('BadgeSettings component', () => {
const button = wrapper.find('[data-testid="delete-badge"]');
button.vm.$emit('click');
await wrapper.vm.$nextTick();
await nextTick();
const modal = wrapper.find(GlModal);
expect(modal.isVisible()).toBe(true);

View File

@ -1,5 +1,6 @@
import { getByRole } from '@testing-library/dom';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { stubComponent } from 'helpers/stub_component';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import { createStore } from '~/batch_comments/stores';
@ -71,45 +72,37 @@ describe('Batch comments draft note component', () => {
);
});
it('sets as loading when draft is publishing', (done) => {
it('sets as loading when draft is publishing', async () => {
createComponent();
wrapper.vm.$store.state.batchComments.currentlyPublishingDrafts.push(1);
wrapper.vm.$nextTick(() => {
const publishNowButton = wrapper.find({ ref: 'publishNowButton' });
await nextTick();
const publishNowButton = wrapper.find({ ref: 'publishNowButton' });
expect(publishNowButton.props().loading).toBe(true);
done();
});
expect(publishNowButton.props().loading).toBe(true);
});
});
describe('update', () => {
it('dispatches updateDraft', (done) => {
it('dispatches updateDraft', async () => {
createComponent();
const note = wrapper.find(NoteableNote);
note.vm.$emit('handleEdit');
wrapper.vm
.$nextTick()
.then(() => {
const formData = {
note: draft,
noteText: 'a',
resolveDiscussion: false,
};
await nextTick();
const formData = {
note: draft,
noteText: 'a',
resolveDiscussion: false,
};
note.vm.$emit('handleUpdateNote', formData);
note.vm.$emit('handleUpdateNote', formData);
expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith(
'batchComments/updateDraft',
formData,
);
})
.then(done)
.catch(done.fail);
expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith(
'batchComments/updateDraft',
formData,
);
});
});
@ -127,7 +120,7 @@ describe('Batch comments draft note component', () => {
});
describe('quick actions', () => {
it('renders referenced commands', (done) => {
it('renders referenced commands', async () => {
createComponent();
wrapper.setProps({
draft: {
@ -138,14 +131,11 @@ describe('Batch comments draft note component', () => {
},
});
wrapper.vm.$nextTick(() => {
const referencedCommands = wrapper.find('.referenced-commands');
await nextTick();
const referencedCommands = wrapper.find('.referenced-commands');
expect(referencedCommands.exists()).toBe(true);
expect(referencedCommands.text()).toContain('test command');
done();
});
expect(referencedCommands.exists()).toBe(true);
expect(referencedCommands.text()).toContain('test command');
});
});

View File

@ -1,4 +1,4 @@
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import DraftsCount from '~/batch_comments/components/drafts_count.vue';
import { createStore } from '~/batch_comments/stores';
@ -27,17 +27,14 @@ describe('Batch comments drafts count component', () => {
expect(vm.$el.textContent).toContain('1');
});
it('renders screen reader text', (done) => {
it('renders screen reader text', async () => {
const el = vm.$el.querySelector('.sr-only');
expect(el.textContent).toContain('draft');
vm.$store.state.batchComments.drafts.push('comment 2');
vm.$nextTick(() => {
expect(el.textContent).toContain('drafts');
done();
});
await nextTick();
expect(el.textContent).toContain('drafts');
});
});

View File

@ -1,4 +1,4 @@
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import PublishButton from '~/batch_comments/components/publish_button.vue';
import { createStore } from '~/batch_comments/stores';
@ -29,13 +29,10 @@ describe('Batch comments publish button component', () => {
expect(vm.$store.dispatch).toHaveBeenCalledWith('batchComments/publishReview', undefined);
});
it('sets loading when isPublishing is true', (done) => {
it('sets loading when isPublishing is true', async () => {
vm.$store.state.batchComments.isPublishing = true;
vm.$nextTick(() => {
expect(vm.$el.getAttribute('disabled')).toBe('disabled');
done();
});
await nextTick();
expect(vm.$el.getAttribute('disabled')).toBe('disabled');
});
});

View File

@ -1,5 +1,6 @@
import { GlFormInput, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import BlobEditHeader from '~/blob/components/blob_edit_header.vue';
describe('Blob Header Editing', () => {
@ -40,7 +41,7 @@ describe('Blob Header Editing', () => {
});
describe('functionality', () => {
it('emits input event when the blob name is changed', () => {
it('emits input event when the blob name is changed', async () => {
const inputComponent = wrapper.find(GlFormInput);
const newValue = 'bar.txt';
@ -51,9 +52,8 @@ describe('Blob Header Editing', () => {
});
inputComponent.vm.$emit('change');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted().input[0]).toEqual([newValue]);
});
await nextTick();
expect(wrapper.emitted().input[0]).toEqual([newValue]);
});
});

View File

@ -1,4 +1,5 @@
import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import BlobHeader from '~/blob/components/blob_header.vue';
import DefaultActions from '~/blob/components/blob_header_default_actions.vue';
import BlobFilepath from '~/blob/components/blob_header_filepath.vue';
@ -139,26 +140,24 @@ describe('Blob Header Default Actions', () => {
expect(wrapper.vm.viewer).toBe(null);
});
it('watches the changes in viewer data and emits event when the change is registered', () => {
it('watches the changes in viewer data and emits event when the change is registered', async () => {
factory();
jest.spyOn(wrapper.vm, '$emit');
wrapper.vm.viewer = newViewer;
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.$emit).toHaveBeenCalledWith('viewer-changed', newViewer);
});
await nextTick();
expect(wrapper.vm.$emit).toHaveBeenCalledWith('viewer-changed', newViewer);
});
it('does not emit event if the switcher is not rendered', () => {
it('does not emit event if the switcher is not rendered', async () => {
factory(true);
expect(wrapper.vm.showViewerSwitcher).toBe(false);
jest.spyOn(wrapper.vm, '$emit');
wrapper.vm.viewer = newViewer;
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.$emit).not.toHaveBeenCalled();
});
await nextTick();
expect(wrapper.vm.$emit).not.toHaveBeenCalled();
});
});
});

View File

@ -1,5 +1,6 @@
import { GlButtonGroup, GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import BlobHeaderViewerSwitcher from '~/blob/components/blob_header_viewer_switcher.vue';
import {
RICH_BLOB_VIEWER,
@ -72,26 +73,24 @@ describe('Blob Header Viewer Switcher', () => {
expect(wrapper.vm.$emit).not.toHaveBeenCalled();
});
it('emits an event when a Rich Viewer button is clicked', () => {
it('emits an event when a Rich Viewer button is clicked', async () => {
factory();
expect(wrapper.vm.value).toBe(SIMPLE_BLOB_VIEWER);
richBtn.vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', RICH_BLOB_VIEWER);
});
await nextTick();
expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', RICH_BLOB_VIEWER);
});
it('emits an event when a Simple Viewer button is clicked', () => {
it('emits an event when a Simple Viewer button is clicked', async () => {
factory({
value: RICH_BLOB_VIEWER,
});
simpleBtn.vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', SIMPLE_BLOB_VIEWER);
});
await nextTick();
expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', SIMPLE_BLOB_VIEWER);
});
});
});

View File

@ -1,6 +1,7 @@
import { GlLabel, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
import { range } from 'lodash';
import Vuex from 'vuex';
import { nextTick } from 'vue';
import setWindowLocation from 'helpers/set_window_location_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { mountExtended } from 'helpers/vue_test_utils_helper';
@ -261,7 +262,7 @@ describe('Board card component', () => {
],
});
await wrapper.vm.$nextTick();
await nextTick();
expect(wrapper.find('.board-card-assignee img').attributes('src')).toBe(
'test_image_from_avatar_url?width=24',
@ -376,7 +377,7 @@ describe('Board card component', () => {
},
});
await wrapper.vm.$nextTick();
await nextTick();
expect(wrapper.find('.board-card-assignee .avatar-counter').text().trim()).toEqual('99+');
});
@ -399,7 +400,7 @@ describe('Board card component', () => {
it('does not render label if label does not have an ID', async () => {
wrapper.setProps({ item: { ...issue, labels: [label1, { title: 'closed' }] } });
await wrapper.vm.$nextTick();
await nextTick();
expect(wrapper.findAll(GlLabel).length).toBe(1);
expect(wrapper.text()).not.toContain('closed');

View File

@ -1,4 +1,5 @@
import Draggable from 'vuedraggable';
import { nextTick } from 'vue';
import { DraggableItemTypes } from 'ee_else_ce/boards/constants';
import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
import waitForPromises from 'helpers/wait_for_promises';
@ -65,14 +66,14 @@ describe('Board list component', () => {
it('shows new issue form', async () => {
wrapper.vm.toggleForm();
await wrapper.vm.$nextTick();
await nextTick();
expect(wrapper.find('.board-new-issue-form').exists()).toBe(true);
});
it('shows new issue form after eventhub event', async () => {
eventHub.$emit(`toggle-issue-form-${wrapper.vm.list.id}`);
await wrapper.vm.$nextTick();
await nextTick();
expect(wrapper.find('.board-new-issue-form').exists()).toBe(true);
});
@ -86,7 +87,7 @@ describe('Board list component', () => {
it('shows count list item', async () => {
wrapper.vm.showCount = true;
await wrapper.vm.$nextTick();
await nextTick();
expect(wrapper.find('.board-list-count').exists()).toBe(true);
expect(wrapper.find('.board-list-count').text()).toBe('Showing all issues');
@ -95,7 +96,7 @@ describe('Board list component', () => {
it('sets data attribute with invalid id', async () => {
wrapper.vm.showCount = true;
await wrapper.vm.$nextTick();
await nextTick();
expect(wrapper.find('.board-list-count').attributes('data-issue-id')).toBe('-1');
});
});
@ -127,7 +128,7 @@ describe('Board list component', () => {
},
});
await wrapper.vm.$nextTick();
await nextTick();
expect(findIssueCountLoadingIcon().exists()).toBe(true);
});
@ -139,9 +140,9 @@ describe('Board list component', () => {
},
});
await wrapper.vm.$nextTick();
await nextTick();
await waitForPromises();
await wrapper.vm.$nextTick();
await nextTick();
expect(wrapper.find('.board-list-count').text()).toBe('Showing 1 of 20 issues');
});
@ -158,7 +159,7 @@ describe('Board list component', () => {
it('sets background to bg-danger-100', async () => {
wrapper.setProps({ list: { issuesCount: 4, maxIssueCount: 3 } });
await wrapper.vm.$nextTick();
await nextTick();
expect(wrapper.find('.bg-danger-100').exists()).toBe(true);
});
});

View File

@ -1,5 +1,5 @@
import { GlButton } from '@gitlab/ui';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
@ -49,7 +49,7 @@ describe('BoardAddNewColumnTrigger', () => {
it('shows the tooltip', async () => {
wrapper.find(GlButton).vm.$emit('click');
await wrapper.vm.$nextTick();
await nextTick();
const tooltip = findTooltipText();

View File

@ -1,6 +1,6 @@
import { GlIcon, GlLink, GlPopover, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@ -39,7 +39,7 @@ describe('BoardBlockedIcon', () => {
const mouseenter = async () => {
findGlIcon().vm.$emit('mouseenter');
await wrapper.vm.$nextTick();
await nextTick();
await waitForApollo();
};

View File

@ -1,6 +1,6 @@
import { GlLabel } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import BoardCard from '~/boards/components/board_card.vue';
@ -65,12 +65,12 @@ describe('Board card', () => {
const selectCard = async () => {
wrapper.trigger('click');
await wrapper.vm.$nextTick();
await nextTick();
};
const multiSelectCard = async () => {
wrapper.trigger('click', { ctrlKey: true });
await wrapper.vm.$nextTick();
await nextTick();
};
beforeEach(() => {

View File

@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import createMockApollo from 'helpers/mock_apollo_helper';
@ -148,7 +148,7 @@ describe('Board List Header Component', () => {
findCaret().vm.$emit('click');
await wrapper.vm.$nextTick();
await nextTick();
expect(toggleListCollapsedSpy).toHaveBeenCalledTimes(1);
});
@ -156,7 +156,7 @@ describe('Board List Header Component', () => {
createComponent({ withLocalStorage: false, currentUserId: 1 });
findCaret().vm.$emit('click');
await wrapper.vm.$nextTick();
await nextTick();
expect(updateListSpy).toHaveBeenCalledTimes(1);
expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.collapsed`)).toBe(null);
@ -168,7 +168,7 @@ describe('Board List Header Component', () => {
});
findCaret().vm.$emit('click');
await wrapper.vm.$nextTick();
await nextTick();
expect(updateListSpy).not.toHaveBeenCalled();
expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.collapsed`)).toBe(String(isCollapsed()));

View File

@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import BoardNewIssue from '~/boards/components/board_new_issue.vue';
import BoardNewItem from '~/boards/components/board_new_item.vue';
@ -45,7 +45,7 @@ describe('Issue boards new issue form', () => {
beforeEach(async () => {
wrapper = createComponent();
await wrapper.vm.$nextTick();
await nextTick();
});
afterEach(() => {
@ -66,7 +66,7 @@ describe('Issue boards new issue form', () => {
it('calls addListNewIssue action when `board-new-item` emits form-submit event', async () => {
findBoardNewItem().vm.$emit('form-submit', { title: 'Foo' });
await wrapper.vm.$nextTick();
await nextTick();
expect(addListNewIssuesSpy).toHaveBeenCalledWith(expect.any(Object), {
list: mockList,
issueInput: {
@ -83,7 +83,7 @@ describe('Issue boards new issue form', () => {
jest.spyOn(eventHub, '$emit').mockImplementation();
findBoardNewItem().vm.$emit('form-cancel');
await wrapper.vm.$nextTick();
await nextTick();
expect(eventHub.$emit).toHaveBeenCalledWith(`toggle-issue-form-${mockList.id}`);
});

View File

@ -1,4 +1,5 @@
import { GlForm, GlFormInput, GlButton } from '@gitlab/ui';
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import BoardNewItem from '~/boards/components/board_new_item.vue';
@ -44,7 +45,7 @@ describe('BoardNewItem', () => {
expect(wrapper.findByTestId('create-button').props('disabled')).toBe(true);
wrapper.find(GlFormInput).vm.$emit('input', 'hello');
await wrapper.vm.$nextTick();
await nextTick();
expect(wrapper.findByTestId('create-button').props('disabled')).toBe(false);
});
@ -54,7 +55,7 @@ describe('BoardNewItem', () => {
it('disables the Create Issue button', async () => {
wrapper.find(GlFormInput).vm.$emit('input', ' ');
await wrapper.vm.$nextTick();
await nextTick();
expect(wrapper.findByTestId('create-button').props('disabled')).toBe(true);
});
@ -124,7 +125,7 @@ describe('BoardNewItem', () => {
it('emits `form-cancel` event and clears title value when `reset` is triggered on gl-form', async () => {
titleInput().setValue('Foo');
await wrapper.vm.$nextTick();
await nextTick();
expect(titleInput().element.value).toBe('Foo');
await glForm().trigger('reset');

View File

@ -1,7 +1,7 @@
import { GlDrawer, GlLabel } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { MountingPortal } from 'portal-vue';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { stubComponent } from 'helpers/stub_component';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@ -86,7 +86,7 @@ describe('BoardSettingsSidebar', () => {
findDrawer().vm.$emit('close');
await wrapper.vm.$nextTick();
await nextTick();
expect(wrapper.find(GlDrawer).exists()).toBe(false);
});
@ -96,7 +96,7 @@ describe('BoardSettingsSidebar', () => {
sidebarEventHub.$emit('sidebar.closeAll');
await wrapper.vm.$nextTick();
await nextTick();
expect(wrapper.find(GlDrawer).exists()).toBe(false);
});

View File

@ -1,5 +1,6 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import BoardSidebarItem from '~/boards/components/sidebar/board_editable_item.vue';
describe('boards sidebar remove issue', () => {
@ -79,17 +80,16 @@ describe('boards sidebar remove issue', () => {
createComponent({ canUpdate: true, slots });
findEditButton().vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(findCollapsed().isVisible()).toBe(false);
expect(findExpanded().isVisible()).toBe(true);
});
await nextTick();
expect(findCollapsed().isVisible()).toBe(false);
expect(findExpanded().isVisible()).toBe(true);
});
it('hides the header while editing if `toggleHeader` is true', async () => {
createComponent({ canUpdate: true, props: { toggleHeader: true } });
findEditButton().vm.$emit('click');
await wrapper.vm.$nextTick();
await nextTick();
expect(findEditButton().isVisible()).toBe(false);
expect(findTitle().isVisible()).toBe(false);
@ -101,14 +101,14 @@ describe('boards sidebar remove issue', () => {
beforeEach(async () => {
createComponent({ canUpdate: true });
findEditButton().vm.$emit('click');
await wrapper.vm.$nextTick();
await nextTick();
});
it('hides expanded section and displays collapsed section', async () => {
expect(findExpanded().isVisible()).toBe(true);
document.body.click();
await wrapper.vm.$nextTick();
await nextTick();
expect(findCollapsed().isVisible()).toBe(true);
expect(findExpanded().isVisible()).toBe(false);
@ -117,7 +117,7 @@ describe('boards sidebar remove issue', () => {
it('emits events', async () => {
document.body.click();
await wrapper.vm.$nextTick();
await nextTick();
expect(wrapper.emitted().close).toHaveLength(1);
expect(wrapper.emitted()['off-click']).toHaveLength(1);
@ -129,7 +129,7 @@ describe('boards sidebar remove issue', () => {
findEditButton().vm.$emit('click');
await wrapper.vm.$nextTick();
await nextTick();
expect(wrapper.emitted().open.length).toBe(1);
});
@ -139,7 +139,7 @@ describe('boards sidebar remove issue', () => {
findEditButton().vm.$emit('click');
await wrapper.vm.$nextTick();
await nextTick();
wrapper.vm.collapse({ emitEvent: false });

View File

@ -1,5 +1,6 @@
import { GlAlert, GlFormInput, GlForm } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { createStore } from '~/boards/stores';
@ -75,7 +76,7 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
});
findFormInput().vm.$emit('input', TEST_TITLE);
findForm().vm.$emit('submit', { preventDefault: () => {} });
await wrapper.vm.$nextTick();
await nextTick();
});
it('collapses sidebar and renders new title', () => {
@ -98,7 +99,7 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {});
findFormInput().vm.$emit('input', '');
findForm().vm.$emit('submit', { preventDefault: () => {} });
await wrapper.vm.$nextTick();
await nextTick();
});
it('commits change to the server', () => {
@ -113,7 +114,7 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
wrapper.vm.$refs.sidebarItem.expand();
findFormInput().vm.$emit('input', TEST_TITLE);
findEditableItem().vm.$emit('off-click');
await wrapper.vm.$nextTick();
await nextTick();
});
it('does not collapses sidebar and shows alert', () => {
@ -148,7 +149,7 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
});
findFormInput().vm.$emit('input', TEST_TITLE);
findCancelButton().vm.$emit('click');
await wrapper.vm.$nextTick();
await nextTick();
});
it('collapses sidebar and render former title', () => {
@ -168,7 +169,7 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
findFormInput().vm.$emit('input', 'Invalid title');
findForm().vm.$emit('submit', { preventDefault: () => {} });
await wrapper.vm.$nextTick();
await nextTick();
});
it('collapses sidebar and renders former item title', () => {

View File

@ -1,6 +1,6 @@
import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import ProjectSelect from '~/boards/components/project_select.vue';
import defaultState from '~/boards/stores/state';
@ -88,7 +88,7 @@ describe('ProjectSelect component', () => {
expect(findGlDropdownLoadingIcon().exists()).toBe(true);
await wrapper.vm.$nextTick();
await nextTick();
expect(findGlDropdownLoadingIcon().exists()).toBe(false);
});

View File

@ -1,5 +1,6 @@
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import CiLint from '~/ci_lint/components/ci_lint.vue';
import CiLintResults from '~/pipeline_editor/components/lint/ci_lint_results.vue';
@ -81,7 +82,7 @@ describe('CI Lint', () => {
it('validation displays results', async () => {
findValidateBtn().vm.$emit('click');
await wrapper.vm.$nextTick();
await nextTick();
expect(findValidateBtn().props('loading')).toBe(true);
@ -96,7 +97,7 @@ describe('CI Lint', () => {
findValidateBtn().vm.$emit('click');
await wrapper.vm.$nextTick();
await nextTick();
expect(findValidateBtn().props('loading')).toBe(true);

View File

@ -1,5 +1,6 @@
import { GlTable, GlBadge } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import TriggersList from '~/ci_settings_pipeline_triggers/components/triggers_list.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@ -25,10 +26,10 @@ describe('TriggersList', () => {
const findEditBtn = (i) => findRowAt(i).find('[data-testid="edit-btn"]');
const findRevokeBtn = (i) => findRowAt(i).find('[data-testid="trigger_revoke_button"]');
beforeEach(() => {
beforeEach(async () => {
createComponent();
return wrapper.vm.$nextTick();
await nextTick();
});
it('displays a table with expected headers', () => {

View File

@ -1,6 +1,6 @@
import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue';
@ -73,7 +73,7 @@ describe('Ci environments dropdown', () => {
describe('Environments found', () => {
beforeEach(async () => {
createComponent('prod');
await wrapper.vm.$nextTick();
await nextTick();
});
it('renders only the environment searched for', () => {
@ -110,7 +110,7 @@ describe('Ci environments dropdown', () => {
it('should emit createClicked if an environment is clicked', async () => {
createComponent('newscope');
await wrapper.vm.$nextTick();
await nextTick();
findDropdownItemByIndex(1).vm.$emit('click');
expect(wrapper.emitted('createClicked')).toEqual([['newscope']]);
});

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import CiVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
import createStore from '~/ci_variable_list/store';
@ -41,13 +41,12 @@ describe('Ci variable table', () => {
expect(findEmptyVariablesPlaceholder().exists()).toBe(true);
});
it('displays correct amount of variables present and no empty message', () => {
it('displays correct amount of variables present and no empty message', async () => {
store.state.variables = mockData.mockVariables;
return wrapper.vm.$nextTick(() => {
expect(wrapper.findAll('.js-ci-variable-row').length).toBe(1);
expect(findEmptyVariablesPlaceholder().exists()).toBe(false);
});
await nextTick();
expect(wrapper.findAll('.js-ci-variable-row').length).toBe(1);
expect(findEmptyVariablesPlaceholder().exists()).toBe(false);
});
});

View File

@ -1,5 +1,6 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import NewCluster from '~/clusters/components/new_cluster.vue';
import createClusterStore from '~/clusters/stores/new_cluster';
@ -7,10 +8,10 @@ describe('NewCluster', () => {
let store;
let wrapper;
const createWrapper = () => {
const createWrapper = async () => {
store = createClusterStore({ clusterConnectHelpPath: '/some/help/path' });
wrapper = shallowMount(NewCluster, { store, stubs: { GlLink, GlSprintf } });
return wrapper.vm.$nextTick();
await nextTick();
};
const findDescription = () => wrapper.find(GlSprintf);

View File

@ -1,5 +1,6 @@
import { GlModal, GlSprintf } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { stubComponent } from 'helpers/stub_component';
import RemoveClusterConfirmation from '~/clusters/components/remove_cluster_confirmation.vue';
import SplitButton from '~/vue_shared/components/split_button.vue';
@ -43,7 +44,7 @@ describe('Remove cluster confirmation modal', () => {
it('opens modal with "cleanup" option', async () => {
findSplitButton().vm.$emit('remove-cluster-and-cleanup');
await wrapper.vm.$nextTick();
await nextTick();
expect(findModal().vm.show).toHaveBeenCalled();
expect(wrapper.vm.confirmCleanup).toEqual(true);
@ -55,7 +56,7 @@ describe('Remove cluster confirmation modal', () => {
it('opens modal without "cleanup" option', async () => {
findSplitButton().vm.$emit('remove-cluster');
await wrapper.vm.$nextTick();
await nextTick();
expect(findModal().vm.show).toHaveBeenCalled();
expect(wrapper.vm.confirmCleanup).toEqual(false);

View File

@ -1,6 +1,6 @@
import { GlToggle, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import IntegrationForm from '~/clusters/forms/components/integration_form.vue';
import { createStore } from '~/clusters/forms/stores/index';
@ -75,32 +75,22 @@ describe('ClusterIntegrationForm', () => {
describe('reactivity', () => {
beforeEach(() => createWrapper());
it('enables the submit button on changing toggle to different value', () => {
return wrapper.vm
.$nextTick()
.then(() => {
// setData is a bad approach because it changes the internal implementation which we should not touch
// but our GlFormInput lacks the ability to set a new value.
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ toggleEnabled: !defaultStoreValues.enabled });
})
.then(() => {
expect(findSubmitButton().props('disabled')).toBe(false);
});
it('enables the submit button on changing toggle to different value', async () => {
await nextTick();
// setData is a bad approach because it changes the internal implementation which we should not touch
// but our GlFormInput lacks the ability to set a new value.
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
await wrapper.setData({ toggleEnabled: !defaultStoreValues.enabled });
expect(findSubmitButton().props('disabled')).toBe(false);
});
it('enables the submit button on changing input values', () => {
return wrapper.vm
.$nextTick()
.then(() => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ envScope: `${defaultStoreValues.environmentScope}1` });
})
.then(() => {
expect(findSubmitButton().props('disabled')).toBe(false);
});
it('enables the submit button on changing input values', async () => {
await nextTick();
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
await wrapper.setData({ envScope: `${defaultStoreValues.environmentScope}1` });
expect(findSubmitButton().props('disabled')).toBe(false);
});
});
});

View File

@ -1,5 +1,5 @@
import { GlDropdown, GlDropdownItem, GlModal, GlFormInput } from '@gitlab/ui';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { ENTER_KEY } from '~/lib/utils/keys';
@ -54,7 +54,7 @@ describe('AgentOptions', () => {
});
};
const createWrapper = ({ mutationResponse = mockDeleteResponse } = {}) => {
const createWrapper = async ({ mutationResponse = mockDeleteResponse } = {}) => {
apolloProvider = createMockApolloProvider({ mutationResponse });
const provide = {
projectPath,
@ -77,7 +77,7 @@ describe('AgentOptions', () => {
wrapper.vm.$refs.modal.hide = jest.fn();
writeQuery();
return wrapper.vm.$nextTick();
await nextTick();
};
const submitAgentToDelete = async () => {

View File

@ -1,6 +1,7 @@
import { GlAlert, GlKeysetPagination, GlLoadingIcon } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { nextTick } from 'vue';
import AgentEmptyState from '~/clusters_list/components/agent_empty_state.vue';
import AgentTable from '~/clusters_list/components/agent_table.vue';
import Agents from '~/clusters_list/components/agents.vue';
@ -21,7 +22,13 @@ describe('Agents', () => {
projectPath: 'path/to/project',
};
const createWrapper = ({ props = {}, agents = [], pageInfo = null, trees = [], count = 0 }) => {
const createWrapper = async ({
props = {},
agents = [],
pageInfo = null,
trees = [],
count = 0,
}) => {
const provide = provideData;
const apolloQueryResponse = {
data: {
@ -47,7 +54,7 @@ describe('Agents', () => {
provide: provideData,
});
return wrapper.vm.$nextTick();
await nextTick();
};
const findAgentTable = () => wrapper.find(AgentTable);
@ -239,14 +246,14 @@ describe('Agents', () => {
},
};
beforeEach(() => {
beforeEach(async () => {
wrapper = shallowMount(Agents, {
mocks,
propsData: defaultProps,
provide: provideData,
});
return wrapper.vm.$nextTick();
await nextTick();
});
it('displays a loading icon', () => {

View File

@ -1,5 +1,6 @@
import { GlLink, GlSprintf, GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import AncestorNotice from '~/clusters_list/components/ancestor_notice.vue';
import ClusterStore from '~/clusters_list/store';
@ -7,10 +8,10 @@ describe('ClustersAncestorNotice', () => {
let store;
let wrapper;
const createWrapper = () => {
const createWrapper = async () => {
store = ClusterStore({ ancestorHelperPath: '/some/ancestor/path' });
wrapper = shallowMount(AncestorNotice, { store, stubs: { GlSprintf, GlAlert } });
return wrapper.vm.$nextTick();
await nextTick();
};
beforeEach(() => {
@ -22,9 +23,9 @@ describe('ClustersAncestorNotice', () => {
});
describe('when cluster does not have ancestors', () => {
beforeEach(() => {
beforeEach(async () => {
store.state.hasAncestorClusters = false;
return wrapper.vm.$nextTick();
await nextTick();
});
it('displays no notice', () => {
@ -33,9 +34,9 @@ describe('ClustersAncestorNotice', () => {
});
describe('when cluster has ancestors', () => {
beforeEach(() => {
beforeEach(async () => {
store.state.hasAncestorClusters = true;
return wrapper.vm.$nextTick();
await nextTick();
});
it('displays notice text', () => {

View File

@ -7,6 +7,7 @@ import {
import * as Sentry from '@sentry/browser';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import Clusters from '~/clusters_list/components/clusters.vue';
import ClustersEmptyState from '~/clusters_list/components/clusters_empty_state.vue';
import ClusterStore from '~/clusters_list/store';
@ -176,9 +177,9 @@ describe('Clusters', () => {
});
describe('nodes finish loading', () => {
beforeEach(() => {
beforeEach(async () => {
wrapper.vm.$store.state.loadingNodes = false;
return wrapper.vm.$nextTick();
await nextTick();
});
it.each`

View File

@ -1,13 +1,14 @@
import { GlPopover } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import NodeErrorHelpText from '~/clusters_list/components/node_error_help_text.vue';
describe('NodeErrorHelpText', () => {
let wrapper;
const createWrapper = (propsData) => {
const createWrapper = async (propsData) => {
wrapper = shallowMount(NodeErrorHelpText, { propsData, stubs: { GlPopover } });
return wrapper.vm.$nextTick();
await nextTick();
};
const findPopover = () => wrapper.find(GlPopover);

View File

@ -1,6 +1,7 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Visibility from 'visibilityjs';
import { nextTick } from 'vue';
import fixture from 'test_fixtures/pipelines/pipelines.json';
import createFlash from '~/flash';
import Poll from '~/lib/utils/poll';
@ -112,7 +113,7 @@ describe('Commit pipeline status component', () => {
createComponent();
});
it('shows the loading icon at start', () => {
it('shows the loading icon at start', async () => {
createComponent();
expect(findLoader().exists()).toBe(true);
@ -120,17 +121,16 @@ describe('Commit pipeline status component', () => {
data: { pipelines: [] },
});
return wrapper.vm.$nextTick().then(() => {
expect(findLoader().exists()).toBe(false);
});
await nextTick();
expect(findLoader().exists()).toBe(false);
});
describe('is successful', () => {
beforeEach(() => {
beforeEach(async () => {
pollConfig.successCallback({
data: { pipelines: [{ details: { status: mockCiStatus } }] },
});
return wrapper.vm.$nextTick();
await nextTick();
});
it('does not render loader', () => {

View File

@ -1,6 +1,7 @@
import { GlEmptyState, GlLoadingIcon, GlModal, GlTableLite } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import fixture from 'test_fixtures/pipelines/pipelines.json';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
@ -238,7 +239,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
it('on desktop, shows a security warning modal', async () => {
await findRunPipelineBtn().trigger('click');
await wrapper.vm.$nextTick();
await nextTick();
expect(findModal()).not.toBeNull();
});

View File

@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import ContributorsCharts from '~/contributors/components/contributors.vue';
import { createStore } from '~/contributors/stores';
import axios from '~/lib/utils/axios_utils';
@ -49,20 +49,18 @@ describe('Contributors charts', () => {
expect(axios.get).toHaveBeenCalledWith(endpoint);
});
it('should display loader whiled loading data', () => {
it('should display loader whiled loading data', async () => {
wrapper.vm.$store.state.loading = true;
return wrapper.vm.$nextTick(() => {
expect(wrapper.find('.contributors-loader').exists()).toBe(true);
});
await nextTick();
expect(wrapper.find('.contributors-loader').exists()).toBe(true);
});
it('should render charts when loading completed and there is chart data', () => {
it('should render charts when loading completed and there is chart data', async () => {
wrapper.vm.$store.state.loading = false;
wrapper.vm.$store.state.chartData = chartData;
return wrapper.vm.$nextTick(() => {
expect(wrapper.find('.contributors-loader').exists()).toBe(false);
expect(wrapper.find('.contributors-charts').exists()).toBe(true);
expect(wrapper.element).toMatchSnapshot();
});
await nextTick();
expect(wrapper.find('.contributors-loader').exists()).toBe(false);
expect(wrapper.find('.contributors-charts').exists()).toBe(true);
expect(wrapper.element).toMatchSnapshot();
});
});

View File

@ -2,6 +2,7 @@ import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import $ from 'jquery';
import { nextTick } from 'vue';
import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
@ -18,35 +19,31 @@ describe('ClusterFormDropdown', () => {
afterEach(() => wrapper.destroy());
describe('when initial value is provided', () => {
it('sets selectedItem to initial value', () => {
it('sets selectedItem to initial value', async () => {
wrapper.setProps({ items, value: secondItem.value });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(DropdownButton).props('toggleText')).toEqual(secondItem.name);
});
await nextTick();
expect(wrapper.find(DropdownButton).props('toggleText')).toEqual(secondItem.name);
});
});
describe('when no item is selected', () => {
it('displays placeholder text', () => {
it('displays placeholder text', async () => {
const placeholder = 'placeholder';
wrapper.setProps({ placeholder });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(DropdownButton).props('toggleText')).toEqual(placeholder);
});
await nextTick();
expect(wrapper.find(DropdownButton).props('toggleText')).toEqual(placeholder);
});
});
describe('when an item is selected', () => {
beforeEach(() => {
beforeEach(async () => {
wrapper.setProps({ items });
return wrapper.vm.$nextTick().then(() => {
wrapper.findAll('.js-dropdown-item').at(1).trigger('click');
return wrapper.vm.$nextTick();
});
await nextTick();
wrapper.findAll('.js-dropdown-item').at(1).trigger('click');
await nextTick();
});
it('emits input event with selected item', () => {
@ -57,18 +54,16 @@ describe('ClusterFormDropdown', () => {
describe('when multiple items are selected', () => {
const value = ['1'];
beforeEach(() => {
beforeEach(async () => {
wrapper.setProps({ items, multiple: true, value });
return wrapper.vm
.$nextTick()
.then(() => {
wrapper.findAll('.js-dropdown-item').at(0).trigger('click');
return wrapper.vm.$nextTick();
})
.then(() => {
wrapper.findAll('.js-dropdown-item').at(1).trigger('click');
return wrapper.vm.$nextTick();
});
await nextTick();
wrapper.findAll('.js-dropdown-item').at(0).trigger('click');
await nextTick();
wrapper.findAll('.js-dropdown-item').at(1).trigger('click');
await nextTick();
});
it('emits input event with an array of selected items', () => {
@ -77,9 +72,9 @@ describe('ClusterFormDropdown', () => {
});
describe('when multiple items can be selected', () => {
beforeEach(() => {
beforeEach(async () => {
wrapper.setProps({ items, multiple: true, value: firstItem.value });
return wrapper.vm.$nextTick();
await nextTick();
});
it('displays a checked GlIcon next to the item', () => {
@ -89,19 +84,18 @@ describe('ClusterFormDropdown', () => {
});
describe('when multiple values can be selected and initial value is null', () => {
it('emits input event with an array of a single selected item', () => {
it('emits input event with an array of a single selected item', async () => {
wrapper.setProps({ items, multiple: true, value: null });
return wrapper.vm.$nextTick().then(() => {
wrapper.findAll('.js-dropdown-item').at(0).trigger('click');
await nextTick();
wrapper.findAll('.js-dropdown-item').at(0).trigger('click');
expect(wrapper.emitted('input')[0]).toEqual([[firstItem.value]]);
});
expect(wrapper.emitted('input')[0]).toEqual([[firstItem.value]]);
});
});
describe('when an item is selected and has a custom label property', () => {
it('displays selected item custom label', () => {
it('displays selected item custom label', async () => {
const labelProperty = 'customLabel';
const label = 'Name';
const currentValue = '1';
@ -109,9 +103,8 @@ describe('ClusterFormDropdown', () => {
wrapper.setProps({ labelProperty, items: customLabelItems, value: currentValue });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(DropdownButton).props('toggleText')).toEqual(label);
});
await nextTick();
expect(wrapper.find(DropdownButton).props('toggleText')).toEqual(label);
});
});
@ -123,86 +116,79 @@ describe('ClusterFormDropdown', () => {
});
describe('when loading and loadingText is provided', () => {
it('uses loading text as toggle button text', () => {
it('uses loading text as toggle button text', async () => {
const loadingText = 'loading text';
wrapper.setProps({ loading: true, loadingText });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(DropdownButton).props('toggleText')).toEqual(loadingText);
});
await nextTick();
expect(wrapper.find(DropdownButton).props('toggleText')).toEqual(loadingText);
});
});
describe('when disabled', () => {
it('dropdown button isDisabled', () => {
it('dropdown button isDisabled', async () => {
wrapper.setProps({ disabled: true });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(DropdownButton).props('isDisabled')).toBe(true);
});
await nextTick();
expect(wrapper.find(DropdownButton).props('isDisabled')).toBe(true);
});
});
describe('when disabled and disabledText is provided', () => {
it('uses disabled text as toggle button text', () => {
it('uses disabled text as toggle button text', async () => {
const disabledText = 'disabled text';
wrapper.setProps({ disabled: true, disabledText });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(DropdownButton).props('toggleText')).toBe(disabledText);
});
await nextTick();
expect(wrapper.find(DropdownButton).props('toggleText')).toBe(disabledText);
});
});
describe('when has errors', () => {
it('sets border-danger class selector to dropdown toggle', () => {
it('sets border-danger class selector to dropdown toggle', async () => {
wrapper.setProps({ hasErrors: true });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(DropdownButton).classes('border-danger')).toBe(true);
});
await nextTick();
expect(wrapper.find(DropdownButton).classes('border-danger')).toBe(true);
});
});
describe('when has errors and an error message', () => {
it('displays error message', () => {
it('displays error message', async () => {
const errorMessage = 'error message';
wrapper.setProps({ hasErrors: true, errorMessage });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find('.js-eks-dropdown-error-message').text()).toEqual(errorMessage);
});
await nextTick();
expect(wrapper.find('.js-eks-dropdown-error-message').text()).toEqual(errorMessage);
});
});
describe('when no results are available', () => {
it('displays empty text', () => {
it('displays empty text', async () => {
const emptyText = 'error message';
wrapper.setProps({ items: [], emptyText });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find('.js-empty-text').text()).toEqual(emptyText);
});
await nextTick();
expect(wrapper.find('.js-empty-text').text()).toEqual(emptyText);
});
});
it('displays search field placeholder', () => {
it('displays search field placeholder', async () => {
const searchFieldPlaceholder = 'Placeholder';
wrapper.setProps({ searchFieldPlaceholder });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(DropdownSearchInput).props('placeholderText')).toEqual(
searchFieldPlaceholder,
);
});
await nextTick();
expect(wrapper.find(DropdownSearchInput).props('placeholderText')).toEqual(
searchFieldPlaceholder,
);
});
it('it filters results by search query', () => {
it('it filters results by search query', async () => {
const searchQuery = secondItem.name;
wrapper.setProps({ items });
@ -210,21 +196,19 @@ describe('ClusterFormDropdown', () => {
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ searchQuery });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.findAll('.js-dropdown-item').length).toEqual(1);
expect(wrapper.find('.js-dropdown-item').text()).toEqual(secondItem.name);
});
await nextTick();
expect(wrapper.findAll('.js-dropdown-item').length).toEqual(1);
expect(wrapper.find('.js-dropdown-item').text()).toEqual(secondItem.name);
});
it('focuses dropdown search input when dropdown is displayed', () => {
it('focuses dropdown search input when dropdown is displayed', async () => {
const dropdownEl = wrapper.find('.dropdown').element;
expect(wrapper.find(DropdownSearchInput).props('focused')).toBe(false);
$(dropdownEl).trigger('shown.bs.dropdown');
return wrapper.vm.$nextTick(() => {
expect(wrapper.find(DropdownSearchInput).props('focused')).toBe(true);
});
await nextTick();
expect(wrapper.find(DropdownSearchInput).props('focused')).toBe(true);
});
});

View File

@ -1,6 +1,6 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import ServiceCredentialsForm from '~/create_cluster/eks_cluster/components/service_credentials_form.vue';
import eksClusterState from '~/create_cluster/eks_cluster/store/state';
@ -65,14 +65,13 @@ describe('ServiceCredentialsForm', () => {
expect(findSubmitButton().attributes('disabled')).toBeTruthy();
});
it('enables submit button when role ARN is not provided', () => {
it('enables submit button when role ARN is not provided', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
vm.setData({ roleArn: '123' });
return vm.vm.$nextTick().then(() => {
expect(findSubmitButton().attributes('disabled')).toBeFalsy();
});
await nextTick();
expect(findSubmitButton().attributes('disabled')).toBeFalsy();
});
it('dispatches createRole action when submit button is clicked', () => {
@ -86,14 +85,14 @@ describe('ServiceCredentialsForm', () => {
});
describe('when is creating role', () => {
beforeEach(() => {
beforeEach(async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
vm.setData({ roleArn: '123' }); // set role ARN to enable button
state.isCreatingRole = true;
return vm.vm.$nextTick();
await nextTick();
});
it('disables submit button', () => {

View File

@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import GkeMachineTypeDropdown from '~/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue';
import createState from '~/create_cluster/gke_cluster/store/state';
@ -73,7 +73,7 @@ describe('GkeMachineTypeDropdown', () => {
expect(dropdownButtonLabel()).toBe(LABELS.DISABLED_NO_ZONE);
});
it('returns loading toggle text', () => {
it('returns loading toggle text', async () => {
store = createStore();
wrapper = createComponent(store);
@ -81,9 +81,8 @@ describe('GkeMachineTypeDropdown', () => {
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isLoading: true });
return wrapper.vm.$nextTick().then(() => {
expect(dropdownButtonLabel()).toBe(LABELS.LOADING);
});
await nextTick();
expect(dropdownButtonLabel()).toBe(LABELS.LOADING);
});
it('returns default toggle text', () => {
@ -113,7 +112,7 @@ describe('GkeMachineTypeDropdown', () => {
});
describe('form input', () => {
it('reflects new value when dropdown item is clicked', () => {
it('reflects new value when dropdown item is clicked', async () => {
store = createStore({
machineTypes: gapiMachineTypesResponseMock.items,
});
@ -123,9 +122,8 @@ describe('GkeMachineTypeDropdown', () => {
wrapper.find('.dropdown-content button').trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(setMachineType).toHaveBeenCalledWith(expect.anything(), selectedMachineTypeMock);
});
await nextTick();
expect(setMachineType).toHaveBeenCalledWith(expect.anything(), selectedMachineTypeMock);
});
});
});

View File

@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import GkeProjectIdDropdown from '~/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue';
import createState from '~/create_cluster/gke_cluster/store/state';
@ -78,19 +78,18 @@ describe('GkeProjectIdDropdown', () => {
expect(dropdownButtonLabel()).toBe(LABELS.VALIDATING_PROJECT_BILLING);
});
it('returns default toggle text', () => {
it('returns default toggle text', async () => {
bootstrap();
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isLoading: false });
return wrapper.vm.$nextTick().then(() => {
expect(dropdownButtonLabel()).toBe(LABELS.DEFAULT);
});
await nextTick();
expect(dropdownButtonLabel()).toBe(LABELS.DEFAULT);
});
it('returns project name if project selected', () => {
it('returns project name if project selected', async () => {
bootstrap(
{
selectedProject: selectedProjectMock,
@ -103,12 +102,11 @@ describe('GkeProjectIdDropdown', () => {
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isLoading: false });
return wrapper.vm.$nextTick().then(() => {
expect(dropdownButtonLabel()).toBe(selectedProjectMock.name);
});
await nextTick();
expect(dropdownButtonLabel()).toBe(selectedProjectMock.name);
});
it('returns empty toggle text', () => {
it('returns empty toggle text', async () => {
bootstrap({
projects: null,
});
@ -116,26 +114,24 @@ describe('GkeProjectIdDropdown', () => {
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isLoading: false });
return wrapper.vm.$nextTick().then(() => {
expect(dropdownButtonLabel()).toBe(LABELS.EMPTY);
});
await nextTick();
expect(dropdownButtonLabel()).toBe(LABELS.EMPTY);
});
});
describe('selectItem', () => {
it('reflects new value when dropdown item is clicked', () => {
it('reflects new value when dropdown item is clicked', async () => {
bootstrap({ projects: gapiProjectsResponseMock.projects });
expect(dropdownHiddenInputValue()).toBe('');
wrapper.find('.dropdown-content button').trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(setProject).toHaveBeenCalledWith(
expect.anything(),
gapiProjectsResponseMock.projects[0],
);
});
await nextTick();
expect(setProject).toHaveBeenCalledWith(
expect.anything(),
gapiProjectsResponseMock.projects[0],
);
});
});
});

View File

@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import GkeZoneDropdown from '~/create_cluster/gke_cluster/components/gke_zone_dropdown.vue';
import { createStore } from '~/create_cluster/gke_cluster/store';
import {
@ -46,11 +47,11 @@ describe('GkeZoneDropdown', () => {
});
describe('isLoading', () => {
beforeEach(() => {
beforeEach(async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isLoading: true });
return wrapper.vm.$nextTick();
await nextTick();
});
it('returns loading toggle text', () => {
@ -59,10 +60,10 @@ describe('GkeZoneDropdown', () => {
});
describe('project is set', () => {
beforeEach(() => {
beforeEach(async () => {
wrapper.vm.$store.commit(SET_PROJECT, selectedProjectMock);
wrapper.vm.$store.commit(SET_PROJECT_BILLING_STATUS, true);
return wrapper.vm.$nextTick();
await nextTick();
});
it('returns default toggle text', () => {
@ -71,9 +72,9 @@ describe('GkeZoneDropdown', () => {
});
describe('project is selected', () => {
beforeEach(() => {
beforeEach(async () => {
wrapper.vm.setItem(selectedZoneMock);
return wrapper.vm.$nextTick();
await nextTick();
});
it('returns project name if project selected', () => {
@ -83,21 +84,20 @@ describe('GkeZoneDropdown', () => {
});
describe('selectItem', () => {
beforeEach(() => {
beforeEach(async () => {
wrapper.vm.$store.commit(SET_ZONES, gapiZonesResponseMock.items);
return wrapper.vm.$nextTick();
await nextTick();
});
it('reflects new value when dropdown item is clicked', () => {
it('reflects new value when dropdown item is clicked', async () => {
const dropdown = wrapper.find(DropdownHiddenInput);
expect(dropdown.attributes('value')).toBe('');
wrapper.find('.dropdown-content button').trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(dropdown.attributes('value')).toBe(selectedZoneMock);
});
await nextTick();
expect(dropdown.attributes('value')).toBe(selectedZoneMock);
});
});
});

View File

@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import Vuex from 'vuex';
@ -42,7 +42,7 @@ const defaultParams = {
};
async function shouldMergeUrlParams(wrapper, result) {
await wrapper.vm.$nextTick();
await nextTick();
expect(urlUtils.mergeUrlParams).toHaveBeenCalledWith(result, window.location.href, {
spreadArrays: true,
});

View File

@ -1,5 +1,6 @@
import { GlEmptyState, GlLoadingIcon, GlTable } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import StageTable from '~/cycle_analytics/components/stage_table.vue';
@ -263,7 +264,7 @@ describe('StageTable', () => {
expect(wrapper.emitted('handleUpdatePagination')).toBeUndefined();
findPagination().vm.$emit('input', 2);
await wrapper.vm.$nextTick();
await nextTick();
expect(wrapper.emitted('handleUpdatePagination')[0]).toEqual([{ page: 2 }]);
});

View File

@ -1,6 +1,7 @@
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import metricsData from 'test_fixtures/projects/analytics/value_stream_analytics/summary.json';
import waitForPromises from 'helpers/wait_for_promises';
import { METRIC_TYPE_SUMMARY } from '~/api/analytics_api';
@ -55,7 +56,7 @@ describe('ValueStreamMetrics', () => {
});
it('will display a loader with pending requests', async () => {
await wrapper.vm.$nextTick();
await nextTick();
expect(wrapper.findComponent(GlSkeletonLoading).exists()).toBe(true);
});
@ -67,7 +68,7 @@ describe('ValueStreamMetrics', () => {
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isLoading: true });
await wrapper.vm.$nextTick();
await nextTick();
const components = findMetrics();

View File

@ -1,6 +1,6 @@
import { GlModal } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import DeployFreezeTable from '~/deploy_freeze/components/deploy_freeze_table.vue';
import createStore from '~/deploy_freeze/store';
@ -56,7 +56,7 @@ describe('Deploy freeze table', () => {
describe('with data', () => {
beforeEach(async () => {
store.commit(RECEIVE_FREEZE_PERIODS_SUCCESS, freezePeriodsFixture);
await wrapper.vm.$nextTick();
await nextTick();
});
it('displays data', () => {
@ -68,7 +68,7 @@ describe('Deploy freeze table', () => {
it('allows user to edit deploy freeze', async () => {
findEditDeployFreezeButton().trigger('click');
await wrapper.vm.$nextTick();
await nextTick();
expect(store.dispatch).toHaveBeenCalledWith(
'setFreezePeriod',

Some files were not shown because too many files have changed in this diff Show More