diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-15 09:11:06 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-15 09:11:06 +0000 |
commit | 5f431529c8eb0fd5c84df1e66a38ee88b8da1ba6 (patch) | |
tree | ae43e505bc222b2ea53ee7bfe7f81748ba944038 | |
parent | defacc074a4a576e15021ba264de745af982b45d (diff) | |
download | gitlab-ce-5f431529c8eb0fd5c84df1e66a38ee88b8da1ba6.tar.gz |
Add latest changes from gitlab-org/gitlab@master
67 files changed, 810 insertions, 253 deletions
diff --git a/.rubocop_todo/layout/hash_alignment.yml b/.rubocop_todo/layout/hash_alignment.yml index b0855f4dd12..c95fa7d7267 100644 --- a/.rubocop_todo/layout/hash_alignment.yml +++ b/.rubocop_todo/layout/hash_alignment.yml @@ -2,27 +2,6 @@ # Cop supports --auto-correct. Layout/HashAlignment: Exclude: - - 'ee/app/graphql/types/ci/namespace_ci_cd_setting_type.rb' - - 'ee/app/graphql/types/compliance_management/merge_requests/compliance_violation_type.rb' - - 'ee/app/graphql/types/dast/profile_schedule_type.rb' - - 'ee/app/graphql/types/dast/profile_type.rb' - - 'ee/app/graphql/types/dast_scanner_profile_type.rb' - - 'ee/app/graphql/types/dast_site_profile_type.rb' - - 'ee/app/graphql/types/dast_site_validation_type.rb' - - 'ee/app/graphql/types/dora_type.rb' - - 'ee/app/graphql/types/epic_issue_type.rb' - - 'ee/app/graphql/types/epic_type.rb' - - 'ee/app/graphql/types/external_issue_type.rb' - - 'ee/app/graphql/types/iteration_type.rb' - - 'ee/app/graphql/types/iterations/cadence_type.rb' - - 'ee/app/graphql/types/merge_requests/approval_state_type.rb' - - 'ee/app/graphql/types/namespaces/namespace_ban_type.rb' - - 'ee/app/graphql/types/requirements_management/requirement_type.rb' - - 'ee/app/graphql/types/requirements_management/test_report_type.rb' - - 'ee/app/graphql/types/security/training_type.rb' - - 'ee/app/graphql/types/time_report_stats_type.rb' - - 'ee/app/graphql/types/timebox_report_interface.rb' - - 'ee/app/graphql/types/timebox_report_type.rb' - 'ee/app/graphql/types/vulnerabilities/asset_type.rb' - 'ee/app/graphql/types/vulnerabilities/container_image_type.rb' - 'ee/app/graphql/types/vulnerabilities/link_type.rb' @@ -345,7 +345,7 @@ gem 'prometheus-client-mmap', '~> 0.16', require: 'prometheus/client' gem 'warning', '~> 1.3.0' group :development do - gem 'lefthook', '~> 1.0.0', require: false + gem 'lefthook', '~> 1.1.0', require: false gem 'rubocop' gem 'solargraph', '~> 0.45.0', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 3b8bdd23647..5359aabb30b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -757,7 +757,7 @@ GEM rest-client (~> 2.0) launchy (2.5.0) addressable (~> 2.7) - lefthook (1.0.2) + lefthook (1.1.0) letter_opener (1.7.0) launchy (~> 2.2) letter_opener_web (2.0.0) @@ -1628,7 +1628,7 @@ DEPENDENCIES knapsack (~> 1.21.1) kramdown (~> 2.3.1) kubeclient (~> 4.9.3) - lefthook (~> 1.0.0) + lefthook (~> 1.1.0) letter_opener_web (~> 2.0.0) license_finder (~> 7.0) licensee (~> 9.14.1) diff --git a/app/graphql/mutations/merge_requests/set_reviewers.rb b/app/graphql/mutations/merge_requests/set_reviewers.rb new file mode 100644 index 00000000000..8d3f8601597 --- /dev/null +++ b/app/graphql/mutations/merge_requests/set_reviewers.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Mutations + module MergeRequests + class SetReviewers < Base + graphql_name 'MergeRequestSetReviewers' + + argument :reviewer_usernames, + [GraphQL::Types::String], + required: true, + description: 'Usernames of reviewers to assign. Replaces existing reviewers by default.' + + argument :operation_mode, + Types::MutationOperationModeEnum, + required: false, + default_value: Types::MutationOperationModeEnum.default_mode, + description: 'Operation to perform. Defaults to REPLACE.' + + def resolve(project_path:, iid:, reviewer_usernames:, operation_mode:) + resource = authorized_find!(project_path: project_path, iid: iid) + + ::MergeRequests::UpdateReviewersService.new( + project: resource.project, + current_user: current_user, + params: { reviewer_ids: reviewer_ids(resource, reviewer_usernames, operation_mode) } + ).execute(resource) + + { + resource.class.name.underscore.to_sym => resource, + errors: errors_on_object(resource) + } + end + + private + + def reviewer_ids(resource, usernames, mode) + new_reviewers = UsersFinder.new(current_user, username: usernames).execute.to_a + new_reviewer_ids = user_ids(new_reviewers) + + case mode + when 'REPLACE' then new_reviewer_ids + when 'APPEND' then user_ids(resource.reviewers) | new_reviewer_ids + when 'REMOVE' then user_ids(resource.reviewers) - new_reviewer_ids + end + end + + def user_ids(users) + users.map(&:id) + end + end + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 47462b5e9f7..499c2e786bf 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -72,6 +72,7 @@ module Types mount_mutation Mutations::MergeRequests::SetSubscription mount_mutation Mutations::MergeRequests::SetDraft, calls_gitaly: true mount_mutation Mutations::MergeRequests::SetAssignees + mount_mutation Mutations::MergeRequests::SetReviewers mount_mutation Mutations::MergeRequests::ReviewerRereview mount_mutation Mutations::Metrics::Dashboard::Annotations::Create mount_mutation Mutations::Metrics::Dashboard::Annotations::Delete diff --git a/app/models/repository.rb b/app/models/repository.rb index 234a3a83320..eb8e45877f3 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -337,11 +337,17 @@ class Repository def expire_branches_cache expire_method_caches(%i(branch_names merged_branch_names branch_count has_visible_content? has_ambiguous_refs?)) + expire_protected_branches_cache + @local_branches = nil @branch_exists_memo = nil @branch_names_include = nil end + def expire_protected_branches_cache + ProtectedBranches::CacheService.new(project).refresh if project # rubocop:disable CodeReuse/ServiceClass + end + def expire_statistics_caches expire_method_caches(%i(size commit_count)) end diff --git a/app/services/merge_requests/update_assignees_service.rb b/app/services/merge_requests/update_assignees_service.rb index 5b23f69ac4a..a6b0235c525 100644 --- a/app/services/merge_requests/update_assignees_service.rb +++ b/app/services/merge_requests/update_assignees_service.rb @@ -11,7 +11,7 @@ module MergeRequests old_assignees = merge_request.assignees.to_a old_ids = old_assignees.map(&:id) - new_ids = new_assignee_ids(merge_request) + new_ids = new_user_ids(merge_request, update_attrs[:assignee_ids], :assignees) return merge_request if merge_request.errors.any? return merge_request if new_ids.size != update_attrs[:assignee_ids].size @@ -32,27 +32,8 @@ module MergeRequests private - def new_assignee_ids(merge_request) - # prime the cache - prevent N+1 lookup during authorization loop. - user_ids = update_attrs[:assignee_ids] - return [] if user_ids.empty? - - merge_request.project.team.max_member_access_for_user_ids(user_ids) - User.id_in(user_ids).map do |user| - if user.can?(:read_merge_request, merge_request) - user.id - else - merge_request.errors.add( - :assignees, - "Cannot assign #{user.to_reference} to #{merge_request.to_reference}" - ) - nil - end - end.compact - end - def assignee_ids - params.fetch(:assignee_ids).reject { _1 == 0 }.first(1) + filter_sentinel_values(params.fetch(:assignee_ids)).first(1) end def params diff --git a/app/services/merge_requests/update_reviewers_service.rb b/app/services/merge_requests/update_reviewers_service.rb new file mode 100644 index 00000000000..8e974d75676 --- /dev/null +++ b/app/services/merge_requests/update_reviewers_service.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module MergeRequests + class UpdateReviewersService < UpdateService + def execute(merge_request) + return merge_request unless current_user&.can?(:update_merge_request, merge_request) + + old_reviewers = merge_request.reviewers.to_a + old_ids = old_reviewers.map(&:id) + new_ids = new_user_ids(merge_request, update_attrs[:reviewer_ids], :reviewers) + + return merge_request if merge_request.errors.any? + return merge_request if new_ids.size != update_attrs[:reviewer_ids].size + return merge_request if old_ids.to_set == new_ids.to_set # no-change + + merge_request.update!(update_attrs.merge(reviewer_ids: new_ids)) + handle_reviewers_change(merge_request, old_reviewers) + resolve_todos_for(merge_request) + execute_reviewers_hooks(merge_request, old_reviewers) + + merge_request + end + + private + + def reviewer_ids + filter_sentinel_values(params.fetch(:reviewer_ids)).first(1) + end + + def update_attrs + @attrs ||= { updated_by: current_user, reviewer_ids: reviewer_ids } + end + + def execute_reviewers_hooks(merge_request, old_reviewers) + execute_hooks( + merge_request, + 'update', + old_associations: { reviewers: old_reviewers } + ) + end + end +end + +MergeRequests::UpdateReviewersService.prepend_mod diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 603da4ef535..0902b5195a1 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -155,11 +155,7 @@ module MergeRequests def resolve_todos(merge_request, old_labels, old_assignees, old_reviewers) return unless has_changes?(merge_request, old_labels: old_labels, old_assignees: old_assignees, old_reviewers: old_reviewers) - service_user = current_user - - merge_request.run_after_commit_or_now do - ::MergeRequests::ResolveTodosService.new(merge_request, service_user).async_execute - end + resolve_todos_for(merge_request) end def handle_target_branch_change(merge_request) @@ -296,6 +292,36 @@ module MergeRequests def add_time_spent_service @add_time_spent_service ||= ::MergeRequests::AddSpentTimeService.new(project: project, current_user: current_user, params: params) end + + def new_user_ids(merge_request, user_ids, attribute) + # prime the cache - prevent N+1 lookup during authorization loop. + return [] if user_ids.empty? + + merge_request.project.team.max_member_access_for_user_ids(user_ids) + User.id_in(user_ids).map do |user| + if user.can?(:read_merge_request, merge_request) + user.id + else + merge_request.errors.add( + attribute, + "Cannot assign #{user.to_reference} to #{merge_request.to_reference}" + ) + nil + end + end.compact + end + + def resolve_todos_for(merge_request) + service_user = current_user + + merge_request.run_after_commit_or_now do + ::MergeRequests::ResolveTodosService.new(merge_request, service_user).async_execute + end + end + + def filter_sentinel_values(param) + param.reject { _1 == 0 } + end end end diff --git a/config/feature_categories.yml b/config/feature_categories.yml index d52f621e436..9b5f3de3f75 100644 --- a/config/feature_categories.yml +++ b/config/feature_categories.yml @@ -8,13 +8,16 @@ # --- - advanced_deployments +- advisory_database - api_security +- application_performance - attack_emulation - audit_events - audit_reports - authentication_and_authorization - auto_devops - backup_restore +- billing_and_subscription_management - build - build_artifacts - chatops @@ -65,6 +68,7 @@ - importers - incident_management - infrastructure_as_code +- infrastructure_cost_data - insider_threat - instance_resiliency - integrations @@ -75,7 +79,6 @@ - kubernetes_management - license_compliance - logging -- memory - merge_trains - metrics - mlops @@ -96,6 +99,7 @@ - product_analytics - projects - provision +- pubsec_services - purchase - quality_management - redis @@ -118,7 +122,6 @@ - snippets - source_code_management - static_application_security_testing -- static_site_editor - subgroups - system_access - team_planning @@ -127,7 +130,6 @@ - users - utilization - value_stream_management -- vulnerability_database - vulnerability_management - web_ide - wiki diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index f3b95824048..8305927e0e8 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -3755,6 +3755,28 @@ Input type: `MergeRequestSetMilestoneInput` | <a id="mutationmergerequestsetmilestoneerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationmergerequestsetmilestonemergerequest"></a>`mergeRequest` | [`MergeRequest`](#mergerequest) | Merge request after mutation. | +### `Mutation.mergeRequestSetReviewers` + +Input type: `MergeRequestSetReviewersInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationmergerequestsetreviewersclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationmergerequestsetreviewersiid"></a>`iid` | [`String!`](#string) | IID of the merge request to mutate. | +| <a id="mutationmergerequestsetreviewersoperationmode"></a>`operationMode` | [`MutationOperationMode`](#mutationoperationmode) | Operation to perform. Defaults to REPLACE. | +| <a id="mutationmergerequestsetreviewersprojectpath"></a>`projectPath` | [`ID!`](#id) | Project the merge request to mutate is in. | +| <a id="mutationmergerequestsetreviewersreviewerusernames"></a>`reviewerUsernames` | [`[String!]!`](#string) | Usernames of reviewers to assign. Replaces existing reviewers by default. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationmergerequestsetreviewersclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationmergerequestsetreviewerserrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +| <a id="mutationmergerequestsetreviewersmergerequest"></a>`mergeRequest` | [`MergeRequest`](#mergerequest) | Merge request after mutation. | + ### `Mutation.mergeRequestSetSubscription` Input type: `MergeRequestSetSubscriptionInput` diff --git a/doc/ci/pipelines/img/pipeline_mini_graph_v15_0.png b/doc/ci/pipelines/img/pipeline_mini_graph_v15_0.png Binary files differnew file mode 100644 index 00000000000..48a0ca9d84f --- /dev/null +++ b/doc/ci/pipelines/img/pipeline_mini_graph_v15_0.png diff --git a/doc/ci/pipelines/multi_project_pipelines.md b/doc/ci/pipelines/multi_project_pipelines.md index 512501f72ba..93034aa233f 100644 --- a/doc/ci/pipelines/multi_project_pipelines.md +++ b/doc/ci/pipelines/multi_project_pipelines.md @@ -395,11 +395,16 @@ downstream projects. On self-managed instances, an administrator can change this ## Multi-project pipeline visualization **(PREMIUM)** -When you configure GitLab CI/CD for your project, you can visualize the stages of your -[jobs](index.md#configure-a-pipeline) on a [pipeline graph](index.md#visualize-pipelines). +When your pipeline triggers a downstream pipeline, the downstream pipeline displays +to the right of the [pipeline graph](index.md#visualize-pipelines). ![Multi-project pipeline graph](img/multi_project_pipeline_graph_v14_3.png) +In [pipeline mini graphs](index.md#pipeline-mini-graphs), the downstream pipeline +displays to the right of the mini graph. + +![Multi-project pipeline mini graph](img/pipeline_mini_graph_v15_0.png) + ## Retry or cancel multi-project pipelines If you have permission to trigger pipelines in the downstream project, you can diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md index d800ce0a1fd..0b36b9b2f2f 100644 --- a/doc/development/api_graphql_styleguide.md +++ b/doc/development/api_graphql_styleguide.md @@ -706,23 +706,21 @@ aware of the support. The documentation will mention that the old Global ID style is now deprecated. -## Mark schema items as alpha +## Mark schema items as Alpha -You can mark fields, arguments, enum values, and mutations as -[alpha](https://about.gitlab.com/handbook/product/gitlab-the-product/#alpha-beta-ga). +You can mark GraphQL schema items (fields, arguments, enum values, and mutations) as +[Alpha](https://about.gitlab.com/handbook/product/gitlab-the-product/#alpha-beta-ga). -An item marked as alpha is exempt from the deprecation process and can be removed -at any time without notice. This way, you can add an item that might be -subject to change and is not ready for public use. +An item marked as Alpha is [exempt from the deprecation process](#breaking-change-exemptions) and can be removed +at any time without notice. Mark an item as Alpha when it is +subject to change and not ready for public use. -You can only mark a new item as alpha. This item then appears as deprecated -in our generated docs and its GraphQL description. You cannot mark an existing item -as alpha because it's already public. - -Like all deprecated schema items, you can test an `alpha` field in [GraphiQL](../api/graphql/index.md#graphiql). However, be aware that the GraphiQL autocomplete editor doesn't suggest deprecated fields. +NOTE: +Only mark new items as Alpha. Never mark existing items +as Alpha because they're already public. -To mark a schema item as alpha, use the `alpha:` keyword. -You must provide the `milestone:` that introduced the alpha item. +To mark a schema item as Alpha, use the `alpha:` keyword. +You must provide the `milestone:` that introduced the Alpha item. For example: @@ -732,6 +730,13 @@ field :token, GraphQL::Types::String, null: true, description: 'Token for login.' ``` +Alpha GraphQL items is a custom GitLab feature that leverages GraphQL deprecations. An Alpha item +appears as deprecated in the GraphQL schema. Like all deprecated schema items, you can test an +Alpha field in [GraphiQL](../api/graphql/index.md#graphiql). However, be aware that the GraphiQL +autocomplete editor doesn't suggest deprecated fields. + +The item shows as Alpha in our generated GraphQL documentation and its GraphQL schema description. + ## Enums GitLab GraphQL enums are defined in `app/graphql/types`. When defining new enums, the diff --git a/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js b/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js index 4a0242b4a46..e7e627b669e 100644 --- a/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js +++ b/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js @@ -39,7 +39,7 @@ describe('Configure Feature Flags Modal', () => { const findSecondaryAction = () => findGlModal().props('actionSecondary'); const findProjectNameInput = () => wrapper.find('#project_name_verification'); const findDangerGlAlert = () => - wrapper.findAll(GlAlert).filter((c) => c.props('variant') === 'danger'); + wrapper.findAllComponents(GlAlert).filter((c) => c.props('variant') === 'danger'); describe('idle', () => { afterEach(() => wrapper.destroy()); diff --git a/spec/frontend/feature_flags/components/empty_state_spec.js b/spec/frontend/feature_flags/components/empty_state_spec.js index 4ac82ae44a6..e3cc6f703c4 100644 --- a/spec/frontend/feature_flags/components/empty_state_spec.js +++ b/spec/frontend/feature_flags/components/empty_state_spec.js @@ -57,7 +57,7 @@ describe('feature_flags/components/feature_flags_tab.vue', () => { beforeEach(() => { wrapper = factory(); - alerts = wrapper.findAll(GlAlert); + alerts = wrapper.findAllComponents(GlAlert); }); it('should show any alerts', () => { @@ -68,7 +68,7 @@ describe('feature_flags/components/feature_flags_tab.vue', () => { it('should emit a dismiss event for a dismissed alert', () => { alerts.at(0).vm.$emit('dismiss'); - expect(wrapper.find(EmptyState).emitted('dismissAlert')).toEqual([[0]]); + expect(wrapper.findComponent(EmptyState).emitted('dismissAlert')).toEqual([[0]]); }); }); @@ -78,8 +78,8 @@ describe('feature_flags/components/feature_flags_tab.vue', () => { }); it('should show a loading icon and nothing else', () => { - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); - expect(wrapper.findAll(GlEmptyState)).toHaveLength(0); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findAllComponents(GlEmptyState)).toHaveLength(0); }); }); @@ -88,7 +88,7 @@ describe('feature_flags/components/feature_flags_tab.vue', () => { beforeEach(() => { wrapper = factory({ errorState: true }); - emptyState = wrapper.find(GlEmptyState); + emptyState = wrapper.findComponent(GlEmptyState); }); it('should show an error state if there has been an error', () => { @@ -106,8 +106,8 @@ describe('feature_flags/components/feature_flags_tab.vue', () => { beforeEach(() => { wrapper = factory({ emptyState: true }); - emptyState = wrapper.find(GlEmptyState); - emptyStateLink = emptyState.find(GlLink); + emptyState = wrapper.findComponent(GlEmptyState); + emptyStateLink = emptyState.findComponent(GlLink); }); it('should show an empty state if it is empty', () => { diff --git a/spec/frontend/feature_flags/components/environments_dropdown_spec.js b/spec/frontend/feature_flags/components/environments_dropdown_spec.js index cca472012e9..e8103df78bc 100644 --- a/spec/frontend/feature_flags/components/environments_dropdown_spec.js +++ b/spec/frontend/feature_flags/components/environments_dropdown_spec.js @@ -23,7 +23,7 @@ describe('Feature flags > Environments dropdown ', () => { }); }; - const findEnvironmentSearchInput = () => wrapper.find(GlSearchBoxByType); + const findEnvironmentSearchInput = () => wrapper.findComponent(GlSearchBoxByType); const findDropdownMenu = () => wrapper.find('.dropdown-menu'); afterEach(() => { @@ -91,7 +91,7 @@ describe('Feature flags > Environments dropdown ', () => { describe('with received data', () => { it('sets is loading to false', () => { expect(wrapper.vm.isLoading).toBe(false); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); }); it('shows the suggestions', () => { @@ -100,7 +100,7 @@ describe('Feature flags > Environments dropdown ', () => { it('emits event when a suggestion is clicked', async () => { const button = wrapper - .findAll(GlButton) + .findAllComponents(GlButton) .filter((b) => b.text() === 'production') .at(0); button.vm.$emit('click'); @@ -111,7 +111,7 @@ describe('Feature flags > Environments dropdown ', () => { describe('on click clear button', () => { beforeEach(async () => { - wrapper.find(GlButton).vm.$emit('click'); + wrapper.findComponent(GlButton).vm.$emit('click'); await nextTick(); }); @@ -137,7 +137,7 @@ describe('Feature flags > Environments dropdown ', () => { }); it('emits create event', async () => { - wrapper.findAll(GlButton).at(0).vm.$emit('click'); + wrapper.findAllComponents(GlButton).at(0).vm.$emit('click'); await nextTick(); expect(wrapper.emitted('createClicked')).toEqual([['production']]); }); diff --git a/spec/frontend/feature_flags/components/feature_flags_table_spec.js b/spec/frontend/feature_flags/components/feature_flags_table_spec.js index 99864a95f59..47f12f70056 100644 --- a/spec/frontend/feature_flags/components/feature_flags_table_spec.js +++ b/spec/frontend/feature_flags/components/feature_flags_table_spec.js @@ -119,7 +119,7 @@ describe('Feature flag table', () => { it('should render an environments specs badge with active class', () => { const envColumn = wrapper.find('.js-feature-flag-environments'); - expect(trimText(envColumn.find(GlBadge).text())).toBe('All Users: All Environments'); + expect(trimText(envColumn.findComponent(GlBadge).text())).toBe('All Users: All Environments'); }); it('should render an actions column', () => { @@ -137,7 +137,7 @@ describe('Feature flag table', () => { beforeEach(() => { props.featureFlags[0].update_path = props.featureFlags[0].destroy_path; createWrapper(props); - toggle = wrapper.find(GlToggle); + toggle = wrapper.findComponent(GlToggle); spy = mockTracking('_category_', toggle.element, jest.spyOn); }); diff --git a/spec/frontend/feature_flags/components/form_spec.js b/spec/frontend/feature_flags/components/form_spec.js index 3ad1225906b..7dd7c709c94 100644 --- a/spec/frontend/feature_flags/components/form_spec.js +++ b/spec/frontend/feature_flags/components/form_spec.js @@ -61,7 +61,7 @@ describe('feature flag form', () => { it('does not render the related issues widget without the featureFlagIssuesEndpoint', () => { factory(requiredProps); - expect(wrapper.find(RelatedIssuesRoot).exists()).toBe(false); + expect(wrapper.findComponent(RelatedIssuesRoot).exists()).toBe(false); }); it('renders the related issues widget when the featureFlagIssuesEndpoint is provided', () => { @@ -73,7 +73,7 @@ describe('feature flag form', () => { }, ); - expect(wrapper.find(RelatedIssuesRoot).exists()).toBe(true); + expect(wrapper.findComponent(RelatedIssuesRoot).exists()).toBe(true); }); describe('without provided data', () => { @@ -114,7 +114,7 @@ describe('feature flag form', () => { }); it('should show the strategy component', () => { - const strategy = wrapper.find(Strategy); + const strategy = wrapper.findComponent(Strategy); expect(strategy.exists()).toBe(true); expect(strategy.props('strategy')).toEqual({ type: ROLLOUT_STRATEGY_PERCENT_ROLLOUT, @@ -124,14 +124,14 @@ describe('feature flag form', () => { }); it('should show one strategy component per strategy', () => { - expect(wrapper.findAll(Strategy)).toHaveLength(2); + expect(wrapper.findAllComponents(Strategy)).toHaveLength(2); }); it('adds an all users strategy when clicking the Add button', async () => { - wrapper.find(GlButton).vm.$emit('click'); + wrapper.findComponent(GlButton).vm.$emit('click'); await nextTick(); - const strategies = wrapper.findAll(Strategy); + const strategies = wrapper.findAllComponents(Strategy); expect(strategies).toHaveLength(3); expect(strategies.at(2).props('strategy')).toEqual(allUsersStrategy); @@ -143,10 +143,10 @@ describe('feature flag form', () => { parameters: { percentage: '30' }, scopes: [], }; - wrapper.find(Strategy).vm.$emit('delete'); + wrapper.findComponent(Strategy).vm.$emit('delete'); await nextTick(); - expect(wrapper.findAll(Strategy)).toHaveLength(1); - expect(wrapper.find(Strategy).props('strategy')).not.toEqual(strategy); + expect(wrapper.findAllComponents(Strategy)).toHaveLength(1); + expect(wrapper.findComponent(Strategy).props('strategy')).not.toEqual(strategy); }); }); }); diff --git a/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js b/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js index 63fa5d19982..1c0c444c296 100644 --- a/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js +++ b/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js @@ -31,17 +31,17 @@ describe('New Environments Dropdown', () => { describe('before results', () => { it('should show a loading icon', () => { axiosMock.onGet(TEST_HOST).reply(() => { - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); - wrapper.find(GlSearchBoxByType).vm.$emit('focus'); + wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus'); return axios.waitForAll(); }); it('should not show any dropdown items', () => { axiosMock.onGet(TEST_HOST).reply(() => { - expect(wrapper.findAll(GlDropdownItem)).toHaveLength(0); + expect(wrapper.findAllComponents(GlDropdownItem)).toHaveLength(0); }); - wrapper.find(GlSearchBoxByType).vm.$emit('focus'); + wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus'); return axios.waitForAll(); }); }); @@ -50,11 +50,11 @@ describe('New Environments Dropdown', () => { let item; beforeEach(async () => { axiosMock.onGet(TEST_HOST).reply(200, []); - wrapper.find(GlSearchBoxByType).vm.$emit('focus'); - wrapper.find(GlSearchBoxByType).vm.$emit('input', TEST_SEARCH); + wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus'); + wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', TEST_SEARCH); await axios.waitForAll(); await nextTick(); - item = wrapper.find(GlDropdownItem); + item = wrapper.findComponent(GlDropdownItem); }); it('should display a Create item label', () => { @@ -62,7 +62,7 @@ describe('New Environments Dropdown', () => { }); it('should display that no matching items are found', () => { - expect(wrapper.find({ ref: 'noResults' }).exists()).toBe(true); + expect(wrapper.findComponent({ ref: 'noResults' }).exists()).toBe(true); }); it('should emit a new scope when selected', () => { @@ -75,10 +75,10 @@ describe('New Environments Dropdown', () => { let items; beforeEach(() => { axiosMock.onGet(TEST_HOST).reply(httpStatusCodes.OK, ['prod', 'production']); - wrapper.find(GlSearchBoxByType).vm.$emit('focus'); - wrapper.find(GlSearchBoxByType).vm.$emit('input', 'prod'); + wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus'); + wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'prod'); return axios.waitForAll().then(() => { - items = wrapper.findAll(GlDropdownItem); + items = wrapper.findAllComponents(GlDropdownItem); }); }); @@ -97,7 +97,7 @@ describe('New Environments Dropdown', () => { }); it('should not display a message about no results', () => { - expect(wrapper.find({ ref: 'noResults' }).exists()).toBe(false); + expect(wrapper.findComponent({ ref: 'noResults' }).exists()).toBe(false); }); }); }); diff --git a/spec/frontend/feature_flags/components/new_feature_flag_spec.js b/spec/frontend/feature_flags/components/new_feature_flag_spec.js index 688ba54f919..300d0e47082 100644 --- a/spec/frontend/feature_flags/components/new_feature_flag_spec.js +++ b/spec/frontend/feature_flags/components/new_feature_flag_spec.js @@ -40,7 +40,7 @@ describe('New feature flag form', () => { }; const findWarningGlAlert = () => - wrapper.findAll(GlAlert).filter((c) => c.props('variant') === 'warning'); + wrapper.findAllComponents(GlAlert).filter((c) => c.props('variant') === 'warning'); beforeEach(() => { factory(); @@ -65,11 +65,11 @@ describe('New feature flag form', () => { }); it('should render feature flag form', () => { - expect(wrapper.find(Form).exists()).toEqual(true); + expect(wrapper.findComponent(Form).exists()).toEqual(true); }); it('has an all users strategy by default', () => { - const strategies = wrapper.find(Form).props('strategies'); + const strategies = wrapper.findComponent(Form).props('strategies'); expect(strategies).toEqual([allUsersStrategy]); }); diff --git a/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js b/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js index 56b14d80ab3..70a9156b5a9 100644 --- a/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js +++ b/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js @@ -34,12 +34,12 @@ describe('feature_flags/components/strategies/flexible_rollout.vue', () => { percentageFormGroup = wrapper .find('[data-testid="strategy-flexible-rollout-percentage"]') - .find(ParameterFormGroup); - percentageInput = percentageFormGroup.find(GlFormInput); + .findComponent(ParameterFormGroup); + percentageInput = percentageFormGroup.findComponent(GlFormInput); stickinessFormGroup = wrapper .find('[data-testid="strategy-flexible-rollout-stickiness"]') - .find(ParameterFormGroup); - stickinessSelect = stickinessFormGroup.find(GlFormSelect); + .findComponent(ParameterFormGroup); + stickinessSelect = stickinessFormGroup.findComponent(GlFormSelect); }); it('displays the current percentage value', () => { @@ -94,7 +94,7 @@ describe('feature_flags/components/strategies/flexible_rollout.vue', () => { it('shows errors', () => { const formGroup = wrapper .find('[data-testid="strategy-flexible-rollout-percentage"]') - .find(ParameterFormGroup); + .findComponent(ParameterFormGroup); expect(formGroup.attributes('state')).toBeUndefined(); }); @@ -108,7 +108,7 @@ describe('feature_flags/components/strategies/flexible_rollout.vue', () => { it('shows errors', () => { const formGroup = wrapper .find('[data-testid="strategy-flexible-rollout-percentage"]') - .find(ParameterFormGroup); + .findComponent(ParameterFormGroup); expect(formGroup.attributes('state')).toBeUndefined(); }); diff --git a/spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js b/spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js index 3b69194494f..96b9434f3ec 100644 --- a/spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js +++ b/spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js @@ -24,10 +24,10 @@ describe('~/feature_flags/components/strategies/gitlab_user_list.vue', () => { propsData: { ...DEFAULT_PROPS, ...props }, }); - const findDropdown = () => wrapper.find(GlDropdown); + const findDropdown = () => wrapper.findComponent(GlDropdown); describe('with user lists', () => { - const findDropdownItem = () => wrapper.find(GlDropdownItem); + const findDropdownItem = () => wrapper.findComponent(GlDropdownItem); beforeEach(() => { Api.searchFeatureFlagUserLists.mockResolvedValue({ data: [userList] }); @@ -69,10 +69,10 @@ describe('~/feature_flags/components/strategies/gitlab_user_list.vue', () => { r = resolve; }), ); - const searchWrapper = wrapper.find(GlSearchBoxByType); + const searchWrapper = wrapper.findComponent(GlSearchBoxByType); searchWrapper.vm.$emit('input', 'new'); await nextTick(); - const loadingIcon = wrapper.find(GlLoadingIcon); + const loadingIcon = wrapper.findComponent(GlLoadingIcon); expect(loadingIcon.exists()).toBe(true); expect(Api.searchFeatureFlagUserLists).toHaveBeenCalledWith('1', 'new'); diff --git a/spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js b/spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js index 33696064d55..23ad0d3a08d 100644 --- a/spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js +++ b/spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js @@ -20,7 +20,7 @@ describe('~/feature_flags/strategies/parameter_form_group.vue', () => { }, }); - formGroup = wrapper.find(GlFormGroup); + formGroup = wrapper.findComponent(GlFormGroup); slot = wrapper.find('[data-testid="slot"]'); }); diff --git a/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js b/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js index 180697e93e4..cb422a018f9 100644 --- a/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js +++ b/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js @@ -30,8 +30,8 @@ describe('~/feature_flags/components/strategies/percent_rollout.vue', () => { beforeEach(() => { wrapper = factory(); - input = wrapper.find(GlFormInput); - formGroup = wrapper.find(ParameterFormGroup); + input = wrapper.findComponent(GlFormInput); + formGroup = wrapper.findComponent(ParameterFormGroup); }); it('displays the current value', () => { @@ -55,8 +55,8 @@ describe('~/feature_flags/components/strategies/percent_rollout.vue', () => { beforeEach(() => { wrapper = factory({ strategy: { parameters: { percentage: '101' } } }); - input = wrapper.find(GlFormInput); - formGroup = wrapper.find(ParameterFormGroup); + input = wrapper.findComponent(GlFormInput); + formGroup = wrapper.findComponent(ParameterFormGroup); }); it('shows errors', () => { @@ -68,8 +68,8 @@ describe('~/feature_flags/components/strategies/percent_rollout.vue', () => { beforeEach(() => { wrapper = factory({ strategy: { parameters: { percentage: '3.14' } } }); - input = wrapper.find(GlFormInput); - formGroup = wrapper.find(ParameterFormGroup); + input = wrapper.findComponent(GlFormInput); + formGroup = wrapper.findComponent(ParameterFormGroup); }); it('shows errors', () => { diff --git a/spec/frontend/feature_flags/components/strategies/users_with_id_spec.js b/spec/frontend/feature_flags/components/strategies/users_with_id_spec.js index 745fbca00fe..0a72714c22a 100644 --- a/spec/frontend/feature_flags/components/strategies/users_with_id_spec.js +++ b/spec/frontend/feature_flags/components/strategies/users_with_id_spec.js @@ -15,7 +15,7 @@ describe('~/feature_flags/components/users_with_id.vue', () => { beforeEach(() => { wrapper = factory(); - textarea = wrapper.find(GlFormTextarea); + textarea = wrapper.findComponent(GlFormTextarea); }); afterEach(() => { diff --git a/spec/frontend/feature_flags/components/strategy_parameters_spec.js b/spec/frontend/feature_flags/components/strategy_parameters_spec.js index 979ca255b08..d0f1f7d0e2a 100644 --- a/spec/frontend/feature_flags/components/strategy_parameters_spec.js +++ b/spec/frontend/feature_flags/components/strategy_parameters_spec.js @@ -51,11 +51,11 @@ describe('~/feature_flags/components/strategy_parameters.vue', () => { }); it('should show the correct component', () => { - expect(wrapper.find(component).exists()).toBe(true); + expect(wrapper.findComponent(component).exists()).toBe(true); }); it('should emit changes from the lower component', () => { - const strategyParameterWrapper = wrapper.find(component); + const strategyParameterWrapper = wrapper.findComponent(component); strategyParameterWrapper.vm.$emit('change', { parameters: { foo: 'bar' } }); @@ -77,7 +77,7 @@ describe('~/feature_flags/components/strategy_parameters.vue', () => { strategy, }); - expect(wrapper.find(UsersWithId).props('strategy')).toEqual(strategy); + expect(wrapper.findComponent(UsersWithId).props('strategy')).toEqual(strategy); }); }); }); diff --git a/spec/frontend/feature_flags/components/strategy_spec.js b/spec/frontend/feature_flags/components/strategy_spec.js index aee3873721c..84d4180fe63 100644 --- a/spec/frontend/feature_flags/components/strategy_spec.js +++ b/spec/frontend/feature_flags/components/strategy_spec.js @@ -32,8 +32,8 @@ Vue.use(Vuex); describe('Feature flags strategy', () => { let wrapper; - const findStrategyParameters = () => wrapper.find(StrategyParameters); - const findDocsLinks = () => wrapper.findAll(GlLink); + const findStrategyParameters = () => wrapper.findComponent(StrategyParameters); + const findDocsLinks = () => wrapper.findAllComponents(GlLink); const factory = ( opts = { @@ -93,7 +93,7 @@ describe('Feature flags strategy', () => { }); it('should set the select to match the strategy name', () => { - expect(wrapper.find(GlFormSelect).element.value).toBe(name); + expect(wrapper.findComponent(GlFormSelect).element.value).toBe(name); }); it('should emit a change if the parameters component does', () => { @@ -118,7 +118,7 @@ describe('Feature flags strategy', () => { }); it('shows an alert asking users to consider using flexibleRollout instead', () => { - expect(wrapper.find(GlAlert).text()).toContain( + expect(wrapper.findComponent(GlAlert).text()).toContain( 'Consider using the more flexible "Percent rollout" strategy instead.', ); }); @@ -139,10 +139,10 @@ describe('Feature flags strategy', () => { }); it('should revert to all-environments scope when last scope is removed', async () => { - const token = wrapper.find(GlToken); + const token = wrapper.findComponent(GlToken); token.vm.$emit('close'); await nextTick(); - expect(wrapper.findAll(GlToken)).toHaveLength(0); + expect(wrapper.findAllComponents(GlToken)).toHaveLength(0); expect(last(wrapper.emitted('change'))).toEqual([ { name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT, @@ -167,7 +167,7 @@ describe('Feature flags strategy', () => { }); it('should change the parameters if a different strategy is chosen', async () => { - const select = wrapper.find(GlFormSelect); + const select = wrapper.findComponent(GlFormSelect); select.setValue(ROLLOUT_STRATEGY_ALL_USERS); await nextTick(); expect(last(wrapper.emitted('change'))).toEqual([ @@ -180,26 +180,26 @@ describe('Feature flags strategy', () => { }); it('should display selected scopes', async () => { - const dropdown = wrapper.find(NewEnvironmentsDropdown); + const dropdown = wrapper.findComponent(NewEnvironmentsDropdown); dropdown.vm.$emit('add', 'production'); await nextTick(); - expect(wrapper.findAll(GlToken)).toHaveLength(1); - expect(wrapper.find(GlToken).text()).toBe('production'); + expect(wrapper.findAllComponents(GlToken)).toHaveLength(1); + expect(wrapper.findComponent(GlToken).text()).toBe('production'); }); it('should display all selected scopes', async () => { - const dropdown = wrapper.find(NewEnvironmentsDropdown); + const dropdown = wrapper.findComponent(NewEnvironmentsDropdown); dropdown.vm.$emit('add', 'production'); dropdown.vm.$emit('add', 'staging'); await nextTick(); - const tokens = wrapper.findAll(GlToken); + const tokens = wrapper.findAllComponents(GlToken); expect(tokens).toHaveLength(2); expect(tokens.at(0).text()).toBe('production'); expect(tokens.at(1).text()).toBe('staging'); }); it('should emit selected scopes', async () => { - const dropdown = wrapper.find(NewEnvironmentsDropdown); + const dropdown = wrapper.findComponent(NewEnvironmentsDropdown); dropdown.vm.$emit('add', 'production'); await nextTick(); expect(last(wrapper.emitted('change'))).toEqual([ @@ -215,7 +215,7 @@ describe('Feature flags strategy', () => { }); it('should emit a delete if the delete button is clicked', () => { - wrapper.find(GlButton).vm.$emit('click'); + wrapper.findComponent(GlButton).vm.$emit('click'); expect(wrapper.emitted('delete')).toEqual([[]]); }); }); @@ -232,26 +232,26 @@ describe('Feature flags strategy', () => { }); it('should display selected scopes', async () => { - const dropdown = wrapper.find(NewEnvironmentsDropdown); + const dropdown = wrapper.findComponent(NewEnvironmentsDropdown); dropdown.vm.$emit('add', 'production'); await nextTick(); - expect(wrapper.findAll(GlToken)).toHaveLength(1); - expect(wrapper.find(GlToken).text()).toBe('production'); + expect(wrapper.findAllComponents(GlToken)).toHaveLength(1); + expect(wrapper.findComponent(GlToken).text()).toBe('production'); }); it('should display all selected scopes', async () => { - const dropdown = wrapper.find(NewEnvironmentsDropdown); + const dropdown = wrapper.findComponent(NewEnvironmentsDropdown); dropdown.vm.$emit('add', 'production'); dropdown.vm.$emit('add', 'staging'); await nextTick(); - const tokens = wrapper.findAll(GlToken); + const tokens = wrapper.findAllComponents(GlToken); expect(tokens).toHaveLength(2); expect(tokens.at(0).text()).toBe('production'); expect(tokens.at(1).text()).toBe('staging'); }); it('should emit selected scopes', async () => { - const dropdown = wrapper.find(NewEnvironmentsDropdown); + const dropdown = wrapper.findComponent(NewEnvironmentsDropdown); dropdown.vm.$emit('add', 'production'); await nextTick(); expect(last(wrapper.emitted('change'))).toEqual([ diff --git a/spec/frontend/ide/components/branches/item_spec.js b/spec/frontend/ide/components/branches/item_spec.js index 271d0600e16..3dbd1210916 100644 --- a/spec/frontend/ide/components/branches/item_spec.js +++ b/spec/frontend/ide/components/branches/item_spec.js @@ -44,8 +44,8 @@ describe('IDE branch item', () => { }); it('renders branch name and timeago', () => { expect(wrapper.text()).toContain(TEST_BRANCH.name); - expect(wrapper.find(Timeago).props('time')).toBe(TEST_BRANCH.committedDate); - expect(wrapper.find(GlIcon).exists()).toBe(false); + expect(wrapper.findComponent(Timeago).props('time')).toBe(TEST_BRANCH.committedDate); + expect(wrapper.findComponent(GlIcon).exists()).toBe(false); }); it('renders link to branch', () => { @@ -60,6 +60,6 @@ describe('IDE branch item', () => { it('renders icon if is not active', () => { createComponent({ isActive: true }); - expect(wrapper.find(GlIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlIcon).exists()).toBe(true); }); }); diff --git a/spec/frontend/ide/components/branches/search_list_spec.js b/spec/frontend/ide/components/branches/search_list_spec.js index b6e3274153a..bbde45d700f 100644 --- a/spec/frontend/ide/components/branches/search_list_spec.js +++ b/spec/frontend/ide/components/branches/search_list_spec.js @@ -47,7 +47,7 @@ describe('IDE branches search list', () => { it('renders loading icon when `isLoading` is true', () => { createComponent({ isLoading: true }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('renders branches not found when search is not empty and branches list is empty', async () => { @@ -61,7 +61,7 @@ describe('IDE branches search list', () => { describe('with branches', () => { it('renders list', () => { createComponent({ branches }); - const items = wrapper.findAll(Item); + const items = wrapper.findAllComponents(Item); expect(items.length).toBe(branches.length); }); @@ -69,7 +69,7 @@ describe('IDE branches search list', () => { it('renders check next to active branch', () => { const activeBranch = 'regular'; createComponent({ branches }, activeBranch); - const items = wrapper.findAll(Item).filter((w) => w.props('isActive')); + const items = wrapper.findAllComponents(Item).filter((w) => w.props('isActive')); expect(items.length).toBe(1); expect(items.at(0).props('item').name).toBe(activeBranch); diff --git a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js index d77e8e3d04c..f6d5833edee 100644 --- a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js @@ -26,8 +26,8 @@ describe('IDE commit editor header', () => { }); }; - const findDiscardModal = () => wrapper.find({ ref: 'discardModal' }); - const findDiscardButton = () => wrapper.find({ ref: 'discardButton' }); + const findDiscardModal = () => wrapper.findComponent({ ref: 'discardModal' }); + const findDiscardButton = () => wrapper.findComponent({ ref: 'discardButton' }); beforeEach(() => { store = createStore(); diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js index 28f62a9775a..a8ee81afa0b 100644 --- a/spec/frontend/ide/components/commit_sidebar/form_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js @@ -58,7 +58,7 @@ describe('IDE commit form', () => { }); const findForm = () => wrapper.find('form'); const submitForm = () => findForm().trigger('submit'); - const findCommitMessageInput = () => wrapper.find(CommitMessageField); + const findCommitMessageInput = () => wrapper.findComponent(CommitMessageField); const setCommitMessageInput = (val) => findCommitMessageInput().vm.$emit('input', val); const findDiscardDraftButton = () => wrapper.find('[data-testid="discard-draft"]'); @@ -302,7 +302,7 @@ describe('IDE commit form', () => { ${() => createCodeownersCommitError('test message')} | ${{ actionPrimary: { text: 'Create new branch' } }} ${createUnexpectedCommitError} | ${{ actionPrimary: null }} `('opens error modal if commitError with $error', async ({ createError, props }) => { - const modal = wrapper.find(GlModal); + const modal = wrapper.findComponent(GlModal); modal.vm.show = jest.fn(); const error = createError(); @@ -343,7 +343,7 @@ describe('IDE commit form', () => { await nextTick(); - wrapper.find(GlModal).vm.$emit('ok'); + wrapper.findComponent(GlModal).vm.$emit('ok'); await waitForPromises(); diff --git a/spec/frontend/ide/components/error_message_spec.js b/spec/frontend/ide/components/error_message_spec.js index 17568158131..204d39de741 100644 --- a/spec/frontend/ide/components/error_message_spec.js +++ b/spec/frontend/ide/components/error_message_spec.js @@ -105,7 +105,7 @@ describe('IDE error message component', () => { findActionButton().trigger('click'); await nextTick(); - expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).isVisible()).toBe(true); resolveAction(); }); @@ -113,7 +113,7 @@ describe('IDE error message component', () => { findActionButton().trigger('click'); await actionMock(); await nextTick(); - expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).isVisible()).toBe(false); }); }); }); diff --git a/spec/frontend/ide/components/file_templates/dropdown_spec.js b/spec/frontend/ide/components/file_templates/dropdown_spec.js index e54b322b9db..ee90d87357c 100644 --- a/spec/frontend/ide/components/file_templates/dropdown_spec.js +++ b/spec/frontend/ide/components/file_templates/dropdown_spec.js @@ -94,7 +94,7 @@ describe('IDE file templates dropdown component', () => { it('shows loader when isLoading is true', () => { createComponent({ props: defaultAsyncProps, state: { isLoading: true } }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('renders templates', () => { diff --git a/spec/frontend/ide/components/ide_file_row_spec.js b/spec/frontend/ide/components/ide_file_row_spec.js index baf3d7cca9d..aa66224fa19 100644 --- a/spec/frontend/ide/components/ide_file_row_spec.js +++ b/spec/frontend/ide/components/ide_file_row_spec.js @@ -39,8 +39,8 @@ describe('Ide File Row component', () => { wrapper = null; }); - const findFileRowExtra = () => wrapper.find(FileRowExtra); - const findFileRow = () => wrapper.find(FileRow); + const findFileRowExtra = () => wrapper.findComponent(FileRowExtra); + const findFileRow = () => wrapper.findComponent(FileRow); const hasDropdownOpen = () => findFileRowExtra().props('dropdownOpen'); it('fileRow component has listeners', async () => { diff --git a/spec/frontend/ide/components/ide_review_spec.js b/spec/frontend/ide/components/ide_review_spec.js index 13d20761263..0759f957374 100644 --- a/spec/frontend/ide/components/ide_review_spec.js +++ b/spec/frontend/ide/components/ide_review_spec.js @@ -42,7 +42,7 @@ describe('IDE review mode', () => { let inititializeSpy; beforeEach(async () => { - inititializeSpy = jest.spyOn(wrapper.find(IdeReview).vm, 'initialize'); + inititializeSpy = jest.spyOn(wrapper.findComponent(IdeReview).vm, 'initialize'); store.state.viewer = 'editor'; await wrapper.vm.reactivate(); @@ -85,7 +85,7 @@ describe('IDE review mode', () => { }); it('renders edit dropdown', () => { - expect(wrapper.find(EditorModeDropdown).exists()).toBe(true); + expect(wrapper.findComponent(EditorModeDropdown).exists()).toBe(true); }); it('renders merge request link & IID', async () => { diff --git a/spec/frontend/ide/components/ide_side_bar_spec.js b/spec/frontend/ide/components/ide_side_bar_spec.js index 4469c3fc901..4784d6c516f 100644 --- a/spec/frontend/ide/components/ide_side_bar_spec.js +++ b/spec/frontend/ide/components/ide_side_bar_spec.js @@ -47,32 +47,32 @@ describe('IdeSidebar', () => { await nextTick(); - expect(wrapper.findAll(GlSkeletonLoader)).toHaveLength(3); + expect(wrapper.findAllComponents(GlSkeletonLoader)).toHaveLength(3); }); describe('deferred rendering components', () => { it('fetches components on demand', async () => { wrapper = createComponent(); - expect(wrapper.find(IdeTree).exists()).toBe(true); - expect(wrapper.find(IdeReview).exists()).toBe(false); - expect(wrapper.find(RepoCommitSection).exists()).toBe(false); + expect(wrapper.findComponent(IdeTree).exists()).toBe(true); + expect(wrapper.findComponent(IdeReview).exists()).toBe(false); + expect(wrapper.findComponent(RepoCommitSection).exists()).toBe(false); store.state.currentActivityView = leftSidebarViews.review.name; await waitForPromises(); await nextTick(); - expect(wrapper.find(IdeTree).exists()).toBe(false); - expect(wrapper.find(IdeReview).exists()).toBe(true); - expect(wrapper.find(RepoCommitSection).exists()).toBe(false); + expect(wrapper.findComponent(IdeTree).exists()).toBe(false); + expect(wrapper.findComponent(IdeReview).exists()).toBe(true); + expect(wrapper.findComponent(RepoCommitSection).exists()).toBe(false); store.state.currentActivityView = leftSidebarViews.commit.name; await waitForPromises(); await nextTick(); - expect(wrapper.find(IdeTree).exists()).toBe(false); - expect(wrapper.find(IdeReview).exists()).toBe(false); - expect(wrapper.find(RepoCommitSection).exists()).toBe(true); + expect(wrapper.findComponent(IdeTree).exists()).toBe(false); + expect(wrapper.findComponent(IdeReview).exists()).toBe(false); + expect(wrapper.findComponent(RepoCommitSection).exists()).toBe(true); }); it.each` view | tree | review | commit @@ -86,23 +86,23 @@ describe('IdeSidebar', () => { await waitForPromises(); await nextTick(); - expect(wrapper.find(IdeTree).exists()).toBe(tree); - expect(wrapper.find(IdeReview).exists()).toBe(review); - expect(wrapper.find(RepoCommitSection).exists()).toBe(commit); + expect(wrapper.findComponent(IdeTree).exists()).toBe(tree); + expect(wrapper.findComponent(IdeReview).exists()).toBe(review); + expect(wrapper.findComponent(RepoCommitSection).exists()).toBe(commit); }); }); it('keeps the current activity view components alive', async () => { wrapper = createComponent(); - const ideTreeComponent = wrapper.find(IdeTree).element; + const ideTreeComponent = wrapper.findComponent(IdeTree).element; store.state.currentActivityView = leftSidebarViews.commit.name; await waitForPromises(); await nextTick(); - expect(wrapper.find(IdeTree).exists()).toBe(false); - expect(wrapper.find(RepoCommitSection).exists()).toBe(true); + expect(wrapper.findComponent(IdeTree).exists()).toBe(false); + expect(wrapper.findComponent(RepoCommitSection).exists()).toBe(true); store.state.currentActivityView = leftSidebarViews.edit.name; @@ -110,6 +110,6 @@ describe('IdeSidebar', () => { await nextTick(); // reference to the elements remains the same, meaning the components were kept alive - expect(wrapper.find(IdeTree).element).toEqual(ideTreeComponent); + expect(wrapper.findComponent(IdeTree).element).toEqual(ideTreeComponent); }); }); diff --git a/spec/frontend/ide/components/ide_sidebar_nav_spec.js b/spec/frontend/ide/components/ide_sidebar_nav_spec.js index 33b33fb62fd..80e8aba4072 100644 --- a/spec/frontend/ide/components/ide_sidebar_nav_spec.js +++ b/spec/frontend/ide/components/ide_sidebar_nav_spec.js @@ -55,7 +55,7 @@ describe('ide/components/ide_sidebar_nav', () => { ariaLabel: button.attributes('aria-label'), classes: button.classes(), qaSelector: button.attributes('data-qa-selector'), - icon: button.find(GlIcon).props('name'), + icon: button.findComponent(GlIcon).props('name'), tooltip: getBinding(button.element, 'tooltip').value, }; }); diff --git a/spec/frontend/ide/components/ide_spec.js b/spec/frontend/ide/components/ide_spec.js index 9172c69b10e..48c670757a2 100644 --- a/spec/frontend/ide/components/ide_spec.js +++ b/spec/frontend/ide/components/ide_spec.js @@ -82,7 +82,7 @@ describe('WebIDE', () => { await waitForPromises(); - expect(wrapper.find(ErrorMessage).exists()).toBe(exists); + expect(wrapper.findComponent(ErrorMessage).exists()).toBe(exists); }, ); }); diff --git a/spec/frontend/ide/components/ide_status_list_spec.js b/spec/frontend/ide/components/ide_status_list_spec.js index 371fbc6becd..0b54e8b6afb 100644 --- a/spec/frontend/ide/components/ide_status_list_spec.js +++ b/spec/frontend/ide/components/ide_status_list_spec.js @@ -25,7 +25,7 @@ describe('ide/components/ide_status_list', () => { let store; let wrapper; - const findLink = () => wrapper.find(GlLink); + const findLink = () => wrapper.findComponent(GlLink); const createComponent = (options = {}) => { store = new Vuex.Store({ getters: { @@ -98,6 +98,6 @@ describe('ide/components/ide_status_list', () => { it('renders terminal sync status', () => { createComponent(); - expect(wrapper.find(TerminalSyncStatusSafe).exists()).toBe(true); + expect(wrapper.findComponent(TerminalSyncStatusSafe).exists()).toBe(true); }); }); diff --git a/spec/frontend/ide/components/ide_status_mr_spec.js b/spec/frontend/ide/components/ide_status_mr_spec.js index 0526d4653f8..0b9111c0e2a 100644 --- a/spec/frontend/ide/components/ide_status_mr_spec.js +++ b/spec/frontend/ide/components/ide_status_mr_spec.js @@ -14,8 +14,8 @@ describe('ide/components/ide_status_mr', () => { propsData: props, }); }; - const findIcon = () => wrapper.find(GlIcon); - const findLink = () => wrapper.find(GlLink); + const findIcon = () => wrapper.findComponent(GlIcon); + const findLink = () => wrapper.findComponent(GlLink); afterEach(() => { wrapper.destroy(); diff --git a/spec/frontend/ide/components/ide_tree_spec.js b/spec/frontend/ide/components/ide_tree_spec.js index 8465ef9f5f3..f00017a2736 100644 --- a/spec/frontend/ide/components/ide_tree_spec.js +++ b/spec/frontend/ide/components/ide_tree_spec.js @@ -41,7 +41,7 @@ describe('IdeTree', () => { let inititializeSpy; beforeEach(async () => { - inititializeSpy = jest.spyOn(wrapper.find(IdeTree).vm, 'initialize'); + inititializeSpy = jest.spyOn(wrapper.findComponent(IdeTree).vm, 'initialize'); store.state.viewer = 'diff'; await wrapper.vm.reactivate(); diff --git a/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js b/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js index d632a34266a..5eb66f75978 100644 --- a/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js +++ b/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js @@ -27,7 +27,7 @@ describe('IDE job log scroll button', () => { beforeEach(() => createComponent({ direction })); it('returns proper icon name', () => { - expect(wrapper.find(GlIcon).props('name')).toBe(icon); + expect(wrapper.findComponent(GlIcon).props('name')).toBe(icon); }); it('returns proper title', () => { diff --git a/spec/frontend/ide/components/jobs/list_spec.js b/spec/frontend/ide/components/jobs/list_spec.js index cb2c9f8f04f..b4c7eb51781 100644 --- a/spec/frontend/ide/components/jobs/list_spec.js +++ b/spec/frontend/ide/components/jobs/list_spec.js @@ -58,29 +58,29 @@ describe('IDE stages list', () => { it('renders loading icon when no stages & loading', () => { createComponent({ loading: true, stages: [] }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('renders stages components for each stage', () => { createComponent({ stages }); - expect(wrapper.findAll(Stage).length).toBe(stages.length); + expect(wrapper.findAllComponents(Stage).length).toBe(stages.length); }); it('triggers fetchJobs action when stage emits fetch event', () => { createComponent({ stages }); - wrapper.find(Stage).vm.$emit('fetch'); + wrapper.findComponent(Stage).vm.$emit('fetch'); expect(storeActions.fetchJobs).toHaveBeenCalled(); }); it('triggers toggleStageCollapsed action when stage emits toggleCollapsed event', () => { createComponent({ stages }); - wrapper.find(Stage).vm.$emit('toggleCollapsed'); + wrapper.findComponent(Stage).vm.$emit('toggleCollapsed'); expect(storeActions.toggleStageCollapsed).toHaveBeenCalled(); }); it('triggers setDetailJob action when stage emits clickViewLog event', () => { createComponent({ stages }); - wrapper.find(Stage).vm.$emit('clickViewLog'); + wrapper.findComponent(Stage).vm.$emit('clickViewLog'); expect(storeActions.setDetailJob).toHaveBeenCalled(); }); diff --git a/spec/frontend/ide/components/jobs/stage_spec.js b/spec/frontend/ide/components/jobs/stage_spec.js index f158c59cd32..1d5e5743a4d 100644 --- a/spec/frontend/ide/components/jobs/stage_spec.js +++ b/spec/frontend/ide/components/jobs/stage_spec.js @@ -18,8 +18,8 @@ describe('IDE pipeline stage', () => { }, }; - const findHeader = () => wrapper.find({ ref: 'cardHeader' }); - const findJobList = () => wrapper.find({ ref: 'jobList' }); + const findHeader = () => wrapper.findComponent({ ref: 'cardHeader' }); + const findJobList = () => wrapper.findComponent({ ref: 'jobList' }); const createComponent = (props) => { wrapper = shallowMount(Stage, { @@ -45,7 +45,7 @@ describe('IDE pipeline stage', () => { stage: { ...defaultProps.stage, isLoading: true, jobs: [] }, }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('emits toggleCollaped event with stage id when clicking header', async () => { @@ -60,7 +60,7 @@ describe('IDE pipeline stage', () => { it('emits clickViewLog entity with job', async () => { const [job] = defaultProps.stage.jobs; createComponent(); - wrapper.findAll(Item).at(0).vm.$emit('clickViewLog', job); + wrapper.findAllComponents(Item).at(0).vm.$emit('clickViewLog', job); await nextTick(); expect(wrapper.emitted().clickViewLog[0][0]).toBe(job); }); diff --git a/spec/frontend/ide/components/merge_requests/list_spec.js b/spec/frontend/ide/components/merge_requests/list_spec.js index 583671a0af6..ea6e2741a85 100644 --- a/spec/frontend/ide/components/merge_requests/list_spec.js +++ b/spec/frontend/ide/components/merge_requests/list_spec.js @@ -14,7 +14,7 @@ describe('IDE merge requests list', () => { let fetchMergeRequestsMock; const findSearchTypeButtons = () => wrapper.findAll('button'); - const findTokenedInput = () => wrapper.find(TokenedInput); + const findTokenedInput = () => wrapper.findComponent(TokenedInput); const createComponent = (state = {}) => { const { mergeRequests = {}, ...restOfState } = state; @@ -63,7 +63,7 @@ describe('IDE merge requests list', () => { it('renders loading icon when merge request is loading', () => { createComponent({ mergeRequests: { isLoading: true } }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('renders no search results text when search is not empty', async () => { @@ -107,8 +107,8 @@ describe('IDE merge requests list', () => { it('renders list', () => { createComponent(defaultStateWithMergeRequests); - expect(wrapper.findAll(Item).length).toBe(1); - expect(wrapper.find(Item).props('item')).toBe( + expect(wrapper.findAllComponents(Item).length).toBe(1); + expect(wrapper.findComponent(Item).props('item')).toBe( defaultStateWithMergeRequests.mergeRequests.mergeRequests[0], ); }); diff --git a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js index 7f2ee0fe7d9..1d38231a767 100644 --- a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js +++ b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js @@ -27,7 +27,7 @@ describe('ide/components/panes/collapsible_sidebar.vue', () => { }); }; - const findSidebarNav = () => wrapper.find(IdeSidebarNav); + const findSidebarNav = () => wrapper.findComponent(IdeSidebarNav); beforeEach(() => { store = createStore(); diff --git a/spec/frontend/ide/components/panes/right_spec.js b/spec/frontend/ide/components/panes/right_spec.js index d12acd6dc4c..4555f519bc2 100644 --- a/spec/frontend/ide/components/panes/right_spec.js +++ b/spec/frontend/ide/components/panes/right_spec.js @@ -37,7 +37,7 @@ describe('ide/components/panes/right.vue', () => { it('is always shown', () => { createComponent(); - expect(wrapper.find(CollapsibleSidebar).props('extensionTabs')).toEqual( + expect(wrapper.findComponent(CollapsibleSidebar).props('extensionTabs')).toEqual( expect.arrayContaining([ expect.objectContaining({ show: true, @@ -65,7 +65,7 @@ describe('ide/components/panes/right.vue', () => { createComponent(); - expect(wrapper.find(CollapsibleSidebar).props('extensionTabs')).toEqual( + expect(wrapper.findComponent(CollapsibleSidebar).props('extensionTabs')).toEqual( expect.arrayContaining([ expect.objectContaining({ show: true, @@ -90,7 +90,7 @@ describe('ide/components/panes/right.vue', () => { store.state.terminal.isVisible = true; await nextTick(); - expect(wrapper.find(CollapsibleSidebar).props('extensionTabs')).toEqual( + expect(wrapper.findComponent(CollapsibleSidebar).props('extensionTabs')).toEqual( expect.arrayContaining([ expect.objectContaining({ show: true, @@ -103,7 +103,7 @@ describe('ide/components/panes/right.vue', () => { it('hides terminal tab when not visible', () => { store.state.terminal.isVisible = false; - expect(wrapper.find(CollapsibleSidebar).props('extensionTabs')).toEqual( + expect(wrapper.findComponent(CollapsibleSidebar).props('extensionTabs')).toEqual( expect.arrayContaining([ expect.objectContaining({ show: false, diff --git a/spec/frontend/ide/components/pipelines/empty_state_spec.js b/spec/frontend/ide/components/pipelines/empty_state_spec.js index f7409fc36be..31081e8f9d5 100644 --- a/spec/frontend/ide/components/pipelines/empty_state_spec.js +++ b/spec/frontend/ide/components/pipelines/empty_state_spec.js @@ -32,7 +32,7 @@ describe('~/ide/components/pipelines/empty_state.vue', () => { }); it('renders empty state', () => { - expect(wrapper.find(GlEmptyState).props()).toMatchObject({ + expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({ title: EmptyState.i18n.title, description: EmptyState.i18n.description, primaryButtonText: EmptyState.i18n.primaryButtonText, diff --git a/spec/frontend/ide/components/pipelines/list_spec.js b/spec/frontend/ide/components/pipelines/list_spec.js index 8a3606e27eb..545924c9c11 100644 --- a/spec/frontend/ide/components/pipelines/list_spec.js +++ b/spec/frontend/ide/components/pipelines/list_spec.js @@ -99,7 +99,7 @@ describe('IDE pipelines list', () => { }, ); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); }); it('renders loading state', () => { @@ -111,7 +111,7 @@ describe('IDE pipelines list', () => { }, ); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); }); @@ -128,7 +128,7 @@ describe('IDE pipelines list', () => { it('renders empty state when no latestPipeline', () => { createComponent({}, { ...defaultPipelinesLoadedState, latestPipeline: null }); - expect(wrapper.find(EmptyState).exists()).toBe(true); + expect(wrapper.findComponent(EmptyState).exists()).toBe(true); expect(wrapper.element).toMatchSnapshot(); }); @@ -144,7 +144,7 @@ describe('IDE pipelines list', () => { it('renders ci icon', () => { createComponent({}, withLatestPipelineState); - expect(wrapper.find(CiIcon).exists()).toBe(true); + expect(wrapper.findComponent(CiIcon).exists()).toBe(true); }); it('renders pipeline data', () => { @@ -158,7 +158,7 @@ describe('IDE pipelines list', () => { const isLoadingJobs = true; createComponent({}, { ...withLatestPipelineState, stages, isLoadingJobs }); - const jobProps = wrapper.findAll(GlTab).at(0).find(JobsList).props(); + const jobProps = wrapper.findAllComponents(GlTab).at(0).findComponent(JobsList).props(); expect(jobProps.stages).toBe(stages); expect(jobProps.loading).toBe(isLoadingJobs); }); @@ -169,7 +169,7 @@ describe('IDE pipelines list', () => { const isLoadingJobs = true; createComponent({}, { ...withLatestPipelineState, isLoadingJobs }); - const jobProps = wrapper.findAll(GlTab).at(1).find(JobsList).props(); + const jobProps = wrapper.findAllComponents(GlTab).at(1).findComponent(JobsList).props(); expect(jobProps.stages).toBe(failedStages); expect(jobProps.loading).toBe(isLoadingJobs); }); diff --git a/spec/frontend/ide/components/preview/clientside_spec.js b/spec/frontend/ide/components/preview/clientside_spec.js index 426fbd5c04c..cf768114e70 100644 --- a/spec/frontend/ide/components/preview/clientside_spec.js +++ b/spec/frontend/ide/components/preview/clientside_spec.js @@ -396,7 +396,7 @@ describe('IDE clientside preview', () => { wrapper.setData({ loading: true }); await nextTick(); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); }); diff --git a/spec/frontend/ide/components/preview/navigator_spec.js b/spec/frontend/ide/components/preview/navigator_spec.js index a199f4704f7..9c4f825ccf5 100644 --- a/spec/frontend/ide/components/preview/navigator_spec.js +++ b/spec/frontend/ide/components/preview/navigator_spec.js @@ -37,13 +37,13 @@ describe('IDE clientside preview navigator', () => { }); it('renders loading icon by default', () => { - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('removes loading icon when done event is fired', async () => { listenHandler({ type: 'done' }); await nextTick(); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); }); it('does not count visiting same url multiple times', async () => { diff --git a/spec/frontend/ide/components/repo_commit_section_spec.js b/spec/frontend/ide/components/repo_commit_section_spec.js index db4181395d3..d3312358402 100644 --- a/spec/frontend/ide/components/repo_commit_section_spec.js +++ b/spec/frontend/ide/components/repo_commit_section_spec.js @@ -77,8 +77,10 @@ describe('RepoCommitSection', () => { }); it('renders no changes text', () => { - expect(wrapper.find(EmptyState).text().trim()).toContain('No changes'); - expect(wrapper.find(EmptyState).find('img').attributes('src')).toBe(TEST_NO_CHANGES_SVG); + expect(wrapper.findComponent(EmptyState).text().trim()).toContain('No changes'); + expect(wrapper.findComponent(EmptyState).find('img').attributes('src')).toBe( + TEST_NO_CHANGES_SVG, + ); }); }); @@ -111,7 +113,7 @@ describe('RepoCommitSection', () => { }); it('does not show empty state', () => { - expect(wrapper.find(EmptyState).exists()).toBe(false); + expect(wrapper.findComponent(EmptyState).exists()).toBe(false); }); }); @@ -157,7 +159,7 @@ describe('RepoCommitSection', () => { }); it('does not show empty state', () => { - expect(wrapper.find(EmptyState).exists()).toBe(false); + expect(wrapper.findComponent(EmptyState).exists()).toBe(false); }); }); @@ -167,7 +169,7 @@ describe('RepoCommitSection', () => { beforeEach(async () => { createComponent(); - inititializeSpy = jest.spyOn(wrapper.find(RepoCommitSection).vm, 'initialize'); + inititializeSpy = jest.spyOn(wrapper.findComponent(RepoCommitSection).vm, 'initialize'); store.state.viewer = 'diff'; await wrapper.vm.reactivate(); diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js index e81f68ce626..9921d8cba18 100644 --- a/spec/frontend/ide/components/repo_editor_spec.js +++ b/spec/frontend/ide/components/repo_editor_spec.js @@ -211,7 +211,7 @@ describe('RepoEditor', () => { it('renders markdown for tempFile', async () => { findPreviewTab().vm.$emit('click'); await waitForPromises(); - expect(wrapper.find(ContentViewer).html()).toContain(dummyFile.text.content); + expect(wrapper.findComponent(ContentViewer).html()).toContain(dummyFile.text.content); }); describe('when file changes to non-markdown file', () => { diff --git a/spec/frontend/ide/components/repo_tab_spec.js b/spec/frontend/ide/components/repo_tab_spec.js index b16fd8f80ba..0dda176b47c 100644 --- a/spec/frontend/ide/components/repo_tab_spec.js +++ b/spec/frontend/ide/components/repo_tab_spec.js @@ -19,7 +19,7 @@ describe('RepoTab', () => { let store; let router; - const findTab = () => wrapper.find(GlTabStub); + const findTab = () => wrapper.findComponent(GlTabStub); function createComponent(propsData) { wrapper = mount(RepoTab, { diff --git a/spec/frontend/ide/components/resizable_panel_spec.js b/spec/frontend/ide/components/resizable_panel_spec.js index 55b9423aba8..fe2a128c9c8 100644 --- a/spec/frontend/ide/components/resizable_panel_spec.js +++ b/spec/frontend/ide/components/resizable_panel_spec.js @@ -35,7 +35,7 @@ describe('~/ide/components/resizable_panel', () => { store, }); }; - const findResizer = () => wrapper.find(PanelResizer); + const findResizer = () => wrapper.findComponent(PanelResizer); const findInlineStyle = () => wrapper.element.style.cssText; const createInlineStyle = (width) => `width: ${width}px;`; diff --git a/spec/frontend/ide/components/terminal/empty_state_spec.js b/spec/frontend/ide/components/terminal/empty_state_spec.js index 57c816747aa..15fb0fe9013 100644 --- a/spec/frontend/ide/components/terminal/empty_state_spec.js +++ b/spec/frontend/ide/components/terminal/empty_state_spec.js @@ -46,7 +46,7 @@ describe('IDE TerminalEmptyState', () => { }, }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('when not loading, does not show loading icon', () => { @@ -56,7 +56,7 @@ describe('IDE TerminalEmptyState', () => { }, }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); }); describe('when valid', () => { @@ -71,7 +71,7 @@ describe('IDE TerminalEmptyState', () => { }, }); - button = wrapper.find(GlButton); + button = wrapper.findComponent(GlButton); }); it('shows button', () => { @@ -100,7 +100,7 @@ describe('IDE TerminalEmptyState', () => { }, }); - expect(wrapper.find(GlButton).props('disabled')).toBe(true); - expect(wrapper.find(GlAlert).html()).toContain(TEST_HTML_MESSAGE); + expect(wrapper.findComponent(GlButton).props('disabled')).toBe(true); + expect(wrapper.findComponent(GlAlert).html()).toContain(TEST_HTML_MESSAGE); }); }); diff --git a/spec/frontend/ide/components/terminal/session_spec.js b/spec/frontend/ide/components/terminal/session_spec.js index 6a70ddb46a8..7e4a56b0610 100644 --- a/spec/frontend/ide/components/terminal/session_spec.js +++ b/spec/frontend/ide/components/terminal/session_spec.js @@ -38,7 +38,7 @@ describe('IDE TerminalSession', () => { }); }; - const findButton = () => wrapper.find(GlButton); + const findButton = () => wrapper.findComponent(GlButton); beforeEach(() => { state = { @@ -60,7 +60,7 @@ describe('IDE TerminalSession', () => { it('shows terminal', () => { factory(); - expect(wrapper.find(Terminal).props()).toEqual({ + expect(wrapper.findComponent(Terminal).props()).toEqual({ terminalPath: TEST_TERMINAL_PATH, status: RUNNING, }); diff --git a/spec/frontend/ide/components/terminal/terminal_controls_spec.js b/spec/frontend/ide/components/terminal/terminal_controls_spec.js index 71ec0dca89d..c18934f0f3b 100644 --- a/spec/frontend/ide/components/terminal/terminal_controls_spec.js +++ b/spec/frontend/ide/components/terminal/terminal_controls_spec.js @@ -12,7 +12,7 @@ describe('IDE TerminalControls', () => { ...options, }); - buttons = wrapper.findAll(ScrollButton); + buttons = wrapper.findAllComponents(ScrollButton); }; it('shows an up and down scroll button', () => { diff --git a/spec/frontend/ide/components/terminal/terminal_spec.js b/spec/frontend/ide/components/terminal/terminal_spec.js index afc49e22c83..4da3e1910e9 100644 --- a/spec/frontend/ide/components/terminal/terminal_spec.js +++ b/spec/frontend/ide/components/terminal/terminal_spec.js @@ -68,7 +68,7 @@ describe('IDE Terminal', () => { it(`shows when starting (${status})`, () => { factory({ status }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); expect(wrapper.find('.top-bar').text()).toBe('Starting...'); }); }); @@ -76,7 +76,7 @@ describe('IDE Terminal', () => { it(`shows when stopping`, () => { factory({ status: STOPPING }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); expect(wrapper.find('.top-bar').text()).toBe('Stopping...'); }); @@ -84,7 +84,7 @@ describe('IDE Terminal', () => { it('hides when not loading', () => { factory({ status }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); expect(wrapper.find('.top-bar').text()).toBe(''); }); }); @@ -107,23 +107,23 @@ describe('IDE Terminal', () => { }); it('is visible if terminal is created', () => { - expect(wrapper.find(TerminalControls).exists()).toBe(true); + expect(wrapper.findComponent(TerminalControls).exists()).toBe(true); }); it('scrolls glterminal on scroll-up', () => { - wrapper.find(TerminalControls).vm.$emit('scroll-up'); + wrapper.findComponent(TerminalControls).vm.$emit('scroll-up'); expect(wrapper.vm.glterminal.scrollToTop).toHaveBeenCalled(); }); it('scrolls glterminal on scroll-down', () => { - wrapper.find(TerminalControls).vm.$emit('scroll-down'); + wrapper.findComponent(TerminalControls).vm.$emit('scroll-down'); expect(wrapper.vm.glterminal.scrollToBottom).toHaveBeenCalled(); }); it('has props set', () => { - expect(wrapper.find(TerminalControls).props()).toEqual({ + expect(wrapper.findComponent(TerminalControls).props()).toEqual({ canScrollUp: false, canScrollDown: false, }); @@ -133,7 +133,7 @@ describe('IDE Terminal', () => { wrapper.setData({ canScrollUp: true, canScrollDown: true }); return nextTick().then(() => { - expect(wrapper.find(TerminalControls).props()).toEqual({ + expect(wrapper.findComponent(TerminalControls).props()).toEqual({ canScrollUp: true, canScrollDown: true, }); diff --git a/spec/frontend/ide/components/terminal/view_spec.js b/spec/frontend/ide/components/terminal/view_spec.js index 49f9513d2ac..57c8da9f5b7 100644 --- a/spec/frontend/ide/components/terminal/view_spec.js +++ b/spec/frontend/ide/components/terminal/view_spec.js @@ -66,7 +66,7 @@ describe('IDE TerminalView', () => { it('renders empty state', async () => { await factory(); - expect(wrapper.find(TerminalEmptyState).props()).toEqual({ + expect(wrapper.findComponent(TerminalEmptyState).props()).toEqual({ helpPath: TEST_HELP_PATH, illustrationPath: TEST_SVG_PATH, ...getters.allCheck(), @@ -79,7 +79,7 @@ describe('IDE TerminalView', () => { expect(actions.startSession).not.toHaveBeenCalled(); expect(actions.hideSplash).not.toHaveBeenCalled(); - wrapper.find(TerminalEmptyState).vm.$emit('start'); + wrapper.findComponent(TerminalEmptyState).vm.$emit('start'); expect(actions.startSession).toHaveBeenCalled(); expect(actions.hideSplash).toHaveBeenCalled(); @@ -89,7 +89,7 @@ describe('IDE TerminalView', () => { state.isShowSplash = false; await factory(); - expect(wrapper.find(TerminalEmptyState).exists()).toBe(false); - expect(wrapper.find(TerminalSession).exists()).toBe(true); + expect(wrapper.findComponent(TerminalEmptyState).exists()).toBe(false); + expect(wrapper.findComponent(TerminalSession).exists()).toBe(true); }); }); diff --git a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js index f921037d744..5b1502cc190 100644 --- a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js +++ b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js @@ -34,13 +34,13 @@ describe('ide/components/terminal_sync/terminal_sync_status_safe', () => { }); it('renders terminal sync status', () => { - expect(wrapper.find(TerminalSyncStatus).exists()).toBe(true); + expect(wrapper.findComponent(TerminalSyncStatus).exists()).toBe(true); }); }); describe('without terminal sync module', () => { it('does not render terminal sync status', () => { - expect(wrapper.find(TerminalSyncStatus).exists()).toBe(false); + expect(wrapper.findComponent(TerminalSyncStatus).exists()).toBe(false); }); }); }); diff --git a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js index 3a326b08fff..147235abc8e 100644 --- a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js +++ b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js @@ -78,19 +78,19 @@ describe('ide/components/terminal_sync/terminal_sync_status', () => { if (!icon) { it('does not render icon', () => { - expect(wrapper.find(GlIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlIcon).exists()).toBe(false); }); it('renders loading icon', () => { - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); } else { it('renders icon', () => { - expect(wrapper.find(GlIcon).props('name')).toEqual(icon); + expect(wrapper.findComponent(GlIcon).props('name')).toEqual(icon); }); it('does not render loading icon', () => { - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); }); } }); diff --git a/spec/graphql/mutations/merge_requests/set_reviewers_spec.rb b/spec/graphql/mutations/merge_requests/set_reviewers_spec.rb new file mode 100644 index 00000000000..df4aa885bbf --- /dev/null +++ b/spec/graphql/mutations/merge_requests/set_reviewers_spec.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::MergeRequests::SetReviewers do + let_it_be(:user) { create(:user) } + let_it_be(:merge_request, reload: true) { create(:merge_request) } + let_it_be(:reviewer) { create(:user) } + let_it_be(:reviewer2) { create(:user) } + + subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) } + + describe '#resolve' do + let(:reviewer_usernames) { [reviewer.username] } + let(:mutated_merge_request) { subject[:merge_request] } + let(:mode) { described_class.arguments['operationMode'].default_value } + + subject do + mutation.resolve(project_path: merge_request.project.full_path, + iid: merge_request.iid, + operation_mode: mode, + reviewer_usernames: reviewer_usernames) + end + + it 'does not change reviewers if the merge_request is not accessible to the reviewers' do + merge_request.project.add_developer(user) + + expect { subject }.not_to change { merge_request.reload.reviewer_ids } + end + + it 'returns an operational error if the merge_request is not accessible to the reviewers' do + merge_request.project.add_developer(user) + + result = subject + + expect(result[:errors]).to include a_string_matching(/Cannot assign/) + end + + context 'when the user does not have permissions' do + it_behaves_like 'permission level for merge request mutation is correctly verified' + end + + context 'when the user can update the merge_request' do + before do + merge_request.project.add_developer(reviewer) + merge_request.project.add_developer(reviewer2) + merge_request.project.add_developer(user) + end + + it 'replaces the reviewer' do + merge_request.reviewers = [reviewer2] + merge_request.save! + + expect(mutated_merge_request).to eq(merge_request) + expect(mutated_merge_request.reviewers).to contain_exactly(reviewer) + expect(subject[:errors]).to be_empty + end + + it 'returns errors when merge_request could not be updated' do + allow(merge_request).to receive(:errors_on_object).and_return(['foo']) + + expect(subject[:errors]).not_to match_array(['foo']) + end + + context 'when passing an empty reviewer list' do + let(:reviewer_usernames) { [] } + + before do + merge_request.reviewers = [reviewer] + merge_request.save! + end + + it 'removes all reviewers' do + expect(mutated_merge_request).to eq(merge_request) + expect(mutated_merge_request.reviewers).to eq([]) + expect(subject[:errors]).to be_empty + end + end + + context 'when passing "append" as true' do + subject do + mutation.resolve( + project_path: merge_request.project.full_path, + iid: merge_request.iid, + reviewer_usernames: reviewer_usernames, + operation_mode: Types::MutationOperationModeEnum.enum[:append] + ) + end + + before do + merge_request.reviewers = [reviewer2] + merge_request.save! + + # In CE, APPEND is a NOOP as you can't have multiple reviewers + # We test multiple assignment in EE specs + stub_licensed_features(multiple_merge_request_reviewers: false) + end + + it 'is a NO-OP in FOSS' do + expect(mutated_merge_request).to eq(merge_request) + expect(mutated_merge_request.reviewers).to contain_exactly(reviewer2) + expect(subject[:errors]).to be_empty + end + end + + context 'when passing "remove" as true' do + before do + merge_request.reviewers = [reviewer] + merge_request.save! + end + + it 'removes named reviewer' do + mutated_merge_request = mutation.resolve( + project_path: merge_request.project.full_path, + iid: merge_request.iid, + reviewer_usernames: reviewer_usernames, + operation_mode: Types::MutationOperationModeEnum.enum[:remove] + )[:merge_request] + + expect(mutated_merge_request).to eq(merge_request) + expect(mutated_merge_request.reviewers).to eq([]) + expect(subject[:errors]).to be_empty + end + + it 'does not remove unnamed reviewer' do + mutated_merge_request = mutation.resolve( + project_path: merge_request.project.full_path, + iid: merge_request.iid, + reviewer_usernames: [reviewer2.username], + operation_mode: Types::MutationOperationModeEnum.enum[:remove] + )[:merge_request] + + expect(mutated_merge_request).to eq(merge_request) + expect(mutated_merge_request.reviewers).to contain_exactly(reviewer) + expect(subject[:errors]).to be_empty + end + end + end + end +end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 780cf7b104a..530b03714b4 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -2277,10 +2277,34 @@ RSpec.describe Repository do .with(%i(branch_names merged_branch_names branch_count has_visible_content? has_ambiguous_refs?)) .and_call_original + expect_next_instance_of(ProtectedBranches::CacheService) do |cache_service| + expect(cache_service).to receive(:refresh) + end + repository.expire_branches_cache end end + describe '#expire_protected_branches_cache' do + it 'expires the cache' do + expect_next_instance_of(ProtectedBranches::CacheService) do |cache_service| + expect(cache_service).to receive(:refresh) + end + + repository.expire_protected_branches_cache + end + + context 'when repository does not have a project' do + let!(:snippet) { create(:personal_snippet, :repository) } + + it 'does not expire the cache' do + expect(ProtectedBranches::CacheService).not_to receive(:new) + + snippet.repository.expire_protected_branches_cache + end + end + end + describe '#expire_tags_cache' do it 'expires the cache' do expect(repository).to receive(:expire_method_caches) diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_reviewers_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_reviewers_spec.rb new file mode 100644 index 00000000000..be786256ef2 --- /dev/null +++ b/spec/requests/api/graphql/mutations/merge_requests/set_reviewers_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Setting reviewers of a merge request', :assume_throttled do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:current_user) { create(:user, developer_projects: [project]) } + let_it_be(:reviewer) { create(:user) } + let_it_be(:reviewer2) { create(:user) } + let_it_be_with_reload(:merge_request) { create(:merge_request, source_project: project) } + + let(:input) { { reviewer_usernames: [reviewer.username] } } + let(:expected_result) do + [{ 'username' => reviewer.username }] + end + + let(:mutation) do + variables = { + project_path: project.full_path, + iid: merge_request.iid.to_s + } + graphql_mutation(:merge_request_set_reviewers, variables.merge(input), + <<-QL.strip_heredoc + clientMutationId + errors + mergeRequest { + id + reviewers { + nodes { + username + } + } + } + QL + ) + end + + def mutation_response + graphql_mutation_response(:merge_request_set_reviewers) + end + + def mutation_reviewer_nodes + mutation_response['mergeRequest']['reviewers']['nodes'] + end + + def run_mutation! + post_graphql_mutation(mutation, current_user: current_user) + end + + before do + project.add_developer(reviewer) + project.add_developer(reviewer2) + + merge_request.update!(reviewers: []) + end + + it 'returns an error if the user is not allowed to update the merge request' do + post_graphql_mutation(mutation, current_user: create(:user)) + + expect(graphql_errors).not_to be_empty + end + + context 'when the current user does not have permission to add reviewers' do + let(:current_user) { create(:user) } + + it 'does not change the reviewers' do + project.add_guest(current_user) + + expect { run_mutation! }.not_to change { merge_request.reset.reviewers.pluck(:id) } + + expect(graphql_errors).not_to be_empty + end + end + + context 'with reviewers already assigned' do + before do + merge_request.reviewers = [reviewer2] + merge_request.save! + end + + it 'replaces the reviewer' do + run_mutation! + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_reviewer_nodes).to match_array(expected_result) + end + end + + context 'when passing an empty list of reviewers' do + let(:input) { { reviewer_usernames: [] } } + + before do + merge_request.reviewers = [reviewer2] + merge_request.save! + end + + it 'removes reviewer' do + run_mutation! + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_reviewer_nodes).to eq([]) + end + end +end diff --git a/spec/services/merge_requests/update_reviewers_service_spec.rb b/spec/services/merge_requests/update_reviewers_service_spec.rb new file mode 100644 index 00000000000..8920141adbb --- /dev/null +++ b/spec/services/merge_requests/update_reviewers_service_spec.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe MergeRequests::UpdateReviewersService do + include AfterNextHelpers + + let_it_be(:group) { create(:group, :public) } + let_it_be(:project) { create(:project, :private, :repository, group: group) } + let_it_be(:user) { create(:user) } + let_it_be(:user2) { create(:user) } + let_it_be(:user3) { create(:user) } + + let_it_be_with_reload(:merge_request) do + create(:merge_request, :simple, :unique_branches, + title: 'Old title', + description: "FYI #{user2.to_reference}", + reviewer_ids: [user3.id], + source_project: project, + target_project: project, + author: create(:user)) + end + + before do + project.add_maintainer(user) + project.add_developer(user2) + project.add_developer(user3) + merge_request.errors.clear + end + + let(:service) { described_class.new(project: project, current_user: user, params: opts) } + let(:opts) { { reviewer_ids: [user2.id] } } + + describe 'execute' do + def set_reviewers + service.execute(merge_request) + end + + def find_note(starting_with) + merge_request.notes.find do |note| + note && note.note.start_with?(starting_with) + end + end + + shared_examples 'removing all reviewers' do + it 'removes all reviewers' do + expect(set_reviewers).to have_attributes(reviewers: be_empty, errors: be_none) + end + end + + context 'when the parameters are valid' do + context 'when using sentinel values' do + let(:opts) { { reviewer_ids: [0] } } + + it_behaves_like 'removing all reviewers' + end + + context 'when the reviewer_ids parameter is the empty list' do + let(:opts) { { reviewer_ids: [] } } + + it_behaves_like 'removing all reviewers' + end + + it 'updates the MR' do + expect { set_reviewers } + .to change { merge_request.reload.reviewers }.from([user3]).to([user2]) + .and change(merge_request, :updated_at) + .and change(merge_request, :updated_by).to(user) + end + + it 'creates system note about merge_request review request' do + set_reviewers + + note = find_note('requested review from') + + expect(note).not_to be_nil + expect(note.note).to include "requested review from #{user2.to_reference}" + end + + it 'creates a pending todo for new review request' do + set_reviewers + + attributes = { + project: project, + author: user, + user: user2, + target_id: merge_request.id, + target_type: merge_request.class.name, + action: Todo::REVIEW_REQUESTED, + state: :pending + } + + expect(Todo.where(attributes).count).to eq 1 + end + + it 'sends email reviewer change notifications to old and new reviewers', :sidekiq_inline, :mailer do + perform_enqueued_jobs do + set_reviewers + end + + should_email(user2) + should_email(user3) + end + + it 'updates open merge request counter for reviewers', :use_clean_rails_memory_store_caching do + # Cache them to ensure the cache gets invalidated on update + expect(user2.review_requested_open_merge_requests_count).to eq(0) + expect(user3.review_requested_open_merge_requests_count).to eq(1) + + set_reviewers + + expect(user2.review_requested_open_merge_requests_count).to eq(1) + expect(user3.review_requested_open_merge_requests_count).to eq(0) + end + + it 'updates the tracking' do + expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) + .to receive(:track_users_review_requested) + .with(users: [user2]) + + set_reviewers + end + + it 'tracks reviewers changed event' do + expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) + .to receive(:track_reviewers_changed_action).once.with(user: user) + + set_reviewers + end + + it 'calls MergeRequest::ResolveTodosService#async_execute' do + expect_next_instance_of(MergeRequests::ResolveTodosService, merge_request, user) do |service| + expect(service).to receive(:async_execute) + end + + set_reviewers + end + + it 'executes hooks with update action' do + expect(service).to receive(:execute_hooks) + .with( + merge_request, + 'update', + old_associations: { + reviewers: [user3] + } + ) + + set_reviewers + end + + it 'does not update the reviewers if they do not have access' do + opts[:reviewer_ids] = [create(:user).id] + + expect(set_reviewers).to have_attributes( + reviewers: [user3], + errors: be_any + ) + end + end + end +end |