Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
868c8c35fb
commit
7d8d5a3dab
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:]"
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -1519,6 +1519,10 @@ class Project < ApplicationRecord
|
|||
group || namespace.try(:owner)
|
||||
end
|
||||
|
||||
def owners
|
||||
Array.wrap(owner)
|
||||
end
|
||||
|
||||
def first_owner
|
||||
obj = owner
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
d2d270a335b3a2441a20673bf19d47553f607533d4503e3a01bc3d6d108bcdb3
|
||||
|
|
@ -0,0 +1 @@
|
|||
7865f26c43c79681f37ceb6e4fecf6153282856907ddfd8211d6d1d57d1fb7f3
|
||||
|
|
@ -0,0 +1 @@
|
|||
a1681c1c619db7f4e7e5a760cee7d06a931aa1f02dccfce46be81d75a03ce8ac
|
||||
|
|
@ -0,0 +1 @@
|
|||
90115936ede32bbf9a299582409cb0686e8072c204c4f91364dfb13195155929
|
||||
|
|
@ -0,0 +1 @@
|
|||
af60c6df0fb178a4820ea8cb40b402178da7fb4b6ebeabb8739dc45b96225f89
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
(
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 }]]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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']]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 }]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue