diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-01-25 18:12:32 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-01-25 18:12:32 +0300 |
commit | 7d8d5a3dab415672a41ab29c3bfa9581f275dc50 (patch) | |
tree | 7b9249d8ca8c12ad899b4e6d968193d58e63f458 | |
parent | 868c8c35fbddd439f4df76a5954e2a1caa2af3cc (diff) |
Add latest changes from gitlab-org/gitlab@master
240 files changed, 3341 insertions, 2532 deletions
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue index 7be4c13f544..3f42f825866 100644 --- a/app/assets/javascripts/issues/show/components/description.vue +++ b/app/assets/javascripts/issues/show/components/description.vue @@ -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> diff --git a/app/assets/javascripts/sidebar/graphql.js b/app/assets/javascripts/sidebar/graphql.js index 5b2ce3fe446..c5d94cfa5e8 100644 --- a/app/assets/javascripts/sidebar/graphql.js +++ b/app/assets/javascripts/sidebar/graphql.js @@ -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, }, }; diff --git a/app/assets/javascripts/work_items/pages/create_work_item.vue b/app/assets/javascripts/work_items/pages/create_work_item.vue index 12bad5606d4..2b9db3e3db5 100644 --- a/app/assets/javascripts/work_items/pages/create_work_item.vue +++ b/app/assets/javascripts/work_items/pages/create_work_item.vue @@ -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> diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index d77c8a40a79..6411d5e1000 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -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; + } + } + } + } +} diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 785fbdaa611..8b5e9fa8bb9 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -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] diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index c1765d367d1..7b688c0ccc2 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -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') diff --git a/app/graphql/types/ci/runner_status_enum.rb b/app/graphql/types/ci/runner_status_enum.rb index dd056191ceb..2e65e2d4e1e 100644 --- a/app/graphql/types/ci/runner_status_enum.rb +++ b/app/graphql/types/ci/runner_status_enum.rb @@ -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', diff --git a/app/graphql/types/ci/runner_type.rb b/app/graphql/types/ci/runner_type.rb index f4a3379c5ca..e3f04ec5814 100644 --- a/app/graphql/types/ci/runner_type.rb +++ b/app/graphql/types/ci/runner_type.rb @@ -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, diff --git a/app/models/ci/namespace_mirror.rb b/app/models/ci/namespace_mirror.rb index ce3faf3546b..d5cbbb96134 100644 --- a/app/models/ci/namespace_mirror.rb +++ b/app/models/ci/namespace_mirror.rb @@ -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:]" diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 0f09b1b8996..032959c385e 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -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? diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index e7bc6bffcaf..fc13ebec7c2 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -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 }) diff --git a/app/models/project.rb b/app/models/project.rb index 1070bb6db4f..27ec475cc8b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1519,6 +1519,10 @@ class Project < ApplicationRecord group || namespace.try(:owner) end + def owners + Array.wrap(owner) + end + def first_owner obj = owner diff --git a/app/presenters/ci/runner_presenter.rb b/app/presenters/ci/runner_presenter.rb index ffd826fab64..482534f27b9 100644 --- a/app/presenters/ci/runner_presenter.rb +++ b/app/presenters/ci/runner_presenter.rb @@ -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 diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index da3cebc2e6d..67cbbaf84f6 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -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 diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index d6557772241..b7299df1bc1 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -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" diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index d0a06c7d5bf..246a31f86c9 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -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' diff --git a/config/feature_flags/development/ci_pipeline_merge_request_presence_check.yml b/config/feature_flags/development/ci_pipeline_merge_request_presence_check.yml new file mode 100644 index 00000000000..19f674aa27c --- /dev/null +++ b/config/feature_flags/development/ci_pipeline_merge_request_presence_check.yml @@ -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 diff --git a/config/metrics/counts_7d/20211102202454_p_ci_templates_security_sast_iac_latest_weekly.yml b/config/metrics/counts_7d/20211102202454_p_ci_templates_security_sast_iac_latest_weekly.yml index e7e2c096902..4fb9a64a234 100644 --- a/config/metrics/counts_7d/20211102202454_p_ci_templates_security_sast_iac_latest_weekly.yml +++ b/config/metrics/counts_7d/20211102202454_p_ci_templates_security_sast_iac_latest_weekly.yml @@ -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: diff --git a/db/post_migrate/20211209103048_backfill_project_namespaces_for_group.rb b/db/post_migrate/20211209103048_backfill_project_namespaces_for_group.rb new file mode 100644 index 00000000000..0b65db7aab4 --- /dev/null +++ b/db/post_migrate/20211209103048_backfill_project_namespaces_for_group.rb @@ -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 diff --git a/db/post_migrate/20220119151221_remove_merge_requests_ci_pipelines_merge_request_id_fk.rb b/db/post_migrate/20220119151221_remove_merge_requests_ci_pipelines_merge_request_id_fk.rb new file mode 100644 index 00000000000..c7ced0ae3b7 --- /dev/null +++ b/db/post_migrate/20220119151221_remove_merge_requests_ci_pipelines_merge_request_id_fk.rb @@ -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 diff --git a/db/post_migrate/20220121221651_remove_projects_ci_variables_project_id_fk.rb b/db/post_migrate/20220121221651_remove_projects_ci_variables_project_id_fk.rb new file mode 100644 index 00000000000..f647d4376ac --- /dev/null +++ b/db/post_migrate/20220121221651_remove_projects_ci_variables_project_id_fk.rb @@ -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 diff --git a/db/post_migrate/20220124204046_remove_projects_ci_sources_pipelines_project_id_fk.rb b/db/post_migrate/20220124204046_remove_projects_ci_sources_pipelines_project_id_fk.rb new file mode 100644 index 00000000000..e15d337045e --- /dev/null +++ b/db/post_migrate/20220124204046_remove_projects_ci_sources_pipelines_project_id_fk.rb @@ -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 diff --git a/db/post_migrate/20220124214131_remove_projects_ci_refs_project_id_fk.rb b/db/post_migrate/20220124214131_remove_projects_ci_refs_project_id_fk.rb new file mode 100644 index 00000000000..f90d477673a --- /dev/null +++ b/db/post_migrate/20220124214131_remove_projects_ci_refs_project_id_fk.rb @@ -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 diff --git a/db/schema_migrations/20211209103048 b/db/schema_migrations/20211209103048 new file mode 100644 index 00000000000..133d5fc1ed4 --- /dev/null +++ b/db/schema_migrations/20211209103048 @@ -0,0 +1 @@ +d2d270a335b3a2441a20673bf19d47553f607533d4503e3a01bc3d6d108bcdb3
\ No newline at end of file diff --git a/db/schema_migrations/20220119151221 b/db/schema_migrations/20220119151221 new file mode 100644 index 00000000000..377170d9cb1 --- /dev/null +++ b/db/schema_migrations/20220119151221 @@ -0,0 +1 @@ +7865f26c43c79681f37ceb6e4fecf6153282856907ddfd8211d6d1d57d1fb7f3
\ No newline at end of file diff --git a/db/schema_migrations/20220121221651 b/db/schema_migrations/20220121221651 new file mode 100644 index 00000000000..562751c6084 --- /dev/null +++ b/db/schema_migrations/20220121221651 @@ -0,0 +1 @@ +a1681c1c619db7f4e7e5a760cee7d06a931aa1f02dccfce46be81d75a03ce8ac
\ No newline at end of file diff --git a/db/schema_migrations/20220124204046 b/db/schema_migrations/20220124204046 new file mode 100644 index 00000000000..f8de3adc215 --- /dev/null +++ b/db/schema_migrations/20220124204046 @@ -0,0 +1 @@ +90115936ede32bbf9a299582409cb0686e8072c204c4f91364dfb13195155929
\ No newline at end of file diff --git a/db/schema_migrations/20220124214131 b/db/schema_migrations/20220124214131 new file mode 100644 index 00000000000..7454bd8b3d1 --- /dev/null +++ b/db/schema_migrations/20220124214131 @@ -0,0 +1 @@ +af60c6df0fb178a4820ea8cb40b402178da7fb4b6ebeabb8739dc45b96225f89
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 0c1cf010705..4e6592174cc 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -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; diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 1b1adff7993..f2d713e9c97 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -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` diff --git a/doc/api/settings.md b/doc/api/settings.md index 2ed841b885c..65196225561 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -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. | diff --git a/doc/development/cicd/templates.md b/doc/development/cicd/templates.md index b1252b86cc0..ba5ca18896e 100644 --- a/doc/development/cicd/templates.md +++ b/doc/development/cicd/templates.md @@ -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 diff --git a/doc/development/service_ping/index.md b/doc/development/service_ping/index.md index 315ff2b090c..428a34461d5 100644 --- a/doc/development/service_ping/index.md +++ b/doc/development/service_ping/index.md @@ -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 diff --git a/doc/user/infrastructure/iac/terraform_state.md b/doc/user/infrastructure/iac/terraform_state.md index 7e994cd5923..9665dc11db7 100644 --- a/doc/user/infrastructure/iac/terraform_state.md +++ b/doc/user/infrastructure/iac/terraform_state.md @@ -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. diff --git a/doc/user/search/advanced_search.md b/doc/user/search/advanced_search.md index 13fba126169..05579696d35 100644 --- a/doc/user/search/advanced_search.md +++ b/doc/user/search/advanced_search.md @@ -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. diff --git a/lib/gitlab/background_migration/batching_strategies/backfill_project_namespace_per_group_batching_strategy.rb b/lib/gitlab/background_migration/batching_strategies/backfill_project_namespace_per_group_batching_strategy.rb new file mode 100644 index 00000000000..f352c527b54 --- /dev/null +++ b/lib/gitlab/background_migration/batching_strategies/backfill_project_namespace_per_group_batching_strategy.rb @@ -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 diff --git a/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy.rb b/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy.rb index 80693728e86..8c2139c6207 100644 --- a/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy.rb +++ b/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy.rb @@ -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) diff --git a/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces.rb b/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces.rb index 8e94c16369e..ba3f7c47047 100644 --- a/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces.rb +++ b/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces.rb @@ -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 ( ( diff --git a/lib/gitlab/database/background_migration/batched_migration_runner.rb b/lib/gitlab/database/background_migration/batched_migration_runner.rb index 14e3919986e..ea994cc09f6 100644 --- a/lib/gitlab/database/background_migration/batched_migration_runner.rb +++ b/lib/gitlab/database/background_migration/batched_migration_runner.rb @@ -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? diff --git a/lib/gitlab/database/gitlab_loose_foreign_keys.yml b/lib/gitlab/database/gitlab_loose_foreign_keys.yml index 13a27c0d1ec..235b49a1878 100644 --- a/lib/gitlab/database/gitlab_loose_foreign_keys.yml +++ b/lib/gitlab/database/gitlab_loose_foreign_keys.yml @@ -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 diff --git a/lib/gitlab/subscription_portal.rb b/lib/gitlab/subscription_portal.rb index d987247fdc4..b8d124541f9 100644 --- a/lib/gitlab/subscription_portal.rb +++ b/lib/gitlab/subscription_portal.rb @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a53e184471b..fa5fe2dea99 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/scripts/decomposition/generate-loose-foreign-key b/scripts/decomposition/generate-loose-foreign-key new file mode 100755 index 00000000000..860da926594 --- /dev/null +++ b/scripts/decomposition/generate-loose-foreign-key @@ -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 diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index d5fe32ac094..af34ae2f69b 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -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 diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index 76d6fded531..eec7fbf625b 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -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 diff --git a/spec/features/issues/filtered_search/dropdown_base_spec.rb b/spec/features/issues/filtered_search/dropdown_base_spec.rb index 3a304515cab..b8fb807dd78 100644 --- a/spec/features/issues/filtered_search/dropdown_base_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_base_spec.rb @@ -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) diff --git a/spec/features/issues/user_edits_issue_spec.rb b/spec/features/issues/user_edits_issue_spec.rb index a036a9a5bbc..8c906e6a27c 100644 --- a/spec/features/issues/user_edits_issue_spec.rb +++ b/spec/features/issues/user_edits_issue_spec.rb @@ -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 diff --git a/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js b/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js index 1918d5c5611..9b93fd26fa0 100644 --- a/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js +++ b/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js @@ -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); }); }); diff --git a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js index 025ae825e0d..f875cd24ee1 100644 --- a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js +++ b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js @@ -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(); diff --git a/spec/frontend/admin/users/components/modals/user_modal_manager_spec.js b/spec/frontend/admin/users/components/modals/user_modal_manager_spec.js index 65ce242662b..4786357faa1 100644 --- a/spec/frontend/admin/users/components/modals/user_modal_manager_spec.js +++ b/spec/frontend/admin/users/components/modals/user_modal_manager_spec.js @@ -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); }); diff --git a/spec/frontend/alert_management/components/alert_management_table_spec.js b/spec/frontend/alert_management/components/alert_management_table_spec.js index 39aab8dc1f8..5b823694b99 100644 --- a/spec/frontend/alert_management/components/alert_management_table_spec.js +++ b/spec/frontend/alert_management/components/alert_management_table_spec.js @@ -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); diff --git a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js index acd80ed8db2..152061790e5 100644 --- a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js +++ b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js @@ -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) diff --git a/spec/frontend/analytics/shared/components/daterange_spec.js b/spec/frontend/analytics/shared/components/daterange_spec.js index 854582abb82..75061dc8934 100644 --- a/spec/frontend/analytics/shared/components/daterange_spec.js +++ b/spec/frontend/analytics/shared/components/daterange_spec.js @@ -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 }]]); }); }); diff --git a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js index 28d7ebe28df..386fb4eb616 100644 --- a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js +++ b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js @@ -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'); }); }); }); diff --git a/spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js b/spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js index f86f2eacf50..9696e069b9c 100644 --- a/spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js +++ b/spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js @@ -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', () => { diff --git a/spec/frontend/analytics/usage_trends/components/users_chart_spec.js b/spec/frontend/analytics/usage_trends/components/users_chart_spec.js index 04ea25a02d5..0cd605ca1bf 100644 --- a/spec/frontend/analytics/usage_trends/components/users_chart_spec.js +++ b/spec/frontend/analytics/usage_trends/components/users_chart_spec.js @@ -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', () => { diff --git a/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js b/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js index 3ba0280deb3..2f3ff2b22f2 100644 --- a/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js +++ b/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js @@ -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(); }); diff --git a/spec/frontend/badges/components/badge_settings_spec.js b/spec/frontend/badges/components/badge_settings_spec.js index 8ac5d2c72d9..79cf5f3e4ff 100644 --- a/spec/frontend/badges/components/badge_settings_spec.js +++ b/spec/frontend/badges/components/badge_settings_spec.js @@ -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); diff --git a/spec/frontend/batch_comments/components/draft_note_spec.js b/spec/frontend/batch_comments/components/draft_note_spec.js index 9c0d59c4443..f93daf91c2f 100644 --- a/spec/frontend/batch_comments/components/draft_note_spec.js +++ b/spec/frontend/batch_comments/components/draft_note_spec.js @@ -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' }); - - expect(publishNowButton.props().loading).toBe(true); + await nextTick(); + const publishNowButton = wrapper.find({ ref: 'publishNowButton' }); - 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, - }; - - note.vm.$emit('handleUpdateNote', formData); - - expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith( - 'batchComments/updateDraft', - formData, - ); - }) - .then(done) - .catch(done.fail); + await nextTick(); + const formData = { + note: draft, + noteText: 'a', + resolveDiscussion: false, + }; + + note.vm.$emit('handleUpdateNote', formData); + + 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'); }); }); diff --git a/spec/frontend/batch_comments/components/drafts_count_spec.js b/spec/frontend/batch_comments/components/drafts_count_spec.js index 5f74de9c014..390ef21929c 100644 --- a/spec/frontend/batch_comments/components/drafts_count_spec.js +++ b/spec/frontend/batch_comments/components/drafts_count_spec.js @@ -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'); }); }); diff --git a/spec/frontend/batch_comments/components/publish_button_spec.js b/spec/frontend/batch_comments/components/publish_button_spec.js index eca424814b4..9a782ec09b6 100644 --- a/spec/frontend/batch_comments/components/publish_button_spec.js +++ b/spec/frontend/batch_comments/components/publish_button_spec.js @@ -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'); }); }); diff --git a/spec/frontend/blob/components/blob_edit_header_spec.js b/spec/frontend/blob/components/blob_edit_header_spec.js index 910fc5c946d..b1ce0e9a4c5 100644 --- a/spec/frontend/blob/components/blob_edit_header_spec.js +++ b/spec/frontend/blob/components/blob_edit_header_spec.js @@ -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]); }); }); diff --git a/spec/frontend/blob/components/blob_header_spec.js b/spec/frontend/blob/components/blob_header_spec.js index bd81b1594bf..8e1b03c6126 100644 --- a/spec/frontend/blob/components/blob_header_spec.js +++ b/spec/frontend/blob/components/blob_header_spec.js @@ -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(); }); }); }); diff --git a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js index 9a560ec11f7..91baaf3ea69 100644 --- a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js +++ b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js @@ -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); }); }); }); diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js index e0446811f64..6de549718d9 100644 --- a/spec/frontend/boards/board_card_inner_spec.js +++ b/spec/frontend/boards/board_card_inner_spec.js @@ -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'); diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js index e29c19d6e97..d07173c8411 100644 --- a/spec/frontend/boards/board_list_spec.js +++ b/spec/frontend/boards/board_list_spec.js @@ -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); }); }); diff --git a/spec/frontend/boards/components/board_add_new_column_trigger_spec.js b/spec/frontend/boards/components/board_add_new_column_trigger_spec.js index c35f2463f69..7dd02bf1d35 100644 --- a/spec/frontend/boards/components/board_add_new_column_trigger_spec.js +++ b/spec/frontend/boards/components/board_add_new_column_trigger_spec.js @@ -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(); diff --git a/spec/frontend/boards/components/board_blocked_icon_spec.js b/spec/frontend/boards/components/board_blocked_icon_spec.js index 7b04942f056..7a5c49bd488 100644 --- a/spec/frontend/boards/components/board_blocked_icon_spec.js +++ b/spec/frontend/boards/components/board_blocked_icon_spec.js @@ -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(); }; diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js index 3af173aa18c..aad89cf8261 100644 --- a/spec/frontend/boards/components/board_card_spec.js +++ b/spec/frontend/boards/components/board_card_spec.js @@ -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(() => { diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js index 8cc0ad5f30c..14870ec76a2 100644 --- a/spec/frontend/boards/components/board_list_header_spec.js +++ b/spec/frontend/boards/components/board_list_header_spec.js @@ -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())); diff --git a/spec/frontend/boards/components/board_new_issue_spec.js b/spec/frontend/boards/components/board_new_issue_spec.js index 12bee842c85..08ef119aad8 100644 --- a/spec/frontend/boards/components/board_new_issue_spec.js +++ b/spec/frontend/boards/components/board_new_issue_spec.js @@ -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}`); }); diff --git a/spec/frontend/boards/components/board_new_item_spec.js b/spec/frontend/boards/components/board_new_item_spec.js index ef83fc925bd..86cebc8a719 100644 --- a/spec/frontend/boards/components/board_new_item_spec.js +++ b/spec/frontend/boards/components/board_new_item_spec.js @@ -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'); diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js index 46dd109ffb1..842c8fd131e 100644 --- a/spec/frontend/boards/components/board_settings_sidebar_spec.js +++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js @@ -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); }); diff --git a/spec/frontend/boards/components/sidebar/board_editable_item_spec.js b/spec/frontend/boards/components/sidebar/board_editable_item_spec.js index 12e9a9ba365..0c76c711b3a 100644 --- a/spec/frontend/boards/components/sidebar/board_editable_item_spec.js +++ b/spec/frontend/boards/components/sidebar/board_editable_item_spec.js @@ -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 }); diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js index 4a8eda298f2..5364d929c38 100644 --- a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js +++ b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js @@ -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', () => { diff --git a/spec/frontend/boards/project_select_spec.js b/spec/frontend/boards/project_select_spec.js index de823094630..05dc7d28eaa 100644 --- a/spec/frontend/boards/project_select_spec.js +++ b/spec/frontend/boards/project_select_spec.js @@ -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); }); diff --git a/spec/frontend/ci_lint/components/ci_lint_spec.js b/spec/frontend/ci_lint/components/ci_lint_spec.js index c4b2927764e..0ad6ed56b0e 100644 --- a/spec/frontend/ci_lint/components/ci_lint_spec.js +++ b/spec/frontend/ci_lint/components/ci_lint_spec.js @@ -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); diff --git a/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js b/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js index 41af257ad89..6bf28a67300 100644 --- a/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js +++ b/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js @@ -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', () => { diff --git a/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js b/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js index f4a0eef1129..e7e4897abfa 100644 --- a/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js +++ b/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js @@ -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']]); }); diff --git a/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js index 2469569b436..ecbbd78b87d 100644 --- a/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js +++ b/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js @@ -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); }); }); diff --git a/spec/frontend/clusters/components/new_cluster_spec.js b/spec/frontend/clusters/components/new_cluster_spec.js index e4bca5eaaa5..b73442f6ec3 100644 --- a/spec/frontend/clusters/components/new_cluster_spec.js +++ b/spec/frontend/clusters/components/new_cluster_spec.js @@ -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); diff --git a/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js b/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js index 41bd492148e..173fefe6167 100644 --- a/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js +++ b/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js @@ -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); diff --git a/spec/frontend/clusters/forms/components/integration_form_spec.js b/spec/frontend/clusters/forms/components/integration_form_spec.js index a9fb5c205fd..dd278bcd2ce 100644 --- a/spec/frontend/clusters/forms/components/integration_form_spec.js +++ b/spec/frontend/clusters/forms/components/integration_form_spec.js @@ -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); }); }); }); diff --git a/spec/frontend/clusters_list/components/agent_options_spec.js b/spec/frontend/clusters_list/components/agent_options_spec.js index f20a7b39a97..1d973ab39cc 100644 --- a/spec/frontend/clusters_list/components/agent_options_spec.js +++ b/spec/frontend/clusters_list/components/agent_options_spec.js @@ -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 () => { diff --git a/spec/frontend/clusters_list/components/agents_spec.js b/spec/frontend/clusters_list/components/agents_spec.js index c9ca10f6bf7..5d7a6a251d2 100644 --- a/spec/frontend/clusters_list/components/agents_spec.js +++ b/spec/frontend/clusters_list/components/agents_spec.js @@ -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', () => { diff --git a/spec/frontend/clusters_list/components/ancestor_notice_spec.js b/spec/frontend/clusters_list/components/ancestor_notice_spec.js index c7ee2a00f5b..a9f11e6fdf8 100644 --- a/spec/frontend/clusters_list/components/ancestor_notice_spec.js +++ b/spec/frontend/clusters_list/components/ancestor_notice_spec.js @@ -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', () => { diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js index 9af25a534d8..82e667093aa 100644 --- a/spec/frontend/clusters_list/components/clusters_spec.js +++ b/spec/frontend/clusters_list/components/clusters_spec.js @@ -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` diff --git a/spec/frontend/clusters_list/components/node_error_help_text_spec.js b/spec/frontend/clusters_list/components/node_error_help_text_spec.js index 18d27f3fd80..8187ab75c58 100644 --- a/spec/frontend/clusters_list/components/node_error_help_text_spec.js +++ b/spec/frontend/clusters_list/components/node_error_help_text_spec.js @@ -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); diff --git a/spec/frontend/commit/commit_pipeline_status_component_spec.js b/spec/frontend/commit/commit_pipeline_status_component_spec.js index 3a549e66eb7..43db6db00c1 100644 --- a/spec/frontend/commit/commit_pipeline_status_component_spec.js +++ b/spec/frontend/commit/commit_pipeline_status_component_spec.js @@ -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', () => { diff --git a/spec/frontend/commit/pipelines/pipelines_table_spec.js b/spec/frontend/commit/pipelines/pipelines_table_spec.js index e209f628aa2..ac432593d2f 100644 --- a/spec/frontend/commit/pipelines/pipelines_table_spec.js +++ b/spec/frontend/commit/pipelines/pipelines_table_spec.js @@ -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(); }); diff --git a/spec/frontend/contributors/component/contributors_spec.js b/spec/frontend/contributors/component/contributors_spec.js index cb7e13b9fed..bdf3b3636ed 100644 --- a/spec/frontend/contributors/component/contributors_spec.js +++ b/spec/frontend/contributors/component/contributors_spec.js @@ -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(); }); }); diff --git a/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js b/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js index 4e92fa1df16..2f835867f5f 100644 --- a/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js +++ b/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js @@ -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); }); }); diff --git a/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js b/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js index a5ac7d5187b..0d823a18012 100644 --- a/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js +++ b/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js @@ -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', () => { diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js index c2cd2104551..f46b84da939 100644 --- a/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js +++ b/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js @@ -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); }); }); }); diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js index 84ae263f2c5..36f8d4bd1e8 100644 --- a/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js +++ b/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js @@ -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], + ); }); }); }); diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js index 22fc681f863..7b4c228b879 100644 --- a/spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js +++ b/spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js @@ -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); }); }); }); diff --git a/spec/frontend/cycle_analytics/filter_bar_spec.js b/spec/frontend/cycle_analytics/filter_bar_spec.js index 5112b038e7c..36933790cf7 100644 --- a/spec/frontend/cycle_analytics/filter_bar_spec.js +++ b/spec/frontend/cycle_analytics/filter_bar_spec.js @@ -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, }); diff --git a/spec/frontend/cycle_analytics/stage_table_spec.js b/spec/frontend/cycle_analytics/stage_table_spec.js index 9605dce2668..107fe5fc865 100644 --- a/spec/frontend/cycle_analytics/stage_table_spec.js +++ b/spec/frontend/cycle_analytics/stage_table_spec.js @@ -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 }]); }); diff --git a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js index 128654f2a51..5a519a07192 100644 --- a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js +++ b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js @@ -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(); diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js index ada819b7e64..137776edfab 100644 --- a/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js +++ b/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js @@ -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', diff --git a/spec/frontend/deploy_keys/components/action_btn_spec.js b/spec/frontend/deploy_keys/components/action_btn_spec.js index 6ac68061518..c4c7a9aea2d 100644 --- a/spec/frontend/deploy_keys/components/action_btn_spec.js +++ b/spec/frontend/deploy_keys/components/action_btn_spec.js @@ -1,5 +1,6 @@ import { GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import data from 'test_fixtures/deploy_keys/keys.json'; import actionBtn from '~/deploy_keys/components/action_btn.vue'; import eventHub from '~/deploy_keys/eventhub'; @@ -37,21 +38,19 @@ describe('Deploy keys action btn', () => { }); }); - it('sends eventHub event with btn type', () => { + it('sends eventHub event with btn type', async () => { jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); findButton().vm.$emit('click'); - return wrapper.vm.$nextTick().then(() => { - expect(eventHub.$emit).toHaveBeenCalledWith('enable.key', deployKey, expect.anything()); - }); + await nextTick(); + expect(eventHub.$emit).toHaveBeenCalledWith('enable.key', deployKey, expect.anything()); }); - it('shows loading spinner after click', () => { + it('shows loading spinner after click', async () => { findButton().vm.$emit('click'); - return wrapper.vm.$nextTick().then(() => { - expect(findButton().props('loading')).toBe(true); - }); + await nextTick(); + expect(findButton().props('loading')).toBe(true); }); }); diff --git a/spec/frontend/deploy_keys/components/app_spec.js b/spec/frontend/deploy_keys/components/app_spec.js index 598b7a0f173..79a9aaa9184 100644 --- a/spec/frontend/deploy_keys/components/app_spec.js +++ b/spec/frontend/deploy_keys/components/app_spec.js @@ -1,5 +1,6 @@ import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; import data from 'test_fixtures/deploy_keys/keys.json'; import waitForPromises from 'helpers/wait_for_promises'; import { TEST_HOST } from 'spec/test_constants'; @@ -39,20 +40,18 @@ describe('Deploy keys app component', () => { const findKeyPanels = () => wrapper.findAll('.deploy-keys .gl-tabs-nav li'); const findModal = () => wrapper.findComponent(ConfirmModal); - it('renders loading icon while waiting for request', () => { + it('renders loading icon while waiting for request', async () => { mock.onGet(TEST_ENDPOINT).reply(() => new Promise()); mountComponent(); - return wrapper.vm.$nextTick().then(() => { - expect(findLoadingIcon().exists()).toBe(true); - }); + await nextTick(); + expect(findLoadingIcon().exists()).toBe(true); }); - it('renders keys panels', () => { - return mountComponent().then(() => { - expect(findKeyPanels().length).toBe(3); - }); + it('renders keys panels', async () => { + await mountComponent(); + expect(findKeyPanels().length).toBe(3); }); it.each` @@ -75,72 +74,55 @@ describe('Deploy keys app component', () => { }); }); - it('re-fetches deploy keys when enabling a key', () => { + it('re-fetches deploy keys when enabling a key', async () => { const key = data.public_keys[0]; - return mountComponent() - .then(() => { - jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {}); - jest.spyOn(wrapper.vm.service, 'enableKey').mockImplementation(() => Promise.resolve()); - - eventHub.$emit('enable.key', key); - - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(wrapper.vm.service.enableKey).toHaveBeenCalledWith(key.id); - expect(wrapper.vm.service.getKeys).toHaveBeenCalled(); - }); + await mountComponent(); + jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {}); + jest.spyOn(wrapper.vm.service, 'enableKey').mockImplementation(() => Promise.resolve()); + + eventHub.$emit('enable.key', key); + + await nextTick(); + expect(wrapper.vm.service.enableKey).toHaveBeenCalledWith(key.id); + expect(wrapper.vm.service.getKeys).toHaveBeenCalled(); }); - it('re-fetches deploy keys when disabling a key', () => { + it('re-fetches deploy keys when disabling a key', async () => { const key = data.public_keys[0]; - return mountComponent() - .then(() => { - jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {}); - jest.spyOn(wrapper.vm.service, 'disableKey').mockImplementation(() => Promise.resolve()); - - eventHub.$emit('disable.key', key, () => {}); - - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(findModal().props('visible')).toBe(true); - findModal().vm.$emit('remove'); - - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(wrapper.vm.service.disableKey).toHaveBeenCalledWith(key.id); - expect(wrapper.vm.service.getKeys).toHaveBeenCalled(); - }); + await mountComponent(); + jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {}); + jest.spyOn(wrapper.vm.service, 'disableKey').mockImplementation(() => Promise.resolve()); + + eventHub.$emit('disable.key', key, () => {}); + + await nextTick(); + expect(findModal().props('visible')).toBe(true); + findModal().vm.$emit('remove'); + + await nextTick(); + expect(wrapper.vm.service.disableKey).toHaveBeenCalledWith(key.id); + expect(wrapper.vm.service.getKeys).toHaveBeenCalled(); }); - it('calls disableKey when removing a key', () => { + it('calls disableKey when removing a key', async () => { const key = data.public_keys[0]; - return mountComponent() - .then(() => { - jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {}); - jest.spyOn(wrapper.vm.service, 'disableKey').mockImplementation(() => Promise.resolve()); - - eventHub.$emit('remove.key', key, () => {}); - - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(findModal().props('visible')).toBe(true); - findModal().vm.$emit('remove'); - - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(wrapper.vm.service.disableKey).toHaveBeenCalledWith(key.id); - expect(wrapper.vm.service.getKeys).toHaveBeenCalled(); - }); + await mountComponent(); + jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {}); + jest.spyOn(wrapper.vm.service, 'disableKey').mockImplementation(() => Promise.resolve()); + + eventHub.$emit('remove.key', key, () => {}); + + await nextTick(); + expect(findModal().props('visible')).toBe(true); + findModal().vm.$emit('remove'); + + await nextTick(); + expect(wrapper.vm.service.disableKey).toHaveBeenCalledWith(key.id); + expect(wrapper.vm.service.getKeys).toHaveBeenCalled(); }); - it('hasKeys returns true when there are keys', () => { - return mountComponent().then(() => { - expect(wrapper.vm.hasKeys).toEqual(3); - }); + it('hasKeys returns true when there are keys', async () => { + await mountComponent(); + expect(wrapper.vm.hasKeys).toEqual(3); }); }); diff --git a/spec/frontend/deploy_keys/components/key_spec.js b/spec/frontend/deploy_keys/components/key_spec.js index 51c120d8213..8599c55c908 100644 --- a/spec/frontend/deploy_keys/components/key_spec.js +++ b/spec/frontend/deploy_keys/components/key_spec.js @@ -1,4 +1,5 @@ import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import data from 'test_fixtures/deploy_keys/keys.json'; import key from '~/deploy_keys/components/key.vue'; import DeployKeysStore from '~/deploy_keys/store'; @@ -95,18 +96,17 @@ describe('Deploy keys key', () => { expect(labels.at(1).attributes('title')).toContain('Expand'); }); - it('expands all project labels after click', () => { + it('expands all project labels after click', async () => { createComponent({ deployKey }); const { length } = deployKey.deploy_keys_projects; wrapper.findAll('.deploy-project-label').at(1).trigger('click'); - return wrapper.vm.$nextTick().then(() => { - const labels = wrapper.findAll('.deploy-project-label'); + await nextTick(); + const labels = wrapper.findAll('.deploy-project-label'); - expect(labels.length).toBe(length); - expect(labels.at(1).text()).not.toContain(`+${length} others`); - expect(labels.at(1).attributes('title')).not.toContain('Expand'); - }); + expect(labels.length).toBe(length); + expect(labels.at(1).text()).not.toContain(`+${length} others`); + expect(labels.at(1).attributes('title')).not.toContain('Expand'); }); it('shows two projects', () => { diff --git a/spec/frontend/design_management/components/delete_button_spec.js b/spec/frontend/design_management/components/delete_button_spec.js index f5a841d35b8..e3907fdbe15 100644 --- a/spec/frontend/design_management/components/delete_button_spec.js +++ b/spec/frontend/design_management/components/delete_button_spec.js @@ -1,5 +1,6 @@ import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import BatchDeleteButton from '~/design_management/components/delete_button.vue'; describe('Batch delete button component', () => { @@ -36,18 +37,15 @@ describe('Batch delete button component', () => { expect(findButton().attributes('disabled')).toBeTruthy(); }); - it('emits `delete-selected-designs` event on modal ok click', () => { + it('emits `delete-selected-designs` event on modal ok click', async () => { createComponent(); findButton().vm.$emit('click'); - return wrapper.vm - .$nextTick() - .then(() => { - findModal().vm.$emit('ok'); - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(wrapper.emitted('delete-selected-designs')).toBeTruthy(); - }); + + await nextTick(); + findModal().vm.$emit('ok'); + + await nextTick(); + expect(wrapper.emitted('delete-selected-designs')).toBeTruthy(); }); it('renders slot content', () => { diff --git a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js index e816a05ba53..bbf2190ad47 100644 --- a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js +++ b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js @@ -1,6 +1,7 @@ import { GlLoadingIcon } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import { ApolloMutation } from 'vue-apollo'; +import { nextTick } from 'vue'; import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue'; import DesignNote from '~/design_management/components/design_notes/design_note.vue'; import DesignNoteSignedOut from '~/design_management/components/design_notes/design_note_signed_out.vue'; @@ -119,12 +120,11 @@ describe('Design discussions component', () => { expect(findResolveIcon().exists()).toBe(false); }); - it('does not render a checkbox in reply form', () => { + it('does not render a checkbox in reply form', async () => { findReplyPlaceholder().vm.$emit('focus'); - return wrapper.vm.$nextTick().then(() => { - expect(findResolveCheckbox().exists()).toBe(false); - }); + await nextTick(); + expect(findResolveCheckbox().exists()).toBe(false); }); }); @@ -150,13 +150,12 @@ describe('Design discussions component', () => { expect(findResolveIcon().props('name')).toBe('check-circle'); }); - it('renders a checkbox with Resolve thread text in reply form', () => { + it('renders a checkbox with Resolve thread text in reply form', async () => { findReplyPlaceholder().vm.$emit('focus'); wrapper.setProps({ discussionWithOpenForm: defaultMockDiscussion.id }); - return wrapper.vm.$nextTick().then(() => { - expect(findResolveCheckbox().text()).toBe('Resolve thread'); - }); + await nextTick(); + expect(findResolveCheckbox().text()).toBe('Resolve thread'); }); it('does not render resolved message', () => { @@ -216,7 +215,7 @@ describe('Design discussions component', () => { findReplyForm().vm.$emit('submitForm'); await mutate(); - await wrapper.vm.$nextTick(); + await nextTick(); const dispatchedEvent = dispatchEventSpy.mock.calls[0][0]; @@ -226,9 +225,9 @@ describe('Design discussions component', () => { }); describe('when replies are expanded', () => { - beforeEach(() => { + beforeEach(async () => { findRepliesWidget().vm.$emit('toggle'); - return wrapper.vm.$nextTick(); + await nextTick(); }); it('renders replies widget with collapsed prop equal to false', () => { @@ -243,26 +242,24 @@ describe('Design discussions component', () => { expect(findReplyPlaceholder().isVisible()).toBe(true); }); - it('renders a checkbox with Unresolve thread text in reply form', () => { + it('renders a checkbox with Unresolve thread text in reply form', async () => { findReplyPlaceholder().vm.$emit('focus'); wrapper.setProps({ discussionWithOpenForm: defaultMockDiscussion.id }); - return wrapper.vm.$nextTick().then(() => { - expect(findResolveCheckbox().text()).toBe('Unresolve thread'); - }); + await nextTick(); + expect(findResolveCheckbox().text()).toBe('Unresolve thread'); }); }); }); - it('hides reply placeholder and opens form on placeholder click', () => { + it('hides reply placeholder and opens form on placeholder click', async () => { createComponent(); findReplyPlaceholder().vm.$emit('focus'); wrapper.setProps({ discussionWithOpenForm: defaultMockDiscussion.id }); - return wrapper.vm.$nextTick().then(() => { - expect(findReplyPlaceholder().exists()).toBe(false); - expect(findReplyForm().exists()).toBe(true); - }); + await nextTick(); + expect(findReplyPlaceholder().exists()).toBe(false); + expect(findReplyForm().exists()).toBe(true); }); it('calls mutation on submitting form and closes the form', async () => { @@ -275,28 +272,24 @@ describe('Design discussions component', () => { expect(mutate).toHaveBeenCalledWith(mutationVariables); await mutate(); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findReplyForm().exists()).toBe(false); }); - it('clears the discussion comment on closing comment form', () => { + it('clears the discussion comment on closing comment form', async () => { createComponent( { discussionWithOpenForm: defaultMockDiscussion.id }, { discussionComment: 'test', isFormRendered: true }, ); - return wrapper.vm - .$nextTick() - .then(() => { - findReplyForm().vm.$emit('cancel-form'); + await nextTick(); + findReplyForm().vm.$emit('cancel-form'); - expect(wrapper.vm.discussionComment).toBe(''); - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(findReplyForm().exists()).toBe(false); - }); + expect(wrapper.vm.discussionComment).toBe(''); + + await nextTick(); + expect(findReplyForm().exists()).toBe(false); }); describe('when any note from a discussion is active', () => { @@ -322,7 +315,7 @@ describe('Design discussions component', () => { ); }); - it('calls toggleResolveDiscussion mutation on resolve thread button click', () => { + it('calls toggleResolveDiscussion mutation on resolve thread button click', async () => { createComponent(); findResolveButton().trigger('click'); expect(mutate).toHaveBeenCalledWith({ @@ -332,9 +325,8 @@ describe('Design discussions component', () => { resolve: true, }, }); - return wrapper.vm.$nextTick(() => { - expect(findResolveLoadingIcon().exists()).toBe(true); - }); + await nextTick(); + expect(findResolveLoadingIcon().exists()).toBe(true); }); it('calls toggleResolveDiscussion mutation after adding a note if checkbox was checked', () => { diff --git a/spec/frontend/design_management/components/design_notes/design_note_spec.js b/spec/frontend/design_management/components/design_notes/design_note_spec.js index 3f5f5bcdfa7..35fd1273270 100644 --- a/spec/frontend/design_management/components/design_notes/design_note_spec.js +++ b/spec/frontend/design_management/components/design_notes/design_note_spec.js @@ -1,5 +1,6 @@ import { shallowMount } from '@vue/test-utils'; import { ApolloMutation } from 'vue-apollo'; +import { nextTick } from 'vue'; import DesignNote from '~/design_management/components/design_notes/design_note.vue'; import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; @@ -96,7 +97,7 @@ describe('Design note component', () => { }); describe('when user has a permission to edit note', () => { - it('should open an edit form on edit button click', () => { + it('should open an edit form on edit button click', async () => { createComponent({ note: { ...note, @@ -108,10 +109,9 @@ describe('Design note component', () => { findEditButton().trigger('click'); - return wrapper.vm.$nextTick().then(() => { - expect(findReplyForm().exists()).toBe(true); - expect(findNoteContent().exists()).toBe(false); - }); + await nextTick(); + expect(findReplyForm().exists()).toBe(true); + expect(findNoteContent().exists()).toBe(false); }); describe('when edit form is rendered', () => { @@ -134,27 +134,22 @@ describe('Design note component', () => { expect(findReplyForm().exists()).toBe(true); }); - it('hides the form on cancel-form event', () => { + it('hides the form on cancel-form event', async () => { findReplyForm().vm.$emit('cancel-form'); - return wrapper.vm.$nextTick().then(() => { - expect(findReplyForm().exists()).toBe(false); - expect(findNoteContent().exists()).toBe(true); - }); + await nextTick(); + expect(findReplyForm().exists()).toBe(false); + expect(findNoteContent().exists()).toBe(true); }); - it('calls a mutation on submit-form event and hides a form', () => { + it('calls a mutation on submit-form event and hides a form', async () => { findReplyForm().vm.$emit('submit-form'); expect(mutate).toHaveBeenCalled(); - return mutate() - .then(() => { - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(findReplyForm().exists()).toBe(false); - expect(findNoteContent().exists()).toBe(true); - }); + await mutate(); + await nextTick(); + expect(findReplyForm().exists()).toBe(false); + expect(findNoteContent().exists()).toBe(true); }); }); }); diff --git a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js index a338a5ef200..d2d1fe6b2d8 100644 --- a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js +++ b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js @@ -1,4 +1,5 @@ import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue'; const showModal = jest.fn(); @@ -64,24 +65,22 @@ describe('Design reply form component', () => { expect(findSubmitButton().attributes().disabled).toBeTruthy(); }); - it('does not emit submitForm event on textarea ctrl+enter keydown', () => { + it('does not emit submitForm event on textarea ctrl+enter keydown', async () => { findTextarea().trigger('keydown.enter', { ctrlKey: true, }); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('submit-form')).toBeFalsy(); - }); + await nextTick(); + expect(wrapper.emitted('submit-form')).toBeFalsy(); }); - it('does not emit submitForm event on textarea meta+enter keydown', () => { + it('does not emit submitForm event on textarea meta+enter keydown', async () => { findTextarea().trigger('keydown.enter', { metaKey: true, }); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('submit-form')).toBeFalsy(); - }); + await nextTick(); + expect(wrapper.emitted('submit-form')).toBeFalsy(); }); it('emits cancelForm event on pressing escape button on textarea', () => { @@ -108,40 +107,36 @@ describe('Design reply form component', () => { expect(findSubmitButton().attributes().disabled).toBeFalsy(); }); - it('emits submitForm event on Comment button click', () => { + it('emits submitForm event on Comment button click', async () => { findSubmitButton().vm.$emit('click'); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('submit-form')).toBeTruthy(); - }); + await nextTick(); + expect(wrapper.emitted('submit-form')).toBeTruthy(); }); - it('emits submitForm event on textarea ctrl+enter keydown', () => { + it('emits submitForm event on textarea ctrl+enter keydown', async () => { findTextarea().trigger('keydown.enter', { ctrlKey: true, }); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('submit-form')).toBeTruthy(); - }); + await nextTick(); + expect(wrapper.emitted('submit-form')).toBeTruthy(); }); - it('emits submitForm event on textarea meta+enter keydown', () => { + it('emits submitForm event on textarea meta+enter keydown', async () => { findTextarea().trigger('keydown.enter', { metaKey: true, }); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('submit-form')).toBeTruthy(); - }); + await nextTick(); + expect(wrapper.emitted('submit-form')).toBeTruthy(); }); - it('emits input event on changing textarea content', () => { + it('emits input event on changing textarea content', async () => { findTextarea().setValue('test2'); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('input')).toBeTruthy(); - }); + await nextTick(); + expect(wrapper.emitted('input')).toBeTruthy(); }); it('emits cancelForm event on Escape key if text was not changed', () => { @@ -150,13 +145,12 @@ describe('Design reply form component', () => { expect(wrapper.emitted('cancel-form')).toBeTruthy(); }); - it('opens confirmation modal on Escape key when text has changed', () => { + it('opens confirmation modal on Escape key when text has changed', async () => { wrapper.setProps({ value: 'test2' }); - return wrapper.vm.$nextTick().then(() => { - findTextarea().trigger('keyup.esc'); - expect(showModal).toHaveBeenCalled(); - }); + await nextTick(); + findTextarea().trigger('keyup.esc'); + expect(showModal).toHaveBeenCalled(); }); it('emits cancelForm event on Cancel button click if text was not changed', () => { @@ -165,13 +159,12 @@ describe('Design reply form component', () => { expect(wrapper.emitted('cancel-form')).toBeTruthy(); }); - it('opens confirmation modal on Cancel button click when text has changed', () => { + it('opens confirmation modal on Cancel button click when text has changed', async () => { wrapper.setProps({ value: 'test2' }); - return wrapper.vm.$nextTick().then(() => { - findCancelButton().trigger('click'); - expect(showModal).toHaveBeenCalled(); - }); + await nextTick(); + findCancelButton().trigger('click'); + expect(showModal).toHaveBeenCalled(); }); it('emits cancelForm event on modal Ok button click', () => { diff --git a/spec/frontend/design_management/components/design_overlay_spec.js b/spec/frontend/design_management/components/design_overlay_spec.js index 4bda5054090..056959425a6 100644 --- a/spec/frontend/design_management/components/design_overlay_spec.js +++ b/spec/frontend/design_management/components/design_overlay_spec.js @@ -1,4 +1,5 @@ import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import DesignOverlay from '~/design_management/components/design_overlay.vue'; import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '~/design_management/constants'; import updateActiveDiscussion from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql'; @@ -17,12 +18,11 @@ describe('Design overlay component', () => { const findFirstBadge = () => findBadgeAtIndex(0); const findSecondBadge = () => findBadgeAtIndex(1); - const clickAndDragBadge = (elem, fromPoint, toPoint) => { + const clickAndDragBadge = async (elem, fromPoint, toPoint) => { elem.trigger('mousedown', { clientX: fromPoint.x, clientY: fromPoint.y }); - return wrapper.vm.$nextTick().then(() => { - elem.trigger('mousemove', { clientX: toPoint.x, clientY: toPoint.y }); - return wrapper.vm.$nextTick(); - }); + await nextTick(); + elem.trigger('mousemove', { clientX: toPoint.x, clientY: toPoint.y }); + await nextTick(); }; function createComponent(props = {}, data = {}) { @@ -59,7 +59,7 @@ describe('Design overlay component', () => { expect(wrapper.attributes().style).toBe('width: 100px; height: 100px; top: 0px; left: 0px;'); }); - it('should emit `openCommentForm` when clicking on overlay', () => { + it('should emit `openCommentForm` when clicking on overlay', async () => { createComponent(); const newCoordinates = { x: 10, @@ -69,11 +69,10 @@ describe('Design overlay component', () => { wrapper .find('[data-qa-selector="design_image_button"]') .trigger('mouseup', { offsetX: newCoordinates.x, offsetY: newCoordinates.y }); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('openCommentForm')).toEqual([ - [{ x: newCoordinates.x, y: newCoordinates.y }], - ]); - }); + await nextTick(); + expect(wrapper.emitted('openCommentForm')).toEqual([ + [{ x: newCoordinates.x, y: newCoordinates.y }], + ]); }); describe('with notes', () => { @@ -116,7 +115,7 @@ describe('Design overlay component', () => { describe('when a discussion is active', () => { it.each([notes[0].discussion.notes.nodes[1], notes[0].discussion.notes.nodes[0]])( 'should not apply inactive class to the pin for the active discussion', - (note) => { + async (note) => { // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax wrapper.setData({ @@ -126,13 +125,12 @@ describe('Design overlay component', () => { }, }); - return wrapper.vm.$nextTick().then(() => { - expect(findBadgeAtIndex(0).classes()).not.toContain('inactive'); - }); + await nextTick(); + expect(findBadgeAtIndex(0).classes()).not.toContain('inactive'); }, ); - it('should apply inactive class to all pins besides the active one', () => { + it('should apply inactive class to all pins besides the active one', 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({ @@ -142,15 +140,14 @@ describe('Design overlay component', () => { }, }); - return wrapper.vm.$nextTick().then(() => { - expect(findSecondBadge().classes()).toContain('inactive'); - expect(findFirstBadge().classes()).not.toContain('inactive'); - }); + await nextTick(); + expect(findSecondBadge().classes()).toContain('inactive'); + expect(findFirstBadge().classes()).not.toContain('inactive'); }); }); }); - it('should recalculate badges positions on window resize', () => { + it('should recalculate badges positions on window resize', async () => { createComponent({ notes, dimensions: { @@ -168,12 +165,11 @@ describe('Design overlay component', () => { }, }); - return wrapper.vm.$nextTick().then(() => { - expect(findFirstBadge().attributes().style).toBe('left: 20px; top: 30px;'); - }); + await nextTick(); + expect(findFirstBadge().attributes().style).toBe('left: 20px; top: 30px;'); }); - it('should call an update active discussion mutation when clicking a note without moving it', () => { + it('should call an update active discussion mutation when clicking a note without moving it', async () => { const note = notes[0]; const { position } = note; const mutationVariables = { @@ -186,31 +182,25 @@ describe('Design overlay component', () => { findFirstBadge().trigger('mousedown', { clientX: position.x, clientY: position.y }); - return wrapper.vm.$nextTick().then(() => { - findFirstBadge().trigger('mouseup', { clientX: position.x, clientY: position.y }); - expect(mutate).toHaveBeenCalledWith(mutationVariables); - }); + await nextTick(); + findFirstBadge().trigger('mouseup', { clientX: position.x, clientY: position.y }); + expect(mutate).toHaveBeenCalledWith(mutationVariables); }); }); describe('when moving notes', () => { - it('should update badge style when note is being moved', () => { + it('should update badge style when note is being moved', async () => { createComponent({ notes, }); const { position } = notes[0]; - return clickAndDragBadge( - findFirstBadge(), - { x: position.x, y: position.y }, - { x: 20, y: 20 }, - ).then(() => { - expect(findFirstBadge().attributes().style).toBe('left: 20px; top: 20px;'); - }); + await clickAndDragBadge(findFirstBadge(), { x: position.x, y: position.y }, { x: 20, y: 20 }); + expect(findFirstBadge().attributes().style).toBe('left: 20px; top: 20px;'); }); - it('should emit `moveNote` event when note-moving action ends', () => { + it('should emit `moveNote` event when note-moving action ends', async () => { createComponent({ notes }); const note = notes[0]; const { position } = note; @@ -231,22 +221,19 @@ describe('Design overlay component', () => { }); const badge = findFirstBadge(); - return clickAndDragBadge(badge, { x: position.x, y: position.y }, newCoordinates) - .then(() => { - badge.trigger('mouseup'); - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(wrapper.emitted('moveNote')).toEqual([ - [ - { - noteId: notes[0].id, - discussionId: notes[0].discussion.id, - coordinates: newCoordinates, - }, - ], - ]); - }); + await clickAndDragBadge(badge, { x: position.x, y: position.y }, newCoordinates); + badge.trigger('mouseup'); + + await nextTick(); + expect(wrapper.emitted('moveNote')).toEqual([ + [ + { + noteId: notes[0].id, + discussionId: notes[0].discussion.id, + coordinates: newCoordinates, + }, + ], + ]); }); describe('without [repositionNote] permission', () => { @@ -262,19 +249,18 @@ describe('Design overlay component', () => { y: mockNoteNotAuthorised.position.y, }; - it('should be unable to move a note', () => { + it('should be unable to move a note', async () => { createComponent({ dimensions: mockDimensions, notes: [mockNoteNotAuthorised], }); const badge = findAllNotes().at(0); - return clickAndDragBadge(badge, { ...mockNoteCoordinates }, { x: 20, y: 20 }).then(() => { - // note position should not change after a click-and-drag attempt - expect(findFirstBadge().attributes().style).toContain( - `left: ${mockNoteCoordinates.x}px; top: ${mockNoteCoordinates.y}px;`, - ); - }); + await clickAndDragBadge(badge, { ...mockNoteCoordinates }, { x: 20, y: 20 }); + // note position should not change after a click-and-drag attempt + expect(findFirstBadge().attributes().style).toContain( + `left: ${mockNoteCoordinates.x}px; top: ${mockNoteCoordinates.y}px;`, + ); }); }); }); @@ -292,7 +278,7 @@ describe('Design overlay component', () => { }); describe('when moving the comment badge', () => { - it('should update badge style to reflect new position', () => { + it('should update badge style to reflect new position', async () => { const { position } = notes[0]; createComponent({ @@ -301,16 +287,15 @@ describe('Design overlay component', () => { }, }); - return clickAndDragBadge( + await clickAndDragBadge( findCommentBadge(), { x: position.x, y: position.y }, { x: 20, y: 20 }, - ).then(() => { - expect(findCommentBadge().attributes().style).toBe('left: 20px; top: 20px;'); - }); + ); + expect(findCommentBadge().attributes().style).toBe('left: 20px; top: 20px;'); }); - it('should update badge style when note-moving action ends', () => { + it('should update badge style when note-moving action ends', async () => { const { position } = notes[0]; createComponent({ currentCommentForm: { @@ -321,19 +306,16 @@ describe('Design overlay component', () => { const commentBadge = findCommentBadge(); const toPoint = { x: 20, y: 20 }; - return clickAndDragBadge(commentBadge, { x: position.x, y: position.y }, toPoint) - .then(() => { - commentBadge.trigger('mouseup'); - // simulates the currentCommentForm being updated in index.vue component, and - // propagated back down to this prop - wrapper.setProps({ - currentCommentForm: { height: position.height, width: position.width, ...toPoint }, - }); - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(commentBadge.attributes().style).toBe('left: 20px; top: 20px;'); - }); + await clickAndDragBadge(commentBadge, { x: position.x, y: position.y }, toPoint); + commentBadge.trigger('mouseup'); + // simulates the currentCommentForm being updated in index.vue component, and + // propagated back down to this prop + wrapper.setProps({ + currentCommentForm: { height: position.height, width: position.width, ...toPoint }, + }); + + await nextTick(); + expect(commentBadge.attributes().style).toBe('left: 20px; top: 20px;'); }); it.each` @@ -342,7 +324,7 @@ describe('Design overlay component', () => { ${'comment badge'} | ${findCommentBadge} | ${'mouseup'} `( 'should emit `openCommentForm` event when $event fired on $element element', - ({ getElementFunc, event }) => { + async ({ getElementFunc, event }) => { createComponent({ notes, currentCommentForm: { @@ -364,9 +346,8 @@ describe('Design overlay component', () => { }); getElementFunc().trigger(event); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('openCommentForm')).toEqual([[newCoordinates]]); - }); + await nextTick(); + expect(wrapper.emitted('openCommentForm')).toEqual([[newCoordinates]]); }, ); }); diff --git a/spec/frontend/design_management/components/design_presentation_spec.js b/spec/frontend/design_management/components/design_presentation_spec.js index adec9ef469d..d79dde84d46 100644 --- a/spec/frontend/design_management/components/design_presentation_spec.js +++ b/spec/frontend/design_management/components/design_presentation_spec.js @@ -74,7 +74,7 @@ describe('Design management design presentation component', () => { .mockReturnValue((childDimensions.height - viewportDimensions.height) * scrollTopPerc); } - function clickDragExplore(startCoords, endCoords, { useTouchEvents, mouseup } = {}) { + async function clickDragExplore(startCoords, endCoords, { useTouchEvents, mouseup } = {}) { const event = useTouchEvents ? { mousedown: 'touchstart', @@ -96,24 +96,17 @@ describe('Design management design presentation component', () => { clientX: startCoords.clientX, clientY: startCoords.clientY, }); - return wrapper.vm - .$nextTick() - .then(() => { - addCommentOverlay.trigger(event.mousemove, { - clientX: endCoords.clientX, - clientY: endCoords.clientY, - }); - - return nextTick(); - }) - .then(() => { - if (mouseup) { - addCommentOverlay.trigger(event.mouseup); - return nextTick(); - } + await nextTick(); + addCommentOverlay.trigger(event.mousemove, { + clientX: endCoords.clientX, + clientY: endCoords.clientY, + }); - return undefined; - }); + await nextTick(); + if (mouseup) { + addCommentOverlay.trigger(event.mouseup); + await nextTick(); + } } beforeEach(() => { @@ -125,7 +118,7 @@ describe('Design management design presentation component', () => { window.gon = originalGon; }); - it('renders image and overlay when image provided', () => { + it('renders image and overlay when image provided', async () => { createComponent( { image: 'test.jpg', @@ -134,20 +127,18 @@ describe('Design management design presentation component', () => { mockOverlayData, ); - return nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); + await nextTick(); + expect(wrapper.element).toMatchSnapshot(); }); - it('renders empty state when no image provided', () => { + it('renders empty state when no image provided', async () => { createComponent(); - return nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); + await nextTick(); + expect(wrapper.element).toMatchSnapshot(); }); - it('openCommentForm event emits correct data', () => { + it('openCommentForm event emits correct data', async () => { createComponent( { image: 'test.jpg', @@ -158,15 +149,14 @@ describe('Design management design presentation component', () => { wrapper.vm.openCommentForm({ x: 1, y: 1 }); - return nextTick().then(() => { - expect(wrapper.emitted('openCommentForm')).toEqual([ - [{ ...mockOverlayData.overlayDimensions, x: 1, y: 1 }], - ]); - }); + await nextTick(); + expect(wrapper.emitted('openCommentForm')).toEqual([ + [{ ...mockOverlayData.overlayDimensions, x: 1, y: 1 }], + ]); }); describe('currentCommentForm', () => { - it('is null when isAnnotating is false', () => { + it('is null when isAnnotating is false', async () => { createComponent( { image: 'test.jpg', @@ -175,13 +165,12 @@ describe('Design management design presentation component', () => { mockOverlayData, ); - return nextTick().then(() => { - expect(wrapper.vm.currentCommentForm).toBeNull(); - expect(wrapper.element).toMatchSnapshot(); - }); + await nextTick(); + expect(wrapper.vm.currentCommentForm).toBeNull(); + expect(wrapper.element).toMatchSnapshot(); }); - it('is null when isAnnotating is true but annotation position is falsey', () => { + it('is null when isAnnotating is true but annotation position is falsey', async () => { createComponent( { image: 'test.jpg', @@ -191,13 +180,12 @@ describe('Design management design presentation component', () => { mockOverlayData, ); - return nextTick().then(() => { - expect(wrapper.vm.currentCommentForm).toBeNull(); - expect(wrapper.element).toMatchSnapshot(); - }); + await nextTick(); + expect(wrapper.vm.currentCommentForm).toBeNull(); + expect(wrapper.element).toMatchSnapshot(); }); - it('is equal to current annotation position when isAnnotating is true', () => { + it('is equal to current annotation position when isAnnotating is true', async () => { createComponent( { image: 'test.jpg', @@ -215,15 +203,14 @@ describe('Design management design presentation component', () => { }, ); - return nextTick().then(() => { - expect(wrapper.vm.currentCommentForm).toEqual({ - x: 1, - y: 1, - width: 100, - height: 100, - }); - expect(wrapper.element).toMatchSnapshot(); + await nextTick(); + expect(wrapper.vm.currentCommentForm).toEqual({ + x: 1, + y: 1, + width: 100, + height: 100, }); + expect(wrapper.element).toMatchSnapshot(); }); }); @@ -388,7 +375,7 @@ describe('Design management design presentation component', () => { }); describe('onImageResize', () => { - beforeEach(() => { + beforeEach(async () => { createComponent( { image: 'test.jpg', @@ -401,7 +388,7 @@ describe('Design management design presentation component', () => { jest.spyOn(wrapper.vm, 'scaleZoomFocalPoint'); jest.spyOn(wrapper.vm, 'scrollToFocalPoint'); wrapper.vm.onImageResize({ width: 10, height: 10 }); - return nextTick(); + await nextTick(); }); it('sets zoom focal point on initial load', () => { @@ -409,12 +396,11 @@ describe('Design management design presentation component', () => { expect(wrapper.vm.initialLoad).toBe(false); }); - it('calls scaleZoomFocalPoint and scrollToFocalPoint after initial load', () => { + it('calls scaleZoomFocalPoint and scrollToFocalPoint after initial load', async () => { wrapper.vm.onImageResize({ width: 10, height: 10 }); - return nextTick().then(() => { - expect(wrapper.vm.scaleZoomFocalPoint).toHaveBeenCalled(); - expect(wrapper.vm.scrollToFocalPoint).toHaveBeenCalled(); - }); + await nextTick(); + expect(wrapper.vm.scaleZoomFocalPoint).toHaveBeenCalled(); + expect(wrapper.vm.scrollToFocalPoint).toHaveBeenCalled(); }); }); @@ -498,7 +484,7 @@ describe('Design management design presentation component', () => { ); }); - it('opens a comment form if design was not dragged', () => { + it('opens a comment form if design was not dragged', async () => { const addCommentOverlay = findOverlayCommentButton(); const startCoords = { clientX: 1, @@ -510,15 +496,10 @@ describe('Design management design presentation component', () => { clientY: startCoords.clientY, }); - return wrapper.vm - .$nextTick() - .then(() => { - addCommentOverlay.trigger('mouseup'); - return nextTick(); - }) - .then(() => { - expect(wrapper.emitted('openCommentForm')).toBeDefined(); - }); + await nextTick(); + addCommentOverlay.trigger('mouseup'); + await nextTick(); + expect(wrapper.emitted('openCommentForm')).toBeDefined(); }); describe('when clicking and dragging', () => { diff --git a/spec/frontend/design_management/components/design_scaler_spec.js b/spec/frontend/design_management/components/design_scaler_spec.js index 095c070e5e8..a04e2ebda5b 100644 --- a/spec/frontend/design_management/components/design_scaler_spec.js +++ b/spec/frontend/design_management/components/design_scaler_spec.js @@ -1,5 +1,6 @@ import { GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import DesignScaler from '~/design_management/components/design_scaler.vue'; describe('Design management design scaler component', () => { @@ -32,7 +33,7 @@ describe('Design management design scaler component', () => { describe('when `scale` value is greater than 1', () => { beforeEach(async () => { setScale(1.6); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('emits @scale event when "reset" button clicked', () => { @@ -68,11 +69,11 @@ describe('Design management design scaler component', () => { it('computes & increments correct stepSize based on maxScale', async () => { wrapper.setProps({ maxScale: 11 }); - await wrapper.vm.$nextTick(); + await nextTick(); getIncreaseScaleButton().vm.$emit('click'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted().scale[0][0]).toBe(3); }); @@ -96,7 +97,7 @@ describe('Design management design scaler component', () => { describe('when `scale` value is maximum', () => { beforeEach(async () => { setScale(2); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('disables the "increment" button', () => { diff --git a/spec/frontend/design_management/components/design_sidebar_spec.js b/spec/frontend/design_management/components/design_sidebar_spec.js index 4cd71bdb7f3..f690974db1b 100644 --- a/spec/frontend/design_management/components/design_sidebar_spec.js +++ b/spec/frontend/design_management/components/design_sidebar_spec.js @@ -1,6 +1,7 @@ import { GlCollapse, GlPopover } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Cookies from 'js-cookie'; +import { nextTick } from 'vue'; import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue'; import DesignNoteSignedOut from '~/design_management/components/design_notes/design_note_signed_out.vue'; import DesignSidebar from '~/design_management/components/design_sidebar.vue'; @@ -138,14 +139,13 @@ describe('Design management design sidebar component', () => { expect(wrapper.emitted('toggleResolvedComments')).toHaveLength(1); }); - it('opens a collapsible when resolvedDiscussionsExpanded prop changes to true', () => { + it('opens a collapsible when resolvedDiscussionsExpanded prop changes to true', async () => { expect(findCollapsible().attributes('visible')).toBeUndefined(); wrapper.setProps({ resolvedDiscussionsExpanded: true, }); - return wrapper.vm.$nextTick().then(() => { - expect(findCollapsible().attributes('visible')).toBe('true'); - }); + await nextTick(); + expect(findCollapsible().attributes('visible')).toBe('true'); }); it('does not popover about resolved comments', () => { @@ -182,12 +182,11 @@ describe('Design management design sidebar component', () => { expect(wrapper.emitted('resolveDiscussionError')).toEqual([['payload']]); }); - it('changes prop correctly on opening discussion form', () => { + it('changes prop correctly on opening discussion form', async () => { findFirstDiscussion().vm.$emit('open-form', 'some-id'); - return wrapper.vm.$nextTick().then(() => { - expect(findFirstDiscussion().props('discussionWithOpenForm')).toBe('some-id'); - }); + await nextTick(); + expect(findFirstDiscussion().props('discussionWithOpenForm')).toBe('some-id'); }); }); @@ -246,11 +245,10 @@ describe('Design management design sidebar component', () => { expect(scrollIntoViewMock).toHaveBeenCalled(); }); - it('dismisses a popover on the outside click', () => { + it('dismisses a popover on the outside click', async () => { wrapper.trigger('click'); - return wrapper.vm.$nextTick(() => { - expect(findPopover().exists()).toBe(false); - }); + await nextTick(); + expect(findPopover().exists()).toBe(false); }); it(`sets a ${cookieKey} cookie on clicking outside the popover`, () => { diff --git a/spec/frontend/design_management/components/design_todo_button_spec.js b/spec/frontend/design_management/components/design_todo_button_spec.js index 757bf50c527..73661c9fcb0 100644 --- a/spec/frontend/design_management/components/design_todo_button_spec.js +++ b/spec/frontend/design_management/components/design_todo_button_spec.js @@ -1,4 +1,5 @@ import { shallowMount, mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import DesignTodoButton from '~/design_management/components/design_todo_button.vue'; import createDesignTodoMutation from '~/design_management/graphql/mutations/create_design_todo.mutation.graphql'; import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql'; @@ -71,7 +72,7 @@ describe('Design management design todo button', () => { describe('when clicked', () => { let dispatchEventSpy; - beforeEach(() => { + beforeEach(async () => { dispatchEventSpy = jest.spyOn(document, 'dispatchEvent'); jest.spyOn(document, 'querySelector').mockReturnValue({ innerText: 2, @@ -79,7 +80,7 @@ describe('Design management design todo button', () => { createComponent({ design: mockDesignWithPendingTodos }, { mountFn: mount }); wrapper.trigger('click'); - return wrapper.vm.$nextTick(); + await nextTick(); }); it('calls `$apollo.mutate` with the `todoMarkDone` mutation and variables containing `id`', async () => { @@ -117,7 +118,7 @@ describe('Design management design todo button', () => { describe('when clicked', () => { let dispatchEventSpy; - beforeEach(() => { + beforeEach(async () => { dispatchEventSpy = jest.spyOn(document, 'dispatchEvent'); jest.spyOn(document, 'querySelector').mockReturnValue({ innerText: 2, @@ -125,7 +126,7 @@ describe('Design management design todo button', () => { createComponent({}, { mountFn: mount }); wrapper.trigger('click'); - return wrapper.vm.$nextTick(); + await nextTick(); }); it('calls `$apollo.mutate` with the `createDesignTodoMutation` mutation and variables containing `issuable_id`, `issue_id`, & `projectPath`', async () => { diff --git a/spec/frontend/design_management/components/image_spec.js b/spec/frontend/design_management/components/image_spec.js index ac3afc73c86..e27b2bc9fa5 100644 --- a/spec/frontend/design_management/components/image_spec.js +++ b/spec/frontend/design_management/components/image_spec.js @@ -1,5 +1,6 @@ import { GlIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import DesignImage from '~/design_management/components/image.vue'; describe('Design management large image component', () => { @@ -36,7 +37,7 @@ describe('Design management large image component', () => { expect(wrapper.element).toMatchSnapshot(); }); - it('sets correct classes and styles if imageStyle is set', () => { + it('sets correct classes and styles if imageStyle is set', async () => { createComponent( { isLoading: false, @@ -50,12 +51,11 @@ describe('Design management large image component', () => { }, }, ); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); + await nextTick(); + expect(wrapper.element).toMatchSnapshot(); }); - it('renders media broken icon on error', () => { + it('renders media broken icon on error', async () => { createComponent({ isLoading: false, image: 'test.jpg', @@ -64,10 +64,9 @@ describe('Design management large image component', () => { const image = wrapper.find('img'); image.trigger('error'); - return wrapper.vm.$nextTick().then(() => { - expect(image.isVisible()).toBe(false); - expect(wrapper.find(GlIcon).element).toMatchSnapshot(); - }); + await nextTick(); + expect(image.isVisible()).toBe(false); + expect(wrapper.find(GlIcon).element).toMatchSnapshot(); }); describe('zoom', () => { @@ -99,37 +98,34 @@ describe('Design management large image component', () => { .mockReturnValue(baseImageHeight); }); - it('emits @resize event on zoom', () => { + it('emits @resize event on zoom', async () => { const zoomAmount = 2; wrapper.vm.zoom(zoomAmount); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('resize')).toEqual([ - [{ width: baseImageWidth * zoomAmount, height: baseImageHeight * zoomAmount }], - ]); - }); + await nextTick(); + expect(wrapper.emitted('resize')).toEqual([ + [{ width: baseImageWidth * zoomAmount, height: baseImageHeight * zoomAmount }], + ]); }); - it('emits @resize event with base image size when scale=1', () => { + it('emits @resize event with base image size when scale=1', async () => { wrapper.vm.zoom(1); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('resize')).toEqual([ - [{ width: baseImageWidth, height: baseImageHeight }], - ]); - }); + await nextTick(); + expect(wrapper.emitted('resize')).toEqual([ + [{ width: baseImageWidth, height: baseImageHeight }], + ]); }); - it('sets image style when zoomed', () => { + it('sets image style when zoomed', async () => { const zoomAmount = 2; wrapper.vm.zoom(zoomAmount); expect(wrapper.vm.imageStyle).toEqual({ width: `${baseImageWidth * zoomAmount}px`, height: `${baseImageHeight * zoomAmount}px`, }); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); + await nextTick(); + expect(wrapper.element).toMatchSnapshot(); }); }); }); diff --git a/spec/frontend/design_management/components/list/item_spec.js b/spec/frontend/design_management/components/list/item_spec.js index 92cb35bd8d6..e00dda2015e 100644 --- a/spec/frontend/design_management/components/list/item_spec.js +++ b/spec/frontend/design_management/components/list/item_spec.js @@ -1,6 +1,6 @@ import { GlIcon, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import VueRouter from 'vue-router'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import Item from '~/design_management/components/list/item.vue'; @@ -71,13 +71,13 @@ describe('Design management list item component', () => { let image; let glIntersectionObserver; - beforeEach(() => { + beforeEach(async () => { createComponent(); image = wrapper.find('img'); glIntersectionObserver = wrapper.find(GlIntersectionObserver); glIntersectionObserver.vm.$emit('appear'); - return wrapper.vm.$nextTick(); + await nextTick(); }); it('renders a tooltip', () => { @@ -91,9 +91,9 @@ describe('Design management list item component', () => { }); describe('after image is loaded', () => { - beforeEach(() => { + beforeEach(async () => { image.trigger('load'); - return wrapper.vm.$nextTick(); + await nextTick(); }); it('renders an image', () => { @@ -101,29 +101,27 @@ describe('Design management list item component', () => { expect(image.isVisible()).toBe(true); }); - it('renders media broken icon when image onerror triggered', () => { + it('renders media broken icon when image onerror triggered', async () => { image.trigger('error'); - return wrapper.vm.$nextTick().then(() => { - expect(image.isVisible()).toBe(false); - expect(wrapper.find(GlIcon).element).toMatchSnapshot(); - }); + await nextTick(); + expect(image.isVisible()).toBe(false); + expect(wrapper.find(GlIcon).element).toMatchSnapshot(); }); describe('when imageV432x230 and image provided', () => { - it('renders imageV432x230 image', () => { + it('renders imageV432x230 image', async () => { const mockSrc = 'mock-imageV432x230-url'; wrapper.setProps({ imageV432x230: mockSrc }); - return wrapper.vm.$nextTick().then(() => { - expect(image.attributes('src')).toBe(mockSrc); - }); + await nextTick(); + expect(image.attributes('src')).toBe(mockSrc); }); }); describe('when image disappears from view and then reappears', () => { - beforeEach(() => { + beforeEach(async () => { glIntersectionObserver.vm.$emit('appear'); - return wrapper.vm.$nextTick(); + await nextTick(); }); it('renders an image', () => { diff --git a/spec/frontend/design_management/components/toolbar/design_navigation_spec.js b/spec/frontend/design_management/components/toolbar/design_navigation_spec.js index 6e0592984a2..38a7fadee79 100644 --- a/spec/frontend/design_management/components/toolbar/design_navigation_spec.js +++ b/spec/frontend/design_management/components/toolbar/design_navigation_spec.js @@ -1,6 +1,7 @@ /* global Mousetrap */ import 'mousetrap'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import DesignNavigation from '~/design_management/components/toolbar/design_navigation.vue'; import { DESIGN_ROUTE_NAME } from '~/design_management/router/constants'; @@ -41,16 +42,15 @@ describe('Design management pagination component', () => { expect(wrapper.element).toMatchSnapshot(); }); - it('renders navigation buttons', () => { + it('renders navigation buttons', 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({ designCollection: { designs: [{ id: '1' }, { id: '2' }] }, }); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); + await nextTick(); + expect(wrapper.element).toMatchSnapshot(); }); describe('keyboard buttons navigation', () => { diff --git a/spec/frontend/design_management/components/toolbar/index_spec.js b/spec/frontend/design_management/components/toolbar/index_spec.js index 804b55bda38..412f3de911e 100644 --- a/spec/frontend/design_management/components/toolbar/index_spec.js +++ b/spec/frontend/design_management/components/toolbar/index_spec.js @@ -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 VueRouter from 'vue-router'; import DeleteButton from '~/design_management/components/delete_button.vue'; import Toolbar from '~/design_management/components/toolbar/index.vue'; @@ -60,60 +60,54 @@ describe('Design management toolbar component', () => { wrapper.destroy(); }); - it('renders design and updated data', () => { + it('renders design and updated data', async () => { createComponent(); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); + await nextTick(); + expect(wrapper.element).toMatchSnapshot(); }); - it('links back to designs list', () => { + it('links back to designs list', async () => { createComponent(); - return wrapper.vm.$nextTick().then(() => { - const link = wrapper.find('a'); + await nextTick(); + const link = wrapper.find('a'); - expect(link.props('to')).toEqual({ - name: DESIGNS_ROUTE_NAME, - query: { - version: undefined, - }, - }); + expect(link.props('to')).toEqual({ + name: DESIGNS_ROUTE_NAME, + query: { + version: undefined, + }, }); }); - it('renders delete button on latest designs version with logged in user', () => { + it('renders delete button on latest designs version with logged in user', async () => { createComponent(); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(DeleteButton).exists()).toBe(true); - }); + await nextTick(); + expect(wrapper.find(DeleteButton).exists()).toBe(true); }); - it('does not render delete button on non-latest version', () => { + it('does not render delete button on non-latest version', async () => { createComponent(false, true, { isLatestVersion: false }); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(DeleteButton).exists()).toBe(false); - }); + await nextTick(); + expect(wrapper.find(DeleteButton).exists()).toBe(false); }); - it('does not render delete button when user is not logged in', () => { + it('does not render delete button when user is not logged in', async () => { createComponent(false, false); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(DeleteButton).exists()).toBe(false); - }); + await nextTick(); + expect(wrapper.find(DeleteButton).exists()).toBe(false); }); - it('emits `delete` event on deleteButton `delete-selected-designs` event', () => { + it('emits `delete` event on deleteButton `delete-selected-designs` event', async () => { createComponent(); - return wrapper.vm.$nextTick().then(() => { - wrapper.find(DeleteButton).vm.$emit('delete-selected-designs'); - expect(wrapper.emitted().delete).toBeTruthy(); - }); + await nextTick(); + wrapper.find(DeleteButton).vm.$emit('delete-selected-designs'); + expect(wrapper.emitted().delete).toBeTruthy(); }); it('renders download button with correct link', () => { diff --git a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js index a4fb671ae13..ec5db04bb80 100644 --- a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js +++ b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js @@ -1,5 +1,6 @@ import { GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import DesignVersionDropdown from '~/design_management/components/upload/design_version_dropdown.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import mockAllVersions from './mock_data/all_versions'; @@ -47,77 +48,69 @@ describe('Design management design version dropdown component', () => { const findVersionLink = (index) => wrapper.findAll(GlDropdownItem).at(index); - it('renders design version dropdown button', () => { + it('renders design version dropdown button', async () => { createComponent(); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); + await nextTick(); + expect(wrapper.element).toMatchSnapshot(); }); - it('renders design version list', () => { + it('renders design version list', async () => { createComponent(); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); + await nextTick(); + expect(wrapper.element).toMatchSnapshot(); }); describe('selected version name', () => { - it('has "latest" on most recent version item', () => { + it('has "latest" on most recent version item', async () => { createComponent(); - return wrapper.vm.$nextTick().then(() => { - expect(findVersionLink(0).text()).toContain('latest'); - }); + await nextTick(); + expect(findVersionLink(0).text()).toContain('latest'); }); }); describe('versions list', () => { - it('displays latest version text by default', () => { + it('displays latest version text by default', async () => { createComponent(); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version'); - }); + await nextTick(); + expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version'); }); - it('displays latest version text when only 1 version is present', () => { + it('displays latest version text when only 1 version is present', async () => { createComponent({ maxVersions: 1 }); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version'); - }); + await nextTick(); + expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version'); }); - it('displays version text when the current version is not the latest', () => { + it('displays version text when the current version is not the latest', async () => { createComponent({ $route: designRouteFactory(PREVIOUS_VERSION_ID) }); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(GlDropdown).attributes('text')).toBe(`Showing version #1`); - }); + await nextTick(); + expect(wrapper.find(GlDropdown).attributes('text')).toBe(`Showing version #1`); }); - it('displays latest version text when the current version is the latest', () => { + it('displays latest version text when the current version is the latest', async () => { createComponent({ $route: designRouteFactory(LATEST_VERSION_ID) }); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version'); - }); + await nextTick(); + expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version'); }); - it('should have the same length as apollo query', () => { + it('should have the same length as apollo query', async () => { createComponent(); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.findAll(GlDropdownItem)).toHaveLength(wrapper.vm.allVersions.length); - }); + await nextTick(); + expect(wrapper.findAll(GlDropdownItem)).toHaveLength(wrapper.vm.allVersions.length); }); it('should render TimeAgo', async () => { createComponent(); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.findAllComponents(TimeAgo)).toHaveLength(wrapper.vm.allVersions.length); }); diff --git a/spec/frontend/design_management/pages/design/index_spec.js b/spec/frontend/design_management/pages/design/index_spec.js index 4477bf4b8e6..55d0fabe402 100644 --- a/spec/frontend/design_management/pages/design/index_spec.js +++ b/spec/frontend/design_management/pages/design/index_spec.js @@ -1,6 +1,6 @@ import { GlAlert } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import { ApolloMutation } from 'vue-apollo'; import VueRouter from 'vue-router'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; @@ -153,11 +153,11 @@ describe('Design management design index page', () => { jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockPageLayoutElement); createComponent({ loading: false }, { data: { design, scale: 2 } }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findDesignPresentation().props('scale')).toBe(2); DesignIndex.beforeRouteUpdate.call(wrapper.vm, {}, {}, jest.fn()); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findDesignPresentation().props('scale')).toBe(1); }); @@ -200,7 +200,7 @@ describe('Design management design index page', () => { }); }); - it('opens a new discussion form', () => { + it('opens a new discussion form', async () => { createComponent( { loading: false }, { @@ -212,9 +212,8 @@ describe('Design management design index page', () => { findDesignPresentation().vm.$emit('openCommentForm', { x: 0, y: 0 }); - return wrapper.vm.$nextTick().then(() => { - expect(findDiscussionForm().exists()).toBe(true); - }); + await nextTick(); + expect(findDiscussionForm().exists()).toBe(true); }); it('keeps new discussion form focused', () => { @@ -233,7 +232,7 @@ describe('Design management design index page', () => { expect(focusInput).toHaveBeenCalled(); }); - it('sends a mutation on submitting form and closes form', () => { + it('sends a mutation on submitting form and closes form', async () => { createComponent( { loading: false }, { @@ -248,17 +247,12 @@ describe('Design management design index page', () => { findDiscussionForm().vm.$emit('submit-form'); expect(mutate).toHaveBeenCalledWith(createDiscussionMutationVariables); - return wrapper.vm - .$nextTick() - .then(() => { - return mutate({ variables: createDiscussionMutationVariables }); - }) - .then(() => { - expect(findDiscussionForm().exists()).toBe(false); - }); + await nextTick(); + await mutate({ variables: createDiscussionMutationVariables }); + expect(findDiscussionForm().exists()).toBe(false); }); - it('closes the form and clears the comment on canceling form', () => { + it('closes the form and clears the comment on canceling form', async () => { createComponent( { loading: false }, { @@ -274,9 +268,8 @@ describe('Design management design index page', () => { expect(wrapper.vm.comment).toBe(''); - return wrapper.vm.$nextTick().then(() => { - expect(findDiscussionForm().exists()).toBe(false); - }); + await nextTick(); + expect(findDiscussionForm().exists()).toBe(false); }); describe('with error', () => { @@ -299,22 +292,21 @@ describe('Design management design index page', () => { describe('onDesignQueryResult', () => { describe('with no designs', () => { - it('redirects to /designs', () => { + it('redirects to /designs', async () => { createComponent({ loading: true }); router.push = jest.fn(); wrapper.vm.onDesignQueryResult({ data: mockResponseNoDesigns, loading: false }); - return wrapper.vm.$nextTick().then(() => { - expect(createFlash).toHaveBeenCalledTimes(1); - expect(createFlash).toHaveBeenCalledWith({ message: DESIGN_NOT_FOUND_ERROR }); - expect(router.push).toHaveBeenCalledTimes(1); - expect(router.push).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME }); - }); + await nextTick(); + expect(createFlash).toHaveBeenCalledTimes(1); + expect(createFlash).toHaveBeenCalledWith({ message: DESIGN_NOT_FOUND_ERROR }); + expect(router.push).toHaveBeenCalledTimes(1); + expect(router.push).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME }); }); }); describe('when no design exists for given version', () => { - it('redirects to /designs', () => { + it('redirects to /designs', async () => { createComponent({ loading: true }); // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax @@ -327,12 +319,11 @@ describe('Design management design index page', () => { router.push = jest.fn(); wrapper.vm.onDesignQueryResult({ data: mockResponseWithDesigns, loading: false }); - return wrapper.vm.$nextTick().then(() => { - expect(createFlash).toHaveBeenCalledTimes(1); - expect(createFlash).toHaveBeenCalledWith({ message: DESIGN_VERSION_NOT_EXIST_ERROR }); - expect(router.push).toHaveBeenCalledTimes(1); - expect(router.push).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME }); - }); + await nextTick(); + expect(createFlash).toHaveBeenCalledTimes(1); + expect(createFlash).toHaveBeenCalledWith({ message: DESIGN_VERSION_NOT_EXIST_ERROR }); + expect(router.push).toHaveBeenCalledTimes(1); + expect(router.push).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME }); }); }); }); diff --git a/spec/frontend/diffs/components/settings_dropdown_spec.js b/spec/frontend/diffs/components/settings_dropdown_spec.js index 2dd35519464..693fc5bfd8f 100644 --- a/spec/frontend/diffs/components/settings_dropdown_spec.js +++ b/spec/frontend/diffs/components/settings_dropdown_spec.js @@ -1,5 +1,6 @@ import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import SettingsDropdown from '~/diffs/components/settings_dropdown.vue'; @@ -10,7 +11,6 @@ import createDiffsStore from '../create_diffs_store'; describe('Diff settings dropdown component', () => { let wrapper; - let vm; let store; function createComponent(extendStore = () => {}) { @@ -23,7 +23,6 @@ describe('Diff settings dropdown component', () => { store, }), ); - vm = wrapper.vm; } function getFileByFileCheckbox(vueWrapper) { @@ -142,7 +141,7 @@ describe('Diff settings dropdown component', () => { checkbox.trigger('click'); - await vm.$nextTick(); + await nextTick(); expect(store.dispatch).toHaveBeenCalledWith('diffs/setShowWhitespace', { showWhitespace: !checked, @@ -185,7 +184,7 @@ describe('Diff settings dropdown component', () => { getFileByFileCheckbox(wrapper).trigger('click'); - await vm.$nextTick(); + await nextTick(); expect(store.dispatch).toHaveBeenCalledWith('diffs/setFileByFile', { fileByFile: setting, diff --git a/spec/frontend/filtered_search/recent_searches_root_spec.js b/spec/frontend/filtered_search/recent_searches_root_spec.js index fa3267c98a1..59c428fb3fa 100644 --- a/spec/frontend/filtered_search/recent_searches_root_spec.js +++ b/spec/frontend/filtered_search/recent_searches_root_spec.js @@ -1,3 +1,4 @@ +import { nextTick } from 'vue'; import { setHTMLFixture } from 'helpers/fixtures'; import RecentSearchesRoot from '~/filtered_search/recent_searches_root'; @@ -10,7 +11,7 @@ describe('RecentSearchesRoot', () => { let vm; let containerEl; - beforeEach(() => { + beforeEach(async () => { setHTMLFixture(` <div id="${containerId}"> <div id="${dropdownElementId}"></div> @@ -33,7 +34,7 @@ describe('RecentSearchesRoot', () => { RecentSearchesRoot.prototype.render.call(recentSearchesRootMockInstance); vm = recentSearchesRootMockInstance.vm; - return vm.$nextTick(); + await nextTick(); }); afterEach(() => { diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js index bc8c6460cf4..630878f80f3 100644 --- a/spec/frontend/groups/components/app_spec.js +++ b/spec/frontend/groups/components/app_spec.js @@ -1,7 +1,7 @@ import { GlModal, GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import AxiosMockAdapter from 'axios-mock-adapter'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; import appComponent from '~/groups/components/app.vue'; @@ -58,7 +58,7 @@ describe('AppComponent', () => { wrapper = null; }); - beforeEach(() => { + beforeEach(async () => { mock = new AxiosMockAdapter(axios); mock.onGet('/dashboard/groups.json').reply(200, mockGroups); Vue.component('GroupFolder', groupFolderComponent); @@ -66,7 +66,7 @@ describe('AppComponent', () => { createShallowComponent(); getGroupsSpy = jest.spyOn(vm.service, 'getGroups'); - return vm.$nextTick(); + await nextTick(); }); describe('computed', () => { @@ -397,66 +397,60 @@ describe('AppComponent', () => { }); describe('created', () => { - it('should bind event listeners on eventHub', () => { + it('should bind event listeners on eventHub', async () => { jest.spyOn(eventHub, '$on').mockImplementation(() => {}); createShallowComponent(); - return vm.$nextTick().then(() => { - expect(eventHub.$on).toHaveBeenCalledWith('fetchPage', expect.any(Function)); - expect(eventHub.$on).toHaveBeenCalledWith('toggleChildren', expect.any(Function)); - expect(eventHub.$on).toHaveBeenCalledWith('showLeaveGroupModal', expect.any(Function)); - expect(eventHub.$on).toHaveBeenCalledWith('updatePagination', expect.any(Function)); - expect(eventHub.$on).toHaveBeenCalledWith('updateGroups', expect.any(Function)); - }); + await nextTick(); + expect(eventHub.$on).toHaveBeenCalledWith('fetchPage', expect.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('toggleChildren', expect.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('showLeaveGroupModal', expect.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('updatePagination', expect.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('updateGroups', expect.any(Function)); }); - it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `false`', () => { + it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `false`', async () => { createShallowComponent(); - return vm.$nextTick().then(() => { - expect(vm.searchEmptyMessage).toBe('No groups or projects matched your search'); - }); + await nextTick(); + expect(vm.searchEmptyMessage).toBe('No groups or projects matched your search'); }); - it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `true`', () => { + it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `true`', async () => { createShallowComponent(true); - return vm.$nextTick().then(() => { - expect(vm.searchEmptyMessage).toBe('No groups matched your search'); - }); + await nextTick(); + expect(vm.searchEmptyMessage).toBe('No groups matched your search'); }); }); describe('beforeDestroy', () => { - it('should unbind event listeners on eventHub', () => { + it('should unbind event listeners on eventHub', async () => { jest.spyOn(eventHub, '$off').mockImplementation(() => {}); createShallowComponent(); wrapper.destroy(); - return vm.$nextTick().then(() => { - expect(eventHub.$off).toHaveBeenCalledWith('fetchPage', expect.any(Function)); - expect(eventHub.$off).toHaveBeenCalledWith('toggleChildren', expect.any(Function)); - expect(eventHub.$off).toHaveBeenCalledWith('showLeaveGroupModal', expect.any(Function)); - expect(eventHub.$off).toHaveBeenCalledWith('updatePagination', expect.any(Function)); - expect(eventHub.$off).toHaveBeenCalledWith('updateGroups', expect.any(Function)); - }); + await nextTick(); + expect(eventHub.$off).toHaveBeenCalledWith('fetchPage', expect.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('toggleChildren', expect.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('showLeaveGroupModal', expect.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('updatePagination', expect.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('updateGroups', expect.any(Function)); }); }); describe('template', () => { - it('should render loading icon', () => { + it('should render loading icon', async () => { vm.isLoading = true; - return vm.$nextTick().then(() => { - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); - }); + await nextTick(); + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); }); - it('should render groups tree', () => { + it('should render groups tree', async () => { vm.store.state.groups = [mockParentGroupItem]; vm.isLoading = false; - return vm.$nextTick().then(() => { - expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined(); - }); + await nextTick(); + expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined(); }); it('renders modal confirmation dialog', () => { diff --git a/spec/frontend/groups/components/groups_spec.js b/spec/frontend/groups/components/groups_spec.js index 0ec1ef5a49e..590b4fb3d57 100644 --- a/spec/frontend/groups/components/groups_spec.js +++ b/spec/frontend/groups/components/groups_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import mountComponent from 'helpers/vue_mount_component_helper'; import groupFolderComponent from '~/groups/components/group_folder.vue'; @@ -21,13 +21,13 @@ const createComponent = (searchEmpty = false) => { describe('GroupsComponent', () => { let vm; - beforeEach(() => { + beforeEach(async () => { Vue.component('GroupFolder', groupFolderComponent); Vue.component('GroupItem', groupItemComponent); vm = createComponent(); - return vm.$nextTick(); + await nextTick(); }); afterEach(() => { @@ -52,20 +52,18 @@ describe('GroupsComponent', () => { }); describe('template', () => { - it('should render component template correctly', () => { - return vm.$nextTick().then(() => { - expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined(); - expect(vm.$el.querySelector('.group-list-tree')).toBeDefined(); - expect(vm.$el.querySelector('.gl-pagination')).toBeDefined(); - expect(vm.$el.querySelectorAll('.has-no-search-results').length).toBe(0); - }); + it('should render component template correctly', async () => { + await nextTick(); + expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined(); + expect(vm.$el.querySelector('.group-list-tree')).toBeDefined(); + expect(vm.$el.querySelector('.gl-pagination')).toBeDefined(); + expect(vm.$el.querySelectorAll('.has-no-search-results').length).toBe(0); }); - it('should render empty search message when `searchEmpty` is `true`', () => { + it('should render empty search message when `searchEmpty` is `true`', async () => { vm.searchEmpty = true; - return vm.$nextTick().then(() => { - expect(vm.$el.querySelector('.has-no-search-results')).toBeDefined(); - }); + await nextTick(); + expect(vm.$el.querySelector('.has-no-search-results')).toBeDefined(); }); }); }); diff --git a/spec/frontend/ide/components/activity_bar_spec.js b/spec/frontend/ide/components/activity_bar_spec.js index 657817eb3d8..39fe2c7e723 100644 --- a/spec/frontend/ide/components/activity_bar_spec.js +++ b/spec/frontend/ide/components/activity_bar_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; import ActivityBar from '~/ide/components/activity_bar.vue'; import { leftSidebarViews } from '~/ide/constants'; @@ -61,14 +61,11 @@ describe('IDE activity bar', () => { expect(vm.$el.querySelector('.js-ide-edit-mode').classList).toContain('active'); }); - it('sets commit item active', (done) => { + it('sets commit item active', async () => { vm.$store.state.currentActivityView = leftSidebarViews.commit.name; - vm.$nextTick(() => { - expect(vm.$el.querySelector('.js-ide-commit-mode').classList).toContain('active'); - - done(); - }); + await nextTick(); + expect(vm.$el.querySelector('.js-ide-commit-mode').classList).toContain('active'); }); }); diff --git a/spec/frontend/ide/components/commit_sidebar/actions_spec.js b/spec/frontend/ide/components/commit_sidebar/actions_spec.js index ed9d11246ae..c9425f6c9cd 100644 --- a/spec/frontend/ide/components/commit_sidebar/actions_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/actions_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; import { projectData, branches } from 'jest/ide/mock_data'; import commitActions from '~/ide/components/commit_sidebar/actions.vue'; @@ -71,15 +71,12 @@ describe('IDE commit sidebar actions', () => { expect(findText()).toContain('Commit to main branch'); }); - it('hides merge request option when project merge requests are disabled', (done) => { + it('hides merge request option when project merge requests are disabled', async () => { createComponent({ hasMR: false }); - vm.$nextTick(() => { - expect(findRadios().length).toBe(2); - expect(findText()).not.toContain('Create a new branch and merge request'); - - done(); - }); + await nextTick(); + expect(findRadios().length).toBe(2); + expect(findText()).not.toContain('Create a new branch and merge request'); }); describe('currentBranchText', () => { @@ -105,22 +102,18 @@ describe('IDE commit sidebar actions', () => { expect(vm.$store.dispatch).not.toHaveBeenCalled(); }); - it('calls again after staged changes', (done) => { + it('calls again after staged changes', async () => { createComponent({ currentBranchId: null }); vm.$store.state.currentBranchId = 'main'; vm.$store.state.changedFiles.push({}); vm.$store.state.stagedFiles.push({}); - vm.$nextTick() - .then(() => { - expect(vm.$store.dispatch).toHaveBeenCalledWith( - ACTION_UPDATE_COMMIT_ACTION, - expect.anything(), - ); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(vm.$store.dispatch).toHaveBeenCalledWith( + ACTION_UPDATE_COMMIT_ACTION, + expect.anything(), + ); }); it.each` diff --git a/spec/frontend/ide/components/commit_sidebar/list_item_spec.js b/spec/frontend/ide/components/commit_sidebar/list_item_spec.js index b91ee88e0d6..78810da7f2c 100644 --- a/spec/frontend/ide/components/commit_sidebar/list_item_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/list_item_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import { trimText } from 'helpers/text_helper'; import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; import listItem from '~/ide/components/commit_sidebar/list_item.vue'; @@ -41,26 +41,18 @@ describe('Multi-file editor commit sidebar list item', () => { expect(findPathText()).toContain(f.path); }); - it('correctly renders renamed entries', (done) => { + it('correctly renders renamed entries', async () => { Vue.set(vm.file, 'prevName', 'Old name'); - vm.$nextTick() - .then(() => { - expect(findPathText()).toEqual(`Old name → ${f.name}`); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(findPathText()).toEqual(`Old name → ${f.name}`); }); - it('correctly renders entry, the name of which did not change after rename (as within a folder)', (done) => { + it('correctly renders entry, the name of which did not change after rename (as within a folder)', async () => { Vue.set(vm.file, 'prevName', f.name); - vm.$nextTick() - .then(() => { - expect(findPathText()).toEqual(f.name); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(findPathText()).toEqual(f.name); }); it('opens a closed file in the editor when clicking the file path', (done) => { @@ -134,14 +126,11 @@ describe('Multi-file editor commit sidebar list item', () => { expect(vm.$el.querySelector('.is-active')).toBe(null); }); - it('adds active class when keys match', (done) => { + it('adds active class when keys match', async () => { vm.keyPrefix = 'staged'; - vm.$nextTick(() => { - expect(vm.$el.querySelector('.is-active')).not.toBe(null); - - done(); - }); + await nextTick(); + expect(vm.$el.querySelector('.is-active')).not.toBe(null); }); }); }); diff --git a/spec/frontend/ide/components/commit_sidebar/message_field_spec.js b/spec/frontend/ide/components/commit_sidebar/message_field_spec.js index 1514fbc2c3b..e66de6bb0b0 100644 --- a/spec/frontend/ide/components/commit_sidebar/message_field_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/message_field_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import createComponent from 'helpers/vue_mount_component_helper'; import CommitMessageField from '~/ide/components/commit_sidebar/message_field.vue'; @@ -23,34 +23,23 @@ describe('IDE commit message field', () => { vm.$destroy(); }); - it('adds is-focused class on focus', (done) => { + it('adds is-focused class on focus', async () => { vm.$el.querySelector('textarea').focus(); - vm.$nextTick(() => { - expect(vm.$el.querySelector('.is-focused')).not.toBeNull(); - - done(); - }); + await nextTick(); + expect(vm.$el.querySelector('.is-focused')).not.toBeNull(); }); - it('removed is-focused class on blur', (done) => { + it('removed is-focused class on blur', async () => { vm.$el.querySelector('textarea').focus(); - vm.$nextTick() - .then(() => { - expect(vm.$el.querySelector('.is-focused')).not.toBeNull(); - - vm.$el.querySelector('textarea').blur(); + await nextTick(); + expect(vm.$el.querySelector('.is-focused')).not.toBeNull(); - return vm.$nextTick(); - }) - .then(() => { - expect(vm.$el.querySelector('.is-focused')).toBeNull(); + vm.$el.querySelector('textarea').blur(); - done(); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(vm.$el.querySelector('.is-focused')).toBeNull(); }); it('emits input event on input', () => { @@ -66,105 +55,78 @@ describe('IDE commit message field', () => { describe('highlights', () => { describe('subject line', () => { - it('does not highlight less than 50 characters', (done) => { + it('does not highlight less than 50 characters', async () => { vm.text = 'text less than 50 chars'; - vm.$nextTick() - .then(() => { - expect(vm.$el.querySelector('.highlights span').textContent).toContain( - 'text less than 50 chars', - ); + await nextTick(); + expect(vm.$el.querySelector('.highlights span').textContent).toContain( + 'text less than 50 chars', + ); - expect(vm.$el.querySelector('mark').style.display).toBe('none'); - }) - .then(done) - .catch(done.fail); + expect(vm.$el.querySelector('mark').style.display).toBe('none'); }); - it('highlights characters over 50 length', (done) => { + it('highlights characters over 50 length', async () => { vm.text = 'text less than 50 chars that should not highlighted. text more than 50 should be highlighted'; - vm.$nextTick() - .then(() => { - expect(vm.$el.querySelector('.highlights span').textContent).toContain( - 'text less than 50 chars that should not highlighte', - ); - - expect(vm.$el.querySelector('mark').style.display).not.toBe('none'); - expect(vm.$el.querySelector('mark').textContent).toBe( - 'd. text more than 50 should be highlighted', - ); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(vm.$el.querySelector('.highlights span').textContent).toContain( + 'text less than 50 chars that should not highlighte', + ); + + expect(vm.$el.querySelector('mark').style.display).not.toBe('none'); + expect(vm.$el.querySelector('mark').textContent).toBe( + 'd. text more than 50 should be highlighted', + ); }); }); describe('body text', () => { - it('does not highlight body text less tan 72 characters', (done) => { + it('does not highlight body text less tan 72 characters', async () => { vm.text = 'subject line\nbody content'; - vm.$nextTick() - .then(() => { - expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2); - expect(vm.$el.querySelectorAll('mark')[1].style.display).toBe('none'); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2); + expect(vm.$el.querySelectorAll('mark')[1].style.display).toBe('none'); }); - it('highlights body text more than 72 characters', (done) => { + it('highlights body text more than 72 characters', async () => { vm.text = 'subject line\nbody content that will be highlighted when it is more than 72 characters in length'; - vm.$nextTick() - .then(() => { - expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2); - expect(vm.$el.querySelectorAll('mark')[1].style.display).not.toBe('none'); - expect(vm.$el.querySelectorAll('mark')[1].textContent).toBe(' in length'); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2); + expect(vm.$el.querySelectorAll('mark')[1].style.display).not.toBe('none'); + expect(vm.$el.querySelectorAll('mark')[1].textContent).toBe(' in length'); }); - it('highlights body text & subject line', (done) => { + it('highlights body text & subject line', async () => { vm.text = 'text less than 50 chars that should not highlighted\nbody content that will be highlighted when it is more than 72 characters in length'; - vm.$nextTick() - .then(() => { - expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2); - expect(vm.$el.querySelectorAll('mark').length).toBe(2); + await nextTick(); + expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2); + expect(vm.$el.querySelectorAll('mark').length).toBe(2); - expect(vm.$el.querySelectorAll('mark')[0].textContent).toContain('d'); - expect(vm.$el.querySelectorAll('mark')[1].textContent).toBe(' in length'); - }) - .then(done) - .catch(done.fail); + expect(vm.$el.querySelectorAll('mark')[0].textContent).toContain('d'); + expect(vm.$el.querySelectorAll('mark')[1].textContent).toBe(' in length'); }); }); }); describe('scrolling textarea', () => { - it('updates transform of highlights', (done) => { + it('updates transform of highlights', async () => { vm.text = 'subject line\n\n\n\n\n\n\n\n\n\n\nbody content'; - vm.$nextTick() - .then(() => { - vm.$el.querySelector('textarea').scrollTo(0, 50); - - vm.handleScroll(); - }) - .then(vm.$nextTick) - .then(() => { - expect(vm.scrollTop).toBe(50); - expect(vm.$el.querySelector('.highlights').style.transform).toBe( - 'translate3d(0, -50px, 0)', - ); - }) - .then(done) - .catch(done.fail); + await nextTick(); + vm.$el.querySelector('textarea').scrollTo(0, 50); + + vm.handleScroll(); + + await nextTick(); + expect(vm.scrollTop).toBe(50); + expect(vm.$el.querySelector('.highlights').style.transform).toBe('translate3d(0, -50px, 0)'); }); }); }); diff --git a/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js b/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js index 4474647552d..64b53264b4d 100644 --- a/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; import { projectData, branches } from 'jest/ide/mock_data'; import NewMergeRequestOption from '~/ide/components/commit_sidebar/new_merge_request_option.vue'; @@ -72,15 +72,11 @@ describe('create new MR checkbox', () => { expect(vm.$el.textContent).not.toBe(''); }); - it('has new MR', (done) => { + it('has new MR', async () => { setMR(); - vm.$nextTick() - .then(() => { - expect(vm.$el.textContent).not.toBe(''); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(vm.$el.textContent).not.toBe(''); }); }); @@ -96,15 +92,11 @@ describe('create new MR checkbox', () => { expect(vm.$el.textContent).toBe(''); }); - it('has new MR', (done) => { + it('has new MR', async () => { setMR(); - vm.$nextTick() - .then(() => { - expect(vm.$el.textContent).toBe(''); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(vm.$el.textContent).toBe(''); }); }); }); @@ -121,15 +113,11 @@ describe('create new MR checkbox', () => { expect(vm.$el.textContent).not.toBe(''); }); - it('is rendered if MR exists', (done) => { + it('is rendered if MR exists', async () => { setMR(); - vm.$nextTick() - .then(() => { - expect(vm.$el.textContent).not.toBe(''); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(vm.$el.textContent).not.toBe(''); }); }); @@ -144,15 +132,11 @@ describe('create new MR checkbox', () => { expect(vm.$el.textContent).not.toBe(''); }); - it('is hidden if MR exists', (done) => { + it('is hidden if MR exists', async () => { setMR(); - vm.$nextTick() - .then(() => { - expect(vm.$el.textContent).toBe(''); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(vm.$el.textContent).toBe(''); }); }); }); @@ -168,15 +152,11 @@ describe('create new MR checkbox', () => { expect(vm.$el.textContent).not.toBe(''); }); - it('is hidden if MR exists', (done) => { + it('is hidden if MR exists', async () => { setMR(); - vm.$nextTick() - .then(() => { - expect(vm.$el.textContent).toBe(''); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(vm.$el.textContent).toBe(''); }); it('shows enablded checkbox', () => { diff --git a/spec/frontend/ide/components/file_row_extra_spec.js b/spec/frontend/ide/components/file_row_extra_spec.js index 641407c7b77..5a7a1fe7db0 100644 --- a/spec/frontend/ide/components/file_row_extra_spec.js +++ b/spec/frontend/ide/components/file_row_extra_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; import FileRowExtra from '~/ide/components/file_row_extra.vue'; import { createStore } from '~/ide/stores'; @@ -70,28 +70,22 @@ describe('IDE extra file row component', () => { expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null); }); - it('does not show when tree is open', (done) => { + it('does not show when tree is open', async () => { vm.file.type = 'tree'; vm.file.opened = true; changesCount = 1; - vm.$nextTick(() => { - expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null); - - done(); - }); + await nextTick(); + expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null); }); - it('shows for trees with changes', (done) => { + it('shows for trees with changes', async () => { vm.file.type = 'tree'; vm.file.opened = false; changesCount = 1; - vm.$nextTick(() => { - expect(vm.$el.querySelector('.ide-tree-changes')).not.toBe(null); - - done(); - }); + await nextTick(); + expect(vm.$el.querySelector('.ide-tree-changes')).not.toBe(null); }); }); @@ -100,55 +94,40 @@ describe('IDE extra file row component', () => { expect(vm.$el.querySelector('.file-changed-icon')).toBe(null); }); - it('shows when file is changed', (done) => { + it('shows when file is changed', async () => { vm.file.changed = true; - vm.$nextTick(() => { - expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null); - - done(); - }); + await nextTick(); + expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null); }); - it('shows when file is staged', (done) => { + it('shows when file is staged', async () => { vm.file.staged = true; - vm.$nextTick(() => { - expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null); - - done(); - }); + await nextTick(); + expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null); }); - it('shows when file is a tempFile', (done) => { + it('shows when file is a tempFile', async () => { vm.file.tempFile = true; - vm.$nextTick(() => { - expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null); - - done(); - }); + await nextTick(); + expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null); }); - it('shows when file is renamed', (done) => { + it('shows when file is renamed', async () => { vm.file.prevPath = 'original-file'; - vm.$nextTick(() => { - expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null); - - done(); - }); + await nextTick(); + expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null); }); - it('hides when file is renamed', (done) => { + it('hides when file is renamed', async () => { vm.file.prevPath = 'original-file'; vm.file.type = 'tree'; - vm.$nextTick(() => { - expect(vm.$el.querySelector('.file-changed-icon')).toBe(null); - - done(); - }); + await nextTick(); + expect(vm.$el.querySelector('.file-changed-icon')).toBe(null); }); }); @@ -157,14 +136,11 @@ describe('IDE extra file row component', () => { expect(vm.$el.querySelector('[data-testid="git-merge-icon"]')).toBe(null); }); - it('shows when a merge request change', (done) => { + it('shows when a merge request change', async () => { vm.file.mrChange = true; - vm.$nextTick(() => { - expect(vm.$el.querySelector('[data-testid="git-merge-icon"]')).not.toBe(null); - - done(); - }); + await nextTick(); + expect(vm.$el.querySelector('[data-testid="git-merge-icon"]')).not.toBe(null); }); }); }); diff --git a/spec/frontend/ide/components/file_templates/bar_spec.js b/spec/frontend/ide/components/file_templates/bar_spec.js index 4ca99f8d055..e8ebfa78fe9 100644 --- a/spec/frontend/ide/components/file_templates/bar_spec.js +++ b/spec/frontend/ide/components/file_templates/bar_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import { mountComponentWithStore } from 'helpers/vue_mount_component_helper'; import Bar from '~/ide/components/file_templates/bar.vue'; import { createStore } from '~/ide/stores'; @@ -46,7 +46,7 @@ describe('IDE file templates bar component', () => { }); describe('template dropdown', () => { - beforeEach((done) => { + beforeEach(async () => { vm.$store.state.fileTemplates.templates = [ { name: 'test', @@ -57,7 +57,7 @@ describe('IDE file templates bar component', () => { key: 'gitlab_ci_ymls', }; - vm.$nextTick(done); + await nextTick(); }); it('renders dropdown component', () => { @@ -75,14 +75,11 @@ describe('IDE file templates bar component', () => { }); }); - it('shows undo button if updateSuccess is true', (done) => { + it('shows undo button if updateSuccess is true', async () => { vm.$store.state.fileTemplates.updateSuccess = true; - vm.$nextTick(() => { - expect(vm.$el.querySelector('.btn-default').style.display).not.toBe('none'); - - done(); - }); + await nextTick(); + expect(vm.$el.querySelector('.btn-default').style.display).not.toBe('none'); }); it('calls undoFileTemplate when clicking undo button', () => { @@ -93,7 +90,7 @@ describe('IDE file templates bar component', () => { expect(vm.undoFileTemplate).toHaveBeenCalled(); }); - it('calls setSelectedTemplateType if activeFile name matches a template', (done) => { + it('calls setSelectedTemplateType if activeFile name matches a template', async () => { const fileName = '.gitlab-ci.yml'; jest.spyOn(vm, 'setSelectedTemplateType').mockImplementation(() => {}); @@ -101,13 +98,10 @@ describe('IDE file templates bar component', () => { vm.setInitialType(); - vm.$nextTick(() => { - expect(vm.setSelectedTemplateType).toHaveBeenCalledWith({ - name: fileName, - key: 'gitlab_ci_ymls', - }); - - done(); + await nextTick(); + expect(vm.setSelectedTemplateType).toHaveBeenCalledWith({ + name: fileName, + key: 'gitlab_ci_ymls', }); }); }); diff --git a/spec/frontend/ide/components/ide_status_bar_spec.js b/spec/frontend/ide/components/ide_status_bar_spec.js index f1a0b64caf2..00ef75fcf3a 100644 --- a/spec/frontend/ide/components/ide_status_bar_spec.js +++ b/spec/frontend/ide/components/ide_status_bar_spec.js @@ -1,5 +1,5 @@ import _ from 'lodash'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import { TEST_HOST } from 'helpers/test_constants'; import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; import IdeStatusBar from '~/ide/components/ide_status_bar.vue'; @@ -73,7 +73,7 @@ describe('ideStatusBar', () => { }); describe('pipeline status', () => { - it('opens right sidebar on clicking icon', (done) => { + it('opens right sidebar on clicking icon', async () => { jest.spyOn(vm, 'openRightPane').mockImplementation(() => {}); Vue.set(vm.$store.state.pipelines, 'latestPipeline', { details: { @@ -88,14 +88,10 @@ describe('ideStatusBar', () => { }, }); - vm.$nextTick() - .then(() => { - vm.$el.querySelector('.ide-status-pipeline button').click(); + await nextTick(); + vm.$el.querySelector('.ide-status-pipeline button').click(); - expect(vm.openRightPane).toHaveBeenCalledWith(rightSidebarViews.pipelines); - }) - .then(done) - .catch(done.fail); + expect(vm.openRightPane).toHaveBeenCalledWith(rightSidebarViews.pipelines); }); }); diff --git a/spec/frontend/ide/components/ide_tree_list_spec.js b/spec/frontend/ide/components/ide_tree_list_spec.js index ace51204374..a85c52f5e86 100644 --- a/spec/frontend/ide/components/ide_tree_list_spec.js +++ b/spec/frontend/ide/components/ide_tree_list_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; import IdeTreeList from '~/ide/components/ide_tree_list.vue'; import { createStore } from '~/ide/stores'; @@ -48,15 +48,12 @@ describe('IDE tree list', () => { expect(vm.$emit).toHaveBeenCalledWith('tree-ready'); }); - it('renders loading indicator', (done) => { + it('renders loading indicator', async () => { store.state.trees['abcproject/main'].loading = true; - vm.$nextTick(() => { - expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull(); - expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3); - - done(); - }); + await nextTick(); + expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull(); + expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3); }); it('renders list of files', () => { diff --git a/spec/frontend/ide/components/jobs/detail_spec.js b/spec/frontend/ide/components/jobs/detail_spec.js index 3634599f328..9122471d421 100644 --- a/spec/frontend/ide/components/jobs/detail_spec.js +++ b/spec/frontend/ide/components/jobs/detail_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import { TEST_HOST } from 'helpers/test_constants'; import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; import JobDetail from '~/ide/components/jobs/detail.vue'; @@ -48,14 +48,11 @@ describe('IDE jobs detail view', () => { expect(vm.$el.querySelector('.bash').textContent).toContain('testing'); }); - it('renders empty message output', (done) => { + it('renders empty message output', async () => { vm.$store.state.pipelines.detailJob.output = ''; - vm.$nextTick(() => { - expect(vm.$el.querySelector('.bash').textContent).toContain('No messages were logged'); - - done(); - }); + await nextTick(); + expect(vm.$el.querySelector('.bash').textContent).toContain('No messages were logged'); }); it('renders loading icon', () => { @@ -68,14 +65,11 @@ describe('IDE jobs detail view', () => { expect(vm.$el.querySelector('.bash').style.display).toBe('none'); }); - it('hide loading icon when isLoading is false', (done) => { + it('hide loading icon when isLoading is false', async () => { vm.$store.state.pipelines.detailJob.isLoading = false; - vm.$nextTick(() => { - expect(vm.$el.querySelector('.build-loader-animation').style.display).toBe('none'); - - done(); - }); + await nextTick(); + expect(vm.$el.querySelector('.build-loader-animation').style.display).toBe('none'); }); it('resets detailJob when clicking header button', () => { @@ -107,17 +101,16 @@ describe('IDE jobs detail view', () => { fnName | btnName | scrollPos ${'scrollDown'} | ${'down'} | ${0} ${'scrollUp'} | ${'up'} | ${1} - `('triggers $fnName when clicking $btnName button', ({ fnName, scrollPos }) => { + `('triggers $fnName when clicking $btnName button', async ({ fnName, scrollPos }) => { jest.spyOn(vm, fnName).mockImplementation(); vm = vm.$mount(); vm.scrollPos = scrollPos; - return vm.$nextTick().then(() => { - vm.$el.querySelector('.btn-scroll:not([disabled])').click(); - expect(vm[fnName]).toHaveBeenCalled(); - }); + await nextTick(); + vm.$el.querySelector('.btn-scroll:not([disabled])').click(); + expect(vm[fnName]).toHaveBeenCalled(); }); }); diff --git a/spec/frontend/ide/components/jobs/item_spec.js b/spec/frontend/ide/components/jobs/item_spec.js index 7343fc80a03..c76760a5522 100644 --- a/spec/frontend/ide/components/jobs/item_spec.js +++ b/spec/frontend/ide/components/jobs/item_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import mountComponent from 'helpers/vue_mount_component_helper'; import JobItem from '~/ide/components/jobs/item.vue'; import { jobs } from '../../mock_data'; @@ -27,13 +27,10 @@ describe('IDE jobs item', () => { expect(vm.$el.querySelector('[data-testid="status_success_borderless-icon"]')).not.toBe(null); }); - it('does not render view logs button if not started', (done) => { + it('does not render view logs button if not started', async () => { vm.job.started = false; - vm.$nextTick(() => { - expect(vm.$el.querySelector('.btn')).toBe(null); - - done(); - }); + await nextTick(); + expect(vm.$el.querySelector('.btn')).toBe(null); }); }); diff --git a/spec/frontend/ide/components/nav_dropdown_button_spec.js b/spec/frontend/ide/components/nav_dropdown_button_spec.js index a02bfa5c391..1c14685df68 100644 --- a/spec/frontend/ide/components/nav_dropdown_button_spec.js +++ b/spec/frontend/ide/components/nav_dropdown_button_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import { trimText } from 'helpers/text_helper'; import { mountComponentWithStore } from 'helpers/vue_mount_component_helper'; import NavDropdownButton from '~/ide/components/nav_dropdown_button.vue'; @@ -36,38 +36,26 @@ describe('NavDropdown', () => { expect(trimText(vm.$el.textContent)).toEqual('- -'); }); - it('renders branch name, if state has currentBranchId', (done) => { + it('renders branch name, if state has currentBranchId', async () => { vm.$store.state.currentBranchId = TEST_BRANCH_ID; - vm.$nextTick() - .then(() => { - expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} -`); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} -`); }); - it('renders mr id, if state has currentMergeRequestId', (done) => { + it('renders mr id, if state has currentMergeRequestId', async () => { vm.$store.state.currentMergeRequestId = TEST_MR_ID; - vm.$nextTick() - .then(() => { - expect(trimText(vm.$el.textContent)).toEqual(`- !${TEST_MR_ID}`); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(trimText(vm.$el.textContent)).toEqual(`- !${TEST_MR_ID}`); }); - it('renders branch and mr, if state has both', (done) => { + it('renders branch and mr, if state has both', async () => { vm.$store.state.currentBranchId = TEST_BRANCH_ID; vm.$store.state.currentMergeRequestId = TEST_MR_ID; - vm.$nextTick() - .then(() => { - expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} !${TEST_MR_ID}`); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} !${TEST_MR_ID}`); }); it('shows icons', () => { diff --git a/spec/frontend/ide/components/nav_dropdown_spec.js b/spec/frontend/ide/components/nav_dropdown_spec.js index 6a1be7ee964..33e638843f5 100644 --- a/spec/frontend/ide/components/nav_dropdown_spec.js +++ b/spec/frontend/ide/components/nav_dropdown_spec.js @@ -1,5 +1,6 @@ import { mount } from '@vue/test-utils'; import $ from 'jquery'; +import { nextTick } from 'vue'; import NavDropdown from '~/ide/components/nav_dropdown.vue'; import { PERMISSION_READ_MR } from '~/ide/constants'; import { createStore } from '~/ide/stores'; @@ -58,29 +59,19 @@ describe('IDE NavDropdown', () => { expect(findNavForm().exists()).toBe(false); }); - it('renders nav form when show.bs.dropdown', (done) => { + it('renders nav form when show.bs.dropdown', async () => { showDropdown(); - wrapper.vm - .$nextTick() - .then(() => { - expect(findNavForm().exists()).toBe(true); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(findNavForm().exists()).toBe(true); }); - it('destroys nav form when closed', (done) => { + it('destroys nav form when closed', async () => { showDropdown(); hideDropdown(); - wrapper.vm - .$nextTick() - .then(() => { - expect(findNavForm().exists()).toBe(false); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(findNavForm().exists()).toBe(false); }); it('renders merge request icon', () => { diff --git a/spec/frontend/ide/components/new_dropdown/button_spec.js b/spec/frontend/ide/components/new_dropdown/button_spec.js index 32fa2babcdb..298d7b810e1 100644 --- a/spec/frontend/ide/components/new_dropdown/button_spec.js +++ b/spec/frontend/ide/components/new_dropdown/button_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import mountComponent from 'helpers/vue_mount_component_helper'; import Button from '~/ide/components/new_dropdown/button.vue'; @@ -37,14 +37,11 @@ describe('IDE new entry dropdown button component', () => { expect(vm.$emit).toHaveBeenCalledWith('click'); }); - it('hides label if showLabel is false', (done) => { + it('hides label if showLabel is false', async () => { vm.showLabel = false; - vm.$nextTick(() => { - expect(vm.$el.textContent).not.toContain('Testing'); - - done(); - }); + await nextTick(); + expect(vm.$el.textContent).not.toContain('Testing'); }); describe('tooltipTitle', () => { @@ -52,14 +49,11 @@ describe('IDE new entry dropdown button component', () => { expect(vm.tooltipTitle).toBe(''); }); - it('returns label', (done) => { + it('returns label', async () => { vm.showLabel = false; - vm.$nextTick(() => { - expect(vm.tooltipTitle).toBe('Testing'); - - done(); - }); + await nextTick(); + expect(vm.tooltipTitle).toBe('Testing'); }); }); }); diff --git a/spec/frontend/ide/components/new_dropdown/modal_spec.js b/spec/frontend/ide/components/new_dropdown/modal_spec.js index 41111f5dbb4..8134248bbf4 100644 --- a/spec/frontend/ide/components/new_dropdown/modal_spec.js +++ b/spec/frontend/ide/components/new_dropdown/modal_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; import createFlash from '~/flash'; import modal from '~/ide/components/new_dropdown/modal.vue'; @@ -19,14 +19,14 @@ describe('new file modal component', () => { ${'tree'} | ${'Create new directory'} | ${'Create directory'} | ${false} ${'blob'} | ${'Create new file'} | ${'Create file'} | ${true} `('$entryType', ({ entryType, modalTitle, btnTitle, showsFileTemplates }) => { - beforeEach((done) => { + beforeEach(async () => { const store = createStore(); vm = createComponentWithStore(Component, store).$mount(); vm.open(entryType); vm.name = 'testing'; - vm.$nextTick(done); + await nextTick(); }); afterEach(() => { @@ -71,16 +71,13 @@ describe('new file modal component', () => { ${'blob'} | ${'Rename file'} | ${'Rename file'} `( 'renders title and button for renaming $entryType', - ({ entryType, modalTitle, btnTitle }, done) => { + async ({ entryType, modalTitle, btnTitle }) => { vm.$store.state.entries['test-path'].type = entryType; vm.open('rename', 'test-path'); - vm.$nextTick(() => { - expect(document.querySelector('.modal-title').textContent.trim()).toBe(modalTitle); - expect(document.querySelector('.btn-success').textContent.trim()).toBe(btnTitle); - - done(); - }); + await nextTick(); + expect(document.querySelector('.modal-title').textContent.trim()).toBe(modalTitle); + expect(document.querySelector('.btn-success').textContent.trim()).toBe(btnTitle); }, ); diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js index 15af2d03704..96c9baeb328 100644 --- a/spec/frontend/ide/components/repo_editor_spec.js +++ b/spec/frontend/ide/components/repo_editor_spec.js @@ -1,7 +1,7 @@ import { shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { editor as monacoEditor, Range } from 'monaco-editor'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import '~/behaviors/markdown/render_gfm'; import waitForPromises from 'helpers/wait_for_promises'; @@ -367,17 +367,17 @@ describe('RepoEditor', () => { expect(vm.$store.state.panelResizing).toBe(false); // default value vm.$store.state.panelResizing = true; - await vm.$nextTick(); + await nextTick(); expect(updateDimensionsSpy).not.toHaveBeenCalled(); vm.$store.state.panelResizing = false; - await vm.$nextTick(); + await nextTick(); expect(updateDimensionsSpy).toHaveBeenCalledTimes(1); vm.$store.state.panelResizing = true; - await vm.$nextTick(); + await nextTick(); expect(updateDimensionsSpy).toHaveBeenCalledTimes(1); }); @@ -387,12 +387,12 @@ describe('RepoEditor', () => { expect(vm.$store.state.rightPane.isOpen).toBe(false); // default value vm.$store.state.rightPane.isOpen = true; - await vm.$nextTick(); + await nextTick(); expect(updateDimensionsSpy).toHaveBeenCalledTimes(1); vm.$store.state.rightPane.isOpen = false; - await vm.$nextTick(); + await nextTick(); expect(updateDimensionsSpy).toHaveBeenCalledTimes(2); }); @@ -411,7 +411,7 @@ describe('RepoEditor', () => { `('tabs in $mode are $isVisible', async ({ mode, isVisible } = {}) => { vm.$store.state.currentActivityView = leftSidebarViews[mode].name; - await vm.$nextTick(); + await nextTick(); expect(wrapper.find('.nav-links').exists()).toBe(isVisible); }); }); @@ -436,7 +436,7 @@ describe('RepoEditor', () => { }); changeViewMode(FILE_VIEW_MODE_PREVIEW); - await vm.$nextTick(); + await nextTick(); }); it('do not show the editor', () => { @@ -448,7 +448,7 @@ describe('RepoEditor', () => { expect(updateDimensionsSpy).not.toHaveBeenCalled(); changeViewMode(FILE_VIEW_MODE_EDITOR); - await vm.$nextTick(); + await nextTick(); expect(updateDimensionsSpy).toHaveBeenCalled(); }); @@ -460,7 +460,7 @@ describe('RepoEditor', () => { jest.spyOn(vm, 'shouldHideEditor', 'get').mockReturnValue(true); vm.initEditor(); - await vm.$nextTick(); + await nextTick(); }; it('does not fetch file information for temp entries', async () => { @@ -511,20 +511,20 @@ describe('RepoEditor', () => { const origFile = vm.file; vm.file.pending = true; - await vm.$nextTick(); + await nextTick(); wrapper.setProps({ file: file('testing'), }); vm.file.content = 'foo'; // need to prevent full cycle of initEditor - await vm.$nextTick(); + await nextTick(); expect(vm.removePendingTab).toHaveBeenCalledWith(origFile); }); it('does not call initEditor if the file did not change', async () => { Vue.set(vm, 'file', vm.file); - await vm.$nextTick(); + await nextTick(); expect(vm.initEditor).not.toHaveBeenCalled(); }); @@ -538,7 +538,7 @@ describe('RepoEditor', () => { key: 'new', }, }); - await vm.$nextTick(); + await nextTick(); expect(vm.initEditor).toHaveBeenCalled(); }); diff --git a/spec/frontend/ide/components/shared/tokened_input_spec.js b/spec/frontend/ide/components/shared/tokened_input_spec.js index 837bfe6b574..a37c08af0a1 100644 --- a/spec/frontend/ide/components/shared/tokened_input_spec.js +++ b/spec/frontend/ide/components/shared/tokened_input_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import mountComponent from 'helpers/vue_mount_component_helper'; import TokenedInput from '~/ide/components/shared/tokened_input.vue'; @@ -54,15 +54,11 @@ describe('IDE shared/TokenedInput', () => { expect(vm.$refs.input).toHaveValue(TEST_VALUE); }); - it('renders placeholder, when tokens are empty', (done) => { + it('renders placeholder, when tokens are empty', async () => { vm.tokens = []; - vm.$nextTick() - .then(() => { - expect(vm.$refs.input).toHaveAttr('placeholder', TEST_PLACEHOLDER); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(vm.$refs.input).toHaveAttr('placeholder', TEST_PLACEHOLDER); }); it('triggers "removeToken" on token click', () => { diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js index d39e00b9c9e..df6f7cb827d 100644 --- a/spec/frontend/issues/show/components/description_spec.js +++ b/spec/frontend/issues/show/components/description_spec.js @@ -1,21 +1,56 @@ import $ from 'jquery'; -import Vue from 'vue'; +import { nextTick } from 'vue'; import '~/behaviors/markdown/render_gfm'; +import { GlPopover, GlModal } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { stubComponent } from 'helpers/stub_component'; import { TEST_HOST } from 'helpers/test_constants'; -import mountComponent from 'helpers/vue_mount_component_helper'; import Description from '~/issues/show/components/description.vue'; import TaskList from '~/task_list'; -import { descriptionProps as props } from '../mock_data/mock_data'; +import CreateWorkItem from '~/work_items/pages/create_work_item.vue'; +import { + descriptionProps as initialProps, + descriptionHtmlWithCheckboxes, +} from '../mock_data/mock_data'; jest.mock('~/task_list'); +const showModal = jest.fn(); +const hideModal = jest.fn(); + describe('Description component', () => { - let vm; - let DescriptionComponent; + let wrapper; + + const findGfmContent = () => wrapper.find('[data-testid="gfm-content"]'); + const findTextarea = () => wrapper.find('[data-testid="textarea"]'); + const findTaskActionButtons = () => wrapper.findAll('.js-add-task'); + const findConvertToTaskButton = () => wrapper.find('[data-testid="convert-to-task"]'); + const findTaskSvg = () => wrapper.find('[data-testid="issue-open-m-icon"]'); + + const findPopovers = () => wrapper.findAllComponents(GlPopover); + const findModal = () => wrapper.findComponent(GlModal); + const findCreateWorkItem = () => wrapper.findComponent(CreateWorkItem); + + function createComponent({ props = {}, provide = {} } = {}) { + wrapper = shallowMount(Description, { + propsData: { + ...initialProps, + ...props, + }, + provide, + stubs: { + GlModal: stubComponent(GlModal, { + methods: { + show: showModal, + hide: hideModal, + }, + }), + GlPopover, + }, + }); + } beforeEach(() => { - DescriptionComponent = Vue.extend(Description); - if (!document.querySelector('.issuable-meta')) { const metaData = document.createElement('div'); metaData.classList.add('issuable-meta'); @@ -24,91 +59,102 @@ describe('Description component', () => { document.body.appendChild(metaData); } - - vm = mountComponent(DescriptionComponent, props); }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); afterAll(() => { $('.issuable-meta .flash-container').remove(); }); - it('doesnt animate first description changes', () => { - vm.descriptionHtml = 'changed'; - - return vm.$nextTick().then(() => { - expect( - vm.$el.querySelector('.md').classList.contains('issue-realtime-pre-pulse'), - ).toBeFalsy(); - jest.runAllTimers(); - return vm.$nextTick(); + it('doesnt animate first description changes', async () => { + createComponent(); + await wrapper.setProps({ + descriptionHtml: 'changed', }); + + expect(findGfmContent().classes()).not.toContain('issue-realtime-pre-pulse'); }); - it('animates description changes on live update', () => { - vm.descriptionHtml = 'changed'; - return vm - .$nextTick() - .then(() => { - vm.descriptionHtml = 'changed second time'; - return vm.$nextTick(); - }) - .then(() => { - expect( - vm.$el.querySelector('.md').classList.contains('issue-realtime-pre-pulse'), - ).toBeTruthy(); - jest.runAllTimers(); - return vm.$nextTick(); - }) - .then(() => { - expect( - vm.$el.querySelector('.md').classList.contains('issue-realtime-trigger-pulse'), - ).toBeTruthy(); - }); + it('animates description changes on live update', async () => { + createComponent(); + await wrapper.setProps({ + descriptionHtml: 'changed', + }); + + expect(findGfmContent().classes()).not.toContain('issue-realtime-pre-pulse'); + + await wrapper.setProps({ + descriptionHtml: 'changed second time', + }); + + expect(findGfmContent().classes()).toContain('issue-realtime-pre-pulse'); + + await jest.runOnlyPendingTimers(); + + expect(findGfmContent().classes()).toContain('issue-realtime-trigger-pulse'); }); - it('applies syntax highlighting and math when description changed', () => { - const vmSpy = jest.spyOn(vm, 'renderGFM'); + it('applies syntax highlighting and math when description changed', async () => { const prototypeSpy = jest.spyOn($.prototype, 'renderGFM'); - vm.descriptionHtml = 'changed'; + createComponent(); - return vm.$nextTick().then(() => { - expect(vm.$refs['gfm-content']).toBeDefined(); - expect(vmSpy).toHaveBeenCalled(); - expect(prototypeSpy).toHaveBeenCalled(); - expect($.prototype.renderGFM).toHaveBeenCalled(); + await wrapper.setProps({ + descriptionHtml: 'changed', }); + + expect(findGfmContent().exists()).toBe(true); + expect(prototypeSpy).toHaveBeenCalled(); }); it('sets data-update-url', () => { - expect(vm.$el.querySelector('textarea').dataset.updateUrl).toEqual(TEST_HOST); + createComponent(); + expect(findTextarea().attributes('data-update-url')).toBe(TEST_HOST); }); describe('TaskList', () => { beforeEach(() => { - vm.$destroy(); TaskList.mockClear(); - vm = mountComponent(DescriptionComponent, { ...props, issuableType: 'issuableType' }); }); it('re-inits the TaskList when description changed', () => { - vm.descriptionHtml = 'changed'; + createComponent({ + props: { + issuableType: 'issuableType', + }, + }); + wrapper.setProps({ + descriptionHtml: 'changed', + }); expect(TaskList).toHaveBeenCalled(); }); - it('does not re-init the TaskList when canUpdate is false', () => { - vm.canUpdate = false; - vm.descriptionHtml = 'changed'; + it('does not re-init the TaskList when canUpdate is false', async () => { + createComponent({ + props: { + issuableType: 'issuableType', + canUpdate: false, + }, + }); + wrapper.setProps({ + descriptionHtml: 'changed', + }); - expect(TaskList).toHaveBeenCalledTimes(1); + expect(TaskList).not.toHaveBeenCalled(); }); it('calls with issuableType dataType', () => { - vm.descriptionHtml = 'changed'; + createComponent({ + props: { + issuableType: 'issuableType', + }, + }); + wrapper.setProps({ + descriptionHtml: 'changed', + }); expect(TaskList).toHaveBeenCalledWith({ dataType: 'issuableType', @@ -123,65 +169,97 @@ describe('Description component', () => { }); describe('taskStatus', () => { - it('adds full taskStatus', () => { - vm.taskStatus = '1 of 1'; - - return vm.$nextTick().then(() => { - expect(document.querySelector('.issuable-meta #task_status').textContent.trim()).toBe( - '1 of 1', - ); + it('adds full taskStatus', async () => { + createComponent({ + props: { + taskStatus: '1 of 1', + }, }); - }); + await nextTick(); - it('adds short taskStatus', () => { - vm.taskStatus = '1 of 1'; + expect(document.querySelector('.issuable-meta #task_status').textContent.trim()).toBe( + '1 of 1', + ); + }); - return vm.$nextTick().then(() => { - expect(document.querySelector('.issuable-meta #task_status_short').textContent.trim()).toBe( - '1/1 task', - ); + it('adds short taskStatus', async () => { + createComponent({ + props: { + taskStatus: '1 of 1', + }, }); - }); + await nextTick(); - it('clears task status text when no tasks are present', () => { - vm.taskStatus = '0 of 0'; + expect(document.querySelector('.issuable-meta #task_status_short').textContent.trim()).toBe( + '1/1 task', + ); + }); - return vm.$nextTick().then(() => { - expect(document.querySelector('.issuable-meta #task_status').textContent.trim()).toBe(''); + it('clears task status text when no tasks are present', async () => { + createComponent({ + props: { + taskStatus: '0 of 0', + }, }); + + await nextTick(); + + expect(document.querySelector('.issuable-meta #task_status').textContent.trim()).toBe(''); }); }); - describe('taskListUpdateStarted', () => { - it('emits event to parent', () => { - const spy = jest.spyOn(vm, '$emit'); - - vm.taskListUpdateStarted(); + describe('with work items feature flag is enabled', () => { + beforeEach(async () => { + createComponent({ + props: { + descriptionHtml: descriptionHtmlWithCheckboxes, + }, + provide: { + glFeatures: { + workItems: true, + }, + }, + }); + await nextTick(); + }); - expect(spy).toHaveBeenCalledWith('taskListUpdateStarted'); + it('renders a list of hidden buttons corresponding to checkboxes in description HTML', () => { + expect(findTaskActionButtons()).toHaveLength(3); }); - }); - describe('taskListUpdateSuccess', () => { - it('emits event to parent', () => { - const spy = jest.spyOn(vm, '$emit'); + it('renders a list of popovers corresponding to checkboxes in description HTML', () => { + expect(findPopovers()).toHaveLength(3); + expect(findPopovers().at(0).props('target')).toBe( + findTaskActionButtons().at(0).attributes('id'), + ); + }); - vm.taskListUpdateSuccess(); + it('does not show a modal by default', () => { + expect(findModal().props('visible')).toBe(false); + }); - expect(spy).toHaveBeenCalledWith('taskListUpdateSucceeded'); + it('opens a modal when a button on popover is clicked and displays correct title', async () => { + findConvertToTaskButton().vm.$emit('click'); + expect(showModal).toHaveBeenCalled(); + await nextTick(); + expect(findCreateWorkItem().props('initialTitle').trim()).toBe('todo 1'); }); - }); - describe('taskListUpdateError', () => { - it('should create flash notification and emit an event to parent', () => { - const msg = - 'Someone edited this issue at the same time you did. The description has been updated and you will need to make your changes again.'; - const spy = jest.spyOn(vm, '$emit'); + it('closes the modal on `closeCreateTaskModal` event', () => { + findConvertToTaskButton().vm.$emit('click'); + findCreateWorkItem().vm.$emit('closeModal'); + expect(hideModal).toHaveBeenCalled(); + }); - vm.taskListUpdateError(); + it('updates description HTML on `onCreate` event', async () => { + const newTitle = 'New title'; + findConvertToTaskButton().vm.$emit('click'); + findCreateWorkItem().vm.$emit('onCreate', newTitle); + expect(hideModal).toHaveBeenCalled(); + await nextTick(); - expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(msg); - expect(spy).toHaveBeenCalledWith('taskListUpdateFailed'); + expect(findTaskSvg().exists()).toBe(true); + expect(wrapper.text()).toContain(newTitle); }); }); }); diff --git a/spec/frontend/issues/show/mock_data/mock_data.js b/spec/frontend/issues/show/mock_data/mock_data.js index a73826954c3..89653ff82b2 100644 --- a/spec/frontend/issues/show/mock_data/mock_data.js +++ b/spec/frontend/issues/show/mock_data/mock_data.js @@ -58,3 +58,17 @@ export const appProps = { zoomMeetingUrl, publishedIncidentUrl, }; + +export const descriptionHtmlWithCheckboxes = ` + <ul dir="auto" class="task-list" data-sourcepos"3:1-5:12"> + <li class="task-list-item" data-sourcepos="3:1-3:11"> + <input class="task-list-item-checkbox" type="checkbox"> todo 1 + </li> + <li class="task-list-item" data-sourcepos="4:1-4:12"> + <input class="task-list-item-checkbox" type="checkbox"> todo 2 + </li> + <li class="task-list-item" data-sourcepos="5:1-5:12"> + <input class="task-list-item-checkbox" type="checkbox"> todo 3 + </li> + </ul> +`; diff --git a/spec/frontend/notebook/cells/code_spec.js b/spec/frontend/notebook/cells/code_spec.js index 669bdc2f89a..6a51731c909 100644 --- a/spec/frontend/notebook/cells/code_spec.js +++ b/spec/frontend/notebook/cells/code_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import fixture from 'test_fixtures/blob/notebook/basic.json'; import CodeComponent from '~/notebook/cells/code.vue'; @@ -58,12 +58,12 @@ describe('Code component', () => { describe('with string for output', () => { // NBFormat Version 4.1 allows outputs.text to be a string - beforeEach(() => { + beforeEach(async () => { const cell = json.cells[2]; cell.outputs[0].text = cell.outputs[0].text.join(''); vm = setupComponent(cell); - return vm.$nextTick(); + await nextTick(); }); it('does not render output prompt', () => { @@ -76,12 +76,12 @@ describe('Code component', () => { }); describe('with string for cell.source', () => { - beforeEach(() => { + beforeEach(async () => { const cell = json.cells[0]; cell.source = cell.source.join(''); vm = setupComponent(cell); - return vm.$nextTick(); + await nextTick(); }); it('renders the same input as when cell.source is an array', () => { diff --git a/spec/frontend/notebook/cells/markdown_spec.js b/spec/frontend/notebook/cells/markdown_spec.js index 36b1e91f15f..7dc6f90d202 100644 --- a/spec/frontend/notebook/cells/markdown_spec.js +++ b/spec/frontend/notebook/cells/markdown_spec.js @@ -1,6 +1,6 @@ import { mount } from '@vue/test-utils'; import katex from 'katex'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import markdownTableJson from 'test_fixtures/blob/notebook/markdown-table.json'; import basicJson from 'test_fixtures/blob/notebook/basic.json'; import mathJson from 'test_fixtures/blob/notebook/math.json'; @@ -37,7 +37,7 @@ describe('Markdown component', () => { let cell; let json; - beforeEach(() => { + beforeEach(async () => { json = basicJson; // eslint-disable-next-line prefer-destructuring @@ -45,7 +45,7 @@ describe('Markdown component', () => { vm = buildCellComponent(cell); - return vm.$nextTick(); + await nextTick(); }); it('does not render prompt', () => { @@ -67,7 +67,7 @@ describe('Markdown component', () => { ], }); - await vm.$nextTick(); + await nextTick(); expect(vm.$el.querySelector('a').getAttribute('href')).toBeNull(); }); @@ -77,7 +77,7 @@ describe('Markdown component', () => { source: ['<a href="test.js" data-remote=true data-type="script" class="xss-link">XSS</a>\n'], }); - await vm.$nextTick(); + await nextTick(); expect(findLink().getAttribute('data-remote')).toBe(null); expect(findLink().getAttribute('data-type')).toBe(null); }); @@ -99,7 +99,7 @@ describe('Markdown component', () => { ])('%s', async ([testMd, mustContain]) => { vm = buildMarkdownComponent([testMd], '/raw/'); - await vm.$nextTick(); + await nextTick(); expect(vm.$el.innerHTML).toContain(mustContain); }); @@ -110,29 +110,28 @@ describe('Markdown component', () => { json = markdownTableJson; }); - it('renders images and text', () => { + it('renders images and text', async () => { vm = buildCellComponent(json.cells[0]); - return vm.$nextTick().then(() => { - const images = vm.$el.querySelectorAll('img'); - expect(images.length).toBe(5); - - const columns = vm.$el.querySelectorAll('td'); - expect(columns.length).toBe(6); - - expect(columns[0].textContent).toEqual('Hello '); - expect(columns[1].textContent).toEqual('Test '); - expect(columns[2].textContent).toEqual('World '); - expect(columns[3].textContent).toEqual('Fake '); - expect(columns[4].textContent).toEqual('External image: '); - expect(columns[5].textContent).toEqual('Empty'); - - expect(columns[0].innerHTML).toContain('<img src="data:image/jpeg;base64'); - expect(columns[1].innerHTML).toContain('<img src="data:image/png;base64'); - expect(columns[2].innerHTML).toContain('<img src="data:image/jpeg;base64'); - expect(columns[3].innerHTML).toContain('<img>'); - expect(columns[4].innerHTML).toContain('<img src="https://www.google.com/'); - }); + await nextTick(); + const images = vm.$el.querySelectorAll('img'); + expect(images.length).toBe(5); + + const columns = vm.$el.querySelectorAll('td'); + expect(columns.length).toBe(6); + + expect(columns[0].textContent).toEqual('Hello '); + expect(columns[1].textContent).toEqual('Test '); + expect(columns[2].textContent).toEqual('World '); + expect(columns[3].textContent).toEqual('Fake '); + expect(columns[4].textContent).toEqual('External image: '); + expect(columns[5].textContent).toEqual('Empty'); + + expect(columns[0].innerHTML).toContain('<img src="data:image/jpeg;base64'); + expect(columns[1].innerHTML).toContain('<img src="data:image/png;base64'); + expect(columns[2].innerHTML).toContain('<img src="data:image/jpeg;base64'); + expect(columns[3].innerHTML).toContain('<img>'); + expect(columns[4].innerHTML).toContain('<img src="https://www.google.com/'); }); }); @@ -144,28 +143,28 @@ describe('Markdown component', () => { it('renders multi-line katex', async () => { vm = buildCellComponent(json.cells[0]); - await vm.$nextTick(); + await nextTick(); expect(vm.$el.querySelector('.katex')).not.toBeNull(); }); it('renders inline katex', async () => { vm = buildCellComponent(json.cells[1]); - await vm.$nextTick(); + await nextTick(); expect(vm.$el.querySelector('p:first-child .katex')).not.toBeNull(); }); it('renders multiple inline katex', async () => { vm = buildCellComponent(json.cells[1]); - await vm.$nextTick(); + await nextTick(); expect(vm.$el.querySelectorAll('p:nth-child(2) .katex')).toHaveLength(4); }); it('output cell in case of katex error', async () => { vm = buildMarkdownComponent(['Some invalid $a & b$ inline formula $b & c$\n', '\n']); - await vm.$nextTick(); + await nextTick(); // expect one paragraph with no katex formula in it expect(vm.$el.querySelectorAll('p')).toHaveLength(1); expect(vm.$el.querySelectorAll('p .katex')).toHaveLength(0); @@ -177,7 +176,7 @@ describe('Markdown component', () => { '\n', ]); - await vm.$nextTick(); + await nextTick(); // expect one paragraph with no katex formula in it expect(vm.$el.querySelectorAll('p')).toHaveLength(1); expect(vm.$el.querySelectorAll('p .katex')).toHaveLength(1); @@ -186,7 +185,7 @@ describe('Markdown component', () => { it('renders math formula in list object', async () => { vm = buildMarkdownComponent(["- list with inline $a=2$ inline formula $a' + b = c$\n", '\n']); - await vm.$nextTick(); + await nextTick(); // expect one list with a katex formula in it expect(vm.$el.querySelectorAll('li')).toHaveLength(1); expect(vm.$el.querySelectorAll('li .katex')).toHaveLength(2); @@ -195,7 +194,7 @@ describe('Markdown component', () => { it("renders math formula with tick ' in it", async () => { vm = buildMarkdownComponent(["- list with inline $a=2$ inline formula $a' + b = c$\n", '\n']); - await vm.$nextTick(); + await nextTick(); // expect one list with a katex formula in it expect(vm.$el.querySelectorAll('li')).toHaveLength(1); expect(vm.$el.querySelectorAll('li .katex')).toHaveLength(2); @@ -204,7 +203,7 @@ describe('Markdown component', () => { it('renders math formula with less-than-operator < in it', async () => { vm = buildMarkdownComponent(['- list with inline $a=2$ inline formula $a + b < c$\n', '\n']); - await vm.$nextTick(); + await nextTick(); // expect one list with a katex formula in it expect(vm.$el.querySelectorAll('li')).toHaveLength(1); expect(vm.$el.querySelectorAll('li .katex')).toHaveLength(2); @@ -213,7 +212,7 @@ describe('Markdown component', () => { it('renders math formula with greater-than-operator > in it', async () => { vm = buildMarkdownComponent(['- list with inline $a=2$ inline formula $a + b > c$\n', '\n']); - await vm.$nextTick(); + await nextTick(); // expect one list with a katex formula in it expect(vm.$el.querySelectorAll('li')).toHaveLength(1); expect(vm.$el.querySelectorAll('li .katex')).toHaveLength(2); diff --git a/spec/frontend/popovers/index_spec.js b/spec/frontend/popovers/index_spec.js index ea3b78332d7..c82fe7b47d9 100644 --- a/spec/frontend/popovers/index_spec.js +++ b/spec/frontend/popovers/index_spec.js @@ -1,8 +1,7 @@ +import { nextTick } from 'vue'; import { initPopovers, dispose, destroy } from '~/popovers'; describe('popovers/index.js', () => { - let popoversApp; - const createPopoverTarget = (trigger = 'hover') => { const target = document.createElement('button'); const dataset = { @@ -22,7 +21,7 @@ describe('popovers/index.js', () => { }; const buildPopoversApp = () => { - popoversApp = initPopovers('[data-toggle="popover"]'); + initPopovers('[data-toggle="popover"]'); }; const triggerEvent = (target, eventName = 'mouseenter') => { @@ -44,7 +43,7 @@ describe('popovers/index.js', () => { triggerEvent(target); - await popoversApp.$nextTick(); + await nextTick(); const html = document.querySelector('.gl-popover').innerHTML; expect(document.querySelector('.gl-popover')).not.toBe(null); @@ -59,7 +58,7 @@ describe('popovers/index.js', () => { buildPopoversApp(); triggerEvent(target, trigger); - await popoversApp.$nextTick(); + await nextTick(); expect(document.querySelector('.gl-popover')).not.toBe(null); expect(document.querySelector('.gl-popover').innerHTML).toContain('default title'); @@ -73,7 +72,7 @@ describe('popovers/index.js', () => { const trigger = 'click'; const target = createPopoverTarget(trigger); triggerEvent(target, trigger); - await popoversApp.$nextTick(); + await nextTick(); expect(document.querySelector('.gl-popover')).not.toBe(null); }); @@ -86,17 +85,17 @@ describe('popovers/index.js', () => { buildPopoversApp(); triggerEvent(target); triggerEvent(createPopoverTarget()); - await popoversApp.$nextTick(); + await nextTick(); expect(document.querySelectorAll('.gl-popover')).toHaveLength(2); dispose([fakeTarget]); - await popoversApp.$nextTick(); + await nextTick(); expect(document.querySelectorAll('.gl-popover')).toHaveLength(2); dispose([target]); - await popoversApp.$nextTick(); + await nextTick(); expect(document.querySelectorAll('.gl-popover')).toHaveLength(1); }); diff --git a/spec/frontend/prometheus_alerts/components/reset_key_spec.js b/spec/frontend/prometheus_alerts/components/reset_key_spec.js index edf5297cc6a..dc5fdb1dffc 100644 --- a/spec/frontend/prometheus_alerts/components/reset_key_spec.js +++ b/spec/frontend/prometheus_alerts/components/reset_key_spec.js @@ -1,6 +1,7 @@ import { GlModal } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; import axios from '~/lib/utils/axios_utils'; import ResetKey from '~/prometheus_alerts/components/reset_key.vue'; @@ -45,37 +46,31 @@ describe('ResetKey', () => { expect(vm.find('.js-reset-auth-key').text()).toEqual('Reset key'); }); - it('reset updates key', () => { + it('reset updates key', async () => { mock.onPost(propsData.changeKeyUrl).replyOnce(200, { token: 'newToken' }); vm.find(GlModal).vm.$emit('ok'); - return vm.vm - .$nextTick() - .then(waitForPromises) - .then(() => { - expect(vm.vm.authorizationKey).toEqual('newToken'); - expect(vm.find('#authorization-key').attributes('value')).toEqual('newToken'); - }); + await nextTick(); + await waitForPromises(); + expect(vm.vm.authorizationKey).toEqual('newToken'); + expect(vm.find('#authorization-key').attributes('value')).toEqual('newToken'); }); - it('reset key failure shows error', () => { + it('reset key failure shows error', async () => { mock.onPost(propsData.changeKeyUrl).replyOnce(500); vm.find(GlModal).vm.$emit('ok'); - return vm.vm - .$nextTick() - .then(waitForPromises) - .then(() => { - expect(vm.find('#authorization-key').attributes('value')).toEqual( - propsData.initialAuthorizationKey, - ); - - expect(document.querySelector('.flash-container').innerText.trim()).toEqual( - 'Failed to reset key. Please try again.', - ); - }); + await nextTick(); + await waitForPromises(); + expect(vm.find('#authorization-key').attributes('value')).toEqual( + propsData.initialAuthorizationKey, + ); + + expect(document.querySelector('.flash-container').innerText.trim()).toEqual( + 'Failed to reset key. Please try again.', + ); }); }); @@ -92,14 +87,13 @@ describe('ResetKey', () => { expect(vm.find('#authorization-key').attributes('value')).toEqual(''); }); - it('Generate key button triggers key change', () => { + it('Generate key button triggers key change', async () => { mock.onPost(propsData.changeKeyUrl).replyOnce(200, { token: 'newToken' }); vm.find('.js-reset-auth-key').vm.$emit('click'); - return waitForPromises().then(() => { - expect(vm.find('#authorization-key').attributes('value')).toEqual('newToken'); - }); + await waitForPromises(); + expect(vm.find('#authorization-key').attributes('value')).toEqual('newToken'); }); }); }); diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js index fe05a981845..bb710c3a96c 100644 --- a/spec/frontend/repository/components/last_commit_spec.js +++ b/spec/frontend/repository/components/last_commit_spec.js @@ -1,5 +1,6 @@ import { GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import LastCommit from '~/repository/components/last_commit.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; @@ -63,7 +64,7 @@ describe('Repository last commit component', () => { `('$label when loading icon $loading is true', async ({ loading }) => { factory(createCommitData(), loading); - await vm.vm.$nextTick(); + await nextTick(); expect(vm.find(GlLoadingIcon).exists()).toBe(loading); }); @@ -71,7 +72,7 @@ describe('Repository last commit component', () => { it('renders commit widget', async () => { factory(); - await vm.vm.$nextTick(); + await nextTick(); expect(vm.element).toMatchSnapshot(); }); @@ -79,7 +80,7 @@ describe('Repository last commit component', () => { it('renders short commit ID', async () => { factory(); - await vm.vm.$nextTick(); + await nextTick(); expect(vm.find('[data-testid="last-commit-id-label"]').text()).toEqual('12345678'); }); @@ -87,7 +88,7 @@ describe('Repository last commit component', () => { it('hides pipeline components when pipeline does not exist', async () => { factory(createCommitData({ pipeline: null })); - await vm.vm.$nextTick(); + await nextTick(); expect(vm.find('.js-commit-pipeline').exists()).toBe(false); }); @@ -95,7 +96,7 @@ describe('Repository last commit component', () => { it('renders pipeline components', async () => { factory(); - await vm.vm.$nextTick(); + await nextTick(); expect(vm.find('.js-commit-pipeline').exists()).toBe(true); }); @@ -103,7 +104,7 @@ describe('Repository last commit component', () => { it('hides author component when author does not exist', async () => { factory(createCommitData({ author: null })); - await vm.vm.$nextTick(); + await nextTick(); expect(vm.find('.js-user-link').exists()).toBe(false); expect(vm.find(UserAvatarLink).exists()).toBe(false); @@ -112,7 +113,7 @@ describe('Repository last commit component', () => { it('does not render description expander when description is null', async () => { factory(createCommitData({ descriptionHtml: null })); - await vm.vm.$nextTick(); + await nextTick(); expect(vm.find('.text-expander').exists()).toBe(false); expect(vm.find('.commit-row-description').exists()).toBe(false); @@ -121,11 +122,11 @@ describe('Repository last commit component', () => { it('expands commit description when clicking expander', async () => { factory(createCommitData({ descriptionHtml: 'Test description' })); - await vm.vm.$nextTick(); + await nextTick(); vm.find('.text-expander').vm.$emit('click'); - await vm.vm.$nextTick(); + await nextTick(); expect(vm.find('.commit-row-description').isVisible()).toBe(true); expect(vm.find('.text-expander').classes('open')).toBe(true); @@ -134,7 +135,7 @@ describe('Repository last commit component', () => { it('strips the first newline of the description', async () => { factory(createCommitData({ descriptionHtml: '
Update ADOPTERS.md' })); - await vm.vm.$nextTick(); + await nextTick(); expect(vm.find('.commit-row-description').html()).toBe( '<pre class="commit-row-description gl-mb-3">Update ADOPTERS.md</pre>', @@ -144,7 +145,7 @@ describe('Repository last commit component', () => { it('renders the signature HTML as returned by the backend', async () => { factory(createCommitData({ signatureHtml: '<button>Verified</button>' })); - await vm.vm.$nextTick(); + await nextTick(); expect(vm.element).toMatchSnapshot(); }); @@ -152,7 +153,7 @@ describe('Repository last commit component', () => { it('sets correct CSS class if the commit message is empty', async () => { factory(createCommitData({ message: '' })); - await vm.vm.$nextTick(); + await nextTick(); expect(vm.find('.item-title').classes()).toContain(emptyMessageClass); }); diff --git a/spec/frontend/repository/components/preview/index_spec.js b/spec/frontend/repository/components/preview/index_spec.js index 2490258a048..0d9bfc62ed5 100644 --- a/spec/frontend/repository/components/preview/index_spec.js +++ b/spec/frontend/repository/components/preview/index_spec.js @@ -1,5 +1,6 @@ import { GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { handleLocationHash } from '~/lib/utils/common_utils'; import Preview from '~/repository/components/preview/index.vue'; @@ -28,7 +29,7 @@ describe('Repository file preview component', () => { vm.destroy(); }); - it('renders file HTML', () => { + it('renders file HTML', async () => { factory({ webPath: 'http://test.com', name: 'README.md', @@ -38,12 +39,11 @@ describe('Repository file preview component', () => { // eslint-disable-next-line no-restricted-syntax vm.setData({ readme: { html: '<div class="blob">test</div>' } }); - return vm.vm.$nextTick(() => { - expect(vm.element).toMatchSnapshot(); - }); + await nextTick(); + expect(vm.element).toMatchSnapshot(); }); - it('handles hash after render', () => { + it('handles hash after render', async () => { factory({ webPath: 'http://test.com', name: 'README.md', @@ -53,15 +53,11 @@ describe('Repository file preview component', () => { // eslint-disable-next-line no-restricted-syntax vm.setData({ readme: { html: '<div class="blob">test</div>' } }); - return vm.vm - .$nextTick() - .then(vm.vm.$nextTick()) - .then(() => { - expect(handleLocationHash).toHaveBeenCalled(); - }); + await nextTick(); + expect(handleLocationHash).toHaveBeenCalled(); }); - it('renders loading icon', () => { + it('renders loading icon', async () => { factory({ webPath: 'http://test.com', name: 'README.md', @@ -71,8 +67,7 @@ describe('Repository file preview component', () => { // eslint-disable-next-line no-restricted-syntax vm.setData({ loading: 1 }); - return vm.vm.$nextTick(() => { - expect(vm.find(GlLoadingIcon).exists()).toBe(true); - }); + await nextTick(); + expect(vm.find(GlLoadingIcon).exists()).toBe(true); }); }); diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js index 2cd88944f81..07c151ad935 100644 --- a/spec/frontend/repository/components/table/index_spec.js +++ b/spec/frontend/repository/components/table/index_spec.js @@ -1,5 +1,6 @@ import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import Table from '~/repository/components/table/index.vue'; import TableRow from '~/repository/components/table/row.vue'; @@ -86,18 +87,17 @@ describe('Repository table component', () => { ${'/'} | ${'main'} ${'app/assets'} | ${'main'} ${'/'} | ${'test'} - `('renders table caption for $ref in $path', ({ path, ref }) => { + `('renders table caption for $ref in $path', async ({ path, ref }) => { factory({ path }); // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax vm.setData({ ref }); - return vm.vm.$nextTick(() => { - expect(vm.find('.table').attributes('aria-label')).toEqual( - `Files, directories, and submodules in the path ${path} for commit reference ${ref}`, - ); - }); + await nextTick(); + expect(vm.find('.table').attributes('aria-label')).toEqual( + `Files, directories, and submodules in the path ${path} for commit reference ${ref}`, + ); }); it('shows loading icon', () => { @@ -140,7 +140,7 @@ describe('Repository table component', () => { showMoreButton().vm.$emit('click'); - await vm.vm.$nextTick(); + await nextTick(); expect(vm.emitted('showMore')).toHaveLength(1); }); diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js index 440baa72a3c..22570b2d6ed 100644 --- a/spec/frontend/repository/components/table/row_spec.js +++ b/spec/frontend/repository/components/table/row_spec.js @@ -1,5 +1,6 @@ import { GlBadge, GlLink, GlIcon, GlIntersectionObserver } from '@gitlab/ui'; import { shallowMount, RouterLinkStub } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import TableRow from '~/repository/components/table/row.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue'; @@ -53,7 +54,7 @@ describe('Repository table row component', () => { vm.destroy(); }); - it('renders table row', () => { + it('renders table row', async () => { factory({ id: '1', sha: '123', @@ -62,12 +63,11 @@ describe('Repository table row component', () => { currentPath: '/', }); - return vm.vm.$nextTick().then(() => { - expect(vm.element).toMatchSnapshot(); - }); + await nextTick(); + expect(vm.element).toMatchSnapshot(); }); - it('renders a symlink table row', () => { + it('renders a symlink table row', async () => { factory({ id: '1', sha: '123', @@ -77,12 +77,11 @@ describe('Repository table row component', () => { mode: FILE_SYMLINK_MODE, }); - return vm.vm.$nextTick().then(() => { - expect(vm.element).toMatchSnapshot(); - }); + await nextTick(); + expect(vm.element).toMatchSnapshot(); }); - it('renders table row for path with special character', () => { + it('renders table row for path with special character', async () => { factory({ id: '1', sha: '123', @@ -91,9 +90,8 @@ describe('Repository table row component', () => { currentPath: 'test$', }); - return vm.vm.$nextTick().then(() => { - expect(vm.element).toMatchSnapshot(); - }); + await nextTick(); + expect(vm.element).toMatchSnapshot(); }); it('renders a gl-hover-load directive', () => { @@ -116,7 +114,7 @@ describe('Repository table row component', () => { ${'tree'} | ${RouterLinkStub} | ${'RouterLink'} ${'blob'} | ${RouterLinkStub} | ${'RouterLink'} ${'commit'} | ${'a'} | ${'hyperlink'} - `('renders a $componentName for type $type', ({ type, component }) => { + `('renders a $componentName for type $type', async ({ type, component }) => { factory({ id: '1', sha: '123', @@ -125,16 +123,15 @@ describe('Repository table row component', () => { currentPath: '/', }); - return vm.vm.$nextTick().then(() => { - expect(vm.find(component).exists()).toBe(true); - }); + await nextTick(); + expect(vm.find(component).exists()).toBe(true); }); it.each` path ${'test#'} ${'Änderungen'} - `('renders link for $path', ({ path }) => { + `('renders link for $path', async ({ path }) => { factory({ id: '1', sha: '123', @@ -143,14 +140,13 @@ describe('Repository table row component', () => { currentPath: '/', }); - return vm.vm.$nextTick().then(() => { - expect(vm.find({ ref: 'link' }).props('to')).toEqual({ - path: `/-/tree/main/${encodeURIComponent(path)}`, - }); + await nextTick(); + expect(vm.find({ ref: 'link' }).props('to')).toEqual({ + path: `/-/tree/main/${encodeURIComponent(path)}`, }); }); - it('renders link for directory with hash', () => { + it('renders link for directory with hash', async () => { factory({ id: '1', sha: '123', @@ -159,12 +155,11 @@ describe('Repository table row component', () => { currentPath: '/', }); - return vm.vm.$nextTick().then(() => { - expect(vm.find('.tree-item-link').props('to')).toEqual({ path: '/-/tree/main/test%23' }); - }); + await nextTick(); + expect(vm.find('.tree-item-link').props('to')).toEqual({ path: '/-/tree/main/test%23' }); }); - it('renders commit ID for submodule', () => { + it('renders commit ID for submodule', async () => { factory({ id: '1', sha: '123', @@ -173,12 +168,11 @@ describe('Repository table row component', () => { currentPath: '/', }); - return vm.vm.$nextTick().then(() => { - expect(vm.find('.commit-sha').text()).toContain('1'); - }); + await nextTick(); + expect(vm.find('.commit-sha').text()).toContain('1'); }); - it('renders link with href', () => { + it('renders link with href', async () => { factory({ id: '1', sha: '123', @@ -188,12 +182,11 @@ describe('Repository table row component', () => { currentPath: '/', }); - return vm.vm.$nextTick().then(() => { - expect(vm.find('a').attributes('href')).toEqual('https://test.com'); - }); + await nextTick(); + expect(vm.find('a').attributes('href')).toEqual('https://test.com'); }); - it('renders LFS badge', () => { + it('renders LFS badge', async () => { factory({ id: '1', sha: '123', @@ -203,12 +196,11 @@ describe('Repository table row component', () => { lfsOid: '1', }); - return vm.vm.$nextTick().then(() => { - expect(vm.find(GlBadge).exists()).toBe(true); - }); + await nextTick(); + expect(vm.find(GlBadge).exists()).toBe(true); }); - it('renders commit and web links with href for submodule', () => { + it('renders commit and web links with href for submodule', async () => { factory({ id: '1', sha: '123', @@ -219,13 +211,12 @@ describe('Repository table row component', () => { currentPath: '/', }); - return vm.vm.$nextTick().then(() => { - expect(vm.find('a').attributes('href')).toEqual('https://test.com'); - expect(vm.find(GlLink).attributes('href')).toEqual('https://test.com/commit'); - }); + await nextTick(); + expect(vm.find('a').attributes('href')).toEqual('https://test.com'); + expect(vm.find(GlLink).attributes('href')).toEqual('https://test.com/commit'); }); - it('renders lock icon', () => { + it('renders lock icon', async () => { factory({ id: '1', sha: '123', @@ -234,10 +225,9 @@ describe('Repository table row component', () => { currentPath: '/', }); - return vm.vm.$nextTick().then(() => { - expect(vm.find(GlIcon).exists()).toBe(true); - expect(vm.find(GlIcon).props('name')).toBe('lock'); - }); + await nextTick(); + expect(vm.find(GlIcon).exists()).toBe(true); + expect(vm.find(GlIcon).props('name')).toBe('lock'); }); it('renders loading icon when path is loading', () => { diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js index 00ad1fc05f6..9d3a5394df8 100644 --- a/spec/frontend/repository/components/tree_content_spec.js +++ b/spec/frontend/repository/components/tree_content_spec.js @@ -1,4 +1,5 @@ import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql'; import FilePreview from '~/repository/components/preview/index.vue'; import FileTable from '~/repository/components/table/index.vue'; @@ -50,7 +51,7 @@ describe('Repository table component', () => { // eslint-disable-next-line no-restricted-syntax vm.setData({ entries: { blobs: [{ name: 'README.md' }] } }); - await vm.vm.$nextTick(); + await nextTick(); expect(vm.find(FilePreview).exists()).toBe(true); }); @@ -60,7 +61,7 @@ describe('Repository table component', () => { jest.spyOn(vm.vm, 'fetchFiles').mockImplementation(() => {}); - await vm.vm.$nextTick(); + await nextTick(); expect(vm.vm.fetchFiles).toHaveBeenCalled(); expect(resetRequestedCommits).toHaveBeenCalled(); @@ -111,7 +112,7 @@ describe('Repository table component', () => { it('is changes hasShowMore to false when "showMore" event is emitted', async () => { findFileTable().vm.$emit('showMore'); - await vm.vm.$nextTick(); + await nextTick(); expect(vm.vm.hasShowMore).toBe(false); }); @@ -119,7 +120,7 @@ describe('Repository table component', () => { it('changes clickedShowMore when "showMore" event is emitted', async () => { findFileTable().vm.$emit('showMore'); - await vm.vm.$nextTick(); + await nextTick(); expect(vm.vm.clickedShowMore).toBe(true); }); @@ -140,7 +141,7 @@ describe('Repository table component', () => { // eslint-disable-next-line no-restricted-syntax vm.setData({ fetchCounter: 5, clickedShowMore: false }); - await vm.vm.$nextTick(); + await nextTick(); expect(vm.vm.hasShowMore).toBe(false); }); @@ -161,7 +162,7 @@ describe('Repository table component', () => { // eslint-disable-next-line no-restricted-syntax vm.setData({ entries: { blobs }, pagesLoaded }); - await vm.vm.$nextTick(); + await nextTick(); expect(findFileTable().props('hasMore')).toBe(limitReached); }); diff --git a/spec/frontend/serverless/components/functions_spec.js b/spec/frontend/serverless/components/functions_spec.js index 1e33e56c2f3..6331f897a87 100644 --- a/spec/frontend/serverless/components/functions_spec.js +++ b/spec/frontend/serverless/components/functions_spec.js @@ -1,6 +1,6 @@ import { GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import AxiosMockAdapter from 'axios-mock-adapter'; import Vuex from 'vuex'; import { TEST_HOST } from 'helpers/test_constants'; @@ -73,15 +73,14 @@ describe('functionsComponent', () => { expect(component.find('.js-functions-loader').exists()).toBe(true); }); - it('should render the functions list', () => { + it('should render the functions list', async () => { store = createStore({ clustersPath: 'clustersPath', helpPath: 'helpPath', statusPath }); component = shallowMount(functionsComponent, { store }); - component.vm.$store.dispatch('receiveFunctionsSuccess', mockServerlessFunctions); + await component.vm.$store.dispatch('receiveFunctionsSuccess', mockServerlessFunctions); - return component.vm.$nextTick().then(() => { - expect(component.find(EnvironmentRow).exists()).toBe(true); - }); + await nextTick(); + expect(component.find(EnvironmentRow).exists()).toBe(true); }); }); diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js index 172089f9ee6..c395112e313 100644 --- a/spec/frontend/snippets/components/snippet_blob_view_spec.js +++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js @@ -86,21 +86,17 @@ describe('Blob Embeddable', () => { expect(wrapper.find(RichViewer).exists()).toBe(true); }); - it('correctly switches viewer type', () => { + it('correctly switches viewer type', async () => { createComponent(); expect(wrapper.find(SimpleViewer).exists()).toBe(true); wrapper.vm.switchViewer(RichViewerMock.type); - return wrapper.vm - .$nextTick() - .then(() => { - expect(wrapper.find(RichViewer).exists()).toBe(true); - wrapper.vm.switchViewer(SimpleViewerMock.type); - }) - .then(() => { - expect(wrapper.find(SimpleViewer).exists()).toBe(true); - }); + await nextTick(); + expect(wrapper.find(RichViewer).exists()).toBe(true); + await wrapper.vm.switchViewer(SimpleViewerMock.type); + + expect(wrapper.find(SimpleViewer).exists()).toBe(true); }); it('passes information about render error down to blob header', () => { @@ -191,22 +187,18 @@ describe('Blob Embeddable', () => { }); describe('switchViewer()', () => { - it('switches to the passed viewer', () => { + it('switches to the passed viewer', async () => { createComponent(); wrapper.vm.switchViewer(RichViewerMock.type); - return wrapper.vm - .$nextTick() - .then(() => { - expect(wrapper.vm.activeViewerType).toBe(RichViewerMock.type); - expect(wrapper.find(RichViewer).exists()).toBe(true); - - wrapper.vm.switchViewer(SimpleViewerMock.type); - }) - .then(() => { - expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type); - expect(wrapper.find(SimpleViewer).exists()).toBe(true); - }); + + await nextTick(); + expect(wrapper.vm.activeViewerType).toBe(RichViewerMock.type); + expect(wrapper.find(RichViewer).exists()).toBe(true); + + await wrapper.vm.switchViewer(SimpleViewerMock.type); + expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type); + expect(wrapper.find(SimpleViewer).exists()).toBe(true); }); }); }); diff --git a/spec/frontend/tooltips/index_spec.js b/spec/frontend/tooltips/index_spec.js index 9c03ca8f4c9..198a0315ef7 100644 --- a/spec/frontend/tooltips/index_spec.js +++ b/spec/frontend/tooltips/index_spec.js @@ -1,3 +1,4 @@ +import { nextTick } from 'vue'; import { add, initTooltips, @@ -57,7 +58,7 @@ describe('tooltips/index.js', () => { triggerEvent(target); - await tooltipsApp.$nextTick(); + await nextTick(); expect(document.querySelector('.gl-tooltip')).not.toBe(null); expect(document.querySelector('.gl-tooltip').innerHTML).toContain('default title'); @@ -69,7 +70,7 @@ describe('tooltips/index.js', () => { buildTooltipsApp(); triggerEvent(target, 'click'); - await tooltipsApp.$nextTick(); + await nextTick(); expect(document.querySelector('.gl-tooltip')).not.toBe(null); expect(document.querySelector('.gl-tooltip').innerHTML).toContain('default title'); @@ -83,7 +84,7 @@ describe('tooltips/index.js', () => { buildTooltipsApp(); add([target], { title: 'custom title' }); - await tooltipsApp.$nextTick(); + await nextTick(); expect(document.querySelector('.gl-tooltip')).not.toBe(null); expect(document.querySelector('.gl-tooltip').innerHTML).toContain('custom title'); @@ -97,13 +98,13 @@ describe('tooltips/index.js', () => { buildTooltipsApp(); triggerEvent(target); - await tooltipsApp.$nextTick(); + await nextTick(); expect(document.querySelector('.gl-tooltip')).not.toBe(null); dispose([target]); - await tooltipsApp.$nextTick(); + await nextTick(); expect(document.querySelector('.gl-tooltip')).toBe(null); }); @@ -122,7 +123,7 @@ describe('tooltips/index.js', () => { buildTooltipsApp(); - await tooltipsApp.$nextTick(); + await nextTick(); jest.spyOn(tooltipsApp, 'triggerEvent'); @@ -137,7 +138,7 @@ describe('tooltips/index.js', () => { buildTooltipsApp(); - await tooltipsApp.$nextTick(); + await nextTick(); jest.spyOn(tooltipsApp, 'fixTitle'); diff --git a/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js b/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js index 1aeb080aa04..82526af7afa 100644 --- a/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js +++ b/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js @@ -1,5 +1,6 @@ import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import MrCollapsibleSection from '~/vue_merge_request_widget/components/mr_collapsible_extension.vue'; describe('Merge Request Collapsible Extension', () => { @@ -46,9 +47,9 @@ describe('Merge Request Collapsible Extension', () => { }); describe('onClick', () => { - beforeEach(() => { + beforeEach(async () => { wrapper.find('button').trigger('click'); - return wrapper.vm.$nextTick(); + await nextTick(); }); it('rendes the provided slot', () => { diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_author_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_author_spec.js index e7c10ab4c2d..8a42e2e2ce7 100644 --- a/spec/frontend/vue_mr_widget/components/mr_widget_author_spec.js +++ b/spec/frontend/vue_mr_widget/components/mr_widget_author_spec.js @@ -1,4 +1,5 @@ import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import MrWidgetAuthor from '~/vue_merge_request_widget/components/mr_widget_author.vue'; window.gl = window.gl || {}; @@ -50,7 +51,7 @@ describe('MrWidgetAuthor', () => { }, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.find('img').attributes('src')).toBe('no_avatar.png'); }); diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_expandable_section_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_expandable_section_spec.js index 3e111cd308a..631aef412a6 100644 --- a/spec/frontend/vue_mr_widget/components/mr_widget_expandable_section_spec.js +++ b/spec/frontend/vue_mr_widget/components/mr_widget_expandable_section_spec.js @@ -1,5 +1,6 @@ import { GlButton, GlCollapse, GlIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import MrCollapsibleSection from '~/vue_merge_request_widget/components/mr_widget_expandable_section.vue'; describe('MrWidgetExpanableSection', () => { @@ -43,9 +44,9 @@ describe('MrWidgetExpanableSection', () => { }); describe('when collapse section is open', () => { - beforeEach(() => { + beforeEach(async () => { findButton().vm.$emit('click'); - return wrapper.vm.$nextTick(); + await nextTick(); }); it('renders button with collapse text', () => { diff --git a/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js b/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js index c30f6f1dfd1..c0add94e6ed 100644 --- a/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js @@ -1,4 +1,5 @@ import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import CommitEdit from '~/vue_merge_request_widget/components/states/commit_edit.vue'; const testCommitMessage = 'Test commit message'; @@ -46,16 +47,15 @@ describe('Commits edit component', () => { expect(findTextarea().element.value).toBe(testCommitMessage); }); - it('emits an input event and receives changed value', () => { + it('emits an input event and receives changed value', async () => { const changedCommitMessage = 'Changed commit message'; findTextarea().element.value = changedCommitMessage; findTextarea().trigger('input'); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted().input[0]).toEqual([changedCommitMessage]); - expect(findTextarea().element.value).toBe(changedCommitMessage); - }); + await nextTick(); + expect(wrapper.emitted().input[0]).toEqual([changedCommitMessage]); + expect(findTextarea().element.value).toBe(changedCommitMessage); }); }); diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js index 4c763f40cbe..663fabb761c 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js @@ -1,5 +1,6 @@ import { GlDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue'; const commits = [ @@ -51,11 +52,10 @@ describe('Commits message dropdown component', () => { expect(findFirstDropdownElement().text()).toContain('Commit 1'); }); - it('should emit a commit title on selecting commit', () => { + it('should emit a commit title on selecting commit', async () => { findFirstDropdownElement().vm.$emit('click'); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted().input[0]).toEqual(['Update test.txt']); - }); + await nextTick(); + expect(wrapper.emitted().input[0]).toEqual(['Update test.txt']); }); }); diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js index 4d05e732f48..2796403b7d0 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js @@ -1,5 +1,6 @@ import { mount } from '@vue/test-utils'; import { GlSprintf } from '@gitlab/ui'; +import { nextTick } from 'vue'; import CommitsHeader from '~/vue_merge_request_widget/components/states/commits_header.vue'; describe('Commits header component', () => { @@ -58,15 +59,14 @@ describe('Commits header component', () => { expect(findCommitToggle().attributes('aria-label')).toBe('Expand'); }); - it('has a chevron-right icon', () => { + it('has a chevron-right icon', async () => { createComponent(); // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax wrapper.setData({ expanded: false }); - return wrapper.vm.$nextTick().then(() => { - expect(findCommitToggle().props('icon')).toBe('chevron-right'); - }); + await nextTick(); + expect(findCommitToggle().props('icon')).toBe('chevron-right'); }); describe('when squash is disabled', () => { @@ -118,25 +118,19 @@ describe('Commits header component', () => { wrapper.setData({ expanded: true }); }); - it('toggle has aria-label equal to collapse', (done) => { - wrapper.vm.$nextTick(() => { - expect(findCommitToggle().attributes('aria-label')).toBe('Collapse'); - done(); - }); + it('toggle has aria-label equal to collapse', async () => { + await nextTick(); + expect(findCommitToggle().attributes('aria-label')).toBe('Collapse'); }); - it('has a chevron-down icon', (done) => { - wrapper.vm.$nextTick(() => { - expect(findCommitToggle().props('icon')).toBe('chevron-down'); - done(); - }); + it('has a chevron-down icon', async () => { + await nextTick(); + expect(findCommitToggle().props('icon')).toBe('chevron-down'); }); - it('has a collapse text', (done) => { - wrapper.vm.$nextTick(() => { - expect(findHeaderWrapper().text()).toBe('Collapse'); - done(); - }); + it('has a collapse text', async () => { + await nextTick(); + expect(findHeaderWrapper().text()).toBe('Collapse'); }); }); }); diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js index ec222e66a97..9dcde3e4f33 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js @@ -1,4 +1,5 @@ import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { TEST_HOST } from 'helpers/test_constants'; import { removeBreakLine } from 'helpers/text_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; @@ -20,7 +21,7 @@ describe('MRWidgetConflicts', () => { const resolveConflictsBtnText = 'Resolve conflicts'; const mergeLocallyBtnText = 'Merge locally'; - function createComponent(propsData = {}) { + async function createComponent(propsData = {}) { wrapper = extendedWrapper( shallowMount(ConflictsComponent, { propsData, @@ -55,7 +56,7 @@ describe('MRWidgetConflicts', () => { }); } - return wrapper.vm.$nextTick(); + await nextTick(); } afterEach(() => { diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js index ec5ec989d0f..ddce07954ab 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js @@ -1,9 +1,10 @@ import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import MissingBranchComponent from '~/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue'; let wrapper; -function factory(sourceBranchRemoved, mergeRequestWidgetGraphql) { +async function factory(sourceBranchRemoved, mergeRequestWidgetGraphql) { wrapper = shallowMount(MissingBranchComponent, { propsData: { mr: { sourceBranchRemoved }, @@ -19,7 +20,7 @@ function factory(sourceBranchRemoved, mergeRequestWidgetGraphql) { wrapper.setData({ state: { sourceBranchExists: !sourceBranchRemoved } }); } - return wrapper.vm.$nextTick(); + await nextTick(); } describe('MRWidgetMissingBranch', () => { diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index b4ef40ccd72..8b4a0b4c28e 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -710,7 +710,7 @@ describe('ReadyToMerge', () => { commitsWithoutMergeCommits: {}, }, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findCommitEditElements().length).toBe(2); }); diff --git a/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js b/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js index 9048975875a..b7c22b403aa 100644 --- a/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js +++ b/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js @@ -1,6 +1,7 @@ import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; import axios from '~/lib/utils/axios_utils'; import Poll from '~/lib/utils/poll'; import MrWidgetExpanableSection from '~/vue_merge_request_widget/components/mr_widget_expandable_section.vue'; @@ -39,15 +40,14 @@ describe('MrWidgetTerraformConainer', () => { }); describe('when data is loading', () => { - beforeEach(() => { + beforeEach(async () => { mockPollingApi(200, plans, {}); - return mountWrapper().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({ loading: true }); - return wrapper.vm.$nextTick(); - }); + await mountWrapper(); + // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details + // eslint-disable-next-line no-restricted-syntax + wrapper.setData({ loading: true }); + await nextTick(); }); it('diplays loading skeleton', () => { diff --git a/spec/frontend/vue_shared/alert_details/alert_details_spec.js b/spec/frontend/vue_shared/alert_details/alert_details_spec.js index 221beed744b..7ee6e29e6de 100644 --- a/spec/frontend/vue_shared/alert_details/alert_details_spec.js +++ b/spec/frontend/vue_shared/alert_details/alert_details_spec.js @@ -2,6 +2,7 @@ import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { joinPaths } from '~/lib/utils/url_utility'; @@ -216,17 +217,16 @@ describe('AlertDetails', () => { expect(findCreateIncidentBtn().exists()).toBe(false); }); - it('should display "Create incident" button when incident doesn\'t exist yet', () => { + it('should display "Create incident" button when incident doesn\'t exist yet', async () => { const issue = null; mountComponent({ mountMethod: mount, data: { alert: { ...mockAlert, issue } }, }); - return wrapper.vm.$nextTick().then(() => { - expect(findViewIncidentBtn().exists()).toBe(false); - expect(findCreateIncidentBtn().exists()).toBe(true); - }); + await nextTick(); + expect(findViewIncidentBtn().exists()).toBe(false); + expect(findCreateIncidentBtn().exists()).toBe(true); }); it('calls `$apollo.mutate` with `createIssueQuery`', () => { diff --git a/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js b/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js index 87ad5e36564..12c5c190e26 100644 --- a/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js +++ b/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js @@ -1,4 +1,5 @@ import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql'; import SidebarTodo from '~/vue_shared/alert_details/components/sidebar/sidebar_todo.vue'; import createAlertTodoMutation from '~/vue_shared/alert_details/graphql/mutations/alert_todo_create.mutation.graphql'; @@ -57,7 +58,7 @@ describe('Alert Details Sidebar To Do', () => { }); it('renders a button for adding a To-Do', async () => { - await wrapper.vm.$nextTick(); + await nextTick(); expect(findToDoButton().text()).toBe('Add a to do'); }); @@ -66,7 +67,7 @@ describe('Alert Details Sidebar To Do', () => { jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult); findToDoButton().trigger('click'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ mutation: createAlertTodoMutation, @@ -88,7 +89,7 @@ describe('Alert Details Sidebar To Do', () => { }); it('renders a Mark As Done button when todo is present', async () => { - await wrapper.vm.$nextTick(); + await nextTick(); expect(findToDoButton().text()).toBe('Mark as done'); }); @@ -97,7 +98,7 @@ describe('Alert Details Sidebar To Do', () => { jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult); findToDoButton().trigger('click'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ mutation: todoMarkDoneMutation, diff --git a/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js b/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js index b5a61a4adc1..1216681038f 100644 --- a/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js +++ b/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js @@ -1,6 +1,7 @@ import { shallowMount } from '@vue/test-utils'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue'; import AlertMetrics from '~/vue_shared/alert_details/components/alert_metrics.vue'; @@ -53,7 +54,7 @@ describe('Alert Metrics', () => { mountComponent({ props: { dashboardUrl: 'metrics.url' } }); await waitForPromises(); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findEmptyState().exists()).toBe(false); expect(findChart().exists()).toBe(true); diff --git a/spec/frontend/vue_shared/alert_details/alert_status_spec.js b/spec/frontend/vue_shared/alert_details/alert_status_spec.js index 3fc13243bce..369e9ac18a4 100644 --- a/spec/frontend/vue_shared/alert_details/alert_status_spec.js +++ b/spec/frontend/vue_shared/alert_details/alert_status_spec.js @@ -1,4 +1,5 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import updateAlertStatusMutation from '~/graphql_shared//mutations/alert_status_update.mutation.graphql'; @@ -121,7 +122,7 @@ describe('AlertManagementStatus', () => { it('emits an error when triggered a second time', async () => { await selectFirstStatusOption(); - await wrapper.vm.$nextTick(); + await nextTick(); await selectFirstStatusOption(); // Should emit two errors [0,1] expect(wrapper.emitted('alert-error').length > 1).toBe(true); diff --git a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js index 29e0eee2c9a..29569734621 100644 --- a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js +++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js @@ -1,6 +1,7 @@ import { GlDropdownItem } from '@gitlab/ui'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import SidebarAssignee from '~/vue_shared/alert_details/components/sidebar/sidebar_assignee.vue'; import SidebarAssignees from '~/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue'; @@ -112,7 +113,7 @@ describe('Alert Details Sidebar Assignees', () => { // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax wrapper.setData({ isDropdownSearching: false }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findDropdown().text()).toBe('Unassigned'); }); @@ -126,7 +127,7 @@ describe('Alert Details Sidebar Assignees', () => { // eslint-disable-next-line no-restricted-syntax wrapper.setData({ isDropdownSearching: false }); - await wrapper.vm.$nextTick(); + await nextTick(); wrapper.find(SidebarAssignee).vm.$emit('update-alert-assignees', 'root'); expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ @@ -139,7 +140,7 @@ describe('Alert Details Sidebar Assignees', () => { }); }); - it('emits an error when request contains error messages', () => { + it('emits an error when request contains error messages', 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({ isDropdownSearching: false }); @@ -153,15 +154,11 @@ describe('Alert Details Sidebar Assignees', () => { }; jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(errorMutationResult); - return wrapper.vm - .$nextTick() - .then(() => { - const SideBarAssigneeItem = wrapper.findAll(SidebarAssignee).at(0); - SideBarAssigneeItem.vm.$emit('update-alert-assignees'); - }) - .then(() => { - expect(wrapper.emitted('alert-error')).toBeDefined(); - }); + + await nextTick(); + const SideBarAssigneeItem = wrapper.findAll(SidebarAssignee).at(0); + await SideBarAssigneeItem.vm.$emit('update-alert-assignees'); + expect(wrapper.emitted('alert-error')).toBeDefined(); }); it('stops updating and cancels loading when the request fails', () => { diff --git a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js index b00a20dab1a..a3adbcf8d3a 100644 --- a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js +++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js @@ -1,4 +1,5 @@ import { GlDropdown, GlLoadingIcon } from '@gitlab/ui'; +import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import AlertStatus from '~/vue_shared/alert_details/components/alert_status.vue'; import AlertSidebarStatus from '~/vue_shared/alert_details/components/sidebar/sidebar_status.vue'; @@ -75,7 +76,7 @@ describe('Alert Details Sidebar Status', () => { loading: false, }); findAlertStatus().vm.$emit('handle-updating', true); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findStatusLoadingIcon().exists()).toBe(true); }); diff --git a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js index 3277aab43f0..1ff98d4aec5 100644 --- a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js +++ b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js @@ -1,4 +1,5 @@ import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { HIGHLIGHT_CLASS_NAME } from '~/vue_shared/components/blob_viewers/constants'; import SimpleViewer from '~/vue_shared/components/blob_viewers/simple_viewer.vue'; @@ -69,7 +70,7 @@ describe('Blob Simple Viewer component', () => { expect(linetoBeHighlighted.classes()).toContain(HIGHLIGHT_CLASS_NAME); }); - it('switches highlighting when another line is selected', () => { + it('switches highlighting when another line is selected', async () => { const currentlyHighlighted = wrapper.find('#LC2'); const hash = '#LC3'; const linetoBeHighlighted = wrapper.find(hash); @@ -78,11 +79,10 @@ describe('Blob Simple Viewer component', () => { wrapper.vm.scrollToLine(hash); - return wrapper.vm.$nextTick(() => { - expect(wrapper.vm.highlightedLine).toBe(linetoBeHighlighted.element); - expect(currentlyHighlighted.classes()).not.toContain(HIGHLIGHT_CLASS_NAME); - expect(linetoBeHighlighted.classes()).toContain(HIGHLIGHT_CLASS_NAME); - }); + await nextTick(); + expect(wrapper.vm.highlightedLine).toBe(linetoBeHighlighted.element); + expect(currentlyHighlighted.classes()).not.toContain(HIGHLIGHT_CLASS_NAME); + expect(linetoBeHighlighted.classes()).toContain(HIGHLIGHT_CLASS_NAME); }); }); }); diff --git a/spec/frontend/vue_shared/components/chronic_duration_input_spec.js b/spec/frontend/vue_shared/components/chronic_duration_input_spec.js index 083a5f60d1d..6932a812287 100644 --- a/spec/frontend/vue_shared/components/chronic_duration_input_spec.js +++ b/spec/frontend/vue_shared/components/chronic_duration_input_spec.js @@ -1,4 +1,5 @@ import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import ChronicDurationInput from '~/vue_shared/components/chronic_duration_input.vue'; const MOCK_VALUE = 2 * 3600 + 20 * 60; @@ -48,7 +49,7 @@ describe('vue_shared/components/chronic_duration_input', () => { describe('change', () => { const createAndDispatch = async (initialValue, humanReadableInput) => { createComponent({ value: initialValue }); - await wrapper.vm.$nextTick(); + await nextTick(); textElement.value = humanReadableInput; textElement.dispatchEvent(new Event('input')); }; @@ -118,7 +119,7 @@ describe('vue_shared/components/chronic_duration_input', () => { it('emits valid with user input', async () => { textElement.value = '1m10s'; textElement.dispatchEvent(new Event('input')); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted('valid')).toEqual([ [{ valid: true, feedback: '' }], @@ -133,7 +134,7 @@ describe('vue_shared/components/chronic_duration_input', () => { textElement.value = ''; textElement.dispatchEvent(new Event('input')); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted('valid')).toEqual([ [{ valid: true, feedback: '' }], @@ -151,7 +152,7 @@ describe('vue_shared/components/chronic_duration_input', () => { it('emits invalid with user input', async () => { textElement.value = 'gobbledygook'; textElement.dispatchEvent(new Event('input')); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted('valid')).toEqual([ [{ valid: true, feedback: '' }], @@ -186,7 +187,7 @@ describe('vue_shared/components/chronic_duration_input', () => { it('emits valid with updated value', async () => { wrapper.setProps({ value: MOCK_VALUE }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted('valid')).toEqual([ [{ valid: null, feedback: '' }], @@ -210,7 +211,7 @@ describe('vue_shared/components/chronic_duration_input', () => { it('emits valid when input is integer', async () => { textElement.value = '2hr20min'; textElement.dispatchEvent(new Event('input')); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted('change')).toEqual([[MOCK_VALUE]]); expect(wrapper.emitted('valid')).toEqual([ @@ -228,7 +229,7 @@ describe('vue_shared/components/chronic_duration_input', () => { it('emits valid when input is decimal', async () => { textElement.value = '1.5s'; textElement.dispatchEvent(new Event('input')); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted('change')).toEqual([[1.5]]); expect(wrapper.emitted('valid')).toEqual([ @@ -252,7 +253,7 @@ describe('vue_shared/components/chronic_duration_input', () => { it('emits valid when input is integer', async () => { textElement.value = '2hr20min'; textElement.dispatchEvent(new Event('input')); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted('change')).toEqual([[MOCK_VALUE]]); expect(wrapper.emitted('valid')).toEqual([ @@ -270,7 +271,7 @@ describe('vue_shared/components/chronic_duration_input', () => { it('emits invalid when input is decimal', async () => { textElement.value = '1.5s'; textElement.dispatchEvent(new Event('input')); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted('change')).toBeUndefined(); expect(wrapper.emitted('valid')).toEqual([ @@ -318,7 +319,7 @@ describe('vue_shared/components/chronic_duration_input', () => { // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax wrapper.setData({ value: MOCK_VALUE }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(textElement.value).toBe('2 hrs 20 mins'); expect(hiddenElement.value).toBe(MOCK_VALUE.toString()); @@ -329,7 +330,7 @@ describe('vue_shared/components/chronic_duration_input', () => { it('passes user input to parent via v-model', async () => { textElement.value = '2hr20min'; textElement.dispatchEvent(new Event('input')); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.findComponent(ChronicDurationInput).props('value')).toBe(MOCK_VALUE); expect(textElement.value).toBe('2hr20min'); @@ -377,7 +378,7 @@ describe('vue_shared/components/chronic_duration_input', () => { it('creates form data with user-specified value', async () => { textElement.value = '1m10s'; textElement.dispatchEvent(new Event('input')); - await wrapper.vm.$nextTick(); + await nextTick(); const formData = new FormData(wrapper.find('[data-testid=myForm]').element); const iter = formData.entries(); diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js index 33667a1bb71..d4b6b987c69 100644 --- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js +++ b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js @@ -1,5 +1,6 @@ import { mount } from '@vue/test-utils'; import timezoneMock from 'timezone-mock'; +import { nextTick } from 'vue'; import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue'; import { defaultTimeRanges, @@ -29,26 +30,23 @@ describe('DateTimePicker', () => { wrapper.destroy(); }); - it('renders dropdown toggle button with selected text', () => { + it('renders dropdown toggle button with selected text', async () => { createComponent(); - return wrapper.vm.$nextTick(() => { - expect(dropdownToggle().text()).toBe(defaultTimeRange.label); - }); + await nextTick(); + expect(dropdownToggle().text()).toBe(defaultTimeRange.label); }); - it('renders dropdown toggle button with selected text and utc label', () => { + it('renders dropdown toggle button with selected text and utc label', async () => { createComponent({ utc: true }); - return wrapper.vm.$nextTick(() => { - expect(dropdownToggle().text()).toContain(defaultTimeRange.label); - expect(dropdownToggle().text()).toContain('UTC'); - }); + await nextTick(); + expect(dropdownToggle().text()).toContain(defaultTimeRange.label); + expect(dropdownToggle().text()).toContain('UTC'); }); - it('renders dropdown with 2 custom time range inputs', () => { + it('renders dropdown with 2 custom time range inputs', async () => { createComponent(); - return wrapper.vm.$nextTick(() => { - expect(wrapper.findAll('input').length).toBe(2); - }); + await nextTick(); + expect(wrapper.findAll('input').length).toBe(2); }); describe('renders label with h/m/s truncated if possible', () => { @@ -80,33 +78,30 @@ describe('DateTimePicker', () => { label: '2019-10-10 00:00:01 to 2019-10-10 00:00:01 UTC', }, ].forEach(({ start, end, utc, label }) => { - it(`for start ${start}, end ${end}, and utc ${utc}, label is ${label}`, () => { + it(`for start ${start}, end ${end}, and utc ${utc}, label is ${label}`, async () => { createComponent({ value: { start, end }, utc, }); - return wrapper.vm.$nextTick(() => { - expect(dropdownToggle().text()).toBe(label); - }); + await nextTick(); + expect(dropdownToggle().text()).toBe(label); }); }); }); - it(`renders dropdown with ${optionsCount} (default) items in quick range`, () => { + it(`renders dropdown with ${optionsCount} (default) items in quick range`, async () => { createComponent(); dropdownToggle().trigger('click'); - return wrapper.vm.$nextTick(() => { - expect(findQuickRangeItems().length).toBe(optionsCount); - }); + await nextTick(); + expect(findQuickRangeItems().length).toBe(optionsCount); }); - it('renders dropdown with a default quick range item selected', () => { + it('renders dropdown with a default quick range item selected', async () => { createComponent(); dropdownToggle().trigger('click'); - return wrapper.vm.$nextTick(() => { - expect(wrapper.find('.dropdown-item.active').exists()).toBe(true); - expect(wrapper.find('.dropdown-item.active').text()).toBe(defaultTimeRange.label); - }); + await nextTick(); + expect(wrapper.find('.dropdown-item.active').exists()).toBe(true); + expect(wrapper.find('.dropdown-item.active').text()).toBe(defaultTimeRange.label); }); it('renders a disabled apply button on wrong input', () => { @@ -118,74 +113,63 @@ describe('DateTimePicker', () => { }); describe('user input', () => { - const fillInputAndBlur = (input, val) => { + const fillInputAndBlur = async (input, val) => { wrapper.find(input).setValue(val); - return wrapper.vm.$nextTick().then(() => { - wrapper.find(input).trigger('blur'); - return wrapper.vm.$nextTick(); - }); + await nextTick(); + wrapper.find(input).trigger('blur'); + await nextTick(); }; - beforeEach(() => { + beforeEach(async () => { createComponent(); - return wrapper.vm.$nextTick(); + await nextTick(); }); - it('displays inline error message if custom time range inputs are invalid', () => { - return fillInputAndBlur('#custom-time-from', '2019-10-01abc') - .then(() => fillInputAndBlur('#custom-time-to', '2019-10-10abc')) - .then(() => { - expect(wrapper.findAll('.invalid-feedback').length).toBe(2); - }); + it('displays inline error message if custom time range inputs are invalid', async () => { + await fillInputAndBlur('#custom-time-from', '2019-10-01abc'); + await fillInputAndBlur('#custom-time-to', '2019-10-10abc'); + expect(wrapper.findAll('.invalid-feedback').length).toBe(2); }); - it('keeps apply button disabled with invalid custom time range inputs', () => { - return fillInputAndBlur('#custom-time-from', '2019-10-01abc') - .then(() => fillInputAndBlur('#custom-time-to', '2019-09-19')) - .then(() => { - expect(applyButtonElement().getAttribute('disabled')).toBe('disabled'); - }); + it('keeps apply button disabled with invalid custom time range inputs', async () => { + await fillInputAndBlur('#custom-time-from', '2019-10-01abc'); + await fillInputAndBlur('#custom-time-to', '2019-09-19'); + expect(applyButtonElement().getAttribute('disabled')).toBe('disabled'); }); - it('enables apply button with valid custom time range inputs', () => { - return fillInputAndBlur('#custom-time-from', '2019-10-01') - .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19')) - .then(() => { - expect(applyButtonElement().getAttribute('disabled')).toBeNull(); - }); + it('enables apply button with valid custom time range inputs', async () => { + await fillInputAndBlur('#custom-time-from', '2019-10-01'); + await fillInputAndBlur('#custom-time-to', '2019-10-19'); + expect(applyButtonElement().getAttribute('disabled')).toBeNull(); }); describe('when "apply" is clicked', () => { - it('emits iso dates', () => { - return fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00') - .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19 00:00:00')) - .then(() => { - applyButtonElement().click(); - - expect(wrapper.emitted().input).toHaveLength(1); - expect(wrapper.emitted().input[0]).toEqual([ - { - end: '2019-10-19T00:00:00Z', - start: '2019-10-01T00:00:00Z', - }, - ]); - }); + it('emits iso dates', async () => { + await fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00'); + await fillInputAndBlur('#custom-time-to', '2019-10-19 00:00:00'); + applyButtonElement().click(); + + expect(wrapper.emitted().input).toHaveLength(1); + expect(wrapper.emitted().input[0]).toEqual([ + { + end: '2019-10-19T00:00:00Z', + start: '2019-10-01T00:00:00Z', + }, + ]); }); - it('emits iso dates, for dates without time of day', () => { - return fillInputAndBlur('#custom-time-from', '2019-10-01') - .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19')) - .then(() => { - applyButtonElement().click(); - - expect(wrapper.emitted().input).toHaveLength(1); - expect(wrapper.emitted().input[0]).toEqual([ - { - end: '2019-10-19T00:00:00Z', - start: '2019-10-01T00:00:00Z', - }, - ]); - }); + it('emits iso dates, for dates without time of day', async () => { + await fillInputAndBlur('#custom-time-from', '2019-10-01'); + await fillInputAndBlur('#custom-time-to', '2019-10-19'); + applyButtonElement().click(); + + expect(wrapper.emitted().input).toHaveLength(1); + expect(wrapper.emitted().input[0]).toEqual([ + { + end: '2019-10-19T00:00:00Z', + start: '2019-10-01T00:00:00Z', + }, + ]); }); describe('when timezone is different', () => { @@ -196,52 +180,46 @@ describe('DateTimePicker', () => { timezoneMock.unregister(); }); - it('emits iso dates', () => { - return fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00') - .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19 12:00:00')) - .then(() => { - applyButtonElement().click(); - - expect(wrapper.emitted().input).toHaveLength(1); - expect(wrapper.emitted().input[0]).toEqual([ - { - start: '2019-10-01T07:00:00Z', - end: '2019-10-19T19:00:00Z', - }, - ]); - }); + it('emits iso dates', async () => { + await fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00'); + await fillInputAndBlur('#custom-time-to', '2019-10-19 12:00:00'); + applyButtonElement().click(); + + expect(wrapper.emitted().input).toHaveLength(1); + expect(wrapper.emitted().input[0]).toEqual([ + { + start: '2019-10-01T07:00:00Z', + end: '2019-10-19T19:00:00Z', + }, + ]); }); - it('emits iso dates with utc format', () => { + it('emits iso dates with utc format', async () => { wrapper.setProps({ utc: true }); - return wrapper.vm - .$nextTick() - .then(() => fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00')) - .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19 12:00:00')) - .then(() => { - applyButtonElement().click(); - - expect(wrapper.emitted().input).toHaveLength(1); - expect(wrapper.emitted().input[0]).toEqual([ - { - start: '2019-10-01T00:00:00Z', - end: '2019-10-19T12:00:00Z', - }, - ]); - }); + await nextTick(); + await fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00'); + await fillInputAndBlur('#custom-time-to', '2019-10-19 12:00:00'); + applyButtonElement().click(); + + expect(wrapper.emitted().input).toHaveLength(1); + expect(wrapper.emitted().input[0]).toEqual([ + { + start: '2019-10-01T00:00:00Z', + end: '2019-10-19T12:00:00Z', + }, + ]); }); }); }); - it('unchecks quick range when text is input is clicked', () => { + it('unchecks quick range when text is input is clicked', async () => { const findActiveItems = () => findQuickRangeItems().filter((w) => w.classes().includes('active')); expect(findActiveItems().length).toBe(1); - return fillInputAndBlur('#custom-time-from', '2019-10-01').then(() => { - expect(findActiveItems().length).toBe(0); - }); + await fillInputAndBlur('#custom-time-from', '2019-10-01'); + expect(findActiveItems().length).toBe(0); }); it('emits dates in an object when a is clicked', () => { @@ -257,16 +235,14 @@ describe('DateTimePicker', () => { }); }); - it('hides the popover with cancel button', () => { + it('hides the popover with cancel button', async () => { dropdownToggle().trigger('click'); - return wrapper.vm.$nextTick(() => { - cancelButton().trigger('click'); + await nextTick(); + cancelButton().trigger('click'); - return wrapper.vm.$nextTick(() => { - expect(dropdownMenu().classes('show')).toBe(false); - }); - }); + await nextTick(); + expect(dropdownMenu().classes('show')).toBe(false); }); }); @@ -293,7 +269,7 @@ describe('DateTimePicker', () => { jest.spyOn(Date, 'now').mockImplementation(() => MOCK_NOW); }); - it('renders dropdown with a label in the quick range', () => { + it('renders dropdown with a label in the quick range', async () => { createComponent({ value: { duration: { seconds: 60 * 5 }, @@ -301,12 +277,11 @@ describe('DateTimePicker', () => { options: otherTimeRanges, }); dropdownToggle().trigger('click'); - return wrapper.vm.$nextTick(() => { - expect(dropdownToggle().text()).toBe('5 minutes'); - }); + await nextTick(); + expect(dropdownToggle().text()).toBe('5 minutes'); }); - it('renders dropdown with a label in the quick range and utc label', () => { + it('renders dropdown with a label in the quick range and utc label', async () => { createComponent({ value: { duration: { seconds: 60 * 5 }, @@ -315,12 +290,11 @@ describe('DateTimePicker', () => { options: otherTimeRanges, }); dropdownToggle().trigger('click'); - return wrapper.vm.$nextTick(() => { - expect(dropdownToggle().text()).toBe('5 minutes UTC'); - }); + await nextTick(); + expect(dropdownToggle().text()).toBe('5 minutes UTC'); }); - it('renders dropdown with quick range items', () => { + it('renders dropdown with quick range items', async () => { createComponent({ value: { duration: { seconds: 60 * 2 }, @@ -328,31 +302,29 @@ describe('DateTimePicker', () => { options: otherTimeRanges, }); dropdownToggle().trigger('click'); - return wrapper.vm.$nextTick(() => { - const items = findQuickRangeItems(); + await nextTick(); + const items = findQuickRangeItems(); - expect(items.length).toBe(Object.keys(otherTimeRanges).length); - expect(items.at(0).text()).toBe('1 minute'); - expect(items.at(0).classes()).not.toContain('active'); + expect(items.length).toBe(Object.keys(otherTimeRanges).length); + expect(items.at(0).text()).toBe('1 minute'); + expect(items.at(0).classes()).not.toContain('active'); - expect(items.at(1).text()).toBe('2 minutes'); - expect(items.at(1).classes()).toContain('active'); + expect(items.at(1).text()).toBe('2 minutes'); + expect(items.at(1).classes()).toContain('active'); - expect(items.at(2).text()).toBe('5 minutes'); - expect(items.at(2).classes()).not.toContain('active'); - }); + expect(items.at(2).text()).toBe('5 minutes'); + expect(items.at(2).classes()).not.toContain('active'); }); - it('renders dropdown with a label not in the quick range', () => { + it('renders dropdown with a label not in the quick range', async () => { createComponent({ value: { duration: { seconds: 60 * 4 }, }, }); dropdownToggle().trigger('click'); - return wrapper.vm.$nextTick(() => { - expect(dropdownToggle().text()).toBe('2020-01-23 19:56:00 to 2020-01-23 20:00:00'); - }); + await nextTick(); + expect(dropdownToggle().text()).toBe('2020-01-23 19:56:00 to 2020-01-23 20:00:00'); }); }); }); diff --git a/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js b/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js index b812ced72c9..59653a0ec13 100644 --- a/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js +++ b/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js @@ -1,4 +1,5 @@ import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import DeployBoardInstance from '~/vue_shared/components/deployment_instance.vue'; import { folder } from './mock_data'; @@ -28,17 +29,15 @@ describe('Deploy Board Instance', () => { expect(wrapper.attributes('title')).toEqual('This is a pod'); }); - it('should render a div without tooltip data', (done) => { + it('should render a div without tooltip data', async () => { wrapper = createComponent({ status: 'deploying', tooltipText: '', }); - wrapper.vm.$nextTick(() => { - expect(wrapper.classes('deployment-instance-deploying')).toBe(true); - expect(wrapper.attributes('title')).toEqual(''); - done(); - }); + await nextTick(); + expect(wrapper.classes('deployment-instance-deploying')).toBe(true); + expect(wrapper.attributes('title')).toEqual(''); }); it('should have a log path computed with a pod name as a parameter', () => { @@ -58,15 +57,13 @@ describe('Deploy Board Instance', () => { wrapper.destroy(); }); - it('should render a div with canary class when stable prop is provided as false', (done) => { + it('should render a div with canary class when stable prop is provided as false', async () => { wrapper = createComponent({ stable: false, }); - wrapper.vm.$nextTick(() => { - expect(wrapper.classes('deployment-instance-canary')).toBe(true); - done(); - }); + await nextTick(); + expect(wrapper.classes('deployment-instance-canary')).toBe(true); }); }); @@ -75,17 +72,15 @@ describe('Deploy Board Instance', () => { wrapper.destroy(); }); - it('should not be a link without a logsPath prop', (done) => { + it('should not be a link without a logsPath prop', async () => { wrapper = createComponent({ stable: false, logsPath: '', }); - wrapper.vm.$nextTick(() => { - expect(wrapper.vm.computedLogPath).toBeNull(); - expect(wrapper.vm.isLink).toBeFalsy(); - done(); - }); + await nextTick(); + expect(wrapper.vm.computedLogPath).toBeNull(); + expect(wrapper.vm.isLink).toBeFalsy(); }); it('should render a link without href if path is not passed', () => { diff --git a/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js b/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js index 8deb466b33c..977e7c9c38c 100644 --- a/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js +++ b/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js @@ -1,5 +1,5 @@ import { mount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import { compileToFunctions } from 'vue-template-compiler'; import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants'; @@ -51,31 +51,28 @@ describe('ImageDiffViewer', () => { wrapper.destroy(); }); - it('renders image diff for replaced', (done) => { + it('renders image diff for replaced', async () => { createComponent({ ...allProps }); - vm.$nextTick(() => { - const metaInfoElements = vm.$el.querySelectorAll('.image-info'); + await nextTick(); + const metaInfoElements = vm.$el.querySelectorAll('.image-info'); - expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL); - - expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(RED_BOX_IMAGE_URL); + expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL); - expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('2-up'); - expect(vm.$el.querySelector('.view-modes-menu li:nth-child(2)').textContent.trim()).toBe( - 'Swipe', - ); + expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(RED_BOX_IMAGE_URL); - expect(vm.$el.querySelector('.view-modes-menu li:nth-child(3)').textContent.trim()).toBe( - 'Onion skin', - ); + expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('2-up'); + expect(vm.$el.querySelector('.view-modes-menu li:nth-child(2)').textContent.trim()).toBe( + 'Swipe', + ); - expect(metaInfoElements.length).toBe(2); - expect(metaInfoElements[0]).toHaveText('2.00 KiB'); - expect(metaInfoElements[1]).toHaveText('1.00 KiB'); + expect(vm.$el.querySelector('.view-modes-menu li:nth-child(3)').textContent.trim()).toBe( + 'Onion skin', + ); - done(); - }); + expect(metaInfoElements.length).toBe(2); + expect(metaInfoElements[0]).toHaveText('2.00 KiB'); + expect(metaInfoElements[1]).toHaveText('1.00 KiB'); }); it('renders image diff for new', (done) => { @@ -151,13 +148,11 @@ describe('ImageDiffViewer', () => { }); }); - it('switches to Swipe Mode', (done) => { + it('switches to Swipe Mode', async () => { vm.$el.querySelector('.view-modes-menu li:nth-child(2)').click(); - vm.$nextTick(() => { - expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('Swipe'); - done(); - }); + await nextTick(); + expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('Swipe'); }); }); @@ -170,29 +165,24 @@ describe('ImageDiffViewer', () => { }); }); - it('switches to Onion Skin Mode', (done) => { + it('switches to Onion Skin Mode', async () => { vm.$el.querySelector('.view-modes-menu li:nth-child(3)').click(); - vm.$nextTick(() => { - expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe( - 'Onion skin', - ); - done(); - }); + await nextTick(); + expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe( + 'Onion skin', + ); }); - it('has working drag handler', (done) => { + it('has working drag handler', async () => { vm.$el.querySelector('.view-modes-menu li:nth-child(3)').click(); - vm.$nextTick(() => { - dragSlider(vm.$el.querySelector('.dragger'), document, 20); + await nextTick(); + dragSlider(vm.$el.querySelector('.dragger'), document, 20); - vm.$nextTick(() => { - expect(vm.$el.querySelector('.dragger').style.left).toBe('20px'); - expect(vm.$el.querySelector('.added.frame').style.opacity).toBe('0.2'); - done(); - }); - }); + await nextTick(); + expect(vm.$el.querySelector('.dragger').style.left).toBe('20px'); + expect(vm.$el.querySelector('.added.frame').style.opacity).toBe('0.2'); }); }); }); diff --git a/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js b/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js index b8d3cbebe16..549388c1a5c 100644 --- a/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js +++ b/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js @@ -1,5 +1,5 @@ import { shallowMount, mount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import { TRANSITION_LOAD_START, @@ -126,15 +126,14 @@ describe('Renamed Diff Viewer', () => { store = null; }); - it('calls the switchToFullDiffFromRenamedFile action when the method is triggered', () => { + it('calls the switchToFullDiffFromRenamedFile action when the method is triggered', async () => { store.dispatch.mockResolvedValue(); wrapper.vm.switchToFull(); - return wrapper.vm.$nextTick().then(() => { - expect(store.dispatch).toHaveBeenCalledWith('diffs/switchToFullDiffFromRenamedFile', { - diffFile, - }); + await nextTick(); + expect(store.dispatch).toHaveBeenCalledWith('diffs/switchToFullDiffFromRenamedFile', { + diffFile, }); }); @@ -144,7 +143,7 @@ describe('Renamed Diff Viewer', () => { ${STATE_ERRORED} | ${'mockRejectedValue'} | ${'rejected'} `( 'moves through the correct states during a $resolution request', - ({ after, resolvePromise }) => { + async ({ after, resolvePromise }) => { store.dispatch[resolvePromise](); expect(wrapper.vm.state).toEqual(STATE_IDLING); @@ -153,16 +152,9 @@ describe('Renamed Diff Viewer', () => { expect(wrapper.vm.state).toEqual(STATE_LOADING); - return ( - wrapper.vm - // This tick is needed for when the action (promise) finishes - .$nextTick() - // This tick waits for the state change in the promise .then/.catch to bubble into the component - .then(() => wrapper.vm.$nextTick()) - .then(() => { - expect(wrapper.vm.state).toEqual(after); - }) - ); + await nextTick(); // This tick is needed for when the action (promise) finishes + await nextTick(); // This tick waits for the state change in the promise .then/.catch to bubble into the component + expect(wrapper.vm.state).toEqual(after); }, ); }); diff --git a/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js b/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js index 194681a6138..4b32fbffebe 100644 --- a/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js +++ b/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js @@ -1,5 +1,6 @@ import { GlAlert, GlSprintf } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import Component from '~/vue_shared/components/dismissible_feedback_alert.vue'; @@ -64,7 +65,7 @@ describe('Dismissible Feedback Alert', () => { it('should not show the alert once dismissed', async () => { localStorage.setItem(STORAGE_DISMISSAL_KEY, 'true'); createFullComponent(); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findAlert().exists()).toBe(false); }); diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js index ec553c52236..b32dbeb8852 100644 --- a/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js +++ b/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js @@ -1,4 +1,5 @@ import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import DropdownSearchInputComponent from '~/vue_shared/components/dropdown/dropdown_search_input.vue'; describe('DropdownSearchInputComponent', () => { @@ -36,16 +37,15 @@ describe('DropdownSearchInputComponent', () => { expect(findInputEl().attributes('placeholder')).toBe(defaultProps.placeholderText); }); - it('focuses input element when focused property equals true', () => { + it('focuses input element when focused property equals true', async () => { const inputEl = findInputEl().element; jest.spyOn(inputEl, 'focus'); wrapper.setProps({ focused: true }); - return wrapper.vm.$nextTick().then(() => { - expect(inputEl.focus).toHaveBeenCalled(); - }); + await nextTick(); + expect(inputEl.focus).toHaveBeenCalled(); }); }); }); diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js index b3af5fd3feb..084d0559665 100644 --- a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js +++ b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js @@ -1,6 +1,7 @@ import { GlDropdown, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue'; describe('DropdownWidget component', () => { @@ -53,7 +54,7 @@ describe('DropdownWidget component', () => { describe('when dropdown is open', () => { beforeEach(async () => { findDropdown().vm.$emit('show'); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('emits search event when typing in search box', () => { @@ -69,7 +70,7 @@ describe('DropdownWidget component', () => { it('emits set-option event when clicking on an option', async () => { wrapper.findAll('[data-testid="unselected-option"]').at(1).trigger('click'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted('set-option')).toEqual([[wrapper.props().options[1]]]); }); diff --git a/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js b/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js index 996df34f2ff..c34041f9305 100644 --- a/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js +++ b/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js @@ -1,4 +1,5 @@ import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue'; import { UP_KEY_CODE, DOWN_KEY_CODE, TAB_KEY_CODE } from '~/lib/utils/keycodes'; @@ -53,7 +54,7 @@ describe('DropdownKeyboardNavigation', () => { it('should $emit @change with the default index when max changes', async () => { wrapper.setProps({ max: 20 }); - await wrapper.vm.$nextTick(); + await nextTick(); // The first @change`call happens on created() so we test for the second [1] expect(wrapper.emitted('change')[1]).toStrictEqual([MOCK_DEFAULT_INDEX]); }); diff --git a/spec/frontend/vue_shared/components/file_finder/index_spec.js b/spec/frontend/vue_shared/components/file_finder/index_spec.js index 181fc4017a3..e5c10a23947 100644 --- a/spec/frontend/vue_shared/components/file_finder/index_spec.js +++ b/spec/frontend/vue_shared/components/file_finder/index_spec.js @@ -1,5 +1,5 @@ import Mousetrap from 'mousetrap'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; import { file } from 'jest/ide/helpers'; import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; @@ -310,34 +310,26 @@ describe('File finder item spec', () => { }); describe('keyboard shortcuts', () => { - beforeEach((done) => { + beforeEach(async () => { createComponent(); jest.spyOn(vm, 'toggle').mockImplementation(() => {}); - vm.$nextTick(done); + await nextTick(); }); - it('calls toggle on `t` key press', (done) => { + it('calls toggle on `t` key press', async () => { Mousetrap.trigger('t'); - vm.$nextTick() - .then(() => { - expect(vm.toggle).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(vm.toggle).toHaveBeenCalled(); }); - it('calls toggle on `mod+p` key press', (done) => { + it('calls toggle on `mod+p` key press', async () => { Mousetrap.trigger('mod+p'); - vm.$nextTick() - .then(() => { - expect(vm.toggle).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(vm.toggle).toHaveBeenCalled(); }); it('always allows `mod+p` to trigger toggle', () => { diff --git a/spec/frontend/vue_shared/components/file_finder/item_spec.js b/spec/frontend/vue_shared/components/file_finder/item_spec.js index 1a4a97efb95..b69c33055c1 100644 --- a/spec/frontend/vue_shared/components/file_finder/item_spec.js +++ b/spec/frontend/vue_shared/components/file_finder/item_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import createComponent from 'helpers/vue_mount_component_helper'; import { file } from 'jest/ide/helpers'; import ItemComponent from '~/vue_shared/components/file_finder/item.vue'; @@ -37,14 +37,11 @@ describe('File finder item spec', () => { expect(vm.$el.classList).toContain('is-focused'); }); - it('does not have is-focused class when not focused', (done) => { + it('does not have is-focused class when not focused', async () => { vm.focused = false; - vm.$nextTick(() => { - expect(vm.$el.classList).not.toContain('is-focused'); - - done(); - }); + await nextTick(); + expect(vm.$el.classList).not.toContain('is-focused'); }); }); @@ -53,24 +50,18 @@ describe('File finder item spec', () => { expect(vm.$el.querySelector('.diff-changed-stats')).toBe(null); }); - it('renders when a changed file', (done) => { + it('renders when a changed file', async () => { vm.file.changed = true; - vm.$nextTick(() => { - expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null); - - done(); - }); + await nextTick(); + expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null); }); - it('renders when a temp file', (done) => { + it('renders when a temp file', async () => { vm.file.tempFile = true; - vm.$nextTick(() => { - expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null); - - done(); - }); + await nextTick(); + expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null); }); }); @@ -85,56 +76,52 @@ describe('File finder item spec', () => { describe('path', () => { let el; - beforeEach((done) => { + beforeEach(async () => { vm.searchText = 'file'; el = vm.$el.querySelector('.diff-changed-file-path'); - vm.$nextTick(done); + nextTick(); }); it('highlights text', () => { expect(el.querySelectorAll('.highlighted').length).toBe(4); }); - it('adds ellipsis to long text', (done) => { + it('adds ellipsis to long text', async () => { vm.file.path = new Array(70) .fill() .map((_, i) => `${i}-`) .join(''); - vm.$nextTick(() => { - expect(el.textContent).toBe(`...${vm.file.path.substr(vm.file.path.length - 60)}`); - done(); - }); + await nextTick(); + expect(el.textContent).toBe(`...${vm.file.path.substr(vm.file.path.length - 60)}`); }); }); describe('name', () => { let el; - beforeEach((done) => { + beforeEach(async () => { vm.searchText = 'file'; el = vm.$el.querySelector('.diff-changed-file-name'); - vm.$nextTick(done); + await nextTick(); }); it('highlights text', () => { expect(el.querySelectorAll('.highlighted').length).toBe(4); }); - it('does not add ellipsis to long text', (done) => { + it('does not add ellipsis to long text', async () => { vm.file.name = new Array(70) .fill() .map((_, i) => `${i}-`) .join(''); - vm.$nextTick(() => { - expect(el.textContent).not.toBe(`...${vm.file.name.substr(vm.file.name.length - 60)}`); - done(); - }); + await nextTick(); + expect(el.textContent).not.toBe(`...${vm.file.name.substr(vm.file.name.length - 60)}`); }); }); }); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js index 4e9eac2dde2..575e8a73050 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js @@ -8,6 +8,7 @@ import { } from '@gitlab/ui'; import { shallowMount, mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store'; import { SortDirection } from '~/vue_shared/components/filtered_search_bar/constants'; @@ -172,7 +173,7 @@ describe('FilteredSearchBarRoot', () => { recentSearches: [{ foo: 'bar' }, 'foo'], }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.filteredRecentSearches).toHaveLength(1); expect(wrapper.vm.filteredRecentSearches[0]).toEqual({ foo: 'bar' }); @@ -188,7 +189,7 @@ describe('FilteredSearchBarRoot', () => { ], }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.filteredRecentSearches).toHaveLength(2); expect(uniqueTokens).toHaveBeenCalled(); @@ -199,7 +200,7 @@ describe('FilteredSearchBarRoot', () => { recentSearchesStorageKey: '', }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.filteredRecentSearches).not.toBeDefined(); }); @@ -208,7 +209,7 @@ describe('FilteredSearchBarRoot', () => { describe('watchers', () => { describe('filterValue', () => { - it('emits component event `onFilter` with empty array and false when filter was never selected', () => { + it('emits component event `onFilter` with empty array and false when filter was never selected', async () => { wrapper = createComponent({ initialFilterValue: [tokenValueEmpty] }); // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax @@ -217,12 +218,11 @@ describe('FilteredSearchBarRoot', () => { filterValue: [tokenValueEmpty], }); - return wrapper.vm.$nextTick(() => { - expect(wrapper.emitted('onFilter')[0]).toEqual([[], false]); - }); + await nextTick(); + expect(wrapper.emitted('onFilter')[0]).toEqual([[], false]); }); - it('emits component event `onFilter` with empty array and true when initially selected filter value was cleared', () => { + it('emits component event `onFilter` with empty array and true when initially selected filter value was cleared', async () => { wrapper = createComponent({ initialFilterValue: [tokenValueLabel] }); // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax @@ -231,9 +231,8 @@ describe('FilteredSearchBarRoot', () => { filterValue: [tokenValueEmpty], }); - return wrapper.vm.$nextTick(() => { - expect(wrapper.emitted('onFilter')[0]).toEqual([[], true]); - }); + await nextTick(); + expect(wrapper.emitted('onFilter')[0]).toEqual([[], true]); }); }); }); @@ -336,7 +335,7 @@ describe('FilteredSearchBarRoot', () => { filterValue: mockFilters, }); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('calls `uniqueTokens` on `filterValue` prop to remove duplicates', () => { @@ -395,7 +394,7 @@ describe('FilteredSearchBarRoot', () => { }); describe('template', () => { - 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({ @@ -404,7 +403,7 @@ describe('FilteredSearchBarRoot', () => { recentSearches: mockHistoryItems, }); - return wrapper.vm.$nextTick(); + await nextTick(); }); it('renders gl-filtered-search component', () => { @@ -439,7 +438,7 @@ describe('FilteredSearchBarRoot', () => { const wrapperFullMount = createComponent({ sortOptions: mockSortOptions, shallow: false }); wrapperFullMount.vm.recentSearchesStore.addRecentSearch(mockHistoryItems[0]); - await wrapperFullMount.vm.$nextTick(); + await nextTick(); const searchHistoryItemsEl = wrapperFullMount.findAll( '.gl-search-box-by-click-menu .gl-search-box-by-click-history-item', @@ -462,7 +461,7 @@ describe('FilteredSearchBarRoot', () => { wrapperFullMount.vm.recentSearchesStore.addRecentSearch([tokenValueMembership]); - await wrapperFullMount.vm.$nextTick(); + await nextTick(); expect(wrapperFullMount.find(GlDropdownItem).text()).toBe('Membership := Direct'); @@ -480,7 +479,7 @@ describe('FilteredSearchBarRoot', () => { wrapperFullMount.vm.recentSearchesStore.addRecentSearch([tokenValueMembership]); - await wrapperFullMount.vm.$nextTick(); + await nextTick(); expect(wrapperFullMount.find(GlDropdownItem).text()).toBe('Membership := exclude'); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js index 5865c6a41b8..87066b70023 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js @@ -6,6 +6,7 @@ import { } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; @@ -167,7 +168,7 @@ describe('AuthorToken', () => { const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - await wrapper.vm.$nextTick(); + await nextTick(); }; it('renders base-token component', () => { @@ -185,23 +186,22 @@ describe('AuthorToken', () => { }); }); - it('renders token item when value is selected', () => { + it('renders token item when value is selected', async () => { wrapper = createComponent({ value: { data: mockAuthors[0].username }, data: { authors: mockAuthors }, stubs: { Portal: true }, }); - return wrapper.vm.$nextTick(() => { - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); + await nextTick(); + const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); - expect(tokenSegments).toHaveLength(3); // Author, =, "Administrator" + expect(tokenSegments).toHaveLength(3); // Author, =, "Administrator" - const tokenValue = tokenSegments.at(2); + const tokenValue = tokenSegments.at(2); - expect(tokenValue.findComponent(GlAvatar).props('src')).toBe(mockAuthors[0].avatar_url); - expect(tokenValue.text()).toBe(mockAuthors[0].name); // "Administrator" - }); + expect(tokenValue.findComponent(GlAvatar).props('src')).toBe(mockAuthors[0].avatar_url); + expect(tokenValue.text()).toBe(mockAuthors[0].name); // "Administrator" }); it('renders token value with correct avatarUrl from author object', async () => { @@ -220,7 +220,7 @@ describe('AuthorToken', () => { stubs: { Portal: true }, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(getAvatarEl().props('src')).toBe(mockAuthors[0].avatar_url); @@ -236,7 +236,7 @@ describe('AuthorToken', () => { ], }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(getAvatarEl().props('src')).toBe(mockAuthors[0].avatar_url); }); @@ -268,7 +268,7 @@ describe('AuthorToken', () => { const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.find(GlDropdownDivider).exists()).toBe(false); }); @@ -323,7 +323,7 @@ describe('AuthorToken', () => { it('does not show current user while searching', async () => { wrapper.findComponent(BaseToken).vm.handleInput({ data: 'foo' }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.findComponent(GlFilteredSearchSuggestion).exists()).toBe(false); }); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js index 4836d54ad07..dd9bf2ff598 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js @@ -1,5 +1,6 @@ import { GlFilteredSearchToken, GlLoadingIcon } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { mockRegularLabel, mockLabels, @@ -202,7 +203,7 @@ describe('BaseToken', () => { jest.useFakeTimers(); findGlFilteredSearchToken().vm.$emit('input', { data: 'foo' }); - await wrapper.vm.$nextTick(); + await nextTick(); jest.runAllTimers(); @@ -221,7 +222,7 @@ describe('BaseToken', () => { jest.useFakeTimers(); findGlFilteredSearchToken().vm.$emit('input', { data: 'foo' }); - await wrapper.vm.$nextTick(); + await nextTick(); jest.runAllTimers(); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js index cd8be765fb5..7a7db434052 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js @@ -6,6 +6,7 @@ import { } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; @@ -115,7 +116,7 @@ describe('BranchToken', () => { const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - await wrapper.vm.$nextTick(); + await nextTick(); } beforeEach(async () => { @@ -127,7 +128,7 @@ describe('BranchToken', () => { branches: mockBranches, }); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('renders gl-filtered-search-token component', () => { diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js index ed9ac7c271e..b163563cea4 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js @@ -6,6 +6,7 @@ import { } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; @@ -129,7 +130,7 @@ describe('EmojiToken', () => { emojis: mockEmojis, }); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('renders gl-filtered-search-token component', () => { @@ -152,7 +153,7 @@ describe('EmojiToken', () => { const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - await wrapper.vm.$nextTick(); + await nextTick(); const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); @@ -171,7 +172,7 @@ describe('EmojiToken', () => { const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false); expect(wrapper.find(GlDropdownDivider).exists()).toBe(false); @@ -186,7 +187,7 @@ describe('EmojiToken', () => { const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - await wrapper.vm.$nextTick(); + await nextTick(); const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js index b9af71ad8a7..52df27c2d00 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js @@ -5,6 +5,7 @@ import { } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; import { mockRegularLabel, @@ -150,7 +151,7 @@ describe('LabelToken', () => { labels: mockLabels, }); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('renders base-token component', () => { @@ -182,7 +183,7 @@ describe('LabelToken', () => { const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - await wrapper.vm.$nextTick(); + await nextTick(); const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); @@ -201,7 +202,7 @@ describe('LabelToken', () => { const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false); expect(wrapper.find(GlDropdownDivider).exists()).toBe(false); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js index c0d8b5fd139..332f865b874 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js @@ -6,6 +6,7 @@ import { } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; @@ -127,7 +128,7 @@ describe('MilestoneToken', () => { milestones: mockMilestones, }); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('renders gl-filtered-search-token component', () => { @@ -150,7 +151,7 @@ describe('MilestoneToken', () => { const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - await wrapper.vm.$nextTick(); + await nextTick(); const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); @@ -169,7 +170,7 @@ describe('MilestoneToken', () => { const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false); expect(wrapper.find(GlDropdownDivider).exists()).toBe(false); @@ -184,7 +185,7 @@ describe('MilestoneToken', () => { const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); - await wrapper.vm.$nextTick(); + await nextTick(); const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js index b2f246a5985..8be21b35414 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js @@ -1,5 +1,6 @@ import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue'; @@ -31,7 +32,7 @@ describe('ReleaseToken', () => { it('renders release value', async () => { wrapper = createComponent({ value: { data: id } }); - await wrapper.vm.$nextTick(); + await nextTick(); const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); diff --git a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js index b837a998cd6..c0a6588833e 100644 --- a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js +++ b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js @@ -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 { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants'; import GlModalVuex from '~/vue_shared/components/gl_modal_vuex.vue'; @@ -118,7 +118,7 @@ describe('GlModalVuex', () => { expect(actions.hide).toHaveBeenCalledTimes(1); }); - it('calls bootstrap show when isVisible changes', (done) => { + it('calls bootstrap show when isVisible changes', async () => { state.isVisible = false; factory(); @@ -126,16 +126,11 @@ describe('GlModalVuex', () => { state.isVisible = true; - wrapper.vm - .$nextTick() - .then(() => { - expect(rootEmit).toHaveBeenCalledWith(BV_SHOW_MODAL, TEST_MODAL_ID); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(rootEmit).toHaveBeenCalledWith(BV_SHOW_MODAL, TEST_MODAL_ID); }); - it('calls bootstrap hide when isVisible changes', (done) => { + it('calls bootstrap hide when isVisible changes', async () => { state.isVisible = true; factory(); @@ -143,13 +138,8 @@ describe('GlModalVuex', () => { state.isVisible = false; - wrapper.vm - .$nextTick() - .then(() => { - expect(rootEmit).toHaveBeenCalledWith(BV_HIDE_MODAL, TEST_MODAL_ID); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(rootEmit).toHaveBeenCalledWith(BV_HIDE_MODAL, TEST_MODAL_ID); }); it.each(['ok', 'cancel'])( diff --git a/spec/frontend/vue_shared/components/local_storage_sync_spec.js b/spec/frontend/vue_shared/components/local_storage_sync_spec.js index 4c5a0c1e601..dac633fe6c8 100644 --- a/spec/frontend/vue_shared/components/local_storage_sync_spec.js +++ b/spec/frontend/vue_shared/components/local_storage_sync_spec.js @@ -1,4 +1,5 @@ import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; describe('Local Storage Sync', () => { @@ -49,7 +50,7 @@ describe('Local Storage Sync', () => { it.each('foo', 3, true, ['foo', 'bar'], { foo: 'bar' })( 'saves updated value to localStorage', - (newValue) => { + async (newValue) => { createComponent({ props: { storageKey, @@ -59,9 +60,8 @@ describe('Local Storage Sync', () => { wrapper.setProps({ value: newValue }); - return wrapper.vm.$nextTick().then(() => { - expect(localStorage.getItem(storageKey)).toBe(String(newValue)); - }); + await nextTick(); + expect(localStorage.getItem(storageKey)).toBe(String(newValue)); }, ); @@ -109,7 +109,7 @@ describe('Local Storage Sync', () => { expect(localStorage.getItem(storageKey)).toBe(savedValue); }); - it('updating the value updates localStorage', () => { + it('updating the value updates localStorage', async () => { createComponent({ props: { storageKey, @@ -122,9 +122,8 @@ describe('Local Storage Sync', () => { value: newValue, }); - return wrapper.vm.$nextTick().then(() => { - expect(localStorage.getItem(storageKey)).toBe(newValue); - }); + await nextTick(); + expect(localStorage.getItem(storageKey)).toBe(newValue); }); it('persists the value by default', async () => { @@ -137,7 +136,7 @@ describe('Local Storage Sync', () => { }); wrapper.setProps({ value: persistedValue }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(localStorage.getItem(storageKey)).toBe(persistedValue); }); @@ -151,7 +150,7 @@ describe('Local Storage Sync', () => { }); wrapper.setProps({ persist: false, value: notPersistedValue }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(localStorage.getItem(storageKey)).not.toBe(notPersistedValue); }); }); @@ -172,7 +171,7 @@ describe('Local Storage Sync', () => { ${{ foo: 'bar' }} | ${'{"foo":"bar"}'} `('given $value', ({ value, serializedValue }) => { describe('is a new value', () => { - beforeEach(() => { + beforeEach(async () => { createComponent({ props: { storageKey, @@ -183,7 +182,7 @@ describe('Local Storage Sync', () => { wrapper.setProps({ value }); - return wrapper.vm.$nextTick(); + await nextTick(); }); it('serializes the value correctly to localStorage', () => { @@ -253,7 +252,7 @@ describe('Local Storage Sync', () => { value, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(localStorage.getItem(storageKey)).toBe(value); @@ -261,7 +260,7 @@ describe('Local Storage Sync', () => { clear: true, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(localStorage.getItem(storageKey)).toBe(null); }); diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js index 0d90ca7f1f6..c31374f6d56 100644 --- a/spec/frontend/vue_shared/components/markdown/field_spec.js +++ b/spec/frontend/vue_shared/components/markdown/field_spec.js @@ -100,58 +100,46 @@ describe('Markdown field component', () => { axiosMock.onPost(markdownPreviewPath).reply(200, { body: previewHTML }); }); - it('sets preview link as active', () => { + it('sets preview link as active', async () => { previewLink = getPreviewLink(); previewLink.trigger('click'); - return subject.vm.$nextTick().then(() => { - expect(previewLink.element.parentNode.classList.contains('active')).toBeTruthy(); - }); + await nextTick(); + expect(previewLink.element.parentNode.classList.contains('active')).toBeTruthy(); }); - it('shows preview loading text', () => { + it('shows preview loading text', async () => { previewLink = getPreviewLink(); previewLink.trigger('click'); - return subject.vm.$nextTick(() => { - expect(subject.find('.md-preview-holder').element.textContent.trim()).toContain( - 'Loading…', - ); - }); + await nextTick(); + expect(subject.find('.md-preview-holder').element.textContent.trim()).toContain('Loading…'); }); - it('renders markdown preview and GFM', () => { + it('renders markdown preview and GFM', async () => { const renderGFMSpy = jest.spyOn($.fn, 'renderGFM'); previewLink = getPreviewLink(); previewLink.trigger('click'); - return axios.waitFor(markdownPreviewPath).then(() => { - expect(subject.find('.md-preview-holder').element.innerHTML).toContain(previewHTML); - expect(renderGFMSpy).toHaveBeenCalled(); - }); + await axios.waitFor(markdownPreviewPath); + expect(subject.find('.md-preview-holder').element.innerHTML).toContain(previewHTML); + expect(renderGFMSpy).toHaveBeenCalled(); }); - it('calls video.pause() on comment input when isSubmitting is changed to true', () => { + it('calls video.pause() on comment input when isSubmitting is changed to true', async () => { previewLink = getPreviewLink(); previewLink.trigger('click'); - let callPause; - - return axios - .waitFor(markdownPreviewPath) - .then(() => { - const video = getVideo(); - callPause = jest.spyOn(video.element, 'pause').mockImplementation(() => true); + await axios.waitFor(markdownPreviewPath); + const video = getVideo(); + const callPause = jest.spyOn(video.element, 'pause').mockImplementation(() => true); - subject.setProps({ isSubmitting: true }); + subject.setProps({ isSubmitting: true }); - return subject.vm.$nextTick(); - }) - .then(() => { - expect(callPause).toHaveBeenCalled(); - }); + await nextTick(); + expect(callPause).toHaveBeenCalled(); }); it('clicking already active write or preview link does nothing', async () => { @@ -159,56 +147,53 @@ describe('Markdown field component', () => { previewLink = getPreviewLink(); writeLink.trigger('click'); - await subject.vm.$nextTick(); + await nextTick(); assertMarkdownTabs(true, writeLink, previewLink, subject); writeLink.trigger('click'); - await subject.vm.$nextTick(); + await nextTick(); assertMarkdownTabs(true, writeLink, previewLink, subject); previewLink.trigger('click'); - await subject.vm.$nextTick(); + await nextTick(); assertMarkdownTabs(false, writeLink, previewLink, subject); previewLink.trigger('click'); - await subject.vm.$nextTick(); + await nextTick(); assertMarkdownTabs(false, writeLink, previewLink, subject); }); }); describe('markdown buttons', () => { - it('converts single words', () => { + it('converts single words', async () => { const textarea = subject.find('textarea').element; textarea.setSelectionRange(0, 7); const markdownButton = getMarkdownButton(); markdownButton.trigger('click'); - return subject.vm.$nextTick(() => { - expect(textarea.value).toContain('**testing**'); - }); + await nextTick(); + expect(textarea.value).toContain('**testing**'); }); - it('converts a line', () => { + it('converts a line', async () => { const textarea = subject.find('textarea').element; textarea.setSelectionRange(0, 0); const markdownButton = getAllMarkdownButtons().wrappers[5]; markdownButton.trigger('click'); - return subject.vm.$nextTick(() => { - expect(textarea.value).toContain('- testing'); - }); + await nextTick(); + expect(textarea.value).toContain('- testing'); }); - it('converts multiple lines', () => { + it('converts multiple lines', async () => { const textarea = subject.find('textarea').element; textarea.setSelectionRange(0, 50); const markdownButton = getAllMarkdownButtons().wrappers[5]; markdownButton.trigger('click'); - return subject.vm.$nextTick(() => { - expect(textarea.value).toContain('- testing\n- 123'); - }); + await nextTick(); + expect(textarea.value).toContain('- testing\n- 123'); }); }); @@ -229,7 +214,7 @@ describe('Markdown field component', () => { // Do something to trigger rerendering the class subject.setProps({ wrapperClasses: 'foo' }); - await subject.vm.$nextTick(); + await nextTick(); }); it('should have rerendered classes and kept gfm-form', () => { diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js index fec6abc9639..70ee72b7e2c 100644 --- a/spec/frontend/vue_shared/components/markdown/header_spec.js +++ b/spec/frontend/vue_shared/components/markdown/header_spec.js @@ -1,5 +1,6 @@ import { shallowMount } from '@vue/test-utils'; import $ from 'jquery'; +import { nextTick } from 'vue'; import HeaderComponent from '~/vue_shared/components/markdown/header.vue'; import ToolbarButton from '~/vue_shared/components/markdown/toolbar_button.vue'; @@ -84,20 +85,16 @@ describe('Markdown field header component', () => { expect(wrapper.find('li:nth-child(2)').classes()).toContain('active'); }); - it('emits toggle markdown event when clicking preview', () => { + it('emits toggle markdown event when clicking preview', async () => { wrapper.find('.js-preview-link').trigger('click'); - return wrapper.vm - .$nextTick() - .then(() => { - expect(wrapper.emitted('preview-markdown').length).toEqual(1); + await nextTick(); + expect(wrapper.emitted('preview-markdown').length).toEqual(1); - wrapper.find('.js-write-link').trigger('click'); - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(wrapper.emitted('write-markdown').length).toEqual(1); - }); + wrapper.find('.js-write-link').trigger('click'); + + await nextTick(); + expect(wrapper.emitted('write-markdown').length).toEqual(1); }); it('does not emit toggle markdown event when triggered from another form', () => { diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js index 9bc2aad1895..9944267cf24 100644 --- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js +++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js @@ -1,5 +1,6 @@ import { GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import ApplySuggestion from '~/vue_shared/components/markdown/apply_suggestion.vue'; import SuggestionDiffHeader from '~/vue_shared/components/markdown/suggestion_diff_header.vue'; @@ -103,15 +104,14 @@ describe('Suggestion Diff component', () => { expect(wrapper.text()).toContain('Applying suggestion...'); }); - it('when callback of apply is called, hides loading', () => { + it('when callback of apply is called, hides loading', async () => { const [callback] = wrapper.emitted().apply[0]; callback(); - return wrapper.vm.$nextTick().then(() => { - expect(findApplyButton().exists()).toBe(true); - expect(findLoading().exists()).toBe(false); - }); + await nextTick(); + expect(findApplyButton().exists()).toBe(true); + expect(findLoading().exists()).toBe(false); }); }); diff --git a/spec/frontend/vue_shared/components/modal_copy_button_spec.js b/spec/frontend/vue_shared/components/modal_copy_button_spec.js index adb72c3ef85..b57efc88d57 100644 --- a/spec/frontend/vue_shared/components/modal_copy_button_spec.js +++ b/spec/frontend/vue_shared/components/modal_copy_button_spec.js @@ -1,4 +1,5 @@ import { shallowMount, createWrapper } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue'; @@ -20,7 +21,7 @@ describe('modal copy button', () => { }); describe('clipboard', () => { - it('should fire a `success` event on click', () => { + it('should fire a `success` event on click', async () => { const root = createWrapper(wrapper.vm.$root); document.execCommand = jest.fn(() => true); window.getSelection = jest.fn(() => ({ @@ -29,20 +30,18 @@ describe('modal copy button', () => { })); wrapper.trigger('click'); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted().success).not.toBeEmpty(); - expect(document.execCommand).toHaveBeenCalledWith('copy'); - expect(root.emitted(BV_HIDE_TOOLTIP)).toEqual([['test-id']]); - }); + await nextTick(); + expect(wrapper.emitted().success).not.toBeEmpty(); + expect(document.execCommand).toHaveBeenCalledWith('copy'); + expect(root.emitted(BV_HIDE_TOOLTIP)).toEqual([['test-id']]); }); - it("should propagate the clipboard error event if execCommand doesn't work", () => { + it("should propagate the clipboard error event if execCommand doesn't work", async () => { document.execCommand = jest.fn(() => false); wrapper.trigger('click'); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted().error).not.toBeEmpty(); - expect(document.execCommand).toHaveBeenCalledWith('copy'); - }); + await nextTick(); + expect(wrapper.emitted().error).not.toBeEmpty(); + expect(document.execCommand).toHaveBeenCalledWith('copy'); }); }); }); diff --git a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js index 835759b1f20..accbf14572d 100644 --- a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js +++ b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js @@ -1,5 +1,6 @@ import { GlIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue'; describe('Issue Warning Component', () => { @@ -64,7 +65,7 @@ describe('Issue Warning Component', () => { expect(findConfidentialBlock().exists()).toBe(true); expect(findConfidentialBlock().element).toMatchSnapshot(); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findConfidentialBlock(wrapper).text()).toContain('This is a confidential issue.'); }); @@ -154,15 +155,15 @@ describe('Issue Warning Component', () => { noteableType: 'Epic', }); - await wrapperLocked.vm.$nextTick(); + await nextTick(); expect(findLockedBlock(wrapperLocked).text()).toContain('This epic is locked.'); - await wrapperConfidential.vm.$nextTick(); + await nextTick(); expect(findConfidentialBlock(wrapperConfidential).text()).toContain( 'This is a confidential epic.', ); - await wrapperLockedAndConfidential.vm.$nextTick(); + await nextTick(); expect(findLockedAndConfidentialBlock(wrapperLockedAndConfidential).text()).toContain( 'This epic is confidential and locked.', ); @@ -179,15 +180,15 @@ describe('Issue Warning Component', () => { noteableType: 'MergeRequest', }); - await wrapperLocked.vm.$nextTick(); + await nextTick(); expect(findLockedBlock(wrapperLocked).text()).toContain('This merge request is locked.'); - await wrapperConfidential.vm.$nextTick(); + await nextTick(); expect(findConfidentialBlock(wrapperConfidential).text()).toContain( 'This is a confidential merge request.', ); - await wrapperLockedAndConfidential.vm.$nextTick(); + await nextTick(); expect(findLockedAndConfidentialBlock(wrapperLockedAndConfidential).text()).toContain( 'This merge request is confidential and locked.', ); diff --git a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js index b330b4f5657..36050a42da7 100644 --- a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js +++ b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js @@ -1,5 +1,6 @@ import { GlAlert, GlBadge, GlPagination, GlTabs, GlTab } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import Tracking from '~/tracking'; import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants'; import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; @@ -219,21 +220,21 @@ describe('AlertManagementEmptyState', () => { it('returns prevPage button', async () => { findPagination().vm.$emit('input', 3); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findPagination().findAll('.page-item').at(0).text()).toBe('Prev'); }); it('returns prevPage number', async () => { findPagination().vm.$emit('input', 3); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.previousPage).toBe(2); }); it('returns 0 when it is the first page', async () => { findPagination().vm.$emit('input', 1); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.previousPage).toBe(0); }); }); @@ -242,7 +243,7 @@ describe('AlertManagementEmptyState', () => { it('returns nextPage button', async () => { findPagination().vm.$emit('input', 3); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findPagination().findAll('.page-item').at(1).text()).toBe('Next'); }); @@ -257,14 +258,14 @@ describe('AlertManagementEmptyState', () => { }); findPagination().vm.$emit('input', 1); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.nextPage).toBe(2); }); it('returns `null` when currentPage is already last page', async () => { findStatusTabs().vm.$emit('input', 1); findPagination().vm.$emit('input', 1); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.nextPage).toBeNull(); }); }); @@ -319,7 +320,7 @@ describe('AlertManagementEmptyState', () => { searchTerm, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.filteredSearchValue).toEqual([searchTerm]); }); diff --git a/spec/frontend/vue_shared/components/project_avatar/default_spec.js b/spec/frontend/vue_shared/components/project_avatar/default_spec.js index 84dad2374cb..d042db6051c 100644 --- a/spec/frontend/vue_shared/components/project_avatar/default_spec.js +++ b/spec/frontend/vue_shared/components/project_avatar/default_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import mountComponent from 'helpers/vue_mount_component_helper'; import { projectData } from 'jest/ide/mock_data'; import { TEST_HOST } from 'spec/test_constants'; @@ -19,7 +19,7 @@ describe('ProjectAvatarDefault component', () => { vm.$destroy(); }); - it('renders identicon if project has no avatar_url', (done) => { + it('renders identicon if project has no avatar_url', async () => { const expectedText = getFirstCharacterCapitalized(projectData.name); vm.project = { @@ -27,18 +27,14 @@ describe('ProjectAvatarDefault component', () => { avatar_url: null, }; - vm.$nextTick() - .then(() => { - const identiconEl = vm.$el.querySelector('.identicon'); + await nextTick(); + const identiconEl = vm.$el.querySelector('.identicon'); - expect(identiconEl).not.toBe(null); - expect(identiconEl.textContent.trim()).toEqual(expectedText); - }) - .then(done) - .catch(done.fail); + expect(identiconEl).not.toBe(null); + expect(identiconEl.textContent.trim()).toEqual(expectedText); }); - it('renders avatar image if project has avatar_url', (done) => { + it('renders avatar image if project has avatar_url', async () => { const avatarUrl = `${TEST_HOST}/images/home/nasa.svg`; vm.project = { @@ -46,13 +42,9 @@ describe('ProjectAvatarDefault component', () => { avatar_url: avatarUrl, }; - vm.$nextTick() - .then(() => { - expect(vm.$el.querySelector('.avatar')).not.toBeNull(); - expect(vm.$el.querySelector('.identicon')).toBeNull(); - expect(vm.$el.querySelector('img')).toHaveAttr('src', avatarUrl); - }) - .then(done) - .catch(done.fail); + await nextTick(); + expect(vm.$el.querySelector('.avatar')).not.toBeNull(); + expect(vm.$el.querySelector('.identicon')).toBeNull(); + expect(vm.$el.querySelector('img')).toHaveAttr('src', avatarUrl); }); }); diff --git a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js index 34cee10392d..379e60c1b2d 100644 --- a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js +++ b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js @@ -1,7 +1,7 @@ import { GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import { head } from 'lodash'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import mockProjects from 'test_fixtures_static/projects.json'; import { trimText } from 'helpers/text_helper'; import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; @@ -77,39 +77,36 @@ describe('ProjectSelector component', () => { expect(vm.$emit).toHaveBeenCalledWith('projectClicked', head(searchResults)); }); - it(`shows a "no results" message if showNoResultsMessage === true`, () => { + it(`shows a "no results" message if showNoResultsMessage === true`, async () => { wrapper.setProps({ showNoResultsMessage: true }); - return vm.$nextTick().then(() => { - const noResultsEl = wrapper.find('.js-no-results-message'); + await nextTick(); + const noResultsEl = wrapper.find('.js-no-results-message'); - expect(noResultsEl.exists()).toBe(true); - expect(trimText(noResultsEl.text())).toEqual('Sorry, no projects matched your search'); - }); + expect(noResultsEl.exists()).toBe(true); + expect(trimText(noResultsEl.text())).toEqual('Sorry, no projects matched your search'); }); - it(`shows a "minimum search query" message if showMinimumSearchQueryMessage === true`, () => { + it(`shows a "minimum search query" message if showMinimumSearchQueryMessage === true`, async () => { wrapper.setProps({ showMinimumSearchQueryMessage: true }); - return vm.$nextTick().then(() => { - const minimumSearchEl = wrapper.find('.js-minimum-search-query-message'); + await nextTick(); + const minimumSearchEl = wrapper.find('.js-minimum-search-query-message'); - expect(minimumSearchEl.exists()).toBe(true); - expect(trimText(minimumSearchEl.text())).toEqual('Enter at least three characters to search'); - }); + expect(minimumSearchEl.exists()).toBe(true); + expect(trimText(minimumSearchEl.text())).toEqual('Enter at least three characters to search'); }); - it(`shows a error message if showSearchErrorMessage === true`, () => { + it(`shows a error message if showSearchErrorMessage === true`, async () => { wrapper.setProps({ showSearchErrorMessage: true }); - return vm.$nextTick().then(() => { - const errorMessageEl = wrapper.find('.js-search-error-message'); + await nextTick(); + const errorMessageEl = wrapper.find('.js-search-error-message'); - expect(errorMessageEl.exists()).toBe(true); - expect(trimText(errorMessageEl.text())).toEqual( - 'Something went wrong, unable to search projects', - ); - }); + expect(errorMessageEl.exists()).toBe(true); + expect(trimText(errorMessageEl.text())).toEqual( + 'Something went wrong, unable to search projects', + ); }); describe('the search results legend', () => { @@ -121,7 +118,7 @@ describe('ProjectSelector component', () => { ${2} | ${3} | ${'Showing 2 of 3 projects'} `( 'is "$expected" given $count results are showing out of $total', - ({ count, total, expected }) => { + async ({ count, total, expected }) => { search('gitlab ui'); wrapper.setProps({ @@ -129,9 +126,8 @@ describe('ProjectSelector component', () => { totalResults: total, }); - return wrapper.vm.$nextTick().then(() => { - expect(findLegendText()).toBe(expected); - }); + await nextTick(); + expect(findLegendText()).toBe(expected); }, ); diff --git a/spec/frontend/vue_shared/components/registry/list_item_spec.js b/spec/frontend/vue_shared/components/registry/list_item_spec.js index ca4bf0b0652..1b93292e37b 100644 --- a/spec/frontend/vue_shared/components/registry/list_item_spec.js +++ b/spec/frontend/vue_shared/components/registry/list_item_spec.js @@ -1,5 +1,6 @@ import { GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import component from '~/vue_shared/components/registry/list_item.vue'; describe('list item', () => { @@ -70,10 +71,10 @@ describe('list item', () => { it('are visible when details is shown', async () => { mountComponent({}, slotMocks); - await wrapper.vm.$nextTick(); + await nextTick(); findToggleDetailsButton().vm.$emit('click'); - await wrapper.vm.$nextTick(); + await nextTick(); slotNames.forEach((name) => { expect(findDetailsSlot(name).exists()).toBe(true); }); @@ -90,7 +91,7 @@ describe('list item', () => { describe('details toggle button', () => { it('is visible when at least one details slot exists', async () => { mountComponent({}, { 'details-foo': '<span></span>' }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findToggleDetailsButton().exists()).toBe(true); }); diff --git a/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js b/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js index 79e41ed0c9e..d4b43caa7b2 100644 --- a/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js @@ -1,5 +1,6 @@ import { shallowMount } from '@vue/test-utils'; import { GlIcon } from '@gitlab/ui'; +import { nextTick } from 'vue'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import CollapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue'; @@ -61,7 +62,7 @@ describe('CollapsedCalendarIcon', () => { it('emits click event when container is clicked', async () => { wrapper.trigger('click'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted('click')[0]).toBeDefined(); }); diff --git a/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js b/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js index 263d1e9d947..ae05d85f74f 100644 --- a/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js @@ -1,5 +1,6 @@ import { GlLoadingIcon } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import DatePicker from '~/vue_shared/components/pikaday.vue'; import SidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue'; @@ -79,7 +80,7 @@ describe('SidebarDatePicker', () => { it('should enable editing when edit button is clicked', async () => { findEditButton().trigger('click'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.editing).toBe(true); }); diff --git a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js index 5336ecc614c..f213e37cbc1 100644 --- a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js @@ -10,6 +10,7 @@ import { import { shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; import axios from '~/lib/utils/axios_utils'; import IssuableMoveDropdown from '~/vue_shared/components/sidebar/issuable_move_dropdown.vue'; @@ -74,7 +75,7 @@ describe('IssuableMoveDropdown', () => { searchKey: 'foo', }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.fetchProjects).toHaveBeenCalledWith('foo'); }); @@ -151,7 +152,7 @@ describe('IssuableMoveDropdown', () => { selectedProject, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.isSelectedProject(project)).toBe(returnValue); }, @@ -164,7 +165,7 @@ describe('IssuableMoveDropdown', () => { selectedProject: null, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.isSelectedProject(mockProjects[0])).toBe(false); }); @@ -218,7 +219,7 @@ describe('IssuableMoveDropdown', () => { projectsListLoading: true, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findDropdownEl().find(GlLoadingIcon).exists()).toBe(true); }); @@ -231,7 +232,7 @@ describe('IssuableMoveDropdown', () => { selectedProject: mockProjects[0], }); - await wrapper.vm.$nextTick(); + await nextTick(); const dropdownItems = wrapper.findAll(GlDropdownItem); @@ -251,7 +252,7 @@ describe('IssuableMoveDropdown', () => { }); // Wait for `searchKey` watcher to run. - await wrapper.vm.$nextTick(); + await nextTick(); // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax @@ -260,7 +261,7 @@ describe('IssuableMoveDropdown', () => { projectsListLoading: false, }); - await wrapper.vm.$nextTick(); + await nextTick(); const dropdownContentEl = wrapper.find('[data-testid="content"]'); @@ -276,7 +277,7 @@ describe('IssuableMoveDropdown', () => { projectsListLoadFailed: true, }); - await wrapper.vm.$nextTick(); + await nextTick(); const dropdownContentEl = wrapper.find('[data-testid="content"]'); @@ -295,7 +296,7 @@ describe('IssuableMoveDropdown', () => { selectedProject: mockProjects[0], }); - await wrapper.vm.$nextTick(); + await nextTick(); expect( wrapper.find('[data-testid="footer"]').find(GlButton).attributes('disabled'), @@ -352,7 +353,7 @@ describe('IssuableMoveDropdown', () => { projects: mockProjects, }); - await wrapper.vm.$nextTick(); + await nextTick(); wrapper.findAll(GlDropdownItem).at(0).vm.$emit('click', mockEvent); @@ -366,7 +367,7 @@ describe('IssuableMoveDropdown', () => { selectedProject: mockProjects[0], }); - await wrapper.vm.$nextTick(); + await nextTick(); wrapper.find('[data-testid="footer"]').find(GlButton).vm.$emit('click'); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js index c4ed975e746..c05513a6d5f 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js @@ -1,6 +1,6 @@ import { GlIcon, GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import DropdownButton from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue'; @@ -71,13 +71,12 @@ describe('DropdownButton', () => { expect(dropdownTextEl.text()).toBe('Label'); }); - it('renders provided button text element', () => { + it('renders provided button text element', async () => { store.state.dropdownButtonText = 'Custom label'; const dropdownTextEl = findDropdownText(); - return wrapper.vm.$nextTick().then(() => { - expect(dropdownTextEl.text()).toBe('Custom label'); - }); + await nextTick(); + expect(dropdownTextEl.text()).toBe('Custom label'); }); it('renders chevron icon element', () => { diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js index 0eff6a1dace..0673ffee22b 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js @@ -1,6 +1,6 @@ import { GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue'; @@ -42,7 +42,7 @@ describe('DropdownContentsCreateView', () => { expect(wrapper.vm.disableCreate).toBe(true); }); - it('returns `true` when `labelCreateInProgress` is true', () => { + it('returns `true` when `labelCreateInProgress` is true', 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({ @@ -51,12 +51,11 @@ describe('DropdownContentsCreateView', () => { }); wrapper.vm.$store.dispatch('requestCreateLabel'); - return wrapper.vm.$nextTick(() => { - expect(wrapper.vm.disableCreate).toBe(true); - }); + await nextTick(); + expect(wrapper.vm.disableCreate).toBe(true); }); - it('returns `false` when label title and color is defined and create request is not already in progress', () => { + it('returns `false` when label title and color is defined and create request is not already in progress', 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({ @@ -64,9 +63,8 @@ describe('DropdownContentsCreateView', () => { selectedColor: '#ff0000', }); - return wrapper.vm.$nextTick(() => { - expect(wrapper.vm.disableCreate).toBe(false); - }); + await nextTick(); + expect(wrapper.vm.disableCreate).toBe(false); }); }); @@ -101,7 +99,7 @@ describe('DropdownContentsCreateView', () => { }); describe('handleCreateClick', () => { - it('calls action `createLabel` with object containing `labelTitle` & `selectedColor`', () => { + it('calls action `createLabel` with object containing `labelTitle` & `selectedColor`', async () => { jest.spyOn(wrapper.vm, 'createLabel').mockImplementation(); // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax @@ -112,14 +110,13 @@ describe('DropdownContentsCreateView', () => { wrapper.vm.handleCreateClick(); - return wrapper.vm.$nextTick(() => { - expect(wrapper.vm.createLabel).toHaveBeenCalledWith( - expect.objectContaining({ - title: 'Foo', - color: '#ff0000', - }), - ); - }); + await nextTick(); + expect(wrapper.vm.createLabel).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'Foo', + color: '#ff0000', + }), + ); }); }); }); @@ -169,25 +166,22 @@ describe('DropdownContentsCreateView', () => { }); }); - it('renders color input element', () => { + it('renders color input element', 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({ selectedColor: '#ff0000', }); - return wrapper.vm.$nextTick(() => { - const colorPreviewEl = wrapper.find( - '.color-input-container > .dropdown-label-color-preview', - ); - const colorInputEl = wrapper.find('.color-input-container').find(GlFormInput); + await nextTick(); + const colorPreviewEl = wrapper.find('.color-input-container > .dropdown-label-color-preview'); + const colorInputEl = wrapper.find('.color-input-container').find(GlFormInput); - expect(colorPreviewEl.exists()).toBe(true); - expect(colorPreviewEl.attributes('style')).toContain('background-color'); - expect(colorInputEl.exists()).toBe(true); - expect(colorInputEl.attributes('placeholder')).toBe('Use custom color #FF0000'); - expect(colorInputEl.attributes('value')).toBe('#ff0000'); - }); + expect(colorPreviewEl.exists()).toBe(true); + expect(colorPreviewEl.attributes('style')).toContain('background-color'); + expect(colorInputEl.exists()).toBe(true); + expect(colorInputEl.attributes('placeholder')).toBe('Use custom color #FF0000'); + expect(colorInputEl.attributes('value')).toBe('#ff0000'); }); it('renders create button element', () => { @@ -197,15 +191,14 @@ describe('DropdownContentsCreateView', () => { expect(createBtnEl.text()).toContain('Create'); }); - it('shows gl-loading-icon within create button element when `labelCreateInProgress` is `true`', () => { + it('shows gl-loading-icon within create button element when `labelCreateInProgress` is `true`', async () => { wrapper.vm.$store.dispatch('requestCreateLabel'); - return wrapper.vm.$nextTick(() => { - const loadingIconEl = wrapper.find('.dropdown-actions').find(GlLoadingIcon); + await nextTick(); + const loadingIconEl = wrapper.find('.dropdown-actions').find(GlLoadingIcon); - expect(loadingIconEl.exists()).toBe(true); - expect(loadingIconEl.isVisible()).toBe(true); - }); + expect(loadingIconEl.exists()).toBe(true); + expect(loadingIconEl.isVisible()).toBe(true); }); it('renders cancel button element', () => { diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js index 93a0e2f75bb..42202db4935 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js @@ -6,7 +6,7 @@ import { GlLink, } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue'; @@ -114,7 +114,7 @@ describe('DropdownContentsLabelsView', () => { wrapper.vm.$store.dispatch('receiveLabelsSuccess', labels); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.showNoMatchingResultsMessage).toBe(returnValue); }, @@ -249,7 +249,7 @@ describe('DropdownContentsLabelsView', () => { expect(wrapper.vm.toggleDropdownContents).toHaveBeenCalled(); }); - it('calls action `scrollIntoViewIfNeeded` in next tick when any key is pressed', () => { + it('calls action `scrollIntoViewIfNeeded` in next tick when any key is pressed', async () => { jest.spyOn(wrapper.vm, 'scrollIntoViewIfNeeded').mockImplementation(); // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax @@ -261,9 +261,8 @@ describe('DropdownContentsLabelsView', () => { keyCode: DOWN_KEY_CODE, }); - return wrapper.vm.$nextTick(() => { - expect(wrapper.vm.scrollIntoViewIfNeeded).toHaveBeenCalled(); - }); + await nextTick(); + expect(wrapper.vm.scrollIntoViewIfNeeded).toHaveBeenCalled(); }); }); @@ -294,15 +293,14 @@ describe('DropdownContentsLabelsView', () => { expect(wrapper.find(GlIntersectionObserver).exists()).toBe(true); }); - it('renders gl-loading-icon component when `labelsFetchInProgress` prop is true', () => { + it('renders gl-loading-icon component when `labelsFetchInProgress` prop is true', async () => { wrapper.vm.$store.dispatch('requestLabels'); - return wrapper.vm.$nextTick(() => { - const loadingIconEl = findLoadingIcon(); + await nextTick(); + const loadingIconEl = findLoadingIcon(); - expect(loadingIconEl.exists()).toBe(true); - expect(loadingIconEl.attributes('class')).toContain('labels-fetch-loading'); - }); + expect(loadingIconEl.exists()).toBe(true); + expect(loadingIconEl.attributes('class')).toContain('labels-fetch-loading'); }); it('renders dropdown title element', () => { @@ -339,47 +337,44 @@ describe('DropdownContentsLabelsView', () => { expect(wrapper.findAll(LabelItem)).toHaveLength(mockLabels.length); }); - it('renders label element with `highlight` set to true when value of `currentHighlightItem` is more than -1', () => { + it('renders label element with `highlight` set to true when value of `currentHighlightItem` is more than -1', 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({ currentHighlightItem: 0, }); - return wrapper.vm.$nextTick(() => { - const labelItemEl = findDropdownContent().find(LabelItem); + await nextTick(); + const labelItemEl = findDropdownContent().find(LabelItem); - expect(labelItemEl.attributes('highlight')).toBe('true'); - }); + expect(labelItemEl.attributes('highlight')).toBe('true'); }); - it('renders element containing "No matching results" when `searchKey` does not match with any label', () => { + it('renders element containing "No matching results" when `searchKey` does not match with any label', 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({ searchKey: 'abc', }); - return wrapper.vm.$nextTick(() => { - const noMatchEl = findDropdownContent().find('li'); + await nextTick(); + const noMatchEl = findDropdownContent().find('li'); - expect(noMatchEl.isVisible()).toBe(true); - expect(noMatchEl.text()).toContain('No matching results'); - }); + expect(noMatchEl.isVisible()).toBe(true); + expect(noMatchEl.text()).toContain('No matching results'); }); - it('renders empty content while loading', () => { + it('renders empty content while loading', async () => { wrapper.vm.$store.state.labelsFetchInProgress = true; - return wrapper.vm.$nextTick(() => { - const dropdownContent = findDropdownContent(); - const loadingIcon = findLoadingIcon(); + await nextTick(); + const dropdownContent = findDropdownContent(); + const loadingIcon = findLoadingIcon(); - expect(dropdownContent.exists()).toBe(true); - expect(dropdownContent.isVisible()).toBe(true); - expect(loadingIcon.exists()).toBe(true); - expect(loadingIcon.isVisible()).toBe(true); - }); + expect(dropdownContent.exists()).toBe(true); + expect(dropdownContent.isVisible()).toBe(true); + expect(loadingIcon.exists()).toBe(true); + expect(loadingIcon.isVisible()).toBe(true); }); it('renders footer list items', () => { @@ -393,14 +388,13 @@ describe('DropdownContentsLabelsView', () => { expect(manageLabelsLink.text()).toBe('Manage labels'); }); - it('does not render "Create label" footer link when `state.allowLabelCreate` is `false`', () => { + it('does not render "Create label" footer link when `state.allowLabelCreate` is `false`', async () => { wrapper.vm.$store.state.allowLabelCreate = false; - return wrapper.vm.$nextTick(() => { - const createLabelLink = findDropdownFooter().findAll(GlLink).at(0); + await nextTick(); + const createLabelLink = findDropdownFooter().findAll(GlLink).at(0); - expect(createLabelLink.text()).not.toBe('Create label'); - }); + expect(createLabelLink.text()).not.toBe('Create label'); }); it('does not render footer list items when `state.variant` is "standalone"', () => { diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js index 110c1d1b7eb..84e9f3f41c3 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js @@ -1,6 +1,6 @@ import { GlButton, GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue'; @@ -47,14 +47,13 @@ describe('DropdownTitle', () => { expect(editBtnEl.text()).toBe('Edit'); }); - it('renders loading icon element when `labelsSelectInProgress` prop is true', () => { + it('renders loading icon element when `labelsSelectInProgress` prop is true', async () => { wrapper.setProps({ labelsSelectInProgress: true, }); - return wrapper.vm.$nextTick(() => { - expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true); - }); + await nextTick(); + expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true); }); }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js index a7f9391cb5f..c6400320dea 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js @@ -1,5 +1,6 @@ import { shallowMount } from '@vue/test-utils'; import { GlIcon } from '@gitlab/ui'; +import { nextTick } from 'vue'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import DropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue'; @@ -42,7 +43,7 @@ describe('DropdownValueCollapsedComponent', () => { wrapper.trigger('click'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted('onValueClick')[0]).toBeDefined(); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js index 4b0ba075eda..31819d0e2f7 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import { isInViewport } from '~/lib/utils/common_utils'; @@ -139,27 +139,26 @@ describe('LabelsSelectRoot', () => { ${'embedded'} | ${'is-embedded'} `( 'renders component root element with CSS class `$cssClass` when `state.variant` is "$variant"', - ({ variant, cssClass }) => { + async ({ variant, cssClass }) => { createComponent({ ...mockConfig, variant, }); - return wrapper.vm.$nextTick(() => { - expect(wrapper.classes()).toContain(cssClass); - }); + await nextTick(); + expect(wrapper.classes()).toContain(cssClass); }, ); it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', async () => { createComponent(); - await wrapper.vm.$nextTick; + await nextTick; expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true); }); it('renders `dropdown-title` component', async () => { createComponent(); - await wrapper.vm.$nextTick; + await nextTick; expect(wrapper.find(DropdownTitle).exists()).toBe(true); }); @@ -167,7 +166,7 @@ describe('LabelsSelectRoot', () => { createComponent(mockConfig, { default: 'None', }); - await wrapper.vm.$nextTick; + await nextTick; const valueComp = wrapper.find(DropdownValue); @@ -178,14 +177,14 @@ describe('LabelsSelectRoot', () => { it('renders `dropdown-button` component when `showDropdownButton` prop is `true`', async () => { createComponent(); wrapper.vm.$store.dispatch('toggleDropdownButton'); - await wrapper.vm.$nextTick; + await nextTick; expect(wrapper.find(DropdownButton).exists()).toBe(true); }); it('renders `dropdown-contents` component when `showDropdownButton` & `showDropdownContents` prop is `true`', async () => { createComponent(); wrapper.vm.$store.dispatch('toggleDropdownContents'); - await wrapper.vm.$nextTick; + await nextTick; expect(wrapper.find(DropdownContents).exists()).toBe(true); }); @@ -198,22 +197,20 @@ describe('LabelsSelectRoot', () => { wrapper.vm.$store.dispatch('toggleDropdownContents'); }); - it('set direction when out of viewport', () => { + it('set direction when out of viewport', async () => { isInViewport.mockImplementation(() => false); wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(true); - }); + await nextTick(); + expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(true); }); - it('does not set direction when inside of viewport', () => { + it('does not set direction when inside of viewport', async () => { isInViewport.mockImplementation(() => true); wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(false); - }); + await nextTick(); + expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(false); }); }, ); diff --git a/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js index a6c9bda1aa2..267a467059d 100644 --- a/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js @@ -1,6 +1,7 @@ import { GlButton } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import ToggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue'; describe('ToggleSidebar', () => { @@ -38,7 +39,7 @@ describe('ToggleSidebar', () => { createComponent({ mountFn: mount }); findGlButton().trigger('click'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted('toggle')[0]).toBeDefined(); }); diff --git a/spec/frontend/vue_shared/components/split_button_spec.js b/spec/frontend/vue_shared/components/split_button_spec.js index ad11e6519c4..4965969bc3e 100644 --- a/spec/frontend/vue_shared/components/split_button_spec.js +++ b/spec/frontend/vue_shared/components/split_button_spec.js @@ -1,6 +1,7 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import SplitButton from '~/vue_shared/components/split_button.vue'; const mockActionItems = [ @@ -27,15 +28,15 @@ describe('SplitButton', () => { const findDropdown = () => wrapper.find(GlDropdown); const findDropdownItem = (index = 0) => findDropdown().findAll(GlDropdownItem).at(index); - const selectItem = (index) => { + const selectItem = async (index) => { findDropdownItem(index).vm.$emit('click'); - return wrapper.vm.$nextTick(); + await nextTick(); }; - const clickToggleButton = () => { + const clickToggleButton = async () => { findDropdown().vm.$emit('click'); - return wrapper.vm.$nextTick(); + await nextTick(); }; it('fails for empty actionItems', () => { diff --git a/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js b/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js index b3cdbccb271..2f5afeec1fc 100644 --- a/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js +++ b/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js @@ -1,5 +1,6 @@ import { GlIcon, GlSprintf } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue'; jest.mock('~/flash'); @@ -84,47 +85,40 @@ describe('Upload dropzone component', () => { ${'contains text'} | ${mockDragEvent({ types: ['text'] })} ${'contains files and text'} | ${mockDragEvent({ types: ['Files', 'text'] })} ${'contains files'} | ${mockDragEvent({ types: ['Files'] })} - `('renders correct template when drag event $description', ({ eventPayload }) => { + `('renders correct template when drag event $description', async ({ eventPayload }) => { createComponent(); wrapper.trigger('dragenter', eventPayload); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); + await nextTick(); + expect(wrapper.element).toMatchSnapshot(); }); - it('renders correct template when dragging stops', () => { + it('renders correct template when dragging stops', async () => { createComponent(); wrapper.trigger('dragenter'); - return wrapper.vm - .$nextTick() - .then(() => { - wrapper.trigger('dragleave'); - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(wrapper.element).toMatchSnapshot(); - }); + + await nextTick(); + wrapper.trigger('dragleave'); + + await nextTick(); + expect(wrapper.element).toMatchSnapshot(); }); }); describe('when dropping', () => { - it('emits upload event', () => { + it('emits upload event', async () => { createComponent(); const mockFile = { name: 'test', type: 'image/jpg' }; const mockEvent = mockDragEvent({ files: [mockFile] }); wrapper.trigger('dragenter', mockEvent); - return wrapper.vm - .$nextTick() - .then(() => { - wrapper.trigger('drop', mockEvent); - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(wrapper.emitted().change[0]).toEqual([[mockFile]]); - }); + + await nextTick(); + wrapper.trigger('drop', mockEvent); + + await nextTick(); + expect(wrapper.emitted().change[0]).toEqual([[mockFile]]); }); }); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js index 1d15da491cd..66bb234aef6 100644 --- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js @@ -1,5 +1,6 @@ import { GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { TEST_HOST } from 'spec/test_constants'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue'; @@ -142,14 +143,13 @@ describe('UserAvatarList', () => { expect(links.length).toEqual(props.items.length); }); - it('with collapse clicked, it renders avatars up to breakpoint', () => { + it('with collapse clicked, it renders avatars up to breakpoint', async () => { clickButton(); - return wrapper.vm.$nextTick(() => { - const links = wrapper.findAll(UserAvatarLink); + await nextTick(); + const links = wrapper.findAll(UserAvatarLink); - expect(links.length).toEqual(TEST_BREAKPOINT); - }); + expect(links.length).toEqual(TEST_BREAKPOINT); }); }); }); diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js index 659d93d6597..9b7c594b910 100644 --- a/spec/frontend/vue_shared/components/web_ide_link_spec.js +++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js @@ -213,7 +213,7 @@ describe('Web IDE link component', () => { findLocalStorageSync().vm.$emit('input', ACTION_GITPOD.key); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findActionsButton().props('selectedKey')).toBe(ACTION_GITPOD.key); }); @@ -223,7 +223,7 @@ describe('Web IDE link component', () => { findActionsButton().vm.$emit('select', ACTION_GITPOD.key); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findActionsButton().props('selectedKey')).toBe(ACTION_GITPOD.key); expect(findLocalStorageSync().props('value')).toBe(ACTION_GITPOD.key); diff --git a/spec/frontend/vue_shared/directives/track_event_spec.js b/spec/frontend/vue_shared/directives/track_event_spec.js index b3f94d0242a..4bf84b06246 100644 --- a/spec/frontend/vue_shared/directives/track_event_spec.js +++ b/spec/frontend/vue_shared/directives/track_event_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import Tracking from '~/tracking'; import TrackEvent from '~/vue_shared/directives/track_event'; @@ -31,7 +31,7 @@ describe('Error Tracking directive', () => { expect(Tracking.event).not.toHaveBeenCalled(); }); - it('should track event on click if tracking info provided', () => { + it('should track event on click if tracking info provided', async () => { const trackingOptions = { category: 'Tracking', action: 'click_trackable_btn', @@ -43,9 +43,8 @@ describe('Error Tracking directive', () => { wrapper.setData({ trackingOptions }); const { category, action, label, property, value } = trackingOptions; - return wrapper.vm.$nextTick(() => { - button.trigger('click'); - expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property, value }); - }); + await nextTick(); + button.trigger('click'); + expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property, value }); }); }); diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js index 0f33a3d1122..7dfeced571a 100644 --- a/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js +++ b/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js @@ -1,5 +1,6 @@ import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import IssuableBulkEditSidebar from '~/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar.vue'; const createComponent = ({ expanded = true } = {}) => @@ -48,7 +49,7 @@ describe('IssuableBulkEditSidebar', () => { expanded, }); - await wrappeCustom.vm.$nextTick(); + await nextTick(); expect(document.querySelector('.layout-page').classList.contains(layoutPageClass)).toBe( true, @@ -78,7 +79,7 @@ describe('IssuableBulkEditSidebar', () => { expanded, }); - await wrappeCustom.vm.$nextTick(); + await nextTick(); expect(wrappeCustom.classes()).toContain(layoutPageClass); diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js index e38a80e7734..0585b6c55fd 100644 --- a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js +++ b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js @@ -1,4 +1,5 @@ import { GlLink, GlLabel, GlIcon, GlFormCheckbox, GlSprintf } from '@gitlab/ui'; +import { nextTick } from 'vue'; import { useFakeDate } from 'helpers/fake_date'; import { shallowMountExtended as shallowMount } from 'helpers/vue_test_utils_helper'; import IssuableItem from '~/vue_shared/issuable/list/components/issuable_item.vue'; @@ -76,7 +77,7 @@ describe('IssuableItem', () => { }, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.authorId).toBe(returnValue); }, @@ -100,7 +101,7 @@ describe('IssuableItem', () => { }, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.isIssuableUrlExternal).toBe(returnValue); }, @@ -122,7 +123,7 @@ describe('IssuableItem', () => { }, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.labels).toEqual(mockLabels); }); @@ -135,7 +136,7 @@ describe('IssuableItem', () => { }, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.labels).toEqual([]); }); @@ -224,7 +225,7 @@ describe('IssuableItem', () => { enableLabelPermalinks: false, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.labelTarget(mockRegularLabel)).toBe('#'); }); @@ -248,7 +249,7 @@ describe('IssuableItem', () => { }, }); - await wrapper.vm.$nextTick(); + await nextTick(); const titleEl = wrapper.find('[data-testid="issuable-title"]'); @@ -264,7 +265,7 @@ describe('IssuableItem', () => { showCheckbox: true, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.find(GlFormCheckbox).exists()).toBe(true); expect(wrapper.find(GlFormCheckbox).attributes('checked')).not.toBeDefined(); @@ -273,7 +274,7 @@ describe('IssuableItem', () => { checked: true, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.find(GlFormCheckbox).attributes('checked')).toBe('true'); }); @@ -286,7 +287,7 @@ describe('IssuableItem', () => { }, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.find('[data-testid="issuable-title"]').find(GlLink).attributes('target')).toBe( '_blank', @@ -301,7 +302,7 @@ describe('IssuableItem', () => { }, }); - await wrapper.vm.$nextTick(); + await nextTick(); const confidentialEl = wrapper.find('[data-testid="issuable-title"]').find(GlIcon); diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js index 14e93108447..64823cd4c6c 100644 --- a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js +++ b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js @@ -2,6 +2,7 @@ import { GlAlert, GlKeysetPagination, GlSkeletonLoading, GlPagination } from '@g import { shallowMount } from '@vue/test-utils'; import VueDraggable from 'vuedraggable'; +import { nextTick } from 'vue'; import { TEST_HOST } from 'helpers/test_constants'; import IssuableItem from '~/vue_shared/issuable/list/components/issuable_item.vue'; @@ -77,7 +78,7 @@ describe('IssuableListRoot', () => { currentPage, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.skeletonItemCount).toBe(returnValue); }, @@ -96,7 +97,7 @@ describe('IssuableListRoot', () => { issuables, }); - await wrapper.vm.$nextTick(); + await nextTick(); // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax @@ -104,7 +105,7 @@ describe('IssuableListRoot', () => { checkedIssuables, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.allIssuablesChecked).toBe(returnValue); }, @@ -119,7 +120,7 @@ describe('IssuableListRoot', () => { checkedIssuables: mockCheckedIssuables, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.bulkEditIssuables).toHaveLength(mIssuables.length); }); @@ -137,7 +138,7 @@ describe('IssuableListRoot', () => { issuables: [mockIssuables[0]], }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(Object.keys(wrapper.vm.checkedIssuables)).toHaveLength(1); expect(wrapper.vm.checkedIssuables[mockIssuables[0].iid]).toEqual({ @@ -160,7 +161,7 @@ describe('IssuableListRoot', () => { urlParams, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(global.window.location.href).toBe( `${TEST_HOST}/?state=${urlParams.state}&sort=${urlParams.sort}&page=${urlParams.page}&search=${urlParams.search}`, @@ -192,7 +193,7 @@ describe('IssuableListRoot', () => { }, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.issuableChecked(mockIssuables[0])).toBe(true); }); diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js index 41bacf18a68..1a93838b03f 100644 --- a/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js @@ -1,4 +1,5 @@ import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { useFakeDate } from 'helpers/fake_date'; import IssuableBody from '~/vue_shared/issuable/show/components/issuable_body.vue'; @@ -68,7 +69,7 @@ describe('IssuableBody', () => { }, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.isUpdated).toBe(returnValue); }, @@ -90,13 +91,13 @@ describe('IssuableBody', () => { editFormVisible: true, }); - await wrapper.vm.$nextTick(); + await nextTick(); wrapper.setProps({ editFormVisible: false, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.initTaskList).toHaveBeenCalled(); }); @@ -182,7 +183,7 @@ describe('IssuableBody', () => { editFormVisible: true, }); - await wrapper.vm.$nextTick(); + await nextTick(); const editFormEl = wrapper.find(IssuableEditForm); expect(editFormEl.exists()).toBe(true); @@ -221,7 +222,7 @@ describe('IssuableBody', () => { editFormVisible: true, }); - await wrapper.vm.$nextTick(); + await nextTick(); const issuableEditForm = wrapper.find(IssuableEditForm); diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js index 051ffd27af4..d3e484cf913 100644 --- a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js @@ -1,6 +1,7 @@ import { GlFormInput } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import IssuableEditForm from '~/vue_shared/issuable/show/components/issuable_edit_form.vue'; import IssuableEventHub from '~/vue_shared/issuable/show/event_hub'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; @@ -52,7 +53,7 @@ describe('IssuableEditForm', () => { }, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.title).toBe('Foo'); expect(wrapper.vm.description).toBe('Foobar'); @@ -67,7 +68,7 @@ describe('IssuableEditForm', () => { }, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.vm.title).toBe(''); expect(wrapper.vm.description).toBe(''); diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js index 41735923957..1cdd709159f 100644 --- a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js @@ -1,5 +1,6 @@ import { GlIcon, GlAvatarLabeled } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import IssuableHeader from '~/vue_shared/issuable/show/components/issuable_header.vue'; @@ -78,7 +79,7 @@ describe('IssuableHeader', () => { blocked: true, }); - await wrapper.vm.$nextTick(); + await nextTick(); const blockedEl = wrapper.findByTestId('blocked'); @@ -91,7 +92,7 @@ describe('IssuableHeader', () => { confidential: true, }); - await wrapper.vm.$nextTick(); + await nextTick(); const confidentialEl = wrapper.findByTestId('confidential'); diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js index cb418371760..93de6dbe306 100644 --- a/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js @@ -1,5 +1,6 @@ import { GlIcon, GlButton, GlIntersectionObserver } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import IssuableTitle from '~/vue_shared/issuable/show/components/issuable_title.vue'; @@ -64,7 +65,7 @@ describe('IssuableTitle', () => { }, }); - await wrapperWithTitle.vm.$nextTick(); + await nextTick(); const titleEl = wrapperWithTitle.find('h2'); expect(titleEl.exists()).toBe(true); @@ -90,7 +91,7 @@ describe('IssuableTitle', () => { stickyTitleVisible: true, }); - await wrapper.vm.$nextTick(); + await nextTick(); const stickyHeaderEl = wrapper.find('[data-testid="header"]'); expect(stickyHeaderEl.exists()).toBe(true); diff --git a/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js b/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js index 788ba70ddc0..1ef1304185d 100644 --- a/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js +++ b/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js @@ -1,5 +1,6 @@ import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; import Cookies from 'js-cookie'; +import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import IssuableSidebarRoot from '~/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue'; @@ -88,7 +89,7 @@ describe('IssuableSidebarRoot', () => { jest.spyOn(bp, 'isDesktop').mockReturnValue(breakpoint === 'lg' || breakpoint === 'xl'); window.dispatchEvent(new Event('resize')); - await wrapper.vm.$nextTick(); + await nextTick(); assertPageLayoutClasses({ isExpanded: isExpandedValue }); }, diff --git a/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js b/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js index 2d51f6dbeeb..c90131fea9a 100644 --- a/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js +++ b/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js @@ -37,9 +37,8 @@ describe('Welcome page', () => { const link = wrapper.find('a'); link.trigger('click'); await nextTick(); - return wrapper.vm.$nextTick().then(() => { - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_tab', { label: 'test' }); - }); + await nextTick(); + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_tab', { label: 'test' }); }); it('renders footer slot if provided', () => { diff --git a/spec/frontend/work_items/pages/create_work_item_spec.js b/spec/frontend/work_items/pages/create_work_item_spec.js index 71e153d30c3..00571c1f5c8 100644 --- a/spec/frontend/work_items/pages/create_work_item_spec.js +++ b/spec/frontend/work_items/pages/create_work_item_spec.js @@ -19,7 +19,7 @@ describe('Create work item component', () => { const findCreateButton = () => wrapper.find('[data-testid="create-button"]'); const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]'); - const createComponent = ({ data = {} } = {}) => { + const createComponent = ({ data = {}, props = {} } = {}) => { fakeApollo = createMockApollo([], resolvers); wrapper = shallowMount(CreateWorkItem, { apolloProvider: fakeApollo, @@ -28,6 +28,9 @@ describe('Create work item component', () => { ...data, }; }, + propsData: { + ...props, + }, mocks: { $router: { go: jest.fn(), @@ -54,40 +57,99 @@ describe('Create work item component', () => { expect(findCreateButton().props('disabled')).toBe(true); }); - it('redirects to the previous page on Cancel button click', () => { - createComponent(); - findCancelButton().vm.$emit('click'); + describe('when displayed on a separate route', () => { + beforeEach(() => { + createComponent(); + }); + + it('redirects to the previous page on Cancel button click', () => { + findCancelButton().vm.$emit('click'); + + expect(wrapper.vm.$router.go).toHaveBeenCalledWith(-1); + }); + + it('redirects to the work item page on successful mutation', async () => { + findTitleInput().vm.$emit('title-input', 'Test title'); + + wrapper.find('form').trigger('submit'); + await waitForPromises(); + + expect(wrapper.vm.$router.push).toHaveBeenCalled(); + }); + + it('adds right margin for create button', () => { + expect(findCreateButton().classes()).toContain('gl-mr-3'); + }); + + it('does not add right margin for cancel button', () => { + expect(findCancelButton().classes()).not.toContain('gl-mr-3'); + }); + }); + + describe('when displayed in a modal', () => { + beforeEach(() => { + createComponent({ + props: { + isModal: true, + }, + }); + }); + + it('emits `closeModal` event on Cancel button click', () => { + findCancelButton().vm.$emit('click'); - expect(wrapper.vm.$router.go).toHaveBeenCalledWith(-1); + expect(wrapper.emitted('closeModal')).toEqual([[]]); + }); + + it('emits `onCreate` on successful mutation', async () => { + const mockTitle = 'Test title'; + findTitleInput().vm.$emit('title-input', 'Test title'); + + wrapper.find('form').trigger('submit'); + await waitForPromises(); + + expect(wrapper.emitted('onCreate')).toEqual([[mockTitle]]); + }); + + it('does not right margin for create button', () => { + expect(findCreateButton().classes()).not.toContain('gl-mr-3'); + }); + + it('adds right margin for cancel button', () => { + expect(findCancelButton().classes()).toContain('gl-mr-3'); + }); }); it('hides the alert on dismissing the error', async () => { createComponent({ data: { error: true } }); + expect(findAlert().exists()).toBe(true); findAlert().vm.$emit('dismiss'); await nextTick(); + expect(findAlert().exists()).toBe(false); }); + it('displays an initial title if passed', () => { + const initialTitle = 'Initial Title'; + createComponent({ + props: { initialTitle }, + }); + expect(findTitleInput().props('initialTitle')).toBe(initialTitle); + }); + describe('when title input field has a text', () => { - beforeEach(async () => { + beforeEach(() => { const mockTitle = 'Test title'; createComponent(); - await findTitleInput().vm.$emit('title-input', mockTitle); + findTitleInput().vm.$emit('title-input', mockTitle); }); it('renders a non-disabled Create button', () => { expect(findCreateButton().props('disabled')).toBe(false); }); - it('redirects to the work item page on successful mutation', async () => { - wrapper.find('form').trigger('submit'); - await waitForPromises(); - - expect(wrapper.vm.$router.push).toHaveBeenCalled(); - }); - // TODO: write a proper test here when we have a backend implementation it.todo('shows an alert on mutation error'); }); diff --git a/spec/frontend_integration/ide/ide_integration_spec.js b/spec/frontend_integration/ide/ide_integration_spec.js index 5f1a5b0d048..aad9b9e526c 100644 --- a/spec/frontend_integration/ide/ide_integration_spec.js +++ b/spec/frontend_integration/ide/ide_integration_spec.js @@ -1,3 +1,4 @@ +import { nextTick } from 'vue'; import { setTestTimeout } from 'helpers/timeout'; import waitForPromises from 'helpers/wait_for_promises'; import { waitForText } from 'helpers/wait_for_text'; @@ -134,7 +135,7 @@ describe('WebIDE', () => { describe('when editor position changes', () => { beforeEach(async () => { editor.setPosition({ lineNumber: 4, column: 10 }); - await vm.$nextTick(); + await nextTick(); }); it('shows new line position', () => { @@ -145,7 +146,7 @@ describe('WebIDE', () => { it('updates after rename', async () => { await ideHelper.renameFile('README.md', 'READMEZ.txt'); await ideHelper.waitForEditorModelChange(editor); - await vm.$nextTick(); + await nextTick(); expect(statusBar).toHaveText('1:1'); expect(statusBar).toHaveText('plaintext'); @@ -166,7 +167,7 @@ describe('WebIDE', () => { await ideHelper.closeFile('README.md'); await ideHelper.openFile('README.md'); await ideHelper.waitForMonacoEditor(); - await vm.$nextTick(); + await nextTick(); expect(statusBar).toHaveText('4:10'); expect(statusBar).toHaveText('markdown'); diff --git a/spec/graphql/types/ci/runner_type_spec.rb b/spec/graphql/types/ci/runner_type_spec.rb index 3f89058f44a..3047817a952 100644 --- a/spec/graphql/types/ci/runner_type_spec.rb +++ b/spec/graphql/types/ci/runner_type_spec.rb @@ -9,7 +9,7 @@ RSpec.describe GitlabSchema.types['CiRunner'] do it 'contains attributes related to a runner' do expected_fields = %w[ - id description created_at contacted_at maximum_timeout access_level active status + id description created_at contacted_at maximum_timeout access_level active paused status version short_sha revision locked run_untagged ip_address runner_type tag_list project_count job_count admin_url edit_admin_url user_permissions executor_name groups projects diff --git a/spec/lib/gitlab/background_migration/batching_strategies/backfill_project_namespace_per_group_batching_strategy_spec.rb b/spec/lib/gitlab/background_migration/batching_strategies/backfill_project_namespace_per_group_batching_strategy_spec.rb new file mode 100644 index 00000000000..7b8a466b37c --- /dev/null +++ b/spec/lib/gitlab/background_migration/batching_strategies/backfill_project_namespace_per_group_batching_strategy_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::BackfillProjectNamespacePerGroupBatchingStrategy, '#next_batch' do + let!(:namespaces) { table(:namespaces) } + let!(:projects) { table(:projects) } + let!(:background_migrations) { table(:batched_background_migrations) } + + let!(:namespace1) { namespaces.create!(name: 'batchtest1', type: 'Group', path: 'batch-test1') } + let!(:namespace2) { namespaces.create!(name: 'batchtest2', type: 'Group', parent_id: namespace1.id, path: 'batch-test2') } + let!(:namespace3) { namespaces.create!(name: 'batchtest3', type: 'Group', parent_id: namespace2.id, path: 'batch-test3') } + + let!(:project1) { projects.create!(name: 'project1', path: 'project1', namespace_id: namespace1.id, visibility_level: 20) } + let!(:project2) { projects.create!(name: 'project2', path: 'project2', namespace_id: namespace2.id, visibility_level: 20) } + let!(:project3) { projects.create!(name: 'project3', path: 'project3', namespace_id: namespace3.id, visibility_level: 20) } + let!(:project4) { projects.create!(name: 'project4', path: 'project4', namespace_id: namespace3.id, visibility_level: 20) } + let!(:batching_strategy) { described_class.new } + + let(:job_arguments) { [namespace1.id, 'up'] } + + context 'when starting on the first batch' do + it 'returns the bounds of the next batch' do + batch_bounds = batching_strategy.next_batch(:projects, :id, batch_min_value: project1.id, batch_size: 3, job_arguments: job_arguments) + + expect(batch_bounds).to match_array([project1.id, project3.id]) + end + end + + context 'when additional batches remain' do + it 'returns the bounds of the next batch' do + batch_bounds = batching_strategy.next_batch(:projects, :id, batch_min_value: project2.id, batch_size: 3, job_arguments: job_arguments) + + expect(batch_bounds).to match_array([project2.id, project4.id]) + end + end + + context 'when on the final batch' do + it 'returns the bounds of the next batch' do + batch_bounds = batching_strategy.next_batch(:projects, :id, batch_min_value: project4.id, batch_size: 3, job_arguments: job_arguments) + + expect(batch_bounds).to match_array([project4.id, project4.id]) + end + end + + context 'when no additional batches remain' do + it 'returns nil' do + batch_bounds = batching_strategy.next_batch(:projects, :id, batch_min_value: project4.id + 1, batch_size: 1, job_arguments: job_arguments) + + expect(batch_bounds).to be_nil + end + end +end diff --git a/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb b/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb index 8febe850e04..39030039125 100644 --- a/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb +++ b/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb @@ -13,7 +13,7 @@ RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchi context 'when starting on the first batch' do it 'returns the bounds of the next batch' do - batch_bounds = batching_strategy.next_batch(:namespaces, :id, batch_min_value: namespace1.id, batch_size: 3) + batch_bounds = batching_strategy.next_batch(:namespaces, :id, batch_min_value: namespace1.id, batch_size: 3, job_arguments: nil) expect(batch_bounds).to eq([namespace1.id, namespace3.id]) end @@ -21,7 +21,7 @@ RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchi context 'when additional batches remain' do it 'returns the bounds of the next batch' do - batch_bounds = batching_strategy.next_batch(:namespaces, :id, batch_min_value: namespace2.id, batch_size: 3) + batch_bounds = batching_strategy.next_batch(:namespaces, :id, batch_min_value: namespace2.id, batch_size: 3, job_arguments: nil) expect(batch_bounds).to eq([namespace2.id, namespace4.id]) end @@ -29,7 +29,7 @@ RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchi context 'when on the final batch' do it 'returns the bounds of the next batch' do - batch_bounds = batching_strategy.next_batch(:namespaces, :id, batch_min_value: namespace4.id, batch_size: 3) + batch_bounds = batching_strategy.next_batch(:namespaces, :id, batch_min_value: namespace4.id, batch_size: 3, job_arguments: nil) expect(batch_bounds).to eq([namespace4.id, namespace4.id]) end @@ -37,7 +37,7 @@ RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchi context 'when no additional batches remain' do it 'returns nil' do - batch_bounds = batching_strategy.next_batch(:namespaces, :id, batch_min_value: namespace4.id + 1, batch_size: 1) + batch_bounds = batching_strategy.next_batch(:namespaces, :id, batch_min_value: namespace4.id + 1, batch_size: 1, job_arguments: nil) expect(batch_bounds).to be_nil end diff --git a/spec/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces_spec.rb b/spec/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces_spec.rb index 24259b06469..2c5de448fbc 100644 --- a/spec/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces_spec.rb +++ b/spec/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces_spec.rb @@ -30,7 +30,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa start_id = ::Project.minimum(:id) end_id = ::Project.maximum(:id) projects_count = ::Project.count - batches_count = (projects_count / described_class::BATCH_SIZE.to_f).ceil + batches_count = (projects_count / described_class::SUB_BATCH_SIZE.to_f).ceil project_namespaces_count = ::Namespace.where(type: 'Project').count migration = described_class.new @@ -39,7 +39,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa expect(migration).to receive(:batch_update_projects).exactly(batches_count).and_call_original expect(migration).to receive(:batch_update_project_namespaces_traversal_ids).exactly(batches_count).and_call_original - expect { migration.perform(start_id, end_id, nil, 'up') }.to change(Namespace.where(type: 'Project'), :count) + expect { migration.perform(start_id, end_id, nil, nil, nil, nil, nil, 'up') }.to change(Namespace.where(type: 'Project'), :count) expect(projects_count).to eq(::Namespace.where(type: 'Project').count) check_projects_in_sync_with(Namespace.where(type: 'Project')) @@ -53,7 +53,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa start_id = backfilled_namespace_projects.minimum(:id) end_id = backfilled_namespace_projects.maximum(:id) group_projects_count = backfilled_namespace_projects.count - batches_count = (group_projects_count / described_class::BATCH_SIZE.to_f).ceil + batches_count = (group_projects_count / described_class::SUB_BATCH_SIZE.to_f).ceil project_namespaces_in_hierarchy = project_namespaces_in_hierarchy(base_ancestor(backfilled_namespace)) migration = described_class.new @@ -66,7 +66,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa expect(group_projects_count).to eq(14) expect(project_namespaces_in_hierarchy.count).to eq(0) - migration.perform(start_id, end_id, backfilled_namespace.id, 'up') + migration.perform(start_id, end_id, nil, nil, nil, nil, backfilled_namespace.id, 'up') expect(project_namespaces_in_hierarchy.count).to eq(14) check_projects_in_sync_with(project_namespaces_in_hierarchy) @@ -79,7 +79,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa start_id = hierarchy1_projects.minimum(:id) end_id = hierarchy1_projects.maximum(:id) - described_class.new.perform(start_id, end_id, parent_group1.id, 'up') + described_class.new.perform(start_id, end_id, nil, nil, nil, nil, parent_group1.id, 'up') end it 'does not duplicate project namespaces' do @@ -87,7 +87,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa projects_count = ::Project.count start_id = ::Project.minimum(:id) end_id = ::Project.maximum(:id) - batches_count = (projects_count / described_class::BATCH_SIZE.to_f).ceil + batches_count = (projects_count / described_class::SUB_BATCH_SIZE.to_f).ceil project_namespaces = ::Namespace.where(type: 'Project') migration = described_class.new @@ -100,7 +100,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa expect(migration).to receive(:batch_update_projects).exactly(batches_count).and_call_original expect(migration).to receive(:batch_update_project_namespaces_traversal_ids).exactly(batches_count).and_call_original - expect { migration.perform(start_id, end_id, nil, 'up') }.to change(project_namespaces, :count).by(14) + expect { migration.perform(start_id, end_id, nil, nil, nil, nil, nil, 'up') }.to change(project_namespaces, :count).by(14) expect(projects_count).to eq(project_namespaces.count) end @@ -125,7 +125,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa context 'back-fill project namespaces in batches' do before do - stub_const("#{described_class.name}::BATCH_SIZE", 2) + stub_const("#{described_class.name}::SUB_BATCH_SIZE", 2) end it_behaves_like 'back-fill project namespaces' @@ -137,7 +137,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa start_id = ::Project.minimum(:id) end_id = ::Project.maximum(:id) # back-fill first - described_class.new.perform(start_id, end_id, nil, 'up') + described_class.new.perform(start_id, end_id, nil, nil, nil, nil, nil, 'up') end shared_examples 'cleanup project namespaces' do @@ -146,7 +146,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa start_id = ::Project.minimum(:id) end_id = ::Project.maximum(:id) migration = described_class.new - batches_count = (projects_count / described_class::BATCH_SIZE.to_f).ceil + batches_count = (projects_count / described_class::SUB_BATCH_SIZE.to_f).ceil expect(projects_count).to be > 0 expect(projects_count).to eq(::Namespace.where(type: 'Project').count) @@ -154,7 +154,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa expect(migration).to receive(:nullify_project_namespaces_in_projects).exactly(batches_count).and_call_original expect(migration).to receive(:delete_project_namespace_records).exactly(batches_count).and_call_original - migration.perform(start_id, end_id, nil, 'down') + migration.perform(start_id, end_id, nil, nil, nil, nil, nil, 'down') expect(::Project.count).to be > 0 expect(::Namespace.where(type: 'Project').count).to eq(0) @@ -168,7 +168,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa start_id = backfilled_namespace_projects.minimum(:id) end_id = backfilled_namespace_projects.maximum(:id) group_projects_count = backfilled_namespace_projects.count - batches_count = (group_projects_count / described_class::BATCH_SIZE.to_f).ceil + batches_count = (group_projects_count / described_class::SUB_BATCH_SIZE.to_f).ceil project_namespaces_in_hierarchy = project_namespaces_in_hierarchy(base_ancestor(backfilled_namespace)) migration = described_class.new @@ -176,7 +176,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa expect(migration).to receive(:nullify_project_namespaces_in_projects).exactly(batches_count).and_call_original expect(migration).to receive(:delete_project_namespace_records).exactly(batches_count).and_call_original - migration.perform(start_id, end_id, backfilled_namespace.id, 'down') + migration.perform(start_id, end_id, nil, nil, nil, nil, backfilled_namespace.id, 'down') expect(::Namespace.where(type: 'Project').count).to be > 0 expect(project_namespaces_in_hierarchy.count).to eq(0) @@ -190,7 +190,7 @@ RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNa context 'cleanup project namespaces in batches' do before do - stub_const("#{described_class.name}::BATCH_SIZE", 2) + stub_const("#{described_class.name}::SUB_BATCH_SIZE", 2) end it_behaves_like 'cleanup project namespaces' diff --git a/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb b/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb index 1f70fbd440c..112ff2c7380 100644 --- a/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb +++ b/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb @@ -25,21 +25,17 @@ RSpec.describe 'cross-database foreign keys' do ci_pending_builds.project_id ci_pipeline_schedules.owner_id ci_pipeline_schedules.project_id - ci_pipelines.merge_request_id ci_pipelines.project_id ci_project_monthly_usages.project_id - ci_refs.project_id ci_resource_groups.project_id ci_runner_namespaces.namespace_id ci_runner_projects.project_id ci_running_builds.project_id - ci_sources_pipelines.project_id ci_sources_projects.source_project_id ci_stages.project_id ci_subscriptions_projects.downstream_project_id ci_subscriptions_projects.upstream_project_id ci_unit_tests.project_id - ci_variables.project_id dast_site_profiles_pipelines.ci_pipeline_id external_pull_requests.project_id vulnerability_feedback.pipeline_id diff --git a/spec/lib/gitlab/subscription_portal_spec.rb b/spec/lib/gitlab/subscription_portal_spec.rb index 627d3bb42c7..fd3654afee0 100644 --- a/spec/lib/gitlab/subscription_portal_spec.rb +++ b/spec/lib/gitlab/subscription_portal_spec.rb @@ -61,7 +61,6 @@ RSpec.describe ::Gitlab::SubscriptionPortal do :subscriptions_more_minutes_url | 'https://customers.staging.gitlab.com/buy_pipeline_minutes' :subscriptions_more_storage_url | 'https://customers.staging.gitlab.com/buy_storage' :subscriptions_manage_url | 'https://customers.staging.gitlab.com/subscriptions' - :subscriptions_plans_url | 'https://about.gitlab.com/pricing/' :subscriptions_instance_review_url | 'https://customers.staging.gitlab.com/instance_review' :subscriptions_gitlab_plans_url | 'https://customers.staging.gitlab.com/gitlab_plans' :edit_account_url | 'https://customers.staging.gitlab.com/customers/edit' diff --git a/spec/migrations/backfill_project_namespaces_for_group_spec.rb b/spec/migrations/backfill_project_namespaces_for_group_spec.rb new file mode 100644 index 00000000000..0d34d19d42a --- /dev/null +++ b/spec/migrations/backfill_project_namespaces_for_group_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe BackfillProjectNamespacesForGroup do + let_it_be(:migration) { described_class::MIGRATION } + + let(:projects) { table(:projects) } + let(:namespaces) { table(:namespaces) } + let(:parent_group1) { namespaces.create!(name: 'parent_group1', path: 'parent_group1', visibility_level: 20, type: 'Group') } + let!(:parent_group1_project) { projects.create!(name: 'parent_group1_project', path: 'parent_group1_project', namespace_id: parent_group1.id, visibility_level: 20) } + + before do + allow(Gitlab).to receive(:com?).and_return(true) + end + + describe '#up' do + before do + stub_const("BackfillProjectNamespacesForGroup::GROUP_ID", parent_group1.id) + end + + it 'schedules background jobs for each batch of namespaces' do + migrate! + + expect(migration).to have_scheduled_batched_migration( + table_name: :projects, + column_name: :id, + job_arguments: [described_class::GROUP_ID, 'up'], + interval: described_class::DELAY_INTERVAL + ) + end + end + + describe '#down' do + it 'deletes all batched migration records' do + migrate! + schema_migrate_down! + + expect(migration).not_to have_scheduled_batched_migration + end + end +end diff --git a/spec/models/ci/namespace_mirror_spec.rb b/spec/models/ci/namespace_mirror_spec.rb index a9d916115fc..38471f15849 100644 --- a/spec/models/ci/namespace_mirror_spec.rb +++ b/spec/models/ci/namespace_mirror_spec.rb @@ -20,10 +20,10 @@ RSpec.describe Ci::NamespaceMirror do end context 'scopes' do - describe '.contains_namespace' do + describe '.by_group_and_descendants' do let_it_be(:another_group) { create(:group) } - subject(:result) { described_class.contains_namespace(group2.id) } + subject(:result) { described_class.by_group_and_descendants(group2.id) } it 'returns groups having group2.id in traversal_ids' do expect(result.pluck(:namespace_id)).to contain_exactly(group2.id, group3.id, group4.id) diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 90f56c1e0a4..8c0f843fdea 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -390,20 +390,63 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#merge_request?' do - let(:pipeline) { create(:ci_pipeline, merge_request: merge_request) } - let(:merge_request) { create(:merge_request) } + let_it_be(:merge_request) { create(:merge_request) } + let_it_be_with_reload(:pipeline) do + create(:ci_pipeline, project: project, merge_request_id: merge_request.id) + end + + it { expect(pipeline).to be_merge_request } - it 'returns true' do - expect(pipeline).to be_merge_request + context 'when merge request is already loaded' do + it 'does not reload the record and returns true' do + expect(pipeline.merge_request).to be_present + + expect { pipeline.merge_request? }.not_to exceed_query_limit(0) + expect(pipeline).to be_merge_request + end end - context 'when merge request is nil' do - let(:merge_request) { nil } + context 'when merge request is not loaded' do + it 'executes a database query and returns true' do + expect(pipeline).to be_present - it 'returns false' do + expect { pipeline.merge_request? }.not_to exceed_query_limit(1) + expect(pipeline).to be_merge_request + end + + it 'caches the result' do + expect(pipeline).to be_merge_request + expect { pipeline.merge_request? }.not_to exceed_query_limit(0) + end + end + + context 'when merge request was removed' do + before do + pipeline.update!(merge_request_id: non_existing_record_id) + end + + it 'executes a database query and returns false' do + expect { pipeline.merge_request? }.not_to exceed_query_limit(1) expect(pipeline).not_to be_merge_request end end + + context 'when merge request id is not present' do + before do + pipeline.update!(merge_request_id: nil) + end + + it { expect(pipeline).not_to be_merge_request } + end + + context 'when the feature flag is disabled' do + before do + stub_feature_flags(ci_pipeline_merge_request_presence_check: false) + pipeline.update!(merge_request_id: non_existing_record_id) + end + + it { expect(pipeline).to be_merge_request } + end end describe '#detached_merge_request_pipeline?' do @@ -4656,9 +4699,11 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do let(:factory_name) { :ci_pipeline } end - it_behaves_like 'cleanup by a loose foreign key' do - let!(:model) { create(:ci_pipeline, user: create(:user)) } - let!(:parent) { model.user } + context 'loose foreign key on ci_pipelines.user_id' do + it_behaves_like 'cleanup by a loose foreign key' do + let!(:model) { create(:ci_pipeline, user: create(:user)) } + let!(:parent) { model.user } + end end describe 'tags count' do @@ -4679,4 +4724,11 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do it { expect(pipeline.distinct_tags_count).to eq(3) } end end + + context 'loose foreign key on ci_pipelines.merge_request_id' do + it_behaves_like 'cleanup by a loose foreign key' do + let!(:parent) { create(:merge_request) } + let!(:model) { create(:ci_pipeline, merge_request: parent) } + end + end end diff --git a/spec/models/ci/ref_spec.rb b/spec/models/ci/ref_spec.rb index 0a9cd5ef2ec..ffbda4b459f 100644 --- a/spec/models/ci/ref_spec.rb +++ b/spec/models/ci/ref_spec.rb @@ -231,4 +231,11 @@ RSpec.describe Ci::Ref do it_behaves_like 'no-op' end end + + context 'loose foreign key on ci_refs.project_id' do + it_behaves_like 'cleanup by a loose foreign key' do + let!(:parent) { create(:project) } + let!(:model) { create(:ci_ref, project: parent) } + end + end end diff --git a/spec/models/ci/sources/pipeline_spec.rb b/spec/models/ci/sources/pipeline_spec.rb index 97fc8d4a56d..73f7cfa739f 100644 --- a/spec/models/ci/sources/pipeline_spec.rb +++ b/spec/models/ci/sources/pipeline_spec.rb @@ -24,4 +24,11 @@ RSpec.describe Ci::Sources::Pipeline do let!(:model) { create(:ci_sources_pipeline, source_project: parent) } end end + + context 'loose foreign key on ci_sources_pipelines.project_id' do + it_behaves_like 'cleanup by a loose foreign key' do + let!(:parent) { create(:project) } + let!(:model) { create(:ci_sources_pipeline, project: parent) } + end + end end diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb index 93a24ba9157..29ca088ee04 100644 --- a/spec/models/ci/variable_spec.rb +++ b/spec/models/ci/variable_spec.rb @@ -44,4 +44,11 @@ RSpec.describe Ci::Variable do end end end + + context 'loose foreign key on ci_variables.project_id' do + it_behaves_like 'cleanup by a loose foreign key' do + let!(:parent) { create(:project) } + let!(:model) { create(:ci_variable, project: parent) } + end + end end diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb index eb1f84e3ef9..647d64002ef 100644 --- a/spec/requests/api/graphql/ci/runner_spec.rb +++ b/spec/requests/api/graphql/ci/runner_spec.rb @@ -68,6 +68,7 @@ RSpec.describe 'Query.runner(id)' do 'revision' => runner.revision, 'locked' => false, 'active' => runner.active, + 'paused' => !runner.active, 'status' => runner.status('14.5').to_s.upcase, 'maximumTimeout' => runner.maximum_timeout, 'accessLevel' => runner.access_level.to_s.upcase, diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb index 81cab973b30..1a3dde78580 100644 --- a/spec/services/groups/create_service_spec.rb +++ b/spec/services/groups/create_service_spec.rb @@ -289,4 +289,33 @@ RSpec.describe Groups::CreateService, '#execute' do end end end + + describe 'logged_out_marketing_header experiment', :experiment do + let(:service) { described_class.new(user, group_params) } + + subject { service.execute } + + before do + stub_experiments(logged_out_marketing_header: :candidate) + end + + it 'tracks signed_up event' do + expect(experiment(:logged_out_marketing_header)).to track( + :namespace_created, + namespace: an_instance_of(Group) + ).on_next_instance.with_context(actor: user) + + subject + end + + context 'when group has not been persisted' do + let(:service) { described_class.new(user, group_params.merge(name: '<script>alert("Attack!")</script>')) } + + it 'does not track signed_up event' do + expect(experiment(:logged_out_marketing_header)).not_to track(:namespace_created) + + subject + end + end + end end |