+
+
+
+ {{ failureMessage }}
+
+
-
- {{ name }}
+
-
-
-
-
-
- {{ content }}
-
-
-
-
{{ name }}
+
+
+
+
+
+
+ {{ content }}
+
+
+
+
+ {{ shortId }}
+
+
+
+
+
+
+
+
- {{ shortId }}
-
-
-
+
+
+
+
+ {{ content }}
+
+
+
+
+
+ {{ $options.i18n.latestBadgeText }}
+
+
+ {{ $options.i18n.mergeTrainBadgeText }}
+
+
+ {{ $options.i18n.invalidBadgeText }}
+
+
+ {{ $options.i18n.failedBadgeText }}
+
+
+ {{ $options.i18n.autoDevopsBadgeText }}
+
+
+ {{ $options.i18n.detachedBadgeText }}
+
+
+ {{ $options.i18n.stuckBadgeText }}
+
+
+
+ {{ totalJobsText }}
+
+
+ v-gl-tooltip
+ :title="$options.i18n.computeCreditsTooltip"
+ class="gl-ml-2"
+ data-testid="compute-credits"
+ >
+
+ {{ computeCredits }}
+
+
+
+ {{ inProgressText }}
+
-
-
- {{ $options.i18n.scheduleBadgeText }}
-
-
+
+
-
-
-
- {{ content }}
-
-
-
-
-
+
+
- {{ $options.i18n.latestBadgeText }}
-
-
- {{ $options.i18n.mergeTrainBadgeText }}
-
-
- {{ $options.i18n.invalidBadgeText }}
-
-
- {{ $options.i18n.failedBadgeText }}
-
-
- {{ $options.i18n.autoDevopsBadgeText }}
-
-
- {{ $options.i18n.detachedBadgeText }}
-
-
- {{ $options.i18n.stuckBadgeText }}
-
-
-
- {{ totalJobsText }}
-
-
-
- {{ computeCredits }}
-
-
-
- {{ inProgressText }}
-
+ {{ $options.i18n.deletePipelineText }}
+
-
+
+
+
+ {{ $options.modal.deleteConfirmationText }}
+
+
diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_refs.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_refs.vue
index 4258332ed6e..25af4cc8082 100644
--- a/app/assets/javascripts/projects/commit_box/info/components/commit_refs.vue
+++ b/app/assets/javascripts/projects/commit_box/info/components/commit_refs.vue
@@ -9,6 +9,8 @@ import {
TAGS,
FETCH_CONTAINING_REFS_EVENT,
FETCH_COMMIT_REFERENCES_ERROR,
+ BRANCHES_REF_TYPE,
+ TAGS_REF_TYPE,
} from '../constants';
import RefsList from './refs_list.vue';
@@ -98,7 +100,9 @@ export default {
tags: TAGS,
errorMessage: FETCH_COMMIT_REFERENCES_ERROR,
},
- fetchContainingRefsEvent: FETCH_CONTAINING_REFS_EVENT,
+ FETCH_CONTAINING_REFS_EVENT,
+ BRANCHES_REF_TYPE,
+ TAGS_REF_TYPE,
};
@@ -112,7 +116,8 @@ export default {
:containing-refs="containingBranches"
:namespace="$options.i18n.branches"
:url-part="commitsUrlPart"
- @[$options.fetchContainingRefsEvent]="fetchContainingBranches"
+ :ref-type="$options.BRANCHES_REF_TYPE"
+ @[$options.FETCH_CONTAINING_REFS_EVENT]="fetchContainingBranches"
/>
diff --git a/app/assets/javascripts/projects/commit_box/info/components/refs_list.vue b/app/assets/javascripts/projects/commit_box/info/components/refs_list.vue
index 7e21040a3b1..8ceab9cb60b 100644
--- a/app/assets/javascripts/projects/commit_box/info/components/refs_list.vue
+++ b/app/assets/javascripts/projects/commit_box/info/components/refs_list.vue
@@ -16,6 +16,10 @@ export default {
type: String,
required: true,
},
+ refType: {
+ type: String,
+ required: true,
+ },
containingRefs: {
type: Array,
required: false,
@@ -60,6 +64,9 @@ export default {
this.toggleCollapse();
this.$emit(FETCH_CONTAINING_REFS_EVENT);
},
+ getRefUrl(ref) {
+ return `${this.urlPart}${ref}?ref_type=${this.refType}`;
+ },
},
i18n: {
containingCommit: CONTAINING_COMMIT,
@@ -73,7 +80,7 @@ export default {
{{ ref }}{{ ref }}
@@ -196,12 +196,9 @@ export default {
{{ $options.i18n.description }}
- {{ $options.i18n.configurationHistory }}
+ {{
+ $options.i18n.configurationHistory
+ }}
diff --git a/app/assets/javascripts/security_configuration/components/auto_dev_ops_alert.vue b/app/assets/javascripts/security_configuration/components/auto_dev_ops_alert.vue
index 315f676e659..c01df3573c5 100644
--- a/app/assets/javascripts/security_configuration/components/auto_dev_ops_alert.vue
+++ b/app/assets/javascripts/security_configuration/components/auto_dev_ops_alert.vue
@@ -28,7 +28,7 @@ export default {
variant="info"
:primary-button-link="autoDevopsPath"
:primary-button-text="$options.i18n.primaryButtonText"
- data-qa-selector="autodevops_container"
+ data-testid="autodevops-container"
@dismiss="dismissMethod"
>
diff --git a/app/assets/javascripts/security_configuration/components/feature_card.vue b/app/assets/javascripts/security_configuration/components/feature_card.vue
index d1b705fe2fc..a757657339b 100644
--- a/app/assets/javascripts/security_configuration/components/feature_card.vue
+++ b/app/assets/javascripts/security_configuration/components/feature_card.vue
@@ -122,7 +122,7 @@ export default {
v-if="isNotSastIACTemporaryHack"
:class="statusClasses"
data-testid="feature-status"
- :data-qa-selector="`${feature.type}_status`"
+ :data-qa-feature="`${feature.type}_${enabled}_status`"
>
{{ configurationButton.text }}
@@ -176,7 +176,7 @@ export default {
variant="confirm"
:category="manageViaMrButtonCategory"
class="gl-mt-5"
- :data-qa-selector="`${feature.type}_mr_button`"
+ :data-testid="`${feature.type}_mr_button`"
@error="onError"
/>
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
index 7b5ded9348f..9023807eba3 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -1,5 +1,5 @@
-
- {{ status.text }}
+
+ {{ status.text }}
+
-
+
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
index faa50a50c69..3bb168e9051 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
@@ -131,7 +131,6 @@ export default {
ref="search"
:value="searchTerm"
:placeholder="searchText"
- class="js-dropdown-input-field"
@input="setSearchTerm"
/>
diff --git a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
index ac4d1517d52..4879baced0d 100644
--- a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
+++ b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
@@ -308,7 +308,7 @@ export default {
diff --git a/app/graphql/types/ci/runner_manager_type.rb b/app/graphql/types/ci/runner_manager_type.rb
index 2a5053f8f07..9c89b6537ea 100644
--- a/app/graphql/types/ci/runner_manager_type.rb
+++ b/app/graphql/types/ci/runner_manager_type.rb
@@ -47,3 +47,5 @@ module Types
end
end
end
+
+Types::Ci::RunnerManagerType.prepend_mod_with('Types::Ci::RunnerManagerType')
diff --git a/app/models/integrations/chat_message/push_message.rb b/app/models/integrations/chat_message/push_message.rb
index 60a3105d1c0..b17e28bb6c6 100644
--- a/app/models/integrations/chat_message/push_message.rb
+++ b/app/models/integrations/chat_message/push_message.rb
@@ -82,12 +82,12 @@ module Integrations
if ref_type == 'tag'
"#{project_url}/-/tags/#{ref}"
else
- "#{project_url}/commits/#{ref}"
+ "#{project_url}/-/commits/#{ref}"
end
end
def compare_url
- "#{project_url}/compare/#{before}...#{after}"
+ "#{project_url}/-/compare/#{before}...#{after}"
end
def ref_link
diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb
index 4ad88188e45..3aba5a2c7ed 100644
--- a/app/presenters/ci/pipeline_presenter.rb
+++ b/app/presenters/ci/pipeline_presenter.rb
@@ -89,13 +89,13 @@ module Ci
def ref_text
if pipeline.detached_merge_request_pipeline?
- _("For merge request %{link_to_merge_request} to merge %{link_to_merge_request_source_branch}")
+ _("Related merge request %{link_to_merge_request} to merge %{link_to_merge_request_source_branch}")
.html_safe % {
link_to_merge_request: link_to_merge_request,
link_to_merge_request_source_branch: link_to_merge_request_source_branch
}
elsif pipeline.merged_result_pipeline?
- _("For merge request %{link_to_merge_request} to merge %{link_to_merge_request_source_branch} into %{link_to_merge_request_target_branch}")
+ _("Related merge request %{link_to_merge_request} to merge %{link_to_merge_request_source_branch} into %{link_to_merge_request_target_branch}")
.html_safe % {
link_to_merge_request: link_to_merge_request,
link_to_merge_request_source_branch: link_to_merge_request_source_branch,
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index 15339becb74..dfa582f4c60 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -26,7 +26,7 @@
.detail-page-header-actions.gl-align-self-start.is-merge-request.js-issuable-actions.gl-display-flex
- if can_update_merge_request
- = render Pajamas::ButtonComponent.new(href: edit_project_merge_request_path(@project, @merge_request), button_options: {class: "gl-display-none gl-md-display-block js-issuable-edit", data: { qa_selector: "edit_button" }}) do
+ = render Pajamas::ButtonComponent.new(href: edit_project_merge_request_path(@project, @merge_request), button_options: {class: "gl-display-none gl-md-display-block js-issuable-edit", data: { qa_selector: "edit_title_button" }}) do
= _('Edit')
- if @merge_request.source_project
diff --git a/app/views/protected_branches/_create_protected_branch.html.haml b/app/views/protected_branches/_create_protected_branch.html.haml
index b4765ab49c2..799f6aa6031 100644
--- a/app/views/protected_branches/_create_protected_branch.html.haml
+++ b/app/views/protected_branches/_create_protected_branch.html.haml
@@ -3,12 +3,12 @@
= dropdown_tag(_('Select'),
options: { toggle_class: 'js-allowed-to-merge wide',
dropdown_class: 'dropdown-menu-selectable capitalize-header', dropdown_qa_selector: 'allowed_to_merge_dropdown_content', dropdown_testid: 'allowed-to-merge-dropdown',
- data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes', qa_selector: 'allowed_to_merge_dropdown' }})
+ data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes', qa_selector: 'select_allowed_to_merge_dropdown' }})
- content_for :push_access_levels do
.push_access_levels-container
= dropdown_tag(_('Select'),
options: { toggle_class: "js-allowed-to-push js-multiselect wide",
dropdown_class: 'dropdown-menu-selectable capitalize-header', dropdown_qa_selector: 'allowed_to_push_dropdown_content' , dropdown_testid: 'allowed-to-push-dropdown',
- data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes', qa_selector: 'allowed_to_push_dropdown' }})
+ data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes', qa_selector: 'select_allowed_to_push_dropdown' }})
= render 'protected_branches/shared/create_protected_branch', protected_branch_entity: protected_branch_entity
diff --git a/config/metrics/counts_28d/20210216175113_merge_request_action_monthly.yml b/config/metrics/counts_28d/20210216175113_merge_request_action_monthly.yml
index 7ee99925c98..a53c14f9b21 100644
--- a/config/metrics/counts_28d/20210216175113_merge_request_action_monthly.yml
+++ b/config/metrics/counts_28d/20210216175113_merge_request_action_monthly.yml
@@ -1,5 +1,5 @@
---
-data_category: optional
+data_category: operational
key_path: redis_hll_counters.source_code.merge_request_action_monthly
description: Count of unique users who perform an action on a merge request
product_section: dev
diff --git a/config/metrics/counts_28d/20210216181446_g_project_management_issue_comment_added_monthly.yml b/config/metrics/counts_28d/20210216181446_g_project_management_issue_comment_added_monthly.yml
index 3062544003d..28850b5285b 100644
--- a/config/metrics/counts_28d/20210216181446_g_project_management_issue_comment_added_monthly.yml
+++ b/config/metrics/counts_28d/20210216181446_g_project_management_issue_comment_added_monthly.yml
@@ -1,5 +1,5 @@
---
-data_category: optional
+data_category: operational
key_path: redis_hll_counters.issues_edit.g_project_management_issue_comment_added_monthly
description: Count of MAU commenting on an issue
product_section: dev
diff --git a/config/metrics/counts_all/20210216180750_groups.yml b/config/metrics/counts_all/20210216180750_groups.yml
index 72d5c97ccb1..904aa534ed6 100644
--- a/config/metrics/counts_all/20210216180750_groups.yml
+++ b/config/metrics/counts_all/20210216180750_groups.yml
@@ -1,5 +1,5 @@
---
-data_category: optional
+data_category: operational
key_path: counts.groups
description: Total count of groups as of usage ping snapshot
product_section: dev
diff --git a/config/routes.rb b/config/routes.rb
index 3a09bf4b136..cf5476b0c77 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -66,11 +66,6 @@ InitializerConnections.raise_if_new_database_connection do
Gitlab.ee do
resource :company, only: [:new, :create], controller: 'company'
- # TODO: remove next line and the controller after the deployment
- # https://gitlab.com/gitlab-org/gitlab/-/issues/411208
- resources :groups_projects, only: [:create] do
- post :import, on: :collection
- end
resources :groups, only: [:new, :create] do
post :import, on: :collection
end
diff --git a/db/post_migrate/20230602063059_remove_broadcast_messages_namespace_id_column.rb b/db/post_migrate/20230602063059_remove_broadcast_messages_namespace_id_column.rb
new file mode 100644
index 00000000000..ad7e23b7cb1
--- /dev/null
+++ b/db/post_migrate/20230602063059_remove_broadcast_messages_namespace_id_column.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class RemoveBroadcastMessagesNamespaceIdColumn < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_broadcast_messages_on_namespace_id'
+
+ def up
+ remove_column :broadcast_messages, :namespace_id
+ end
+
+ def down
+ # rubocop:disable Migration/SchemaAdditionMethodsNoPost
+ add_column :broadcast_messages, :namespace_id, :bigint unless column_exists?(:broadcast_messages, :namespace_id)
+ # rubocop:enable Migration/SchemaAdditionMethodsNoPost
+
+ add_concurrent_index :broadcast_messages, :namespace_id, name: INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20230602063059 b/db/schema_migrations/20230602063059
new file mode 100644
index 00000000000..53ae46fb8f5
--- /dev/null
+++ b/db/schema_migrations/20230602063059
@@ -0,0 +1 @@
+915530f0de68a448bb9c88572896dc0979a38b5624dc5006811a4c635e35c71e
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 7f46cc56198..9856a06a66f 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -12761,8 +12761,7 @@ CREATE TABLE broadcast_messages (
broadcast_type smallint DEFAULT 1 NOT NULL,
dismissable boolean,
target_access_levels integer[] DEFAULT '{}'::integer[] NOT NULL,
- theme smallint DEFAULT 0 NOT NULL,
- namespace_id bigint
+ theme smallint DEFAULT 0 NOT NULL
);
CREATE SEQUENCE broadcast_messages_id_seq
@@ -29976,8 +29975,6 @@ CREATE INDEX index_boards_on_project_id ON boards USING btree (project_id);
CREATE INDEX index_broadcast_message_on_ends_at_and_broadcast_type_and_id ON broadcast_messages USING btree (ends_at, broadcast_type, id);
-CREATE INDEX index_broadcast_messages_on_namespace_id ON broadcast_messages USING btree (namespace_id);
-
CREATE INDEX index_btree_namespaces_traversal_ids ON namespaces USING btree (traversal_ids);
CREATE INDEX index_bulk_import_batch_trackers_on_tracker_id ON bulk_import_batch_trackers USING btree (tracker_id);
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 810dae24f25..816fb8ab439 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -10348,6 +10348,29 @@ The edge type for [`ProductAnalyticsDashboardPanel`](#productanalyticsdashboardp
| `cursor` | [`String!`](#string) | A cursor for use in pagination. |
| `node` | [`ProductAnalyticsDashboardPanel`](#productanalyticsdashboardpanel) | The item at the end of the edge. |
+#### `ProductAnalyticsDashboardVisualizationConnection`
+
+The connection type for [`ProductAnalyticsDashboardVisualization`](#productanalyticsdashboardvisualization).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `edges` | [`[ProductAnalyticsDashboardVisualizationEdge]`](#productanalyticsdashboardvisualizationedge) | A list of edges. |
+| `nodes` | [`[ProductAnalyticsDashboardVisualization]`](#productanalyticsdashboardvisualization) | A list of nodes. |
+| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `ProductAnalyticsDashboardVisualizationEdge`
+
+The edge type for [`ProductAnalyticsDashboardVisualization`](#productanalyticsdashboardvisualization).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| `node` | [`ProductAnalyticsDashboardVisualization`](#productanalyticsdashboardvisualization) | The item at the end of the edge. |
+
#### `ProjectConnection`
The connection type for [`Project`](#project).
@@ -12941,6 +12964,7 @@ Returns [`CiRunnerStatus!`](#cirunnerstatus).
| `runner` | [`CiRunner`](#cirunner) | Runner configuration for the runner manager. |
| `status` | [`CiRunnerStatus!`](#cirunnerstatus) | Status of the runner manager. |
| `systemId` | [`String!`](#string) | System ID associated with the runner manager. |
+| `upgradeStatus` **{warning-solid}** | [`CiRunnerUpgradeStatus`](#cirunnerupgradestatus) | **Introduced** in 16.1. This feature is an Experiment. It can be changed or removed at any time. Availability of upgrades for the runner manager. |
| `version` | [`String`](#string) | Version of the runner. |
### `CiSecureFileRegistry`
@@ -19505,6 +19529,7 @@ Represents a product analytics dashboard visualization.
| ---- | ---- | ----------- |
| `data` | [`JSON!`](#json) | Data of the visualization. |
| `options` | [`JSON!`](#json) | Options of the visualization. |
+| `slug` | [`String!`](#string) | Slug of the visualization. |
| `type` | [`String!`](#string) | Type of the visualization. |
### `Project`
@@ -20562,6 +20587,26 @@ four standard [pagination arguments](#connection-pagination-arguments):
| ---- | ---- | ----------- |
| `slug` | [`String`](#string) | Find by dashboard slug. |
+##### `Project.productAnalyticsVisualizations`
+
+Visualizations of the project or associated configuration project.
+
+WARNING:
+**Introduced** in 16.1.
+This feature is an Experiment. It can be changed or removed at any time.
+
+Returns [`ProductAnalyticsDashboardVisualizationConnection`](#productanalyticsdashboardvisualizationconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#connection-pagination-arguments):
+`before: String`, `after: String`, `first: Int`, `last: Int`.
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `slug` | [`String`](#string) | Slug of the visualization to return. |
+
##### `Project.projectMembers`
Members of the project.
diff --git a/doc/architecture/blueprints/runner_tokens/index.md b/doc/architecture/blueprints/runner_tokens/index.md
index 0dc592531a3..39130e3384b 100644
--- a/doc/architecture/blueprints/runner_tokens/index.md
+++ b/doc/architecture/blueprints/runner_tokens/index.md
@@ -411,31 +411,32 @@ scope.
### Stage 5 - Optional disabling of registration token
-| Component | Milestone | Changes |
-|------------------|----------:|---------|
-| GitLab Rails app | `%16.0` | Adapt `register_{group|project}_runner` permissions to take [application setting](https://gitlab.com/gitlab-org/gitlab/-/issues/386712) in consideration. |
-| GitLab Rails app | | Add UI to allow disabling use of registration tokens at project or group level. |
-| GitLab Rails app | | Introduce `:enforce_create_runner_workflow` feature flag (disabled by default) to control whether use of registration tokens is allowed. |
-| GitLab Rails app | | Make [`POST /api/v4/runners` endpoint](../../../api/runners.md#register-a-new-runner) permanently return `HTTP 410 Gone` if either `allow_runner_registration_token` setting or `:enforce_create_runner_workflow` feature flag disables registration tokens.
A future v5 version of the API should return `HTTP 404 Not Found`. |
-| GitLab Rails app | | Hide legacy UI showing registration with a registration token, if `:enforce_create_runner_workflow` feature flag disables registration tokens. |
+| Component | Milestone | Changes |
+|------------------|----------:|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| GitLab Rails app | `%16.0` | Adapt `register_{group|project}_runner` permissions to take [application setting](https://gitlab.com/gitlab-org/gitlab/-/issues/386712) in consideration. |
+| GitLab Rails app | | Add UI to allow disabling use of registration tokens in top-level group settings. |
+| GitLab Rails app | | Make [`POST /api/v4/runners` endpoint](../../../api/runners.md#register-a-new-runner) permanently return `HTTP 410 Gone` if either `allow_runner_registration_token` setting disables registration tokens.
A future v5 version of the API should return `HTTP 404 Not Found`. |
+| GitLab Rails app | | Hide legacy UI showing registration with a registration token, if it disabled on in top-level group settings or by admins. |
### Stage 6 - Enforcement
-| Component | Milestone | Changes |
-|------------------|----------:|---------|
-| GitLab Rails app | `%16.6` | Enable `:enforce_create_runner_workflow` feature flag by default. |
-| GitLab Rails app | | Implement new `:create_runner` PPGAT scope so that we don't require a full `api` scope. |
-| GitLab Rails app | | Document gotchas when [automatically rotating runner tokens](../../../ci/runners/configure_runners.md#automatically-rotate-authentication-tokens) with multiple machines. |
+| Component | Milestone | Changes |
+|------------------|----------:|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| GitLab Rails app | `%16.6` | Disable registration tokens for all groups by running database migration (only on GitLab.com) | |
+| GitLab Rails app | `%16.6` | Disable registration tokens on the instance level by running database migration (except GitLab.com) | |
+| GitLab Rails app | `%16.8` | Disable registration tokens on the instance level for GitLab.com | |
+| GitLab Rails app | | Implement new `:create_runner` PPGAT scope so that we don't require a full `api` scope. |
+| GitLab Rails app | | Document gotchas when [automatically rotating runner tokens](../../../ci/runners/configure_runners.md#automatically-rotate-authentication-tokens) with multiple machines. |
### Stage 7 - Removals
-| Component | Milestone | Changes |
-|------------------|----------:|---------|
-| GitLab Rails app | `17.0` | Remove legacy UI showing registration with a registration token. |
-| GitLab Runner | `17.0` | Remove runner model arguments from `register` command (for example `--run-untagged`, `--tag-list`, etc.) |
-| GitLab Rails app | `17.0` | Create database migrations to drop `allow_runner_registration_token` setting columns from `application_settings` and `namespace_settings` tables. |
+| Component | Milestone | Changes |
+|------------------|----------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| GitLab Rails app | `17.0` | Remove UI enabling registration tokens on the group and instance levels. |
+| GitLab Rails app | `17.0` | Remove legacy UI showing registration with a registration token. |
+| GitLab Runner | `17.0` | Remove runner model arguments from `register` command (for example `--run-untagged`, `--tag-list`, etc.) |
+| GitLab Rails app | `17.0` | Create database migrations to drop `allow_runner_registration_token` setting columns from `application_settings` and `namespace_settings` tables. |
| GitLab Rails app | `17.0` | Create database migrations to drop:
- `runners_registration_token`/`runners_registration_token_encrypted` columns from `application_settings`;
- `runners_token`/`runners_token_encrypted` from `namespaces` table;
- `runners_token`/`runners_token_encrypted` from `projects` table. |
-| GitLab Rails app | `17.0` | Remove `:enforce_create_runner_workflow` feature flag. |
## FAQ
@@ -444,13 +445,16 @@ scope.
If no action is taken before your GitLab instance is upgraded to 16.6, then your runner registration
workflow will break.
Until then, both the new and the old workflow will coexist side-by-side.
-For self-managed instances, to continue using the previous runner registration process,
-you can disable the `enforce_create_runner_workflow` feature flag until GitLab 17.0.
To avoid a broken workflow, you need to first create a runner in the GitLab runners admin page.
After that, you'll need to replace the registration token you're using in your runner registration
workflow with the obtained runner authentication token.
+### Can I use the old runner registration process after 15.6?
+
+- If you're using GitLab.com, you'll be able to manually re-enable the previous runner registration process in the top-level group settings until GitLab 16.8.
+- If you're running GitLab self-managed, you'll be able re-enable the previous runner registration process in admin settings until GitLab 17.0.
+
### What is the new runner registration process?
When the new runner registration process is introduced, you will:
@@ -476,12 +480,6 @@ This allows the GitLab instance to display which system executed a given job.
- In GitLab 15.10, we plan to implement runner creation directly in the runners administration page,
and prepare the runner to follow the new workflow.
- In GitLab 16.6, we plan to disable registration tokens.
- For self-managed instances, to continue using
- registration tokens, you can disable the `enforce_create_runner_workflow` feature flag until
- GitLab 17.0.
-
- Previous `gitlab-runner` versions (that don't include the new `system_id` value) will start to be
- rejected by the GitLab instance;
- In GitLab 17.0, we plan to completely remove support for runner registration tokens.
### How will the `gitlab-runner register` command syntax change?
diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md
index 001a599776a..67430da0739 100644
--- a/doc/ci/variables/predefined_variables.md
+++ b/doc/ci/variables/predefined_variables.md
@@ -35,7 +35,7 @@ as it can cause the pipeline to behave unexpectedly.
| `CI_COMMIT_DESCRIPTION` | 10.8 | all | The description of the commit. If the title is shorter than 100 characters, the message without the first line. |
| `CI_COMMIT_MESSAGE` | 10.8 | all | The full commit message. |
| `CI_COMMIT_REF_NAME` | 9.0 | all | The branch or tag name for which project is built. |
-| `CI_COMMIT_REF_PROTECTED` | 11.11 | all | `true` if the job is running for a protected reference. |
+| `CI_COMMIT_REF_PROTECTED` | 11.11 | all | `true` if the job is running for a protected reference, `false` otherwise. |
| `CI_COMMIT_REF_SLUG` | 9.0 | all | `CI_COMMIT_REF_NAME` in lowercase, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. No leading / trailing `-`. Use in URLs, host names and domain names. |
| `CI_COMMIT_SHA` | 9.0 | all | The commit revision the project is built for. |
| `CI_COMMIT_SHORT_SHA` | 11.7 | all | The first eight characters of `CI_COMMIT_SHA`. |
diff --git a/doc/ci/yaml/workflow.md b/doc/ci/yaml/workflow.md
index 82144e55216..e88a96ae1f5 100644
--- a/doc/ci/yaml/workflow.md
+++ b/doc/ci/yaml/workflow.md
@@ -129,7 +129,7 @@ workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_TAG
- - if: $CI_COMMIT_REF_PROTECTED
+ - if: $CI_COMMIT_REF_PROTECTED == "true"
```
This example assumes that your long-lived branches are [protected](../../user/project/protected_branches.md).
diff --git a/doc/development/pipelines/internals.md b/doc/development/pipelines/internals.md
index 9e511fb88f6..4cdaf50641e 100644
--- a/doc/development/pipelines/internals.md
+++ b/doc/development/pipelines/internals.md
@@ -136,9 +136,9 @@ that are scoped to a single [configuration keyword](../../ci/yaml/index.md#job-k
| `.qa-cache` | Allows a job to use a default `cache` definition suitable for QA tasks. |
| `.yarn-cache` | Allows a job to use a default `cache` definition suitable for frontend jobs that do a `yarn install`. |
| `.assets-compile-cache` | Allows a job to use a default `cache` definition suitable for frontend jobs that compile assets. |
-| `.use-pg13` | Allows a job to use the `postgres` 13 and `redis` services (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific versions of the services). |
+| `.use-pg13` | Allows a job to use the `postgres` 13, `redis`, and `rediscluster` services (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific versions of the services). |
| `.use-pg13-ee` | Same as `.use-pg13` but also use an `elasticsearch` service (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific version of the service). |
-| `.use-pg14` | Allows a job to use the `postgres` 14 and `redis` services (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific versions of the services). |
+| `.use-pg14` | Allows a job to use the `postgres` 14, `redis`, and `rediscluster` services (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific versions of the services). |
| `.use-pg14-ee` | Same as `.use-pg14` but also use an `elasticsearch` service (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific version of the service). |
| `.use-kaniko` | Allows a job to use the `kaniko` tool to build Docker images. |
| `.as-if-foss` | Simulate the FOSS project by setting the `FOSS_ONLY='1'` CI/CD variable. |
diff --git a/doc/tutorials/more_tutorials.md b/doc/tutorials/more_tutorials.md
index c52de180bff..19b3a709ab7 100644
--- a/doc/tutorials/more_tutorials.md
+++ b/doc/tutorials/more_tutorials.md
@@ -13,8 +13,8 @@ If you're learning about GitLab, to find more tutorial content:
- Find recent tutorials on the GitLab blog by [searching by the `tutorial` tag](https://about.gitlab.com/blog/tags.html#tutorial).
-- Browse the **Learn@GitLab** [playlist on YouTube](https://www.youtube.com/playlist?list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED)
+- Browse the **GitLab Snapshots** [playlist on YouTube](https://www.youtube.com/playlist?list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED)
to find video tutorials.
If you find an article, video, or other resource that would be a
-great addition to this page, add it in a [merge request](../development/documentation/index.md).
+great addition to the tutorial pages, add it in a [merge request](../development/documentation/index.md).
diff --git a/lib/gitlab/ci/status/scheduled.rb b/lib/gitlab/ci/status/scheduled.rb
index e9068c326cf..8526becfef9 100644
--- a/lib/gitlab/ci/status/scheduled.rb
+++ b/lib/gitlab/ci/status/scheduled.rb
@@ -5,11 +5,11 @@ module Gitlab
module Status
class Scheduled < Status::Core
def text
- s_('CiStatusText|delayed')
+ s_('CiStatusText|scheduled')
end
def label
- s_('CiStatusLabel|delayed')
+ s_('CiStatusLabel|scheduled')
end
def icon
diff --git a/lib/gitlab/ci/status/success_warning.rb b/lib/gitlab/ci/status/success_warning.rb
index 47623ad945f..84a0e52f518 100644
--- a/lib/gitlab/ci/status/success_warning.rb
+++ b/lib/gitlab/ci/status/success_warning.rb
@@ -9,7 +9,7 @@ module Gitlab
#
class SuccessWarning < Status::Extended
def text
- s_('CiStatusText|passed')
+ s_('CiStatusText|warning')
end
def label
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 04f4852b9a2..95b4baadf46 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -9619,9 +9619,6 @@ msgstr ""
msgid "CiStatusLabel|created"
msgstr ""
-msgid "CiStatusLabel|delayed"
-msgstr ""
-
msgid "CiStatusLabel|failed"
msgstr ""
@@ -9640,6 +9637,9 @@ msgstr ""
msgid "CiStatusLabel|preparing"
msgstr ""
+msgid "CiStatusLabel|scheduled"
+msgstr ""
+
msgid "CiStatusLabel|skipped"
msgstr ""
@@ -9679,12 +9679,18 @@ msgstr ""
msgid "CiStatusText|preparing"
msgstr ""
+msgid "CiStatusText|scheduled"
+msgstr ""
+
msgid "CiStatusText|skipped"
msgstr ""
msgid "CiStatusText|waiting"
msgstr ""
+msgid "CiStatusText|warning"
+msgstr ""
+
msgid "CiStatus|running"
msgstr ""
@@ -19289,12 +19295,6 @@ msgstr ""
msgid "For investigating IT service disruptions or outages"
msgstr ""
-msgid "For merge request %{link_to_merge_request} to merge %{link_to_merge_request_source_branch}"
-msgstr ""
-
-msgid "For merge request %{link_to_merge_request} to merge %{link_to_merge_request_source_branch} into %{link_to_merge_request_target_branch}"
-msgstr ""
-
msgid "For more info, read the documentation."
msgstr ""
@@ -32155,14 +32155,15 @@ msgstr ""
msgid "PackageRegistry|Debian"
msgstr ""
-msgid "PackageRegistry|Delete 1 asset"
-msgid_plural "PackageRegistry|Delete %d assets"
-msgstr[0] ""
-msgstr[1] ""
+msgid "PackageRegistry|Delete %{count} assets"
+msgstr ""
msgid "PackageRegistry|Delete Package Version"
msgstr ""
+msgid "PackageRegistry|Delete asset"
+msgstr ""
+
msgid "PackageRegistry|Delete package"
msgstr ""
@@ -32471,6 +32472,9 @@ msgstr ""
msgid "PackageRegistry|Yes, delete selected packages"
msgstr ""
+msgid "PackageRegistry|You are about to delete %{count} assets. This operation is irreversible."
+msgstr ""
+
msgid "PackageRegistry|You are about to delete %{count} packages. This operation is irreversible."
msgstr ""
@@ -32480,11 +32484,6 @@ msgstr ""
msgid "PackageRegistry|You are about to delete %{name}, are you sure?"
msgstr ""
-msgid "PackageRegistry|You are about to delete 1 asset. This operation is irreversible."
-msgid_plural "PackageRegistry|You are about to delete %d assets. This operation is irreversible."
-msgstr[0] ""
-msgstr[1] ""
-
msgid "PackageRegistry|You are about to delete version %{version} of %{name}. Are you sure?"
msgstr ""
@@ -33574,6 +33573,12 @@ msgstr ""
msgid "Pipelines|Token"
msgstr ""
+msgid "Pipelines|Total amount of compute credits used for the pipeline"
+msgstr ""
+
+msgid "Pipelines|Total number of jobs for the pipeline"
+msgstr ""
+
msgid "Pipelines|Trigger user has insufficient permissions to project"
msgstr ""
@@ -37681,6 +37686,12 @@ msgstr ""
msgid "Related issues"
msgstr ""
+msgid "Related merge request %{link_to_merge_request} to merge %{link_to_merge_request_source_branch}"
+msgstr ""
+
+msgid "Related merge request %{link_to_merge_request} to merge %{link_to_merge_request_source_branch} into %{link_to_merge_request_target_branch}"
+msgstr ""
+
msgid "Related merge requests"
msgstr ""
@@ -39078,6 +39089,9 @@ msgid_plural "Runners|%{highlightStart}%{duration}%{highlightEnd} seconds"
msgstr[0] ""
msgstr[1] ""
+msgid "Runners|%{linkStart}Create a new runner%{linkEnd} to get started."
+msgstr ""
+
msgid "Runners|%{link_start}These runners%{link_end} are available to all groups and projects."
msgstr ""
@@ -39306,6 +39320,9 @@ msgstr ""
msgid "Runners|Filter projects"
msgstr ""
+msgid "Runners|Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner."
+msgstr ""
+
msgid "Runners|Get started with runners"
msgstr ""
@@ -39632,10 +39649,7 @@ msgstr ""
msgid "Runners|Runners are grouped when they have the same authentication token. This happens when you re-use a runner configuration in more than one runner manager. %{linkStart}How does this work?%{linkEnd}"
msgstr ""
-msgid "Runners|Runners are the agents that run your CI/CD jobs. Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner."
-msgstr ""
-
-msgid "Runners|Runners are the agents that run your CI/CD jobs. To register new runners, please contact your administrator."
+msgid "Runners|Runners are the agents that run your CI/CD jobs."
msgstr ""
msgid "Runners|Runners performance"
@@ -39707,6 +39721,9 @@ msgstr ""
msgid "Runners|Step 3 (optional)"
msgstr ""
+msgid "Runners|Still using registration tokens?"
+msgstr ""
+
msgid "Runners|Stop the runner from accepting new jobs."
msgstr ""
@@ -39778,6 +39795,9 @@ msgstr ""
msgid "Runners|To install Runner in a container follow the instructions described in the GitLab documentation"
msgstr ""
+msgid "Runners|To register new runners, contact your administrator."
+msgstr ""
+
msgid "Runners|To register them, go to the %{link_start}group's Runners page%{link_end}."
msgstr ""
diff --git a/qa/qa/page/element.rb b/qa/qa/page/element.rb
index 6bfdf98587b..db2f2153560 100644
--- a/qa/qa/page/element.rb
+++ b/qa/qa/page/element.rb
@@ -13,9 +13,7 @@ module QA
@attributes[:pattern] ||= selector
options.each do |option|
- if option.is_a?(String) || option.is_a?(Regexp)
- @attributes[:pattern] = option
- end
+ @attributes[:pattern] = option if option.is_a?(String) || option.is_a?(Regexp)
end
end
@@ -28,7 +26,7 @@ module QA
end
def selector_css
- %Q([data-qa-selector="#{@name}"]#{additional_selectors},.#{selector})
+ %(#{qa_selector}#{additional_selectors},.#{selector})
end
def expression
@@ -40,14 +38,26 @@ module QA
end
def matches?(line)
- !!(line =~ /["']#{name}['"]|#{expression}/)
+ !!(line =~ /["']#{name}['"]|["']#{convert_to_kebabcase(name)}['"]|#{expression}/)
end
private
+ def convert_to_kebabcase(text)
+ text.to_s.tr('_', '-')
+ end
+
+ def qa_selector
+ [
+ %([data-testid="#{name}"]#{additional_selectors}),
+ %([data-testid="#{convert_to_kebabcase(name)}"]#{additional_selectors}),
+ %([data-qa-selector="#{name}"]#{additional_selectors})
+ ].join(',')
+ end
+
def additional_selectors
@attributes.dup.delete_if { |attr| attr == :pattern || attr == :required }.map do |key, value|
- %Q([data-qa-#{key.to_s.tr('_', '-')}="#{value}"])
+ %([data-qa-#{key.to_s.tr('_', '-')}="#{value}"])
end.join
end
end
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index d5d8c52a0e3..9eb55989ea8 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -118,7 +118,7 @@ module QA
end
view 'app/views/projects/merge_requests/_mr_title.html.haml' do
- element :edit_button
+ element :edit_title_button
element :title_content, required: true
end
@@ -211,7 +211,7 @@ module QA
# Click by JS is needed to bypass the Moved MR actions popover
# Change back to regular click_element when moved_mr_sidebar FF is removed
# Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/385460
- click_by_javascript(find_element(:edit_button))
+ click_by_javascript(find_element(:edit_title_button))
end
def fast_forward_not_possible?
diff --git a/qa/qa/page/project/secure/configuration_form.rb b/qa/qa/page/project/secure/configuration_form.rb
index 493ec08d023..70eff31bfa9 100644
--- a/qa/qa/page/project/secure/configuration_form.rb
+++ b/qa/qa/page/project/secure/configuration_form.rb
@@ -9,15 +9,13 @@ module QA
view 'app/assets/javascripts/security_configuration/components/app.vue' do
element :security_configuration_container
- element :security_configuration_history_link
+ element :security_view_history_link
end
view 'app/assets/javascripts/security_configuration/components/feature_card.vue' do
- element :dependency_scanning_status, "`${feature.type}_status`" # rubocop:disable QA/ElementWithPattern
- element :sast_status, "`${feature.type}_status`" # rubocop:disable QA/ElementWithPattern
+ element :feature_status
element :sast_enable_button, "`${feature.type}_enable_button`" # rubocop:disable QA/ElementWithPattern
element :dependency_scanning_mr_button, "`${feature.type}_mr_button`" # rubocop:disable QA/ElementWithPattern
- element :license_scanning_status, "`${feature.type}_status`" # rubocop:disable QA/ElementWithPattern
end
view 'app/assets/javascripts/security_configuration/components/auto_dev_ops_alert.vue' do
@@ -25,15 +23,15 @@ module QA
end
def has_security_configuration_history_link?
- has_element?(:security_configuration_history_link)
+ has_element?(:security_view_history_link)
end
def has_no_security_configuration_history_link?
- has_no_element?(:security_configuration_history_link)
+ has_no_element?(:security_view_history_link)
end
def click_security_configuration_history_link
- click_element(:security_configuration_history_link)
+ click_element(:security_view_history_link)
end
def click_sast_enable_button
@@ -44,40 +42,20 @@ module QA
click_element(:dependency_scanning_mr_button)
end
- def has_sast_status?(status_text)
- within_element(:sast_status) do
- has_text?(status_text)
- end
+ def has_true_sast_status?
+ has_element?(:feature_status, feature: 'sast_true_status')
end
- def has_no_sast_status?(status_text)
- within_element(:sast_status) do
- has_no_text?(status_text)
- end
+ def has_false_sast_status?
+ has_element?(:feature_status, feature: 'sast_false_status')
end
- def has_dependency_scanning_status?(status_text)
- within_element(:dependency_scanning_status) do
- has_text?(status_text)
- end
+ def has_true_dependency_scanning_status?
+ has_element?(:feature_status, feature: 'dependency_scanning_true_status')
end
- def has_no_dependency_scanning_status?(status_text)
- within_element(:dependency_scanning_status) do
- has_no_text?(status_text)
- end
- end
-
- def has_license_compliance_status?(status_text)
- within_element(:license_scanning_status) do
- has_text?(status_text)
- end
- end
-
- def has_no_license_compliance_status?(status_text)
- within_element(:license_scanning_status) do
- has_no_text?(status_text)
- end
+ def has_false_dependency_scanning_status?
+ has_element?(:feature_status, feature: 'dependency_scanning_false_status')
end
def has_auto_devops_container?
diff --git a/qa/qa/page/project/settings/protected_branches.rb b/qa/qa/page/project/settings/protected_branches.rb
index 3eddd0fd33a..e6b13ed77a0 100644
--- a/qa/qa/page/project/settings/protected_branches.rb
+++ b/qa/qa/page/project/settings/protected_branches.rb
@@ -11,9 +11,9 @@ module QA
end
view 'app/views/protected_branches/_create_protected_branch.html.haml' do
- element :allowed_to_push_dropdown
+ element :select_allowed_to_push_dropdown
element :allowed_to_push_dropdown_content
- element :allowed_to_merge_dropdown
+ element :select_allowed_to_merge_dropdown
element :allowed_to_merge_dropdown_content
end
@@ -45,7 +45,7 @@ module QA
private
def select_allowed(action, allowed)
- click_element :"allowed_to_#{action}_dropdown"
+ click_element :"select_allowed_to_#{action}_dropdown"
allowed[:roles] = Resource::ProtectedBranch::Roles::NO_ONE unless allowed.key?(:roles)
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb
index fd818c3797b..b2dca4fc312 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb
@@ -43,7 +43,7 @@ module QA
end
it 'mentions another user in an issue',
-testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347988' do
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347988' do
Page::Project::Issue::Show.perform do |show|
at_username = "@#{user.username}"
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
index 0ec231ed66e..41ef38d2d66 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
@@ -4,7 +4,7 @@ module QA
RSpec.describe 'Create' do
describe 'Git push over HTTP', :smoke, :skip_fips_env, product_group: :source_code do
it 'user using a personal access token pushes code to the repository',
-testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347749' do
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347749' do
Flow::Login.sign_in
access_token = Resource::PersonalAccessToken.fabricate!.token
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
index efa1f9fe2c9..edc85849356 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
@@ -4,7 +4,7 @@ module QA
RSpec.describe 'Create' do
describe 'Git push over HTTP', product_group: :source_code do
it 'user pushes code to the repository', :smoke, :skip_fips_env,
- testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347747' do
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347747' do
Flow::Login.sign_in
Resource::Repository::ProjectPush.fabricate! do |push|
@@ -20,7 +20,7 @@ module QA
end
it 'pushes to a project using a specific Praefect repository storage', :smoke, :skip_fips_env, :requires_admin,
- :requires_praefect, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347789' do
+ :requires_praefect, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347789' do
Flow::Login.sign_in_as_admin
project = Resource::Project.fabricate_via_api! do |storage_project|
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb
index f281f441e8a..dcee723a1c4 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb
@@ -27,7 +27,7 @@ module QA
end
it 'pushes code to the repository via SSH', :smoke, :skip_fips_env,
- testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347825' do
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347825' do
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.ssh_key = @key
@@ -43,7 +43,7 @@ module QA
end
it 'pushes multiple branches and tags together', :smoke, :skip_fips_env,
- testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347826' do
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347826' do
branches = []
tags = []
Git::Repository.perform do |repository|
diff --git a/qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb b/qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb
index ffe340eb0dd..9b1df337065 100644
--- a/qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb
@@ -17,7 +17,7 @@ module QA
end
it 'can preview markdown side-by-side while editing',
- testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/367749' do
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/367749' do
project.visit!
Page::Project::Show.perform do |project|
project.click_file('README.md')
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_with_image_pull_policy_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_with_image_pull_policy_spec.rb
index aaaa11ef867..c693a57605e 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_with_image_pull_policy_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_with_image_pull_policy_spec.rb
@@ -71,10 +71,10 @@ module QA
if pull_image
expect(job_log).to have_content(message),
- "Expected to find #{message} in #{job_log}, but didn't."
+ "Expected to find #{message} in #{job_log}, but didn't."
else
expect(job_log).not_to have_content(message),
- "Found #{message} in #{job_log}, but didn't expect to."
+ "Found #{message} in #{job_log}, but didn't expect to."
end
end
end
@@ -96,7 +96,7 @@ module QA
visit_job
expect(job_log).to include(text1, text2),
- "Expected to find contents #{text1} and #{text2} in #{job_log}, but didn't."
+ "Expected to find contents #{text1} and #{text2} in #{job_log}, but didn't."
end
end
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_project_level_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_project_level_spec.rb
index 27bca6c17a2..908585c8423 100644
--- a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_project_level_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_project_level_spec.rb
@@ -2,10 +2,10 @@
module QA
RSpec.describe 'Package', :orchestrated, :requires_admin, :packages, :object_storage, :reliable,
- feature_flag: {
- name: 'maven_central_request_forwarding',
- scope: :global
- } do
+ feature_flag: {
+ name: 'maven_central_request_forwarding',
+ scope: :global
+ } do
describe 'Maven project level endpoint', product_group: :package_registry do
include Runtime::Fixtures
include Support::Helpers::MaskToken
@@ -218,16 +218,14 @@ module QA
) do
Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
Resource::Repository::Commit.fabricate_via_api! do |commit|
- gitlab_ci_yaml = ERB.new(read_fixture('package_managers/maven/project/request_forwarding',
- 'gitlab_ci.yaml.erb'
- )
- )
- .result(binding)
- settings_xml = ERB.new(read_fixture('package_managers/maven/project/request_forwarding',
- 'settings.xml.erb'
- )
- )
- .result(binding)
+ gitlab_ci_yaml = ERB.new(read_fixture(
+ 'package_managers/maven/project/request_forwarding',
+ 'gitlab_ci.yaml.erb'
+ )).result(binding)
+ settings_xml = ERB.new(read_fixture(
+ 'package_managers/maven/project/request_forwarding',
+ 'settings.xml.erb'
+ )).result(binding)
commit.project = imported_project
commit.commit_message = 'Add files'
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_project_level_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_project_level_spec.rb
index ad5835d8c9d..adb299eaab5 100644
--- a/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_project_level_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_project_level_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Package', :skip_live_env, :orchestrated, :packages, :object_storage,
-product_group: :package_registry do
+ product_group: :package_registry do
describe 'NuGet project level endpoint' do
include Support::Helpers::MaskToken
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/rubygems_registry_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/rubygems_registry_spec.rb
index 1af1fc7c231..374111b3498 100644
--- a/qa/qa/specs/features/browser_ui/5_package/package_registry/rubygems_registry_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/rubygems_registry_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Package', :orchestrated, :packages, :object_storage,
- feature_flag: { name: 'rubygem_packages', scope: :project } do
+ feature_flag: { name: 'rubygem_packages', scope: :project } do
describe 'RubyGems Repository', product_group: :package_registry do
include Runtime::Fixtures
diff --git a/qa/spec/page/element_spec.rb b/qa/spec/page/element_spec.rb
index fbf58b5e18a..da1fd224564 100644
--- a/qa/spec/page/element_spec.rb
+++ b/qa/spec/page/element_spec.rb
@@ -73,7 +73,7 @@ RSpec.describe QA::Page::Element do
subject { described_class.new(:something, /link_to 'something'/) }
it 'has an attribute[pattern] of the pattern' do
- expect(subject.attributes[:pattern]).to eq /link_to 'something'/
+ expect(subject.attributes[:pattern]).to eq(/link_to 'something'/)
end
it 'is not required by default' do
@@ -98,7 +98,7 @@ RSpec.describe QA::Page::Element do
subject { described_class.new(:something, /link_to 'something_else_entirely'/, required: true) }
it 'has an attribute[pattern] of the passed pattern' do
- expect(subject.attributes[:pattern]).to eq /link_to 'something_else_entirely'/
+ expect(subject.attributes[:pattern]).to eq(/link_to 'something_else_entirely'/)
end
it 'is required' do
@@ -118,6 +118,10 @@ RSpec.describe QA::Page::Element do
expect(subject.selector_css).to include(%q([data-qa-selector="my_element"]))
end
+ it 'properly translates to a data-testid' do
+ expect(subject.selector_css).to include(%q([data-testid="my_element"]))
+ end
+
context 'additional selectors' do
let(:element) { described_class.new(:my_element, index: 3, another_match: 'something') }
let(:required_element) { described_class.new(:my_element, required: true, index: 3) }
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index ee71181fba2..4cf558b04cc 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -162,7 +162,7 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
open_assignees_dropdown
page.within '.dropdown-menu-user' do
- find('.js-dropdown-input-field').find('input').set(user2.name)
+ find('[data-testid="user-search-input"]').set(user2.name)
wait_for_requests
@@ -182,7 +182,7 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
it 'keeps your filtered term after filtering and dismissing the dropdown' do
open_assignees_dropdown
- find('.js-dropdown-input-field').find('input').set(user2.name)
+ find('[data-testid="user-search-input"]').set(user2.name)
wait_for_requests
page.within '.dropdown-menu-user' do
@@ -199,7 +199,7 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
expect(page.all('[data-testid="selected-participant"]').length).to eq(1)
end
- expect(find('.js-dropdown-input-field').find('input').value).to eq(user2.name)
+ expect(find('[data-testid="user-search-input"]').value).to eq(user2.name)
end
end
end
diff --git a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
index 7d024103943..ca12e0e2b65 100644
--- a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
@@ -67,7 +67,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
- expect(page).to have_selector('.ci-created', count: 2)
+ expect(page).to have_selector('[data-testid="ci-badge-created"]', count: 2)
expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{detached_merge_request_pipeline.id}")
end
end
@@ -103,7 +103,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
- expect(page).to have_selector('.ci-pending', count: 4)
+ expect(page).to have_selector('[data-testid="ci-badge-pending"]', count: 4)
expect(all('[data-testid="pipeline-url-link"]')[0])
.to have_content("##{detached_merge_request_pipeline_2.id}")
@@ -246,7 +246,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees a branch pipeline in pipeline tab' do
page.within('.ci-table') do
- expect(page).to have_selector('.ci-created', count: 1)
+ expect(page).to have_selector('[data-testid="ci-badge-created"]', count: 1)
expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{push_pipeline.id}")
end
end
@@ -299,7 +299,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
- expect(page).to have_selector('.ci-pending', count: 2)
+ expect(page).to have_selector('[data-testid="ci-badge-pending"]', count: 2)
expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{detached_merge_request_pipeline.id}")
end
end
@@ -315,7 +315,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees pipeline list in forked project' do
visit project_pipelines_path(forked_project)
- expect(page).to have_selector('.ci-pending', count: 2)
+ expect(page).to have_selector('[data-testid="ci-badge-pending"]', count: 2)
end
context 'when a user updated a merge request from a forked project to the parent project' do
@@ -341,7 +341,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
- expect(page).to have_selector('.ci-pending', count: 4)
+ expect(page).to have_selector('[data-testid="ci-badge-pending"]', count: 4)
expect(all('[data-testid="pipeline-url-link"]')[0])
.to have_content("##{detached_merge_request_pipeline_2.id}")
@@ -384,7 +384,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees pipeline list in forked project' do
visit project_pipelines_path(forked_project)
- expect(page).to have_selector('.ci-pending', count: 4)
+ expect(page).to have_selector('[data-testid="ci-badge-pending"]', count: 4)
end
end
diff --git a/spec/features/merge_request/user_sees_pipelines_spec.rb b/spec/features/merge_request/user_sees_pipelines_spec.rb
index f92ce3865a9..a2796cd250b 100644
--- a/spec/features/merge_request/user_sees_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_pipelines_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe 'Merge request > User sees pipelines', :js, feature_category: :co
wait_for_requests
page.within('[data-testid="pipeline-table-row"]') do
- expect(page).to have_selector('.ci-success')
+ expect(page).to have_selector('[data-testid="ci-badge-passed"]')
expect(page).to have_content(pipeline.id)
expect(page).to have_content('API')
expect(page).to have_css('[data-testid="pipeline-mini-graph"]')
diff --git a/spec/features/projects/commit/user_sees_pipelines_tab_spec.rb b/spec/features/projects/commit/user_sees_pipelines_tab_spec.rb
index da83bbcb63a..e44364c7f2d 100644
--- a/spec/features/projects/commit/user_sees_pipelines_tab_spec.rb
+++ b/spec/features/projects/commit/user_sees_pipelines_tab_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe 'Commit > Pipelines tab', :js, feature_category: :source_code_man
wait_for_requests
page.within('[data-testid="pipeline-table-row"]') do
- expect(page).to have_selector('.ci-success')
+ expect(page).to have_selector('[data-testid="ci-badge-passed"]')
expect(page).to have_content(pipeline.id)
expect(page).to have_content('API')
expect(page).to have_css('[data-testid="pipeline-mini-graph"]')
diff --git a/spec/features/projects/jobs/user_browses_jobs_spec.rb b/spec/features/projects/jobs/user_browses_jobs_spec.rb
index d74a3d28068..aeba53c22b6 100644
--- a/spec/features/projects/jobs/user_browses_jobs_spec.rb
+++ b/spec/features/projects/jobs/user_browses_jobs_spec.rb
@@ -72,7 +72,7 @@ RSpec.describe 'User browses jobs', feature_category: :groups_and_projects do
wait_for_requests
- expect(page).to have_selector('.ci-canceled')
+ expect(page).to have_selector('[data-testid="ci-badge-canceled"]')
expect(page).not_to have_selector('[data-testid="jobs-table-error-alert"]')
end
end
@@ -94,7 +94,7 @@ RSpec.describe 'User browses jobs', feature_category: :groups_and_projects do
wait_for_requests
- expect(page).to have_selector('.ci-pending')
+ expect(page).to have_selector('[data-testid="ci-badge-pending"]')
end
end
@@ -134,7 +134,7 @@ RSpec.describe 'User browses jobs', feature_category: :groups_and_projects do
wait_for_requests
- expect(page).to have_selector('.ci-pending')
+ expect(page).to have_selector('[data-testid="ci-badge-pending"]')
end
it 'unschedules a job successfully' do
@@ -142,7 +142,7 @@ RSpec.describe 'User browses jobs', feature_category: :groups_and_projects do
wait_for_requests
- expect(page).to have_selector('.ci-manual')
+ expect(page).to have_selector('[data-testid="ci-badge-manual"]')
end
end
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 01f8f2166ac..a16db71354c 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -66,7 +66,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :grou
wait_for_requests
- expect(page).to have_css('.ci-status.ci-success', text: 'passed')
+ expect(page).to have_css('[data-testid="ci-badge-passed"]', text: 'passed')
end
it 'shows commit`s data', :js do
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index b8bb81991fc..70f9961ced8 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -120,7 +120,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
it 'indicates that pipeline can be canceled' do
expect(page).to have_selector('.js-pipelines-cancel-button')
- expect(page).to have_selector('.ci-running')
+ expect(page).to have_selector('[data-testid="ci-badge-running"]')
end
context 'when canceling' do
@@ -132,7 +132,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
it 'indicated that pipelines was canceled', :sidekiq_might_not_need_inline do
expect(page).not_to have_selector('.js-pipelines-cancel-button')
- expect(page).to have_selector('.ci-canceled')
+ expect(page).to have_selector('[data-testid="ci-badge-canceled"]')
end
end
end
@@ -150,7 +150,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
it 'indicates that pipeline can be retried' do
expect(page).to have_selector('.js-pipelines-retry-button')
- expect(page).to have_selector('.ci-failed')
+ expect(page).to have_selector('[data-testid="ci-badge-failed"]')
end
context 'when retrying' do
@@ -161,7 +161,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
it 'shows running pipeline that is not retryable' do
expect(page).not_to have_selector('.js-pipelines-retry-button')
- expect(page).to have_selector('.ci-running')
+ expect(page).to have_selector('[data-testid="ci-badge-running"]')
end
end
end
@@ -400,7 +400,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
end
it 'shows the pipeline as preparing' do
- expect(page).to have_selector('.ci-preparing')
+ expect(page).to have_selector('[data-testid="ci-badge-preparing"]')
end
end
@@ -421,7 +421,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
end
it 'has pipeline running' do
- expect(page).to have_selector('.ci-running')
+ expect(page).to have_selector('[data-testid="ci-badge-running"]')
end
context 'when canceling' do
@@ -432,7 +432,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
it 'indicates that pipeline was canceled', :sidekiq_might_not_need_inline do
expect(page).not_to have_selector('.js-pipelines-cancel-button')
- expect(page).to have_selector('.ci-canceled')
+ expect(page).to have_selector('[data-testid="ci-badge-canceled"]')
end
end
end
@@ -454,7 +454,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
end
it 'has failed pipeline', :sidekiq_might_not_need_inline do
- expect(page).to have_selector('.ci-failed')
+ expect(page).to have_selector('[data-testid="ci-badge-failed"]')
end
end
end
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
index 23b3b13d69c..9260718a94b 100644
--- a/spec/frontend/boards/components/board_content_spec.js
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -1,9 +1,11 @@
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
import Vue from 'vue';
import Draggable from 'vuedraggable';
import Vuex from 'vuex';
import eventHub from '~/boards/eventhub';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
@@ -11,9 +13,18 @@ import getters from 'ee_else_ce/boards/stores/getters';
import BoardColumn from '~/boards/components/board_column.vue';
import BoardContent from '~/boards/components/board_content.vue';
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
+import updateBoardListMutation from '~/boards/graphql/board_list_update.mutation.graphql';
import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
-import { mockLists, mockListsById } from '../mock_data';
+import { DraggableItemTypes } from 'ee_else_ce/boards/constants';
+import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
+import {
+ mockLists,
+ mockListsById,
+ updateBoardListResponse,
+ boardListsQueryResponse,
+} from '../mock_data';
+Vue.use(VueApollo);
Vue.use(Vuex);
const actions = {
@@ -22,6 +33,9 @@ const actions = {
describe('BoardContent', () => {
let wrapper;
+ let mockApollo;
+
+ const updateListHandler = jest.fn().mockResolvedValue(updateBoardListResponse);
const defaultState = {
isShowingEpicsSwimlanes: false,
@@ -47,21 +61,32 @@ describe('BoardContent', () => {
isIssueBoard = true,
isEpicBoard = false,
} = {}) => {
+ mockApollo = createMockApollo([[updateBoardListMutation, updateListHandler]]);
+ const listQueryVariables = { isProject: true };
+
+ mockApollo.clients.defaultClient.writeQuery({
+ query: boardListsQuery,
+ variables: listQueryVariables,
+ data: boardListsQueryResponse.data,
+ });
+
const store = createStore({
...defaultState,
...state,
});
wrapper = shallowMount(BoardContent, {
+ apolloProvider: mockApollo,
propsData: {
boardId: 'gid://gitlab/Board/1',
filterParams: {},
isSwimlanesOn: false,
boardListsApollo: mockListsById,
- listQueryVariables: {},
+ listQueryVariables,
addColumnFormVisible: false,
...props,
},
provide: {
+ boardType: 'project',
canAdminList,
issuableType,
isIssueBoard,
@@ -81,6 +106,7 @@ describe('BoardContent', () => {
const findBoardColumns = () => wrapper.findAllComponents(BoardColumn);
const findBoardAddNewColumn = () => wrapper.findComponent(BoardAddNewColumn);
+ const findDraggable = () => wrapper.findComponent(Draggable);
describe('default', () => {
beforeEach(() => {
@@ -128,7 +154,7 @@ describe('BoardContent', () => {
});
it('renders draggable component', () => {
- expect(wrapper.findComponent(Draggable).exists()).toBe(true);
+ expect(findDraggable().exists()).toBe(true);
});
});
@@ -138,7 +164,7 @@ describe('BoardContent', () => {
});
it('does not render draggable component', () => {
- expect(wrapper.findComponent(Draggable).exists()).toBe(false);
+ expect(findDraggable().exists()).toBe(false);
});
});
@@ -164,6 +190,21 @@ describe('BoardContent', () => {
expect(eventHub.$on).toHaveBeenCalledWith('updateBoard', wrapper.vm.refetchLists);
});
+
+ it('reorders lists', async () => {
+ const movableListsOrder = [mockLists[0].id, mockLists[1].id];
+
+ findDraggable().vm.$emit('end', {
+ item: { dataset: { listId: mockLists[0].id, draggableItemType: DraggableItemTypes.list } },
+ newIndex: 1,
+ to: {
+ children: movableListsOrder.map((listId) => ({ dataset: { listId } })),
+ },
+ });
+ await waitForPromises();
+
+ expect(updateListHandler).toHaveBeenCalled();
+ });
});
describe('when "add column" form is visible', () => {
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index 60f906d2157..68f665e004c 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -1023,6 +1023,7 @@ export const updateBoardListResponse = {
data: {
updateBoardList: {
list: mockList,
+ errors: [],
},
},
};
diff --git a/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
index cc4a022c2df..89ce3a2e18c 100644
--- a/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
@@ -1,5 +1,6 @@
+import Vue from 'vue';
import { GlAlert, GlButton, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
@@ -53,9 +54,6 @@ jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
}));
-const localVue = createLocalVue();
-localVue.use(VueApollo);
-
const defaultProvide = {
ciConfigPath: mockCiConfigPath,
defaultBranch: mockDefaultBranch,
@@ -74,24 +72,10 @@ describe('Pipeline editor app component', () => {
let mockLatestCommitShaQuery;
let mockPipelineQuery;
- const createComponent = ({
- blobLoading = false,
- options = {},
- provide = {},
- stubs = {},
- } = {}) => {
+ const createComponent = ({ options = {}, provide = {}, stubs = {} } = {}) => {
wrapper = shallowMount(PipelineEditorApp, {
provide: { ...defaultProvide, ...provide },
stubs,
- mocks: {
- $apollo: {
- queries: {
- initialCiFileContent: {
- loading: blobLoading,
- },
- },
- },
- },
...options,
});
};
@@ -101,6 +85,8 @@ describe('Pipeline editor app component', () => {
stubs = {},
withUndefinedBranch = false,
} = {}) => {
+ Vue.use(VueApollo);
+
const handlers = [
[getBlobContent, mockBlobContentData],
[getCiConfigData, mockCiConfigData],
@@ -137,7 +123,6 @@ describe('Pipeline editor app component', () => {
});
const options = {
- localVue,
mocks: {},
apolloProvider: mockApollo,
};
@@ -164,7 +149,7 @@ describe('Pipeline editor app component', () => {
describe('loading state', () => {
it('displays a loading icon if the blob query is loading', () => {
- createComponent({ blobLoading: true });
+ createComponentWithApollo();
expect(findLoadingIcon().exists()).toBe(true);
expect(findEditorHome().exists()).toBe(false);
@@ -246,10 +231,6 @@ describe('Pipeline editor app component', () => {
describe('when file exists', () => {
beforeEach(async () => {
await createComponentWithApollo();
-
- jest
- .spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
- .mockImplementation(jest.fn());
});
it('shows pipeline editor home component', () => {
@@ -268,8 +249,8 @@ describe('Pipeline editor app component', () => {
});
});
- it('does not poll for the commit sha', () => {
- expect(wrapper.vm.$apollo.queries.commitSha.startPolling).toHaveBeenCalledTimes(0);
+ it('calls once and does not start poll for the commit sha', () => {
+ expect(mockLatestCommitShaQuery).toHaveBeenCalledTimes(1);
});
});
@@ -281,10 +262,6 @@ describe('Pipeline editor app component', () => {
PipelineEditorEmptyState,
},
});
-
- jest
- .spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
- .mockImplementation(jest.fn());
});
it('shows an empty state and does not show editor home component', () => {
@@ -293,8 +270,8 @@ describe('Pipeline editor app component', () => {
expect(findEditorHome().exists()).toBe(false);
});
- it('does not poll for the commit sha', () => {
- expect(wrapper.vm.$apollo.queries.commitSha.startPolling).toHaveBeenCalledTimes(0);
+ it('calls once and does not start poll for the commit sha', () => {
+ expect(mockLatestCommitShaQuery).toHaveBeenCalledTimes(1);
});
describe('because of a fetching error', () => {
@@ -381,38 +358,27 @@ describe('Pipeline editor app component', () => {
});
it('polls for commit sha while pipeline data is not yet available for current branch', async () => {
- jest
- .spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
- .mockImplementation(jest.fn());
-
- // simulate a commit to the current branch
findEditorHome().vm.$emit('updateCommitSha');
await waitForPromises();
- expect(wrapper.vm.$apollo.queries.commitSha.startPolling).toHaveBeenCalledTimes(1);
+ expect(mockLatestCommitShaQuery).toHaveBeenCalledTimes(2);
});
it('stops polling for commit sha when pipeline data is available for newly committed branch', async () => {
- jest
- .spyOn(wrapper.vm.$apollo.queries.commitSha, 'stopPolling')
- .mockImplementation(jest.fn());
-
mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
- await wrapper.vm.$apollo.queries.commitSha.refetch();
+ await waitForPromises();
- expect(wrapper.vm.$apollo.queries.commitSha.stopPolling).toHaveBeenCalledTimes(1);
+ await findEditorHome().vm.$emit('updateCommitSha');
+
+ expect(mockLatestCommitShaQuery).toHaveBeenCalledTimes(2);
});
it('stops polling for commit sha when pipeline data is available for current branch', async () => {
- jest
- .spyOn(wrapper.vm.$apollo.queries.commitSha, 'stopPolling')
- .mockImplementation(jest.fn());
-
mockLatestCommitShaQuery.mockResolvedValue(mockNewCommitShaResults);
findEditorHome().vm.$emit('updateCommitSha');
await waitForPromises();
- expect(wrapper.vm.$apollo.queries.commitSha.stopPolling).toHaveBeenCalledTimes(1);
+ expect(mockLatestCommitShaQuery).toHaveBeenCalledTimes(2);
});
});
@@ -497,15 +463,12 @@ describe('Pipeline editor app component', () => {
it('refetches blob content', async () => {
await createComponentWithApollo();
- jest
- .spyOn(wrapper.vm.$apollo.queries.initialCiFileContent, 'refetch')
- .mockImplementation(jest.fn());
- expect(wrapper.vm.$apollo.queries.initialCiFileContent.refetch).toHaveBeenCalledTimes(0);
+ expect(mockBlobContentData).toHaveBeenCalledTimes(1);
- await wrapper.vm.refetchContent();
+ findEditorHome().vm.$emit('refetchContent');
- expect(wrapper.vm.$apollo.queries.initialCiFileContent.refetch).toHaveBeenCalledTimes(1);
+ expect(mockBlobContentData).toHaveBeenCalledTimes(2);
});
it('hides start screen when refetch fetches CI file', async () => {
@@ -516,7 +479,8 @@ describe('Pipeline editor app component', () => {
expect(findEditorHome().exists()).toBe(false);
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
- await wrapper.vm.$apollo.queries.initialCiFileContent.refetch();
+ findEmptyState().vm.$emit('refetchContent');
+ await waitForPromises();
expect(findEmptyState().exists()).toBe(false);
expect(findEditorHome().exists()).toBe(true);
@@ -573,10 +537,6 @@ describe('Pipeline editor app component', () => {
mockGetTemplate.mockResolvedValue(mockCiTemplateQueryResponse);
await createComponentWithApollo();
-
- jest
- .spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
- .mockImplementation(jest.fn());
});
it('skips empty state and shows editor home component', () => {
diff --git a/spec/frontend/ci/runner/components/runner_form_fields_spec.js b/spec/frontend/ci/runner/components/runner_form_fields_spec.js
index 98f170d8f18..93be4d9d35e 100644
--- a/spec/frontend/ci/runner/components/runner_form_fields_spec.js
+++ b/spec/frontend/ci/runner/components/runner_form_fields_spec.js
@@ -21,6 +21,7 @@ describe('RunnerFormFields', () => {
const findInput = (name) => wrapper.find(`input[name="${name}"]`);
const expectRendersFields = () => {
+ expect(wrapper.text()).toContain(s__('Runners|Tags'));
expect(wrapper.text()).toContain(s__('Runners|Details'));
expect(wrapper.text()).toContain(s__('Runners|Configuration'));
@@ -42,10 +43,11 @@ describe('RunnerFormFields', () => {
});
it('renders a loading frame', () => {
+ expect(wrapper.text()).toContain(s__('Runners|Tags'));
expect(wrapper.text()).toContain(s__('Runners|Details'));
expect(wrapper.text()).toContain(s__('Runners|Configuration'));
- expect(wrapper.findAllComponents(GlSkeletonLoader)).toHaveLength(2);
+ expect(wrapper.findAllComponents(GlSkeletonLoader)).toHaveLength(3);
expect(wrapper.findAll('input')).toHaveLength(0);
});
@@ -101,23 +103,23 @@ describe('RunnerFormFields', () => {
it('checks checkbox fields', async () => {
createComponent({
value: {
+ runUntagged: false,
paused: false,
accessLevel: ACCESS_LEVEL_NOT_PROTECTED,
- runUntagged: false,
},
});
+ findInput('run-untagged').setChecked(true);
findInput('paused').setChecked(true);
findInput('protected').setChecked(true);
- findInput('run-untagged').setChecked(true);
await nextTick();
expect(wrapper.emitted('input').at(-1)).toEqual([
{
+ runUntagged: true,
paused: true,
accessLevel: ACCESS_LEVEL_REF_PROTECTED,
- runUntagged: true,
},
]);
});
diff --git a/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
index 9d521b0b8ca..22797433b58 100644
--- a/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
+++ b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
@@ -1,27 +1,46 @@
import EMPTY_STATE_SVG_URL from '@gitlab/svgs/dist/illustrations/empty-state/empty-pipeline-md.svg?url';
import FILTERED_SVG_URL from '@gitlab/svgs/dist/illustrations/empty-state/empty-search-md.svg?url';
import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
-import { s__ } from '~/locale';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
+import {
+ I18N_GET_STARTED,
+ I18N_RUNNERS_ARE_AGENTS,
+ I18N_CREATE_RUNNER_LINK,
+ I18N_STILL_USING_REGISTRATION_TOKENS,
+ I18N_CONTACT_ADMIN_TO_REGISTER,
+ I18N_FOLLOW_REGISTRATION_INSTRUCTIONS,
+ I18N_NO_RESULTS,
+ I18N_EDIT_YOUR_SEARCH,
+} from '~/ci/runner/constants';
-import { mockRegistrationToken, newRunnerPath } from 'jest/ci/runner/mock_data';
+import {
+ mockRegistrationToken,
+ newRunnerPath as mockNewRunnerPath,
+} from 'jest/ci/runner/mock_data';
import RunnerListEmptyState from '~/ci/runner/components/runner_list_empty_state.vue';
describe('RunnerListEmptyState', () => {
let wrapper;
+ let glFeatures;
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findLinks = () => wrapper.findAllComponents(GlLink);
const findLink = () => wrapper.findComponent(GlLink);
const findRunnerInstructionsModal = () => wrapper.findComponent(RunnerInstructionsModal);
- const createComponent = ({ props, mountFn = shallowMountExtended, ...options } = {}) => {
+ const expectTitleToBe = (title) => {
+ expect(findEmptyState().find('h1').text()).toBe(title);
+ };
+ const expectDescriptionToBe = (sentences) => {
+ expect(findEmptyState().find('p').text()).toMatchInterpolatedText(sentences.join(' '));
+ };
+
+ const createComponent = ({ props, mountFn = shallowMountExtended } = {}) => {
wrapper = mountFn(RunnerListEmptyState, {
propsData: {
- registrationToken: mockRegistrationToken,
- newRunnerPath,
...props,
},
directives: {
@@ -30,109 +49,146 @@ describe('RunnerListEmptyState', () => {
stubs: {
GlEmptyState,
GlSprintf,
- GlLink,
},
- ...options,
+ provide: { glFeatures },
});
};
+ beforeEach(() => {
+ glFeatures = null;
+ });
+
describe('when search is not filtered', () => {
- const title = s__('Runners|Get started with runners');
-
- describe('when there is a registration token', () => {
+ describe.each([
+ { createRunnerWorkflowForAdmin: true },
+ { createRunnerWorkflowForNamespace: true },
+ ])('when createRunnerWorkflow is enabled by %o', (currentGlFeatures) => {
beforeEach(() => {
- createComponent();
+ glFeatures = currentGlFeatures;
});
- it('renders an illustration', () => {
- expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL);
- });
-
- it('displays "no results" text with instructions', () => {
- const desc = s__(
- 'Runners|Runners are the agents that run your CI/CD jobs. Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner.',
- );
-
- expect(findEmptyState().text()).toMatchInterpolatedText(`${title} ${desc}`);
- });
-
- describe.each([
- { createRunnerWorkflowForAdmin: true },
- { createRunnerWorkflowForNamespace: true },
- ])('when %o', (glFeatures) => {
- describe('when newRunnerPath is defined', () => {
- beforeEach(() => {
- createComponent({
- provide: {
- glFeatures,
- },
- });
- });
-
- it('shows a link to the new runner page', () => {
- expect(findLink().attributes('href')).toBe(newRunnerPath);
- });
- });
-
- describe('when newRunnerPath not defined', () => {
+ describe.each`
+ newRunnerPath | registrationToken | expectedMessages
+ ${mockNewRunnerPath} | ${mockRegistrationToken} | ${[I18N_CREATE_RUNNER_LINK, I18N_STILL_USING_REGISTRATION_TOKENS]}
+ ${mockNewRunnerPath} | ${null} | ${[I18N_CREATE_RUNNER_LINK]}
+ ${null} | ${mockRegistrationToken} | ${[I18N_STILL_USING_REGISTRATION_TOKENS]}
+ ${null} | ${null} | ${[I18N_CONTACT_ADMIN_TO_REGISTER]}
+ `(
+ 'when newRunnerPath is $newRunnerPath and registrationToken is $registrationToken',
+ ({ newRunnerPath, registrationToken, expectedMessages }) => {
beforeEach(() => {
createComponent({
props: {
- newRunnerPath: null,
- },
- provide: {
- glFeatures,
+ newRunnerPath,
+ registrationToken,
},
});
});
- it('opens a runner registration instructions modal with a link', () => {
- const { value } = getBinding(findLink().element, 'gl-modal');
-
- expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ it('shows title', () => {
+ expectTitleToBe(I18N_GET_STARTED);
});
+
+ it('renders an illustration', () => {
+ expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL);
+ });
+
+ it(`shows description: "${expectedMessages.join(' ')}"`, () => {
+ expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, ...expectedMessages]);
+ });
+ },
+ );
+
+ describe('with newRunnerPath and registration token', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ registrationToken: mockRegistrationToken,
+ newRunnerPath: mockNewRunnerPath,
+ },
+ });
+ });
+
+ it('shows links to the new runner page and registration instructions', () => {
+ expect(findLinks().at(0).attributes('href')).toBe(mockNewRunnerPath);
+
+ const { value } = getBinding(findLinks().at(1).element, 'gl-modal');
+ expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
});
});
- describe.each([
- { createRunnerWorkflowForAdmin: false },
- { createRunnerWorkflowForNamespace: false },
- ])('when %o', (glFeatures) => {
+ describe('with newRunnerPath and no registration token', () => {
beforeEach(() => {
createComponent({
- provide: {
- glFeatures,
+ props: {
+ registrationToken: mockRegistrationToken,
+ newRunnerPath: null,
},
});
});
it('opens a runner registration instructions modal with a link', () => {
const { value } = getBinding(findLink().element, 'gl-modal');
-
expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
});
});
+
+ describe('with no newRunnerPath nor registration token', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ registrationToken: null,
+ newRunnerPath: null,
+ },
+ });
+ });
+
+ it('has no link', () => {
+ expect(findLink().exists()).toBe(false);
+ });
+ });
});
- describe('when there is no registration token', () => {
- beforeEach(() => {
- createComponent({ props: { registrationToken: null } });
+ describe('when createRunnerWorkflow is disabled', () => {
+ describe('when there is a registration token', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ registrationToken: mockRegistrationToken,
+ },
+ });
+ });
+
+ it('renders an illustration', () => {
+ expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL);
+ });
+
+ it('opens a runner registration instructions modal with a link', () => {
+ const { value } = getBinding(findLink().element, 'gl-modal');
+ expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ });
+
+ it('displays text with registration instructions', () => {
+ expectTitleToBe(I18N_GET_STARTED);
+
+ expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, I18N_FOLLOW_REGISTRATION_INSTRUCTIONS]);
+ });
});
- it('renders an illustration', () => {
- expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL);
- });
+ describe('when there is no registration token', () => {
+ beforeEach(() => {
+ createComponent({ props: { registrationToken: null } });
+ });
- it('displays "no results" text', () => {
- const desc = s__(
- 'Runners|Runners are the agents that run your CI/CD jobs. To register new runners, please contact your administrator.',
- );
+ it('displays "contact admin" text', () => {
+ expectTitleToBe(I18N_GET_STARTED);
- expect(findEmptyState().text()).toMatchInterpolatedText(`${title} ${desc}`);
- });
+ expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, I18N_CONTACT_ADMIN_TO_REGISTER]);
+ });
- it('has no registration instructions link', () => {
- expect(findLink().exists()).toBe(false);
+ it('has no registration instructions link', () => {
+ expect(findLink().exists()).toBe(false);
+ });
});
});
});
@@ -147,8 +203,9 @@ describe('RunnerListEmptyState', () => {
});
it('displays "no filtered results" text', () => {
- expect(findEmptyState().text()).toContain(s__('Runners|No results found'));
- expect(findEmptyState().text()).toContain(s__('Runners|Edit your search and try again'));
+ expectTitleToBe(I18N_NO_RESULTS);
+
+ expectDescriptionToBe([I18N_EDIT_YOUR_SEARCH]);
});
});
});
diff --git a/spec/frontend/commit/components/refs_list_spec.js b/spec/frontend/commit/components/refs_list_spec.js
index 594f8827d58..cc783dc3b58 100644
--- a/spec/frontend/commit/components/refs_list_spec.js
+++ b/spec/frontend/commit/components/refs_list_spec.js
@@ -61,7 +61,7 @@ describe('Commit references component', () => {
it('renders links to refs', () => {
const index = 0;
const refBadge = findTippingRefs().at(index);
- const refUrl = `${refsListPropsMock.urlPart}${refsListPropsMock.tippingRefs[index]}`;
+ const refUrl = `${refsListPropsMock.urlPart}${refsListPropsMock.tippingRefs[index]}?ref_type=${refsListPropsMock.refType}`;
expect(refBadge.attributes('href')).toBe(refUrl);
});
diff --git a/spec/frontend/commit/mock_data.js b/spec/frontend/commit/mock_data.js
index 9c8f9266986..2a618e08c50 100644
--- a/spec/frontend/commit/mock_data.js
+++ b/spec/frontend/commit/mock_data.js
@@ -289,4 +289,5 @@ export const refsListPropsMock = {
tippingRefs: tippingBranchesMock,
isLoading: false,
urlPart: '/some/project/-/commits/',
+ refType: 'heads',
};
diff --git a/spec/frontend/fixtures/pipeline_header.rb b/spec/frontend/fixtures/pipeline_header.rb
index a4fba7e8675..d25bf12623f 100644
--- a/spec/frontend/fixtures/pipeline_header.rb
+++ b/spec/frontend/fixtures/pipeline_header.rb
@@ -51,6 +51,8 @@ RSpec.describe "GraphQL Pipeline Header", '(JavaScript fixtures)', type: :reques
)
end
+ let_it_be(:build) { create(:ci_build, :running, pipeline: pipeline, ref: 'master') }
+
it "graphql/pipelines/pipeline_header_running.json" do
query = get_graphql_query_as_string(query_path)
@@ -59,4 +61,29 @@ RSpec.describe "GraphQL Pipeline Header", '(JavaScript fixtures)', type: :reques
expect_graphql_errors_to_be_empty
end
end
+
+ context 'with failed pipeline' do
+ let_it_be(:pipeline) do
+ create(
+ :ci_pipeline,
+ project: project,
+ sha: commit.id,
+ ref: 'master',
+ user: user,
+ status: :failed,
+ started_at: 1.hour.ago,
+ finished_at: Time.current
+ )
+ end
+
+ let_it_be(:build) { create(:ci_build, :canceled, pipeline: pipeline, ref: 'master') }
+
+ it "graphql/pipelines/pipeline_header_failed.json" do
+ query = get_graphql_query_as_string(query_path)
+
+ post_graphql(query, current_user: user, variables: { fullPath: project.full_path, iid: pipeline.iid })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
end
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
index 5c36dbf9c9c..2b60684e60a 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
@@ -1,22 +1,37 @@
-import { GlAlert, GlDropdown, GlButton, GlFormCheckbox, GlLoadingIcon } from '@gitlab/ui';
+import { GlAlert, GlDropdown, GlButton, GlFormCheckbox, GlLoadingIcon, GlModal } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { stubComponent } from 'helpers/stub_component';
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import Tracking from '~/tracking';
import { s__ } from '~/locale';
+import { createAlert } from '~/alert';
import {
packageFiles as packageFilesMock,
packageFilesQuery,
+ packageDestroyFilesMutation,
+ packageDestroyFilesMutationError,
} from 'jest/packages_and_registries/package_registry/mock_data';
+import {
+ DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION,
+ DELETE_ALL_PACKAGE_FILES_MODAL_CONTENT,
+ DELETE_LAST_PACKAGE_FILE_MODAL_CONTENT,
+ DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
+ DELETE_PACKAGE_FILE_ERROR_MESSAGE,
+ DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
+ DELETE_PACKAGE_FILES_ERROR_MESSAGE,
+} from '~/packages_and_registries/package_registry/constants';
import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import getPackageFiles from '~/packages_and_registries/package_registry/graphql/queries/get_package_files.query.graphql';
+import destroyPackageFilesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql';
Vue.use(VueApollo);
+jest.mock('~/alert');
describe('Package Files', () => {
let wrapper;
@@ -24,6 +39,7 @@ describe('Package Files', () => {
const findAllRows = () => wrapper.findAllByTestId('file-row');
const findDeleteSelectedButton = () => wrapper.findByTestId('delete-selected');
+ const findDeleteFilesModal = () => wrapper.findByTestId('delete-files-modal');
const findFirstRow = () => extendedWrapper(findAllRows().at(0));
const findSecondRow = () => extendedWrapper(findAllRows().at(1));
const findPackageFilesAlert = () => wrapper.findComponent(GlAlert);
@@ -41,27 +57,39 @@ describe('Package Files', () => {
const files = packageFilesMock();
const [file] = files;
+ const showMock = jest.fn();
+ const eventCategory = 'UI::NpmPackages';
+
const createComponent = ({
packageId = '1',
packageType = 'NPM',
- isLoading = false,
+ projectPath = 'gitlab-test',
canDelete = true,
stubs,
- resolver = jest.fn().mockResolvedValue(packageFilesQuery([file])),
+ resolver = jest.fn().mockResolvedValue(packageFilesQuery({ files: [file] })),
+ filesDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFilesMutation()),
} = {}) => {
- const requestHandlers = [[getPackageFiles, resolver]];
+ const requestHandlers = [
+ [getPackageFiles, resolver],
+ [destroyPackageFilesMutation, filesDeleteMutationResolver],
+ ];
apolloProvider = createMockApollo(requestHandlers);
wrapper = mountExtended(PackageFiles, {
apolloProvider,
propsData: {
canDelete,
- isLoading,
packageId,
packageType,
+ projectPath,
},
stubs: {
GlTable: false,
+ GlModal: stubComponent(GlModal, {
+ methods: {
+ show: showMock,
+ },
+ }),
...stubs,
},
});
@@ -122,10 +150,16 @@ describe('Package Files', () => {
expect(findFirstRowDownloadLink().attributes('href')).toBe(file.downloadPath);
});
- it('emits "download-file" event on click', () => {
+ it('tracks "download-file" event on click', () => {
+ const eventSpy = jest.spyOn(Tracking, 'event');
+
findFirstRowDownloadLink().vm.$emit('click');
- expect(wrapper.emitted('download-file')).toEqual([[]]);
+ expect(eventSpy).toHaveBeenCalledWith(
+ eventCategory,
+ DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION,
+ expect.any(Object),
+ );
});
});
@@ -179,12 +213,14 @@ describe('Package Files', () => {
expect(findActionMenuDelete().exists()).toBe(true);
});
- it('emits a delete event when clicked', async () => {
+ it('shows delete file confirmation modal', async () => {
await findActionMenuDelete().trigger('click');
- const [[items]] = wrapper.emitted('delete-files');
- const [{ id }] = items;
- expect(id).toBe(file.id);
+ expect(showMock).toHaveBeenCalledTimes(1);
+
+ expect(findDeleteFilesModal().text()).toBe(
+ 'You are about to delete foo-1.0.1.tgz. This is a destructive action that may render your package unusable. Are you sure?',
+ );
});
});
});
@@ -213,21 +249,6 @@ describe('Package Files', () => {
expect(findDeleteSelectedButton().props('disabled')).toBe(true);
});
- it('delete selected button exists & is disabled when isLoading prop is true', async () => {
- createComponent();
- await waitForPromises();
- const first = findAllRowCheckboxes().at(0);
-
- await first.setChecked(true);
-
- expect(findDeleteSelectedButton().props('disabled')).toBe(false);
-
- await wrapper.setProps({ isLoading: true });
-
- expect(findDeleteSelectedButton().props('disabled')).toBe(true);
- expect(findLoadingIcon().exists()).toBe(true);
- });
-
it('checkboxes to select file are visible', async () => {
createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery()) });
await waitForPromises();
@@ -295,7 +316,7 @@ describe('Package Files', () => {
});
});
- it('emits a delete event when selected', async () => {
+ it('shows delete modal with single file confirmation text when delete selected is clicked', async () => {
createComponent();
await waitForPromises();
@@ -305,12 +326,14 @@ describe('Package Files', () => {
await findDeleteSelectedButton().trigger('click');
- const [[items]] = wrapper.emitted('delete-files');
- const [{ id }] = items;
- expect(id).toBe(file.id);
+ expect(showMock).toHaveBeenCalledTimes(1);
+
+ expect(findDeleteFilesModal().text()).toBe(
+ 'You are about to delete foo-1.0.1.tgz. This is a destructive action that may render your package unusable. Are you sure?',
+ );
});
- it('emits delete event with both items when all are selected', async () => {
+ it('shows delete modal with multiple files confirmation text when delete selected is clicked', async () => {
createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery()) });
await waitForPromises();
@@ -318,8 +341,63 @@ describe('Package Files', () => {
await findDeleteSelectedButton().trigger('click');
- const [[items]] = wrapper.emitted('delete-files');
- expect(items).toHaveLength(2);
+ expect(showMock).toHaveBeenCalledTimes(1);
+
+ expect(findDeleteFilesModal().text()).toMatchInterpolatedText(
+ 'You are about to delete 2 assets. This operation is irreversible.',
+ );
+ });
+
+ describe('emits delete-all-files event', () => {
+ it('with right content for last file in package', async () => {
+ createComponent({
+ resolver: jest.fn().mockResolvedValue(
+ packageFilesQuery({
+ files: [file],
+ pageInfo: {
+ hasNextPage: false,
+ },
+ }),
+ ),
+ });
+ await waitForPromises();
+ const first = findAllRowCheckboxes().at(0);
+
+ await first.setChecked(true);
+
+ await findDeleteSelectedButton().trigger('click');
+
+ expect(showMock).toHaveBeenCalledTimes(0);
+
+ expect(wrapper.emitted('delete-all-files')).toHaveLength(1);
+ expect(wrapper.emitted('delete-all-files')[0]).toEqual([
+ DELETE_LAST_PACKAGE_FILE_MODAL_CONTENT,
+ ]);
+ });
+
+ it('with right content for all files in package', async () => {
+ createComponent({
+ resolver: jest.fn().mockResolvedValue(
+ packageFilesQuery({
+ pageInfo: {
+ hasNextPage: false,
+ },
+ }),
+ ),
+ });
+ await waitForPromises();
+
+ await findCheckAllCheckbox().setChecked(true);
+
+ await findDeleteSelectedButton().trigger('click');
+
+ expect(showMock).toHaveBeenCalledTimes(0);
+
+ expect(wrapper.emitted('delete-all-files')).toHaveLength(1);
+ expect(wrapper.emitted('delete-all-files')[0]).toEqual([
+ DELETE_ALL_PACKAGE_FILES_MODAL_CONTENT,
+ ]);
+ });
});
});
@@ -343,6 +421,195 @@ describe('Package Files', () => {
});
});
+ describe('deleting a file', () => {
+ const doDeleteFile = async () => {
+ const first = findAllRowCheckboxes().at(0);
+
+ await first.setChecked(true);
+
+ await findDeleteSelectedButton().trigger('click');
+
+ findDeleteFilesModal().vm.$emit('primary');
+ };
+
+ it('confirming on the modal sets the loading state', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ await doDeleteFile();
+
+ await nextTick();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('confirming on the modal deletes the file and shows a success message', async () => {
+ const resolver = jest.fn().mockResolvedValue(packageFilesQuery({ files: [file] }));
+ const filesDeleteMutationResolver = jest
+ .fn()
+ .mockResolvedValue(packageDestroyFilesMutation());
+ createComponent({ resolver, filesDeleteMutationResolver });
+
+ await waitForPromises();
+
+ await doDeleteFile();
+
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
+ }),
+ );
+
+ expect(filesDeleteMutationResolver).toHaveBeenCalledWith({
+ ids: [file.id],
+ projectPath: 'gitlab-test',
+ });
+
+ // we are re-fetching the package files, so we expect the resolver to have been called twice
+ expect(resolver).toHaveBeenCalledTimes(2);
+ expect(resolver).toHaveBeenCalledWith({
+ id: '1',
+ first: 100,
+ });
+ });
+
+ describe('errors', () => {
+ it('shows an error when the mutation request fails', async () => {
+ createComponent({ filesDeleteMutationResolver: jest.fn().mockRejectedValue() });
+ await waitForPromises();
+
+ await doDeleteFile();
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: DELETE_PACKAGE_FILE_ERROR_MESSAGE,
+ }),
+ );
+ });
+
+ it('shows an error when the mutation request returns an error payload', async () => {
+ createComponent({
+ filesDeleteMutationResolver: jest
+ .fn()
+ .mockResolvedValue(packageDestroyFilesMutationError()),
+ });
+ await waitForPromises();
+
+ await doDeleteFile();
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: DELETE_PACKAGE_FILE_ERROR_MESSAGE,
+ }),
+ );
+ });
+ });
+ });
+
+ describe('deleting multiple files', () => {
+ const doDeleteFiles = async () => {
+ await findCheckAllCheckbox().setChecked(true);
+
+ await findDeleteSelectedButton().trigger('click');
+
+ findDeleteFilesModal().vm.$emit('primary');
+ };
+
+ it('confirming on the modal sets the loading state', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ await doDeleteFiles();
+
+ await nextTick();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('confirming on the modal deletes the file and shows a success message', async () => {
+ const resolver = jest.fn().mockResolvedValue(packageFilesQuery());
+ const filesDeleteMutationResolver = jest
+ .fn()
+ .mockResolvedValue(packageDestroyFilesMutation());
+ createComponent({ resolver, filesDeleteMutationResolver });
+
+ await waitForPromises();
+
+ await doDeleteFiles();
+
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
+ }),
+ );
+
+ expect(filesDeleteMutationResolver).toHaveBeenCalledWith({
+ ids: files.map(({ id }) => id),
+ projectPath: 'gitlab-test',
+ });
+
+ // we are re-fetching the package files, so we expect the resolver to have been called twice
+ expect(resolver).toHaveBeenCalledTimes(2);
+ expect(resolver).toHaveBeenCalledWith({
+ id: '1',
+ first: 100,
+ });
+ });
+
+ describe('errors', () => {
+ it('shows an error when the mutation request fails', async () => {
+ const resolver = jest.fn().mockResolvedValue(packageFilesQuery());
+ createComponent({ filesDeleteMutationResolver: jest.fn().mockRejectedValue(), resolver });
+ await waitForPromises();
+
+ await doDeleteFiles();
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: DELETE_PACKAGE_FILES_ERROR_MESSAGE,
+ }),
+ );
+ });
+
+ it('shows an error when the mutation request returns an error payload', async () => {
+ const resolver = jest.fn().mockResolvedValue(packageFilesQuery());
+ createComponent({
+ filesDeleteMutationResolver: jest
+ .fn()
+ .mockResolvedValue(packageDestroyFilesMutationError()),
+ resolver,
+ });
+ await waitForPromises();
+
+ await doDeleteFiles();
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: DELETE_PACKAGE_FILES_ERROR_MESSAGE,
+ }),
+ );
+ });
+ });
+ });
+
describe('additional details', () => {
describe('details toggle button', () => {
it('exists', async () => {
@@ -357,7 +624,9 @@ describe('Package Files', () => {
noShaFile.fileSha256 = null;
noShaFile.fileMd5 = null;
noShaFile.fileSha1 = null;
- createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery([noShaFile])) });
+ createComponent({
+ resolver: jest.fn().mockResolvedValue(packageFilesQuery({ files: [noShaFile] })),
+ });
await waitForPromises();
expect(findFirstToggleDetailsButton().exists()).toBe(false);
@@ -410,7 +679,9 @@ describe('Package Files', () => {
const { ...missingMd5 } = file;
missingMd5.fileMd5 = null;
- createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery([missingMd5])) });
+ createComponent({
+ resolver: jest.fn().mockResolvedValue(packageFilesQuery({ files: [missingMd5] })),
+ });
await waitForPromises();
await showShaFiles();
diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js
index fa6a69b1a1f..f1dab38a9e6 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -254,9 +254,6 @@ export const packageDetailsQuery = ({
__typename: 'PipelineConnection',
},
packageFiles: {
- pageInfo: {
- hasNextPage: true,
- },
nodes: packageFiles().map(({ id, size }) => ({ id, size })),
__typename: 'PackageFileConnection',
},
@@ -285,11 +282,15 @@ export const packagePipelinesQuery = (pipelines = packagePipelines()) => ({
},
});
-export const packageFilesQuery = (files = packageFiles()) => ({
+export const packageFilesQuery = ({ files = packageFiles(), pageInfo = {} } = {}) => ({
data: {
package: {
id: 'gid://gitlab/Packages::Package/111',
packageFiles: {
+ pageInfo: {
+ hasNextPage: true,
+ ...pageInfo,
+ },
nodes: files,
__typename: 'PackageFileConnection',
},
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
index 8b15dfd7d4a..0f91a7aeb50 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
@@ -21,10 +21,7 @@ import {
REQUEST_FORWARDING_HELP_PAGE_PATH,
FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
PACKAGE_TYPE_COMPOSER,
- DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
- DELETE_PACKAGE_FILE_ERROR_MESSAGE,
- DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
- DELETE_PACKAGE_FILES_ERROR_MESSAGE,
+ DELETE_ALL_PACKAGE_FILES_MODAL_CONTENT,
PACKAGE_TYPE_NUGET,
PACKAGE_TYPE_MAVEN,
PACKAGE_TYPE_CONAN,
@@ -32,7 +29,6 @@ import {
PACKAGE_TYPE_NPM,
} from '~/packages_and_registries/package_registry/constants';
-import destroyPackageFilesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql';
import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql';
import getPackageVersionsQuery from '~/packages_and_registries/package_registry/graphql//queries/get_package_versions.query.graphql';
import {
@@ -41,9 +37,6 @@ import {
packageVersions,
dependencyLinks,
emptyPackageDetailsQuery,
- packageFiles,
- packageDestroyFilesMutation,
- packageDestroyFilesMutationError,
defaultPackageGroupSettings,
} from '../mock_data';
@@ -74,13 +67,9 @@ describe('PackagesApp', () => {
function createComponent({
resolver = jest.fn().mockResolvedValue(packageDetailsQuery()),
- filesDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFilesMutation()),
routeId = '1',
} = {}) {
- const requestHandlers = [
- [getPackageDetails, resolver],
- [destroyPackageFilesMutation, filesDeleteMutationResolver],
- ];
+ const requestHandlers = [[getPackageDetails, resolver]];
apolloProvider = createMockApollo(requestHandlers);
wrapper = shallowMountExtended(PackagesApp, {
@@ -117,8 +106,6 @@ describe('PackagesApp', () => {
const findDeleteModal = () => wrapper.findByTestId('delete-modal');
const findDeleteButton = () => wrapper.findByTestId('delete-package');
const findPackageFiles = () => wrapper.findComponent(PackageFiles);
- const findDeleteFileModal = () => wrapper.findByTestId('delete-file-modal');
- const findDeleteFilesModal = () => wrapper.findByTestId('delete-files-modal');
const findVersionsList = () => wrapper.findComponent(PackageVersionsList);
const findVersionsCountBadge = () => wrapper.findByTestId('other-versions-badge');
const findNoVersionsMessage = () => wrapper.findByTestId('no-versions-message');
@@ -336,9 +323,9 @@ describe('PackagesApp', () => {
expect(findPackageFiles().props()).toMatchObject({
canDelete: packageData().canDestroy,
- isLoading: false,
packageId: packageData().id,
packageType: packageData().packageType,
+ projectPath: 'gitlab-test',
});
});
@@ -356,250 +343,26 @@ describe('PackagesApp', () => {
expect(findPackageFiles().exists()).toBe(false);
});
- describe('deleting a file', () => {
- const [fileToDelete] = packageFiles();
-
- const doDeleteFile = () => {
- findPackageFiles().vm.$emit('delete-files', [fileToDelete]);
-
- findDeleteFileModal().vm.$emit('primary');
-
- return waitForPromises();
- };
-
- it('opens delete file confirmation modal', async () => {
- createComponent();
-
- await waitForPromises();
-
- findPackageFiles().vm.$emit('delete-files', [fileToDelete]);
-
- expect(showMock).toHaveBeenCalledTimes(1);
-
- await waitForPromises();
-
- expect(findDeleteFileModal().text()).toBe(
- 'You are about to delete foo-1.0.1.tgz. This is a destructive action that may render your package unusable. Are you sure?',
- );
- });
-
- it('when its the only file opens delete package confirmation modal', async () => {
- const [packageFile] = packageFiles();
+ describe('emits delete-all-files event', () => {
+ it('opens the delete package confirmation modal and shows confirmation text', async () => {
const resolver = jest.fn().mockResolvedValue(
packageDetailsQuery({
- extendPackage: {
- packageFiles: {
- pageInfo: {
- hasNextPage: false,
- },
- nodes: [packageFile],
- __typename: 'PackageFileConnection',
- },
- },
+ extendPackage: {},
packageSettings: {
...defaultPackageGroupSettings,
npmPackageRequestsForwarding: false,
},
}),
);
-
- createComponent({
- resolver,
- });
-
- await waitForPromises();
-
- findPackageFiles().vm.$emit('delete-files', [fileToDelete]);
-
- expect(showMock).toHaveBeenCalledTimes(1);
-
- await waitForPromises();
-
- expect(findDeleteModal().text()).toBe(
- 'Deleting the last package asset will remove version 1.0.0 of @gitlab-org/package-15. Are you sure?',
- );
- });
-
- it('confirming on the modal sets the loading state', async () => {
- createComponent();
-
- await waitForPromises();
-
- findPackageFiles().vm.$emit('delete-files', [fileToDelete]);
-
- findDeleteFileModal().vm.$emit('primary');
-
- await nextTick();
-
- expect(findPackageFiles().props('isLoading')).toEqual(true);
- });
-
- it('confirming on the modal deletes the file and shows a success message', async () => {
- const resolver = jest.fn().mockResolvedValue(packageDetailsQuery());
createComponent({ resolver });
await waitForPromises();
- await doDeleteFile();
-
- expect(createAlert).toHaveBeenCalledWith(
- expect.objectContaining({
- message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
- }),
- );
- // we are re-fetching the package details, so we expect the resolver to have been called twice
- expect(resolver).toHaveBeenCalledTimes(2);
- });
-
- describe('errors', () => {
- it('shows an error when the mutation request fails', async () => {
- createComponent({ filesDeleteMutationResolver: jest.fn().mockRejectedValue() });
- await waitForPromises();
-
- await doDeleteFile();
-
- expect(createAlert).toHaveBeenCalledWith(
- expect.objectContaining({
- message: DELETE_PACKAGE_FILE_ERROR_MESSAGE,
- }),
- );
- });
-
- it('shows an error when the mutation request returns an error payload', async () => {
- createComponent({
- filesDeleteMutationResolver: jest
- .fn()
- .mockResolvedValue(packageDestroyFilesMutationError()),
- });
- await waitForPromises();
-
- await doDeleteFile();
-
- expect(createAlert).toHaveBeenCalledWith(
- expect.objectContaining({
- message: DELETE_PACKAGE_FILE_ERROR_MESSAGE,
- }),
- );
- });
- });
- });
-
- describe('deleting multiple files', () => {
- const doDeleteFiles = () => {
- findPackageFiles().vm.$emit('delete-files', packageFiles());
-
- findDeleteFilesModal().vm.$emit('primary');
-
- return waitForPromises();
- };
-
- it('opens delete files confirmation modal', async () => {
- createComponent();
-
- await waitForPromises();
-
- const showDeleteFilesSpy = jest.spyOn(wrapper.vm.$refs.deleteFilesModal, 'show');
-
- findPackageFiles().vm.$emit('delete-files', packageFiles());
-
- expect(showDeleteFilesSpy).toHaveBeenCalled();
- });
-
- it('confirming on the modal sets the loading state', async () => {
- createComponent();
-
- await waitForPromises();
-
- findPackageFiles().vm.$emit('delete-files', packageFiles());
-
- findDeleteFilesModal().vm.$emit('primary');
-
- await nextTick();
-
- expect(findPackageFiles().props('isLoading')).toEqual(true);
- });
-
- it('confirming on the modal deletes the file and shows a success message', async () => {
- const resolver = jest.fn().mockResolvedValue(packageDetailsQuery());
- createComponent({ resolver });
-
- await waitForPromises();
-
- await doDeleteFiles();
-
- expect(resolver).toHaveBeenCalledTimes(2);
-
- expect(createAlert).toHaveBeenCalledWith(
- expect.objectContaining({
- message: DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
- }),
- );
- // we are re-fetching the package details, so we expect the resolver to have been called twice
- expect(resolver).toHaveBeenCalledTimes(2);
- });
-
- describe('errors', () => {
- it('shows an error when the mutation request fails', async () => {
- createComponent({ filesDeleteMutationResolver: jest.fn().mockRejectedValue() });
- await waitForPromises();
-
- await doDeleteFiles();
-
- expect(createAlert).toHaveBeenCalledWith(
- expect.objectContaining({
- message: DELETE_PACKAGE_FILES_ERROR_MESSAGE,
- }),
- );
- });
-
- it('shows an error when the mutation request returns an error payload', async () => {
- createComponent({
- filesDeleteMutationResolver: jest
- .fn()
- .mockResolvedValue(packageDestroyFilesMutationError()),
- });
- await waitForPromises();
-
- await doDeleteFiles();
-
- expect(createAlert).toHaveBeenCalledWith(
- expect.objectContaining({
- message: DELETE_PACKAGE_FILES_ERROR_MESSAGE,
- }),
- );
- });
- });
- });
-
- describe('deleting all files', () => {
- it('opens the delete package confirmation modal', async () => {
- const resolver = jest.fn().mockResolvedValue(
- packageDetailsQuery({
- extendPackage: {
- packageFiles: {
- pageInfo: {
- hasNextPage: false,
- },
- nodes: packageFiles(),
- },
- },
- packageSettings: {
- ...defaultPackageGroupSettings,
- npmPackageRequestsForwarding: false,
- },
- }),
- );
- createComponent({
- resolver,
- });
-
- await waitForPromises();
-
- findPackageFiles().vm.$emit('delete-files', packageFiles());
+ findPackageFiles().vm.$emit('delete-all-files', DELETE_ALL_PACKAGE_FILES_MODAL_CONTENT);
expect(showMock).toHaveBeenCalledTimes(1);
- await waitForPromises();
+ await nextTick();
expect(findDeleteModal().text()).toBe(
'Deleting all package assets will remove version 1.0.0 of @gitlab-org/package-15. Are you sure?',
diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js
index fd654eb6f10..8bbe0ef78c0 100644
--- a/spec/frontend/pipelines/mock_data.js
+++ b/spec/frontend/pipelines/mock_data.js
@@ -1,5 +1,6 @@
import pipelineHeaderSuccess from 'test_fixtures/graphql/pipelines/pipeline_header_success.json';
import pipelineHeaderRunning from 'test_fixtures/graphql/pipelines/pipeline_header_running.json';
+import pipelineHeaderFailed from 'test_fixtures/graphql/pipelines/pipeline_header_failed.json';
const PIPELINE_RUNNING = 'RUNNING';
const PIPELINE_CANCELED = 'CANCELED';
@@ -8,7 +9,31 @@ const PIPELINE_FAILED = 'FAILED';
const threeWeeksAgo = new Date();
threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
-export { pipelineHeaderSuccess, pipelineHeaderRunning };
+export { pipelineHeaderSuccess, pipelineHeaderRunning, pipelineHeaderFailed };
+
+export const pipelineRetryMutationResponseSuccess = {
+ data: { pipelineRetry: { errors: [] } },
+};
+
+export const pipelineRetryMutationResponseFailed = {
+ data: { pipelineRetry: { errors: ['error'] } },
+};
+
+export const pipelineCancelMutationResponseSuccess = {
+ data: { pipelineRetry: { errors: [] } },
+};
+
+export const pipelineCancelMutationResponseFailed = {
+ data: { pipelineRetry: { errors: ['error'] } },
+};
+
+export const pipelineDeleteMutationResponseSuccess = {
+ data: { pipelineRetry: { errors: [] } },
+};
+
+export const pipelineDeleteMutationResponseFailed = {
+ data: { pipelineRetry: { errors: ['error'] } },
+};
export const mockPipelineHeader = {
detailedStatus: {},
diff --git a/spec/frontend/pipelines/pipeline_details_header_spec.js b/spec/frontend/pipelines/pipeline_details_header_spec.js
index 08ae35fe808..7141e10fb17 100644
--- a/spec/frontend/pipelines/pipeline_details_header_spec.js
+++ b/spec/frontend/pipelines/pipeline_details_header_spec.js
@@ -1,23 +1,59 @@
-import { GlBadge, GlLoadingIcon } from '@gitlab/ui';
-import Vue from 'vue';
+import { GlAlert, GlBadge, GlLoadingIcon, GlModal } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PipelineDetailsHeader from '~/pipelines/components/pipeline_details_header.vue';
+import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL } from '~/pipelines/constants';
import TimeAgo from '~/pipelines/components/pipelines_list/time_ago.vue';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import cancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
+import deletePipelineMutation from '~/pipelines/graphql/mutations/delete_pipeline.mutation.graphql';
+import retryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
import getPipelineDetailsQuery from '~/pipelines/graphql/queries/get_pipeline_header_data.query.graphql';
-import { pipelineHeaderSuccess, pipelineHeaderRunning } from './mock_data';
+import {
+ pipelineHeaderSuccess,
+ pipelineHeaderRunning,
+ pipelineHeaderFailed,
+ pipelineRetryMutationResponseSuccess,
+ pipelineCancelMutationResponseSuccess,
+ pipelineDeleteMutationResponseSuccess,
+ pipelineRetryMutationResponseFailed,
+ pipelineCancelMutationResponseFailed,
+ pipelineDeleteMutationResponseFailed,
+} from './mock_data';
Vue.use(VueApollo);
describe('Pipeline details header', () => {
let wrapper;
+ let glModalDirective;
const successHandler = jest.fn().mockResolvedValue(pipelineHeaderSuccess);
const runningHandler = jest.fn().mockResolvedValue(pipelineHeaderRunning);
+ const failedHandler = jest.fn().mockResolvedValue(pipelineHeaderFailed);
+ const retryMutationHandlerSuccess = jest
+ .fn()
+ .mockResolvedValue(pipelineRetryMutationResponseSuccess);
+ const cancelMutationHandlerSuccess = jest
+ .fn()
+ .mockResolvedValue(pipelineCancelMutationResponseSuccess);
+ const deleteMutationHandlerSuccess = jest
+ .fn()
+ .mockResolvedValue(pipelineDeleteMutationResponseSuccess);
+ const retryMutationHandlerFailed = jest
+ .fn()
+ .mockResolvedValue(pipelineRetryMutationResponseFailed);
+ const cancelMutationHandlerFailed = jest
+ .fn()
+ .mockResolvedValue(pipelineCancelMutationResponseFailed);
+ const deleteMutationHandlerFailed = jest
+ .fn()
+ .mockResolvedValue(pipelineDeleteMutationResponseFailed);
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
const findStatus = () => wrapper.findComponent(CiBadgeLink);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findTimeAgo = () => wrapper.findComponent(TimeAgo);
@@ -28,6 +64,10 @@ describe('Pipeline details header', () => {
const findCommitLink = () => wrapper.findByTestId('commit-link');
const findPipelineRunningText = () => wrapper.findByTestId('pipeline-running-text').text();
const findPipelineRefText = () => wrapper.findByTestId('pipeline-ref-text').text();
+ const findRetryButton = () => wrapper.findByTestId('retry-pipeline');
+ const findCancelButton = () => wrapper.findByTestId('cancel-pipeline');
+ const findDeleteButton = () => wrapper.findByTestId('delete-pipeline');
+ const findDeleteModal = () => wrapper.findComponent(GlModal);
const defaultHandlers = [[getPipelineDetailsQuery, successHandler]];
@@ -58,7 +98,7 @@ describe('Pipeline details header', () => {
stuck: false,
},
refText:
- 'For merge request !1 to merge test',
+ 'Related merge request !1 to merge test',
};
const createMockApolloProvider = (handlers) => {
@@ -66,6 +106,8 @@ describe('Pipeline details header', () => {
};
const createComponent = (handlers = defaultHandlers, props = defaultProps) => {
+ glModalDirective = jest.fn();
+
wrapper = shallowMountExtended(PipelineDetailsHeader, {
provide: {
...defaultProvideOptions,
@@ -73,6 +115,13 @@ describe('Pipeline details header', () => {
propsData: {
...props,
},
+ directives: {
+ glModal: {
+ bind(_, { value }) {
+ glModalDirective(value);
+ },
+ },
+ },
apolloProvider: createMockApolloProvider(handlers),
});
};
@@ -125,7 +174,7 @@ describe('Pipeline details header', () => {
});
it('displays ref text', () => {
- expect(findPipelineRefText()).toBe('For merge request !1 to merge test');
+ expect(findPipelineRefText()).toBe('Related merge request !1 to merge test');
});
});
@@ -164,4 +213,155 @@ describe('Pipeline details header', () => {
expect(findPipelineRunningText()).toBe('In progress, queued for 3600 seconds');
});
});
+
+ describe('actions', () => {
+ describe('retry action', () => {
+ beforeEach(async () => {
+ createComponent([
+ [getPipelineDetailsQuery, failedHandler],
+ [retryPipelineMutation, retryMutationHandlerSuccess],
+ ]);
+
+ await waitForPromises();
+ });
+
+ it('should call retryPipeline Mutation with pipeline id', () => {
+ findRetryButton().vm.$emit('click');
+
+ expect(retryMutationHandlerSuccess).toHaveBeenCalledWith({
+ id: pipelineHeaderFailed.data.project.pipeline.id,
+ });
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it('should render retry action tooltip', () => {
+ expect(findRetryButton().attributes('title')).toBe(BUTTON_TOOLTIP_RETRY);
+ });
+ });
+
+ describe('retry action failed', () => {
+ beforeEach(async () => {
+ createComponent([
+ [getPipelineDetailsQuery, failedHandler],
+ [retryPipelineMutation, retryMutationHandlerFailed],
+ ]);
+
+ await waitForPromises();
+ });
+
+ it('should display error message on failure', async () => {
+ findRetryButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ it('retry button loading state should reset on error', async () => {
+ findRetryButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(findRetryButton().props('loading')).toBe(true);
+
+ await waitForPromises();
+
+ expect(findRetryButton().props('loading')).toBe(false);
+ });
+ });
+
+ describe('cancel action', () => {
+ it('should call cancelPipeline Mutation with pipeline id', async () => {
+ createComponent([
+ [getPipelineDetailsQuery, runningHandler],
+ [cancelPipelineMutation, cancelMutationHandlerSuccess],
+ ]);
+
+ await waitForPromises();
+
+ findCancelButton().vm.$emit('click');
+
+ expect(cancelMutationHandlerSuccess).toHaveBeenCalledWith({
+ id: pipelineHeaderRunning.data.project.pipeline.id,
+ });
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it('should render cancel action tooltip', async () => {
+ createComponent([
+ [getPipelineDetailsQuery, runningHandler],
+ [cancelPipelineMutation, cancelMutationHandlerSuccess],
+ ]);
+
+ await waitForPromises();
+
+ expect(findCancelButton().attributes('title')).toBe(BUTTON_TOOLTIP_CANCEL);
+ });
+
+ it('should display error message on failure', async () => {
+ createComponent([
+ [getPipelineDetailsQuery, runningHandler],
+ [cancelPipelineMutation, cancelMutationHandlerFailed],
+ ]);
+
+ await waitForPromises();
+
+ findCancelButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ });
+ });
+
+ describe('delete action', () => {
+ it('displays delete modal when clicking on delete and does not call the delete action', async () => {
+ createComponent([
+ [getPipelineDetailsQuery, successHandler],
+ [deletePipelineMutation, deleteMutationHandlerSuccess],
+ ]);
+
+ await waitForPromises();
+
+ findDeleteButton().vm.$emit('click');
+
+ const modalId = 'pipeline-delete-modal';
+
+ expect(findDeleteModal().props('modalId')).toBe(modalId);
+ expect(glModalDirective).toHaveBeenCalledWith(modalId);
+ expect(deleteMutationHandlerSuccess).not.toHaveBeenCalled();
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it('should call deletePipeline Mutation with pipeline id when modal is submitted', async () => {
+ createComponent([
+ [getPipelineDetailsQuery, successHandler],
+ [deletePipelineMutation, deleteMutationHandlerSuccess],
+ ]);
+
+ await waitForPromises();
+
+ findDeleteModal().vm.$emit('primary');
+
+ expect(deleteMutationHandlerSuccess).toHaveBeenCalledWith({
+ id: pipelineHeaderSuccess.data.project.pipeline.id,
+ });
+ });
+
+ it('should display error message on failure', async () => {
+ createComponent([
+ [getPipelineDetailsQuery, successHandler],
+ [deletePipelineMutation, deleteMutationHandlerFailed],
+ ]);
+
+ await waitForPromises();
+
+ findDeleteModal().vm.$emit('primary');
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ });
+ });
+ });
});
diff --git a/spec/frontend/pipelines/pipeline_multi_actions_spec.js b/spec/frontend/pipelines/pipeline_multi_actions_spec.js
index e3c9983aa52..43336bbc748 100644
--- a/spec/frontend/pipelines/pipeline_multi_actions_spec.js
+++ b/spec/frontend/pipelines/pipeline_multi_actions_spec.js
@@ -1,9 +1,11 @@
+import { nextTick } from 'vue';
import { GlAlert, GlDropdown, GlSprintf, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { stubComponent } from 'helpers/stub_component';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import PipelineMultiActions, {
@@ -14,6 +16,7 @@ import { TRACKING_CATEGORIES } from '~/pipelines/constants';
describe('Pipeline Multi Actions Dropdown', () => {
let wrapper;
let mockAxios;
+ const focusInputMock = jest.fn();
const artifacts = [
{
@@ -30,7 +33,7 @@ describe('Pipeline Multi Actions Dropdown', () => {
const artifactsEndpoint = `endpoint/${artifactsEndpointPlaceholder}/artifacts.json`;
const pipelineId = 108;
- const createComponent = ({ mockData = {} } = {}) => {
+ const createComponent = () => {
wrapper = extendedWrapper(
shallowMount(PipelineMultiActions, {
provide: {
@@ -40,14 +43,12 @@ describe('Pipeline Multi Actions Dropdown', () => {
propsData: {
pipelineId,
},
- data() {
- return {
- ...mockData,
- };
- },
stubs: {
GlSprintf,
GlDropdown,
+ GlSearchBoxByType: stubComponent(GlSearchBoxByType, {
+ methods: { focusInput: focusInputMock },
+ }),
},
}),
);
@@ -76,70 +77,91 @@ describe('Pipeline Multi Actions Dropdown', () => {
});
describe('Artifacts', () => {
- it('should fetch artifacts and show search box on dropdown click', async () => {
- const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId);
- mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_OK, { artifacts });
- createComponent();
- findDropdown().vm.$emit('show');
- await waitForPromises();
-
- expect(mockAxios.history.get).toHaveLength(1);
- expect(wrapper.vm.artifacts).toEqual(artifacts);
- expect(findSearchBox().exists()).toBe(true);
- });
-
- it('should focus the search box when opened with artifacts', () => {
- createComponent({ mockData: { artifacts } });
- wrapper.vm.$refs.searchInput.focusInput = jest.fn();
-
- findDropdown().vm.$emit('shown');
-
- expect(wrapper.vm.$refs.searchInput.focusInput).toHaveBeenCalled();
- });
-
- it('should render all the provided artifacts when search query is empty', () => {
- const searchQuery = '';
- createComponent({ mockData: { searchQuery, artifacts } });
-
- expect(findAllArtifactItems()).toHaveLength(artifacts.length);
- expect(findEmptyMessage().exists()).toBe(false);
- });
-
- it('should render filtered artifacts when search query is not empty', () => {
- const searchQuery = 'job-2';
- createComponent({ mockData: { searchQuery, artifacts } });
-
- expect(findAllArtifactItems()).toHaveLength(1);
- expect(findEmptyMessage().exists()).toBe(false);
- });
-
- it('should render the correct artifact name and path', () => {
- createComponent({ mockData: { artifacts } });
-
- expect(findFirstArtifactItem().attributes('href')).toBe(artifacts[0].path);
- expect(findFirstArtifactItem().text()).toBe(artifacts[0].name);
- });
-
- it('should render empty message and no search box when no artifacts are found', () => {
- createComponent({ mockData: { artifacts: [] } });
-
- expect(findEmptyMessage().exists()).toBe(true);
- expect(findSearchBox().exists()).toBe(false);
- });
+ const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId);
describe('while loading artifacts', () => {
- it('should render a loading spinner and no empty message', () => {
- createComponent({ mockData: { isLoading: true, artifacts: [] } });
+ beforeEach(() => {
+ mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_OK, { artifacts });
+ });
+
+ it('should render a loading spinner and no empty message', async () => {
+ createComponent();
+
+ findDropdown().vm.$emit('show');
+ await nextTick();
expect(findLoadingIcon().exists()).toBe(true);
expect(findEmptyMessage().exists()).toBe(false);
});
});
+ describe('artifacts loaded successfully', () => {
+ describe('artifacts exist', () => {
+ beforeEach(async () => {
+ mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_OK, { artifacts });
+
+ createComponent();
+
+ findDropdown().vm.$emit('show');
+ await waitForPromises();
+ });
+
+ it('should fetch artifacts and show search box on dropdown click', () => {
+ expect(mockAxios.history.get).toHaveLength(1);
+ expect(findSearchBox().exists()).toBe(true);
+ });
+
+ it('should focus the search box when opened with artifacts', () => {
+ findDropdown().vm.$emit('shown');
+
+ expect(focusInputMock).toHaveBeenCalled();
+ });
+
+ it('should render all the provided artifacts when search query is empty', () => {
+ findSearchBox().vm.$emit('input', '');
+
+ expect(findAllArtifactItems()).toHaveLength(artifacts.length);
+ expect(findEmptyMessage().exists()).toBe(false);
+ });
+
+ it('should render filtered artifacts when search query is not empty', async () => {
+ findSearchBox().vm.$emit('input', 'job-2');
+ await waitForPromises();
+
+ expect(findAllArtifactItems()).toHaveLength(1);
+ expect(findEmptyMessage().exists()).toBe(false);
+ });
+
+ it('should render the correct artifact name and path', () => {
+ expect(findFirstArtifactItem().attributes('href')).toBe(artifacts[0].path);
+ expect(findFirstArtifactItem().text()).toBe(artifacts[0].name);
+ });
+ });
+
+ describe('artifacts list is empty', () => {
+ beforeEach(() => {
+ mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_OK, { artifacts: [] });
+ });
+
+ it('should render empty message and no search box when no artifacts are found', async () => {
+ createComponent();
+
+ findDropdown().vm.$emit('show');
+ await waitForPromises();
+
+ expect(findEmptyMessage().exists()).toBe(true);
+ expect(findSearchBox().exists()).toBe(false);
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ });
+ });
+
describe('with a failing request', () => {
- it('should render an error message', async () => {
- const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId);
+ beforeEach(() => {
mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ });
+
+ it('should render an error message', async () => {
createComponent();
findDropdown().vm.$emit('show');
await waitForPromises();
diff --git a/spec/frontend/vue_shared/components/ci_badge_link_spec.js b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
index afb509b9fe6..8c860c9b06f 100644
--- a/spec/frontend/vue_shared/components/ci_badge_link_spec.js
+++ b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
@@ -1,4 +1,4 @@
-import { GlLink } from '@gitlab/ui';
+import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
@@ -46,6 +46,13 @@ describe('CI Badge Link Component', () => {
icon: 'status_pending',
details_path: 'status/pending',
},
+ preparing: {
+ text: 'preparing',
+ label: 'preparing',
+ group: 'preparing',
+ icon: 'status_preparing',
+ details_path: 'status/preparing',
+ },
running: {
text: 'running',
label: 'running',
@@ -53,6 +60,13 @@ describe('CI Badge Link Component', () => {
icon: 'status_running',
details_path: 'status/running',
},
+ scheduled: {
+ text: 'scheduled',
+ label: 'scheduled',
+ group: 'scheduled',
+ icon: 'status_scheduled',
+ details_path: 'status/scheduled',
+ },
skipped: {
text: 'skipped',
label: 'skipped',
@@ -61,8 +75,8 @@ describe('CI Badge Link Component', () => {
details_path: 'status/skipped',
},
success_warining: {
- text: 'passed',
- label: 'passed',
+ text: 'warning',
+ label: 'passed with warnings',
group: 'success-with-warnings',
icon: 'status_warning',
details_path: 'status/warning',
@@ -77,6 +91,8 @@ describe('CI Badge Link Component', () => {
};
const findIcon = () => wrapper.findComponent(CiIcon);
+ const findBadge = () => wrapper.findComponent(GlBadge);
+ const findBadgeText = () => wrapper.find('[data-testid="ci-badge-text"');
const createComponent = (propsData) => {
wrapper = shallowMount(CiBadgeLink, { propsData });
@@ -87,22 +103,50 @@ describe('CI Badge Link Component', () => {
expect(wrapper.attributes('href')).toBe(statuses[status].details_path);
expect(wrapper.text()).toBe(statuses[status].text);
- expect(wrapper.classes()).toContain('ci-status');
- expect(wrapper.classes()).toContain(`ci-${statuses[status].group}`);
+ expect(findBadge().props('size')).toBe('md');
expect(findIcon().exists()).toBe(true);
});
+ it.each`
+ status | textColor | variant
+ ${statuses.success} | ${'gl-text-green-700'} | ${'success'}
+ ${statuses.success_warining} | ${'gl-text-orange-700'} | ${'warning'}
+ ${statuses.failed} | ${'gl-text-red-700'} | ${'danger'}
+ ${statuses.running} | ${'gl-text-blue-700'} | ${'info'}
+ ${statuses.pending} | ${'gl-text-orange-700'} | ${'warning'}
+ ${statuses.preparing} | ${'gl-text-gray-600'} | ${'muted'}
+ ${statuses.canceled} | ${'gl-text-gray-700'} | ${'neutral'}
+ ${statuses.scheduled} | ${'gl-text-gray-600'} | ${'muted'}
+ ${statuses.skipped} | ${'gl-text-gray-600'} | ${'muted'}
+ ${statuses.manual} | ${'gl-text-gray-700'} | ${'neutral'}
+ ${statuses.created} | ${'gl-text-gray-600'} | ${'muted'}
+ `(
+ 'should contain correct badge class and variant for status: $status.text',
+ ({ status, textColor, variant }) => {
+ createComponent({ status });
+
+ expect(findBadgeText().classes()).toContain(textColor);
+ expect(findBadge().props('variant')).toBe(variant);
+ },
+ );
+
it('should not render label', () => {
createComponent({ status: statuses.canceled, showText: false });
expect(wrapper.text()).toBe('');
});
- it('should emit ciStatusBadgeClick event', async () => {
+ it('should emit ciStatusBadgeClick event', () => {
createComponent({ status: statuses.success });
- await wrapper.findComponent(GlLink).vm.$emit('click');
+ findBadge().vm.$emit('click');
expect(wrapper.emitted('ciStatusBadgeClick')).toEqual([[]]);
});
+
+ it('should render dynamic badge size', () => {
+ createComponent({ status: statuses.success, badgeSize: 'lg' });
+
+ expect(findBadge().props('size')).toBe('lg');
+ });
});
diff --git a/spec/graphql/types/ci/runner_manager_type_spec.rb b/spec/graphql/types/ci/runner_manager_type_spec.rb
index 240e1edbf78..6f73171cd8f 100644
--- a/spec/graphql/types/ci/runner_manager_type_spec.rb
+++ b/spec/graphql/types/ci/runner_manager_type_spec.rb
@@ -13,6 +13,6 @@ RSpec.describe GitlabSchema.types['CiRunnerManager'], feature_category: :runner_
runner status system_id version
]
- expect(described_class).to have_graphql_fields(*expected_fields)
+ expect(described_class).to include_graphql_fields(*expected_fields)
end
end
diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb
index 21eca97331e..f71f3d47452 100644
--- a/spec/lib/gitlab/ci/status/build/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb
@@ -370,7 +370,7 @@ RSpec.describe Gitlab::Ci::Status::Build::Factory do
end
it 'fabricates status with correct details' do
- expect(status.text).to eq s_('CiStatusText|delayed')
+ expect(status.text).to eq s_('CiStatusText|scheduled')
expect(status.group).to eq 'scheduled'
expect(status.icon).to eq 'status_scheduled'
expect(status.favicon).to eq 'favicon_status_scheduled'
diff --git a/spec/lib/gitlab/ci/status/scheduled_spec.rb b/spec/lib/gitlab/ci/status/scheduled_spec.rb
index 8a923faf3f9..df72455d3c1 100644
--- a/spec/lib/gitlab/ci/status/scheduled_spec.rb
+++ b/spec/lib/gitlab/ci/status/scheduled_spec.rb
@@ -2,17 +2,17 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Status::Scheduled do
+RSpec.describe Gitlab::Ci::Status::Scheduled, feature_category: :continuous_integration do
subject do
described_class.new(double('subject'), double('user'))
end
describe '#text' do
- it { expect(subject.text).to eq 'delayed' }
+ it { expect(subject.text).to eq 'scheduled' }
end
describe '#label' do
- it { expect(subject.label).to eq 'delayed' }
+ it { expect(subject.label).to eq 'scheduled' }
end
describe '#icon' do
diff --git a/spec/lib/gitlab/ci/status/success_warning_spec.rb b/spec/lib/gitlab/ci/status/success_warning_spec.rb
index 86b826ad272..1725f90a0cf 100644
--- a/spec/lib/gitlab/ci/status/success_warning_spec.rb
+++ b/spec/lib/gitlab/ci/status/success_warning_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Status::SuccessWarning do
+RSpec.describe Gitlab::Ci::Status::SuccessWarning, feature_category: :continuous_integration do
let(:status) { double('status') }
subject do
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::Ci::Status::SuccessWarning do
end
describe '#test' do
- it { expect(subject.text).to eq 'passed' }
+ it { expect(subject.text).to eq 'warning' }
end
describe '#label' do
diff --git a/spec/models/integrations/chat_message/push_message_spec.rb b/spec/models/integrations/chat_message/push_message_spec.rb
index 8d2d0f9f9a8..5c9c5c64d7e 100644
--- a/spec/models/integrations/chat_message/push_message_spec.rb
+++ b/spec/models/integrations/chat_message/push_message_spec.rb
@@ -38,8 +38,8 @@ RSpec.describe Integrations::ChatMessage::PushMessage do
context 'without markdown' do
it 'returns a message regarding pushes' do
expect(subject.pretext).to eq(
- 'test.user pushed to branch of '\
- ' ()')
+ 'test.user pushed to branch of '\
+ ' ()')
expect(subject.attachments).to eq([{
text: ": message1 - author1\n\n"\
": message2 w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w ... - author2",
@@ -55,13 +55,13 @@ RSpec.describe Integrations::ChatMessage::PushMessage do
it 'returns a message regarding pushes' do
expect(subject.pretext).to eq(
- 'test.user pushed to branch [master](http://url.com/commits/master) of [project_name](http://url.com) ([Compare changes](http://url.com/compare/before...after))')
+ 'test.user pushed to branch [master](http://url.com/-/commits/master) of [project_name](http://url.com) ([Compare changes](http://url.com/-/compare/before...after))')
expect(subject.attachments).to eq(
"[abcdefgh](http://url1.com): message1 - author1\n\n[12345678](http://url2.com): message2 w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w ... - author2")
expect(subject.activity).to eq(
- title: 'test.user pushed to branch [master](http://url.com/commits/master)',
+ title: 'test.user pushed to branch [master](http://url.com/-/commits/master)',
subtitle: 'in [project_name](http://url.com)',
- text: '[Compare changes](http://url.com/compare/before...after)',
+ text: '[Compare changes](http://url.com/-/compare/before...after)',
image: 'http://someavatar.com'
)
end
@@ -102,7 +102,7 @@ RSpec.describe Integrations::ChatMessage::PushMessage do
expect(subject.activity).to eq(
title: 'test.user pushed new tag [new_tag](http://url.com/-/tags/new_tag)',
subtitle: 'in [project_name](http://url.com)',
- text: '[Compare changes](http://url.com/compare/0000000000000000000000000000000000000000...after)',
+ text: '[Compare changes](http://url.com/-/compare/0000000000000000000000000000000000000000...after)',
image: 'http://someavatar.com'
)
end
@@ -143,7 +143,7 @@ RSpec.describe Integrations::ChatMessage::PushMessage do
expect(subject.activity).to eq(
title: 'test.user removed tag new_tag',
subtitle: 'in [project_name](http://url.com)',
- text: '[Compare changes](http://url.com/compare/before...0000000000000000000000000000000000000000)',
+ text: '[Compare changes](http://url.com/-/compare/before...0000000000000000000000000000000000000000)',
image: 'http://someavatar.com'
)
end
@@ -158,7 +158,7 @@ RSpec.describe Integrations::ChatMessage::PushMessage do
context 'without markdown' do
it 'returns a message regarding a new branch' do
expect(subject.pretext).to eq(
- 'test.user pushed new branch to '\
+ 'test.user pushed new branch to '\
'')
expect(subject.attachments).to be_empty
end
@@ -171,12 +171,12 @@ RSpec.describe Integrations::ChatMessage::PushMessage do
it 'returns a message regarding a new branch' do
expect(subject.pretext).to eq(
- 'test.user pushed new branch [master](http://url.com/commits/master) to [project_name](http://url.com)')
+ 'test.user pushed new branch [master](http://url.com/-/commits/master) to [project_name](http://url.com)')
expect(subject.attachments).to be_empty
expect(subject.activity).to eq(
- title: 'test.user pushed new branch [master](http://url.com/commits/master)',
+ title: 'test.user pushed new branch [master](http://url.com/-/commits/master)',
subtitle: 'in [project_name](http://url.com)',
- text: '[Compare changes](http://url.com/compare/0000000000000000000000000000000000000000...after)',
+ text: '[Compare changes](http://url.com/-/compare/0000000000000000000000000000000000000000...after)',
image: 'http://someavatar.com'
)
end
@@ -208,7 +208,7 @@ RSpec.describe Integrations::ChatMessage::PushMessage do
expect(subject.activity).to eq(
title: 'test.user removed branch master',
subtitle: 'in [project_name](http://url.com)',
- text: '[Compare changes](http://url.com/compare/before...0000000000000000000000000000000000000000)',
+ text: '[Compare changes](http://url.com/-/compare/before...0000000000000000000000000000000000000000)',
image: 'http://someavatar.com'
)
end
diff --git a/spec/models/integrations/discord_spec.rb b/spec/models/integrations/discord_spec.rb
index 138a56d1872..42ea4a287fe 100644
--- a/spec/models/integrations/discord_spec.rb
+++ b/spec/models/integrations/discord_spec.rb
@@ -52,7 +52,7 @@ RSpec.describe Integrations::Discord do
subject.execute(sample_data)
expect(builder.to_json_hash[:embeds].first).to include(
- description: start_with("#{user.name} pushed to branch [master](http://localhost/#{project.namespace.path}/#{project.path}/commits/master) of"),
+ description: start_with("#{user.name} pushed to branch [master](http://localhost/#{project.namespace.path}/#{project.path}/-/commits/master) of"),
author: hash_including(
icon_url: start_with('https://www.gravatar.com/avatar/'),
name: user.name
diff --git a/spec/presenters/ci/pipeline_presenter_spec.rb b/spec/presenters/ci/pipeline_presenter_spec.rb
index 86e4bb703dc..cc68cdff7c1 100644
--- a/spec/presenters/ci/pipeline_presenter_spec.rb
+++ b/spec/presenters/ci/pipeline_presenter_spec.rb
@@ -217,7 +217,7 @@ RSpec.describe Ci::PipelinePresenter do
let(:pipeline) { merge_request.all_pipelines.last }
it 'returns a correct ref text' do
- is_expected.to eq("For merge request #{merge_request.to_reference} " \
+ is_expected.to eq("Related merge request #{merge_request.to_reference} " \
"to merge #{merge_request.source_branch}")
end
end
@@ -227,7 +227,7 @@ RSpec.describe Ci::PipelinePresenter do
let(:pipeline) { merge_request.all_pipelines.last }
it 'returns a correct ref text' do
- is_expected.to eq("For merge request #{merge_request.to_reference} " \
+ is_expected.to eq("Related merge request #{merge_request.to_reference} " \
"to merge #{merge_request.source_branch} " \
"into #{merge_request.target_branch}")
end
diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb
index f237516021d..756fcd8b7cd 100644
--- a/spec/requests/api/graphql/ci/jobs_spec.rb
+++ b/spec/requests/api/graphql/ci/jobs_spec.rb
@@ -433,8 +433,6 @@ RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integrati
end
it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do
- admin2 = create(:admin)
-
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
post_graphql(query, current_user: admin)
end
@@ -442,7 +440,7 @@ RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integrati
runner_manager2 = create(:ci_runner_machine)
create(:ci_build, pipeline: pipeline, name: 'my test job2', runner_manager: runner_manager2)
- expect { post_graphql(query, current_user: admin2) }.not_to exceed_all_query_limit(control)
+ expect { post_graphql(query, current_user: admin) }.not_to exceed_all_query_limit(control)
end
end