summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-08-15 09:11:06 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-08-15 09:11:06 +0000
commit5f431529c8eb0fd5c84df1e66a38ee88b8da1ba6 (patch)
treeae43e505bc222b2ea53ee7bfe7f81748ba944038
parentdefacc074a4a576e15021ba264de745af982b45d (diff)
downloadgitlab-ce-5f431529c8eb0fd5c84df1e66a38ee88b8da1ba6.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/layout/hash_alignment.yml21
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/graphql/mutations/merge_requests/set_reviewers.rb52
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/models/repository.rb6
-rw-r--r--app/services/merge_requests/update_assignees_service.rb23
-rw-r--r--app/services/merge_requests/update_reviewers_service.rb44
-rw-r--r--app/services/merge_requests/update_service.rb36
-rw-r--r--config/feature_categories.yml8
-rw-r--r--doc/api/graphql/reference/index.md22
-rw-r--r--doc/ci/pipelines/img/pipeline_mini_graph_v15_0.pngbin0 -> 6061 bytes
-rw-r--r--doc/ci/pipelines/multi_project_pipelines.md9
-rw-r--r--doc/development/api_graphql_styleguide.md31
-rw-r--r--spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js2
-rw-r--r--spec/frontend/feature_flags/components/empty_state_spec.js14
-rw-r--r--spec/frontend/feature_flags/components/environments_dropdown_spec.js10
-rw-r--r--spec/frontend/feature_flags/components/feature_flags_table_spec.js4
-rw-r--r--spec/frontend/feature_flags/components/form_spec.js18
-rw-r--r--spec/frontend/feature_flags/components/new_environments_dropdown_spec.js24
-rw-r--r--spec/frontend/feature_flags/components/new_feature_flag_spec.js6
-rw-r--r--spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js12
-rw-r--r--spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js8
-rw-r--r--spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js2
-rw-r--r--spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js12
-rw-r--r--spec/frontend/feature_flags/components/strategies/users_with_id_spec.js2
-rw-r--r--spec/frontend/feature_flags/components/strategy_parameters_spec.js6
-rw-r--r--spec/frontend/feature_flags/components/strategy_spec.js40
-rw-r--r--spec/frontend/ide/components/branches/item_spec.js6
-rw-r--r--spec/frontend/ide/components/branches/search_list_spec.js6
-rw-r--r--spec/frontend/ide/components/commit_sidebar/editor_header_spec.js4
-rw-r--r--spec/frontend/ide/components/commit_sidebar/form_spec.js6
-rw-r--r--spec/frontend/ide/components/error_message_spec.js4
-rw-r--r--spec/frontend/ide/components/file_templates/dropdown_spec.js2
-rw-r--r--spec/frontend/ide/components/ide_file_row_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_review_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_side_bar_spec.js34
-rw-r--r--spec/frontend/ide/components/ide_sidebar_nav_spec.js2
-rw-r--r--spec/frontend/ide/components/ide_spec.js2
-rw-r--r--spec/frontend/ide/components/ide_status_list_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_status_mr_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_tree_spec.js2
-rw-r--r--spec/frontend/ide/components/jobs/detail/scroll_button_spec.js2
-rw-r--r--spec/frontend/ide/components/jobs/list_spec.js10
-rw-r--r--spec/frontend/ide/components/jobs/stage_spec.js8
-rw-r--r--spec/frontend/ide/components/merge_requests/list_spec.js8
-rw-r--r--spec/frontend/ide/components/panes/collapsible_sidebar_spec.js2
-rw-r--r--spec/frontend/ide/components/panes/right_spec.js8
-rw-r--r--spec/frontend/ide/components/pipelines/empty_state_spec.js2
-rw-r--r--spec/frontend/ide/components/pipelines/list_spec.js12
-rw-r--r--spec/frontend/ide/components/preview/clientside_spec.js2
-rw-r--r--spec/frontend/ide/components/preview/navigator_spec.js4
-rw-r--r--spec/frontend/ide/components/repo_commit_section_spec.js12
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js2
-rw-r--r--spec/frontend/ide/components/repo_tab_spec.js2
-rw-r--r--spec/frontend/ide/components/resizable_panel_spec.js2
-rw-r--r--spec/frontend/ide/components/terminal/empty_state_spec.js10
-rw-r--r--spec/frontend/ide/components/terminal/session_spec.js4
-rw-r--r--spec/frontend/ide/components/terminal/terminal_controls_spec.js2
-rw-r--r--spec/frontend/ide/components/terminal/terminal_spec.js16
-rw-r--r--spec/frontend/ide/components/terminal/view_spec.js8
-rw-r--r--spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js4
-rw-r--r--spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js8
-rw-r--r--spec/graphql/mutations/merge_requests/set_reviewers_spec.rb140
-rw-r--r--spec/models/repository_spec.rb24
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_reviewers_spec.rb106
-rw-r--r--spec/services/merge_requests/update_reviewers_service_spec.rb162
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'
diff --git a/Gemfile b/Gemfile
index e8cdf812b9c..5cf417a67ae 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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
new file mode 100644
index 00000000000..48a0ca9d84f
--- /dev/null
+++ b/doc/ci/pipelines/img/pipeline_mini_graph_v15_0.png
Binary files differ
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