summaryrefslogtreecommitdiff
path: root/spec/frontend/ci
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/ci')
-rw-r--r--spec/frontend/ci/artifacts/components/app_spec.js118
-rw-r--r--spec/frontend/ci/artifacts/components/artifact_row_spec.js127
-rw-r--r--spec/frontend/ci/artifacts/components/artifacts_bulk_delete_spec.js58
-rw-r--r--spec/frontend/ci/artifacts/components/artifacts_table_row_details_spec.js138
-rw-r--r--spec/frontend/ci/artifacts/components/feedback_banner_spec.js59
-rw-r--r--spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js684
-rw-r--r--spec/frontend/ci/artifacts/components/job_checkbox_spec.js132
-rw-r--r--spec/frontend/ci/artifacts/graphql/cache_update_spec.js67
-rw-r--r--spec/frontend/ci/ci_lint/components/ci_lint_spec.js1
-rw-r--r--spec/frontend/ci/ci_variable_list/ci_variable_list/ci_variable_list_spec.js10
-rw-r--r--spec/frontend/ci/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js5
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js26
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js153
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js26
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js31
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js57
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js67
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js749
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js189
-rw-r--r--spec/frontend/ci/ci_variable_list/mocks.js5
-rw-r--r--spec/frontend/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js12
-rw-r--r--spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js46
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/cards/getting_started_card_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js6
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/editor/ci_config_merged_preview_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js43
-rw-r--r--spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js76
-rw-r--r--spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js6
-rw-r--r--spec/frontend/ci/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js9
-rw-r--r--spec/frontend/ci/pipeline_editor/components/file-tree/file_item_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js5
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js3
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/validation_segment_spec.js102
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item_spec.js127
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item_spec.js39
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item_spec.js60
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js70
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item_spec.js79
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js252
-rw-r--r--spec/frontend/ci/pipeline_editor/components/lint/ci_lint_results_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/lint/ci_lint_warnings_spec.js5
-rw-r--r--spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js7
-rw-r--r--spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js5
-rw-r--r--spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js6
-rw-r--r--spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js6
-rw-r--r--spec/frontend/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/ui/editor_tab_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js10
-rw-r--r--spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js2
-rw-r--r--spec/frontend/ci/pipeline_editor/mock_data.js32
-rw-r--r--spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js20
-rw-r--r--spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js2
-rw-r--r--spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js113
-rw-r--r--spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js190
-rw-r--r--spec/frontend/ci/pipeline_new/mock_data.js15
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js8
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js6
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js20
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/pipeline_schedules_table_spec.js7
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_legacy_spec.js14
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_schedules/mock_data.js2
-rw-r--r--spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js9
-rw-r--r--spec/frontend/ci/reports/components/grouped_issues_list_spec.js4
-rw-r--r--spec/frontend/ci/reports/components/issue_status_icon_spec.js5
-rw-r--r--spec/frontend/ci/reports/components/report_link_spec.js4
-rw-r--r--spec/frontend/ci/reports/components/report_section_spec.js4
-rw-r--r--spec/frontend/ci/reports/components/summary_row_spec.js5
-rw-r--r--spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js127
-rw-r--r--spec/frontend/ci/runner/admin_register_runner/admin_register_runner_app_spec.js122
-rw-r--r--spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js15
-rw-r--r--spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js44
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js6
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js12
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js58
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js6
-rw-r--r--spec/frontend/ci/runner/components/registration/__snapshots__/utils_spec.js.snap201
-rw-r--r--spec/frontend/ci/runner/components/registration/cli_command_spec.js39
-rw-r--r--spec/frontend/ci/runner/components/registration/platforms_drawer_spec.js108
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_compatibility_alert_spec.js53
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js149
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_feedback_banner_spec.js52
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_instructions_spec.js326
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js16
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_token_spec.js46
-rw-r--r--spec/frontend/ci/runner/components/registration/utils_spec.js94
-rw-r--r--spec/frontend/ci/runner/components/runner_assigned_item_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_bulk_delete_spec.js18
-rw-r--r--spec/frontend/ci/runner/components/runner_create_form_spec.js189
-rw-r--r--spec/frontend/ci/runner/components/runner_delete_button_spec.js24
-rw-r--r--spec/frontend/ci/runner/components/runner_details_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_edit_button_spec.js6
-rw-r--r--spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js18
-rw-r--r--spec/frontend/ci/runner/components/runner_groups_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_header_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_jobs_empty_state_spec.js35
-rw-r--r--spec/frontend/ci/runner/components/runner_jobs_spec.js16
-rw-r--r--spec/frontend/ci/runner/components/runner_jobs_table_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_list_empty_state_spec.js78
-rw-r--r--spec/frontend/ci/runner/components/runner_list_spec.js6
-rw-r--r--spec/frontend/ci/runner/components/runner_membership_toggle_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_pagination_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_pause_button_spec.js12
-rw-r--r--spec/frontend/ci/runner/components/runner_paused_badge_spec.js6
-rw-r--r--spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js14
-rw-r--r--spec/frontend/ci/runner/components/runner_platforms_radio_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_projects_spec.js9
-rw-r--r--spec/frontend/ci/runner/components/runner_status_badge_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_tag_spec.js8
-rw-r--r--spec/frontend/ci/runner/components/runner_tags_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_type_badge_spec.js9
-rw-r--r--spec/frontend/ci/runner/components/runner_type_tabs_spec.js7
-rw-r--r--spec/frontend/ci/runner/components/runner_update_form_spec.js14
-rw-r--r--spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js5
-rw-r--r--spec/frontend/ci/runner/components/stat/runner_count_spec.js14
-rw-r--r--spec/frontend/ci/runner/components/stat/runner_single_stat_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/stat/runner_stats_spec.js4
-rw-r--r--spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js124
-rw-r--r--spec/frontend/ci/runner/group_register_runner_app/group_register_runner_app_spec.js120
-rw-r--r--spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js13
-rw-r--r--spec/frontend/ci/runner/group_runners/group_runners_app_spec.js89
-rw-r--r--spec/frontend/ci/runner/local_storage_alert/show_alert_from_local_storage_spec.js4
-rw-r--r--spec/frontend/ci/runner/mock_data.js138
-rw-r--r--spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js125
-rw-r--r--spec/frontend/ci/runner/project_register_runner_app/project_register_runner_app_spec.js120
-rw-r--r--spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js5
-rw-r--r--spec/frontend/ci/runner/runner_search_utils_spec.js5
-rw-r--r--spec/frontend/ci/runner/sentry_utils_spec.js2
141 files changed, 5525 insertions, 1446 deletions
diff --git a/spec/frontend/ci/artifacts/components/app_spec.js b/spec/frontend/ci/artifacts/components/app_spec.js
new file mode 100644
index 00000000000..c6874428e2a
--- /dev/null
+++ b/spec/frontend/ci/artifacts/components/app_spec.js
@@ -0,0 +1,118 @@
+import { GlSkeletonLoader } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import Vue from 'vue';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import ArtifactsApp from '~/ci/artifacts/components/app.vue';
+import JobArtifactsTable from '~/ci/artifacts/components/job_artifacts_table.vue';
+import getBuildArtifactsSizeQuery from '~/ci/artifacts/graphql/queries/get_build_artifacts_size.query.graphql';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { PAGE_TITLE, TOTAL_ARTIFACTS_SIZE, SIZE_UNKNOWN } from '~/ci/artifacts/constants';
+
+const TEST_BUILD_ARTIFACTS_SIZE = 1024;
+const TEST_PROJECT_PATH = 'project/path';
+const TEST_PROJECT_ID = 'gid://gitlab/Project/22';
+
+const createBuildArtifactsSizeResponse = ({
+ buildArtifactsSize = TEST_BUILD_ARTIFACTS_SIZE,
+ nullStatistics = false,
+}) => ({
+ data: {
+ project: {
+ __typename: 'Project',
+ id: TEST_PROJECT_ID,
+ statistics: nullStatistics
+ ? null
+ : {
+ __typename: 'ProjectStatistics',
+ buildArtifactsSize,
+ },
+ },
+ },
+});
+
+Vue.use(VueApollo);
+
+describe('ArtifactsApp component', () => {
+ let wrapper;
+ let apolloProvider;
+ let getBuildArtifactsSizeSpy;
+
+ const findTitle = () => wrapper.findByTestId('artifacts-page-title');
+ const findBuildArtifactsSize = () => wrapper.findByTestId('build-artifacts-size');
+ const findJobArtifactsTable = () => wrapper.findComponent(JobArtifactsTable);
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ArtifactsApp, {
+ provide: { projectPath: 'project/path' },
+ apolloProvider,
+ });
+ };
+
+ beforeEach(() => {
+ getBuildArtifactsSizeSpy = jest.fn();
+
+ apolloProvider = createMockApollo([[getBuildArtifactsSizeQuery, getBuildArtifactsSizeSpy]]);
+ });
+
+ describe('when loading', () => {
+ beforeEach(() => {
+ // Promise that never resolves so it's always loading
+ getBuildArtifactsSizeSpy.mockReturnValue(new Promise(() => {}));
+
+ createComponent();
+ });
+
+ it('shows the page title', () => {
+ expect(findTitle().text()).toBe(PAGE_TITLE);
+ });
+
+ it('shows a skeleton while loading the artifacts size', () => {
+ expect(findSkeletonLoader().exists()).toBe(true);
+ });
+
+ it('shows the job artifacts table', () => {
+ expect(findJobArtifactsTable().exists()).toBe(true);
+ });
+
+ it('does not show message', () => {
+ expect(findBuildArtifactsSize().text()).toBe('');
+ });
+
+ it('calls apollo query', () => {
+ expect(getBuildArtifactsSizeSpy).toHaveBeenCalledWith({ projectPath: TEST_PROJECT_PATH });
+ });
+ });
+
+ describe.each`
+ buildArtifactsSize | nullStatistics | expectedText
+ ${TEST_BUILD_ARTIFACTS_SIZE} | ${false} | ${numberToHumanSize(TEST_BUILD_ARTIFACTS_SIZE)}
+ ${null} | ${false} | ${SIZE_UNKNOWN}
+ ${null} | ${true} | ${SIZE_UNKNOWN}
+ `(
+ 'when buildArtifactsSize is $buildArtifactsSize',
+ ({ buildArtifactsSize, nullStatistics, expectedText }) => {
+ beforeEach(async () => {
+ getBuildArtifactsSizeSpy.mockResolvedValue(
+ createBuildArtifactsSizeResponse({ buildArtifactsSize, nullStatistics }),
+ );
+
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('hides loader', () => {
+ expect(findSkeletonLoader().exists()).toBe(false);
+ });
+
+ it('shows the size', () => {
+ expect(findBuildArtifactsSize().text()).toMatchInterpolatedText(
+ `${TOTAL_ARTIFACTS_SIZE} ${expectedText}`,
+ );
+ });
+ },
+ );
+});
diff --git a/spec/frontend/ci/artifacts/components/artifact_row_spec.js b/spec/frontend/ci/artifacts/components/artifact_row_spec.js
new file mode 100644
index 00000000000..96ddedc3a9d
--- /dev/null
+++ b/spec/frontend/ci/artifacts/components/artifact_row_spec.js
@@ -0,0 +1,127 @@
+import { GlBadge, GlFriendlyWrap, GlFormCheckbox } from '@gitlab/ui';
+import mockGetJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import ArtifactRow from '~/ci/artifacts/components/artifact_row.vue';
+import { BULK_DELETE_FEATURE_FLAG, I18N_BULK_DELETE_MAX_SELECTED } from '~/ci/artifacts/constants';
+
+describe('ArtifactRow component', () => {
+ let wrapper;
+
+ const artifact = mockGetJobArtifactsResponse.data.project.jobs.nodes[0].artifacts.nodes[0];
+
+ const findName = () => wrapper.findByTestId('job-artifact-row-name');
+ const findBadge = () => wrapper.findComponent(GlBadge);
+ const findSize = () => wrapper.findByTestId('job-artifact-row-size');
+ const findDownloadButton = () => wrapper.findByTestId('job-artifact-row-download-button');
+ const findDeleteButton = () => wrapper.findByTestId('job-artifact-row-delete-button');
+ const findCheckbox = () => wrapper.findComponent(GlFormCheckbox);
+
+ const createComponent = ({ canDestroyArtifacts = true, glFeatures = {}, props = {} } = {}) => {
+ wrapper = shallowMountExtended(ArtifactRow, {
+ propsData: {
+ artifact,
+ isSelected: false,
+ isLoading: false,
+ isLastRow: false,
+ isSelectedArtifactsLimitReached: false,
+ ...props,
+ },
+ provide: { canDestroyArtifacts, glFeatures },
+ stubs: { GlBadge, GlFriendlyWrap },
+ });
+ };
+
+ describe('artifact details', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('displays the artifact name and type', () => {
+ expect(findName().text()).toContain(artifact.name);
+ expect(findBadge().text()).toBe(artifact.fileType.toLowerCase());
+ });
+
+ it('displays the artifact size', () => {
+ expect(findSize().text()).toBe(numberToHumanSize(artifact.size));
+ });
+
+ it('displays the download button as a link to the download path', () => {
+ expect(findDownloadButton().attributes('href')).toBe(artifact.downloadPath);
+ });
+ });
+
+ describe('delete button', () => {
+ it('does not show when user does not have permission', () => {
+ createComponent({ canDestroyArtifacts: false });
+
+ expect(findDeleteButton().exists()).toBe(false);
+ });
+
+ it('shows when user has permission', () => {
+ createComponent();
+
+ expect(findDeleteButton().exists()).toBe(true);
+ });
+
+ it('emits the delete event when clicked', async () => {
+ createComponent();
+
+ expect(wrapper.emitted('delete')).toBeUndefined();
+
+ findDeleteButton().vm.$emit('click');
+ await waitForPromises();
+
+ expect(wrapper.emitted('delete')).toBeDefined();
+ });
+ });
+
+ describe('bulk delete checkbox', () => {
+ describe('with permission and feature flag enabled', () => {
+ it('emits selectArtifact when toggled', () => {
+ createComponent({ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true } });
+
+ findCheckbox().vm.$emit('input', true);
+
+ expect(wrapper.emitted('selectArtifact')).toStrictEqual([[artifact, true]]);
+ });
+
+ describe('when the selected artifacts limit is reached', () => {
+ it('remains enabled if the artifact was selected', () => {
+ createComponent({
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
+ props: { isSelected: true, isSelectedArtifactsLimitReached: true },
+ });
+
+ expect(findCheckbox().attributes('disabled')).toBeUndefined();
+ expect(findCheckbox().attributes('title')).toBe('');
+ });
+
+ it('is disabled if the artifact was not selected', () => {
+ createComponent({
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
+ props: { isSelected: false, isSelectedArtifactsLimitReached: true },
+ });
+
+ expect(findCheckbox().attributes('disabled')).toBeDefined();
+ expect(findCheckbox().attributes('title')).toBe(I18N_BULK_DELETE_MAX_SELECTED);
+ });
+ });
+ });
+
+ it('is not shown without permission', () => {
+ createComponent({ canDestroyArtifacts: false });
+
+ expect(findCheckbox().exists()).toBe(false);
+ });
+
+ it('is not shown with feature flag disabled', () => {
+ createComponent();
+
+ expect(findCheckbox().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/ci/artifacts/components/artifacts_bulk_delete_spec.js b/spec/frontend/ci/artifacts/components/artifacts_bulk_delete_spec.js
new file mode 100644
index 00000000000..549f6e1e375
--- /dev/null
+++ b/spec/frontend/ci/artifacts/components/artifacts_bulk_delete_spec.js
@@ -0,0 +1,58 @@
+import { GlSprintf, GlAlert } from '@gitlab/ui';
+import mockGetJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ArtifactsBulkDelete from '~/ci/artifacts/components/artifacts_bulk_delete.vue';
+import { I18N_BULK_DELETE_MAX_SELECTED } from '~/ci/artifacts/constants';
+
+describe('ArtifactsBulkDelete component', () => {
+ let wrapper;
+
+ const selectedArtifacts = [
+ mockGetJobArtifactsResponse.data.project.jobs.nodes[0].artifacts.nodes[0].id,
+ mockGetJobArtifactsResponse.data.project.jobs.nodes[0].artifacts.nodes[1].id,
+ ];
+
+ const findText = () => wrapper.findComponent(GlSprintf).text();
+ const findDeleteButton = () => wrapper.findByTestId('bulk-delete-delete-button');
+ const findClearButton = () => wrapper.findByTestId('bulk-delete-clear-button');
+ const findAlertText = () => wrapper.findComponent(GlAlert).text();
+
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(ArtifactsBulkDelete, {
+ propsData: {
+ selectedArtifacts,
+ isSelectedArtifactsLimitReached: false,
+ ...props,
+ },
+ stubs: { GlSprintf },
+ });
+ };
+
+ describe('selected artifacts box', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('displays selected artifacts count', () => {
+ expect(findText()).toContain(String(selectedArtifacts.length));
+ });
+
+ it('emits showBulkDeleteModal event when the delete button is clicked', () => {
+ findDeleteButton().vm.$emit('click');
+
+ expect(wrapper.emitted('showBulkDeleteModal')).toBeDefined();
+ });
+
+ it('emits clearSelectedArtifacts event when the clear button is clicked', () => {
+ findClearButton().vm.$emit('click');
+
+ expect(wrapper.emitted('clearSelectedArtifacts')).toBeDefined();
+ });
+ });
+
+ it('shows an alert when the selected artifacts limit is reached', () => {
+ createComponent({ isSelectedArtifactsLimitReached: true });
+
+ expect(findAlertText()).toBe(I18N_BULK_DELETE_MAX_SELECTED);
+ });
+});
diff --git a/spec/frontend/ci/artifacts/components/artifacts_table_row_details_spec.js b/spec/frontend/ci/artifacts/components/artifacts_table_row_details_spec.js
new file mode 100644
index 00000000000..479ecf6b183
--- /dev/null
+++ b/spec/frontend/ci/artifacts/components/artifacts_table_row_details_spec.js
@@ -0,0 +1,138 @@
+import { GlModal } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import getJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import waitForPromises from 'helpers/wait_for_promises';
+import ArtifactsTableRowDetails from '~/ci/artifacts/components/artifacts_table_row_details.vue';
+import ArtifactRow from '~/ci/artifacts/components/artifact_row.vue';
+import ArtifactDeleteModal from '~/ci/artifacts/components/artifact_delete_modal.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import destroyArtifactMutation from '~/ci/artifacts/graphql/mutations/destroy_artifact.mutation.graphql';
+import { I18N_DESTROY_ERROR, I18N_MODAL_TITLE } from '~/ci/artifacts/constants';
+import { createAlert } from '~/alert';
+
+jest.mock('~/alert');
+
+const { artifacts } = getJobArtifactsResponse.data.project.jobs.nodes[0];
+const refetchArtifacts = jest.fn();
+
+Vue.use(VueApollo);
+
+describe('ArtifactsTableRowDetails component', () => {
+ let wrapper;
+ let requestHandlers;
+
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ const createComponent = ({
+ handlers = {
+ destroyArtifactMutation: jest.fn(),
+ },
+ selectedArtifacts = [],
+ } = {}) => {
+ requestHandlers = handlers;
+ wrapper = mountExtended(ArtifactsTableRowDetails, {
+ apolloProvider: createMockApollo([
+ [destroyArtifactMutation, requestHandlers.destroyArtifactMutation],
+ ]),
+ propsData: {
+ artifacts,
+ selectedArtifacts,
+ refetchArtifacts,
+ queryVariables: {},
+ isSelectedArtifactsLimitReached: false,
+ },
+ provide: { canDestroyArtifacts: true },
+ data() {
+ return { deletingArtifactId: null };
+ },
+ });
+ };
+
+ describe('passes correct props', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('to the artifact rows', () => {
+ [0, 1, 2].forEach((index) => {
+ expect(wrapper.findAllComponents(ArtifactRow).at(index).props()).toMatchObject({
+ artifact: artifacts.nodes[index],
+ });
+ });
+ });
+ });
+
+ describe('when the artifact row emits the delete event', () => {
+ it('shows the artifact delete modal', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(findModal().props('visible')).toBe(false);
+
+ await wrapper.findComponent(ArtifactRow).vm.$emit('delete');
+
+ expect(findModal().props('visible')).toBe(true);
+ expect(findModal().props('title')).toBe(I18N_MODAL_TITLE(artifacts.nodes[0].name));
+ });
+ });
+
+ describe('when the artifact delete modal emits its primary event', () => {
+ it('triggers the destroyArtifact GraphQL mutation', async () => {
+ createComponent();
+ await waitForPromises();
+
+ wrapper.findComponent(ArtifactRow).vm.$emit('delete');
+ wrapper.findComponent(ArtifactDeleteModal).vm.$emit('primary');
+
+ expect(requestHandlers.destroyArtifactMutation).toHaveBeenCalledWith({
+ id: artifacts.nodes[0].id,
+ });
+ });
+
+ it('displays an alert message and refetches artifacts when the mutation fails', async () => {
+ createComponent({
+ destroyArtifactMutation: jest.fn().mockRejectedValue(new Error('Error!')),
+ });
+ await waitForPromises();
+
+ expect(wrapper.emitted('refetch')).toBeUndefined();
+
+ wrapper.findComponent(ArtifactRow).vm.$emit('delete');
+ wrapper.findComponent(ArtifactDeleteModal).vm.$emit('primary');
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({ message: I18N_DESTROY_ERROR });
+ expect(wrapper.emitted('refetch')).toBeDefined();
+ });
+ });
+
+ describe('when the artifact delete modal is cancelled', () => {
+ it('does not trigger the destroyArtifact GraphQL mutation', async () => {
+ createComponent();
+ await waitForPromises();
+
+ wrapper.findComponent(ArtifactRow).vm.$emit('delete');
+ wrapper.findComponent(ArtifactDeleteModal).vm.$emit('cancel');
+
+ expect(requestHandlers.destroyArtifactMutation).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('bulk delete selection', () => {
+ it('is not selected for unselected artifact', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(wrapper.findAllComponents(ArtifactRow).at(0).props('isSelected')).toBe(false);
+ });
+
+ it('is selected for selected artifacts', async () => {
+ createComponent({ selectedArtifacts: [artifacts.nodes[0].id] });
+ await waitForPromises();
+
+ expect(wrapper.findAllComponents(ArtifactRow).at(0).props('isSelected')).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/ci/artifacts/components/feedback_banner_spec.js b/spec/frontend/ci/artifacts/components/feedback_banner_spec.js
new file mode 100644
index 00000000000..53e0fdac6f6
--- /dev/null
+++ b/spec/frontend/ci/artifacts/components/feedback_banner_spec.js
@@ -0,0 +1,59 @@
+import { GlBanner } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import FeedbackBanner from '~/ci/artifacts/components/feedback_banner.vue';
+import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
+import {
+ I18N_FEEDBACK_BANNER_TITLE,
+ I18N_FEEDBACK_BANNER_BUTTON,
+ FEEDBACK_URL,
+} from '~/ci/artifacts/constants';
+
+const mockBannerImagePath = 'banner/image/path';
+
+describe('Artifacts management feedback banner', () => {
+ let wrapper;
+ let userCalloutDismissSpy;
+
+ const findBanner = () => wrapper.findComponent(GlBanner);
+
+ const createComponent = ({ shouldShowCallout = true } = {}) => {
+ userCalloutDismissSpy = jest.fn();
+
+ wrapper = shallowMount(FeedbackBanner, {
+ provide: {
+ artifactsManagementFeedbackImagePath: mockBannerImagePath,
+ },
+ stubs: {
+ UserCalloutDismisser: makeMockUserCalloutDismisser({
+ dismiss: userCalloutDismissSpy,
+ shouldShowCallout,
+ }),
+ },
+ });
+ };
+
+ it('is displayed with the correct props', () => {
+ createComponent();
+
+ expect(findBanner().props()).toMatchObject({
+ title: I18N_FEEDBACK_BANNER_TITLE,
+ buttonText: I18N_FEEDBACK_BANNER_BUTTON,
+ buttonLink: FEEDBACK_URL,
+ svgPath: mockBannerImagePath,
+ });
+ });
+
+ it('dismisses the callout when closed', () => {
+ createComponent();
+
+ findBanner().vm.$emit('close');
+
+ expect(userCalloutDismissSpy).toHaveBeenCalled();
+ });
+
+ it('is not displayed once it has been dismissed', () => {
+ createComponent({ shouldShowCallout: false });
+
+ expect(findBanner().exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js b/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js
new file mode 100644
index 00000000000..514644a92f2
--- /dev/null
+++ b/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js
@@ -0,0 +1,684 @@
+import {
+ GlLoadingIcon,
+ GlTable,
+ GlLink,
+ GlBadge,
+ GlPagination,
+ GlModal,
+ GlFormCheckbox,
+} from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import getJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import JobArtifactsTable from '~/ci/artifacts/components/job_artifacts_table.vue';
+import FeedbackBanner from '~/ci/artifacts/components/feedback_banner.vue';
+import ArtifactsTableRowDetails from '~/ci/artifacts/components/artifacts_table_row_details.vue';
+import ArtifactDeleteModal from '~/ci/artifacts/components/artifact_delete_modal.vue';
+import ArtifactsBulkDelete from '~/ci/artifacts/components/artifacts_bulk_delete.vue';
+import BulkDeleteModal from '~/ci/artifacts/components/bulk_delete_modal.vue';
+import JobCheckbox from '~/ci/artifacts/components/job_checkbox.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import getJobArtifactsQuery from '~/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql';
+import bulkDestroyArtifactsMutation from '~/ci/artifacts/graphql/mutations/bulk_destroy_job_artifacts.mutation.graphql';
+import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPENAME_PROJECT } from '~/graphql_shared/constants';
+import {
+ ARCHIVE_FILE_TYPE,
+ JOBS_PER_PAGE,
+ I18N_FETCH_ERROR,
+ INITIAL_CURRENT_PAGE,
+ BULK_DELETE_FEATURE_FLAG,
+ I18N_BULK_DELETE_ERROR,
+ SELECTED_ARTIFACTS_MAX_COUNT,
+} from '~/ci/artifacts/constants';
+import { totalArtifactsSizeForJob } from '~/ci/artifacts/utils';
+import { createAlert } from '~/alert';
+
+jest.mock('~/alert');
+
+Vue.use(VueApollo);
+
+describe('JobArtifactsTable component', () => {
+ let wrapper;
+ let requestHandlers;
+
+ const mockToastShow = jest.fn();
+
+ const findBanner = () => wrapper.findComponent(FeedbackBanner);
+
+ const findLoadingState = () => wrapper.findComponent(GlLoadingIcon);
+ const findTable = () => wrapper.findComponent(GlTable);
+ const findDetailsRows = () => wrapper.findAllComponents(ArtifactsTableRowDetails);
+ const findDetailsInRow = (i) =>
+ findTable().findAll('tbody tr').at(i).findComponent(ArtifactsTableRowDetails);
+
+ const findCount = () => wrapper.findByTestId('job-artifacts-count');
+ const findCountAt = (i) => wrapper.findAllByTestId('job-artifacts-count').at(i);
+
+ const findDeleteModal = () => wrapper.findComponent(ArtifactDeleteModal);
+ const findBulkDeleteModal = () => wrapper.findComponent(BulkDeleteModal);
+
+ const findStatuses = () => wrapper.findAllByTestId('job-artifacts-job-status');
+ const findSuccessfulJobStatus = () => findStatuses().at(0);
+ const findFailedJobStatus = () => findStatuses().at(1);
+
+ const findLinks = () => wrapper.findAllComponents(GlLink);
+ const findJobLink = () => findLinks().at(0);
+ const findPipelineLink = () => findLinks().at(1);
+ const findRefLink = () => findLinks().at(2);
+ const findCommitLink = () => findLinks().at(3);
+
+ const findSize = () => wrapper.findByTestId('job-artifacts-size');
+ const findCreated = () => wrapper.findByTestId('job-artifacts-created');
+
+ const findDownloadButton = () => wrapper.findByTestId('job-artifacts-download-button');
+ const findBrowseButton = () => wrapper.findByTestId('job-artifacts-browse-button');
+ const findDeleteButton = () => wrapper.findByTestId('job-artifacts-delete-button');
+ const findArtifactDeleteButton = () => wrapper.findByTestId('job-artifact-row-delete-button');
+
+ // first checkbox is a "select all", this finder should get the first job checkbox
+ const findJobCheckbox = (i = 1) => wrapper.findAllComponents(GlFormCheckbox).at(i);
+ const findAnyCheckbox = () => wrapper.findComponent(GlFormCheckbox);
+ const findBulkDelete = () => wrapper.findComponent(ArtifactsBulkDelete);
+ const findBulkDeleteContainer = () => wrapper.findByTestId('bulk-delete-container');
+
+ const findPagination = () => wrapper.findComponent(GlPagination);
+ const setPage = async (page) => {
+ findPagination().vm.$emit('input', page);
+ await waitForPromises();
+ };
+
+ const projectId = 'some/project/id';
+
+ let enoughJobsToPaginate = [...getJobArtifactsResponse.data.project.jobs.nodes];
+ while (enoughJobsToPaginate.length <= JOBS_PER_PAGE) {
+ enoughJobsToPaginate = [
+ ...enoughJobsToPaginate,
+ ...getJobArtifactsResponse.data.project.jobs.nodes,
+ ];
+ }
+ const getJobArtifactsResponseThatPaginates = {
+ data: {
+ project: {
+ jobs: {
+ nodes: enoughJobsToPaginate,
+ pageInfo: { ...getJobArtifactsResponse.data.project.jobs.pageInfo, hasNextPage: true },
+ },
+ },
+ },
+ };
+
+ const job = getJobArtifactsResponse.data.project.jobs.nodes[0];
+ const archiveArtifact = job.artifacts.nodes.find(
+ (artifact) => artifact.fileType === ARCHIVE_FILE_TYPE,
+ );
+ const job2 = getJobArtifactsResponse.data.project.jobs.nodes[1];
+
+ const destroyedCount = job.artifacts.nodes.length;
+ const destroyedIds = job.artifacts.nodes.map((node) => node.id);
+ const bulkDestroyMutationHandler = jest.fn().mockResolvedValue({
+ data: {
+ bulkDestroyJobArtifacts: { errors: [], destroyedCount, destroyedIds },
+ },
+ });
+
+ const maxSelectedArtifacts = new Array(SELECTED_ARTIFACTS_MAX_COUNT).fill({});
+
+ const createComponent = ({
+ handlers = {
+ getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponse),
+ bulkDestroyArtifactsMutation: bulkDestroyMutationHandler,
+ },
+ data = {},
+ canDestroyArtifacts = true,
+ glFeatures = {},
+ } = {}) => {
+ requestHandlers = handlers;
+ wrapper = mountExtended(JobArtifactsTable, {
+ apolloProvider: createMockApollo([
+ [getJobArtifactsQuery, requestHandlers.getJobArtifactsQuery],
+ [bulkDestroyArtifactsMutation, requestHandlers.bulkDestroyArtifactsMutation],
+ ]),
+ provide: {
+ projectPath: 'project/path',
+ projectId,
+ canDestroyArtifacts,
+ artifactsManagementFeedbackImagePath: 'banner/image/path',
+ glFeatures,
+ },
+ mocks: {
+ $toast: {
+ show: mockToastShow,
+ },
+ },
+ data() {
+ return data;
+ },
+ });
+ };
+
+ it('renders feedback banner', () => {
+ createComponent();
+
+ expect(findBanner().exists()).toBe(true);
+ });
+
+ it('when loading, shows a loading state', () => {
+ createComponent();
+
+ expect(findLoadingState().exists()).toBe(true);
+ });
+
+ it('on error, shows an alert', async () => {
+ createComponent({
+ handlers: {
+ getJobArtifactsQuery: jest.fn().mockRejectedValue(new Error('Error!')),
+ },
+ });
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({ message: I18N_FETCH_ERROR });
+ });
+
+ it('with data, renders the table', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findTable().exists()).toBe(true);
+ });
+
+ describe('job details', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('shows the artifact count', () => {
+ expect(findCount().text()).toBe(`${job.artifacts.nodes.length} files`);
+ });
+
+ it('shows the job status as an icon for a successful job', () => {
+ expect(findSuccessfulJobStatus().findComponent(CiIcon).exists()).toBe(true);
+ expect(findSuccessfulJobStatus().findComponent(GlBadge).exists()).toBe(false);
+ });
+
+ it('shows the job status as a badge for other job statuses', () => {
+ expect(findFailedJobStatus().findComponent(GlBadge).exists()).toBe(true);
+ expect(findFailedJobStatus().findComponent(CiIcon).exists()).toBe(false);
+ });
+
+ it('shows links to the job, pipeline, ref, and commit', () => {
+ expect(findJobLink().text()).toBe(job.name);
+ expect(findJobLink().attributes('href')).toBe(job.webPath);
+
+ expect(findPipelineLink().text()).toBe(`#${getIdFromGraphQLId(job.pipeline.id)}`);
+ expect(findPipelineLink().attributes('href')).toBe(job.pipeline.path);
+
+ expect(findRefLink().text()).toBe(job.refName);
+ expect(findRefLink().attributes('href')).toBe(job.refPath);
+
+ expect(findCommitLink().text()).toBe(job.shortSha);
+ expect(findCommitLink().attributes('href')).toBe(job.commitPath);
+ });
+
+ it('shows the total size of artifacts', () => {
+ expect(findSize().text()).toBe(totalArtifactsSizeForJob(job));
+ });
+
+ it('shows the created time', () => {
+ expect(findCreated().text()).toBe('5 years ago');
+ });
+
+ describe('row expansion', () => {
+ it('toggles the visibility of the row details', async () => {
+ expect(findDetailsRows().length).toBe(0);
+
+ findCount().trigger('click');
+ await nextTick();
+
+ expect(findDetailsRows().length).toBe(1);
+
+ findCount().trigger('click');
+ await nextTick();
+
+ expect(findDetailsRows().length).toBe(0);
+ });
+
+ it('expands and collapses jobs', async () => {
+ // both jobs start collapsed
+ expect(findDetailsInRow(0).exists()).toBe(false);
+ expect(findDetailsInRow(1).exists()).toBe(false);
+
+ findCountAt(0).trigger('click');
+ await nextTick();
+
+ // first job is expanded, second row has its details
+ expect(findDetailsInRow(0).exists()).toBe(false);
+ expect(findDetailsInRow(1).exists()).toBe(true);
+ expect(findDetailsInRow(2).exists()).toBe(false);
+
+ findCountAt(1).trigger('click');
+ await nextTick();
+
+ // both jobs are expanded, each has details below it
+ expect(findDetailsInRow(0).exists()).toBe(false);
+ expect(findDetailsInRow(1).exists()).toBe(true);
+ expect(findDetailsInRow(2).exists()).toBe(false);
+ expect(findDetailsInRow(3).exists()).toBe(true);
+
+ findCountAt(0).trigger('click');
+ await nextTick();
+
+ // first job collapsed, second job expanded
+ expect(findDetailsInRow(0).exists()).toBe(false);
+ expect(findDetailsInRow(1).exists()).toBe(false);
+ expect(findDetailsInRow(2).exists()).toBe(true);
+ });
+
+ it('keeps the job expanded when an artifact is deleted', async () => {
+ findCount().trigger('click');
+ await waitForPromises();
+
+ expect(findDetailsInRow(0).exists()).toBe(false);
+ expect(findDetailsInRow(1).exists()).toBe(true);
+
+ findArtifactDeleteButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findDeleteModal().findComponent(GlModal).props('visible')).toBe(true);
+
+ findDeleteModal().vm.$emit('primary');
+ await waitForPromises();
+
+ expect(findDetailsInRow(0).exists()).toBe(false);
+ expect(findDetailsInRow(1).exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('download button', () => {
+ it('is a link to the download path for the archive artifact', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findDownloadButton().attributes('href')).toBe(archiveArtifact.downloadPath);
+ });
+
+ it('is disabled when there is no download path', async () => {
+ const jobWithoutDownloadPath = {
+ ...job,
+ archive: { downloadPath: null },
+ };
+
+ createComponent({
+ handlers: { getJobArtifactsQuery: jest.fn() },
+ data: { jobArtifacts: [jobWithoutDownloadPath] },
+ });
+
+ await waitForPromises();
+
+ expect(findDownloadButton().attributes('disabled')).toBeDefined();
+ });
+ });
+
+ describe('browse button', () => {
+ it('is a link to the browse path for the job', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findBrowseButton().attributes('href')).toBe(job.browseArtifactsPath);
+ });
+
+ it('is disabled when there is no browse path', async () => {
+ const jobWithoutBrowsePath = {
+ ...job,
+ browseArtifactsPath: null,
+ };
+
+ createComponent({
+ handlers: { getJobArtifactsQuery: jest.fn() },
+ data: { jobArtifacts: [jobWithoutBrowsePath] },
+ });
+
+ await waitForPromises();
+
+ expect(findBrowseButton().attributes('disabled')).toBeDefined();
+ });
+ });
+
+ describe('delete button', () => {
+ const artifactsFromJob = job.artifacts.nodes.map((node) => node.id);
+
+ describe('with delete permission and bulk delete feature flag enabled', () => {
+ beforeEach(async () => {
+ createComponent({
+ canDestroyArtifacts: true,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
+ });
+
+ await waitForPromises();
+ });
+
+ it('opens the confirmation modal with the artifacts from the job', async () => {
+ await findDeleteButton().vm.$emit('click');
+
+ expect(findBulkDeleteModal().props()).toMatchObject({
+ visible: true,
+ artifactsToDelete: artifactsFromJob,
+ });
+ });
+
+ it('on confirm, deletes the artifacts from the job and shows a toast', async () => {
+ findDeleteButton().vm.$emit('click');
+ findBulkDeleteModal().vm.$emit('primary');
+
+ expect(bulkDestroyMutationHandler).toHaveBeenCalledWith({
+ projectId: convertToGraphQLId(TYPENAME_PROJECT, projectId),
+ ids: artifactsFromJob,
+ });
+
+ await waitForPromises();
+
+ expect(mockToastShow).toHaveBeenCalledWith(
+ `${artifactsFromJob.length} selected artifacts deleted`,
+ );
+ });
+
+ it('does not clear selected artifacts on success', async () => {
+ // select job 2 via checkbox
+ findJobCheckbox(2).vm.$emit('input', true);
+
+ // click delete button job 1
+ findDeleteButton().vm.$emit('click');
+
+ // job 2's artifacts should still be selected
+ expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(
+ job2.artifacts.nodes.map((node) => node.id),
+ );
+
+ // confirm delete
+ findBulkDeleteModal().vm.$emit('primary');
+
+ // job 1's artifacts should be deleted
+ expect(bulkDestroyMutationHandler).toHaveBeenCalledWith({
+ projectId: convertToGraphQLId(TYPENAME_PROJECT, projectId),
+ ids: artifactsFromJob,
+ });
+
+ await waitForPromises();
+
+ // job 2's artifacts should still be selected
+ expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(
+ job2.artifacts.nodes.map((node) => node.id),
+ );
+ });
+ });
+
+ it('shows an alert and does not clear selected artifacts on error', async () => {
+ createComponent({
+ canDestroyArtifacts: true,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
+ handlers: {
+ getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponse),
+ bulkDestroyArtifactsMutation: jest.fn().mockRejectedValue(),
+ },
+ });
+ await waitForPromises();
+
+ // select job 2 via checkbox
+ findJobCheckbox(2).vm.$emit('input', true);
+
+ // click delete button job 1
+ findDeleteButton().vm.$emit('click');
+
+ // confirm delete
+ findBulkDeleteModal().vm.$emit('primary');
+
+ await waitForPromises();
+
+ // job 2's artifacts should still be selected
+ expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(
+ job2.artifacts.nodes.map((node) => node.id),
+ );
+ expect(createAlert).toHaveBeenCalledWith({
+ captureError: true,
+ error: expect.any(Error),
+ message: I18N_BULK_DELETE_ERROR,
+ });
+ });
+
+ it('is disabled when bulk delete feature flag is disabled', async () => {
+ createComponent({
+ canDestroyArtifacts: true,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: false },
+ });
+
+ await waitForPromises();
+
+ expect(findDeleteButton().attributes('disabled')).toBeDefined();
+ });
+
+ it('is hidden when user does not have delete permission', async () => {
+ createComponent({
+ canDestroyArtifacts: false,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: false },
+ });
+
+ await waitForPromises();
+
+ expect(findDeleteButton().exists()).toBe(false);
+ });
+ });
+
+ describe('bulk delete', () => {
+ const selectedArtifacts = job.artifacts.nodes.map((node) => node.id);
+
+ describe('with permission and feature flag enabled', () => {
+ beforeEach(async () => {
+ createComponent({
+ canDestroyArtifacts: true,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
+ });
+
+ await waitForPromises();
+ });
+
+ it('shows selected artifacts when a job is checked', async () => {
+ expect(findBulkDeleteContainer().exists()).toBe(false);
+
+ await findJobCheckbox().vm.$emit('input', true);
+
+ expect(findBulkDeleteContainer().exists()).toBe(true);
+ expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(selectedArtifacts);
+ });
+
+ it('disappears when selected artifacts are cleared', async () => {
+ await findJobCheckbox().vm.$emit('input', true);
+
+ expect(findBulkDeleteContainer().exists()).toBe(true);
+
+ await findBulkDelete().vm.$emit('clearSelectedArtifacts');
+
+ expect(findBulkDeleteContainer().exists()).toBe(false);
+ });
+
+ it('shows a modal to confirm bulk delete', async () => {
+ findJobCheckbox().vm.$emit('input', true);
+ findBulkDelete().vm.$emit('showBulkDeleteModal');
+
+ await nextTick();
+
+ expect(findBulkDeleteModal().props('visible')).toBe(true);
+ });
+
+ it('deletes the selected artifacts and shows a toast', async () => {
+ findJobCheckbox().vm.$emit('input', true);
+ findBulkDelete().vm.$emit('showBulkDeleteModal');
+ findBulkDeleteModal().vm.$emit('primary');
+
+ expect(bulkDestroyMutationHandler).toHaveBeenCalledWith({
+ projectId: convertToGraphQLId(TYPENAME_PROJECT, projectId),
+ ids: selectedArtifacts,
+ });
+
+ await waitForPromises();
+
+ expect(mockToastShow).toHaveBeenCalledWith(
+ `${selectedArtifacts.length} selected artifacts deleted`,
+ );
+ });
+
+ it('clears selected artifacts on success', async () => {
+ findJobCheckbox().vm.$emit('input', true);
+ findBulkDelete().vm.$emit('showBulkDeleteModal');
+ findBulkDeleteModal().vm.$emit('primary');
+
+ await waitForPromises();
+
+ expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([]);
+ });
+ });
+
+ describe('when the selected artifacts limit is reached', () => {
+ beforeEach(async () => {
+ createComponent({
+ canDestroyArtifacts: true,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
+ data: { selectedArtifacts: maxSelectedArtifacts },
+ });
+
+ await nextTick();
+ });
+
+ it('passes isSelectedArtifactsLimitReached to bulk delete', () => {
+ expect(findBulkDelete().props('isSelectedArtifactsLimitReached')).toBe(true);
+ });
+
+ it('passes isSelectedArtifactsLimitReached to job checkbox', () => {
+ expect(wrapper.findComponent(JobCheckbox).props('isSelectedArtifactsLimitReached')).toBe(
+ true,
+ );
+ });
+
+ it('passes isSelectedArtifactsLimitReached to table row details', async () => {
+ findCount().trigger('click');
+ await nextTick();
+
+ expect(findDetailsInRow(1).props('isSelectedArtifactsLimitReached')).toBe(true);
+ });
+ });
+
+ it('shows an alert and does not clear selected artifacts on error', async () => {
+ createComponent({
+ canDestroyArtifacts: true,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
+ handlers: {
+ getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponse),
+ bulkDestroyArtifactsMutation: jest.fn().mockRejectedValue(),
+ },
+ });
+
+ await waitForPromises();
+
+ findJobCheckbox().vm.$emit('input', true);
+ findBulkDelete().vm.$emit('showBulkDeleteModal');
+ findBulkDeleteModal().vm.$emit('primary');
+
+ await waitForPromises();
+
+ expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(selectedArtifacts);
+ expect(createAlert).toHaveBeenCalledWith({
+ captureError: true,
+ error: expect.any(Error),
+ message: I18N_BULK_DELETE_ERROR,
+ });
+ });
+
+ it('shows no checkboxes without permission', async () => {
+ createComponent({
+ canDestroyArtifacts: false,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
+ });
+
+ await waitForPromises();
+
+ expect(findAnyCheckbox().exists()).toBe(false);
+ });
+
+ it('shows no checkboxes with feature flag disabled', async () => {
+ createComponent({
+ canDestroyArtifacts: true,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: false },
+ });
+
+ await waitForPromises();
+
+ expect(findAnyCheckbox().exists()).toBe(false);
+ });
+ });
+
+ describe('pagination', () => {
+ const { pageInfo } = getJobArtifactsResponseThatPaginates.data.project.jobs;
+ const query = jest.fn().mockResolvedValue(getJobArtifactsResponseThatPaginates);
+
+ beforeEach(async () => {
+ createComponent({
+ handlers: {
+ getJobArtifactsQuery: query,
+ },
+ data: { pageInfo },
+ });
+
+ await nextTick();
+ });
+
+ it('renders pagination and passes page props', () => {
+ expect(findPagination().props()).toMatchObject({
+ value: INITIAL_CURRENT_PAGE,
+ prevPage: Number(pageInfo.hasPreviousPage),
+ nextPage: Number(pageInfo.hasNextPage),
+ });
+
+ expect(query).toHaveBeenCalledWith({
+ projectPath: 'project/path',
+ firstPageSize: JOBS_PER_PAGE,
+ lastPageSize: null,
+ nextPageCursor: '',
+ prevPageCursor: '',
+ });
+ });
+
+ it('updates query variables when going to previous page', async () => {
+ await setPage(1);
+
+ expect(query).toHaveBeenLastCalledWith({
+ projectPath: 'project/path',
+ firstPageSize: null,
+ lastPageSize: JOBS_PER_PAGE,
+ prevPageCursor: pageInfo.startCursor,
+ });
+ expect(findPagination().props('value')).toEqual(1);
+ });
+
+ it('updates query variables when going to next page', async () => {
+ await setPage(2);
+
+ expect(query).toHaveBeenLastCalledWith({
+ projectPath: 'project/path',
+ firstPageSize: JOBS_PER_PAGE,
+ lastPageSize: null,
+ prevPageCursor: '',
+ nextPageCursor: pageInfo.endCursor,
+ });
+ expect(findPagination().props('value')).toEqual(2);
+ });
+ });
+});
diff --git a/spec/frontend/ci/artifacts/components/job_checkbox_spec.js b/spec/frontend/ci/artifacts/components/job_checkbox_spec.js
new file mode 100644
index 00000000000..8b47571239c
--- /dev/null
+++ b/spec/frontend/ci/artifacts/components/job_checkbox_spec.js
@@ -0,0 +1,132 @@
+import { GlFormCheckbox } from '@gitlab/ui';
+import mockGetJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import JobCheckbox from '~/ci/artifacts/components/job_checkbox.vue';
+import { I18N_BULK_DELETE_MAX_SELECTED } from '~/ci/artifacts/constants';
+
+describe('JobCheckbox component', () => {
+ let wrapper;
+
+ const mockArtifactNodes = mockGetJobArtifactsResponse.data.project.jobs.nodes[0].artifacts.nodes;
+ const mockSelectedArtifacts = [mockArtifactNodes[0], mockArtifactNodes[1]];
+ const mockUnselectedArtifacts = [mockArtifactNodes[2]];
+
+ const findCheckbox = () => wrapper.findComponent(GlFormCheckbox);
+
+ const createComponent = ({
+ hasArtifacts = true,
+ selectedArtifacts = mockSelectedArtifacts,
+ unselectedArtifacts = mockUnselectedArtifacts,
+ isSelectedArtifactsLimitReached = false,
+ } = {}) => {
+ wrapper = shallowMountExtended(JobCheckbox, {
+ propsData: {
+ hasArtifacts,
+ selectedArtifacts,
+ unselectedArtifacts,
+ isSelectedArtifactsLimitReached,
+ },
+ mocks: { GlFormCheckbox },
+ });
+ };
+
+ it('is disabled when the job has no artifacts', () => {
+ createComponent({ hasArtifacts: false });
+
+ expect(findCheckbox().attributes('disabled')).toBeDefined();
+ });
+
+ describe('when some artifacts from this job are selected', () => {
+ describe('when the selected artifacts limit has not been reached', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('is indeterminate', () => {
+ expect(findCheckbox().attributes('indeterminate')).toBe('true');
+ expect(findCheckbox().attributes('checked')).toBeUndefined();
+ });
+
+ it('selects the unselected artifacts on click', () => {
+ findCheckbox().vm.$emit('input', true);
+
+ expect(wrapper.emitted('selectArtifact')).toMatchObject([
+ [mockUnselectedArtifacts[0], true],
+ ]);
+ });
+ });
+
+ describe('when the selected artifacts limit has been reached', () => {
+ beforeEach(() => {
+ // limit has been reached by selecting artifacts from this job
+ createComponent({
+ selectedArtifacts: mockSelectedArtifacts,
+ isSelectedArtifactsLimitReached: true,
+ });
+ });
+
+ it('remains enabled', () => {
+ // job checkbox remains enabled to allow de-selection
+ expect(findCheckbox().attributes('disabled')).toBeUndefined();
+ expect(findCheckbox().attributes('title')).not.toBe(I18N_BULK_DELETE_MAX_SELECTED);
+ });
+ });
+ });
+
+ describe('when all artifacts from this job are selected', () => {
+ beforeEach(() => {
+ createComponent({ unselectedArtifacts: [] });
+ });
+
+ it('is checked', () => {
+ expect(findCheckbox().attributes('checked')).toBe('true');
+ });
+
+ it('deselects the selected artifacts on click', () => {
+ findCheckbox().vm.$emit('input', false);
+
+ expect(wrapper.emitted('selectArtifact')).toMatchObject([
+ [mockSelectedArtifacts[0], false],
+ [mockSelectedArtifacts[1], false],
+ ]);
+ });
+ });
+
+ describe('when no artifacts from this job are selected', () => {
+ describe('when the selected artifacts limit has not been reached', () => {
+ beforeEach(() => {
+ createComponent({ selectedArtifacts: [] });
+ });
+
+ it('is enabled and not checked', () => {
+ expect(findCheckbox().attributes('checked')).toBeUndefined();
+ expect(findCheckbox().attributes('disabled')).toBeUndefined();
+ expect(findCheckbox().attributes('title')).toBe('');
+ });
+
+ it('selects the artifacts on click', () => {
+ findCheckbox().vm.$emit('input', true);
+
+ expect(wrapper.emitted('selectArtifact')).toMatchObject([
+ [mockUnselectedArtifacts[0], true],
+ ]);
+ });
+ });
+
+ describe('when the selected artifacts limit has been reached', () => {
+ beforeEach(() => {
+ // limit has been reached by selecting artifacts from other jobs
+ createComponent({
+ selectedArtifacts: [],
+ isSelectedArtifactsLimitReached: true,
+ });
+ });
+
+ it('is disabled when the selected artifacts limit has been reached', () => {
+ // job checkbox is disabled to block further selection
+ expect(findCheckbox().attributes('disabled')).toBeDefined();
+ expect(findCheckbox().attributes('title')).toBe(I18N_BULK_DELETE_MAX_SELECTED);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/artifacts/graphql/cache_update_spec.js b/spec/frontend/ci/artifacts/graphql/cache_update_spec.js
new file mode 100644
index 00000000000..3c415534c7c
--- /dev/null
+++ b/spec/frontend/ci/artifacts/graphql/cache_update_spec.js
@@ -0,0 +1,67 @@
+import getJobArtifactsQuery from '~/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql';
+import { removeArtifactFromStore } from '~/ci/artifacts/graphql/cache_update';
+
+describe('Artifact table cache updates', () => {
+ let store;
+
+ const cacheMock = {
+ project: {
+ jobs: {
+ nodes: [
+ { artifacts: { nodes: [{ id: 'foo' }] } },
+ { artifacts: { nodes: [{ id: 'bar' }] } },
+ ],
+ },
+ },
+ };
+
+ const query = getJobArtifactsQuery;
+ const variables = { fullPath: 'path/to/project' };
+
+ beforeEach(() => {
+ store = {
+ readQuery: jest.fn().mockReturnValue(cacheMock),
+ writeQuery: jest.fn(),
+ };
+ });
+
+ describe('removeArtifactFromStore', () => {
+ it('calls readQuery', () => {
+ removeArtifactFromStore(store, 'foo', query, variables);
+ expect(store.readQuery).toHaveBeenCalledWith({ query, variables });
+ });
+
+ it('writes the correct result in the cache', () => {
+ removeArtifactFromStore(store, 'foo', query, variables);
+ expect(store.writeQuery).toHaveBeenCalledWith({
+ query,
+ variables,
+ data: {
+ project: {
+ jobs: {
+ nodes: [{ artifacts: { nodes: [] } }, { artifacts: { nodes: [{ id: 'bar' }] } }],
+ },
+ },
+ },
+ });
+ });
+
+ it('does not remove an unknown artifact', () => {
+ removeArtifactFromStore(store, 'baz', query, variables);
+ expect(store.writeQuery).toHaveBeenCalledWith({
+ query,
+ variables,
+ data: {
+ project: {
+ jobs: {
+ nodes: [
+ { artifacts: { nodes: [{ id: 'foo' }] } },
+ { artifacts: { nodes: [{ id: 'bar' }] } },
+ ],
+ },
+ },
+ },
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/ci_lint/components/ci_lint_spec.js b/spec/frontend/ci/ci_lint/components/ci_lint_spec.js
index d4f588a0e09..4b7ca36f331 100644
--- a/spec/frontend/ci/ci_lint/components/ci_lint_spec.js
+++ b/spec/frontend/ci/ci_lint/components/ci_lint_spec.js
@@ -48,7 +48,6 @@ describe('CI Lint', () => {
afterEach(() => {
mockMutate.mockClear();
- wrapper.destroy();
});
it('displays the editor', () => {
diff --git a/spec/frontend/ci/ci_variable_list/ci_variable_list/ci_variable_list_spec.js b/spec/frontend/ci/ci_variable_list/ci_variable_list/ci_variable_list_spec.js
index e4abedb412f..8990a70d4ef 100644
--- a/spec/frontend/ci/ci_variable_list/ci_variable_list/ci_variable_list_spec.js
+++ b/spec/frontend/ci/ci_variable_list/ci_variable_list/ci_variable_list_spec.js
@@ -1,5 +1,7 @@
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlPipelineSchedulesEdit from 'test_fixtures/pipeline_schedules/edit.html';
+import htmlPipelineSchedulesEditWithVariables from 'test_fixtures/pipeline_schedules/edit_with_variables.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import VariableList from '~/ci/ci_variable_list/ci_variable_list';
const HIDE_CLASS = 'hide';
@@ -11,7 +13,7 @@ describe('VariableList', () => {
describe('with only key/value inputs', () => {
describe('with no variables', () => {
beforeEach(() => {
- loadHTMLFixture('pipeline_schedules/edit.html');
+ setHTMLFixture(htmlPipelineSchedulesEdit);
$wrapper = $('.js-ci-variable-list-section');
variableList = new VariableList({
@@ -69,7 +71,7 @@ describe('VariableList', () => {
describe('with persisted variables', () => {
beforeEach(() => {
- loadHTMLFixture('pipeline_schedules/edit_with_variables.html');
+ setHTMLFixture(htmlPipelineSchedulesEditWithVariables);
$wrapper = $('.js-ci-variable-list-section');
variableList = new VariableList({
@@ -106,7 +108,7 @@ describe('VariableList', () => {
describe('toggleEnableRow method', () => {
beforeEach(() => {
- loadHTMLFixture('pipeline_schedules/edit_with_variables.html');
+ setHTMLFixture(htmlPipelineSchedulesEditWithVariables);
$wrapper = $('.js-ci-variable-list-section');
variableList = new VariableList({
diff --git a/spec/frontend/ci/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js b/spec/frontend/ci/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js
index 71e8e6d3afb..3ef5427f288 100644
--- a/spec/frontend/ci/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js
+++ b/spec/frontend/ci/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js
@@ -1,12 +1,13 @@
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlPipelineSchedulesEdit from 'test_fixtures/pipeline_schedules/edit.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import setupNativeFormVariableList from '~/ci/ci_variable_list/native_form_variable_list';
describe('NativeFormVariableList', () => {
let $wrapper;
beforeEach(() => {
- loadHTMLFixture('pipeline_schedules/edit.html');
+ setHTMLFixture(htmlPipelineSchedulesEdit);
$wrapper = $('.js-ci-variable-list-section');
setupNativeFormVariableList({
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js
index 5e0c35c9f90..1d0dcf242a4 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js
@@ -1,7 +1,16 @@
import { shallowMount } from '@vue/test-utils';
+import {
+ ADD_MUTATION_ACTION,
+ DELETE_MUTATION_ACTION,
+ UPDATE_MUTATION_ACTION,
+} from '~/ci/ci_variable_list/constants';
import ciAdminVariables from '~/ci/ci_variable_list/components/ci_admin_variables.vue';
import ciVariableShared from '~/ci/ci_variable_list/components/ci_variable_shared.vue';
+import addAdminVariable from '~/ci/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql';
+import deleteAdminVariable from '~/ci/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql';
+import updateAdminVariable from '~/ci/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql';
+import getAdminVariables from '~/ci/ci_variable_list/graphql/queries/variables.query.graphql';
describe('Ci Project Variable wrapper', () => {
let wrapper;
@@ -16,18 +25,23 @@ describe('Ci Project Variable wrapper', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Passes down the correct props to ci_variable_shared', () => {
expect(findCiShared().props()).toEqual({
areScopedVariablesAvailable: false,
componentName: 'InstanceVariables',
entity: '',
hideEnvironmentScope: true,
- mutationData: wrapper.vm.$options.mutationData,
- queryData: wrapper.vm.$options.queryData,
+ mutationData: {
+ [ADD_MUTATION_ACTION]: addAdminVariable,
+ [UPDATE_MUTATION_ACTION]: updateAdminVariable,
+ [DELETE_MUTATION_ACTION]: deleteAdminVariable,
+ },
+ queryData: {
+ ciVariables: {
+ lookup: expect.any(Function),
+ query: getAdminVariables,
+ },
+ },
refetchAfterMutation: true,
fullPath: null,
id: null,
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js
index 2fd395a1230..1937e3b34b7 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js
@@ -1,13 +1,17 @@
import { GlListboxItem, GlCollapsibleListbox, GlDropdownItem, GlIcon } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import { allEnvironments } from '~/ci/ci_variable_list/constants';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { allEnvironments, ENVIRONMENT_QUERY_LIMIT } from '~/ci/ci_variable_list/constants';
import CiEnvironmentsDropdown from '~/ci/ci_variable_list/components/ci_environments_dropdown.vue';
describe('Ci environments dropdown', () => {
let wrapper;
const envs = ['dev', 'prod', 'staging'];
- const defaultProps = { environments: envs, selectedEnvironmentScope: '' };
+ const defaultProps = {
+ areEnvironmentsLoading: false,
+ environments: envs,
+ selectedEnvironmentScope: '',
+ };
const findAllListboxItems = () => wrapper.findAllComponents(GlListboxItem);
const findListboxItemByIndex = (index) => wrapper.findAllComponents(GlListboxItem).at(index);
@@ -15,22 +19,24 @@ describe('Ci environments dropdown', () => {
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findListboxText = () => findListbox().props('toggleText');
const findCreateWildcardButton = () => wrapper.findComponent(GlDropdownItem);
+ const findMaxEnvNote = () => wrapper.findByTestId('max-envs-notice');
- const createComponent = ({ props = {}, searchTerm = '' } = {}) => {
- wrapper = mount(CiEnvironmentsDropdown, {
+ const createComponent = ({ props = {}, searchTerm = '', enableFeatureFlag = false } = {}) => {
+ wrapper = mountExtended(CiEnvironmentsDropdown, {
propsData: {
...defaultProps,
...props,
},
+ provide: {
+ glFeatures: {
+ ciLimitEnvironmentScope: enableFeatureFlag,
+ },
+ },
});
findListbox().vm.$emit('search', searchTerm);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('No environments found', () => {
beforeEach(() => {
createComponent({ searchTerm: 'stable' });
@@ -44,19 +50,32 @@ describe('Ci environments dropdown', () => {
});
describe('Search term is empty', () => {
- beforeEach(() => {
- createComponent({ props: { environments: envs } });
- });
+ describe.each`
+ featureFlag | flagStatus | defaultEnvStatus | firstItemValue | envIndices
+ ${true} | ${'enabled'} | ${'prepends'} | ${'*'} | ${[1, 2, 3]}
+ ${false} | ${'disabled'} | ${'does not prepend'} | ${envs[0]} | ${[0, 1, 2]}
+ `(
+ 'when ciLimitEnvironmentScope feature flag is $flagStatus',
+ ({ featureFlag, defaultEnvStatus, firstItemValue, envIndices }) => {
+ beforeEach(() => {
+ createComponent({ props: { environments: envs }, enableFeatureFlag: featureFlag });
+ });
- it('renders all environments when search term is empty', () => {
- expect(findListboxItemByIndex(0).text()).toBe(envs[0]);
- expect(findListboxItemByIndex(1).text()).toBe(envs[1]);
- expect(findListboxItemByIndex(2).text()).toBe(envs[2]);
- });
+ it(`${defaultEnvStatus} * in listbox`, () => {
+ expect(findListboxItemByIndex(0).text()).toBe(firstItemValue);
+ });
- it('does not display active checkmark on the inactive stage', () => {
- expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(true);
- });
+ it('renders all environments', () => {
+ expect(findListboxItemByIndex(envIndices[0]).text()).toBe(envs[0]);
+ expect(findListboxItemByIndex(envIndices[1]).text()).toBe(envs[1]);
+ expect(findListboxItemByIndex(envIndices[2]).text()).toBe(envs[2]);
+ });
+
+ it('does not display active checkmark', () => {
+ expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(true);
+ });
+ },
+ );
});
describe('when `*` is the value of selectedEnvironmentScope props', () => {
@@ -72,46 +91,92 @@ describe('Ci environments dropdown', () => {
});
});
- describe('Environments found', () => {
+ describe('When ciLimitEnvironmentScope feature flag is disabled', () => {
const currentEnv = envs[2];
beforeEach(() => {
- createComponent({ searchTerm: currentEnv });
+ createComponent();
});
- it('renders only the environment searched for', () => {
+ it('filters on the frontend and renders only the environment searched for', async () => {
+ await findListbox().vm.$emit('search', currentEnv);
+
expect(findAllListboxItems()).toHaveLength(1);
expect(findListboxItemByIndex(0).text()).toBe(currentEnv);
});
- it('does not display create button', () => {
- expect(findCreateWildcardButton().exists()).toBe(false);
+ it('does not emit event when searching', async () => {
+ expect(wrapper.emitted('search-environment-scope')).toBeUndefined();
+
+ await findListbox().vm.$emit('search', currentEnv);
+
+ expect(wrapper.emitted('search-environment-scope')).toBeUndefined();
});
- describe('Custom events', () => {
- describe('when selecting an environment', () => {
- const itemIndex = 0;
+ it('does not display note about max environments shown', () => {
+ expect(findMaxEnvNote().exists()).toBe(false);
+ });
+ });
- beforeEach(() => {
- createComponent();
- });
+ describe('When ciLimitEnvironmentScope feature flag is enabled', () => {
+ const currentEnv = envs[2];
- it('emits `select-environment` when an environment is clicked', () => {
- findListbox().vm.$emit('select', envs[itemIndex]);
- expect(wrapper.emitted('select-environment')).toEqual([[envs[itemIndex]]]);
- });
+ beforeEach(() => {
+ createComponent({ enableFeatureFlag: true });
+ });
+
+ it('renders environments passed down to it', async () => {
+ await findListbox().vm.$emit('search', currentEnv);
+
+ expect(findAllListboxItems()).toHaveLength(envs.length);
+ });
+
+ it('emits event when searching', async () => {
+ expect(wrapper.emitted('search-environment-scope')).toHaveLength(1);
+
+ await findListbox().vm.$emit('search', currentEnv);
+
+ expect(wrapper.emitted('search-environment-scope')).toHaveLength(2);
+ expect(wrapper.emitted('search-environment-scope')[1]).toEqual([currentEnv]);
+ });
+
+ it('renders loading icon while search query is loading', () => {
+ createComponent({ enableFeatureFlag: true, props: { areEnvironmentsLoading: true } });
+
+ expect(findListbox().props('searching')).toBe(true);
+ });
+
+ it('displays note about max environments shown', () => {
+ expect(findMaxEnvNote().exists()).toBe(true);
+ expect(findMaxEnvNote().text()).toContain(String(ENVIRONMENT_QUERY_LIMIT));
+ });
+ });
+
+ describe('Custom events', () => {
+ describe('when selecting an environment', () => {
+ const itemIndex = 0;
+
+ beforeEach(() => {
+ createComponent();
});
- describe('when creating a new environment from a search term', () => {
- const search = 'new-env';
- beforeEach(() => {
- createComponent({ searchTerm: search });
- });
+ it('emits `select-environment` when an environment is clicked', () => {
+ findListbox().vm.$emit('select', envs[itemIndex]);
- it('emits create-environment-scope', () => {
- findCreateWildcardButton().vm.$emit('click');
- expect(wrapper.emitted('create-environment-scope')).toEqual([[search]]);
- });
+ expect(wrapper.emitted('select-environment')).toEqual([[envs[itemIndex]]]);
+ });
+ });
+
+ describe('when creating a new environment from a search term', () => {
+ const search = 'new-env';
+ beforeEach(() => {
+ createComponent({ searchTerm: search });
+ });
+
+ it('emits create-environment-scope', () => {
+ findCreateWildcardButton().vm.$emit('click');
+
+ expect(wrapper.emitted('create-environment-scope')).toEqual([[search]]);
});
});
});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js
index c0fb133b9b1..7436210fe70 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js
@@ -4,6 +4,15 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
import ciGroupVariables from '~/ci/ci_variable_list/components/ci_group_variables.vue';
import ciVariableShared from '~/ci/ci_variable_list/components/ci_variable_shared.vue';
+import {
+ ADD_MUTATION_ACTION,
+ DELETE_MUTATION_ACTION,
+ UPDATE_MUTATION_ACTION,
+} from '~/ci/ci_variable_list/constants';
+import getGroupVariables from '~/ci/ci_variable_list/graphql/queries/group_variables.query.graphql';
+import addGroupVariable from '~/ci/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql';
+import deleteGroupVariable from '~/ci/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql';
+import updateGroupVariable from '~/ci/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql';
const mockProvide = {
glFeatures: {
@@ -24,10 +33,6 @@ describe('Ci Group Variable wrapper', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Props', () => {
beforeEach(() => {
createComponent();
@@ -41,8 +46,17 @@ describe('Ci Group Variable wrapper', () => {
entity: 'group',
fullPath: mockProvide.groupPath,
hideEnvironmentScope: false,
- mutationData: wrapper.vm.$options.mutationData,
- queryData: wrapper.vm.$options.queryData,
+ mutationData: {
+ [ADD_MUTATION_ACTION]: addGroupVariable,
+ [UPDATE_MUTATION_ACTION]: updateGroupVariable,
+ [DELETE_MUTATION_ACTION]: deleteGroupVariable,
+ },
+ queryData: {
+ ciVariables: {
+ lookup: expect.any(Function),
+ query: getGroupVariables,
+ },
+ },
refetchAfterMutation: false,
});
});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js
index bd1e6b17d6b..69b0d4261b2 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js
@@ -4,6 +4,16 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
import ciProjectVariables from '~/ci/ci_variable_list/components/ci_project_variables.vue';
import ciVariableShared from '~/ci/ci_variable_list/components/ci_variable_shared.vue';
+import {
+ ADD_MUTATION_ACTION,
+ DELETE_MUTATION_ACTION,
+ UPDATE_MUTATION_ACTION,
+} from '~/ci/ci_variable_list/constants';
+import getProjectEnvironments from '~/ci/ci_variable_list/graphql/queries/project_environments.query.graphql';
+import getProjectVariables from '~/ci/ci_variable_list/graphql/queries/project_variables.query.graphql';
+import addProjectVariable from '~/ci/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql';
+import deleteProjectVariable from '~/ci/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql';
+import updateProjectVariable from '~/ci/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql';
const mockProvide = {
projectFullPath: '/namespace/project',
@@ -25,10 +35,6 @@ describe('Ci Project Variable wrapper', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Passes down the correct props to ci_variable_shared', () => {
expect(findCiShared().props()).toEqual({
id: convertToGraphQLId(TYPENAME_PROJECT, mockProvide.projectId),
@@ -37,8 +43,21 @@ describe('Ci Project Variable wrapper', () => {
entity: 'project',
fullPath: mockProvide.projectFullPath,
hideEnvironmentScope: false,
- mutationData: wrapper.vm.$options.mutationData,
- queryData: wrapper.vm.$options.queryData,
+ mutationData: {
+ [ADD_MUTATION_ACTION]: addProjectVariable,
+ [UPDATE_MUTATION_ACTION]: updateProjectVariable,
+ [DELETE_MUTATION_ACTION]: deleteProjectVariable,
+ },
+ queryData: {
+ ciVariables: {
+ lookup: expect.any(Function),
+ query: getProjectVariables,
+ },
+ environments: {
+ lookup: expect.any(Function),
+ query: getProjectEnvironments,
+ },
+ },
refetchAfterMutation: false,
});
});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
index 508af964ca3..b6ffde9b33f 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
@@ -10,10 +10,12 @@ import {
EVENT_LABEL,
EVENT_ACTION,
ENVIRONMENT_SCOPE_LINK_TITLE,
+ groupString,
instanceString,
+ projectString,
variableOptions,
} from '~/ci/ci_variable_list/constants';
-import { mockVariablesWithScopes } from '../mocks';
+import { mockEnvs, mockVariablesWithScopes, mockVariablesWithUniqueScopes } from '../mocks';
import ModalStub from '../stubs';
describe('Ci variable modal', () => {
@@ -42,12 +44,13 @@ describe('Ci variable modal', () => {
};
const defaultProps = {
+ areEnvironmentsLoading: false,
areScopedVariablesAvailable: true,
environments: [],
hideEnvironmentScope: false,
mode: ADD_VARIABLE_ACTION,
selectedVariable: {},
- variable: [],
+ variables: [],
};
const createComponent = ({ mountFn = shallowMountExtended, props = {}, provide = {} } = {}) => {
@@ -85,10 +88,6 @@ describe('Ci variable modal', () => {
const findVariableTypeDropdown = () => wrapper.find('#ci-variable-type');
const findEnvironmentScopeText = () => wrapper.findByText('Environment scope');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Adding a variable', () => {
describe('when no key/value pair are present', () => {
beforeEach(() => {
@@ -96,7 +95,7 @@ describe('Ci variable modal', () => {
});
it('shows the submit button as disabled', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBe('true');
+ expect(findAddorUpdateButton().attributes('disabled')).toBeDefined();
});
});
@@ -115,7 +114,6 @@ describe('Ci variable modal', () => {
beforeEach(() => {
createComponent({ props: { selectedVariable: currentVariable } });
- jest.spyOn(wrapper.vm, '$emit');
});
it('Dispatches `add-variable` action on submit', () => {
@@ -156,7 +154,7 @@ describe('Ci variable modal', () => {
findModal().vm.$emit('shown');
});
- it('keeps the value as false', async () => {
+ it('keeps the value as false', () => {
expect(
findProtectedVariableCheckbox().attributes('data-is-protected-checked'),
).toBeUndefined();
@@ -241,7 +239,6 @@ describe('Ci variable modal', () => {
it('defaults to expanded and raw:false when adding a variable', () => {
createComponent({ props: { selectedVariable: variable } });
- jest.spyOn(wrapper.vm, '$emit');
findModal().vm.$emit('shown');
@@ -266,7 +263,6 @@ describe('Ci variable modal', () => {
mode: EDIT_VARIABLE_ACTION,
},
});
- jest.spyOn(wrapper.vm, '$emit');
findModal().vm.$emit('shown');
await findExpandedVariableCheckbox().vm.$emit('change');
@@ -305,7 +301,6 @@ describe('Ci variable modal', () => {
beforeEach(() => {
createComponent({ props: { selectedVariable: variable, mode: EDIT_VARIABLE_ACTION } });
- jest.spyOn(wrapper.vm, '$emit');
});
it('button text is Update variable when updating', () => {
@@ -353,6 +348,42 @@ describe('Ci variable modal', () => {
expect(link.attributes('title')).toBe(ENVIRONMENT_SCOPE_LINK_TITLE);
expect(link.attributes('href')).toBe(defaultProvide.environmentScopeLink);
});
+
+ describe('when feature flag is enabled', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ environments: mockEnvs,
+ variables: mockVariablesWithUniqueScopes(projectString),
+ },
+ provide: { glFeatures: { ciLimitEnvironmentScope: true } },
+ });
+ });
+
+ it('does not merge environment scope sources', () => {
+ const expectedLength = mockEnvs.length;
+
+ expect(findCiEnvironmentsDropdown().props('environments')).toHaveLength(expectedLength);
+ });
+ });
+
+ describe('when feature flag is disabled', () => {
+ const mockGroupVariables = mockVariablesWithUniqueScopes(groupString);
+ beforeEach(() => {
+ createComponent({
+ props: {
+ environments: mockEnvs,
+ variables: mockGroupVariables,
+ },
+ });
+ });
+
+ it('merges environment scope sources', () => {
+ const expectedLength = mockGroupVariables.length + mockEnvs.length;
+
+ expect(findCiEnvironmentsDropdown().props('environments')).toHaveLength(expectedLength);
+ });
+ });
});
describe('and section is hidden', () => {
@@ -476,7 +507,7 @@ describe('Ci variable modal', () => {
});
it('disables the submit button', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBe('disabled');
+ expect(findAddorUpdateButton().attributes('disabled')).toBeDefined();
});
it('shows the correct error text', () => {
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
index 32af2ec4de9..12ca9a78369 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
@@ -1,4 +1,3 @@
-import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import CiVariableSettings from '~/ci/ci_variable_list/components/ci_variable_settings.vue';
import ciVariableModal from '~/ci/ci_variable_list/components/ci_variable_modal.vue';
@@ -16,12 +15,14 @@ describe('Ci variable table', () => {
let wrapper;
const defaultProps = {
+ areEnvironmentsLoading: false,
areScopedVariablesAvailable: true,
entity: 'project',
environments: mapEnvironmentNames(mockEnvs),
hideEnvironmentScope: false,
isLoading: false,
maxVariableLimit: 5,
+ pageInfo: { after: '' },
variables: mockVariablesWithScopes(projectString),
};
@@ -37,10 +38,6 @@ describe('Ci variable table', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('props passing', () => {
it('passes props down correctly to the ci table', () => {
createComponent();
@@ -49,6 +46,7 @@ describe('Ci variable table', () => {
entity: 'project',
isLoading: defaultProps.isLoading,
maxVariableLimit: defaultProps.maxVariableLimit,
+ pageInfo: defaultProps.pageInfo,
variables: defaultProps.variables,
});
});
@@ -56,10 +54,10 @@ describe('Ci variable table', () => {
it('passes props down correctly to the ci modal', async () => {
createComponent();
- findCiVariableTable().vm.$emit('set-selected-variable');
- await nextTick();
+ await findCiVariableTable().vm.$emit('set-selected-variable');
expect(findCiVariableModal().props()).toEqual({
+ areEnvironmentsLoading: defaultProps.areEnvironmentsLoading,
areScopedVariablesAvailable: defaultProps.areScopedVariablesAvailable,
environments: defaultProps.environments,
hideEnvironmentScope: defaultProps.hideEnvironmentScope,
@@ -76,15 +74,13 @@ describe('Ci variable table', () => {
});
it('passes down ADD mode when receiving an empty variable', async () => {
- findCiVariableTable().vm.$emit('set-selected-variable');
- await nextTick();
+ await findCiVariableTable().vm.$emit('set-selected-variable');
expect(findCiVariableModal().props('mode')).toBe(ADD_VARIABLE_ACTION);
});
it('passes down EDIT mode when receiving a variable', async () => {
- findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
- await nextTick();
+ await findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
expect(findCiVariableModal().props('mode')).toBe(EDIT_VARIABLE_ACTION);
});
@@ -100,25 +96,21 @@ describe('Ci variable table', () => {
});
it('shows modal when adding a new variable', async () => {
- findCiVariableTable().vm.$emit('set-selected-variable');
- await nextTick();
+ await findCiVariableTable().vm.$emit('set-selected-variable');
expect(findCiVariableModal().exists()).toBe(true);
});
it('shows modal when updating a variable', async () => {
- findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
- await nextTick();
+ await findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
expect(findCiVariableModal().exists()).toBe(true);
});
it('hides modal when receiving the event from the modal', async () => {
- findCiVariableTable().vm.$emit('set-selected-variable');
- await nextTick();
+ await findCiVariableTable().vm.$emit('set-selected-variable');
- findCiVariableModal().vm.$emit('hideModal');
- await nextTick();
+ await findCiVariableModal().vm.$emit('hideModal');
expect(findCiVariableModal().exists()).toBe(false);
});
@@ -135,13 +127,42 @@ describe('Ci variable table', () => {
${'update-variable'}
${'delete-variable'}
`('bubbles up the $eventName event', async ({ eventName }) => {
- findCiVariableTable().vm.$emit('set-selected-variable');
- await nextTick();
+ await findCiVariableTable().vm.$emit('set-selected-variable');
- findCiVariableModal().vm.$emit(eventName, newVariable);
- await nextTick();
+ await findCiVariableModal().vm.$emit(eventName, newVariable);
expect(wrapper.emitted(eventName)).toEqual([[newVariable]]);
});
});
+
+ describe('pages events', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it.each`
+ eventName | args
+ ${'handle-prev-page'} | ${undefined}
+ ${'handle-next-page'} | ${undefined}
+ ${'sort-changed'} | ${{ sortDesc: true }}
+ `('bubbles up the $eventName event', async ({ args, eventName }) => {
+ await findCiVariableTable().vm.$emit(eventName, args);
+
+ expect(wrapper.emitted(eventName)).toEqual([[args]]);
+ });
+ });
+
+ describe('environment events', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('bubbles up the search event', async () => {
+ await findCiVariableTable().vm.$emit('set-selected-variable');
+
+ await findCiVariableModal().vm.$emit('search-environment-scope', 'staging');
+
+ expect(wrapper.emitted('search-environment-scope')).toEqual([['staging']]);
+ });
+ });
});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
index c977ae773db..a25d325f7a1 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
@@ -1,13 +1,12 @@
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { assertProps } from 'helpers/assert_props';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { resolvers } from '~/ci/ci_variable_list/graphql/settings';
-import { TYPENAME_GROUP } from '~/graphql_shared/constants';
-import { convertToGraphQLId } from '~/graphql_shared/utils';
import ciVariableShared from '~/ci/ci_variable_list/components/ci_variable_shared.vue';
import ciVariableSettings from '~/ci/ci_variable_list/components/ci_variable_settings.vue';
@@ -18,12 +17,11 @@ import getGroupVariables from '~/ci/ci_variable_list/graphql/queries/group_varia
import getProjectVariables from '~/ci/ci_variable_list/graphql/queries/project_variables.query.graphql';
import {
- ADD_MUTATION_ACTION,
- DELETE_MUTATION_ACTION,
- UPDATE_MUTATION_ACTION,
+ ENVIRONMENT_QUERY_LIMIT,
environmentFetchErrorText,
genericMutationErrorText,
variableFetchErrorText,
+ mapMutationActionToToast,
} from '~/ci/ci_variable_list/constants';
import {
@@ -41,7 +39,7 @@ import {
mockAdminVariables,
} from '../mocks';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
@@ -53,6 +51,7 @@ const mockProvide = {
const defaultProps = {
areScopedVariablesAvailable: true,
+ pageInfo: {},
hideEnvironmentScope: false,
refetchAfterMutation: false,
};
@@ -62,15 +61,22 @@ describe('Ci Variable Shared Component', () => {
let mockApollo;
let mockEnvironments;
+ let mockMutation;
+ let mockAddMutation;
+ let mockUpdateMutation;
+ let mockDeleteMutation;
let mockVariables;
+ const mockToastShow = jest.fn();
+
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findCiTable = () => wrapper.findComponent(GlTable);
const findCiSettings = () => wrapper.findComponent(ciVariableSettings);
// eslint-disable-next-line consistent-return
- async function createComponentWithApollo({
+ function createComponentWithApollo({
customHandlers = null,
+ customResolvers = null,
isLoading = false,
props = { ...createProjectProps() },
provide = {},
@@ -80,7 +86,9 @@ describe('Ci Variable Shared Component', () => {
[getProjectVariables, mockVariables],
];
- mockApollo = createMockApollo(handlers, resolvers);
+ const mutationResolvers = customResolvers || resolvers;
+
+ mockApollo = createMockApollo(handlers, mutationResolvers);
wrapper = shallowMount(ciVariableShared, {
propsData: {
@@ -93,6 +101,11 @@ describe('Ci Variable Shared Component', () => {
},
apolloProvider: mockApollo,
stubs: { ciVariableSettings, ciVariableTable },
+ mocks: {
+ $toast: {
+ show: mockToastShow,
+ },
+ },
});
if (!isLoading) {
@@ -103,347 +116,525 @@ describe('Ci Variable Shared Component', () => {
beforeEach(() => {
mockEnvironments = jest.fn();
mockVariables = jest.fn();
+ mockMutation = jest.fn();
+ mockAddMutation = jest.fn();
+ mockUpdateMutation = jest.fn();
+ mockDeleteMutation = jest.fn();
});
- describe('while queries are being fetch', () => {
- beforeEach(() => {
- createComponentWithApollo({ isLoading: true });
- });
+ describe.each`
+ isVariablePagesEnabled | text
+ ${true} | ${'enabled'}
+ ${false} | ${'disabled'}
+ `('When Pages FF is $text', ({ isVariablePagesEnabled }) => {
+ const pagesFeatureFlagProvide = isVariablePagesEnabled
+ ? { glFeatures: { ciVariablesPages: true } }
+ : {};
+
+ describe('while queries are being fetched', () => {
+ beforeEach(() => {
+ createComponentWithApollo({ isLoading: true });
+ });
- it('shows a loading icon', () => {
- expect(findLoadingIcon().exists()).toBe(true);
- expect(findCiTable().exists()).toBe(false);
+ it('shows a loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findCiTable().exists()).toBe(false);
+ });
});
- });
- describe('when queries are resolved', () => {
- describe('successfully', () => {
- beforeEach(async () => {
- mockEnvironments.mockResolvedValue(mockProjectEnvironments);
- mockVariables.mockResolvedValue(mockProjectVariables);
+ describe('when queries are resolved', () => {
+ describe('successfully', () => {
+ beforeEach(async () => {
+ mockEnvironments.mockResolvedValue(mockProjectEnvironments);
+ mockVariables.mockResolvedValue(mockProjectVariables);
- await createComponentWithApollo({ provide: createProjectProvide() });
- });
+ await createComponentWithApollo({
+ provide: { ...createProjectProvide(), ...pagesFeatureFlagProvide },
+ });
+ });
- it('passes down the expected max variable limit as props', () => {
- expect(findCiSettings().props('maxVariableLimit')).toBe(
- mockProjectVariables.data.project.ciVariables.limit,
- );
- });
+ it('passes down the expected max variable limit as props', () => {
+ expect(findCiSettings().props('maxVariableLimit')).toBe(
+ mockProjectVariables.data.project.ciVariables.limit,
+ );
+ });
- it('passes down the expected environments as props', () => {
- expect(findCiSettings().props('environments')).toEqual([prodName, devName]);
- });
+ it('passes down the expected environments as props', () => {
+ expect(findCiSettings().props('environments')).toEqual([prodName, devName]);
+ });
- it('passes down the expected variables as props', () => {
- expect(findCiSettings().props('variables')).toEqual(
- mockProjectVariables.data.project.ciVariables.nodes,
- );
- });
+ it('passes down the expected variables as props', () => {
+ expect(findCiSettings().props('variables')).toEqual(
+ mockProjectVariables.data.project.ciVariables.nodes,
+ );
+ });
- it('createAlert was not called', () => {
- expect(createAlert).not.toHaveBeenCalled();
+ it('createAlert was not called', () => {
+ expect(createAlert).not.toHaveBeenCalled();
+ });
});
- });
- describe('with an error for variables', () => {
- beforeEach(async () => {
- mockEnvironments.mockResolvedValue(mockProjectEnvironments);
- mockVariables.mockRejectedValue();
+ describe('with an error for variables', () => {
+ beforeEach(async () => {
+ mockEnvironments.mockResolvedValue(mockProjectEnvironments);
+ mockVariables.mockRejectedValue();
- await createComponentWithApollo();
- });
+ await createComponentWithApollo({ provide: pagesFeatureFlagProvide });
+ });
- it('calls createAlert with the expected error message', () => {
- expect(createAlert).toHaveBeenCalledWith({ message: variableFetchErrorText });
+ it('calls createAlert with the expected error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: variableFetchErrorText });
+ });
});
- });
- describe('with an error for environments', () => {
- beforeEach(async () => {
- mockEnvironments.mockRejectedValue();
- mockVariables.mockResolvedValue(mockProjectVariables);
+ describe('with an error for environments', () => {
+ beforeEach(async () => {
+ mockEnvironments.mockRejectedValue();
+ mockVariables.mockResolvedValue(mockProjectVariables);
- await createComponentWithApollo();
- });
+ await createComponentWithApollo({ provide: pagesFeatureFlagProvide });
+ });
- it('calls createAlert with the expected error message', () => {
- expect(createAlert).toHaveBeenCalledWith({ message: environmentFetchErrorText });
+ it('calls createAlert with the expected error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: environmentFetchErrorText });
+ });
});
});
- });
- describe('environment query', () => {
- describe('when there is an environment key in queryData', () => {
- beforeEach(async () => {
- mockEnvironments.mockResolvedValue(mockProjectEnvironments);
- mockVariables.mockResolvedValue(mockProjectVariables);
+ describe('environment query', () => {
+ describe('when there is an environment key in queryData', () => {
+ beforeEach(() => {
+ mockEnvironments.mockResolvedValue(mockProjectEnvironments);
- await createComponentWithApollo({ props: { ...createProjectProps() } });
- });
+ mockVariables.mockResolvedValue(mockProjectVariables);
+ });
- it('is executed', () => {
- expect(mockVariables).toHaveBeenCalled();
- });
- });
+ it('environments are fetched', async () => {
+ await createComponentWithApollo({
+ props: { ...createProjectProps() },
+ provide: pagesFeatureFlagProvide,
+ });
- describe('when there isnt an environment key in queryData', () => {
- beforeEach(async () => {
- mockVariables.mockResolvedValue(mockGroupVariables);
+ expect(mockEnvironments).toHaveBeenCalled();
+ });
- await createComponentWithApollo({ props: { ...createGroupProps() } });
- });
+ describe('when Limit Environment Scope FF is enabled', () => {
+ beforeEach(async () => {
+ await createComponentWithApollo({
+ props: { ...createProjectProps() },
+ provide: {
+ glFeatures: {
+ ciLimitEnvironmentScope: true,
+ ciVariablesPages: isVariablePagesEnabled,
+ },
+ },
+ });
+ });
- it('is skipped', () => {
- expect(mockVariables).not.toHaveBeenCalled();
- });
- });
- });
+ it('initial query is called with the correct variables', () => {
+ expect(mockEnvironments).toHaveBeenCalledWith({
+ first: ENVIRONMENT_QUERY_LIMIT,
+ fullPath: '/namespace/project/',
+ search: '',
+ });
+ });
- describe('mutations', () => {
- const groupProps = createGroupProps();
+ it(`refetches environments when search term is present`, async () => {
+ expect(mockEnvironments).toHaveBeenCalledTimes(1);
+ expect(mockEnvironments).toHaveBeenCalledWith(expect.objectContaining({ search: '' }));
- beforeEach(async () => {
- mockVariables.mockResolvedValue(mockGroupVariables);
+ await findCiSettings().vm.$emit('search-environment-scope', 'staging');
- await createComponentWithApollo({
- customHandlers: [[getGroupVariables, mockVariables]],
- props: groupProps,
- });
- });
- it.each`
- actionName | mutation | event
- ${'add'} | ${groupProps.mutationData[ADD_MUTATION_ACTION]} | ${'add-variable'}
- ${'update'} | ${groupProps.mutationData[UPDATE_MUTATION_ACTION]} | ${'update-variable'}
- ${'delete'} | ${groupProps.mutationData[DELETE_MUTATION_ACTION]} | ${'delete-variable'}
- `(
- 'calls the right mutation from propsData when user performs $actionName variable',
- async ({ event, mutation }) => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
-
- await findCiSettings().vm.$emit(event, newVariable);
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation,
- variables: {
- endpoint: mockProvide.endpoint,
- fullPath: groupProps.fullPath,
- id: convertToGraphQLId(TYPENAME_GROUP, groupProps.id),
- variable: newVariable,
- },
- });
- },
- );
-
- it.each`
- actionName | event
- ${'add'} | ${'add-variable'}
- ${'update'} | ${'update-variable'}
- ${'delete'} | ${'delete-variable'}
- `(
- 'throws with the specific graphql error if present when user performs $actionName variable',
- async ({ event }) => {
- const graphQLErrorMessage = 'There is a problem with this graphQL action';
- jest
- .spyOn(wrapper.vm.$apollo, 'mutate')
- .mockResolvedValue({ data: { ciVariableMutation: { errors: [graphQLErrorMessage] } } });
- await findCiSettings().vm.$emit(event, newVariable);
- await nextTick();
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
- expect(createAlert).toHaveBeenCalledWith({ message: graphQLErrorMessage });
- },
- );
-
- it.each`
- actionName | event
- ${'add'} | ${'add-variable'}
- ${'update'} | ${'update-variable'}
- ${'delete'} | ${'delete-variable'}
- `(
- 'throws generic error on failure with no graphql errors and user performs $actionName variable',
- async ({ event }) => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementationOnce(() => {
- throw new Error();
+ expect(mockEnvironments).toHaveBeenCalledTimes(2);
+ expect(mockEnvironments).toHaveBeenCalledWith(
+ expect.objectContaining({ search: 'staging' }),
+ );
+ });
});
- await findCiSettings().vm.$emit(event, newVariable);
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
- expect(createAlert).toHaveBeenCalledWith({ message: genericMutationErrorText });
- },
- );
+ describe('when Limit Environment Scope FF is disabled', () => {
+ beforeEach(async () => {
+ await createComponentWithApollo({
+ props: { ...createProjectProps() },
+ provide: pagesFeatureFlagProvide,
+ });
+ });
+
+ it('initial query is called with the correct variables', () => {
+ expect(mockEnvironments).toHaveBeenCalledWith({ fullPath: '/namespace/project/' });
+ });
- describe('without fullpath and ID props', () => {
- beforeEach(async () => {
- mockVariables.mockResolvedValue(mockAdminVariables);
+ it(`does not refetch environments when search term is present`, async () => {
+ expect(mockEnvironments).toHaveBeenCalledTimes(1);
- await createComponentWithApollo({
- customHandlers: [[getAdminVariables, mockVariables]],
- props: createInstanceProps(),
+ await findCiSettings().vm.$emit('search-environment-scope', 'staging');
+
+ expect(mockEnvironments).toHaveBeenCalledTimes(1);
+ });
});
});
- it('does not pass fullPath and ID to the mutation', async () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
+ describe("when there isn't an environment key in queryData", () => {
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockGroupVariables);
- await findCiSettings().vm.$emit('add-variable', newVariable);
+ await createComponentWithApollo({
+ props: { ...createGroupProps() },
+ provide: pagesFeatureFlagProvide,
+ });
+ });
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: wrapper.props().mutationData[ADD_MUTATION_ACTION],
- variables: {
- endpoint: mockProvide.endpoint,
- variable: newVariable,
- },
+ it('fetching environments is skipped', () => {
+ expect(mockEnvironments).not.toHaveBeenCalled();
});
});
});
- });
- describe('Props', () => {
- const mockGroupCiVariables = mockGroupVariables.data.group.ciVariables;
- const mockProjectCiVariables = mockProjectVariables.data.project.ciVariables;
+ describe('mutations', () => {
+ const groupProps = createGroupProps();
+ const instanceProps = createInstanceProps();
+ const projectProps = createProjectProps();
- describe('in a specific context as', () => {
- it.each`
- name | mockVariablesValue | mockEnvironmentsValue | withEnvironments | expectedEnvironments | propsFn | provideFn | mutation | maxVariableLimit
- ${'project'} | ${mockProjectVariables} | ${mockProjectEnvironments} | ${true} | ${['prod', 'dev']} | ${createProjectProps} | ${createProjectProvide} | ${null} | ${mockProjectCiVariables.limit}
- ${'group'} | ${mockGroupVariables} | ${[]} | ${false} | ${[]} | ${createGroupProps} | ${createGroupProvide} | ${getGroupVariables} | ${mockGroupCiVariables.limit}
- ${'instance'} | ${mockAdminVariables} | ${[]} | ${false} | ${[]} | ${createInstanceProps} | ${() => {}} | ${getAdminVariables} | ${0}
- `(
- 'passes down all the required props when its a $name component',
- async ({
- mutation,
- maxVariableLimit,
- mockVariablesValue,
- mockEnvironmentsValue,
- withEnvironments,
- expectedEnvironments,
- propsFn,
- provideFn,
- }) => {
- const props = propsFn();
- const provide = provideFn();
+ let mockMutationMap;
- mockVariables.mockResolvedValue(mockVariablesValue);
+ describe('error handling and feedback', () => {
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockGroupVariables);
+ mockMutation.mockResolvedValue({ ...mockGroupVariables.data, errors: [] });
+
+ await createComponentWithApollo({
+ customHandlers: [[getGroupVariables, mockVariables]],
+ customResolvers: {
+ Mutation: {
+ ...resolvers.Mutation,
+ addGroupVariable: mockMutation,
+ updateGroupVariable: mockMutation,
+ deleteGroupVariable: mockMutation,
+ },
+ },
+ props: groupProps,
+ provide: pagesFeatureFlagProvide,
+ });
+ });
- if (withEnvironments) {
- mockEnvironments.mockResolvedValue(mockEnvironmentsValue);
- }
+ it.each`
+ actionName | event
+ ${'add'} | ${'add-variable'}
+ ${'update'} | ${'update-variable'}
+ ${'delete'} | ${'delete-variable'}
+ `(
+ 'throws the specific graphql error if present when user performs $actionName variable',
+ async ({ event }) => {
+ const graphQLErrorMessage = 'There is a problem with this graphQL action';
+ mockMutation.mockResolvedValue({
+ ...mockGroupVariables.data,
+ errors: [graphQLErrorMessage],
+ });
- let customHandlers = null;
+ await findCiSettings().vm.$emit(event, newVariable);
+ await waitForPromises();
- if (mutation) {
- customHandlers = [[mutation, mockVariables]];
- }
+ expect(mockMutation).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalledWith({ message: graphQLErrorMessage });
+ },
+ );
- await createComponentWithApollo({ customHandlers, props, provide });
+ it.each`
+ actionName | event
+ ${'add'} | ${'add-variable'}
+ ${'update'} | ${'update-variable'}
+ ${'delete'} | ${'delete-variable'}
+ `(
+ 'throws generic error on failure with no graphql errors and user performs $actionName variable',
+ async ({ event }) => {
+ mockMutation.mockRejectedValue();
+
+ await findCiSettings().vm.$emit(event, newVariable);
+ await waitForPromises();
+
+ expect(mockMutation).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalledWith({ message: genericMutationErrorText });
+ },
+ );
- expect(findCiSettings().props()).toEqual({
- areScopedVariablesAvailable: wrapper.props().areScopedVariablesAvailable,
- hideEnvironmentScope: defaultProps.hideEnvironmentScope,
- isLoading: false,
- maxVariableLimit,
- variables: wrapper.props().queryData.ciVariables.lookup(mockVariablesValue.data)?.nodes,
- entity: props.entity,
- environments: expectedEnvironments,
+ it.each`
+ actionName | event
+ ${'add'} | ${'add-variable'}
+ ${'update'} | ${'update-variable'}
+ ${'delete'} | ${'delete-variable'}
+ `(
+ 'displays toast message after user performs $actionName variable',
+ async ({ actionName, event }) => {
+ await findCiSettings().vm.$emit(event, newVariable);
+ await waitForPromises();
+
+ expect(mockMutation).toHaveBeenCalled();
+ expect(mockToastShow).toHaveBeenCalledWith(
+ mapMutationActionToToast[actionName](newVariable.key),
+ );
+ },
+ );
+ });
+
+ const setupMockMutations = (mockResolvedMutation) => {
+ mockAddMutation.mockResolvedValue(mockResolvedMutation);
+ mockUpdateMutation.mockResolvedValue(mockResolvedMutation);
+ mockDeleteMutation.mockResolvedValue(mockResolvedMutation);
+
+ return {
+ add: mockAddMutation,
+ update: mockUpdateMutation,
+ delete: mockDeleteMutation,
+ };
+ };
+
+ describe.each`
+ scope | mockVariablesResolvedValue | getVariablesHandler | addMutationName | updateMutationName | deleteMutationName | props
+ ${'instance'} | ${mockVariables} | ${getAdminVariables} | ${'addAdminVariable'} | ${'updateAdminVariable'} | ${'deleteAdminVariable'} | ${instanceProps}
+ ${'group'} | ${mockGroupVariables} | ${getGroupVariables} | ${'addGroupVariable'} | ${'updateGroupVariable'} | ${'deleteGroupVariable'} | ${groupProps}
+ ${'project'} | ${mockProjectVariables} | ${getProjectVariables} | ${'addProjectVariable'} | ${'updateProjectVariable'} | ${'deleteProjectVariable'} | ${projectProps}
+ `(
+ '$scope variable mutations',
+ ({
+ addMutationName,
+ deleteMutationName,
+ getVariablesHandler,
+ mockVariablesResolvedValue,
+ updateMutationName,
+ props,
+ }) => {
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockVariablesResolvedValue);
+ mockMutationMap = setupMockMutations({ ...mockVariables.data, errors: [] });
+
+ await createComponentWithApollo({
+ customHandlers: [[getVariablesHandler, mockVariables]],
+ customResolvers: {
+ Mutation: {
+ ...resolvers.Mutation,
+ [addMutationName]: mockAddMutation,
+ [updateMutationName]: mockUpdateMutation,
+ [deleteMutationName]: mockDeleteMutation,
+ },
+ },
+ props,
+ provide: pagesFeatureFlagProvide,
+ });
});
+
+ it.each`
+ actionName | event
+ ${'add'} | ${'add-variable'}
+ ${'update'} | ${'update-variable'}
+ ${'delete'} | ${'delete-variable'}
+ `(
+ 'calls the right mutation when user performs $actionName variable',
+ async ({ event, actionName }) => {
+ await findCiSettings().vm.$emit(event, newVariable);
+ await waitForPromises();
+
+ expect(mockMutationMap[actionName]).toHaveBeenCalledWith(
+ expect.anything(),
+ {
+ endpoint: mockProvide.endpoint,
+ fullPath: props.fullPath,
+ id: props.id,
+ variable: newVariable,
+ },
+ expect.anything(),
+ expect.anything(),
+ );
+ },
+ );
},
);
- });
- describe('refetchAfterMutation', () => {
- it.each`
- bool | text
- ${true} | ${'refetches the variables'}
- ${false} | ${'does not refetch the variables'}
- `('when $bool it $text', async ({ bool }) => {
- await createComponentWithApollo({
- props: { ...createInstanceProps(), refetchAfterMutation: bool },
+ describe('without fullpath and ID props', () => {
+ beforeEach(async () => {
+ mockMutation.mockResolvedValue({ ...mockAdminVariables.data, errors: [] });
+ mockVariables.mockResolvedValue(mockAdminVariables);
+
+ await createComponentWithApollo({
+ customHandlers: [[getAdminVariables, mockVariables]],
+ customResolvers: {
+ Mutation: {
+ ...resolvers.Mutation,
+ addAdminVariable: mockMutation,
+ },
+ },
+ props: createInstanceProps(),
+ provide: pagesFeatureFlagProvide,
+ });
});
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ data: {} });
- jest.spyOn(wrapper.vm.$apollo.queries.ciVariables, 'refetch').mockImplementation(jest.fn());
+ it('does not pass fullPath and ID to the mutation', async () => {
+ await findCiSettings().vm.$emit('add-variable', newVariable);
+ await waitForPromises();
+
+ expect(mockMutation).toHaveBeenCalledWith(
+ expect.anything(),
+ {
+ endpoint: mockProvide.endpoint,
+ variable: newVariable,
+ },
+ expect.anything(),
+ expect.anything(),
+ );
+ });
+ });
+ });
- await findCiSettings().vm.$emit('add-variable', newVariable);
+ describe('Props', () => {
+ const mockGroupCiVariables = mockGroupVariables.data.group.ciVariables;
+ const mockProjectCiVariables = mockProjectVariables.data.project.ciVariables;
+
+ describe('in a specific context as', () => {
+ it.each`
+ name | mockVariablesValue | mockEnvironmentsValue | withEnvironments | expectedEnvironments | propsFn | provideFn | mutation | maxVariableLimit
+ ${'project'} | ${mockProjectVariables} | ${mockProjectEnvironments} | ${true} | ${['prod', 'dev']} | ${createProjectProps} | ${createProjectProvide} | ${null} | ${mockProjectCiVariables.limit}
+ ${'group'} | ${mockGroupVariables} | ${[]} | ${false} | ${[]} | ${createGroupProps} | ${createGroupProvide} | ${getGroupVariables} | ${mockGroupCiVariables.limit}
+ ${'instance'} | ${mockAdminVariables} | ${[]} | ${false} | ${[]} | ${createInstanceProps} | ${() => {}} | ${getAdminVariables} | ${0}
+ `(
+ 'passes down all the required props when its a $name component',
+ async ({
+ mutation,
+ maxVariableLimit,
+ mockVariablesValue,
+ mockEnvironmentsValue,
+ withEnvironments,
+ expectedEnvironments,
+ propsFn,
+ provideFn,
+ }) => {
+ const props = propsFn();
+ const provide = provideFn();
- await nextTick();
+ mockVariables.mockResolvedValue(mockVariablesValue);
- if (bool) {
- expect(wrapper.vm.$apollo.queries.ciVariables.refetch).toHaveBeenCalled();
- } else {
- expect(wrapper.vm.$apollo.queries.ciVariables.refetch).not.toHaveBeenCalled();
- }
- });
- });
+ if (withEnvironments) {
+ mockEnvironments.mockResolvedValue(mockEnvironmentsValue);
+ }
- describe('Validators', () => {
- describe('queryData', () => {
- let error;
+ let customHandlers = null;
- beforeEach(async () => {
- mockVariables.mockResolvedValue(mockGroupVariables);
- });
+ if (mutation) {
+ customHandlers = [[mutation, mockVariables]];
+ }
- it('will mount component with right data', async () => {
- try {
await createComponentWithApollo({
- customHandlers: [[getGroupVariables, mockVariables]],
- props: { ...createGroupProps() },
+ customHandlers,
+ props,
+ provide: { ...provide, ...pagesFeatureFlagProvide },
});
- } catch (e) {
- error = e;
- } finally {
- expect(wrapper.exists()).toBe(true);
- expect(error).toBeUndefined();
- }
- });
- it('will not mount component with wrong data', async () => {
- try {
- await createComponentWithApollo({
- customHandlers: [[getGroupVariables, mockVariables]],
- props: { ...createGroupProps(), queryData: { wrongKey: {} } },
+ expect(findCiSettings().props()).toEqual({
+ areEnvironmentsLoading: false,
+ areScopedVariablesAvailable: wrapper.props().areScopedVariablesAvailable,
+ hideEnvironmentScope: defaultProps.hideEnvironmentScope,
+ pageInfo: defaultProps.pageInfo,
+ isLoading: false,
+ maxVariableLimit,
+ variables: wrapper.props().queryData.ciVariables.lookup(mockVariablesValue.data)
+ ?.nodes,
+ entity: props.entity,
+ environments: expectedEnvironments,
});
- } catch (e) {
- error = e;
- } finally {
- expect(wrapper.exists()).toBe(false);
- expect(error.toString()).toContain('custom validator check failed for prop');
- }
- });
+ },
+ );
});
- describe('mutationData', () => {
- let error;
+ describe('refetchAfterMutation', () => {
+ it.each`
+ bool | text | timesQueryCalled
+ ${true} | ${'refetches the variables'} | ${2}
+ ${false} | ${'does not refetch the variables'} | ${1}
+ `('when $bool it $text', async ({ bool, timesQueryCalled }) => {
+ mockMutation.mockResolvedValue({ ...mockAdminVariables.data, errors: [] });
+ mockVariables.mockResolvedValue(mockAdminVariables);
+
+ await createComponentWithApollo({
+ customHandlers: [[getAdminVariables, mockVariables]],
+ customResolvers: {
+ Mutation: {
+ ...resolvers.Mutation,
+ addAdminVariable: mockMutation,
+ },
+ },
+ props: { ...createInstanceProps(), refetchAfterMutation: bool },
+ provide: pagesFeatureFlagProvide,
+ });
+
+ await findCiSettings().vm.$emit('add-variable', newVariable);
+ await waitForPromises();
- beforeEach(async () => {
- mockVariables.mockResolvedValue(mockGroupVariables);
+ expect(mockVariables).toHaveBeenCalledTimes(timesQueryCalled);
});
+ });
- it('will mount component with right data', async () => {
- try {
- await createComponentWithApollo({
- props: { ...createGroupProps() },
- });
- } catch (e) {
- error = e;
- } finally {
- expect(wrapper.exists()).toBe(true);
- expect(error).toBeUndefined();
- }
+ describe('Validators', () => {
+ describe('queryData', () => {
+ let error;
+
+ beforeEach(() => {
+ mockVariables.mockResolvedValue(mockGroupVariables);
+ });
+
+ it('will mount component with right data', async () => {
+ try {
+ await createComponentWithApollo({
+ customHandlers: [[getGroupVariables, mockVariables]],
+ props: { ...createGroupProps() },
+ provide: pagesFeatureFlagProvide,
+ });
+ } catch (e) {
+ error = e;
+ } finally {
+ expect(wrapper.exists()).toBe(true);
+ expect(error).toBeUndefined();
+ }
+ });
+
+ it('report custom validator error on wrong data', () => {
+ expect(() =>
+ assertProps(
+ ciVariableShared,
+ { ...defaultProps, ...createGroupProps(), queryData: { wrongKey: {} } },
+ { provide: mockProvide },
+ ),
+ ).toThrow('custom validator check failed for prop');
+ });
});
- it('will not mount component with wrong data', async () => {
- try {
- await createComponentWithApollo({
- props: { ...createGroupProps(), mutationData: { wrongKey: {} } },
- });
- } catch (e) {
- error = e;
- } finally {
- expect(wrapper.exists()).toBe(false);
- expect(error.toString()).toContain('custom validator check failed for prop');
- }
+ describe('mutationData', () => {
+ let error;
+
+ beforeEach(() => {
+ mockVariables.mockResolvedValue(mockGroupVariables);
+ });
+
+ it('will mount component with right data', async () => {
+ try {
+ await createComponentWithApollo({
+ props: { ...createGroupProps() },
+ provide: pagesFeatureFlagProvide,
+ });
+ } catch (e) {
+ error = e;
+ } finally {
+ expect(wrapper.exists()).toBe(true);
+ expect(error).toBeUndefined();
+ }
+ });
+
+ it('report custom validator error on wrong data', () => {
+ expect(() =>
+ assertProps(
+ ciVariableShared,
+ { ...defaultProps, ...createGroupProps(), mutationData: { wrongKey: {} } },
+ { provide: { ...mockProvide, ...pagesFeatureFlagProvide } },
+ ),
+ ).toThrow('custom validator check failed for prop');
+ });
});
});
});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js
index 9e2508c56ee..0b28cb06cec 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js
@@ -12,18 +12,25 @@ describe('Ci variable table', () => {
entity: 'project',
isLoading: false,
maxVariableLimit: mockVariables(projectString).length + 1,
+ pageInfo: {},
variables: mockVariables(projectString),
};
const mockMaxVariableLimit = defaultProps.variables.length;
- const createComponent = ({ props = {} } = {}) => {
+ const createComponent = ({ props = {}, provide = {} } = {}) => {
wrapper = mountExtended(CiVariableTable, {
attachTo: document.body,
propsData: {
...defaultProps,
...props,
},
+ provide: {
+ glFeatures: {
+ ciVariablesPages: false,
+ },
+ ...provide,
+ },
});
};
@@ -41,132 +48,136 @@ describe('Ci variable table', () => {
return sprintf(EXCEEDS_VARIABLE_LIMIT_TEXT, { entity, currentVariableCount, maxVariableLimit });
};
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('When table is empty', () => {
- beforeEach(() => {
- createComponent({ props: { variables: [] } });
- });
+ describe.each`
+ isVariablePagesEnabled | text
+ ${true} | ${'enabled'}
+ ${false} | ${'disabled'}
+ `('When Pages FF is $text', ({ isVariablePagesEnabled }) => {
+ const provide = isVariablePagesEnabled ? { glFeatures: { ciVariablesPages: true } } : {};
- it('displays empty message', () => {
- expect(findEmptyVariablesPlaceholder().exists()).toBe(true);
- });
-
- it('hides the reveal button', () => {
- expect(findRevealButton().exists()).toBe(false);
- });
- });
+ describe('When table is empty', () => {
+ beforeEach(() => {
+ createComponent({ props: { variables: [] }, provide });
+ });
- describe('When table has variables', () => {
- beforeEach(() => {
- createComponent();
- });
+ it('displays empty message', () => {
+ expect(findEmptyVariablesPlaceholder().exists()).toBe(true);
+ });
- it('does not display the empty message', () => {
- expect(findEmptyVariablesPlaceholder().exists()).toBe(false);
+ it('hides the reveal button', () => {
+ expect(findRevealButton().exists()).toBe(false);
+ });
});
- it('displays the reveal button', () => {
- expect(findRevealButton().exists()).toBe(true);
- });
+ describe('When table has variables', () => {
+ beforeEach(() => {
+ createComponent({ provide });
+ });
- it('displays the correct amount of variables', async () => {
- expect(wrapper.findAll('.js-ci-variable-row')).toHaveLength(defaultProps.variables.length);
- });
+ it('does not display the empty message', () => {
+ expect(findEmptyVariablesPlaceholder().exists()).toBe(false);
+ });
- it('displays the correct variable options', async () => {
- expect(findOptionsValues(0)).toBe('Protected, Expanded');
- expect(findOptionsValues(1)).toBe('Masked');
- });
+ it('displays the reveal button', () => {
+ expect(findRevealButton().exists()).toBe(true);
+ });
- it('enables the Add Variable button', () => {
- expect(findAddButton().props('disabled')).toBe(false);
- });
- });
+ it('displays the correct amount of variables', () => {
+ expect(wrapper.findAll('.js-ci-variable-row')).toHaveLength(defaultProps.variables.length);
+ });
- describe('When variables have exceeded the max limit', () => {
- beforeEach(() => {
- createComponent({ props: { maxVariableLimit: mockVariables(projectString).length } });
- });
+ it('displays the correct variable options', () => {
+ expect(findOptionsValues(0)).toBe('Protected, Expanded');
+ expect(findOptionsValues(1)).toBe('Masked');
+ });
- it('disables the Add Variable button', () => {
- expect(findAddButton().props('disabled')).toBe(true);
+ it('enables the Add Variable button', () => {
+ expect(findAddButton().props('disabled')).toBe(false);
+ });
});
- });
- describe('max limit reached alert', () => {
- describe('when there is no variable limit', () => {
+ describe('When variables have exceeded the max limit', () => {
beforeEach(() => {
createComponent({
- props: { maxVariableLimit: 0 },
+ props: { maxVariableLimit: mockVariables(projectString).length },
+ provide,
});
});
- it('hides alert', () => {
- expect(findLimitReachedAlerts().length).toBe(0);
+ it('disables the Add Variable button', () => {
+ expect(findAddButton().props('disabled')).toBe(true);
});
});
- describe('when variable limit exists', () => {
- it('hides alert when limit has not been reached', () => {
- createComponent();
+ describe('max limit reached alert', () => {
+ describe('when there is no variable limit', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { maxVariableLimit: 0 },
+ provide,
+ });
+ });
- expect(findLimitReachedAlerts().length).toBe(0);
+ it('hides alert', () => {
+ expect(findLimitReachedAlerts().length).toBe(0);
+ });
});
- it('shows alert when limit has been reached', () => {
- const exceedsVariableLimitText = generateExceedsVariableLimitText(
- defaultProps.entity,
- defaultProps.variables.length,
- mockMaxVariableLimit,
- );
+ describe('when variable limit exists', () => {
+ it('hides alert when limit has not been reached', () => {
+ createComponent({ provide });
- createComponent({
- props: { maxVariableLimit: mockMaxVariableLimit },
+ expect(findLimitReachedAlerts().length).toBe(0);
});
- expect(findLimitReachedAlerts().length).toBe(2);
+ it('shows alert when limit has been reached', () => {
+ const exceedsVariableLimitText = generateExceedsVariableLimitText(
+ defaultProps.entity,
+ defaultProps.variables.length,
+ mockMaxVariableLimit,
+ );
+
+ createComponent({
+ props: { maxVariableLimit: mockMaxVariableLimit },
+ });
- expect(findLimitReachedAlerts().at(0).props('dismissible')).toBe(false);
- expect(findLimitReachedAlerts().at(0).text()).toContain(exceedsVariableLimitText);
+ expect(findLimitReachedAlerts().length).toBe(2);
- expect(findLimitReachedAlerts().at(1).props('dismissible')).toBe(false);
- expect(findLimitReachedAlerts().at(1).text()).toContain(exceedsVariableLimitText);
+ expect(findLimitReachedAlerts().at(0).props('dismissible')).toBe(false);
+ expect(findLimitReachedAlerts().at(0).text()).toContain(exceedsVariableLimitText);
+
+ expect(findLimitReachedAlerts().at(1).props('dismissible')).toBe(false);
+ expect(findLimitReachedAlerts().at(1).text()).toContain(exceedsVariableLimitText);
+ });
});
});
- });
- describe('Table click actions', () => {
- beforeEach(() => {
- createComponent();
- });
+ describe('Table click actions', () => {
+ beforeEach(() => {
+ createComponent({ provide });
+ });
- it('reveals secret values when button is clicked', async () => {
- expect(findHiddenValues()).toHaveLength(defaultProps.variables.length);
- expect(findRevealedValues()).toHaveLength(0);
+ it('reveals secret values when button is clicked', async () => {
+ expect(findHiddenValues()).toHaveLength(defaultProps.variables.length);
+ expect(findRevealedValues()).toHaveLength(0);
- await findRevealButton().trigger('click');
+ await findRevealButton().trigger('click');
- expect(findHiddenValues()).toHaveLength(0);
- expect(findRevealedValues()).toHaveLength(defaultProps.variables.length);
- });
+ expect(findHiddenValues()).toHaveLength(0);
+ expect(findRevealedValues()).toHaveLength(defaultProps.variables.length);
+ });
- it('dispatches `setSelectedVariable` with correct variable to edit', async () => {
- await findEditButton().trigger('click');
+ it('dispatches `setSelectedVariable` with correct variable to edit', async () => {
+ await findEditButton().trigger('click');
- expect(wrapper.emitted('set-selected-variable')).toEqual([[defaultProps.variables[0]]]);
- });
+ expect(wrapper.emitted('set-selected-variable')).toEqual([[defaultProps.variables[0]]]);
+ });
- it('dispatches `setSelectedVariable` with no variable when adding a new one', async () => {
- await findAddButton().trigger('click');
+ it('dispatches `setSelectedVariable` with no variable when adding a new one', async () => {
+ await findAddButton().trigger('click');
- expect(wrapper.emitted('set-selected-variable')).toEqual([[null]]);
+ expect(wrapper.emitted('set-selected-variable')).toEqual([[null]]);
+ });
});
});
});
diff --git a/spec/frontend/ci/ci_variable_list/mocks.js b/spec/frontend/ci/ci_variable_list/mocks.js
index 4da4f53f69f..f9450803308 100644
--- a/spec/frontend/ci/ci_variable_list/mocks.js
+++ b/spec/frontend/ci/ci_variable_list/mocks.js
@@ -56,6 +56,11 @@ export const mockVariablesWithScopes = (kind) =>
return { ...variable, environmentScope: '*' };
});
+export const mockVariablesWithUniqueScopes = (kind) =>
+ mockVariables(kind).map((variable) => {
+ return { ...variable, environmentScope: variable.value };
+ });
+
const createDefaultVars = ({ withScope = true, kind } = {}) => {
let base = mockVariables(kind);
diff --git a/spec/frontend/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js b/spec/frontend/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js
index b00e1adab63..48a85eba433 100644
--- a/spec/frontend/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js
@@ -41,10 +41,6 @@ describe('EE - CodeSnippetAlert', () => {
createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it("provides a link to the feature's documentation", () => {
const docsLink = findDocsLink();
diff --git a/spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js b/spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js
index 8e1d8081dd8..4b0ddacef93 100644
--- a/spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js
@@ -33,12 +33,8 @@ describe('Pipeline Editor | Commit Form', () => {
const findSubmitBtn = () => wrapper.find('[type="submit"]');
const findCancelBtn = () => wrapper.find('[type="reset"]');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when the form is displayed', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent();
});
@@ -61,7 +57,7 @@ describe('Pipeline Editor | Commit Form', () => {
});
describe('when buttons are clicked', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent({}, mount);
});
@@ -97,7 +93,7 @@ describe('Pipeline Editor | Commit Form', () => {
createComponent({ props: { hasUnsavedChanges, isNewCiConfigFile } });
if (isDisabled) {
- expect(findSubmitBtn().attributes('disabled')).toBe('true');
+ expect(findSubmitBtn().attributes('disabled')).toBeDefined();
} else {
expect(findSubmitBtn().attributes('disabled')).toBeUndefined();
}
@@ -136,7 +132,7 @@ describe('Pipeline Editor | Commit Form', () => {
it('when the commit message is empty, submit button is disabled', async () => {
await findCommitTextarea().setValue('');
- expect(findSubmitBtn().attributes('disabled')).toBe('disabled');
+ expect(findSubmitBtn().attributes('disabled')).toBeDefined();
});
});
diff --git a/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js b/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js
index f6e93c55bbb..8834231aaef 100644
--- a/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js
@@ -4,6 +4,7 @@ import { mount } from '@vue/test-utils';
import Vue from 'vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { mockTracking } from 'helpers/tracking_helper';
import CommitForm from '~/ci/pipeline_editor/components/commit/commit_form.vue';
import CommitSection from '~/ci/pipeline_editor/components/commit/commit_section.vue';
import {
@@ -11,12 +12,12 @@ import {
COMMIT_ACTION_UPDATE,
COMMIT_SUCCESS,
COMMIT_SUCCESS_WITH_REDIRECT,
+ pipelineEditorTrackingOptions,
} from '~/ci/pipeline_editor/constants';
import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
import commitCreate from '~/ci/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql';
import getCurrentBranch from '~/ci/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
import updatePipelineEtag from '~/ci/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql';
-
import {
mockCiConfigPath,
mockCiYml,
@@ -113,10 +114,6 @@ describe('Pipeline Editor | Commit section', () => {
await waitForPromises();
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when the user commits a new file', () => {
beforeEach(async () => {
mockMutateCommitData.mockResolvedValue(mockCommitCreateResponse);
@@ -284,4 +281,43 @@ describe('Pipeline Editor | Commit section', () => {
createComponent({ props: { 'scroll-to-commit-form': true } });
expect(findCommitForm().props('scrollToCommitForm')).toBe(true);
});
+
+ describe('tracking', () => {
+ let trackingSpy;
+ const { actions, label } = pipelineEditorTrackingOptions;
+
+ beforeEach(() => {
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ });
+
+ describe('when user commit a new file', () => {
+ beforeEach(async () => {
+ mockMutateCommitData.mockResolvedValue(mockCommitCreateResponse);
+ createComponentWithApollo({ props: { isNewCiConfigFile: true } });
+ await submitCommit();
+ });
+
+ it('calls tracking event with the CREATE property', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, actions.commitCiConfig, {
+ label,
+ property: COMMIT_ACTION_CREATE,
+ });
+ });
+ });
+
+ describe('when user commit an update to the CI file', () => {
+ beforeEach(async () => {
+ mockMutateCommitData.mockResolvedValue(mockCommitCreateResponse);
+ createComponentWithApollo({ props: { isNewCiConfigFile: false } });
+ await submitCommit();
+ });
+
+ it('calls the tracking event with the UPDATE property', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, actions.commitCiConfig, {
+ label,
+ property: COMMIT_ACTION_UPDATE,
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/ci/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
index 137137ec657..0ecb77674d5 100644
--- a/spec/frontend/ci/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
@@ -21,10 +21,6 @@ describe('First pipeline card', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the title', () => {
expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title);
});
diff --git a/spec/frontend/ci/pipeline_editor/components/drawer/cards/getting_started_card_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/cards/getting_started_card_spec.js
index cdce757ce7c..417597eaf1f 100644
--- a/spec/frontend/ci/pipeline_editor/components/drawer/cards/getting_started_card_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/cards/getting_started_card_spec.js
@@ -12,10 +12,6 @@ describe('Getting started card', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the title', () => {
expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title);
});
diff --git a/spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js
index 6909916c3e6..0296ab5a65c 100644
--- a/spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js
@@ -33,10 +33,6 @@ describe('Pipeline config reference card', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the title', () => {
expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title);
});
@@ -72,7 +68,7 @@ describe('Pipeline config reference card', () => {
});
};
- it('tracks help page links', async () => {
+ it('tracks help page links', () => {
const {
CI_EXAMPLES_LINK,
CI_HELP_LINK,
diff --git a/spec/frontend/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js
index 0c6879020de..547ba3cbd8b 100644
--- a/spec/frontend/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js
@@ -12,10 +12,6 @@ describe('Visual and Lint card', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the title', () => {
expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title);
});
diff --git a/spec/frontend/ci/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
index 42e372cc1db..b07d63dd5d9 100644
--- a/spec/frontend/ci/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
@@ -11,10 +11,6 @@ describe('Pipeline editor drawer', () => {
wrapper = shallowMount(PipelineEditorDrawer);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('emits close event when closing the drawer', () => {
createComponent();
diff --git a/spec/frontend/ci/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js
index f510c61ee74..b0c889cfc9f 100644
--- a/spec/frontend/ci/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js
@@ -17,10 +17,6 @@ describe('Demo job pill', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the jobName', () => {
expect(wrapper.text()).toContain(jobName);
});
diff --git a/spec/frontend/ci/pipeline_editor/components/editor/ci_config_merged_preview_spec.js b/spec/frontend/ci/pipeline_editor/components/editor/ci_config_merged_preview_spec.js
index 2a2bc2547cc..2182b6e9cc6 100644
--- a/spec/frontend/ci/pipeline_editor/components/editor/ci_config_merged_preview_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/editor/ci_config_merged_preview_spec.js
@@ -34,10 +34,6 @@ describe('Text editor component', () => {
const findIcon = () => wrapper.findComponent(GlIcon);
const findEditor = () => wrapper.findComponent(MockSourceEditor);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when status is valid', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js b/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js
index dc72694d26f..f1a5c4169fb 100644
--- a/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js
@@ -11,12 +11,25 @@ describe('CI Editor Header', () => {
let wrapper;
let trackingSpy = null;
- const createComponent = ({ showDrawer = false, showJobAssistantDrawer = false } = {}) => {
+ const createComponent = ({
+ showDrawer = false,
+ showJobAssistantDrawer = false,
+ showAiAssistantDrawer = false,
+ aiChatAvailable = false,
+ aiCiConfigGenerator = false,
+ } = {}) => {
wrapper = extendedWrapper(
shallowMount(CiEditorHeader, {
+ provide: {
+ aiChatAvailable,
+ glFeatures: {
+ aiCiConfigGenerator,
+ },
+ },
propsData: {
showDrawer,
showJobAssistantDrawer,
+ showAiAssistantDrawer,
},
}),
);
@@ -24,9 +37,9 @@ describe('CI Editor Header', () => {
const findLinkBtn = () => wrapper.findByTestId('template-repo-link');
const findHelpBtn = () => wrapper.findByTestId('drawer-toggle');
+ const findAiAssistnantBtn = () => wrapper.findByTestId('ai-assistant-drawer-toggle');
afterEach(() => {
- wrapper.destroy();
unmockTracking();
});
@@ -40,7 +53,29 @@ describe('CI Editor Header', () => {
label,
});
};
+ describe('Ai Assistant toggle button', () => {
+ describe('when feature is unavailable', () => {
+ it('should not show ai button when feature toggle is off', () => {
+ createComponent({ aiChatAvailable: true });
+ mockTracking(undefined, wrapper.element, jest.spyOn);
+ expect(findAiAssistnantBtn().exists()).toBe(false);
+ });
+
+ it('should not show ai button when feature is unavailable', () => {
+ createComponent({ aiCiConfigGenerator: true });
+ mockTracking(undefined, wrapper.element, jest.spyOn);
+ expect(findAiAssistnantBtn().exists()).toBe(false);
+ });
+ });
+ describe('when feature is available', () => {
+ it('should show ai button', () => {
+ createComponent({ aiCiConfigGenerator: true, aiChatAvailable: true });
+ mockTracking(undefined, wrapper.element, jest.spyOn);
+ expect(findAiAssistnantBtn().exists()).toBe(true);
+ });
+ });
+ });
describe('link button', () => {
beforeEach(() => {
createComponent();
@@ -59,7 +94,7 @@ describe('CI Editor Header', () => {
expect(findLinkBtn().props('icon')).toBe('external-link');
});
- it('tracks the click on the browse button', async () => {
+ it('tracks the click on the browse button', () => {
const { browseTemplates } = pipelineEditorTrackingOptions.actions;
testTracker(findLinkBtn(), browseTemplates);
@@ -92,7 +127,7 @@ describe('CI Editor Header', () => {
expect(wrapper.emitted('open-drawer')).toHaveLength(1);
});
- it('tracks open help drawer action', async () => {
+ it('tracks open help drawer action', () => {
const { actions } = pipelineEditorTrackingOptions;
testTracker(findHelpBtn(), actions.openHelpDrawer);
diff --git a/spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js b/spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js
index ec987be8cb8..0be26570fbf 100644
--- a/spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js
@@ -1,7 +1,11 @@
import { shallowMount } from '@vue/test-utils';
+import { editor as monacoEditor } from 'monaco-editor';
+import SourceEditor from '~/vue_shared/components/source_editor.vue';
import { EDITOR_READY_EVENT } from '~/editor/constants';
+import { CiSchemaExtension as MockedCiSchemaExtension } from '~/editor/extensions/source_editor_ci_schema_ext';
import { SOURCE_EDITOR_DEBOUNCE } from '~/ci/pipeline_editor/constants';
+import eventHub, { SCROLL_EDITOR_TO_BOTTOM } from '~/ci/pipeline_editor/event_hub';
import TextEditor from '~/ci/pipeline_editor/components/editor/text_editor.vue';
import {
mockCiConfigPath,
@@ -12,19 +16,26 @@ import {
mockDefaultBranch,
} from '../../mock_data';
+jest.mock('monaco-editor');
+jest.mock('~/editor/extensions/source_editor_ci_schema_ext', () => {
+ const { createMockSourceEditorExtension } = jest.requireActual(
+ 'helpers/create_mock_source_editor_extension',
+ );
+ const { CiSchemaExtension } = jest.requireActual(
+ '~/editor/extensions/source_editor_ci_schema_ext',
+ );
+
+ return {
+ CiSchemaExtension: createMockSourceEditorExtension(CiSchemaExtension),
+ };
+});
+
describe('Pipeline Editor | Text editor component', () => {
let wrapper;
let editorReadyListener;
- let mockUse;
- let mockRegisterCiSchema;
- let mockEditorInstance;
- let editorInstanceDetail;
-
- const MockSourceEditor = {
- template: '<div/>',
- props: ['value', 'fileName', 'editorOptions', 'debounceValue'],
- };
+
+ const getMonacoEditor = () => monacoEditor.create.mock.results[0].value;
const createComponent = (mountFn = shallowMount) => {
wrapper = mountFn(TextEditor, {
@@ -44,33 +55,17 @@ describe('Pipeline Editor | Text editor component', () => {
[EDITOR_READY_EVENT]: editorReadyListener,
},
stubs: {
- SourceEditor: MockSourceEditor,
+ SourceEditor,
},
});
};
- const findEditor = () => wrapper.findComponent(MockSourceEditor);
+ const findEditor = () => wrapper.findComponent(SourceEditor);
beforeEach(() => {
- editorReadyListener = jest.fn();
- mockUse = jest.fn();
- mockRegisterCiSchema = jest.fn();
- mockEditorInstance = {
- use: mockUse,
- registerCiSchema: mockRegisterCiSchema,
- };
- editorInstanceDetail = {
- detail: {
- instance: mockEditorInstance,
- },
- };
- });
+ jest.spyOn(monacoEditor, 'create');
- afterEach(() => {
- wrapper.destroy();
-
- mockUse.mockClear();
- mockRegisterCiSchema.mockClear();
+ editorReadyListener = jest.fn();
});
describe('template', () => {
@@ -99,21 +94,34 @@ describe('Pipeline Editor | Text editor component', () => {
});
it('bubbles up events', () => {
- findEditor().vm.$emit(EDITOR_READY_EVENT, editorInstanceDetail);
-
expect(editorReadyListener).toHaveBeenCalled();
});
+
+ it('scrolls editor to bottom on scroll editor to bottom event', () => {
+ const setScrollTop = jest.spyOn(getMonacoEditor(), 'setScrollTop');
+
+ eventHub.$emit(SCROLL_EDITOR_TO_BOTTOM);
+
+ expect(setScrollTop).toHaveBeenCalledWith(getMonacoEditor().getScrollHeight());
+ });
+
+ it('when destroyed, destroys scroll listener', () => {
+ const setScrollTop = jest.spyOn(getMonacoEditor(), 'setScrollTop');
+
+ wrapper.destroy();
+ eventHub.$emit(SCROLL_EDITOR_TO_BOTTOM);
+
+ expect(setScrollTop).not.toHaveBeenCalled();
+ });
});
describe('CI schema', () => {
beforeEach(() => {
createComponent();
- findEditor().vm.$emit(EDITOR_READY_EVENT, editorInstanceDetail);
});
it('configures editor with syntax highlight', () => {
- expect(mockUse).toHaveBeenCalledTimes(1);
- expect(mockRegisterCiSchema).toHaveBeenCalledTimes(1);
+ expect(MockedCiSchemaExtension.mockedMethods.registerCiSchema).toHaveBeenCalledTimes(1);
});
});
});
diff --git a/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js b/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js
index a26232df58f..3a99949413b 100644
--- a/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js
@@ -133,10 +133,6 @@ describe('Pipeline editor branch switcher', () => {
mockAvailableBranchQuery = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const testErrorHandling = () => {
expect(wrapper.emitted('showError')).toBeDefined();
expect(wrapper.emitted('showError')[0]).toEqual([
@@ -292,7 +288,7 @@ describe('Pipeline editor branch switcher', () => {
});
describe('with a search term', () => {
- beforeEach(async () => {
+ beforeEach(() => {
mockAvailableBranchQuery.mockResolvedValue(mockSearchBranches);
});
diff --git a/spec/frontend/ci/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js b/spec/frontend/ci/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js
index 907db16913c..19c113689c2 100644
--- a/spec/frontend/ci/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js
@@ -48,10 +48,6 @@ describe('Pipeline editor file nav', () => {
const findFileTreeBtn = () => wrapper.findByTestId('file-tree-toggle');
const findPopoverContainer = () => wrapper.findComponent(FileTreePopover);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js b/spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js
index 11ba517e0eb..f2effcb2966 100644
--- a/spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js
@@ -22,7 +22,7 @@ describe('Pipeline editor file nav', () => {
includes,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
stubs,
}),
@@ -35,7 +35,6 @@ describe('Pipeline editor file nav', () => {
afterEach(() => {
localStorage.clear();
- wrapper.destroy();
});
describe('template', () => {
@@ -61,11 +60,11 @@ describe('Pipeline editor file nav', () => {
expect(fileTreeItems().exists()).toBe(false);
});
- it('renders alert tip', async () => {
+ it('renders alert tip', () => {
expect(findTip().exists()).toBe(true);
});
- it('renders learn more link', async () => {
+ it('renders learn more link', () => {
expect(findTip().props('secondaryButtonLink')).toBe(mockIncludesHelpPagePath);
});
@@ -88,7 +87,7 @@ describe('Pipeline editor file nav', () => {
});
});
- it('does not render alert tip', async () => {
+ it('does not render alert tip', () => {
expect(findTip().exists()).toBe(false);
});
});
diff --git a/spec/frontend/ci/pipeline_editor/components/file-tree/file_item_spec.js b/spec/frontend/ci/pipeline_editor/components/file-tree/file_item_spec.js
index bceb741f91c..80737e9a8ab 100644
--- a/spec/frontend/ci/pipeline_editor/components/file-tree/file_item_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/file-tree/file_item_spec.js
@@ -18,10 +18,6 @@ describe('Pipeline editor file nav', () => {
const fileIcon = () => wrapper.findComponent(FileIcon);
const link = () => wrapper.findComponent(GlLink);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js
index 555b9f29fbf..a651664851e 100644
--- a/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js
@@ -26,11 +26,6 @@ describe('Pipeline editor header', () => {
const findPipelineStatus = () => wrapper.findComponent(PipelineStatus);
const findValidationSegment = () => wrapper.findComponent(ValidationSegment);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('template', () => {
it('hides the pipeline status for new projects without a CI file', () => {
createComponent({ props: { isNewCiConfigFile: true } });
diff --git a/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
index 7bf955012c7..b8526e569ec 100644
--- a/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
@@ -96,7 +96,7 @@ describe('Pipeline Status', () => {
await waitForPromises();
});
- it('should emit an error event when query fails', async () => {
+ it('should emit an error event when query fails', () => {
expect(wrapper.emitted('showError')).toHaveLength(1);
expect(wrapper.emitted('showError')[0]).toEqual([
{
diff --git a/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
index a62c51ffb59..8ca88472bf1 100644
--- a/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
@@ -48,7 +48,6 @@ describe('Pipeline Status', () => {
afterEach(() => {
mockPipelineQuery.mockReset();
- wrapper.destroy();
});
describe('loading icon', () => {
@@ -78,7 +77,7 @@ describe('Pipeline Status', () => {
await waitForPromises();
});
- it('query is called with correct variables', async () => {
+ it('query is called with correct variables', () => {
expect(mockPipelineQuery).toHaveBeenCalledTimes(1);
expect(mockPipelineQuery).toHaveBeenCalledWith({
fullPath: mockProjectFullPath,
diff --git a/spec/frontend/ci/pipeline_editor/components/header/validation_segment_spec.js b/spec/frontend/ci/pipeline_editor/components/header/validation_segment_spec.js
index 0853a6f4ca4..a107a626c6d 100644
--- a/spec/frontend/ci/pipeline_editor/components/header/validation_segment_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/validation_segment_spec.js
@@ -1,11 +1,10 @@
import VueApollo from 'vue-apollo';
-import { GlIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import Vue from 'vue';
import { escape } from 'lodash';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import createMockApollo from 'helpers/mock_apollo_helper';
import { sprintf } from '~/locale';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
import ValidationSegment, {
i18n,
} from '~/ci/pipeline_editor/components/header/validation_segment.vue';
@@ -20,8 +19,8 @@ import {
} from '~/ci/pipeline_editor/constants';
import {
mergeUnwrappedCiConfig,
+ mockCiTroubleshootingPath,
mockCiYml,
- mockLintUnavailableHelpPagePath,
mockYmlHelpPagePath,
} from '../../mock_data';
@@ -43,29 +42,27 @@ describe('Validation segment component', () => {
},
});
- wrapper = extendedWrapper(
- shallowMount(ValidationSegment, {
- apolloProvider: mockApollo,
- provide: {
- ymlHelpPagePath: mockYmlHelpPagePath,
- lintUnavailableHelpPagePath: mockLintUnavailableHelpPagePath,
- },
- propsData: {
- ciConfig: mergeUnwrappedCiConfig(),
- ciFileContent: mockCiYml,
- ...props,
- },
- }),
- );
+ wrapper = shallowMountExtended(ValidationSegment, {
+ apolloProvider: mockApollo,
+ provide: {
+ ymlHelpPagePath: mockYmlHelpPagePath,
+ ciTroubleshootingPath: mockCiTroubleshootingPath,
+ },
+ propsData: {
+ ciConfig: mergeUnwrappedCiConfig(),
+ ciFileContent: mockCiYml,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
};
const findIcon = () => wrapper.findComponent(GlIcon);
- const findLearnMoreLink = () => wrapper.findByTestId('learnMoreLink');
- const findValidationMsg = () => wrapper.findByTestId('validationMsg');
-
- afterEach(() => {
- wrapper.destroy();
- });
+ const findHelpLink = () => wrapper.findComponent(GlLink);
+ const findValidationMsg = () => wrapper.findComponent(GlSprintf);
+ const findValidationSegment = () => wrapper.findByTestId('validation-segment');
it('shows the loading state', () => {
createComponent({ appStatus: EDITOR_APP_STATUS_LOADING });
@@ -82,8 +79,12 @@ describe('Validation segment component', () => {
expect(findIcon().props('name')).toBe('check');
});
+ it('does not render a link', () => {
+ expect(findHelpLink().exists()).toBe(false);
+ });
+
it('shows a message for empty state', () => {
- expect(findValidationMsg().text()).toBe(i18n.empty);
+ expect(findValidationSegment().text()).toBe(i18n.empty);
});
});
@@ -97,12 +98,15 @@ describe('Validation segment component', () => {
});
it('shows a message for valid state', () => {
- expect(findValidationMsg().text()).toContain(i18n.valid);
+ expect(findValidationSegment().text()).toBe(
+ sprintf(i18n.valid, { linkStart: '', linkEnd: '' }),
+ );
});
it('shows the learn more link', () => {
- expect(findLearnMoreLink().attributes('href')).toBe(mockYmlHelpPagePath);
- expect(findLearnMoreLink().text()).toBe(i18n.learnMore);
+ expect(findValidationMsg().exists()).toBe(true);
+ expect(findValidationMsg().text()).toBe('Learn more');
+ expect(findHelpLink().attributes('href')).toBe(mockYmlHelpPagePath);
});
});
@@ -117,13 +121,16 @@ describe('Validation segment component', () => {
expect(findIcon().props('name')).toBe('warning-solid');
});
- it('has message for invalid state', () => {
- expect(findValidationMsg().text()).toBe(i18n.invalid);
+ it('shows a message for invalid state', () => {
+ expect(findValidationSegment().text()).toBe(
+ sprintf(i18n.invalid, { linkStart: '', linkEnd: '' }),
+ );
});
it('shows the learn more link', () => {
- expect(findLearnMoreLink().attributes('href')).toBe(mockYmlHelpPagePath);
- expect(findLearnMoreLink().text()).toBe('Learn more');
+ expect(findValidationMsg().exists()).toBe(true);
+ expect(findValidationMsg().text()).toBe('Learn more');
+ expect(findHelpLink().attributes('href')).toBe(mockYmlHelpPagePath);
});
describe('with multiple errors', () => {
@@ -140,11 +147,16 @@ describe('Validation segment component', () => {
},
});
});
+
+ it('shows the learn more link', () => {
+ expect(findValidationMsg().exists()).toBe(true);
+ expect(findValidationMsg().text()).toBe('Learn more');
+ expect(findHelpLink().attributes('href')).toBe(mockYmlHelpPagePath);
+ });
+
it('shows an invalid state with an error', () => {
- // Test the error is shown _and_ the string matches
- expect(findValidationMsg().text()).toContain(firstError);
- expect(findValidationMsg().text()).toBe(
- sprintf(i18n.invalidWithReason, { reason: firstError }),
+ expect(findValidationSegment().text()).toBe(
+ sprintf(i18n.invalidWithReason, { reason: firstError, linkStart: '', linkEnd: '' }),
);
});
});
@@ -163,10 +175,8 @@ describe('Validation segment component', () => {
});
});
it('shows an invalid state with an error while preventing XSS', () => {
- const { innerHTML } = findValidationMsg().element;
-
- expect(innerHTML).not.toContain(evilError);
- expect(innerHTML).toContain(escape(evilError));
+ expect(findValidationSegment().html()).not.toContain(evilError);
+ expect(findValidationSegment().html()).toContain(escape(evilError));
});
});
});
@@ -182,16 +192,18 @@ describe('Validation segment component', () => {
});
it('show a message that the service is unavailable', () => {
- expect(findValidationMsg().text()).toBe(i18n.unavailableValidation);
+ expect(findValidationSegment().text()).toBe(
+ sprintf(i18n.unavailableValidation, { linkStart: '', linkEnd: '' }),
+ );
});
it('shows the time-out icon', () => {
expect(findIcon().props('name')).toBe('time-out');
});
- it('shows the learn more link', () => {
- expect(findLearnMoreLink().attributes('href')).toBe(mockLintUnavailableHelpPagePath);
- expect(findLearnMoreLink().text()).toBe(i18n.learnMore);
+ it('shows the link to ci troubleshooting', () => {
+ expect(findValidationMsg().exists()).toBe(true);
+ expect(findHelpLink().attributes('href')).toBe(mockCiTroubleshootingPath);
});
});
});
diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item_spec.js
new file mode 100644
index 00000000000..9046be4a45e
--- /dev/null
+++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item_spec.js
@@ -0,0 +1,127 @@
+import ArtifactsAndCacheItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { JOB_TEMPLATE } from '~/ci/pipeline_editor/components/job_assistant_drawer/constants';
+
+describe('Artifacts and cache item', () => {
+ let wrapper;
+
+ const findArtifactsPathsInputByIndex = (index) =>
+ wrapper.findByTestId(`artifacts-paths-input-${index}`);
+ const findArtifactsExcludeInputByIndex = (index) =>
+ wrapper.findByTestId(`artifacts-exclude-input-${index}`);
+ const findCachePathsInputByIndex = (index) => wrapper.findByTestId(`cache-paths-input-${index}`);
+ const findCacheKeyInput = () => wrapper.findByTestId('cache-key-input');
+ const findDeleteArtifactsPathsButtonByIndex = (index) =>
+ wrapper.findByTestId(`delete-artifacts-paths-button-${index}`);
+ const findDeleteArtifactsExcludeButtonByIndex = (index) =>
+ wrapper.findByTestId(`delete-artifacts-exclude-button-${index}`);
+ const findDeleteCachePathsButtonByIndex = (index) =>
+ wrapper.findByTestId(`delete-cache-paths-button-${index}`);
+ const findAddArtifactsPathsButton = () => wrapper.findByTestId('add-artifacts-paths-button');
+ const findAddArtifactsExcludeButton = () => wrapper.findByTestId('add-artifacts-exclude-button');
+ const findAddCachePathsButton = () => wrapper.findByTestId('add-cache-paths-button');
+
+ const dummyArtifactsPath = 'dummyArtifactsPath';
+ const dummyArtifactsExclude = 'dummyArtifactsExclude';
+ const dummyCachePath = 'dummyCachePath';
+ const dummyCacheKey = 'dummyCacheKey';
+
+ const createComponent = ({ job = JSON.parse(JSON.stringify(JOB_TEMPLATE)) } = {}) => {
+ wrapper = shallowMountExtended(ArtifactsAndCacheItem, {
+ propsData: {
+ job,
+ },
+ });
+ };
+
+ it('should emit update job event when filling inputs', () => {
+ createComponent();
+
+ expect(wrapper.emitted('update-job')).toBeUndefined();
+
+ findArtifactsPathsInputByIndex(0).vm.$emit('input', dummyArtifactsPath);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')[0]).toStrictEqual([
+ 'artifacts.paths[0]',
+ dummyArtifactsPath,
+ ]);
+
+ findArtifactsExcludeInputByIndex(0).vm.$emit('input', dummyArtifactsExclude);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(2);
+ expect(wrapper.emitted('update-job')[1]).toStrictEqual([
+ 'artifacts.exclude[0]',
+ dummyArtifactsExclude,
+ ]);
+
+ findCachePathsInputByIndex(0).vm.$emit('input', dummyCachePath);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(3);
+ expect(wrapper.emitted('update-job')[2]).toStrictEqual(['cache.paths[0]', dummyCachePath]);
+
+ findCacheKeyInput().vm.$emit('input', dummyCacheKey);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(4);
+ expect(wrapper.emitted('update-job')[3]).toStrictEqual(['cache.key', dummyCacheKey]);
+ });
+
+ it('should emit update job event when click add item button', () => {
+ createComponent();
+
+ findAddArtifactsPathsButton().vm.$emit('click');
+
+ expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')[0]).toStrictEqual(['artifacts.paths[1]', '']);
+
+ findAddArtifactsExcludeButton().vm.$emit('click');
+
+ expect(wrapper.emitted('update-job')).toHaveLength(2);
+ expect(wrapper.emitted('update-job')[1]).toStrictEqual(['artifacts.exclude[1]', '']);
+
+ findAddCachePathsButton().vm.$emit('click');
+
+ expect(wrapper.emitted('update-job')).toHaveLength(3);
+ expect(wrapper.emitted('update-job')[2]).toStrictEqual(['cache.paths[1]', '']);
+ });
+
+ it('should emit update job event when click delete item button', () => {
+ createComponent({
+ job: {
+ artifacts: {
+ paths: ['0', '1'],
+ exclude: ['0', '1'],
+ },
+ cache: {
+ paths: ['0', '1'],
+ key: '',
+ },
+ },
+ });
+
+ findDeleteArtifactsPathsButtonByIndex(0).vm.$emit('click');
+
+ expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')[0]).toStrictEqual(['artifacts.paths[0]']);
+
+ findDeleteArtifactsExcludeButtonByIndex(0).vm.$emit('click');
+
+ expect(wrapper.emitted('update-job')).toHaveLength(2);
+ expect(wrapper.emitted('update-job')[1]).toStrictEqual(['artifacts.exclude[0]']);
+
+ findDeleteCachePathsButtonByIndex(0).vm.$emit('click');
+
+ expect(wrapper.emitted('update-job')).toHaveLength(3);
+ expect(wrapper.emitted('update-job')[2]).toStrictEqual(['cache.paths[0]']);
+ });
+
+ it('should not emit update job event when click the only one delete item button', () => {
+ createComponent();
+
+ findDeleteArtifactsPathsButtonByIndex(0).vm.$emit('click');
+ findDeleteArtifactsExcludeButtonByIndex(0).vm.$emit('click');
+ findDeleteCachePathsButtonByIndex(0).vm.$emit('click');
+
+ expect(wrapper.emitted('update-job')).toBeUndefined();
+ });
+});
diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item_spec.js
new file mode 100644
index 00000000000..f99d7277612
--- /dev/null
+++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item_spec.js
@@ -0,0 +1,39 @@
+import ImageItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { JOB_TEMPLATE } from '~/ci/pipeline_editor/components/job_assistant_drawer/constants';
+
+describe('Image item', () => {
+ let wrapper;
+
+ const findImageNameInput = () => wrapper.findByTestId('image-name-input');
+ const findImageEntrypointInput = () => wrapper.findByTestId('image-entrypoint-input');
+
+ const dummyImageName = 'a';
+ const dummyImageEntrypoint = ['b', 'c'];
+
+ const createComponent = ({ job = JSON.parse(JSON.stringify(JOB_TEMPLATE)) } = {}) => {
+ wrapper = shallowMountExtended(ImageItem, {
+ propsData: {
+ job,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should emit update job event when filling inputs', () => {
+ expect(wrapper.emitted('update-job')).toBeUndefined();
+
+ findImageNameInput().vm.$emit('input', dummyImageName);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')[0]).toEqual(['image.name', dummyImageName]);
+
+ findImageEntrypointInput().vm.$emit('input', dummyImageEntrypoint.join('\n'));
+
+ expect(wrapper.emitted('update-job')).toHaveLength(2);
+ expect(wrapper.emitted('update-job')[1]).toEqual(['image.entrypoint', dummyImageEntrypoint]);
+ });
+});
diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item_spec.js
new file mode 100644
index 00000000000..373fb1b70c7
--- /dev/null
+++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item_spec.js
@@ -0,0 +1,60 @@
+import JobSetupItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { JOB_TEMPLATE } from '~/ci/pipeline_editor/components/job_assistant_drawer/constants';
+
+describe('Job setup item', () => {
+ let wrapper;
+
+ const findJobNameInput = () => wrapper.findByTestId('job-name-input');
+ const findJobScriptInput = () => wrapper.findByTestId('job-script-input');
+ const findJobTagsInput = () => wrapper.findByTestId('job-tags-input');
+ const findJobStageInput = () => wrapper.findByTestId('job-stage-input');
+
+ const dummyJobName = 'dummyJobName';
+ const dummyJobScript = 'dummyJobScript';
+ const dummyJobStage = 'dummyJobStage';
+ const dummyJobTags = ['tag1'];
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(JobSetupItem, {
+ propsData: {
+ availableStages: ['.pre', dummyJobStage, '.post'],
+ tagOptions: [
+ { id: 'tag1', name: 'tag1' },
+ { id: 'tag2', name: 'tag2' },
+ ],
+ isNameValid: true,
+ isScriptValid: true,
+ job: JSON.parse(JSON.stringify(JOB_TEMPLATE)),
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should emit update job event when filling inputs', () => {
+ expect(wrapper.emitted('update-job')).toBeUndefined();
+
+ findJobNameInput().vm.$emit('input', dummyJobName);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')[0]).toEqual(['name', dummyJobName]);
+
+ findJobScriptInput().vm.$emit('input', dummyJobScript);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(2);
+ expect(wrapper.emitted('update-job')[1]).toEqual(['script', dummyJobScript]);
+
+ findJobStageInput().vm.$emit('input', dummyJobStage);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(3);
+ expect(wrapper.emitted('update-job')[2]).toEqual(['stage', dummyJobStage]);
+
+ findJobTagsInput().vm.$emit('input', dummyJobTags);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(4);
+ expect(wrapper.emitted('update-job')[3]).toEqual(['tags', dummyJobTags]);
+ });
+});
diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js
new file mode 100644
index 00000000000..659ccb25996
--- /dev/null
+++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js
@@ -0,0 +1,70 @@
+import RulesItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import {
+ JOB_TEMPLATE,
+ JOB_RULES_WHEN,
+ JOB_RULES_START_IN,
+} from '~/ci/pipeline_editor/components/job_assistant_drawer/constants';
+
+describe('Rules item', () => {
+ let wrapper;
+
+ const findRulesWhenSelect = () => wrapper.findByTestId('rules-when-select');
+ const findRulesStartInNumberInput = () => wrapper.findByTestId('rules-start-in-number-input');
+ const findRulesStartInUnitSelect = () => wrapper.findByTestId('rules-start-in-unit-select');
+ const findRulesAllowFailureCheckBox = () => wrapper.findByTestId('rules-allow-failure-checkbox');
+
+ const dummyRulesWhen = JOB_RULES_WHEN.delayed.value;
+ const dummyRulesStartInNumber = 2;
+ const dummyRulesStartInUnit = JOB_RULES_START_IN.week.value;
+ const dummyRulesAllowFailure = true;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(RulesItem, {
+ propsData: {
+ isStartValid: true,
+ job: JSON.parse(JSON.stringify(JOB_TEMPLATE)),
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should emit update job event when filling inputs', () => {
+ expect(wrapper.emitted('update-job')).toBeUndefined();
+
+ findRulesWhenSelect().vm.$emit('input', dummyRulesWhen);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')[0]).toEqual([
+ 'rules[0].when',
+ JOB_RULES_WHEN.delayed.value,
+ ]);
+
+ findRulesStartInNumberInput().vm.$emit('input', dummyRulesStartInNumber);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(2);
+ expect(wrapper.emitted('update-job')[1]).toEqual([
+ 'rules[0].start_in',
+ `2 ${JOB_RULES_START_IN.second.value}s`,
+ ]);
+
+ findRulesStartInUnitSelect().vm.$emit('input', dummyRulesStartInUnit);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(3);
+ expect(wrapper.emitted('update-job')[2]).toEqual([
+ 'rules[0].start_in',
+ `2 ${dummyRulesStartInUnit}s`,
+ ]);
+
+ findRulesAllowFailureCheckBox().vm.$emit('input', dummyRulesAllowFailure);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(4);
+ expect(wrapper.emitted('update-job')[3]).toEqual([
+ 'rules[0].allow_failure',
+ dummyRulesAllowFailure,
+ ]);
+ });
+});
diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item_spec.js
new file mode 100644
index 00000000000..284d639c77f
--- /dev/null
+++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item_spec.js
@@ -0,0 +1,79 @@
+import ServicesItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { JOB_TEMPLATE } from '~/ci/pipeline_editor/components/job_assistant_drawer/constants';
+
+describe('Services item', () => {
+ let wrapper;
+
+ const findServiceNameInputByIndex = (index) =>
+ wrapper.findByTestId(`service-name-input-${index}`);
+ const findServiceEntrypointInputByIndex = (index) =>
+ wrapper.findByTestId(`service-entrypoint-input-${index}`);
+ const findDeleteItemButtonByIndex = (index) =>
+ wrapper.findByTestId(`delete-job-service-button-${index}`);
+ const findAddItemButton = () => wrapper.findByTestId('add-job-service-button');
+
+ const dummyServiceName = 'a';
+ const dummyServiceEntrypoint = ['b', 'c'];
+
+ const createComponent = ({ job = JSON.parse(JSON.stringify(JOB_TEMPLATE)) } = {}) => {
+ wrapper = shallowMountExtended(ServicesItem, {
+ propsData: {
+ job,
+ },
+ });
+ };
+
+ it('should emit update job event when filling inputs', () => {
+ createComponent();
+
+ expect(wrapper.emitted('update-job')).toBeUndefined();
+
+ findServiceNameInputByIndex(0).vm.$emit('input', dummyServiceName);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')[0]).toEqual(['services[0].name', dummyServiceName]);
+
+ findServiceEntrypointInputByIndex(0).vm.$emit('input', dummyServiceEntrypoint.join('\n'));
+
+ expect(wrapper.emitted('update-job')).toHaveLength(2);
+ expect(wrapper.emitted('update-job')[1]).toEqual([
+ 'services[0].entrypoint',
+ dummyServiceEntrypoint,
+ ]);
+ });
+
+ it('should emit update job event when click add item button', () => {
+ createComponent();
+
+ findAddItemButton().vm.$emit('click');
+
+ expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')[0]).toEqual([
+ 'services[1]',
+ { name: '', entrypoint: [''] },
+ ]);
+ });
+
+ it('should emit update job event when click delete item button', () => {
+ createComponent({
+ job: {
+ services: [
+ { name: 'a', entrypoint: ['a'] },
+ { name: 'b', entrypoint: ['b'] },
+ ],
+ },
+ });
+
+ findDeleteItemButtonByIndex(0).vm.$emit('click');
+
+ expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')[0]).toEqual(['services[0]']);
+ });
+
+ it('should not show delete item button when there is only one service', () => {
+ createComponent();
+
+ expect(findDeleteItemButtonByIndex(0).exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js
index 79200d92598..0258a1a8c7f 100644
--- a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js
@@ -1,24 +1,64 @@
import { GlDrawer } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
+import { stringify } from 'yaml';
import JobAssistantDrawer from '~/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue';
+import JobSetupItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue';
+import ImageItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue';
+import ServicesItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item.vue';
+import ArtifactsAndCacheItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item.vue';
+import RulesItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item.vue';
+import { JOB_RULES_WHEN } from '~/ci/pipeline_editor/components/job_assistant_drawer/constants';
+import getRunnerTags from '~/ci/pipeline_editor/graphql/queries/runner_tags.query.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import eventHub, { SCROLL_EDITOR_TO_BOTTOM } from '~/ci/pipeline_editor/event_hub';
+import { mockRunnersTagsQueryResponse, mockLintResponse, mockCiYml } from '../../mock_data';
Vue.use(VueApollo);
describe('Job assistant drawer', () => {
let wrapper;
+ let mockApollo;
+
+ const dummyJobName = 'dummyJobName';
+ const dummyJobScript = 'dummyJobScript';
+ const dummyImageName = 'dummyImageName';
+ const dummyImageEntrypoint = 'dummyImageEntrypoint';
+ const dummyServicesName = 'dummyServicesName';
+ const dummyServicesEntrypoint = 'dummyServicesEntrypoint';
+ const dummyArtifactsPath = 'dummyArtifactsPath';
+ const dummyArtifactsExclude = 'dummyArtifactsExclude';
+ const dummyCachePath = 'dummyCachePath';
+ const dummyCacheKey = 'dummyCacheKey';
+ const dummyRulesWhen = JOB_RULES_WHEN.delayed.value;
+ const dummyRulesStartIn = '1 second';
+ const dummyRulesAllowFailure = true;
const findDrawer = () => wrapper.findComponent(GlDrawer);
+ const findJobSetupItem = () => wrapper.findComponent(JobSetupItem);
+ const findImageItem = () => wrapper.findComponent(ImageItem);
+ const findServicesItem = () => wrapper.findComponent(ServicesItem);
+ const findArtifactsAndCacheItem = () => wrapper.findComponent(ArtifactsAndCacheItem);
+ const findRulesItem = () => wrapper.findComponent(RulesItem);
+ const findConfirmButton = () => wrapper.findByTestId('confirm-button');
const findCancelButton = () => wrapper.findByTestId('cancel-button');
const createComponent = () => {
+ mockApollo = createMockApollo([
+ [getRunnerTags, jest.fn().mockResolvedValue(mockRunnersTagsQueryResponse)],
+ ]);
+
wrapper = mountExtended(JobAssistantDrawer, {
propsData: {
+ ciConfigData: mockLintResponse,
+ ciFileContent: mockCiYml,
isVisible: true,
},
+ apolloProvider: mockApollo,
});
};
@@ -27,6 +67,35 @@ describe('Job assistant drawer', () => {
await waitForPromises();
});
+ it('should contain job setup accordion', () => {
+ expect(findJobSetupItem().exists()).toBe(true);
+ });
+
+ it('job setup item should have tag options', () => {
+ expect(findJobSetupItem().props('tagOptions')).toEqual([
+ { id: 'tag1', name: 'tag1' },
+ { id: 'tag2', name: 'tag2' },
+ { id: 'tag3', name: 'tag3' },
+ { id: 'tag4', name: 'tag4' },
+ ]);
+ });
+
+ it('should contain image accordion', () => {
+ expect(findImageItem().exists()).toBe(true);
+ });
+
+ it('should contain services accordion', () => {
+ expect(findServicesItem().exists()).toBe(true);
+ });
+
+ it('should contain artifacts and cache item accordion', () => {
+ expect(findArtifactsAndCacheItem().exists()).toBe(true);
+ });
+
+ it('should contain rules accordion', () => {
+ expect(findRulesItem().exists()).toBe(true);
+ });
+
it('should emit close job assistant drawer event when closing the drawer', () => {
expect(wrapper.emitted('close-job-assistant-drawer')).toBeUndefined();
@@ -42,4 +111,185 @@ describe('Job assistant drawer', () => {
expect(wrapper.emitted('close-job-assistant-drawer')).toHaveLength(1);
});
+
+ it('should block submit if job name is empty', async () => {
+ findJobSetupItem().vm.$emit('update-job', 'script', 'b');
+ findConfirmButton().trigger('click');
+
+ await nextTick();
+
+ expect(findJobSetupItem().props('isNameValid')).toBe(false);
+ expect(findJobSetupItem().props('isScriptValid')).toBe(true);
+ expect(wrapper.emitted('updateCiConfig')).toBeUndefined();
+ });
+
+ it('should block submit if rules when is delayed and start in is out of range', async () => {
+ findRulesItem().vm.$emit('update-job', 'rules[0].when', JOB_RULES_WHEN.delayed.value);
+ findRulesItem().vm.$emit('update-job', 'rules[0].start_in', '2 weeks');
+ findConfirmButton().trigger('click');
+
+ await nextTick();
+
+ expect(wrapper.emitted('updateCiConfig')).toBeUndefined();
+ });
+
+ describe('when enter valid input', () => {
+ beforeEach(() => {
+ findJobSetupItem().vm.$emit('update-job', 'name', dummyJobName);
+ findJobSetupItem().vm.$emit('update-job', 'script', dummyJobScript);
+ findImageItem().vm.$emit('update-job', 'image.name', dummyImageName);
+ findImageItem().vm.$emit('update-job', 'image.entrypoint', [dummyImageEntrypoint]);
+ findServicesItem().vm.$emit('update-job', 'services[0].name', dummyServicesName);
+ findServicesItem().vm.$emit('update-job', 'services[0].entrypoint', [
+ dummyServicesEntrypoint,
+ ]);
+ findArtifactsAndCacheItem().vm.$emit('update-job', 'artifacts.paths', [dummyArtifactsPath]);
+ findArtifactsAndCacheItem().vm.$emit('update-job', 'artifacts.exclude', [
+ dummyArtifactsExclude,
+ ]);
+ findArtifactsAndCacheItem().vm.$emit('update-job', 'cache.paths', [dummyCachePath]);
+ findArtifactsAndCacheItem().vm.$emit('update-job', 'cache.key', dummyCacheKey);
+ findRulesItem().vm.$emit('update-job', 'rules[0].allow_failure', dummyRulesAllowFailure);
+ findRulesItem().vm.$emit('update-job', 'rules[0].when', dummyRulesWhen);
+ findRulesItem().vm.$emit('update-job', 'rules[0].start_in', dummyRulesStartIn);
+ });
+
+ it('passes correct prop to accordions', () => {
+ const accordions = [
+ findJobSetupItem(),
+ findImageItem(),
+ findServicesItem(),
+ findArtifactsAndCacheItem(),
+ findRulesItem(),
+ ];
+ accordions.forEach((accordion) => {
+ expect(accordion.props('job')).toMatchObject({
+ name: dummyJobName,
+ script: dummyJobScript,
+ image: {
+ name: dummyImageName,
+ entrypoint: [dummyImageEntrypoint],
+ },
+ services: [
+ {
+ name: dummyServicesName,
+ entrypoint: [dummyServicesEntrypoint],
+ },
+ ],
+ artifacts: {
+ paths: [dummyArtifactsPath],
+ exclude: [dummyArtifactsExclude],
+ },
+ cache: {
+ paths: [dummyCachePath],
+ key: dummyCacheKey,
+ },
+ rules: [
+ {
+ allow_failure: dummyRulesAllowFailure,
+ when: dummyRulesWhen,
+ start_in: dummyRulesStartIn,
+ },
+ ],
+ });
+ });
+ });
+
+ it('job name and script state should be valid', () => {
+ expect(findJobSetupItem().props('isNameValid')).toBe(true);
+ expect(findJobSetupItem().props('isScriptValid')).toBe(true);
+ });
+
+ it('should clear job data when click confirm button', async () => {
+ findConfirmButton().trigger('click');
+
+ await nextTick();
+
+ expect(findJobSetupItem().props('job')).toMatchObject({ name: '', script: '' });
+ });
+
+ it('should clear job data when click cancel button', async () => {
+ findCancelButton().trigger('click');
+
+ await nextTick();
+
+ expect(findJobSetupItem().props('job')).toMatchObject({ name: '', script: '' });
+ });
+
+ it('should omit keys with default value when click add button', () => {
+ findRulesItem().vm.$emit('update-job', 'rules[0].allow_failure', false);
+ findRulesItem().vm.$emit('update-job', 'rules[0].when', JOB_RULES_WHEN.onSuccess.value);
+ findRulesItem().vm.$emit('update-job', 'rules[0].start_in', dummyRulesStartIn);
+ findConfirmButton().trigger('click');
+
+ expect(wrapper.emitted('updateCiConfig')).toStrictEqual([
+ [
+ `${wrapper.props('ciFileContent')}\n${stringify({
+ [dummyJobName]: {
+ script: dummyJobScript,
+ image: { name: dummyImageName, entrypoint: [dummyImageEntrypoint] },
+ services: [
+ {
+ name: dummyServicesName,
+ entrypoint: [dummyServicesEntrypoint],
+ },
+ ],
+ artifacts: {
+ paths: [dummyArtifactsPath],
+ exclude: [dummyArtifactsExclude],
+ },
+ cache: {
+ paths: [dummyCachePath],
+ key: dummyCacheKey,
+ },
+ },
+ })}`,
+ ],
+ ]);
+ });
+
+ it('should update correct ci content when click add button', () => {
+ findConfirmButton().trigger('click');
+
+ expect(wrapper.emitted('updateCiConfig')).toStrictEqual([
+ [
+ `${wrapper.props('ciFileContent')}\n${stringify({
+ [dummyJobName]: {
+ script: dummyJobScript,
+ image: { name: dummyImageName, entrypoint: [dummyImageEntrypoint] },
+ services: [
+ {
+ name: dummyServicesName,
+ entrypoint: [dummyServicesEntrypoint],
+ },
+ ],
+ artifacts: {
+ paths: [dummyArtifactsPath],
+ exclude: [dummyArtifactsExclude],
+ },
+ cache: {
+ paths: [dummyCachePath],
+ key: dummyCacheKey,
+ },
+ rules: [
+ {
+ allow_failure: dummyRulesAllowFailure,
+ when: dummyRulesWhen,
+ start_in: dummyRulesStartIn,
+ },
+ ],
+ },
+ })}`,
+ ],
+ ]);
+ });
+
+ it('should emit scroll editor to button event when click add button', () => {
+ const eventHubSpy = jest.spyOn(eventHub, '$emit');
+
+ findConfirmButton().trigger('click');
+
+ expect(eventHubSpy).toHaveBeenCalledWith(SCROLL_EDITOR_TO_BOTTOM);
+ });
+ });
});
diff --git a/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_results_spec.js b/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_results_spec.js
index d43bdec3a33..cc9a77ae525 100644
--- a/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_results_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_results_spec.js
@@ -40,10 +40,6 @@ describe('CI Lint Results', () => {
const findAfterScripts = findAllByTestId('after-script');
const filterEmptyScripts = (property) => mockJobs.filter((job) => job[property].length !== 0);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Empty results', () => {
it('renders with no jobs, errors or warnings defined', () => {
createComponent({ jobs: undefined, errors: undefined, warnings: undefined }, shallowMount);
diff --git a/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_warnings_spec.js b/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_warnings_spec.js
index b5e3ea06c2c..d09e22898cd 100644
--- a/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_warnings_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_warnings_spec.js
@@ -21,11 +21,6 @@ describe('CI lint warnings', () => {
const findWarnings = () => wrapper.findAll('[data-testid="ci-lint-warning"]');
const findWarningMessage = () => trimText(wrapper.findComponent(GlSprintf).text());
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('displays the warning alert', () => {
createComponent();
diff --git a/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js
index f40db50aab7..471b033913b 100644
--- a/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js
@@ -57,6 +57,7 @@ describe('Pipeline editor tabs component', () => {
isNewCiConfigFile: true,
showDrawer: false,
showJobAssistantDrawer: false,
+ showAiAssistantDrawer: false,
...props,
},
data() {
@@ -65,6 +66,7 @@ describe('Pipeline editor tabs component', () => {
};
},
provide: {
+ aiChatAvailable: false,
ciConfigPath: '/path/to/ci-config',
ciLintPath: mockCiLintPath,
currentBranch: 'main',
@@ -119,6 +121,7 @@ describe('Pipeline editor tabs component', () => {
});
afterEach(() => {
+ // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy
wrapper.destroy();
});
@@ -313,13 +316,13 @@ describe('Pipeline editor tabs component', () => {
createComponent();
});
- it('shows walkthrough popover', async () => {
+ it('shows walkthrough popover', () => {
expect(findWalkthroughPopover().exists()).toBe(true);
});
});
describe('when isNewCiConfigFile prop is false', () => {
- it('does not show walkthrough popover', async () => {
+ it('does not show walkthrough popover', () => {
createComponent({ props: { isNewCiConfigFile: false } });
expect(findWalkthroughPopover().exists()).toBe(false);
});
diff --git a/spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js b/spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js
index 63ebfc0559d..3d84f06967a 100644
--- a/spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js
@@ -22,11 +22,10 @@ describe('FileTreePopover component', () => {
afterEach(() => {
localStorage.clear();
- wrapper.destroy();
});
describe('default', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent({ stubs: { GlSprintf } });
});
@@ -46,7 +45,7 @@ describe('FileTreePopover component', () => {
});
describe('when popover has already been dismissed before', () => {
- it('does not render popover', async () => {
+ it('does not render popover', () => {
localStorage.setItem(FILE_TREE_POPOVER_DISMISSED_KEY, 'true');
createComponent();
diff --git a/spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js b/spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js
index cf0b974081e..18eec48ad83 100644
--- a/spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js
@@ -19,12 +19,8 @@ describe('ValidatePopover component', () => {
const findHelpLink = () => wrapper.findByTestId('help-link');
const findFeedbackLink = () => wrapper.findByTestId('feedback-link');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent({
stubs: { GlLink, GlSprintf },
});
diff --git a/spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js b/spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js
index ca6033f2ff5..37339b1c422 100644
--- a/spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js
@@ -12,17 +12,13 @@ describe('WalkthroughPopover component', () => {
return extendedWrapper(mountFn(WalkthroughPopover));
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('CTA button clicked', () => {
beforeEach(async () => {
wrapper = createComponent(mount);
await wrapper.findByTestId('ctaBtn').trigger('click');
});
- it('emits "walkthrough-popover-cta-clicked" event', async () => {
+ it('emits "walkthrough-popover-cta-clicked" event', () => {
expect(wrapper.emitted()['walkthrough-popover-cta-clicked']).toHaveLength(1);
});
});
diff --git a/spec/frontend/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js b/spec/frontend/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js
index b22c98e5544..8b8dd4d22c2 100644
--- a/spec/frontend/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js
@@ -4,10 +4,9 @@ import ConfirmDialog from '~/ci/pipeline_editor/components/ui/confirm_unsaved_ch
describe('pipeline_editor/components/ui/confirm_unsaved_changes_dialog', () => {
let beforeUnloadEvent;
let setDialogContent;
- let wrapper;
const createComponent = (propsData = {}) => {
- wrapper = shallowMount(ConfirmDialog, {
+ shallowMount(ConfirmDialog, {
propsData,
});
};
@@ -21,7 +20,6 @@ describe('pipeline_editor/components/ui/confirm_unsaved_changes_dialog', () => {
afterEach(() => {
beforeUnloadEvent.preventDefault.mockRestore();
setDialogContent.mockRestore();
- wrapper.destroy();
});
it('shows confirmation dialog when there are unsaved changes', () => {
diff --git a/spec/frontend/ci/pipeline_editor/components/ui/editor_tab_spec.js b/spec/frontend/ci/pipeline_editor/components/ui/editor_tab_spec.js
index a4e7abba7b0..f02b1f5efbc 100644
--- a/spec/frontend/ci/pipeline_editor/components/ui/editor_tab_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/ui/editor_tab_spec.js
@@ -64,7 +64,7 @@ describe('~/ci/pipeline_editor/components/ui/editor_tab.vue', () => {
mockChildMounted = jest.fn();
});
- it('tabs are mounted lazily', async () => {
+ it('tabs are mounted lazily', () => {
createMockedWrapper();
expect(mockChildMounted).toHaveBeenCalledTimes(0);
@@ -192,7 +192,7 @@ describe('~/ci/pipeline_editor/components/ui/editor_tab.vue', () => {
createMockedWrapper();
});
- it('renders correct number of badges', async () => {
+ it('renders correct number of badges', () => {
expect(findBadges()).toHaveLength(1);
expect(findBadges().at(0).text()).toBe('NEW');
});
diff --git a/spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js b/spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
index 3c68f74af43..e636a89c6d9 100644
--- a/spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
@@ -23,10 +23,6 @@ describe('Pipeline editor empty state', () => {
const findConfirmButton = () => wrapper.findComponent(GlButton);
const findDescription = () => wrapper.findComponent(GlSprintf);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when project uses an external CI config', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js b/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js
index ae25142b455..2349816fa86 100644
--- a/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlDropdown, GlIcon, GlLoadingIcon, GlPopover } from '@gitlab/ui';
+import { GlAlert, GlDisclosureDropdown, GlIcon, GlLoadingIcon, GlPopover } from '@gitlab/ui';
import { nextTick } from 'vue';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
@@ -90,7 +90,7 @@ describe('Pipeline Editor Validate Tab', () => {
const findHelpIcon = () => wrapper.findComponent(GlIcon);
const findIllustration = () => wrapper.findByRole('img');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findPipelineSource = () => wrapper.findComponent(GlDropdown);
+ const findPipelineSource = () => wrapper.findComponent(GlDisclosureDropdown);
const findPopover = () => wrapper.findComponent(GlPopover);
const findCiLintResults = () => wrapper.findComponent(CiLintResults);
const findResultsCta = () => wrapper.findByTestId('resimulate-pipeline-button');
@@ -99,10 +99,6 @@ describe('Pipeline Editor Validate Tab', () => {
mockBlobContentData = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('while initial CI content is loading', () => {
beforeEach(() => {
createComponent({ isBlobLoading: true });
@@ -122,7 +118,7 @@ describe('Pipeline Editor Validate Tab', () => {
it('renders disabled pipeline source dropdown', () => {
expect(findPipelineSource().exists()).toBe(true);
- expect(findPipelineSource().attributes('text')).toBe(i18n.pipelineSourceDefault);
+ expect(findPipelineSource().attributes('toggletext')).toBe(i18n.pipelineSourceDefault);
expect(findPipelineSource().props('disabled')).toBe(true);
});
diff --git a/spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js b/spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js
index 6a6cc3a14de..893f6775ac5 100644
--- a/spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js
+++ b/spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js
@@ -34,7 +34,7 @@ describe('~/ci/pipeline_editor/graphql/resolvers', () => {
});
/* eslint-disable no-underscore-dangle */
- it('lint data has correct type names', async () => {
+ it('lint data has correct type names', () => {
expect(result.__typename).toBe('CiLintContent');
expect(result.jobs[0].__typename).toBe('CiLintJob');
diff --git a/spec/frontend/ci/pipeline_editor/mock_data.js b/spec/frontend/ci/pipeline_editor/mock_data.js
index 541123d7efc..865dd34fbfe 100644
--- a/spec/frontend/ci/pipeline_editor/mock_data.js
+++ b/spec/frontend/ci/pipeline_editor/mock_data.js
@@ -12,7 +12,7 @@ export const mockCommitSha = 'aabbccdd';
export const mockCommitNextSha = 'eeffgghh';
export const mockIncludesHelpPagePath = '/-/includes/help';
export const mockLintHelpPagePath = '/-/lint-help';
-export const mockLintUnavailableHelpPagePath = '/-/pipeline-editor/troubleshoot';
+export const mockCiTroubleshootingPath = '/-/pipeline-editor/troubleshoot';
export const mockSimulatePipelineHelpPagePath = '/-/simulate-pipeline-help';
export const mockYmlHelpPagePath = '/-/yml-help';
export const mockCommitMessage = 'My commit message';
@@ -583,6 +583,36 @@ export const mockCommitCreateResponse = {
},
};
+export const mockRunnersTagsQueryResponse = {
+ data: {
+ runners: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::Runner/1',
+ tagList: ['tag1', 'tag2'],
+ __typename: 'CiRunner',
+ },
+ {
+ id: 'gid://gitlab/Ci::Runner/2',
+ tagList: ['tag2', 'tag3'],
+ __typename: 'CiRunner',
+ },
+ {
+ id: 'gid://gitlab/Ci::Runner/3',
+ tagList: ['tag2', 'tag4'],
+ __typename: 'CiRunner',
+ },
+ {
+ id: 'gid://gitlab/Ci::Runner/4',
+ tagList: [],
+ __typename: 'CiRunner',
+ },
+ ],
+ __typename: 'CiRunnerConnection',
+ },
+ },
+};
+
export const mockCommitCreateResponseNewEtag = {
data: {
commitCreate: {
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 a103acb33bc..cc4a022c2df 100644
--- a/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlAlert, GlButton, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -6,7 +6,7 @@ import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
-import { objectToQuery, redirectTo } from '~/lib/utils/url_utility';
+import { objectToQuery, redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
import PipelineEditorTabs from '~/ci/pipeline_editor/components/pipeline_editor_tabs.vue';
import PipelineEditorEmptyState from '~/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
@@ -96,7 +96,7 @@ describe('Pipeline editor app component', () => {
});
};
- const createComponentWithApollo = async ({
+ const createComponentWithApollo = ({
provide = {},
stubs = {},
withUndefinedBranch = false,
@@ -162,10 +162,6 @@ describe('Pipeline editor app component', () => {
mockPipelineQuery = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('loading state', () => {
it('displays a loading icon if the blob query is loading', () => {
createComponent({ blobLoading: true });
@@ -264,7 +260,7 @@ describe('Pipeline editor app component', () => {
expect(findAlert().exists()).toBe(false);
});
- it('ci config query is called with correct variables', async () => {
+ it('ci config query is called with correct variables', () => {
expect(mockCiConfigData).toHaveBeenCalledWith({
content: mockCiYml,
projectPath: mockProjectFullPath,
@@ -291,7 +287,7 @@ describe('Pipeline editor app component', () => {
.mockImplementation(jest.fn());
});
- it('shows an empty state and does not show editor home component', async () => {
+ it('shows an empty state and does not show editor home component', () => {
expect(findEmptyState().exists()).toBe(true);
expect(findAlert().exists()).toBe(false);
expect(findEditorHome().exists()).toBe(false);
@@ -351,7 +347,9 @@ describe('Pipeline editor app component', () => {
});
it('shows that the lint service is down', () => {
- expect(findValidationSegment().text()).toContain(
+ const validationMessage = findValidationSegment().findComponent(GlSprintf);
+
+ expect(validationMessage.attributes('message')).toContain(
validationSegmenti18n.unavailableValidation,
);
});
@@ -436,7 +434,7 @@ describe('Pipeline editor app component', () => {
'merge_request[target_branch]': mockDefaultBranch,
});
- expect(redirectTo).toHaveBeenCalledWith(`${mockNewMergeRequestPath}?${branchesQuery}`);
+ expect(redirectTo).toHaveBeenCalledWith(`${mockNewMergeRequestPath}?${branchesQuery}`); // eslint-disable-line import/no-deprecated
});
});
diff --git a/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js
index 4f8f2112abe..576263d5418 100644
--- a/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js
+++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js
@@ -41,6 +41,7 @@ describe('Pipeline editor home wrapper', () => {
...props,
},
provide: {
+ aiChatAvailable: false,
projectFullPath: '',
totalBranches: 19,
glFeatures: {
@@ -67,7 +68,6 @@ describe('Pipeline editor home wrapper', () => {
afterEach(() => {
localStorage.clear();
- wrapper.destroy();
});
describe('renders', () => {
diff --git a/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js
index 6f18899ebac..1d4ae33c667 100644
--- a/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js
+++ b/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js
@@ -4,7 +4,7 @@ import { GlForm, GlDropdownItem, GlSprintf, GlLoadingIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import CreditCardValidationRequiredAlert from 'ee_component/billings/components/cc_validation_required_alert.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
-import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
@@ -13,7 +13,7 @@ import {
HTTP_STATUS_INTERNAL_SERVER_ERROR,
HTTP_STATUS_OK,
} from '~/lib/utils/http_status';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import PipelineNewForm, {
POLLING_INTERVAL,
} from '~/ci/pipeline_new/components/pipeline_new_form.vue';
@@ -30,8 +30,8 @@ import {
mockQueryParams,
mockPostParams,
mockProjectId,
- mockRefs,
mockYamlVariables,
+ mockPipelineConfigButtonText,
} from '../mock_data';
Vue.use(VueApollo);
@@ -40,8 +40,8 @@ jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
}));
-const projectRefsEndpoint = '/root/project/refs';
const pipelinesPath = '/root/project/-/pipelines';
+const pipelinesEditorPath = '/root/project/-/ci/editor';
const projectPath = '/root/project/-/pipelines/config_variables';
const newPipelinePostResponse = { id: 1 };
const defaultBranch = 'main';
@@ -65,6 +65,7 @@ describe('Pipeline New Form', () => {
wrapper.findAllByTestId('pipeline-form-ci-variable-value-dropdown');
const findValueDropdownItems = (dropdown) => dropdown.findAllComponents(GlDropdownItem);
const findErrorAlert = () => wrapper.findByTestId('run-pipeline-error-alert');
+ const findPipelineConfigButton = () => wrapper.findByTestId('ci-cd-pipeline-configuration');
const findWarningAlert = () => wrapper.findByTestId('run-pipeline-warning-alert');
const findWarningAlertSummary = () => findWarningAlert().findComponent(GlSprintf);
const findWarnings = () => wrapper.findAllByTestId('run-pipeline-warning');
@@ -88,24 +89,23 @@ describe('Pipeline New Form', () => {
const changeKeyInputValue = async (keyInputIndex, value) => {
const input = findKeyInputs().at(keyInputIndex);
- input.element.value = value;
- input.trigger('change');
+ input.vm.$emit('input', value);
+ input.vm.$emit('change');
await nextTick();
};
- const createComponentWithApollo = ({ method = shallowMountExtended, props = {} } = {}) => {
+ const createComponentWithApollo = ({ props = {} } = {}) => {
const handlers = [[ciConfigVariablesQuery, mockCiConfigVariables]];
mockApollo = createMockApollo(handlers, resolvers);
- wrapper = method(PipelineNewForm, {
+ wrapper = shallowMountExtended(PipelineNewForm, {
apolloProvider: mockApollo,
- provide: {
- projectRefsEndpoint,
- },
propsData: {
projectId: mockProjectId,
pipelinesPath,
+ pipelinesEditorPath,
+ canViewPipelineEditor: true,
projectPath,
defaultBranch,
refParam: defaultBranch,
@@ -119,7 +119,6 @@ describe('Pipeline New Form', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
mockCiConfigVariables = jest.fn();
- mock.onGet(projectRefsEndpoint).reply(HTTP_STATUS_OK, mockRefs);
dummySubmitEvent = {
preventDefault: jest.fn(),
@@ -128,17 +127,16 @@ describe('Pipeline New Form', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('Form', () => {
beforeEach(async () => {
mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
- createComponentWithApollo({ props: mockQueryParams, method: mountExtended });
+ createComponentWithApollo({ props: mockQueryParams });
await waitForPromises();
});
- it('displays the correct values for the provided query params', async () => {
+ it('displays the correct values for the provided query params', () => {
expect(findVariableTypes().at(0).props('text')).toBe('Variable');
expect(findVariableTypes().at(1).props('text')).toBe('File');
expect(findRefsDropdown().props('value')).toEqual({ shortName: 'tag-1' });
@@ -146,13 +144,13 @@ describe('Pipeline New Form', () => {
});
it('displays a variable from provided query params', () => {
- expect(findKeyInputs().at(0).element.value).toBe('test_var');
- expect(findValueInputs().at(0).element.value).toBe('test_var_val');
+ expect(findKeyInputs().at(0).attributes('value')).toBe('test_var');
+ expect(findValueInputs().at(0).attributes('value')).toBe('test_var_val');
});
- it('displays an empty variable for the user to fill out', async () => {
- expect(findKeyInputs().at(2).element.value).toBe('');
- expect(findValueInputs().at(2).element.value).toBe('');
+ it('displays an empty variable for the user to fill out', () => {
+ expect(findKeyInputs().at(2).attributes('value')).toBe('');
+ expect(findValueInputs().at(2).attributes('value')).toBe('');
expect(findVariableTypes().at(2).props('text')).toBe('Variable');
});
@@ -161,7 +159,7 @@ describe('Pipeline New Form', () => {
});
it('removes ci variable row on remove icon button click', async () => {
- findRemoveIcons().at(1).trigger('click');
+ findRemoveIcons().at(1).vm.$emit('click');
await nextTick();
@@ -170,24 +168,25 @@ describe('Pipeline New Form', () => {
it('creates blank variable on input change event', async () => {
const input = findKeyInputs().at(2);
- input.element.value = 'test_var_2';
- input.trigger('change');
+
+ input.vm.$emit('input', 'test_var_2');
+ input.vm.$emit('change');
await nextTick();
expect(findVariableRows()).toHaveLength(4);
- expect(findKeyInputs().at(3).element.value).toBe('');
- expect(findValueInputs().at(3).element.value).toBe('');
+ expect(findKeyInputs().at(3).attributes('value')).toBe('');
+ expect(findValueInputs().at(3).attributes('value')).toBe('');
});
});
describe('Pipeline creation', () => {
- beforeEach(async () => {
+ beforeEach(() => {
mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
mock.onPost(pipelinesPath).reply(HTTP_STATUS_OK, newPipelinePostResponse);
});
- it('does not submit the native HTML form', async () => {
+ it('does not submit the native HTML form', () => {
createComponentWithApollo();
findForm().vm.$emit('submit', dummySubmitEvent);
@@ -213,7 +212,7 @@ describe('Pipeline New Form', () => {
await waitForPromises();
expect(getFormPostParams().ref).toEqual(`refs/heads/${defaultBranch}`);
- expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`);
+ expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`); // eslint-disable-line import/no-deprecated
});
it('creates a pipeline with short ref and variables from the query params', async () => {
@@ -226,14 +225,14 @@ describe('Pipeline New Form', () => {
await waitForPromises();
expect(getFormPostParams()).toEqual(mockPostParams);
- expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`);
+ expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`); // eslint-disable-line import/no-deprecated
});
});
describe('When the ref has been changed', () => {
beforeEach(async () => {
mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
- createComponentWithApollo({ method: mountExtended });
+ createComponentWithApollo();
await waitForPromises();
});
@@ -247,12 +246,12 @@ describe('Pipeline New Form', () => {
await selectBranch('main');
- expect(findKeyInputs().at(0).element.value).toBe('build_var');
+ expect(findKeyInputs().at(0).attributes('value')).toBe('build_var');
expect(findVariableRows().length).toBe(2);
await selectBranch('branch-1');
- expect(findKeyInputs().at(0).element.value).toBe('deploy_var');
+ expect(findKeyInputs().at(0).attributes('value')).toBe('deploy_var');
expect(findVariableRows().length).toBe(2);
});
@@ -276,7 +275,7 @@ describe('Pipeline New Form', () => {
describe('When there are no variables in the API cache', () => {
beforeEach(async () => {
mockCiConfigVariables.mockResolvedValue(mockNoCachedCiConfigVariablesResponse);
- createComponentWithApollo({ method: mountExtended });
+ createComponentWithApollo();
await waitForPromises();
});
@@ -324,9 +323,9 @@ describe('Pipeline New Form', () => {
});
const testBehaviorWhenCacheIsPopulated = (queryResponse) => {
- beforeEach(async () => {
+ beforeEach(() => {
mockCiConfigVariables.mockResolvedValue(queryResponse);
- createComponentWithApollo({ method: mountExtended });
+ createComponentWithApollo();
});
it('does not poll for new values', async () => {
@@ -341,6 +340,9 @@ describe('Pipeline New Form', () => {
});
it('loading icon is shown when content is requested and hidden when received', async () => {
+ mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
+ createComponentWithApollo({ props: mockQueryParams });
+
expect(findLoadingIcon().exists()).toBe(true);
await waitForPromises();
@@ -354,11 +356,11 @@ describe('Pipeline New Form', () => {
it('displays an empty form', async () => {
mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
- createComponentWithApollo({ method: mountExtended });
+ createComponentWithApollo();
await waitForPromises();
- expect(findKeyInputs().at(0).element.value).toBe('');
- expect(findValueInputs().at(0).element.value).toBe('');
+ expect(findKeyInputs().at(0).attributes('value')).toBe('');
+ expect(findValueInputs().at(0).attributes('value')).toBe('');
expect(findVariableTypes().at(0).props('text')).toBe('Variable');
});
});
@@ -369,12 +371,12 @@ describe('Pipeline New Form', () => {
describe('with different predefined values', () => {
beforeEach(async () => {
mockCiConfigVariables.mockResolvedValue(mockCiConfigVariablesResponse);
- createComponentWithApollo({ method: mountExtended });
+ createComponentWithApollo();
await waitForPromises();
});
it('multi-line strings are added to the value field without removing line breaks', () => {
- expect(findValueInputs().at(1).element.value).toBe(mockYamlVariables[1].value);
+ expect(findValueInputs().at(1).attributes('value')).toBe(mockYamlVariables[1].value);
});
it('multiple predefined values are rendered as a dropdown', () => {
@@ -398,24 +400,24 @@ describe('Pipeline New Form', () => {
describe('with description', () => {
beforeEach(async () => {
mockCiConfigVariables.mockResolvedValue(mockCiConfigVariablesResponse);
- createComponentWithApollo({ props: mockQueryParams, method: mountExtended });
+ createComponentWithApollo({ props: mockQueryParams });
await waitForPromises();
});
- it('displays all the variables', async () => {
+ it('displays all the variables', () => {
expect(findVariableRows()).toHaveLength(6);
});
it('displays a variable from yml', () => {
- expect(findKeyInputs().at(0).element.value).toBe(mockYamlVariables[0].key);
- expect(findValueInputs().at(0).element.value).toBe(mockYamlVariables[0].value);
+ expect(findKeyInputs().at(0).attributes('value')).toBe(mockYamlVariables[0].key);
+ expect(findValueInputs().at(0).attributes('value')).toBe(mockYamlVariables[0].value);
});
it('displays a variable from provided query params', () => {
- expect(findKeyInputs().at(3).element.value).toBe(
+ expect(findKeyInputs().at(3).attributes('value')).toBe(
Object.keys(mockQueryParams.variableParams)[0],
);
- expect(findValueInputs().at(3).element.value).toBe(
+ expect(findValueInputs().at(3).attributes('value')).toBe(
Object.values(mockQueryParams.fileParams)[0],
);
});
@@ -425,7 +427,7 @@ describe('Pipeline New Form', () => {
});
it('removes the description when a variable key changes', async () => {
- findKeyInputs().at(0).element.value = 'yml_var_modified';
+ findKeyInputs().at(0).vm.$emit('input', 'yml_var_modified');
findKeyInputs().at(0).trigger('change');
await nextTick();
@@ -437,11 +439,11 @@ describe('Pipeline New Form', () => {
describe('without description', () => {
beforeEach(async () => {
mockCiConfigVariables.mockResolvedValue(mockCiConfigVariablesResponseWithoutDesc);
- createComponentWithApollo({ method: mountExtended });
+ createComponentWithApollo();
await waitForPromises();
});
- it('displays variables with description only', async () => {
+ it('displays variables with description only', () => {
expect(findVariableRows()).toHaveLength(2); // extra empty variable is added at the end
});
});
@@ -456,7 +458,7 @@ describe('Pipeline New Form', () => {
describe('when the refs cannot be loaded', () => {
beforeEach(() => {
mock
- .onGet(projectRefsEndpoint, { params: { search: '' } })
+ .onGet('/api/v4/projects/8/repository/branches', { params: { search: '' } })
.reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
findRefsDropdown().vm.$emit('loadingError');
@@ -500,6 +502,17 @@ describe('Pipeline New Form', () => {
expect(findSubmitButton().props('disabled')).toBe(false);
});
+ it('shows pipeline configuration button for user who can view', () => {
+ expect(findPipelineConfigButton().exists()).toBe(true);
+ expect(findPipelineConfigButton().text()).toBe(mockPipelineConfigButtonText);
+ });
+
+ it('does not show pipeline configuration button for user who can not view', () => {
+ createComponentWithApollo({ props: { canViewPipelineEditor: false } });
+
+ expect(findPipelineConfigButton().exists()).toBe(false);
+ });
+
it('does not show the credit card validation required alert', () => {
expect(findCCAlert().exists()).toBe(false);
});
diff --git a/spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js b/spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js
index cf8009e388f..01c7dd7eb84 100644
--- a/spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js
+++ b/spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js
@@ -1,35 +1,22 @@
-import { GlListbox, GlListboxItem } from '@gitlab/ui';
-import MockAdapter from 'axios-mock-adapter';
-import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import axios from '~/lib/utils/axios_utils';
-import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { shallowMount } from '@vue/test-utils';
+import RefSelector from '~/ref/components/ref_selector.vue';
import RefsDropdown from '~/ci/pipeline_new/components/refs_dropdown.vue';
+import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
-import { mockBranches, mockRefs, mockFilteredRefs, mockTags } from '../mock_data';
-
-const projectRefsEndpoint = '/root/project/refs';
+const projectId = '8';
const refShortName = 'main';
const refFullName = 'refs/heads/main';
-jest.mock('~/flash');
-
describe('Pipeline New Form', () => {
let wrapper;
- let mock;
- const findDropdown = () => wrapper.findComponent(GlListbox);
- const findRefsDropdownItems = () => wrapper.findAllComponents(GlListboxItem);
- const findSearchBox = () => wrapper.findByTestId('listbox-search-input');
- const findListboxGroups = () => wrapper.findAll('ul[role="group"]');
+ const findRefSelector = () => wrapper.findComponent(RefSelector);
- const createComponent = (props = {}, mountFn = shallowMountExtended) => {
- wrapper = mountFn(RefsDropdown, {
- provide: {
- projectRefsEndpoint,
- },
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(RefsDropdown, {
propsData: {
+ projectId,
value: {
shortName: refShortName,
fullName: refFullName,
@@ -39,163 +26,54 @@ describe('Pipeline New Form', () => {
});
};
- beforeEach(() => {
- mock = new MockAdapter(axios);
- mock.onGet(projectRefsEndpoint, { params: { search: '' } }).reply(HTTP_STATUS_OK, mockRefs);
- });
-
- beforeEach(() => {
- createComponent();
- });
-
- it('displays empty dropdown initially', () => {
- findDropdown().vm.$emit('shown');
-
- expect(findRefsDropdownItems()).toHaveLength(0);
- });
-
- it('does not make requests immediately', async () => {
- expect(mock.history.get).toHaveLength(0);
- });
-
describe('when user opens dropdown', () => {
- beforeEach(async () => {
- createComponent({}, mountExtended);
- findDropdown().vm.$emit('shown');
- await waitForPromises();
+ beforeEach(() => {
+ createComponent();
});
- it('requests unfiltered tags and branches', () => {
- expect(mock.history.get).toHaveLength(1);
- expect(mock.history.get[0].url).toBe(projectRefsEndpoint);
- expect(mock.history.get[0].params).toEqual({ search: '' });
+ it('has default selected branch', () => {
+ expect(findRefSelector().props('value')).toBe('main');
});
- it('displays dropdown with branches and tags', () => {
- const refLength = mockRefs.Tags.length + mockRefs.Branches.length;
- expect(findRefsDropdownItems()).toHaveLength(refLength);
- });
-
- it('displays the names of refs', () => {
- // Branches
- expect(findRefsDropdownItems().at(0).text()).toBe(mockRefs.Branches[0]);
-
- // Tags (appear after branches)
- const firstTag = mockRefs.Branches.length;
- expect(findRefsDropdownItems().at(firstTag).text()).toBe(mockRefs.Tags[0]);
- });
-
- it('when user shows dropdown a second time, only one request is done', () => {
- expect(mock.history.get).toHaveLength(1);
+ it('has ref selector for branches and tags', () => {
+ expect(findRefSelector().props('enabledRefTypes')).toEqual([
+ REF_TYPE_BRANCHES,
+ REF_TYPE_TAGS,
+ ]);
});
describe('when user selects a value', () => {
- const selectedIndex = 1;
-
- beforeEach(async () => {
- findRefsDropdownItems().at(selectedIndex).vm.$emit('select', 'refs/heads/branch-1');
- await waitForPromises();
- });
+ const fullName = `refs/heads/conflict-contains-conflict-markers`;
it('component emits @input', () => {
+ findRefSelector().vm.$emit('input', fullName);
+
const inputs = wrapper.emitted('input');
expect(inputs).toHaveLength(1);
- expect(inputs[0]).toEqual([{ shortName: 'branch-1', fullName: 'refs/heads/branch-1' }]);
- });
- });
-
- describe('when user types searches for a tag', () => {
- const mockSearchTerm = 'my-search';
-
- beforeEach(async () => {
- mock
- .onGet(projectRefsEndpoint, { params: { search: mockSearchTerm } })
- .reply(HTTP_STATUS_OK, mockFilteredRefs);
-
- await findSearchBox().vm.$emit('input', mockSearchTerm);
- await waitForPromises();
- });
-
- it('requests filtered tags and branches', async () => {
- expect(mock.history.get).toHaveLength(2);
- expect(mock.history.get[1].params).toEqual({
- search: mockSearchTerm,
- });
- });
-
- it('displays dropdown with branches and tags', async () => {
- const filteredRefLength = mockFilteredRefs.Tags.length + mockFilteredRefs.Branches.length;
-
- expect(findRefsDropdownItems()).toHaveLength(filteredRefLength);
+ expect(inputs[0]).toEqual([
+ {
+ shortName: 'conflict-contains-conflict-markers',
+ fullName: 'refs/heads/conflict-contains-conflict-markers',
+ },
+ ]);
});
});
});
describe('when user has selected a value', () => {
- const selectedIndex = 1;
- const mockShortName = mockRefs.Branches[selectedIndex];
+ const mockShortName = 'conflict-contains-conflict-markers';
const mockFullName = `refs/heads/${mockShortName}`;
- beforeEach(async () => {
- mock
- .onGet(projectRefsEndpoint, {
- params: { ref: mockFullName },
- })
- .reply(HTTP_STATUS_OK, mockRefs);
-
- createComponent(
- {
- value: {
- shortName: mockShortName,
- fullName: mockFullName,
- },
- },
- mountExtended,
- );
- findDropdown().vm.$emit('shown');
- await waitForPromises();
- });
-
it('branch is checked', () => {
- expect(findRefsDropdownItems().at(selectedIndex).props('isSelected')).toBe(true);
- });
- });
-
- describe('when server returns an error', () => {
- beforeEach(async () => {
- mock
- .onGet(projectRefsEndpoint, { params: { search: '' } })
- .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
-
- findDropdown().vm.$emit('shown');
- await waitForPromises();
- });
+ createComponent({
+ value: {
+ shortName: mockShortName,
+ fullName: mockFullName,
+ },
+ });
- it('loading error event is emitted', () => {
- expect(wrapper.emitted('loadingError')).toHaveLength(1);
- expect(wrapper.emitted('loadingError')[0]).toEqual([expect.any(Error)]);
+ expect(findRefSelector().props('value')).toBe(mockShortName);
});
});
-
- describe('should display branches and tags based on its length', () => {
- it.each`
- mockData | expectedGroupLength | expectedListboxItemsLength
- ${{ ...mockBranches, Tags: [] }} | ${1} | ${mockBranches.Branches.length}
- ${{ Branches: [], ...mockTags }} | ${1} | ${mockTags.Tags.length}
- ${{ ...mockRefs }} | ${2} | ${mockBranches.Branches.length + mockTags.Tags.length}
- ${{ Branches: undefined, Tags: undefined }} | ${0} | ${0}
- `(
- 'should render branches and tags based on presence',
- async ({ mockData, expectedGroupLength, expectedListboxItemsLength }) => {
- mock.onGet(projectRefsEndpoint, { params: { search: '' } }).reply(HTTP_STATUS_OK, mockData);
- createComponent({}, mountExtended);
- findDropdown().vm.$emit('shown');
- await waitForPromises();
-
- expect(findListboxGroups()).toHaveLength(expectedGroupLength);
- expect(findRefsDropdownItems()).toHaveLength(expectedListboxItemsLength);
- },
- );
- });
});
diff --git a/spec/frontend/ci/pipeline_new/mock_data.js b/spec/frontend/ci/pipeline_new/mock_data.js
index 5b935c0c819..76a88f63298 100644
--- a/spec/frontend/ci/pipeline_new/mock_data.js
+++ b/spec/frontend/ci/pipeline_new/mock_data.js
@@ -1,16 +1,3 @@
-export const mockBranches = {
- Branches: ['main', 'branch-1', 'branch-2'],
-};
-
-export const mockTags = {
- Tags: ['1.0.0', '1.1.0', '1.2.0'],
-};
-
-export const mockRefs = {
- ...mockBranches,
- ...mockTags,
-};
-
export const mockFilteredRefs = {
Branches: ['branch-1'],
Tags: ['1.0.0', '1.1.0'],
@@ -133,3 +120,5 @@ export const mockCiConfigVariablesResponseWithoutDesc = mockCiConfigVariablesQue
mockYamlVariablesWithoutDesc,
);
export const mockNoCachedCiConfigVariablesResponse = mockCiConfigVariablesQueryResponse(null);
+
+export const mockPipelineConfigButtonText = 'Go to the pipeline editor';
diff --git a/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js b/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js
index ba948f12b33..e48f556c246 100644
--- a/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js
@@ -20,17 +20,13 @@ describe('Delete pipeline schedule modal', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('emits the deleteSchedule event', async () => {
+ it('emits the deleteSchedule event', () => {
findModal().vm.$emit('primary');
expect(wrapper.emitted()).toEqual({ deleteSchedule: [[]] });
});
- it('emits the hideModal event', async () => {
+ it('emits the hideModal event', () => {
findModal().vm.$emit('hide');
expect(wrapper.emitted()).toEqual({ hideModal: [[]] });
diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
index 611993556e3..50008cedd9c 100644
--- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
@@ -16,6 +16,7 @@ import getPipelineSchedulesQuery from '~/ci/pipeline_schedules/graphql/queries/g
import {
mockGetPipelineSchedulesGraphQLResponse,
mockPipelineScheduleNodes,
+ mockPipelineScheduleCurrentUser,
deleteMutationResponse,
playMutationResponse,
takeOwnershipMutationResponse,
@@ -79,10 +80,6 @@ describe('Pipeline schedules app', () => {
const findSchedulesCharacteristics = () =>
wrapper.findByTestId('pipeline-schedules-characteristics');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
@@ -115,6 +112,7 @@ describe('Pipeline schedules app', () => {
await waitForPromises();
expect(findTable().props('schedules')).toEqual(mockPipelineScheduleNodes);
+ expect(findTable().props('currentUser')).toEqual(mockPipelineScheduleCurrentUser);
});
it('shows query error alert', async () => {
diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
index 6fb6a8bc33b..be0052fc7cf 100644
--- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
@@ -3,6 +3,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PipelineScheduleActions from '~/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue';
import {
mockPipelineScheduleNodes,
+ mockPipelineScheduleCurrentUser,
mockPipelineScheduleAsGuestNodes,
mockTakeOwnershipNodes,
} from '../../../mock_data';
@@ -12,6 +13,7 @@ describe('Pipeline schedule actions', () => {
const defaultProps = {
schedule: mockPipelineScheduleNodes[0],
+ currentUser: mockPipelineScheduleCurrentUser,
};
const createComponent = (props = defaultProps) => {
@@ -27,18 +29,17 @@ describe('Pipeline schedule actions', () => {
const findTakeOwnershipBtn = () => wrapper.findByTestId('take-ownership-pipeline-schedule-btn');
const findPlayScheduleBtn = () => wrapper.findByTestId('play-pipeline-schedule-btn');
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('displays action buttons', () => {
+ it('displays buttons when user is the owner of schedule and has adminPipelineSchedule permissions', () => {
createComponent();
expect(findAllButtons()).toHaveLength(3);
});
- it('does not display action buttons', () => {
- createComponent({ schedule: mockPipelineScheduleAsGuestNodes[0] });
+ it('does not display action buttons when user is not owner and does not have adminPipelineSchedule permission', () => {
+ createComponent({
+ schedule: mockPipelineScheduleAsGuestNodes[0],
+ currentUser: mockPipelineScheduleCurrentUser,
+ });
expect(findAllButtons()).toHaveLength(0);
});
@@ -54,7 +55,10 @@ describe('Pipeline schedule actions', () => {
});
it('take ownership button emits showTakeOwnershipModal event and schedule id', () => {
- createComponent({ schedule: mockTakeOwnershipNodes[0] });
+ createComponent({
+ schedule: mockTakeOwnershipNodes[0],
+ currentUser: mockPipelineScheduleCurrentUser,
+ });
findTakeOwnershipBtn().vm.$emit('click');
diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
index 0821c59c8a0..ae069145292 100644
--- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
@@ -21,10 +21,6 @@ describe('Pipeline schedule last pipeline', () => {
const findCIBadgeLink = () => wrapper.findComponent(CiBadgeLink);
const findStatusText = () => wrapper.findByTestId('pipeline-schedule-status-text');
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays pipeline status', () => {
createComponent();
diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js
index 1c06c411097..3bdbb371ddc 100644
--- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js
@@ -21,10 +21,6 @@ describe('Pipeline schedule next run', () => {
const findTimeAgo = () => wrapper.findComponent(TimeAgoTooltip);
const findInactive = () => wrapper.findByTestId('pipeline-schedule-inactive');
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays time ago', () => {
createComponent();
diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js
index 6c1991cb4ac..849bef80f42 100644
--- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js
@@ -25,10 +25,6 @@ describe('Pipeline schedule owner', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays avatar', () => {
expect(findAvatar().exists()).toBe(true);
expect(findAvatar().props('src')).toBe(defaultProps.schedule.owner.avatarUrl);
diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js
index f531f04a736..5cc3829efbd 100644
--- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js
@@ -25,10 +25,6 @@ describe('Pipeline schedule target', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays icon', () => {
expect(findIcon().exists()).toBe(true);
expect(findIcon().props('name')).toBe('fork');
diff --git a/spec/frontend/ci/pipeline_schedules/components/table/pipeline_schedules_table_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/pipeline_schedules_table_spec.js
index 316b3bcf926..e488a36f3dc 100644
--- a/spec/frontend/ci/pipeline_schedules/components/table/pipeline_schedules_table_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/pipeline_schedules_table_spec.js
@@ -1,13 +1,14 @@
import { GlTableLite } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import PipelineSchedulesTable from '~/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue';
-import { mockPipelineScheduleNodes } from '../../mock_data';
+import { mockPipelineScheduleNodes, mockPipelineScheduleCurrentUser } from '../../mock_data';
describe('Pipeline schedules table', () => {
let wrapper;
const defaultProps = {
schedules: mockPipelineScheduleNodes,
+ currentUser: mockPipelineScheduleCurrentUser,
};
const createComponent = (props = defaultProps) => {
@@ -25,10 +26,6 @@ describe('Pipeline schedules table', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays table', () => {
expect(findTable().exists()).toBe(true);
});
diff --git a/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_legacy_spec.js b/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_legacy_spec.js
index 7e6d4ec4bf8..e4ff9a0545b 100644
--- a/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_legacy_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_legacy_spec.js
@@ -25,14 +25,12 @@ describe('Take ownership modal', () => {
const actionPrimary = findModal().props('actionPrimary');
expect(actionPrimary.attributes).toEqual(
- expect.objectContaining([
- {
- category: 'primary',
- variant: 'confirm',
- href: url,
- 'data-method': 'post',
- },
- ]),
+ expect.objectContaining({
+ category: 'primary',
+ variant: 'confirm',
+ href: url,
+ 'data-method': 'post',
+ }),
);
});
diff --git a/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_spec.js b/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_spec.js
index e3965d13c19..7cc254b7653 100644
--- a/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_spec.js
@@ -26,13 +26,13 @@ describe('Take ownership modal', () => {
);
});
- it('emits the takeOwnership event', async () => {
+ it('emits the takeOwnership event', () => {
findModal().vm.$emit('primary');
expect(wrapper.emitted()).toEqual({ takeOwnership: [[]] });
});
- it('emits the hideModal event', async () => {
+ it('emits the hideModal event', () => {
findModal().vm.$emit('hide');
expect(wrapper.emitted()).toEqual({ hideModal: [[]] });
diff --git a/spec/frontend/ci/pipeline_schedules/mock_data.js b/spec/frontend/ci/pipeline_schedules/mock_data.js
index 2826c054249..1485f6beea4 100644
--- a/spec/frontend/ci/pipeline_schedules/mock_data.js
+++ b/spec/frontend/ci/pipeline_schedules/mock_data.js
@@ -5,6 +5,7 @@ import mockGetPipelineSchedulesTakeOwnershipGraphQLResponse from 'test_fixtures/
const {
data: {
+ currentUser,
project: {
pipelineSchedules: { nodes },
},
@@ -28,6 +29,7 @@ const {
} = mockGetPipelineSchedulesTakeOwnershipGraphQLResponse;
export const mockPipelineScheduleNodes = nodes;
+export const mockPipelineScheduleCurrentUser = currentUser;
export const mockPipelineScheduleAsGuestNodes = guestNodes;
diff --git a/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js b/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js
index 90ca2a07266..847862be183 100644
--- a/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js
+++ b/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js
@@ -30,22 +30,17 @@ describe('code quality issue body issue body', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('severity rating', () => {
it.each`
severity | iconClass | iconName
${'INFO'} | ${'gl-text-blue-400'} | ${'severity-info'}
- ${'MINOR'} | ${'gl-text-orange-200'} | ${'severity-low'}
+ ${'MINOR'} | ${'gl-text-orange-300'} | ${'severity-low'}
${'CRITICAL'} | ${'gl-text-red-600'} | ${'severity-high'}
${'BLOCKER'} | ${'gl-text-red-800'} | ${'severity-critical'}
${'UNKNOWN'} | ${'gl-text-gray-400'} | ${'severity-unknown'}
${'INVALID'} | ${'gl-text-gray-400'} | ${'severity-unknown'}
${'info'} | ${'gl-text-blue-400'} | ${'severity-info'}
- ${'minor'} | ${'gl-text-orange-200'} | ${'severity-low'}
+ ${'minor'} | ${'gl-text-orange-300'} | ${'severity-low'}
${'major'} | ${'gl-text-orange-400'} | ${'severity-medium'}
${'critical'} | ${'gl-text-red-600'} | ${'severity-high'}
${'blocker'} | ${'gl-text-red-800'} | ${'severity-critical'}
diff --git a/spec/frontend/ci/reports/components/grouped_issues_list_spec.js b/spec/frontend/ci/reports/components/grouped_issues_list_spec.js
index 3e4adfc7794..8beec220802 100644
--- a/spec/frontend/ci/reports/components/grouped_issues_list_spec.js
+++ b/spec/frontend/ci/reports/components/grouped_issues_list_spec.js
@@ -15,10 +15,6 @@ describe('Grouped Issues List', () => {
const findHeading = (groupName) => wrapper.find(`[data-testid="${groupName}Heading"`);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a smart virtual list with the correct props', () => {
createComponent({
propsData: {
diff --git a/spec/frontend/ci/reports/components/issue_status_icon_spec.js b/spec/frontend/ci/reports/components/issue_status_icon_spec.js
index fb13d4407e2..82b655dd598 100644
--- a/spec/frontend/ci/reports/components/issue_status_icon_spec.js
+++ b/spec/frontend/ci/reports/components/issue_status_icon_spec.js
@@ -13,11 +13,6 @@ describe('IssueStatusIcon', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it.each([STATUS_SUCCESS, STATUS_NEUTRAL, STATUS_FAILED])(
'renders "%s" state correctly',
(status) => {
diff --git a/spec/frontend/ci/reports/components/report_link_spec.js b/spec/frontend/ci/reports/components/report_link_spec.js
index ba541ba0303..4a97afd77df 100644
--- a/spec/frontend/ci/reports/components/report_link_spec.js
+++ b/spec/frontend/ci/reports/components/report_link_spec.js
@@ -4,10 +4,6 @@ import ReportLink from '~/ci/reports/components/report_link.vue';
describe('app/assets/javascripts/ci/reports/components/report_link.vue', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- });
-
const defaultProps = {
issue: {},
};
diff --git a/spec/frontend/ci/reports/components/report_section_spec.js b/spec/frontend/ci/reports/components/report_section_spec.js
index f032b210184..f4012fe0215 100644
--- a/spec/frontend/ci/reports/components/report_section_spec.js
+++ b/spec/frontend/ci/reports/components/report_section_spec.js
@@ -49,10 +49,6 @@ describe('ReportSection component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('computed', () => {
describe('isCollapsible', () => {
const testMatrix = [
diff --git a/spec/frontend/ci/reports/components/summary_row_spec.js b/spec/frontend/ci/reports/components/summary_row_spec.js
index fb2ae5371d5..b1ae9e26b5b 100644
--- a/spec/frontend/ci/reports/components/summary_row_spec.js
+++ b/spec/frontend/ci/reports/components/summary_row_spec.js
@@ -31,11 +31,6 @@ describe('Summary row', () => {
const findStatusIcon = () => wrapper.findByTestId('summary-row-icon');
const findHelpPopover = () => wrapper.findComponent(HelpPopover);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders provided summary', () => {
createComponent();
expect(findSummary().text()).toContain(summary);
diff --git a/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js b/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js
index edf3d1706cc..4c56dd74f1a 100644
--- a/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js
+++ b/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js
@@ -1,40 +1,47 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
import { 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 { createAlert, VARIANT_SUCCESS } from '~/alert';
import AdminNewRunnerApp from '~/ci/runner/admin_new_runner/admin_new_runner_app.vue';
-import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
+import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
+import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue';
+import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
-import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue';
-import { DEFAULT_PLATFORM } from '~/ci/runner/constants';
+import {
+ PARAM_KEY_PLATFORM,
+ INSTANCE_TYPE,
+ DEFAULT_PLATFORM,
+ WINDOWS_PLATFORM,
+} from '~/ci/runner/constants';
+import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import { runnerCreateResult } from '../mock_data';
-const mockLegacyRegistrationToken = 'LEGACY_REGISTRATION_TOKEN';
+jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
+jest.mock('~/alert');
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ redirectTo: jest.fn(),
+}));
-Vue.use(VueApollo);
+const mockCreatedRunner = runnerCreateResult.data.runnerCreate.runner;
describe('AdminNewRunnerApp', () => {
let wrapper;
- const findLegacyInstructionsLink = () => wrapper.findByTestId('legacy-instructions-link');
- const findRunnerInstructionsModal = () => wrapper.findComponent(RunnerInstructionsModal);
const findRunnerPlatformsRadioGroup = () => wrapper.findComponent(RunnerPlatformsRadioGroup);
- const findRunnerFormFields = () => wrapper.findComponent(RunnerFormFields);
+ const findRegistrationFeedbackBanner = () => wrapper.findComponent(RegistrationFeedbackBanner);
+ const findRegistrationCompatibilityAlert = () =>
+ wrapper.findComponent(RegistrationCompatibilityAlert);
+ const findRunnerCreateForm = () => wrapper.findComponent(RunnerCreateForm);
- const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => {
- wrapper = mountFn(AdminNewRunnerApp, {
- propsData: {
- legacyRegistrationToken: mockLegacyRegistrationToken,
- ...props,
- },
- directives: {
- GlModal: createMockDirective(),
- },
+ const createComponent = () => {
+ wrapper = shallowMountExtended(AdminNewRunnerApp, {
stubs: {
GlSprintf,
},
- ...options,
});
};
@@ -42,39 +49,71 @@ describe('AdminNewRunnerApp', () => {
createComponent();
});
- describe('Shows legacy modal', () => {
- it('passes legacy registration to modal', () => {
- expect(findRunnerInstructionsModal().props('registrationToken')).toEqual(
- mockLegacyRegistrationToken,
- );
- });
+ it('shows a registration feedback banner', () => {
+ expect(findRegistrationFeedbackBanner().exists()).toBe(true);
+ });
- it('opens a modal with the legacy instructions', () => {
- const modalId = getBinding(findLegacyInstructionsLink().element, 'gl-modal').value;
+ it('shows a registration compatibility alert', () => {
+ expect(findRegistrationCompatibilityAlert().props('alertKey')).toBe(INSTANCE_TYPE);
+ });
- expect(findRunnerInstructionsModal().props('modalId')).toBe(modalId);
+ describe('Platform', () => {
+ it('shows the platforms radio group', () => {
+ expect(findRunnerPlatformsRadioGroup().props('value')).toBe(DEFAULT_PLATFORM);
});
});
- describe('New runner form fields', () => {
- describe('Platform', () => {
- it('shows the platforms radio group', () => {
- expect(findRunnerPlatformsRadioGroup().props('value')).toBe(DEFAULT_PLATFORM);
+ describe('Runner form', () => {
+ it('shows the runner create form for an instance runner', () => {
+ expect(findRunnerCreateForm().props()).toEqual({
+ runnerType: INSTANCE_TYPE,
+ groupId: null,
+ projectId: null,
});
});
- describe('Runner', () => {
- it('shows the runners fields', () => {
- expect(findRunnerFormFields().props('value')).toEqual({
- accessLevel: 'NOT_PROTECTED',
- paused: false,
- description: '',
- maintenanceNote: '',
- maximumTimeout: ' ',
- runUntagged: false,
- tagList: '',
+ describe('When a runner is saved', () => {
+ beforeEach(() => {
+ findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner);
+ });
+
+ it('pushes an alert to be shown after redirection', () => {
+ expect(saveAlertToLocalStorage).toHaveBeenCalledWith({
+ message: s__('Runners|Runner created.'),
+ variant: VARIANT_SUCCESS,
});
});
+
+ it('redirects to the registration page', () => {
+ const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${DEFAULT_PLATFORM}`;
+
+ expect(redirectTo).toHaveBeenCalledWith(url); // eslint-disable-line import/no-deprecated
+ });
+ });
+
+ describe('When another platform is selected and a runner is saved', () => {
+ beforeEach(() => {
+ findRunnerPlatformsRadioGroup().vm.$emit('input', WINDOWS_PLATFORM);
+ findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner);
+ });
+
+ it('redirects to the registration page with the platform', () => {
+ const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`;
+
+ expect(redirectTo).toHaveBeenCalledWith(url); // eslint-disable-line import/no-deprecated
+ });
+ });
+
+ describe('When runner fails to save', () => {
+ const ERROR_MSG = 'Cannot save!';
+
+ beforeEach(() => {
+ findRunnerCreateForm().vm.$emit('error', new Error(ERROR_MSG));
+ });
+
+ it('shows an error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: ERROR_MSG });
+ });
});
});
});
diff --git a/spec/frontend/ci/runner/admin_register_runner/admin_register_runner_app_spec.js b/spec/frontend/ci/runner/admin_register_runner/admin_register_runner_app_spec.js
new file mode 100644
index 00000000000..60244ba5bc2
--- /dev/null
+++ b/spec/frontend/ci/runner/admin_register_runner/admin_register_runner_app_spec.js
@@ -0,0 +1,122 @@
+import { nextTick } from 'vue';
+import { GlButton } from '@gitlab/ui';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { TEST_HOST } from 'helpers/test_constants';
+
+import { updateHistory } from '~/lib/utils/url_utility';
+import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM, WINDOWS_PLATFORM } from '~/ci/runner/constants';
+import AdminRegisterRunnerApp from '~/ci/runner/admin_register_runner/admin_register_runner_app.vue';
+import RegistrationInstructions from '~/ci/runner/components/registration/registration_instructions.vue';
+import PlatformsDrawer from '~/ci/runner/components/registration/platforms_drawer.vue';
+import { runnerForRegistration } from '../mock_data';
+
+const mockRunnerId = runnerForRegistration.data.runner.id;
+const mockRunnersPath = '/admin/runners';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ updateHistory: jest.fn(),
+}));
+
+describe('AdminRegisterRunnerApp', () => {
+ let wrapper;
+
+ const findRegistrationInstructions = () => wrapper.findComponent(RegistrationInstructions);
+ const findPlatformsDrawer = () => wrapper.findComponent(PlatformsDrawer);
+ const findBtn = () => wrapper.findComponent(GlButton);
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(AdminRegisterRunnerApp, {
+ propsData: {
+ runnerId: mockRunnerId,
+ runnersPath: mockRunnersPath,
+ },
+ });
+ };
+
+ describe('When showing runner details', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('when runner token is available', () => {
+ it('shows registration instructions', () => {
+ expect(findRegistrationInstructions().props()).toEqual({
+ platform: DEFAULT_PLATFORM,
+ runnerId: mockRunnerId,
+ });
+ });
+
+ it('configures platform drawer', () => {
+ expect(findPlatformsDrawer().props()).toEqual({
+ open: false,
+ platform: DEFAULT_PLATFORM,
+ });
+ });
+
+ it('shows runner list button', () => {
+ expect(findBtn().attributes('href')).toBe(mockRunnersPath);
+ expect(findBtn().props('variant')).toBe('confirm');
+ });
+ });
+ });
+
+ describe('When another platform has been selected', () => {
+ beforeEach(() => {
+ setWindowLocation(`?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`);
+
+ createComponent();
+ });
+
+ it('shows registration instructions for the platform', () => {
+ expect(findRegistrationInstructions().props('platform')).toBe(WINDOWS_PLATFORM);
+ });
+ });
+
+ describe('When opening install instructions', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ findRegistrationInstructions().vm.$emit('toggleDrawer');
+ await nextTick();
+ });
+
+ it('opens platform drawer', () => {
+ expect(findPlatformsDrawer().props('open')).toBe(true);
+ });
+
+ it('closes platform drawer', async () => {
+ findRegistrationInstructions().vm.$emit('toggleDrawer');
+ await nextTick();
+
+ expect(findPlatformsDrawer().props('open')).toBe(false);
+ });
+
+ it('closes platform drawer from drawer', async () => {
+ findPlatformsDrawer().vm.$emit('close');
+ await nextTick();
+
+ expect(findPlatformsDrawer().props('open')).toBe(false);
+ });
+
+ describe('when selecting a platform', () => {
+ beforeEach(async () => {
+ findPlatformsDrawer().vm.$emit('selectPlatform', WINDOWS_PLATFORM);
+ await nextTick();
+ });
+
+ it('updates the url', () => {
+ expect(updateHistory).toHaveBeenCalledTimes(1);
+ expect(updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`,
+ });
+ });
+
+ it('updates the registration instructions', () => {
+ expect(findRegistrationInstructions().props('platform')).toBe(WINDOWS_PLATFORM);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
index ed4f43c12d8..9787b1ef83f 100644
--- a/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
+++ b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
@@ -4,8 +4,8 @@ import VueApollo from 'vue-apollo';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert, VARIANT_SUCCESS } from '~/flash';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerHeader from '~/ci/runner/components/runner_header.vue';
@@ -24,7 +24,7 @@ import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_al
import { runnerData } from '../mock_data';
jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility');
@@ -72,7 +72,6 @@ describe('AdminRunnerShowApp', () => {
afterEach(() => {
mockRunnerQuery.mockReset();
- wrapper.destroy();
});
describe('When showing runner details', () => {
@@ -82,7 +81,7 @@ describe('AdminRunnerShowApp', () => {
await createComponent({ mountFn: mountExtended });
});
- it('expect GraphQL ID to be requested', async () => {
+ it('expect GraphQL ID to be requested', () => {
expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunnerGraphqlId });
});
@@ -90,7 +89,7 @@ describe('AdminRunnerShowApp', () => {
expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId}`);
});
- it('displays the runner edit and pause buttons', async () => {
+ it('displays the runner edit and pause buttons', () => {
expect(findRunnerEditButton().attributes('href')).toBe(mockRunner.editAdminUrl);
expect(findRunnerPauseButton().exists()).toBe(true);
expect(findRunnerDeleteButton().exists()).toBe(true);
@@ -100,7 +99,7 @@ describe('AdminRunnerShowApp', () => {
expect(findRunnerDetailsTabs().props('runner')).toEqual(mockRunner);
});
- it('shows basic runner details', async () => {
+ it('shows basic runner details', () => {
const expected = `Description My Runner
Last contact Never contacted
Version 1.0.0
@@ -181,7 +180,7 @@ describe('AdminRunnerShowApp', () => {
message: 'Runner deleted',
variant: VARIANT_SUCCESS,
});
- expect(redirectTo).toHaveBeenCalledWith(mockRunnersPath);
+ expect(redirectTo).toHaveBeenCalledWith(mockRunnersPath); // eslint-disable-line import/no-deprecated
});
});
diff --git a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
index 7fc240e520b..c3d33c88422 100644
--- a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
@@ -9,7 +9,7 @@ import {
mountExtended,
} from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
@@ -57,20 +57,18 @@ import {
allRunnersDataPaginated,
onlineContactTimeoutSecs,
staleTimeoutSecs,
+ mockRegistrationToken,
newRunnerPath,
emptyPageInfo,
- emptyStateSvgPath,
- emptyStateFilteredSvgPath,
} from '../mock_data';
-const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
const mockRunners = allRunnersData.data.runners.nodes;
const mockRunnersCount = runnersCountData.data.runners.count;
const mockRunnersHandler = jest.fn();
const mockRunnersCountHandler = jest.fn();
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
@@ -122,8 +120,6 @@ describe('AdminRunnersApp', () => {
localMutations,
onlineContactTimeoutSecs,
staleTimeoutSecs,
- emptyStateSvgPath,
- emptyStateFilteredSvgPath,
...provide,
},
...options,
@@ -143,7 +139,6 @@ describe('AdminRunnersApp', () => {
mockRunnersHandler.mockReset();
mockRunnersCountHandler.mockReset();
showToast.mockReset();
- wrapper.destroy();
});
it('shows the runner setup instructions', () => {
@@ -209,13 +204,13 @@ describe('AdminRunnersApp', () => {
it('runner item links to the runner admin page', async () => {
await createComponent({ mountFn: mountExtended });
- const { id, shortSha } = mockRunners[0];
+ const { id, shortSha, adminUrl } = mockRunners[0];
const numericId = getIdFromGraphQLId(id);
const runnerLink = wrapper.find('tr [data-testid="td-summary"]').findComponent(GlLink);
expect(runnerLink.text()).toBe(`#${numericId} (${shortSha})`);
- expect(runnerLink.attributes('href')).toBe(`http://localhost/admin/runners/${numericId}`);
+ expect(runnerLink.attributes('href')).toBe(adminUrl);
});
it('renders runner actions for each runner', async () => {
@@ -265,7 +260,7 @@ describe('AdminRunnersApp', () => {
});
describe('Single runner row', () => {
- const { id: graphqlId, shortSha } = mockRunners[0];
+ const { id: graphqlId, shortSha, adminUrl } = mockRunners[0];
const id = getIdFromGraphQLId(graphqlId);
beforeEach(async () => {
@@ -274,11 +269,11 @@ describe('AdminRunnersApp', () => {
await createComponent({ mountFn: mountExtended });
});
- it('Links to the runner page', async () => {
+ it('Links to the runner page', () => {
const runnerLink = wrapper.find('tr [data-testid="td-summary"]').findComponent(GlLink);
expect(runnerLink.text()).toBe(`#${id} (${shortSha})`);
- expect(runnerLink.attributes('href')).toBe(`http://localhost/admin/runners/${id}`);
+ expect(runnerLink.attributes('href')).toBe(adminUrl);
});
it('Shows job status and links to jobs', () => {
@@ -287,13 +282,10 @@ describe('AdminRunnersApp', () => {
.findComponent(RunnerJobStatusBadge);
expect(badge.props('jobStatus')).toBe(mockRunners[0].jobExecutionStatus);
-
- const badgeHref = new URL(badge.attributes('href'));
- expect(badgeHref.pathname).toBe(`/admin/runners/${id}`);
- expect(badgeHref.hash).toBe(`#${JOBS_ROUTE_PATH}`);
+ expect(badge.attributes('href')).toBe(`${adminUrl}#${JOBS_ROUTE_PATH}`);
});
- it('When runner is paused or unpaused, some data is refetched', async () => {
+ it('When runner is paused or unpaused, some data is refetched', () => {
expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES);
findRunnerActionsCell().vm.$emit('toggledPaused');
@@ -302,7 +294,7 @@ describe('AdminRunnersApp', () => {
expect(showToast).toHaveBeenCalledTimes(0);
});
- it('When runner is deleted, data is refetched and a toast message is shown', async () => {
+ it('When runner is deleted, data is refetched and a toast message is shown', () => {
findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
expect(showToast).toHaveBeenCalledTimes(1);
@@ -325,7 +317,7 @@ describe('AdminRunnersApp', () => {
{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } },
{ type: PARAM_KEY_PAUSED, value: { data: 'true', operator: '=' } },
],
- sort: 'CREATED_DESC',
+ sort: DEFAULT_SORT,
pagination: {},
});
});
@@ -392,7 +384,7 @@ describe('AdminRunnersApp', () => {
it('when runners have not loaded, shows a loading state', () => {
createComponent();
expect(findRunnerList().props('loading')).toBe(true);
- expect(findRunnerPagination().attributes('disabled')).toBe('true');
+ expect(findRunnerPagination().attributes('disabled')).toBeDefined();
});
describe('Bulk delete', () => {
@@ -411,7 +403,7 @@ describe('AdminRunnersApp', () => {
await createComponent({ mountFn: mountExtended });
});
- it('count data is refetched', async () => {
+ it('count data is refetched', () => {
expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES);
findRunnerList().vm.$emit('deleted', { message: 'Runners deleted' });
@@ -419,7 +411,7 @@ describe('AdminRunnersApp', () => {
expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES * 2);
});
- it('toast is shown', async () => {
+ it('toast is shown', () => {
expect(showToast).toHaveBeenCalledTimes(0);
findRunnerList().vm.$emit('deleted', { message: 'Runners deleted' });
@@ -452,9 +444,7 @@ describe('AdminRunnersApp', () => {
expect(findRunnerListEmptyState().props()).toEqual({
newRunnerPath,
isSearchFiltered: false,
- filteredSvgPath: emptyStateFilteredSvgPath,
registrationToken: mockRegistrationToken,
- svgPath: emptyStateSvgPath,
});
});
@@ -481,11 +471,11 @@ describe('AdminRunnersApp', () => {
await createComponent();
});
- it('error is shown to the user', async () => {
+ it('error is shown to the user', () => {
expect(createAlert).toHaveBeenCalledTimes(1);
});
- it('error is reported to sentry', async () => {
+ it('error is reported to sentry', () => {
expect(captureException).toHaveBeenCalledWith({
error: new Error('Error!'),
component: 'AdminRunnersApp',
diff --git a/spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js
index 82e262d1b73..8ac0c5a61f8 100644
--- a/spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js
@@ -31,10 +31,6 @@ describe('RunnerActionsCell', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Edit Action', () => {
it('Displays the runner edit link with the correct href', () => {
createComponent();
diff --git a/spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js
index 3097e43e583..03f1ace3897 100644
--- a/spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js
@@ -16,7 +16,7 @@ describe('RunnerOwnerCell', () => {
const createComponent = ({ runner } = {}) => {
wrapper = shallowMount(RunnerOwnerCell, {
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
propsData: {
runner,
@@ -24,10 +24,6 @@ describe('RunnerOwnerCell', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('When its an instance runner', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js
index 1ff60ff1a9d..c435dd57de2 100644
--- a/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js
@@ -1,4 +1,4 @@
-import { mount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import RunnerStatusCell from '~/ci/runner/components/cells/runner_status_cell.vue';
import RunnerStatusBadge from '~/ci/runner/components/runner_status_badge.vue';
@@ -20,7 +20,7 @@ describe('RunnerStatusCell', () => {
const findPausedBadge = () => wrapper.findComponent(RunnerPausedBadge);
const createComponent = ({ runner = {}, ...options } = {}) => {
- wrapper = mount(RunnerStatusCell, {
+ wrapper = shallowMount(RunnerStatusCell, {
propsData: {
runner: {
runnerType: INSTANCE_TYPE,
@@ -30,14 +30,14 @@ describe('RunnerStatusCell', () => {
...runner,
},
},
+ stubs: {
+ RunnerStatusBadge,
+ RunnerPausedBadge,
+ },
...options,
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays online status', () => {
createComponent();
diff --git a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
index 1711df42491..64e9c11a584 100644
--- a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
@@ -1,5 +1,6 @@
-import { __ } from '~/locale';
+import { __, sprintf } from '~/locale';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import RunnerSummaryCell from '~/ci/runner/components/cells/runner_summary_cell.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import RunnerTags from '~/ci/runner/components/runner_tags.vue';
@@ -11,11 +12,13 @@ import {
I18N_INSTANCE_TYPE,
PROJECT_TYPE,
I18N_NO_DESCRIPTION,
+ I18N_CREATED_AT_LABEL,
+ I18N_CREATED_AT_BY_LABEL,
} from '~/ci/runner/constants';
-import { allRunnersData } from '../../mock_data';
+import { allRunnersWithCreatorData } from '../../mock_data';
-const mockRunner = allRunnersData.data.runners.nodes[0];
+const mockRunner = allRunnersWithCreatorData.data.runners.nodes[0];
describe('RunnerTypeCell', () => {
let wrapper;
@@ -45,10 +48,6 @@ describe('RunnerTypeCell', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays the runner name as id and short token', () => {
expect(wrapper.text()).toContain(
`#${getIdFromGraphQLId(mockRunner.id)} (${mockRunner.shortSha})`,
@@ -83,14 +82,15 @@ describe('RunnerTypeCell', () => {
it('Displays the runner description', () => {
expect(wrapper.text()).toContain(mockRunner.description);
+ expect(wrapper.findByText(I18N_NO_DESCRIPTION).exists()).toBe(false);
});
- it('Displays the no runner description', () => {
+ it('Displays "No description" for missing runner description', () => {
createComponent({
description: null,
});
- expect(wrapper.text()).toContain(I18N_NO_DESCRIPTION);
+ expect(wrapper.findByText(I18N_NO_DESCRIPTION).classes()).toContain('gl-text-secondary');
});
it('Displays last contact', () => {
@@ -146,10 +146,42 @@ describe('RunnerTypeCell', () => {
expect(findRunnerSummaryField('pipeline').text()).toContain('1,000+');
});
- it('Displays created at', () => {
- expect(findRunnerSummaryField('calendar').findComponent(TimeAgo).props('time')).toBe(
- mockRunner.createdAt,
- );
+ describe('Displays creation info', () => {
+ const findCreatedTime = () => findRunnerSummaryField('calendar').findComponent(TimeAgo);
+
+ it('Displays created at ...', () => {
+ createComponent({
+ createdBy: null,
+ });
+
+ expect(findRunnerSummaryField('calendar').text()).toMatchInterpolatedText(
+ sprintf(I18N_CREATED_AT_LABEL, {
+ timeAgo: findCreatedTime().text(),
+ }),
+ );
+ expect(findCreatedTime().props('time')).toBe(mockRunner.createdAt);
+ });
+
+ it('Displays created at ... by ...', () => {
+ expect(findRunnerSummaryField('calendar').text()).toMatchInterpolatedText(
+ sprintf(I18N_CREATED_AT_BY_LABEL, {
+ timeAgo: findCreatedTime().text(),
+ avatar: mockRunner.createdBy.username,
+ }),
+ );
+ expect(findCreatedTime().props('time')).toBe(mockRunner.createdAt);
+ });
+
+ it('Displays creator avatar', () => {
+ const { name, avatarUrl, webUrl, username } = mockRunner.createdBy;
+
+ expect(wrapper.findComponent(UserAvatarLink).props()).toMatchObject({
+ imgAlt: expect.stringContaining(name),
+ imgSrc: avatarUrl,
+ linkHref: webUrl,
+ tooltipText: username,
+ });
+ });
});
it('Displays tag list', () => {
diff --git a/spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js b/spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js
index f536e0dcbcf..7748890cf77 100644
--- a/spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js
@@ -17,16 +17,12 @@ describe('RunnerSummaryField', () => {
...props,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
...options,
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('shows content in slot', () => {
createComponent({
slots: { default: 'content' },
diff --git a/spec/frontend/ci/runner/components/registration/__snapshots__/utils_spec.js.snap b/spec/frontend/ci/runner/components/registration/__snapshots__/utils_spec.js.snap
new file mode 100644
index 00000000000..5eb7ffaacd6
--- /dev/null
+++ b/spec/frontend/ci/runner/components/registration/__snapshots__/utils_spec.js.snap
@@ -0,0 +1,201 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`registration utils for "linux" platform commandPrompt is correct 1`] = `"$"`;
+
+exports[`registration utils for "linux" platform installScript is correct for "386" architecture 1`] = `
+"# Download the binary for your system
+sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-386
+
+# Give it permission to execute
+sudo chmod +x /usr/local/bin/gitlab-runner
+
+# Create a GitLab Runner user
+sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
+
+# Install and run as a service
+sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
+sudo gitlab-runner start"
+`;
+
+exports[`registration utils for "linux" platform installScript is correct for "amd64" architecture 1`] = `
+"# Download the binary for your system
+sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64
+
+# Give it permission to execute
+sudo chmod +x /usr/local/bin/gitlab-runner
+
+# Create a GitLab Runner user
+sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
+
+# Install and run as a service
+sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
+sudo gitlab-runner start"
+`;
+
+exports[`registration utils for "linux" platform installScript is correct for "arm" architecture 1`] = `
+"# Download the binary for your system
+sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm
+
+# Give it permission to execute
+sudo chmod +x /usr/local/bin/gitlab-runner
+
+# Create a GitLab Runner user
+sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
+
+# Install and run as a service
+sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
+sudo gitlab-runner start"
+`;
+
+exports[`registration utils for "linux" platform installScript is correct for "arm64" architecture 1`] = `
+"# Download the binary for your system
+sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm64
+
+# Give it permission to execute
+sudo chmod +x /usr/local/bin/gitlab-runner
+
+# Create a GitLab Runner user
+sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
+
+# Install and run as a service
+sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
+sudo gitlab-runner start"
+`;
+
+exports[`registration utils for "linux" platform platformArchitectures returns correct list of architectures 1`] = `
+Array [
+ "amd64",
+ "386",
+ "arm",
+ "arm64",
+]
+`;
+
+exports[`registration utils for "linux" platform registerCommand is correct 1`] = `
+Array [
+ "gitlab-runner register",
+ " --url http://test.host",
+ " --token MOCK_AUTHENTICATION_TOKEN",
+]
+`;
+
+exports[`registration utils for "linux" platform registerCommand is correct 2`] = `
+Array [
+ "gitlab-runner register",
+ " --url http://test.host",
+]
+`;
+
+exports[`registration utils for "linux" platform runCommand is correct 1`] = `"gitlab-runner run"`;
+
+exports[`registration utils for "osx" platform commandPrompt is correct 1`] = `"$"`;
+
+exports[`registration utils for "osx" platform installScript is correct for "amd64" architecture 1`] = `
+"# Download the binary for your system
+sudo curl --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-amd64
+
+# Give it permission to execute
+sudo chmod +x /usr/local/bin/gitlab-runner
+
+# The rest of the commands execute as the user who will run the runner
+# Register the runner (steps below), then run
+cd ~
+gitlab-runner install
+gitlab-runner start"
+`;
+
+exports[`registration utils for "osx" platform installScript is correct for "arm64" architecture 1`] = `
+"# Download the binary for your system
+sudo curl --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-arm64
+
+# Give it permission to execute
+sudo chmod +x /usr/local/bin/gitlab-runner
+
+# The rest of the commands execute as the user who will run the runner
+# Register the runner (steps below), then run
+cd ~
+gitlab-runner install
+gitlab-runner start"
+`;
+
+exports[`registration utils for "osx" platform platformArchitectures returns correct list of architectures 1`] = `
+Array [
+ "amd64",
+ "arm64",
+]
+`;
+
+exports[`registration utils for "osx" platform registerCommand is correct 1`] = `
+Array [
+ "gitlab-runner register",
+ " --url http://test.host",
+ " --token MOCK_AUTHENTICATION_TOKEN",
+]
+`;
+
+exports[`registration utils for "osx" platform registerCommand is correct 2`] = `
+Array [
+ "gitlab-runner register",
+ " --url http://test.host",
+]
+`;
+
+exports[`registration utils for "osx" platform runCommand is correct 1`] = `"gitlab-runner run"`;
+
+exports[`registration utils for "windows" platform commandPrompt is correct 1`] = `">"`;
+
+exports[`registration utils for "windows" platform installScript is correct for "386" architecture 1`] = `
+"# Run PowerShell: https://docs.microsoft.com/en-us/powershell/scripting/windows-powershell/starting-windows-powershell?view=powershell-7#with-administrative-privileges-run-as-administrator
+# Create a folder somewhere on your system, for example: C:\\\\GitLab-Runner
+New-Item -Path 'C:\\\\GitLab-Runner' -ItemType Directory
+
+# Change to the folder
+cd 'C:\\\\GitLab-Runner'
+
+# Download binary
+Invoke-WebRequest -Uri \\"https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-386.exe\\" -OutFile \\"gitlab-runner.exe\\"
+
+# Register the runner (steps below), then run
+.\\\\gitlab-runner.exe install
+.\\\\gitlab-runner.exe start"
+`;
+
+exports[`registration utils for "windows" platform installScript is correct for "amd64" architecture 1`] = `
+"# Run PowerShell: https://docs.microsoft.com/en-us/powershell/scripting/windows-powershell/starting-windows-powershell?view=powershell-7#with-administrative-privileges-run-as-administrator
+# Create a folder somewhere on your system, for example: C:\\\\GitLab-Runner
+New-Item -Path 'C:\\\\GitLab-Runner' -ItemType Directory
+
+# Change to the folder
+cd 'C:\\\\GitLab-Runner'
+
+# Download binary
+Invoke-WebRequest -Uri \\"https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-amd64.exe\\" -OutFile \\"gitlab-runner.exe\\"
+
+# Register the runner (steps below), then run
+.\\\\gitlab-runner.exe install
+.\\\\gitlab-runner.exe start"
+`;
+
+exports[`registration utils for "windows" platform platformArchitectures returns correct list of architectures 1`] = `
+Array [
+ "amd64",
+ "386",
+]
+`;
+
+exports[`registration utils for "windows" platform registerCommand is correct 1`] = `
+Array [
+ ".\\\\gitlab-runner.exe register",
+ " --url http://test.host",
+ " --token MOCK_AUTHENTICATION_TOKEN",
+]
+`;
+
+exports[`registration utils for "windows" platform registerCommand is correct 2`] = `
+Array [
+ ".\\\\gitlab-runner.exe register",
+ " --url http://test.host",
+]
+`;
+
+exports[`registration utils for "windows" platform runCommand is correct 1`] = `".\\\\gitlab-runner.exe run"`;
diff --git a/spec/frontend/ci/runner/components/registration/cli_command_spec.js b/spec/frontend/ci/runner/components/registration/cli_command_spec.js
new file mode 100644
index 00000000000..78c2b94c3ea
--- /dev/null
+++ b/spec/frontend/ci/runner/components/registration/cli_command_spec.js
@@ -0,0 +1,39 @@
+import CliCommand from '~/ci/runner/components/registration/cli_command.vue';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('CliCommand', () => {
+ let wrapper;
+
+ // use .textContent instead of .text() to capture whitespace that's visible in <pre>
+ const getPreTextContent = () => wrapper.find('pre').element.textContent;
+ const getClipboardText = () => wrapper.findComponent(ClipboardButton).props('text');
+
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(CliCommand, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ it('when rendering a command', () => {
+ createComponent({
+ prompt: '#',
+ command: 'echo hi',
+ });
+
+ expect(getPreTextContent()).toBe('# echo hi');
+ expect(getClipboardText()).toBe('echo hi');
+ });
+
+ it('when rendering a multi-line command', () => {
+ createComponent({
+ prompt: '#',
+ command: ['git', ' --version'],
+ });
+
+ expect(getPreTextContent()).toBe('# git --version');
+ expect(getClipboardText()).toBe('git --version');
+ });
+});
diff --git a/spec/frontend/ci/runner/components/registration/platforms_drawer_spec.js b/spec/frontend/ci/runner/components/registration/platforms_drawer_spec.js
new file mode 100644
index 00000000000..0b438455b5b
--- /dev/null
+++ b/spec/frontend/ci/runner/components/registration/platforms_drawer_spec.js
@@ -0,0 +1,108 @@
+import { nextTick } from 'vue';
+import { GlDrawer, GlLink, GlIcon, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+
+import PlatformsDrawer from '~/ci/runner/components/registration/platforms_drawer.vue';
+import CliCommand from '~/ci/runner/components/registration/cli_command.vue';
+import {
+ LINUX_PLATFORM,
+ MACOS_PLATFORM,
+ WINDOWS_PLATFORM,
+ INSTALL_HELP_URL,
+} from '~/ci/runner/constants';
+import { installScript, platformArchitectures } from '~/ci/runner/components/registration/utils';
+
+const MOCK_WRAPPER_HEIGHT = '99px';
+const LINUX_ARCHS = platformArchitectures({ platform: LINUX_PLATFORM });
+const MACOS_ARCHS = platformArchitectures({ platform: MACOS_PLATFORM });
+
+jest.mock('~/lib/utils/dom_utils', () => ({
+ getContentWrapperHeight: () => MOCK_WRAPPER_HEIGHT,
+}));
+
+describe('RegistrationInstructions', () => {
+ let wrapper;
+
+ const findDrawer = () => wrapper.findComponent(GlDrawer);
+ const findEnvironmentOptions = () =>
+ wrapper.findByLabelText(s__('Runners|Environment')).findAll('option');
+ const findArchitectureOptions = () =>
+ wrapper.findByLabelText(s__('Runners|Architecture')).findAll('option');
+ const findCliCommand = () => wrapper.findComponent(CliCommand);
+ const findLink = () => wrapper.findComponent(GlLink);
+
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
+ wrapper = mountFn(PlatformsDrawer, {
+ propsData: {
+ open: true,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ it('shows drawer', () => {
+ createComponent();
+
+ expect(findDrawer().props()).toMatchObject({
+ open: true,
+ headerHeight: MOCK_WRAPPER_HEIGHT,
+ });
+ });
+
+ it('closes drawer', () => {
+ createComponent();
+ findDrawer().vm.$emit('close');
+
+ expect(wrapper.emitted('close')).toHaveLength(1);
+ });
+
+ it('shows selection options', () => {
+ createComponent({ mountFn: mountExtended });
+
+ expect(findEnvironmentOptions().wrappers.map((w) => w.attributes('value'))).toEqual([
+ LINUX_PLATFORM,
+ MACOS_PLATFORM,
+ WINDOWS_PLATFORM,
+ ]);
+
+ expect(findArchitectureOptions().wrappers.map((w) => w.attributes('value'))).toEqual(
+ LINUX_ARCHS,
+ );
+ });
+
+ it('shows script', () => {
+ createComponent();
+
+ expect(findCliCommand().props('command')).toBe(
+ installScript({ platform: LINUX_PLATFORM, architecture: LINUX_ARCHS[0] }),
+ );
+ });
+
+ it('shows selection options for another platform', async () => {
+ createComponent({ mountFn: mountExtended });
+
+ findEnvironmentOptions().at(1).setSelected(); // macos
+ await nextTick();
+
+ expect(wrapper.emitted('selectPlatform')).toEqual([[MACOS_PLATFORM]]);
+
+ expect(findArchitectureOptions().wrappers.map((w) => w.attributes('value'))).toEqual(
+ MACOS_ARCHS,
+ );
+
+ expect(findCliCommand().props('command')).toBe(
+ installScript({ platform: MACOS_PLATFORM, architecture: MACOS_ARCHS[0] }),
+ );
+ });
+
+ it('shows external link for more information', () => {
+ createComponent();
+
+ expect(findLink().attributes('href')).toBe(INSTALL_HELP_URL);
+ expect(findLink().findComponent(GlIcon).props('name')).toBe('external-link');
+ });
+});
diff --git a/spec/frontend/ci/runner/components/registration/registration_compatibility_alert_spec.js b/spec/frontend/ci/runner/components/registration/registration_compatibility_alert_spec.js
new file mode 100644
index 00000000000..75658270104
--- /dev/null
+++ b/spec/frontend/ci/runner/components/registration/registration_compatibility_alert_spec.js
@@ -0,0 +1,53 @@
+import { GlAlert, GlLink } from '@gitlab/ui';
+import DismissibleFeedbackAlert from '~/vue_shared/components/dismissible_feedback_alert.vue';
+import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
+import { CHANGELOG_URL } from '~/ci/runner/constants';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+
+const ALERT_KEY = 'ALERT_KEY';
+
+describe('RegistrationCompatibilityAlert', () => {
+ let wrapper;
+
+ const findDismissibleFeedbackAlert = () => wrapper.findComponent(DismissibleFeedbackAlert);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findLink = () => wrapper.findComponent(GlLink);
+
+ const createComponent = ({ mountFn = shallowMountExtended, ...options } = {}) => {
+ wrapper = mountFn(RegistrationCompatibilityAlert, {
+ propsData: {
+ alertKey: ALERT_KEY,
+ },
+ ...options,
+ });
+ };
+
+ it('configures a featureName', () => {
+ createComponent();
+
+ expect(findDismissibleFeedbackAlert().props('featureName')).toBe(
+ `new_runner_compatibility_${ALERT_KEY}`,
+ );
+ });
+
+ it('alert has warning appearance', () => {
+ createComponent({
+ stubs: {
+ DismissibleFeedbackAlert,
+ },
+ });
+
+ expect(findAlert().props()).toMatchObject({
+ dismissible: true,
+ variant: 'warning',
+ title: expect.any(String),
+ });
+ });
+
+ it('shows alert content and link', () => {
+ createComponent({ mountFn: mountExtended });
+
+ expect(findAlert().text()).not.toBe('');
+ expect(findLink().attributes('href')).toBe(CHANGELOG_URL);
+ });
+});
diff --git a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
index 0daaca9c4ff..e564cf49ca0 100644
--- a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
@@ -1,10 +1,10 @@
-import { GlModal, GlDropdown, GlDropdownItem, GlDropdownForm } from '@gitlab/ui';
-import { mount, shallowMount, createWrapper } from '@vue/test-utils';
+import { GlModal, GlDropdown, GlDropdownItem, GlDropdownForm, GlIcon } from '@gitlab/ui';
+import { createWrapper } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { s__ } from '~/locale';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -12,7 +12,14 @@ import RegistrationDropdown from '~/ci/runner/components/registration/registrati
import RegistrationToken from '~/ci/runner/components/registration/registration_token.vue';
import RegistrationTokenResetDropdownItem from '~/ci/runner/components/registration/registration_token_reset_dropdown_item.vue';
-import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/ci/runner/constants';
+import {
+ INSTANCE_TYPE,
+ GROUP_TYPE,
+ PROJECT_TYPE,
+ I18N_REGISTER_INSTANCE_TYPE,
+ I18N_REGISTER_GROUP_TYPE,
+ I18N_REGISTER_PROJECT_TYPE,
+} from '~/ci/runner/constants';
import getRunnerPlatformsQuery from '~/vue_shared/components/runner_instructions/graphql/get_runner_platforms.query.graphql';
import getRunnerSetupInstructionsQuery from '~/vue_shared/components/runner_instructions/graphql/get_runner_setup.query.graphql';
@@ -21,9 +28,7 @@ import {
mockRunnerPlatforms,
mockInstructions,
} from 'jest/vue_shared/components/runner_instructions/mock_data';
-
-const mockToken = '0123456789';
-const maskToken = '**********';
+import { mockRegistrationToken } from '../../mock_data';
Vue.use(VueApollo);
@@ -31,7 +36,7 @@ describe('RegistrationDropdown', () => {
let wrapper;
const findDropdown = () => wrapper.findComponent(GlDropdown);
-
+ const findDropdownBtn = () => findDropdown().find('button');
const findRegistrationInstructionsDropdownItem = () => wrapper.findComponent(GlDropdownItem);
const findTokenDropdownItem = () => wrapper.findComponent(GlDropdownForm);
const findRegistrationToken = () => wrapper.findComponent(RegistrationToken);
@@ -53,17 +58,15 @@ describe('RegistrationDropdown', () => {
await waitForPromises();
};
- const createComponent = ({ props = {}, ...options } = {}, mountFn = shallowMount) => {
- wrapper = extendedWrapper(
- mountFn(RegistrationDropdown, {
- propsData: {
- registrationToken: mockToken,
- type: INSTANCE_TYPE,
- ...props,
- },
- ...options,
- }),
- );
+ const createComponent = ({ props = {}, ...options } = {}, mountFn = shallowMountExtended) => {
+ wrapper = mountFn(RegistrationDropdown, {
+ propsData: {
+ registrationToken: mockRegistrationToken,
+ type: INSTANCE_TYPE,
+ ...props,
+ },
+ ...options,
+ });
};
const createComponentWithModal = () => {
@@ -79,27 +82,40 @@ describe('RegistrationDropdown', () => {
// Use `attachTo` to find the modal
attachTo: document.body,
},
- mount,
+ mountExtended,
);
};
it.each`
type | text
- ${INSTANCE_TYPE} | ${s__('Runners|Register an instance runner')}
- ${GROUP_TYPE} | ${s__('Runners|Register a group runner')}
- ${PROJECT_TYPE} | ${s__('Runners|Register a project runner')}
- `('Dropdown text for type $type is "$text"', () => {
- createComponent({ props: { type: INSTANCE_TYPE } }, mount);
+ ${INSTANCE_TYPE} | ${I18N_REGISTER_INSTANCE_TYPE}
+ ${GROUP_TYPE} | ${I18N_REGISTER_GROUP_TYPE}
+ ${PROJECT_TYPE} | ${I18N_REGISTER_PROJECT_TYPE}
+ `('Dropdown text for type $type is "$text"', ({ type, text }) => {
+ createComponent({ props: { type } }, mountExtended);
- expect(wrapper.text()).toContain('Register an instance runner');
+ expect(wrapper.text()).toContain(text);
});
- it('Passes attributes to the dropdown component', () => {
+ it('Passes attributes to dropdown', () => {
createComponent({ attrs: { right: true } });
expect(findDropdown().attributes()).toMatchObject({ right: 'true' });
});
+ it('Passes default props and attributes to dropdown', () => {
+ createComponent();
+
+ expect(findDropdown().props()).toMatchObject({
+ category: 'primary',
+ variant: 'confirm',
+ });
+
+ expect(findDropdown().attributes()).toMatchObject({
+ toggleclass: '',
+ });
+ });
+
describe('Instructions dropdown item', () => {
it('Displays "Show runner" dropdown item', () => {
createComponent();
@@ -111,15 +127,11 @@ describe('RegistrationDropdown', () => {
describe('When the dropdown item is clicked', () => {
beforeEach(async () => {
- createComponentWithModal({}, mount);
+ createComponentWithModal({}, mountExtended);
await openModal();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('opens the modal with contents', () => {
const modalText = findModalContent();
@@ -146,7 +158,15 @@ describe('RegistrationDropdown', () => {
});
it('Displays masked value by default', () => {
- createComponent({}, mount);
+ const mockToken = '0123456789';
+ const maskToken = '**********';
+
+ createComponent(
+ {
+ props: { registrationToken: mockToken },
+ },
+ mountExtended,
+ );
expect(findRegistrationTokenInput().element.value).toBe(maskToken);
});
@@ -175,7 +195,7 @@ describe('RegistrationDropdown', () => {
};
it('Updates token input', async () => {
- createComponent({}, mount);
+ createComponent({}, mountExtended);
expect(findRegistrationToken().props('value')).not.toBe(newToken);
@@ -185,15 +205,72 @@ describe('RegistrationDropdown', () => {
});
it('Updates token in modal', async () => {
- createComponentWithModal({}, mount);
+ createComponentWithModal({}, mountExtended);
await openModal();
- expect(findModalContent()).toContain(mockToken);
+ expect(findModalContent()).toContain(mockRegistrationToken);
await resetToken();
expect(findModalContent()).toContain(newToken);
});
});
+
+ describe.each([
+ { createRunnerWorkflowForAdmin: true },
+ { createRunnerWorkflowForNamespace: true },
+ ])('When showing a "deprecated" warning', (glFeatures) => {
+ it('passes deprecated variant props and attributes to dropdown', () => {
+ createComponent({
+ provide: { glFeatures },
+ });
+
+ expect(findDropdown().props()).toMatchObject({
+ category: 'tertiary',
+ variant: 'default',
+ text: '',
+ });
+
+ expect(findDropdown().attributes()).toMatchObject({
+ toggleclass: 'gl-px-3!',
+ });
+ });
+
+ it.each`
+ type | text
+ ${INSTANCE_TYPE} | ${I18N_REGISTER_INSTANCE_TYPE}
+ ${GROUP_TYPE} | ${I18N_REGISTER_GROUP_TYPE}
+ ${PROJECT_TYPE} | ${I18N_REGISTER_PROJECT_TYPE}
+ `('dropdown text for type $type is "$text"', ({ type, text }) => {
+ createComponent({ props: { type } }, mountExtended);
+
+ expect(wrapper.text()).toContain(text);
+ });
+
+ it('shows warning text', () => {
+ createComponent(
+ {
+ provide: { glFeatures },
+ },
+ mountExtended,
+ );
+
+ const text = wrapper.findByText(s__('Runners|Support for registration tokens is deprecated'));
+
+ expect(text.exists()).toBe(true);
+ });
+
+ it('button shows ellipsis icon', () => {
+ createComponent(
+ {
+ provide: { glFeatures },
+ },
+ mountExtended,
+ );
+
+ expect(findDropdownBtn().findComponent(GlIcon).props('name')).toBe('ellipsis_v');
+ expect(findDropdownBtn().findAllComponents(GlIcon)).toHaveLength(1);
+ });
+ });
});
diff --git a/spec/frontend/ci/runner/components/registration/registration_feedback_banner_spec.js b/spec/frontend/ci/runner/components/registration/registration_feedback_banner_spec.js
new file mode 100644
index 00000000000..fa6b7ad7c63
--- /dev/null
+++ b/spec/frontend/ci/runner/components/registration/registration_feedback_banner_spec.js
@@ -0,0 +1,52 @@
+import { GlBanner } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue';
+import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
+import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
+
+describe('Runner registration feeback banner', () => {
+ let wrapper;
+ let userCalloutDismissSpy;
+
+ const findUserCalloutDismisser = () => wrapper.findComponent(UserCalloutDismisser);
+ const findBanner = () => wrapper.findComponent(GlBanner);
+
+ const createComponent = ({ shouldShowCallout = true } = {}) => {
+ userCalloutDismissSpy = jest.fn();
+
+ wrapper = shallowMount(RegistrationFeedbackBanner, {
+ stubs: {
+ UserCalloutDismisser: makeMockUserCalloutDismisser({
+ dismiss: userCalloutDismissSpy,
+ shouldShowCallout,
+ }),
+ },
+ });
+ };
+
+ it('banner is shown', () => {
+ createComponent();
+
+ expect(findBanner().exists()).toBe(true);
+ });
+
+ it('dismisses the callout when closed', () => {
+ createComponent();
+
+ findBanner().vm.$emit('close');
+
+ expect(userCalloutDismissSpy).toHaveBeenCalled();
+ });
+
+ it('sets feature name to create_runner_workflow_banner', () => {
+ createComponent();
+
+ expect(findUserCalloutDismisser().props('featureName')).toBe('create_runner_workflow_banner');
+ });
+
+ it('is not displayed once it has been dismissed', () => {
+ createComponent({ shouldShowCallout: false });
+
+ expect(findBanner().exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js b/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js
new file mode 100644
index 00000000000..8c196d7b5e3
--- /dev/null
+++ b/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js
@@ -0,0 +1,326 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { extendedWrapper, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { TEST_HOST } from 'helpers/test_constants';
+
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import RegistrationInstructions from '~/ci/runner/components/registration/registration_instructions.vue';
+import runnerForRegistrationQuery from '~/ci/runner/graphql/register/runner_for_registration.query.graphql';
+import CliCommand from '~/ci/runner/components/registration/cli_command.vue';
+import {
+ DEFAULT_PLATFORM,
+ EXECUTORS_HELP_URL,
+ SERVICE_COMMANDS_HELP_URL,
+ STATUS_NEVER_CONTACTED,
+ STATUS_ONLINE,
+ RUNNER_REGISTRATION_POLLING_INTERVAL_MS,
+ I18N_REGISTRATION_SUCCESS,
+} from '~/ci/runner/constants';
+import { runnerForRegistration, mockAuthenticationToken } from '../../mock_data';
+
+Vue.use(VueApollo);
+
+const mockRunner = {
+ ...runnerForRegistration.data.runner,
+ ephemeralAuthenticationToken: mockAuthenticationToken,
+};
+const mockRunnerWithoutToken = {
+ ...runnerForRegistration.data.runner,
+ ephemeralAuthenticationToken: null,
+};
+
+const mockRunnerId = `${getIdFromGraphQLId(mockRunner.id)}`;
+
+describe('RegistrationInstructions', () => {
+ let wrapper;
+ let mockRunnerQuery;
+
+ const findHeading = () => wrapper.find('h1');
+ const findStepAt = (i) => extendedWrapper(wrapper.findAll('section').at(i));
+ const findByText = (text, container = wrapper) => container.findByText(text);
+
+ const waitForPolling = async () => {
+ jest.advanceTimersByTime(RUNNER_REGISTRATION_POLLING_INTERVAL_MS);
+ await waitForPromises();
+ };
+
+ const mockBeforeunload = () => {
+ const event = new Event('beforeunload');
+ const preventDefault = jest.spyOn(event, 'preventDefault');
+ const returnValueSetter = jest.spyOn(event, 'returnValue', 'set');
+
+ return {
+ event,
+ preventDefault,
+ returnValueSetter,
+ };
+ };
+
+ const mockResolvedRunner = (runner = mockRunner) => {
+ mockRunnerQuery.mockResolvedValue({
+ data: {
+ runner,
+ },
+ });
+ };
+
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(RegistrationInstructions, {
+ apolloProvider: createMockApollo([[runnerForRegistrationQuery, mockRunnerQuery]]),
+ propsData: {
+ runnerId: mockRunnerId,
+ platform: DEFAULT_PLATFORM,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mockRunnerQuery = jest.fn();
+ mockResolvedRunner();
+ });
+
+ beforeEach(() => {
+ window.gon.gitlab_url = TEST_HOST;
+ });
+
+ it('loads runner with id', () => {
+ createComponent();
+
+ expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunner.id });
+ });
+
+ describe('heading', () => {
+ it('when runner is loaded, shows heading', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(findHeading().text()).toContain(mockRunner.description);
+ });
+
+ it('when runner is loaded, shows heading safely', async () => {
+ mockResolvedRunner({
+ ...mockRunner,
+ description: '<script>hacked();</script>',
+ });
+
+ createComponent();
+ await waitForPromises();
+
+ expect(findHeading().text()).toBe('Register "<script>hacked();</script>" runner');
+ expect(findHeading().element.innerHTML).toBe(
+ 'Register "&lt;script&gt;hacked();&lt;/script&gt;" runner',
+ );
+ });
+
+ it('when runner is loading, shows default heading', () => {
+ createComponent();
+
+ expect(findHeading().text()).toBe(s__('Runners|Register runner'));
+ });
+ });
+
+ it('renders legacy instructions', () => {
+ createComponent();
+
+ findByText('How do I install GitLab Runner?').vm.$emit('click');
+
+ expect(wrapper.emitted('toggleDrawer')).toHaveLength(1);
+ });
+
+ describe('step 1', () => {
+ it('renders step 1', async () => {
+ createComponent();
+ await waitForPromises();
+
+ const step1 = findStepAt(0);
+
+ expect(step1.findComponent(CliCommand).props()).toEqual({
+ command: [
+ 'gitlab-runner register',
+ ` --url ${TEST_HOST}`,
+ ` --token ${mockAuthenticationToken}`,
+ ],
+ prompt: '$',
+ });
+ expect(step1.findByTestId('runner-token').text()).toBe(mockAuthenticationToken);
+ expect(step1.findComponent(ClipboardButton).props('text')).toBe(mockAuthenticationToken);
+ });
+
+ it('renders step 1 in loading state', () => {
+ createComponent();
+
+ const step1 = findStepAt(0);
+
+ expect(step1.findComponent(GlSkeletonLoader).exists()).toBe(true);
+ expect(step1.find('code').exists()).toBe(false);
+ expect(step1.findComponent(ClipboardButton).exists()).toBe(false);
+ });
+
+ it('render step 1 after token is not visible', async () => {
+ mockResolvedRunner(mockRunnerWithoutToken);
+
+ createComponent();
+ await waitForPromises();
+
+ const step1 = findStepAt(0);
+
+ expect(step1.findComponent(CliCommand).props('command')).toEqual([
+ 'gitlab-runner register',
+ ` --url ${TEST_HOST}`,
+ ]);
+ expect(step1.findByTestId('runner-token').exists()).toBe(false);
+ expect(step1.findComponent(ClipboardButton).exists()).toBe(false);
+ });
+
+ describe('polling for changes', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('fetches data', () => {
+ expect(mockRunnerQuery).toHaveBeenCalledTimes(1);
+ });
+
+ it('polls', async () => {
+ await waitForPolling();
+ expect(mockRunnerQuery).toHaveBeenCalledTimes(2);
+
+ await waitForPolling();
+ expect(mockRunnerQuery).toHaveBeenCalledTimes(3);
+ });
+
+ it('when runner is online, stops polling', async () => {
+ mockResolvedRunner({ ...mockRunner, status: STATUS_ONLINE });
+ await waitForPolling();
+
+ expect(mockRunnerQuery).toHaveBeenCalledTimes(2);
+ await waitForPolling();
+
+ expect(mockRunnerQuery).toHaveBeenCalledTimes(2);
+ });
+
+ it('when token is no longer visible in the API, it is still visible in the UI', async () => {
+ mockResolvedRunner(mockRunnerWithoutToken);
+ await waitForPolling();
+
+ const step1 = findStepAt(0);
+ expect(step1.findComponent(CliCommand).props('command')).toEqual([
+ 'gitlab-runner register',
+ ` --url ${TEST_HOST}`,
+ ` --token ${mockAuthenticationToken}`,
+ ]);
+ expect(step1.findByTestId('runner-token').text()).toBe(mockAuthenticationToken);
+ expect(step1.findComponent(ClipboardButton).props('text')).toBe(mockAuthenticationToken);
+ });
+
+ it('when runner is not available (e.g. deleted), the UI does not update', async () => {
+ mockResolvedRunner(null);
+ await waitForPolling();
+
+ const step1 = findStepAt(0);
+ expect(step1.findComponent(CliCommand).props('command')).toEqual([
+ 'gitlab-runner register',
+ ` --url ${TEST_HOST}`,
+ ` --token ${mockAuthenticationToken}`,
+ ]);
+ expect(step1.findByTestId('runner-token').text()).toBe(mockAuthenticationToken);
+ expect(step1.findComponent(ClipboardButton).props('text')).toBe(mockAuthenticationToken);
+ });
+ });
+ });
+
+ it('renders step 2', () => {
+ createComponent();
+ const step2 = findStepAt(1);
+
+ expect(findByText('Not sure which one to select?', step2).attributes('href')).toBe(
+ EXECUTORS_HELP_URL,
+ );
+ });
+
+ it('renders step 3', () => {
+ createComponent();
+ const step3 = findStepAt(2);
+
+ expect(step3.findComponent(CliCommand).props()).toEqual({
+ command: 'gitlab-runner run',
+ prompt: '$',
+ });
+
+ expect(findByText('system or user service', step3).attributes('href')).toBe(
+ SERVICE_COMMANDS_HELP_URL,
+ );
+ });
+
+ describe('success state', () => {
+ describe('when the runner has not been registered', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await waitForPolling();
+
+ mockResolvedRunner({ ...mockRunner, status: STATUS_NEVER_CONTACTED });
+
+ await waitForPolling();
+ });
+
+ it('does not show success message', () => {
+ expect(wrapper.text()).not.toContain(I18N_REGISTRATION_SUCCESS);
+ });
+
+ describe('when the page is closing', () => {
+ it('warns the user against closing', () => {
+ const { event, preventDefault, returnValueSetter } = mockBeforeunload();
+
+ expect(preventDefault).not.toHaveBeenCalled();
+ expect(returnValueSetter).not.toHaveBeenCalled();
+
+ window.dispatchEvent(event);
+
+ expect(preventDefault).toHaveBeenCalledWith();
+ expect(returnValueSetter).toHaveBeenCalledWith(expect.any(String));
+ });
+ });
+ });
+
+ describe('when the runner has been registered', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPolling();
+
+ mockResolvedRunner({ ...mockRunner, status: STATUS_ONLINE });
+ await waitForPolling();
+ });
+
+ it('shows success message', () => {
+ expect(wrapper.text()).toContain('🎉');
+ expect(wrapper.text()).toContain(I18N_REGISTRATION_SUCCESS);
+ });
+
+ describe('when the page is closing', () => {
+ it('does not warn the user against closing', () => {
+ const { event, preventDefault, returnValueSetter } = mockBeforeunload();
+
+ expect(preventDefault).not.toHaveBeenCalled();
+ expect(returnValueSetter).not.toHaveBeenCalled();
+
+ window.dispatchEvent(event);
+
+ expect(preventDefault).not.toHaveBeenCalled();
+ expect(returnValueSetter).not.toHaveBeenCalled();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js b/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js
index 783a4d9252a..bfdde922e17 100644
--- a/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js
@@ -5,20 +5,20 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import RegistrationTokenResetDropdownItem from '~/ci/runner/components/registration/registration_token_reset_dropdown_item.vue';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/ci/runner/constants';
import runnersRegistrationTokenResetMutation from '~/ci/runner/graphql/list/runners_registration_token_reset.mutation.graphql';
import { captureException } from '~/ci/runner/sentry_utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
Vue.use(VueApollo);
Vue.use(GlToast);
-const mockNewToken = 'NEW_TOKEN';
+const mockNewRegistrationToken = 'MOCK_NEW_REGISTRATION_TOKEN';
const modalID = 'token-reset-modal';
describe('RegistrationTokenResetDropdownItem', () => {
@@ -43,7 +43,7 @@ describe('RegistrationTokenResetDropdownItem', () => {
[runnersRegistrationTokenResetMutation, runnersRegistrationTokenResetMutationHandler],
]),
directives: {
- GlModal: createMockDirective(),
+ GlModal: createMockDirective('gl-modal'),
},
});
@@ -54,7 +54,7 @@ describe('RegistrationTokenResetDropdownItem', () => {
runnersRegistrationTokenResetMutationHandler = jest.fn().mockResolvedValue({
data: {
runnersRegistrationTokenReset: {
- token: mockNewToken,
+ token: mockNewRegistrationToken,
errors: [],
},
},
@@ -63,10 +63,6 @@ describe('RegistrationTokenResetDropdownItem', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays reset button', () => {
expect(findDropdownItem().exists()).toBe(true);
});
@@ -113,7 +109,7 @@ describe('RegistrationTokenResetDropdownItem', () => {
it('emits result', () => {
expect(wrapper.emitted('tokenReset')).toHaveLength(1);
- expect(wrapper.emitted('tokenReset')[0]).toEqual([mockNewToken]);
+ expect(wrapper.emitted('tokenReset')[0]).toEqual([mockNewRegistrationToken]);
});
it('does not show a loading state', () => {
diff --git a/spec/frontend/ci/runner/components/registration/registration_token_spec.js b/spec/frontend/ci/runner/components/registration/registration_token_spec.js
index d2a51c0d910..869c032c0b5 100644
--- a/spec/frontend/ci/runner/components/registration/registration_token_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_token_spec.js
@@ -3,9 +3,7 @@ import Vue from 'vue';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RegistrationToken from '~/ci/runner/components/registration/registration_token.vue';
import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
-
-const mockToken = '01234567890';
-const mockMasked = '***********';
+import { mockRegistrationToken } from '../../mock_data';
describe('RegistrationToken', () => {
let wrapper;
@@ -15,26 +13,23 @@ describe('RegistrationToken', () => {
const findInputCopyToggleVisibility = () => wrapper.findComponent(InputCopyToggleVisibility);
- const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => {
wrapper = mountFn(RegistrationToken, {
propsData: {
- value: mockToken,
+ value: mockRegistrationToken,
inputId: 'token-value',
...props,
},
+ ...options,
});
showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null;
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays value and copy button', () => {
createComponent();
- expect(findInputCopyToggleVisibility().props('value')).toBe(mockToken);
+ expect(findInputCopyToggleVisibility().props('value')).toBe(mockRegistrationToken);
expect(findInputCopyToggleVisibility().props('copyButtonTitle')).toBe(
'Copy registration token',
);
@@ -42,9 +37,17 @@ describe('RegistrationToken', () => {
// Component integration test to ensure secure masking
it('Displays masked value by default', () => {
- createComponent({ mountFn: mountExtended });
+ const mockToken = '0123456789';
+ const maskToken = '**********';
+
+ createComponent({
+ props: {
+ value: mockToken,
+ },
+ mountFn: mountExtended,
+ });
- expect(wrapper.find('input').element.value).toBe(mockMasked);
+ expect(wrapper.find('input').element.value).toBe(maskToken);
});
describe('When the copy to clipboard button is clicked', () => {
@@ -59,4 +62,23 @@ describe('RegistrationToken', () => {
expect(showToast).toHaveBeenCalledWith('Registration token copied!');
});
});
+
+ describe('When slots are used', () => {
+ const slotName = 'label-description';
+ const slotContent = 'Label Description';
+
+ beforeEach(() => {
+ createComponent({
+ slots: {
+ [slotName]: slotContent,
+ },
+ });
+ });
+
+ it('passes slots to the input component', () => {
+ const slot = findInputCopyToggleVisibility().vm.$scopedSlots[slotName];
+
+ expect(slot()[0].text).toBe(slotContent);
+ });
+ });
});
diff --git a/spec/frontend/ci/runner/components/registration/utils_spec.js b/spec/frontend/ci/runner/components/registration/utils_spec.js
new file mode 100644
index 00000000000..997cc5769ee
--- /dev/null
+++ b/spec/frontend/ci/runner/components/registration/utils_spec.js
@@ -0,0 +1,94 @@
+import { TEST_HOST } from 'helpers/test_constants';
+import {
+ DEFAULT_PLATFORM,
+ LINUX_PLATFORM,
+ MACOS_PLATFORM,
+ WINDOWS_PLATFORM,
+} from '~/ci/runner/constants';
+
+import {
+ commandPrompt,
+ registerCommand,
+ runCommand,
+ installScript,
+ platformArchitectures,
+} from '~/ci/runner/components/registration/utils';
+
+import { mockAuthenticationToken } from '../../mock_data';
+
+describe('registration utils', () => {
+ beforeEach(() => {
+ window.gon.gitlab_url = TEST_HOST;
+ });
+
+ describe.each([LINUX_PLATFORM, MACOS_PLATFORM, WINDOWS_PLATFORM])(
+ 'for "%s" platform',
+ (platform) => {
+ it('commandPrompt is correct', () => {
+ expect(commandPrompt({ platform })).toMatchSnapshot();
+ });
+
+ it('registerCommand is correct', () => {
+ expect(
+ registerCommand({
+ platform,
+ token: mockAuthenticationToken,
+ }),
+ ).toMatchSnapshot();
+
+ expect(registerCommand({ platform })).toMatchSnapshot();
+ });
+
+ it('runCommand is correct', () => {
+ expect(runCommand({ platform })).toMatchSnapshot();
+ });
+ },
+ );
+
+ describe('for missing platform', () => {
+ it('commandPrompt uses the default', () => {
+ const expected = commandPrompt({ platform: DEFAULT_PLATFORM });
+
+ expect(commandPrompt({ platform: null })).toEqual(expected);
+ expect(commandPrompt({ platform: undefined })).toEqual(expected);
+ });
+
+ it('registerCommand uses the default', () => {
+ const expected = registerCommand({
+ platform: DEFAULT_PLATFORM,
+ token: mockAuthenticationToken,
+ });
+
+ expect(registerCommand({ platform: null, token: mockAuthenticationToken })).toEqual(expected);
+ expect(registerCommand({ platform: undefined, token: mockAuthenticationToken })).toEqual(
+ expected,
+ );
+ });
+
+ it('runCommand uses the default', () => {
+ const expected = runCommand({ platform: DEFAULT_PLATFORM });
+
+ expect(runCommand({ platform: null })).toEqual(expected);
+ expect(runCommand({ platform: undefined })).toEqual(expected);
+ });
+ });
+
+ describe.each([LINUX_PLATFORM, MACOS_PLATFORM, WINDOWS_PLATFORM])(
+ 'for "%s" platform',
+ (platform) => {
+ describe('platformArchitectures', () => {
+ it('returns correct list of architectures', () => {
+ expect(platformArchitectures({ platform })).toMatchSnapshot();
+ });
+ });
+
+ describe('installScript', () => {
+ const architectures = platformArchitectures({ platform });
+
+ it.each(architectures)('is correct for "%s" architecture', (architecture) => {
+ expect(installScript({ platform, architecture })).toMatchSnapshot();
+ });
+ });
+ },
+ );
+});
diff --git a/spec/frontend/ci/runner/components/runner_assigned_item_spec.js b/spec/frontend/ci/runner/components/runner_assigned_item_spec.js
index 5df2e04c340..a1fd9e4c1aa 100644
--- a/spec/frontend/ci/runner/components/runner_assigned_item_spec.js
+++ b/spec/frontend/ci/runner/components/runner_assigned_item_spec.js
@@ -33,10 +33,6 @@ describe('RunnerAssignedItem', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Shows an avatar', () => {
const avatar = findAvatar();
diff --git a/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js b/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js
index 0dc5a90fb83..7bd4b701002 100644
--- a/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js
+++ b/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js
@@ -2,12 +2,13 @@ import Vue from 'vue';
import { makeVar } from '@apollo/client/core';
import { GlModal, GlSprintf } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { s__ } from '~/locale';
import RunnerBulkDelete from '~/ci/runner/components/runner_bulk_delete.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { stubComponent } from 'helpers/stub_component';
import BulkRunnerDeleteMutation from '~/ci/runner/graphql/list/bulk_runner_delete.mutation.graphql';
import { createLocalState } from '~/ci/runner/graphql/list/local_state';
import waitForPromises from 'helpers/wait_for_promises';
@@ -15,7 +16,7 @@ import { allRunnersData } from '../mock_data';
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('RunnerBulkDelete', () => {
let wrapper;
@@ -34,7 +35,7 @@ describe('RunnerBulkDelete', () => {
const bulkRunnerDeleteHandler = jest.fn();
- const createComponent = () => {
+ const createComponent = ({ stubs } = {}) => {
const { cacheConfig, localMutations } = mockState;
const apolloProvider = createMockApollo(
[[BulkRunnerDeleteMutation, bulkRunnerDeleteHandler]],
@@ -51,11 +52,12 @@ describe('RunnerBulkDelete', () => {
runners: mockRunners,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
stubs: {
GlSprintf,
GlModal,
+ ...stubs,
},
});
@@ -135,11 +137,15 @@ describe('RunnerBulkDelete', () => {
beforeEach(() => {
mockCheckedRunnerIds([mockId1, mockId2]);
+ mockHideModal = jest.fn();
- createComponent();
+ createComponent({
+ stubs: {
+ GlModal: stubComponent(GlModal, { methods: { hide: mockHideModal } }),
+ },
+ });
jest.spyOn(mockState.localMutations, 'clearChecked').mockImplementation(() => {});
- mockHideModal = jest.spyOn(findModal().vm, 'hide').mockImplementation(() => {});
});
describe('when deletion is confirmed', () => {
diff --git a/spec/frontend/ci/runner/components/runner_create_form_spec.js b/spec/frontend/ci/runner/components/runner_create_form_spec.js
new file mode 100644
index 00000000000..329dd2f73ee
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_create_form_spec.js
@@ -0,0 +1,189 @@
+import Vue from 'vue';
+import { GlForm } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
+import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue';
+import {
+ DEFAULT_ACCESS_LEVEL,
+ INSTANCE_TYPE,
+ GROUP_TYPE,
+ PROJECT_TYPE,
+} from '~/ci/runner/constants';
+import runnerCreateMutation from '~/ci/runner/graphql/new/runner_create.mutation.graphql';
+import { captureException } from '~/ci/runner/sentry_utils';
+import { runnerCreateResult } from '../mock_data';
+
+jest.mock('~/ci/runner/sentry_utils');
+
+const mockCreatedRunner = runnerCreateResult.data.runnerCreate.runner;
+
+const defaultRunnerModel = {
+ description: '',
+ accessLevel: DEFAULT_ACCESS_LEVEL,
+ paused: false,
+ maintenanceNote: '',
+ maximumTimeout: '',
+ runUntagged: false,
+ tagList: '',
+};
+
+Vue.use(VueApollo);
+
+describe('RunnerCreateForm', () => {
+ let wrapper;
+ let runnerCreateHandler;
+
+ const findForm = () => wrapper.findComponent(GlForm);
+ const findRunnerFormFields = () => wrapper.findComponent(RunnerFormFields);
+ const findSubmitBtn = () => wrapper.find('[type="submit"]');
+
+ const createComponent = ({ props } = {}) => {
+ wrapper = shallowMountExtended(RunnerCreateForm, {
+ propsData: {
+ runnerType: INSTANCE_TYPE,
+ ...props,
+ },
+ apolloProvider: createMockApollo([[runnerCreateMutation, runnerCreateHandler]]),
+ });
+ };
+
+ beforeEach(() => {
+ runnerCreateHandler = jest.fn().mockResolvedValue(runnerCreateResult);
+ });
+
+ it('shows default runner values', () => {
+ createComponent();
+
+ expect(findRunnerFormFields().props('value')).toEqual(defaultRunnerModel);
+ });
+
+ it('shows a submit button', () => {
+ createComponent();
+
+ expect(findSubmitBtn().exists()).toBe(true);
+ });
+
+ describe.each`
+ typeName | props | scopeData
+ ${'an instance runner'} | ${{ runnerType: INSTANCE_TYPE }} | ${{ runnerType: INSTANCE_TYPE }}
+ ${'a group runner'} | ${{ runnerType: GROUP_TYPE, groupId: 'gid://gitlab/Group/72' }} | ${{ runnerType: GROUP_TYPE, groupId: 'gid://gitlab/Group/72' }}
+ ${'a project runner'} | ${{ runnerType: PROJECT_TYPE, projectId: 'gid://gitlab/Project/42' }} | ${{ runnerType: PROJECT_TYPE, projectId: 'gid://gitlab/Project/42' }}
+ `('when user submits $typeName', ({ props, scopeData }) => {
+ let preventDefault;
+
+ beforeEach(() => {
+ createComponent({ props });
+
+ preventDefault = jest.fn();
+
+ findRunnerFormFields().vm.$emit('input', {
+ ...defaultRunnerModel,
+ description: 'My runner',
+ maximumTimeout: 0,
+ tagList: 'tag1, tag2',
+ });
+ });
+
+ describe('immediately after submit', () => {
+ beforeEach(() => {
+ findForm().vm.$emit('submit', { preventDefault });
+ });
+
+ it('prevents default form submission', () => {
+ expect(preventDefault).toHaveBeenCalledTimes(1);
+ });
+
+ it('shows a saving state', () => {
+ expect(findSubmitBtn().props('loading')).toBe(true);
+ });
+
+ it('saves runner', () => {
+ expect(runnerCreateHandler).toHaveBeenCalledWith({
+ input: {
+ ...defaultRunnerModel,
+ ...scopeData,
+ description: 'My runner',
+ maximumTimeout: 0,
+ tagList: ['tag1', 'tag2'],
+ },
+ });
+ });
+ });
+
+ describe('when saved successfully', () => {
+ beforeEach(async () => {
+ findForm().vm.$emit('submit', { preventDefault });
+ await waitForPromises();
+ });
+
+ it('emits "saved" result', () => {
+ expect(wrapper.emitted('saved')[0]).toEqual([mockCreatedRunner]);
+ });
+
+ it('does not show a saving state', () => {
+ expect(findSubmitBtn().props('loading')).toBe(false);
+ });
+ });
+
+ describe('when a server error occurs', () => {
+ const error = new Error('Error!');
+
+ beforeEach(async () => {
+ runnerCreateHandler.mockRejectedValue(error);
+
+ findForm().vm.$emit('submit', { preventDefault });
+ await waitForPromises();
+ });
+
+ it('emits "error" result', () => {
+ expect(wrapper.emitted('error')[0]).toEqual([error]);
+ });
+
+ it('does not show a saving state', () => {
+ expect(findSubmitBtn().props('loading')).toBe(false);
+ });
+
+ it('reports error', () => {
+ expect(captureException).toHaveBeenCalledTimes(1);
+ expect(captureException).toHaveBeenCalledWith({
+ component: 'RunnerCreateForm',
+ error,
+ });
+ });
+ });
+
+ describe('when a validation error occurs', () => {
+ const errorMsg1 = 'Issue1!';
+ const errorMsg2 = 'Issue2!';
+
+ beforeEach(async () => {
+ runnerCreateHandler.mockResolvedValue({
+ data: {
+ runnerCreate: {
+ errors: [errorMsg1, errorMsg2],
+ runner: null,
+ },
+ },
+ });
+
+ findForm().vm.$emit('submit', { preventDefault });
+ await waitForPromises();
+ });
+
+ it('emits "error" results', () => {
+ expect(wrapper.emitted('error')[0]).toEqual([new Error(`${errorMsg1} ${errorMsg2}`)]);
+ });
+
+ it('does not show a saving state', () => {
+ expect(findSubmitBtn().props('loading')).toBe(false);
+ });
+
+ it('does not report error', () => {
+ expect(captureException).not.toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_delete_button_spec.js b/spec/frontend/ci/runner/components/runner_delete_button_spec.js
index 02960ad427e..3123f2894fb 100644
--- a/spec/frontend/ci/runner/components/runner_delete_button_spec.js
+++ b/spec/frontend/ci/runner/components/runner_delete_button_spec.js
@@ -8,7 +8,7 @@ import runnerDeleteMutation from '~/ci/runner/graphql/shared/runner_delete.mutat
import waitForPromises from 'helpers/wait_for_promises';
import { captureException } from '~/ci/runner/sentry_utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { I18N_DELETE_RUNNER } from '~/ci/runner/constants';
import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
@@ -21,7 +21,7 @@ const mockRunnerName = `#${mockRunnerId} (${mockRunner.shortSha})`;
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
describe('RunnerDeleteButton', () => {
@@ -53,8 +53,8 @@ describe('RunnerDeleteButton', () => {
},
apolloProvider,
directives: {
- GlTooltip: createMockDirective(),
- GlModal: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
+ GlModal: createMockDirective('gl-modal'),
},
});
};
@@ -83,10 +83,6 @@ describe('RunnerDeleteButton', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays a delete button without an icon', () => {
expect(findBtn().props()).toMatchObject({
loading: false,
@@ -128,15 +124,15 @@ describe('RunnerDeleteButton', () => {
});
describe('Immediately after the delete button is clicked', () => {
- beforeEach(async () => {
+ beforeEach(() => {
findModal().vm.$emit('primary');
});
- it('The button has a loading state', async () => {
+ it('The button has a loading state', () => {
expect(findBtn().props('loading')).toBe(true);
});
- it('The stale tooltip is removed', async () => {
+ it('The stale tooltip is removed', () => {
expect(getTooltip()).toBe('');
});
});
@@ -259,15 +255,15 @@ describe('RunnerDeleteButton', () => {
});
describe('Immediately after the button is clicked', () => {
- beforeEach(async () => {
+ beforeEach(() => {
findModal().vm.$emit('primary');
});
- it('The button has a loading state', async () => {
+ it('The button has a loading state', () => {
expect(findBtn().props('loading')).toBe(true);
});
- it('The stale tooltip is removed', async () => {
+ it('The stale tooltip is removed', () => {
expect(getTooltip()).toBe('');
});
});
diff --git a/spec/frontend/ci/runner/components/runner_details_spec.js b/spec/frontend/ci/runner/components/runner_details_spec.js
index 65a81973869..c2d9e86aa91 100644
--- a/spec/frontend/ci/runner/components/runner_details_spec.js
+++ b/spec/frontend/ci/runner/components/runner_details_spec.js
@@ -37,10 +37,6 @@ describe('RunnerDetails', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Details tab', () => {
describe.each`
field | runner | expectedValue
diff --git a/spec/frontend/ci/runner/components/runner_edit_button_spec.js b/spec/frontend/ci/runner/components/runner_edit_button_spec.js
index 907cdc90100..5cc1ee049f4 100644
--- a/spec/frontend/ci/runner/components/runner_edit_button_spec.js
+++ b/spec/frontend/ci/runner/components/runner_edit_button_spec.js
@@ -11,7 +11,7 @@ describe('RunnerEditButton', () => {
wrapper = mountFn(RunnerEditButton, {
attrs,
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -20,10 +20,6 @@ describe('RunnerEditButton', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays Edit text', () => {
expect(wrapper.attributes('aria-label')).toBe('Edit');
});
diff --git a/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js b/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
index 408750e646f..7572122a5f3 100644
--- a/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
+++ b/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
@@ -1,5 +1,6 @@
import { GlFilteredSearch, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { assertProps } from 'helpers/assert_props';
import RunnerFilteredSearchBar from '~/ci/runner/components/runner_filtered_search_bar.vue';
import { statusTokenConfig } from '~/ci/runner/components/search_tokens/status_token_config';
import TagToken from '~/ci/runner/components/search_tokens/tag_token.vue';
@@ -43,12 +44,12 @@ describe('RunnerList', () => {
expect(inputs[inputs.length - 1][0]).toEqual(value);
};
+ const defaultProps = { namespace: 'runners', tokens: [], value: mockSearch };
+
const createComponent = ({ props = {}, options = {} } = {}) => {
wrapper = shallowMountExtended(RunnerFilteredSearchBar, {
propsData: {
- namespace: 'runners',
- tokens: [],
- value: mockSearch,
+ ...defaultProps,
...props,
},
stubs: {
@@ -65,10 +66,6 @@ describe('RunnerList', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('binds a namespace to the filtered search', () => {
expect(findFilteredSearch().props('namespace')).toBe('runners');
});
@@ -113,11 +110,14 @@ describe('RunnerList', () => {
it('fails validation for v-model with the wrong shape', () => {
expect(() => {
- createComponent({ props: { value: { filters: 'wrong_filters', sort: 'sort' } } });
+ assertProps(RunnerFilteredSearchBar, {
+ ...defaultProps,
+ value: { filters: 'wrong_filters', sort: 'sort' },
+ });
}).toThrow('Invalid prop: custom validator check failed');
expect(() => {
- createComponent({ props: { value: { sort: 'sort' } } });
+ assertProps(RunnerFilteredSearchBar, { ...defaultProps, value: { sort: 'sort' } });
}).toThrow('Invalid prop: custom validator check failed');
});
diff --git a/spec/frontend/ci/runner/components/runner_groups_spec.js b/spec/frontend/ci/runner/components/runner_groups_spec.js
index 0991feb2e55..e4f5f55ab4b 100644
--- a/spec/frontend/ci/runner/components/runner_groups_spec.js
+++ b/spec/frontend/ci/runner/components/runner_groups_spec.js
@@ -23,10 +23,6 @@ describe('RunnerGroups', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Shows a heading', () => {
createComponent();
diff --git a/spec/frontend/ci/runner/components/runner_header_spec.js b/spec/frontend/ci/runner/components/runner_header_spec.js
index abe3b47767e..c851966431d 100644
--- a/spec/frontend/ci/runner/components/runner_header_spec.js
+++ b/spec/frontend/ci/runner/components/runner_header_spec.js
@@ -42,10 +42,6 @@ describe('RunnerHeader', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays the runner status', () => {
createComponent({
mountFn: mountExtended,
diff --git a/spec/frontend/ci/runner/components/runner_jobs_empty_state_spec.js b/spec/frontend/ci/runner/components/runner_jobs_empty_state_spec.js
new file mode 100644
index 00000000000..59c9383cb31
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_jobs_empty_state_spec.js
@@ -0,0 +1,35 @@
+import EMPTY_STATE_SVG_URL from '@gitlab/svgs/dist/illustrations/pipelines_empty.svg?url';
+
+import { shallowMount } from '@vue/test-utils';
+import { GlEmptyState } from '@gitlab/ui';
+import RunnerJobsEmptyState from '~/ci/runner/components/runner_jobs_empty_state.vue';
+
+const DEFAULT_PROPS = {
+ emptyTitle: 'This runner has not run any jobs',
+ emptyDescription:
+ 'Make sure the runner is online and available to run jobs (not paused). Jobs display here when the runner picks them up.',
+};
+
+describe('RunnerJobsEmptyStateComponent', () => {
+ let wrapper;
+
+ const mountComponent = () => {
+ wrapper = shallowMount(RunnerJobsEmptyState);
+ };
+
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ describe('empty', () => {
+ it('should show an empty state if it is empty', () => {
+ const emptyState = findEmptyState();
+
+ expect(emptyState.props('svgPath')).toBe(EMPTY_STATE_SVG_URL);
+ expect(emptyState.props('title')).toBe(DEFAULT_PROPS.emptyTitle);
+ expect(emptyState.text()).toContain(DEFAULT_PROPS.emptyDescription);
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_jobs_spec.js b/spec/frontend/ci/runner/components/runner_jobs_spec.js
index bdb8a4a31a3..179b37cfa21 100644
--- a/spec/frontend/ci/runner/components/runner_jobs_spec.js
+++ b/spec/frontend/ci/runner/components/runner_jobs_spec.js
@@ -4,18 +4,19 @@ 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 { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import RunnerJobs from '~/ci/runner/components/runner_jobs.vue';
import RunnerJobsTable from '~/ci/runner/components/runner_jobs_table.vue';
import RunnerPagination from '~/ci/runner/components/runner_pagination.vue';
+import RunnerJobsEmptyState from '~/ci/runner/components/runner_jobs_empty_state.vue';
import { captureException } from '~/ci/runner/sentry_utils';
-import { I18N_NO_JOBS_FOUND, RUNNER_DETAILS_JOBS_PAGE_SIZE } from '~/ci/runner/constants';
+import { RUNNER_DETAILS_JOBS_PAGE_SIZE } from '~/ci/runner/constants';
import runnerJobsQuery from '~/ci/runner/graphql/show/runner_jobs.query.graphql';
import { runnerData, runnerJobsData } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
const mockRunner = runnerData.data.runner;
@@ -31,7 +32,7 @@ describe('RunnerJobs', () => {
const findGlSkeletonLoading = () => wrapper.findComponent(GlSkeletonLoader);
const findRunnerJobsTable = () => wrapper.findComponent(RunnerJobsTable);
const findRunnerPagination = () => wrapper.findComponent(RunnerPagination);
-
+ const findEmptyState = () => wrapper.findComponent(RunnerJobsEmptyState);
const createComponent = ({ mountFn = shallowMountExtended } = {}) => {
wrapper = mountFn(RunnerJobs, {
apolloProvider: createMockApollo([[runnerJobsQuery, mockRunnerJobsQuery]]),
@@ -47,7 +48,6 @@ describe('RunnerJobs', () => {
afterEach(() => {
mockRunnerJobsQuery.mockReset();
- wrapper.destroy();
});
it('Requests runner jobs', async () => {
@@ -100,7 +100,7 @@ describe('RunnerJobs', () => {
expect(findGlSkeletonLoading().exists()).toBe(true);
expect(findRunnerJobsTable().exists()).toBe(false);
- expect(findRunnerPagination().attributes('disabled')).toBe('true');
+ expect(findRunnerPagination().attributes('disabled')).toBeDefined();
});
});
@@ -128,8 +128,8 @@ describe('RunnerJobs', () => {
await waitForPromises();
});
- it('Shows a "None" label', () => {
- expect(wrapper.text()).toBe(I18N_NO_JOBS_FOUND);
+ it('should render empty state', () => {
+ expect(findEmptyState().exists()).toBe(true);
});
});
diff --git a/spec/frontend/ci/runner/components/runner_jobs_table_spec.js b/spec/frontend/ci/runner/components/runner_jobs_table_spec.js
index 281aa1aeb77..694c5a6ed17 100644
--- a/spec/frontend/ci/runner/components/runner_jobs_table_spec.js
+++ b/spec/frontend/ci/runner/components/runner_jobs_table_spec.js
@@ -37,10 +37,6 @@ describe('RunnerJobsTable', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Sets job id as a row key', () => {
createComponent();
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 6aea3ddf58c..0de2759ea8a 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,19 +1,15 @@
+import EMPTY_STATE_SVG_URL from '@gitlab/svgs/dist/illustrations/pipelines_empty.svg?url';
+import FILTERED_SVG_URL from '@gitlab/svgs/dist/illustrations/magnifying-glass.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 {
- newRunnerPath,
- emptyStateSvgPath,
- emptyStateFilteredSvgPath,
-} from 'jest/ci/runner/mock_data';
+import { mockRegistrationToken, newRunnerPath } from 'jest/ci/runner/mock_data';
import RunnerListEmptyState from '~/ci/runner/components/runner_list_empty_state.vue';
-const mockRegistrationToken = 'REGISTRATION_TOKEN';
-
describe('RunnerListEmptyState', () => {
let wrapper;
@@ -24,14 +20,12 @@ describe('RunnerListEmptyState', () => {
const createComponent = ({ props, mountFn = shallowMountExtended, ...options } = {}) => {
wrapper = mountFn(RunnerListEmptyState, {
propsData: {
- svgPath: emptyStateSvgPath,
- filteredSvgPath: emptyStateFilteredSvgPath,
registrationToken: mockRegistrationToken,
newRunnerPath,
...props,
},
directives: {
- GlModal: createMockDirective(),
+ GlModal: createMockDirective('gl-modal'),
},
stubs: {
GlEmptyState,
@@ -51,7 +45,7 @@ describe('RunnerListEmptyState', () => {
});
it('renders an illustration', () => {
- expect(findEmptyState().props('svgPath')).toBe(emptyStateSvgPath);
+ expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL);
});
it('displays "no results" text with instructions', () => {
@@ -62,44 +56,52 @@ describe('RunnerListEmptyState', () => {
expect(findEmptyState().text()).toMatchInterpolatedText(`${title} ${desc}`);
});
- describe('when create_runner_workflow is enabled', () => {
- beforeEach(() => {
- createComponent({
- provide: {
- glFeatures: { createRunnerWorkflow: true },
- },
+ 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);
+ it('shows a link to the new runner page', () => {
+ expect(findLink().attributes('href')).toBe(newRunnerPath);
+ });
});
- });
- describe('when create_runner_workflow is enabled and newRunnerPath not defined', () => {
- beforeEach(() => {
- createComponent({
- props: {
- newRunnerPath: null,
- },
- provide: {
- glFeatures: { createRunnerWorkflow: true },
- },
+ describe('when newRunnerPath not defined', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ newRunnerPath: null,
+ },
+ provide: {
+ glFeatures,
+ },
+ });
});
- });
- it('opens a runner registration instructions modal with a link', () => {
- const { value } = getBinding(findLink().element, 'gl-modal');
+ it('opens a runner registration instructions modal with a link', () => {
+ const { value } = getBinding(findLink().element, 'gl-modal');
- expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ });
});
});
- describe('when create_runner_workflow is disabled', () => {
+ describe.each([
+ { createRunnerWorkflowForAdmin: false },
+ { createRunnerWorkflowForNamespace: false },
+ ])('when %o', (glFeatures) => {
beforeEach(() => {
createComponent({
provide: {
- glFeatures: { createRunnerWorkflow: false },
+ glFeatures,
},
});
});
@@ -118,7 +120,7 @@ describe('RunnerListEmptyState', () => {
});
it('renders an illustration', () => {
- expect(findEmptyState().props('svgPath')).toBe(emptyStateSvgPath);
+ expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL);
});
it('displays "no results" text', () => {
@@ -141,7 +143,7 @@ describe('RunnerListEmptyState', () => {
});
it('renders a "filtered search" illustration', () => {
- expect(findEmptyState().props('svgPath')).toBe(emptyStateFilteredSvgPath);
+ expect(findEmptyState().props('svgPath')).toBe(FILTERED_SVG_URL);
});
it('displays "no filtered results" text', () => {
diff --git a/spec/frontend/ci/runner/components/runner_list_spec.js b/spec/frontend/ci/runner/components/runner_list_spec.js
index 2e5d1dbd063..0f4ec717c3e 100644
--- a/spec/frontend/ci/runner/components/runner_list_spec.js
+++ b/spec/frontend/ci/runner/components/runner_list_spec.js
@@ -57,10 +57,6 @@ describe('RunnerList', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays headers', () => {
createComponent(
{
@@ -168,7 +164,7 @@ describe('RunnerList', () => {
});
});
- it('Emits a deleted event', async () => {
+ it('Emits a deleted event', () => {
const event = { message: 'Deleted!' };
findRunnerBulkDelete().vm.$emit('deleted', event);
diff --git a/spec/frontend/ci/runner/components/runner_membership_toggle_spec.js b/spec/frontend/ci/runner/components/runner_membership_toggle_spec.js
index f089becd400..7ff3ec92042 100644
--- a/spec/frontend/ci/runner/components/runner_membership_toggle_spec.js
+++ b/spec/frontend/ci/runner/components/runner_membership_toggle_spec.js
@@ -18,10 +18,6 @@ describe('RunnerMembershipToggle', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays text', () => {
createComponent({ mountFn: mount });
diff --git a/spec/frontend/ci/runner/components/runner_pagination_spec.js b/spec/frontend/ci/runner/components/runner_pagination_spec.js
index f835ee4514d..6d84eb810f8 100644
--- a/spec/frontend/ci/runner/components/runner_pagination_spec.js
+++ b/spec/frontend/ci/runner/components/runner_pagination_spec.js
@@ -16,10 +16,6 @@ describe('RunnerPagination', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('When in between pages', () => {
const mockPageInfo = {
startCursor: mockStartCursor,
diff --git a/spec/frontend/ci/runner/components/runner_pause_button_spec.js b/spec/frontend/ci/runner/components/runner_pause_button_spec.js
index 12680e01b98..350d029f3fc 100644
--- a/spec/frontend/ci/runner/components/runner_pause_button_spec.js
+++ b/spec/frontend/ci/runner/components/runner_pause_button_spec.js
@@ -7,7 +7,7 @@ import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_help
import runnerToggleActiveMutation from '~/ci/runner/graphql/shared/runner_toggle_active.mutation.graphql';
import waitForPromises from 'helpers/wait_for_promises';
import { captureException } from '~/ci/runner/sentry_utils';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import {
I18N_PAUSE,
I18N_PAUSE_TOOLTIP,
@@ -22,7 +22,7 @@ const mockRunner = allRunnersData.data.runners.nodes[0];
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
describe('RunnerPauseButton', () => {
@@ -46,7 +46,7 @@ describe('RunnerPauseButton', () => {
},
apolloProvider: createMockApollo([[runnerToggleActiveMutation, runnerToggleActiveHandler]]),
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -74,10 +74,6 @@ describe('RunnerPauseButton', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Pause/Resume action', () => {
describe.each`
runnerState | icon | content | tooltip | isActive | newActiveValue
@@ -138,7 +134,7 @@ describe('RunnerPauseButton', () => {
await clickAndWait();
});
- it(`The mutation to that sets active to ${newActiveValue} is called`, async () => {
+ it(`The mutation to that sets active to ${newActiveValue} is called`, () => {
expect(runnerToggleActiveHandler).toHaveBeenCalledTimes(1);
expect(runnerToggleActiveHandler).toHaveBeenCalledWith({
input: {
diff --git a/spec/frontend/ci/runner/components/runner_paused_badge_spec.js b/spec/frontend/ci/runner/components/runner_paused_badge_spec.js
index b051ebe99a7..54768ea50da 100644
--- a/spec/frontend/ci/runner/components/runner_paused_badge_spec.js
+++ b/spec/frontend/ci/runner/components/runner_paused_badge_spec.js
@@ -16,7 +16,7 @@ describe('RunnerTypeBadge', () => {
...props,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -25,10 +25,6 @@ describe('RunnerTypeBadge', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders paused state', () => {
expect(wrapper.text()).toBe(I18N_PAUSED);
expect(findBadge().props('variant')).toBe('warning');
diff --git a/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js b/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js
index db6fd2c369b..eddc1438fff 100644
--- a/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js
+++ b/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js
@@ -6,19 +6,12 @@ import {
LINUX_PLATFORM,
MACOS_PLATFORM,
WINDOWS_PLATFORM,
- AWS_PLATFORM,
DOCKER_HELP_URL,
KUBERNETES_HELP_URL,
} from '~/ci/runner/constants';
import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
-const mockProvide = {
- awsImgPath: 'awsLogo.svg',
- dockerImgPath: 'dockerLogo.svg',
- kubernetesImgPath: 'kubernetesLogo.svg',
-};
-
describe('RunnerPlatformsRadioGroup', () => {
let wrapper;
@@ -35,7 +28,6 @@ describe('RunnerPlatformsRadioGroup', () => {
value: null,
...props,
},
- provide: mockProvide,
...options,
});
};
@@ -48,10 +40,9 @@ describe('RunnerPlatformsRadioGroup', () => {
const labels = findFormRadios().map((w) => [w.text(), w.props('image')]);
expect(labels).toEqual([
- ['Linux', null],
+ ['Linux', expect.any(String)],
['macOS', null],
['Windows', null],
- ['AWS', expect.any(String)],
['Docker', expect.any(String)],
['Kubernetes', expect.any(String)],
]);
@@ -69,7 +60,6 @@ describe('RunnerPlatformsRadioGroup', () => {
${'Linux'} | ${LINUX_PLATFORM}
${'macOS'} | ${MACOS_PLATFORM}
${'Windows'} | ${WINDOWS_PLATFORM}
- ${'AWS'} | ${AWS_PLATFORM}
`('user can select "$text"', async ({ text, value }) => {
const radio = findFormRadioByText(text);
expect(radio.props('value')).toBe(value);
@@ -84,7 +74,7 @@ describe('RunnerPlatformsRadioGroup', () => {
text | href
${'Docker'} | ${DOCKER_HELP_URL}
${'Kubernetes'} | ${KUBERNETES_HELP_URL}
- `('provides link to "$text" docs', async ({ text, href }) => {
+ `('provides link to "$text" docs', ({ text, href }) => {
const radio = findFormRadioByText(text);
expect(radio.findComponent(GlLink).attributes()).toEqual({
diff --git a/spec/frontend/ci/runner/components/runner_platforms_radio_spec.js b/spec/frontend/ci/runner/components/runner_platforms_radio_spec.js
index fb81edd1ae2..340b04637f8 100644
--- a/spec/frontend/ci/runner/components/runner_platforms_radio_spec.js
+++ b/spec/frontend/ci/runner/components/runner_platforms_radio_spec.js
@@ -41,7 +41,7 @@ describe('RunnerPlatformsRadio', () => {
expect(findFormRadio().attributes('value')).toBe(mockValue);
});
- it('emits when item is clicked', async () => {
+ it('emits when item is clicked', () => {
findDiv().trigger('click');
expect(wrapper.emitted('input')).toEqual([[mockValue]]);
@@ -94,7 +94,7 @@ describe('RunnerPlatformsRadio', () => {
expect(wrapper.classes('gl-cursor-pointer')).toBe(false);
});
- it('does not emit when item is clicked', async () => {
+ it('does not emit when item is clicked', () => {
findDiv().trigger('click');
expect(wrapper.emitted('input')).toBe(undefined);
diff --git a/spec/frontend/ci/runner/components/runner_projects_spec.js b/spec/frontend/ci/runner/components/runner_projects_spec.js
index 17517c4db66..736a1f7d3ce 100644
--- a/spec/frontend/ci/runner/components/runner_projects_spec.js
+++ b/spec/frontend/ci/runner/components/runner_projects_spec.js
@@ -4,7 +4,7 @@ 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 { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { sprintf } from '~/locale';
import {
I18N_ASSIGNED_PROJECTS,
@@ -22,7 +22,7 @@ import runnerProjectsQuery from '~/ci/runner/graphql/show/runner_projects.query.
import { runnerData, runnerProjectsData } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
const mockRunner = runnerData.data.runner;
@@ -56,7 +56,6 @@ describe('RunnerProjects', () => {
afterEach(() => {
mockRunnerProjectsQuery.mockReset();
- wrapper.destroy();
});
it('Requests runner projects', async () => {
@@ -90,7 +89,7 @@ describe('RunnerProjects', () => {
await waitForPromises();
});
- it('Shows a heading', async () => {
+ it('Shows a heading', () => {
const expected = sprintf(I18N_ASSIGNED_PROJECTS, { projectCount: mockProjects.length });
expect(findHeading().text()).toBe(expected);
@@ -195,7 +194,7 @@ describe('RunnerProjects', () => {
expect(wrapper.findByText(I18N_NO_PROJECTS_FOUND).exists()).toBe(false);
expect(findRunnerAssignedItems().length).toBe(0);
- expect(findRunnerPagination().attributes('disabled')).toBe('true');
+ expect(findRunnerPagination().attributes('disabled')).toBeDefined();
expect(findGlSearchBoxByType().props('isLoading')).toBe(true);
});
});
diff --git a/spec/frontend/ci/runner/components/runner_status_badge_spec.js b/spec/frontend/ci/runner/components/runner_status_badge_spec.js
index 45b410df2d4..e1eb81f2d23 100644
--- a/spec/frontend/ci/runner/components/runner_status_badge_spec.js
+++ b/spec/frontend/ci/runner/components/runner_status_badge_spec.js
@@ -31,7 +31,7 @@ describe('RunnerTypeBadge', () => {
...props,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -43,8 +43,6 @@ describe('RunnerTypeBadge', () => {
afterEach(() => {
jest.useFakeTimers({ legacyFakeTimers: true });
-
- wrapper.destroy();
});
it('renders online state', () => {
diff --git a/spec/frontend/ci/runner/components/runner_tag_spec.js b/spec/frontend/ci/runner/components/runner_tag_spec.js
index 7bcb046ae43..e3d46e5d6df 100644
--- a/spec/frontend/ci/runner/components/runner_tag_spec.js
+++ b/spec/frontend/ci/runner/components/runner_tag_spec.js
@@ -29,8 +29,8 @@ describe('RunnerTag', () => {
...props,
},
directives: {
- GlTooltip: createMockDirective(),
- GlResizeObserver: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
+ GlResizeObserver: createMockDirective('gl-resize-observer'),
},
});
};
@@ -39,10 +39,6 @@ describe('RunnerTag', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays tag text', () => {
expect(wrapper.text()).toBe(mockTag);
});
diff --git a/spec/frontend/ci/runner/components/runner_tags_spec.js b/spec/frontend/ci/runner/components/runner_tags_spec.js
index 96bec00302b..bcb1d1f9e13 100644
--- a/spec/frontend/ci/runner/components/runner_tags_spec.js
+++ b/spec/frontend/ci/runner/components/runner_tags_spec.js
@@ -21,10 +21,6 @@ describe('RunnerTags', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays tags text', () => {
expect(wrapper.text()).toMatchInterpolatedText('tag1 tag2');
diff --git a/spec/frontend/ci/runner/components/runner_type_badge_spec.js b/spec/frontend/ci/runner/components/runner_type_badge_spec.js
index 58f09362759..f7ecd108967 100644
--- a/spec/frontend/ci/runner/components/runner_type_badge_spec.js
+++ b/spec/frontend/ci/runner/components/runner_type_badge_spec.js
@@ -2,6 +2,7 @@ import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import RunnerTypeBadge from '~/ci/runner/components/runner_type_badge.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { assertProps } from 'helpers/assert_props';
import {
INSTANCE_TYPE,
GROUP_TYPE,
@@ -23,15 +24,11 @@ describe('RunnerTypeBadge', () => {
...props,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe.each`
type | text
${INSTANCE_TYPE} | ${I18N_INSTANCE_TYPE}
@@ -54,7 +51,7 @@ describe('RunnerTypeBadge', () => {
it('validation fails for an incorrect type', () => {
expect(() => {
- createComponent({ props: { type: 'AN_UNKNOWN_VALUE' } });
+ assertProps(RunnerTypeBadge, { type: 'AN_UNKNOWN_VALUE' });
}).toThrow();
});
diff --git a/spec/frontend/ci/runner/components/runner_type_tabs_spec.js b/spec/frontend/ci/runner/components/runner_type_tabs_spec.js
index 3347c190083..71dcc5b4226 100644
--- a/spec/frontend/ci/runner/components/runner_type_tabs_spec.js
+++ b/spec/frontend/ci/runner/components/runner_type_tabs_spec.js
@@ -8,6 +8,7 @@ import {
PROJECT_TYPE,
DEFAULT_MEMBERSHIP,
DEFAULT_SORT,
+ STATUS_ONLINE,
} from '~/ci/runner/constants';
const mockSearch = {
@@ -63,10 +64,6 @@ describe('RunnerTypeTabs', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Renders all options to filter runners by default', () => {
createComponent();
@@ -115,7 +112,7 @@ describe('RunnerTypeTabs', () => {
it('Renders a count next to each tab', () => {
const mockVariables = {
paused: true,
- status: 'ONLINE',
+ status: STATUS_ONLINE,
};
createComponent({
diff --git a/spec/frontend/ci/runner/components/runner_update_form_spec.js b/spec/frontend/ci/runner/components/runner_update_form_spec.js
index a0e51ebf958..db4c236bfff 100644
--- a/spec/frontend/ci/runner/components/runner_update_form_spec.js
+++ b/spec/frontend/ci/runner/components/runner_update_form_spec.js
@@ -5,8 +5,8 @@ import { __ } from '~/locale';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert, VARIANT_SUCCESS } from '~/flash';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import RunnerUpdateForm from '~/ci/runner/components/runner_update_form.vue';
import {
INSTANCE_TYPE,
@@ -21,7 +21,7 @@ import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_al
import { runnerFormData } from '../mock_data';
jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility');
@@ -86,7 +86,7 @@ describe('RunnerUpdateForm', () => {
variant: VARIANT_SUCCESS,
}),
);
- expect(redirectTo).toHaveBeenCalledWith(mockRunnerPath);
+ expect(redirectTo).toHaveBeenCalledWith(mockRunnerPath); // eslint-disable-line import/no-deprecated
};
beforeEach(() => {
@@ -107,10 +107,6 @@ describe('RunnerUpdateForm', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Form has a submit button', () => {
expect(findSubmit().exists()).toBe(true);
});
@@ -282,7 +278,7 @@ describe('RunnerUpdateForm', () => {
expect(captureException).not.toHaveBeenCalled();
expect(saveAlertToLocalStorage).not.toHaveBeenCalled();
- expect(redirectTo).not.toHaveBeenCalled();
+ expect(redirectTo).not.toHaveBeenCalled(); // eslint-disable-line import/no-deprecated
});
});
});
diff --git a/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js b/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js
index b7d9d3ad23e..e9f2e888b9a 100644
--- a/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js
+++ b/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js
@@ -3,14 +3,14 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import TagToken, { TAG_SUGGESTIONS_PATH } from '~/ci/runner/components/search_tokens/tag_token.vue';
import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
import { getRecentlyUsedSuggestions } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils', () => ({
...jest.requireActual('~/vue_shared/components/filtered_search_bar/filtered_search_utils'),
@@ -90,7 +90,6 @@ describe('TagToken', () => {
afterEach(() => {
getRecentlyUsedSuggestions.mockReset();
- wrapper.destroy();
});
describe('when the tags token is displayed', () => {
diff --git a/spec/frontend/ci/runner/components/stat/runner_count_spec.js b/spec/frontend/ci/runner/components/stat/runner_count_spec.js
index 42d8c9a1080..df774ba3e57 100644
--- a/spec/frontend/ci/runner/components/stat/runner_count_spec.js
+++ b/spec/frontend/ci/runner/components/stat/runner_count_spec.js
@@ -2,7 +2,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
import RunnerCount from '~/ci/runner/components/stat/runner_count.vue';
-import { INSTANCE_TYPE, GROUP_TYPE } from '~/ci/runner/constants';
+import { INSTANCE_TYPE, GROUP_TYPE, STATUS_ONLINE } from '~/ci/runner/constants';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { captureException } from '~/ci/runner/sentry_utils';
@@ -47,7 +47,7 @@ describe('RunnerCount', () => {
});
describe('in admin scope', () => {
- const mockVariables = { status: 'ONLINE' };
+ const mockVariables = { status: STATUS_ONLINE };
beforeEach(async () => {
await createComponent({ props: { scope: INSTANCE_TYPE } });
@@ -67,7 +67,7 @@ describe('RunnerCount', () => {
expect(wrapper.html()).toBe(`<strong>${runnersCountData.data.runners.count}</strong>`);
});
- it('does not fetch from the group query', async () => {
+ it('does not fetch from the group query', () => {
expect(mockGroupRunnersCountHandler).not.toHaveBeenCalled();
});
@@ -89,7 +89,7 @@ describe('RunnerCount', () => {
await createComponent({ props: { scope: INSTANCE_TYPE, skip: true } });
});
- it('does not fetch data', async () => {
+ it('does not fetch data', () => {
expect(mockRunnersCountHandler).not.toHaveBeenCalled();
expect(mockGroupRunnersCountHandler).not.toHaveBeenCalled();
@@ -106,7 +106,7 @@ describe('RunnerCount', () => {
await createComponent({ props: { scope: INSTANCE_TYPE } });
});
- it('data is not shown and error is reported', async () => {
+ it('data is not shown and error is reported', () => {
expect(wrapper.html()).toBe('<strong></strong>');
expect(captureException).toHaveBeenCalledWith({
@@ -121,7 +121,7 @@ describe('RunnerCount', () => {
await createComponent({ props: { scope: GROUP_TYPE } });
});
- it('fetches data from the group query', async () => {
+ it('fetches data from the group query', () => {
expect(mockGroupRunnersCountHandler).toHaveBeenCalledTimes(1);
expect(mockGroupRunnersCountHandler).toHaveBeenCalledWith({});
@@ -141,7 +141,7 @@ describe('RunnerCount', () => {
wrapper.vm.refetch();
});
- it('data is not shown and error is reported', async () => {
+ it('data is not shown and error is reported', () => {
expect(mockRunnersCountHandler).toHaveBeenCalledTimes(2);
});
});
diff --git a/spec/frontend/ci/runner/components/stat/runner_single_stat_spec.js b/spec/frontend/ci/runner/components/stat/runner_single_stat_spec.js
index cad61f26012..f30b75ee614 100644
--- a/spec/frontend/ci/runner/components/stat/runner_single_stat_spec.js
+++ b/spec/frontend/ci/runner/components/stat/runner_single_stat_spec.js
@@ -32,10 +32,6 @@ describe('RunnerStats', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
case | count | value
${'number'} | ${99} | ${'99'}
diff --git a/spec/frontend/ci/runner/components/stat/runner_stats_spec.js b/spec/frontend/ci/runner/components/stat/runner_stats_spec.js
index 3d45674d106..13366a788d5 100644
--- a/spec/frontend/ci/runner/components/stat/runner_stats_spec.js
+++ b/spec/frontend/ci/runner/components/stat/runner_stats_spec.js
@@ -47,10 +47,6 @@ describe('RunnerStats', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays all the stats', () => {
createComponent({
mountFn: mount,
diff --git a/spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js b/spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js
new file mode 100644
index 00000000000..1c052b00fc3
--- /dev/null
+++ b/spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js
@@ -0,0 +1,124 @@
+import { GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
+
+import GroupRunnerRunnerApp from '~/ci/runner/group_new_runner/group_new_runner_app.vue';
+import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
+import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue';
+import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
+import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
+import {
+ PARAM_KEY_PLATFORM,
+ GROUP_TYPE,
+ DEFAULT_PLATFORM,
+ WINDOWS_PLATFORM,
+} from '~/ci/runner/constants';
+import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import { runnerCreateResult } from '../mock_data';
+
+const mockGroupId = 'gid://gitlab/Group/72';
+
+jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
+jest.mock('~/alert');
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ redirectTo: jest.fn(),
+}));
+
+const mockCreatedRunner = runnerCreateResult.data.runnerCreate.runner;
+
+describe('GroupRunnerRunnerApp', () => {
+ let wrapper;
+
+ const findRunnerPlatformsRadioGroup = () => wrapper.findComponent(RunnerPlatformsRadioGroup);
+ const findRegistrationFeedbackBanner = () => wrapper.findComponent(RegistrationFeedbackBanner);
+ const findRegistrationCompatibilityAlert = () =>
+ wrapper.findComponent(RegistrationCompatibilityAlert);
+ const findRunnerCreateForm = () => wrapper.findComponent(RunnerCreateForm);
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(GroupRunnerRunnerApp, {
+ propsData: {
+ groupId: mockGroupId,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows a registration feedback banner', () => {
+ expect(findRegistrationFeedbackBanner().exists()).toBe(true);
+ });
+
+ it('shows a registration compatibility alert', () => {
+ expect(findRegistrationCompatibilityAlert().props('alertKey')).toBe(mockGroupId);
+ });
+
+ describe('Platform', () => {
+ it('shows the platforms radio group', () => {
+ expect(findRunnerPlatformsRadioGroup().props('value')).toBe(DEFAULT_PLATFORM);
+ });
+ });
+
+ describe('Runner form', () => {
+ it('shows the runner create form for an instance runner', () => {
+ expect(findRunnerCreateForm().props()).toEqual({
+ runnerType: GROUP_TYPE,
+ groupId: mockGroupId,
+ projectId: null,
+ });
+ });
+
+ describe('When a runner is saved', () => {
+ beforeEach(() => {
+ findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner);
+ });
+
+ it('pushes an alert to be shown after redirection', () => {
+ expect(saveAlertToLocalStorage).toHaveBeenCalledWith({
+ message: s__('Runners|Runner created.'),
+ variant: VARIANT_SUCCESS,
+ });
+ });
+
+ it('redirects to the registration page', () => {
+ const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${DEFAULT_PLATFORM}`;
+
+ expect(redirectTo).toHaveBeenCalledWith(url); // eslint-disable-line import/no-deprecated
+ });
+ });
+
+ describe('When another platform is selected and a runner is saved', () => {
+ beforeEach(() => {
+ findRunnerPlatformsRadioGroup().vm.$emit('input', WINDOWS_PLATFORM);
+ findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner);
+ });
+
+ it('redirects to the registration page with the platform', () => {
+ const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`;
+
+ expect(redirectTo).toHaveBeenCalledWith(url); // eslint-disable-line import/no-deprecated
+ });
+ });
+
+ describe('When runner fails to save', () => {
+ const ERROR_MSG = 'Cannot save!';
+
+ beforeEach(() => {
+ findRunnerCreateForm().vm.$emit('error', new Error(ERROR_MSG));
+ });
+
+ it('shows an error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: ERROR_MSG });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/group_register_runner_app/group_register_runner_app_spec.js b/spec/frontend/ci/runner/group_register_runner_app/group_register_runner_app_spec.js
new file mode 100644
index 00000000000..2f0807c700c
--- /dev/null
+++ b/spec/frontend/ci/runner/group_register_runner_app/group_register_runner_app_spec.js
@@ -0,0 +1,120 @@
+import { nextTick } from 'vue';
+import { GlButton } from '@gitlab/ui';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { TEST_HOST } from 'helpers/test_constants';
+
+import { updateHistory } from '~/lib/utils/url_utility';
+import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM, WINDOWS_PLATFORM } from '~/ci/runner/constants';
+import GroupRegisterRunnerApp from '~/ci/runner/group_register_runner/group_register_runner_app.vue';
+import RegistrationInstructions from '~/ci/runner/components/registration/registration_instructions.vue';
+import PlatformsDrawer from '~/ci/runner/components/registration/platforms_drawer.vue';
+import { runnerForRegistration } from '../mock_data';
+
+const mockRunnerId = runnerForRegistration.data.runner.id;
+const mockRunnersPath = '/groups/group1/-/runners';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ updateHistory: jest.fn(),
+}));
+
+describe('GroupRegisterRunnerApp', () => {
+ let wrapper;
+
+ const findRegistrationInstructions = () => wrapper.findComponent(RegistrationInstructions);
+ const findPlatformsDrawer = () => wrapper.findComponent(PlatformsDrawer);
+ const findBtn = () => wrapper.findComponent(GlButton);
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(GroupRegisterRunnerApp, {
+ propsData: {
+ runnerId: mockRunnerId,
+ runnersPath: mockRunnersPath,
+ },
+ });
+ };
+
+ describe('When showing runner details', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('when runner token is available', () => {
+ it('shows registration instructions', () => {
+ expect(findRegistrationInstructions().props()).toEqual({
+ platform: DEFAULT_PLATFORM,
+ runnerId: mockRunnerId,
+ });
+ });
+
+ it('configures platform drawer', () => {
+ expect(findPlatformsDrawer().props()).toEqual({
+ open: false,
+ platform: DEFAULT_PLATFORM,
+ });
+ });
+
+ it('shows runner list button', () => {
+ expect(findBtn().attributes('href')).toBe(mockRunnersPath);
+ expect(findBtn().props('variant')).toBe('confirm');
+ });
+ });
+ });
+
+ describe('When another platform has been selected', () => {
+ beforeEach(() => {
+ setWindowLocation(`?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`);
+
+ createComponent();
+ });
+
+ it('shows registration instructions for the platform', () => {
+ expect(findRegistrationInstructions().props('platform')).toBe(WINDOWS_PLATFORM);
+ });
+ });
+
+ describe('When opening install instructions', () => {
+ beforeEach(() => {
+ createComponent();
+
+ findRegistrationInstructions().vm.$emit('toggleDrawer');
+ });
+
+ it('opens platform drawer', () => {
+ expect(findPlatformsDrawer().props('open')).toBe(true);
+ });
+
+ it('closes platform drawer', async () => {
+ findRegistrationInstructions().vm.$emit('toggleDrawer');
+ await nextTick();
+
+ expect(findPlatformsDrawer().props('open')).toBe(false);
+ });
+
+ it('closes platform drawer from drawer', async () => {
+ findPlatformsDrawer().vm.$emit('close');
+ await nextTick();
+
+ expect(findPlatformsDrawer().props('open')).toBe(false);
+ });
+
+ describe('when selecting a platform', () => {
+ beforeEach(() => {
+ findPlatformsDrawer().vm.$emit('selectPlatform', WINDOWS_PLATFORM);
+ });
+
+ it('updates the url', () => {
+ expect(updateHistory).toHaveBeenCalledTimes(1);
+ expect(updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`,
+ });
+ });
+
+ it('updates the registration instructions', () => {
+ expect(findRegistrationInstructions().props('platform')).toBe(WINDOWS_PLATFORM);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js b/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
index 2ad31dea774..0c594e8005c 100644
--- a/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
+++ b/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
@@ -4,8 +4,8 @@ import VueApollo from 'vue-apollo';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert, VARIANT_SUCCESS } from '~/flash';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerHeader from '~/ci/runner/components/runner_header.vue';
@@ -24,7 +24,7 @@ import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_al
import { runnerData } from '../mock_data';
jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility');
@@ -74,7 +74,6 @@ describe('GroupRunnerShowApp', () => {
afterEach(() => {
mockRunnerQuery.mockReset();
- wrapper.destroy();
});
describe('When showing runner details', () => {
@@ -84,7 +83,7 @@ describe('GroupRunnerShowApp', () => {
await createComponent({ mountFn: mountExtended });
});
- it('expect GraphQL ID to be requested', async () => {
+ it('expect GraphQL ID to be requested', () => {
expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunnerGraphqlId });
});
@@ -92,7 +91,7 @@ describe('GroupRunnerShowApp', () => {
expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId}`);
});
- it('displays the runner edit and pause buttons', async () => {
+ it('displays the runner edit and pause buttons', () => {
expect(findRunnerEditButton().attributes('href')).toBe(mockEditGroupRunnerPath);
expect(findRunnerPauseButton().exists()).toBe(true);
expect(findRunnerDeleteButton().exists()).toBe(true);
@@ -186,7 +185,7 @@ describe('GroupRunnerShowApp', () => {
message: 'Runner deleted',
variant: VARIANT_SUCCESS,
});
- expect(redirectTo).toHaveBeenCalledWith(mockRunnersPath);
+ expect(redirectTo).toHaveBeenCalledWith(mockRunnersPath); // eslint-disable-line import/no-deprecated
});
});
});
diff --git a/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
index 39ea5cade28..41be72b1645 100644
--- a/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
@@ -9,7 +9,7 @@ import {
mountExtended,
} from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
@@ -58,23 +58,22 @@ import {
groupRunnersCountData,
onlineContactTimeoutSecs,
staleTimeoutSecs,
+ mockRegistrationToken,
+ newRunnerPath,
emptyPageInfo,
- emptyStateSvgPath,
- emptyStateFilteredSvgPath,
} from '../mock_data';
Vue.use(VueApollo);
Vue.use(GlToast);
const mockGroupFullPath = 'group1';
-const mockRegistrationToken = 'AABBCC';
const mockGroupRunnersEdges = groupRunnersData.data.group.runners.edges;
const mockGroupRunnersCount = mockGroupRunnersEdges.length;
const mockGroupRunnersHandler = jest.fn();
const mockGroupRunnersCountHandler = jest.fn();
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
@@ -87,6 +86,7 @@ describe('GroupRunnersApp', () => {
const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
+ const findNewRunnerBtn = () => wrapper.findByText(s__('Runners|New group runner'));
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
const findRunnerList = () => wrapper.findComponent(RunnerList);
const findRunnerListEmptyState = () => wrapper.findComponent(RunnerListEmptyState);
@@ -114,14 +114,13 @@ describe('GroupRunnersApp', () => {
propsData: {
registrationToken: mockRegistrationToken,
groupFullPath: mockGroupFullPath,
+ newRunnerPath,
...props,
},
provide: {
localMutations,
onlineContactTimeoutSecs,
staleTimeoutSecs,
- emptyStateSvgPath,
- emptyStateFilteredSvgPath,
...provide,
},
...options,
@@ -138,7 +137,6 @@ describe('GroupRunnersApp', () => {
afterEach(() => {
mockGroupRunnersHandler.mockReset();
mockGroupRunnersCountHandler.mockReset();
- wrapper.destroy();
});
it('shows the runner tabs with a runner count for each type', async () => {
@@ -288,7 +286,7 @@ describe('GroupRunnersApp', () => {
});
});
- it('When runner is paused or unpaused, some data is refetched', async () => {
+ it('When runner is paused or unpaused, some data is refetched', () => {
expect(mockGroupRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES);
findRunnerActionsCell().vm.$emit('toggledPaused');
@@ -300,7 +298,7 @@ describe('GroupRunnersApp', () => {
expect(showToast).toHaveBeenCalledTimes(0);
});
- it('When runner is deleted, data is refetched and a toast message is shown', async () => {
+ it('When runner is deleted, data is refetched and a toast message is shown', () => {
findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
expect(showToast).toHaveBeenCalledTimes(1);
@@ -389,7 +387,7 @@ describe('GroupRunnersApp', () => {
it('when runners have not loaded, shows a loading state', () => {
createComponent();
expect(findRunnerList().props('loading')).toBe(true);
- expect(findRunnerPagination().attributes('disabled')).toBe('true');
+ expect(findRunnerPagination().attributes('disabled')).toBeDefined();
});
it('runners can be deleted in bulk', () => {
@@ -417,8 +415,12 @@ describe('GroupRunnersApp', () => {
expect(createAlert).not.toHaveBeenCalled();
});
- it('shows an empty state', async () => {
- expect(findRunnerListEmptyState().exists()).toBe(true);
+ it('shows an empty state', () => {
+ expect(findRunnerListEmptyState().props()).toMatchObject({
+ isSearchFiltered: false,
+ newRunnerPath,
+ registrationToken: mockRegistrationToken,
+ });
});
});
@@ -428,11 +430,11 @@ describe('GroupRunnersApp', () => {
await createComponent();
});
- it('error is shown to the user', async () => {
+ it('error is shown to the user', () => {
expect(createAlert).toHaveBeenCalledTimes(1);
});
- it('error is reported to sentry', async () => {
+ it('error is reported to sentry', () => {
expect(captureException).toHaveBeenCalledWith({
error: new Error('Error!'),
component: 'GroupRunnersApp',
@@ -469,32 +471,69 @@ describe('GroupRunnersApp', () => {
});
describe('when user has permission to register group runner', () => {
- beforeEach(() => {
+ it('shows the register group runner button', () => {
createComponent({
- propsData: {
+ props: {
registrationToken: mockRegistrationToken,
- groupFullPath: mockGroupFullPath,
},
});
+ expect(findRegistrationDropdown().exists()).toBe(true);
});
- it('shows the register group runner button', () => {
- expect(findRegistrationDropdown().exists()).toBe(true);
+ it('when create_runner_workflow_for_namespace is enabled', () => {
+ createComponent({
+ props: {
+ newRunnerPath,
+ },
+ provide: {
+ glFeatures: {
+ createRunnerWorkflowForNamespace: true,
+ },
+ },
+ });
+
+ expect(findNewRunnerBtn().attributes('href')).toBe(newRunnerPath);
+ });
+
+ it('when create_runner_workflow_for_namespace is disabled', () => {
+ createComponent({
+ props: {
+ newRunnerPath,
+ },
+ provide: {
+ glFeatures: {
+ createRunnerWorkflowForNamespace: false,
+ },
+ },
+ });
+
+ expect(findNewRunnerBtn().exists()).toBe(false);
});
});
describe('when user has no permission to register group runner', () => {
- beforeEach(() => {
+ it('does not show the register group runner button', () => {
createComponent({
- propsData: {
+ props: {
registrationToken: null,
- groupFullPath: mockGroupFullPath,
},
});
+ expect(findRegistrationDropdown().exists()).toBe(false);
});
- it('does not show the register group runner button', () => {
- expect(findRegistrationDropdown().exists()).toBe(false);
+ it('when create_runner_workflow_for_namespace is enabled', () => {
+ createComponent({
+ props: {
+ newRunnerPath: null,
+ },
+ provide: {
+ glFeatures: {
+ createRunnerWorkflowForNamespace: true,
+ },
+ },
+ });
+
+ expect(findNewRunnerBtn().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/ci/runner/local_storage_alert/show_alert_from_local_storage_spec.js b/spec/frontend/ci/runner/local_storage_alert/show_alert_from_local_storage_spec.js
index 03908891cfd..30e49fc7644 100644
--- a/spec/frontend/ci/runner/local_storage_alert/show_alert_from_local_storage_spec.js
+++ b/spec/frontend/ci/runner/local_storage_alert/show_alert_from_local_storage_spec.js
@@ -2,9 +2,9 @@ import AccessorUtilities from '~/lib/utils/accessor';
import { showAlertFromLocalStorage } from '~/ci/runner/local_storage_alert/show_alert_from_local_storage';
import { LOCAL_STORAGE_ALERT_KEY } from '~/ci/runner/local_storage_alert/constants';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('showAlertFromLocalStorage', () => {
useLocalStorageSpy();
diff --git a/spec/frontend/ci/runner/mock_data.js b/spec/frontend/ci/runner/mock_data.js
index 5cdf0ea4e3b..223a156795c 100644
--- a/spec/frontend/ci/runner/mock_data.js
+++ b/spec/frontend/ci/runner/mock_data.js
@@ -1,6 +1,19 @@
// Fixtures generated by: spec/frontend/fixtures/runner.rb
+// List queries
+import allRunnersWithCreatorData from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.with_creator.json';
+import allRunnersData from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.json';
+import allRunnersDataPaginated from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.paginated.json';
+import runnersCountData from 'test_fixtures/graphql/ci/runner/list/all_runners_count.query.graphql.json';
+import groupRunnersData from 'test_fixtures/graphql/ci/runner/list/group_runners.query.graphql.json';
+import groupRunnersDataPaginated from 'test_fixtures/graphql/ci/runner/list/group_runners.query.graphql.paginated.json';
+import groupRunnersCountData from 'test_fixtures/graphql/ci/runner/list/group_runners_count.query.graphql.json';
+
+// Register runner queries
+import runnerForRegistration from 'test_fixtures/graphql/ci/runner/register/runner_for_registration.query.graphql.json';
+
// Show runner queries
+import runnerCreateResult from 'test_fixtures/graphql/ci/runner/new/runner_create.mutation.graphql.json';
import runnerData from 'test_fixtures/graphql/ci/runner/show/runner.query.graphql.json';
import runnerWithGroupData from 'test_fixtures/graphql/ci/runner/show/runner.query.graphql.with_group.json';
import runnerProjectsData from 'test_fixtures/graphql/ci/runner/show/runner_projects.query.graphql.json';
@@ -9,15 +22,16 @@ import runnerJobsData from 'test_fixtures/graphql/ci/runner/show/runner_jobs.que
// Edit runner queries
import runnerFormData from 'test_fixtures/graphql/ci/runner/edit/runner_form.query.graphql.json';
-// List queries
-import allRunnersData from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.json';
-import allRunnersDataPaginated from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.paginated.json';
-import runnersCountData from 'test_fixtures/graphql/ci/runner/list/all_runners_count.query.graphql.json';
-import groupRunnersData from 'test_fixtures/graphql/ci/runner/list/group_runners.query.graphql.json';
-import groupRunnersDataPaginated from 'test_fixtures/graphql/ci/runner/list/group_runners.query.graphql.paginated.json';
-import groupRunnersCountData from 'test_fixtures/graphql/ci/runner/list/group_runners_count.query.graphql.json';
-
-import { DEFAULT_MEMBERSHIP, RUNNER_PAGE_SIZE } from '~/ci/runner/constants';
+// New runner queries
+import {
+ DEFAULT_MEMBERSHIP,
+ INSTANCE_TYPE,
+ CREATED_DESC,
+ CREATED_ASC,
+ STATUS_ONLINE,
+ STATUS_STALE,
+ RUNNER_PAGE_SIZE,
+} from '~/ci/runner/constants';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
const emptyPageInfo = {
@@ -40,29 +54,29 @@ export const mockSearchExamples = [
membership: DEFAULT_MEMBERSHIP,
filters: [],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
membership: DEFAULT_MEMBERSHIP,
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
},
isDefault: true,
},
{
name: 'a single status',
- urlQuery: '?status[]=ACTIVE',
+ urlQuery: '?status[]=ONLINE',
search: {
runnerType: null,
membership: DEFAULT_MEMBERSHIP,
- filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }],
+ filters: [{ type: 'status', value: { data: STATUS_ONLINE, operator: '=' } }],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
membership: DEFAULT_MEMBERSHIP,
- status: 'ACTIVE',
- sort: 'CREATED_DESC',
+ status: STATUS_ONLINE,
+ sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
},
},
@@ -79,12 +93,12 @@ export const mockSearchExamples = [
},
],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
membership: DEFAULT_MEMBERSHIP,
search: 'something',
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
},
},
@@ -105,12 +119,12 @@ export const mockSearchExamples = [
},
],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
membership: DEFAULT_MEMBERSHIP,
search: 'something else',
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
},
},
@@ -118,54 +132,54 @@ export const mockSearchExamples = [
name: 'single instance type',
urlQuery: '?runner_type[]=INSTANCE_TYPE',
search: {
- runnerType: 'INSTANCE_TYPE',
+ runnerType: INSTANCE_TYPE,
membership: DEFAULT_MEMBERSHIP,
filters: [],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
- type: 'INSTANCE_TYPE',
+ type: INSTANCE_TYPE,
membership: DEFAULT_MEMBERSHIP,
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
},
},
{
name: 'multiple runner status',
- urlQuery: '?status[]=ACTIVE&status[]=PAUSED',
+ urlQuery: '?status[]=ONLINE&status[]=STALE',
search: {
runnerType: null,
membership: DEFAULT_MEMBERSHIP,
filters: [
- { type: 'status', value: { data: 'ACTIVE', operator: '=' } },
- { type: 'status', value: { data: 'PAUSED', operator: '=' } },
+ { type: 'status', value: { data: STATUS_ONLINE, operator: '=' } },
+ { type: 'status', value: { data: STATUS_STALE, operator: '=' } },
],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
- status: 'ACTIVE',
+ status: STATUS_ONLINE,
membership: DEFAULT_MEMBERSHIP,
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
},
},
{
name: 'multiple status, a single instance type and a non default sort',
- urlQuery: '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&sort=CREATED_ASC',
+ urlQuery: '?status[]=ONLINE&runner_type[]=INSTANCE_TYPE&sort=CREATED_ASC',
search: {
- runnerType: 'INSTANCE_TYPE',
+ runnerType: INSTANCE_TYPE,
membership: DEFAULT_MEMBERSHIP,
- filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }],
+ filters: [{ type: 'status', value: { data: STATUS_ONLINE, operator: '=' } }],
pagination: {},
- sort: 'CREATED_ASC',
+ sort: CREATED_ASC,
},
graphqlVariables: {
- status: 'ACTIVE',
- type: 'INSTANCE_TYPE',
+ status: STATUS_ONLINE,
+ type: INSTANCE_TYPE,
membership: DEFAULT_MEMBERSHIP,
- sort: 'CREATED_ASC',
+ sort: CREATED_ASC,
first: RUNNER_PAGE_SIZE,
},
},
@@ -177,13 +191,13 @@ export const mockSearchExamples = [
membership: DEFAULT_MEMBERSHIP,
filters: [{ type: 'tag', value: { data: 'tag-1', operator: '=' } }],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
membership: DEFAULT_MEMBERSHIP,
tagList: ['tag-1'],
- first: 20,
- sort: 'CREATED_DESC',
+ first: RUNNER_PAGE_SIZE,
+ sort: CREATED_DESC,
},
},
{
@@ -197,13 +211,13 @@ export const mockSearchExamples = [
{ type: 'tag', value: { data: 'tag-2', operator: '=' } },
],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
membership: DEFAULT_MEMBERSHIP,
tagList: ['tag-1', 'tag-2'],
- first: 20,
- sort: 'CREATED_DESC',
+ first: RUNNER_PAGE_SIZE,
+ sort: CREATED_DESC,
},
},
{
@@ -214,11 +228,11 @@ export const mockSearchExamples = [
membership: DEFAULT_MEMBERSHIP,
filters: [],
pagination: { after: 'AFTER_CURSOR' },
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
membership: DEFAULT_MEMBERSHIP,
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
after: 'AFTER_CURSOR',
first: RUNNER_PAGE_SIZE,
},
@@ -231,11 +245,11 @@ export const mockSearchExamples = [
membership: DEFAULT_MEMBERSHIP,
filters: [],
pagination: { before: 'BEFORE_CURSOR' },
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
membership: DEFAULT_MEMBERSHIP,
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
before: 'BEFORE_CURSOR',
last: RUNNER_PAGE_SIZE,
},
@@ -243,24 +257,24 @@ export const mockSearchExamples = [
{
name: 'the next page filtered by a status, an instance type, tags and a non default sort',
urlQuery:
- '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&tag[]=tag-1&tag[]=tag-2&sort=CREATED_ASC&after=AFTER_CURSOR',
+ '?status[]=ONLINE&runner_type[]=INSTANCE_TYPE&tag[]=tag-1&tag[]=tag-2&sort=CREATED_ASC&after=AFTER_CURSOR',
search: {
- runnerType: 'INSTANCE_TYPE',
+ runnerType: INSTANCE_TYPE,
membership: DEFAULT_MEMBERSHIP,
filters: [
- { type: 'status', value: { data: 'ACTIVE', operator: '=' } },
+ { type: 'status', value: { data: STATUS_ONLINE, operator: '=' } },
{ type: 'tag', value: { data: 'tag-1', operator: '=' } },
{ type: 'tag', value: { data: 'tag-2', operator: '=' } },
],
pagination: { after: 'AFTER_CURSOR' },
- sort: 'CREATED_ASC',
+ sort: CREATED_ASC,
},
graphqlVariables: {
- status: 'ACTIVE',
- type: 'INSTANCE_TYPE',
+ status: STATUS_ONLINE,
+ type: INSTANCE_TYPE,
membership: DEFAULT_MEMBERSHIP,
tagList: ['tag-1', 'tag-2'],
- sort: 'CREATED_ASC',
+ sort: CREATED_ASC,
after: 'AFTER_CURSOR',
first: RUNNER_PAGE_SIZE,
},
@@ -273,12 +287,12 @@ export const mockSearchExamples = [
membership: DEFAULT_MEMBERSHIP,
filters: [{ type: 'paused', value: { data: 'true', operator: '=' } }],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
paused: true,
membership: DEFAULT_MEMBERSHIP,
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
},
},
@@ -290,12 +304,12 @@ export const mockSearchExamples = [
membership: DEFAULT_MEMBERSHIP,
filters: [{ type: 'paused', value: { data: 'false', operator: '=' } }],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
paused: false,
membership: DEFAULT_MEMBERSHIP,
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
},
},
@@ -304,12 +318,14 @@ export const mockSearchExamples = [
export const onlineContactTimeoutSecs = 2 * 60 * 60;
export const staleTimeoutSecs = 7889238; // Ruby's `3.months`
+export const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
+export const mockAuthenticationToken = 'MOCK_AUTHENTICATION_TOKEN';
+
export const newRunnerPath = '/runners/new';
-export const emptyStateSvgPath = 'emptyStateSvgPath.svg';
-export const emptyStateFilteredSvgPath = 'emptyStateFilteredSvgPath.svg';
export {
allRunnersData,
+ allRunnersWithCreatorData,
allRunnersDataPaginated,
runnersCountData,
groupRunnersData,
@@ -321,4 +337,6 @@ export {
runnerProjectsData,
runnerJobsData,
runnerFormData,
+ runnerCreateResult,
+ runnerForRegistration,
};
diff --git a/spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js b/spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js
new file mode 100644
index 00000000000..5bfbbfdc074
--- /dev/null
+++ b/spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js
@@ -0,0 +1,125 @@
+import { GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
+
+import ProjectRunnerRunnerApp from '~/ci/runner/project_new_runner/project_new_runner_app.vue';
+import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
+import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue';
+import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
+import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
+import {
+ PARAM_KEY_PLATFORM,
+ PROJECT_TYPE,
+ DEFAULT_PLATFORM,
+ WINDOWS_PLATFORM,
+} from '~/ci/runner/constants';
+import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import { runnerCreateResult, mockRegistrationToken } from '../mock_data';
+
+const mockProjectId = 'gid://gitlab/Project/72';
+
+jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
+jest.mock('~/alert');
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ redirectTo: jest.fn(),
+}));
+
+const mockCreatedRunner = runnerCreateResult.data.runnerCreate.runner;
+
+describe('ProjectRunnerRunnerApp', () => {
+ let wrapper;
+
+ const findRunnerPlatformsRadioGroup = () => wrapper.findComponent(RunnerPlatformsRadioGroup);
+ const findRegistrationFeedbackBanner = () => wrapper.findComponent(RegistrationFeedbackBanner);
+ const findRegistrationCompatibilityAlert = () =>
+ wrapper.findComponent(RegistrationCompatibilityAlert);
+ const findRunnerCreateForm = () => wrapper.findComponent(RunnerCreateForm);
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ProjectRunnerRunnerApp, {
+ propsData: {
+ projectId: mockProjectId,
+ legacyRegistrationToken: mockRegistrationToken,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows a registration feedback banner', () => {
+ expect(findRegistrationFeedbackBanner().exists()).toBe(true);
+ });
+
+ it('shows a registration compatibility alert', () => {
+ expect(findRegistrationCompatibilityAlert().props('alertKey')).toBe(mockProjectId);
+ });
+
+ describe('Platform', () => {
+ it('shows the platforms radio group', () => {
+ expect(findRunnerPlatformsRadioGroup().props('value')).toBe(DEFAULT_PLATFORM);
+ });
+ });
+
+ describe('Runner form', () => {
+ it('shows the runner create form for an instance runner', () => {
+ expect(findRunnerCreateForm().props()).toEqual({
+ runnerType: PROJECT_TYPE,
+ projectId: mockProjectId,
+ groupId: null,
+ });
+ });
+
+ describe('When a runner is saved', () => {
+ beforeEach(() => {
+ findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner);
+ });
+
+ it('pushes an alert to be shown after redirection', () => {
+ expect(saveAlertToLocalStorage).toHaveBeenCalledWith({
+ message: s__('Runners|Runner created.'),
+ variant: VARIANT_SUCCESS,
+ });
+ });
+
+ it('redirects to the registration page', () => {
+ const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${DEFAULT_PLATFORM}`;
+
+ expect(redirectTo).toHaveBeenCalledWith(url); // eslint-disable-line import/no-deprecated
+ });
+ });
+
+ describe('When another platform is selected and a runner is saved', () => {
+ beforeEach(() => {
+ findRunnerPlatformsRadioGroup().vm.$emit('input', WINDOWS_PLATFORM);
+ findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner);
+ });
+
+ it('redirects to the registration page with the platform', () => {
+ const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`;
+
+ expect(redirectTo).toHaveBeenCalledWith(url); // eslint-disable-line import/no-deprecated
+ });
+ });
+
+ describe('When runner fails to save', () => {
+ const ERROR_MSG = 'Cannot save!';
+
+ beforeEach(() => {
+ findRunnerCreateForm().vm.$emit('error', new Error(ERROR_MSG));
+ });
+
+ it('shows an error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: ERROR_MSG });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/project_register_runner_app/project_register_runner_app_spec.js b/spec/frontend/ci/runner/project_register_runner_app/project_register_runner_app_spec.js
new file mode 100644
index 00000000000..240fd82fb3b
--- /dev/null
+++ b/spec/frontend/ci/runner/project_register_runner_app/project_register_runner_app_spec.js
@@ -0,0 +1,120 @@
+import { nextTick } from 'vue';
+import { GlButton } from '@gitlab/ui';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { TEST_HOST } from 'helpers/test_constants';
+
+import { updateHistory } from '~/lib/utils/url_utility';
+import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM, WINDOWS_PLATFORM } from '~/ci/runner/constants';
+import ProjectRegisterRunnerApp from '~/ci/runner/project_register_runner/project_register_runner_app.vue';
+import RegistrationInstructions from '~/ci/runner/components/registration/registration_instructions.vue';
+import PlatformsDrawer from '~/ci/runner/components/registration/platforms_drawer.vue';
+import { runnerForRegistration } from '../mock_data';
+
+const mockRunnerId = runnerForRegistration.data.runner.id;
+const mockRunnersPath = '/group1/project1/-/settings/ci_cd';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ updateHistory: jest.fn(),
+}));
+
+describe('ProjectRegisterRunnerApp', () => {
+ let wrapper;
+
+ const findRegistrationInstructions = () => wrapper.findComponent(RegistrationInstructions);
+ const findPlatformsDrawer = () => wrapper.findComponent(PlatformsDrawer);
+ const findBtn = () => wrapper.findComponent(GlButton);
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ProjectRegisterRunnerApp, {
+ propsData: {
+ runnerId: mockRunnerId,
+ runnersPath: mockRunnersPath,
+ },
+ });
+ };
+
+ describe('When showing runner details', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('when runner token is available', () => {
+ it('shows registration instructions', () => {
+ expect(findRegistrationInstructions().props()).toEqual({
+ platform: DEFAULT_PLATFORM,
+ runnerId: mockRunnerId,
+ });
+ });
+
+ it('configures platform drawer', () => {
+ expect(findPlatformsDrawer().props()).toEqual({
+ open: false,
+ platform: DEFAULT_PLATFORM,
+ });
+ });
+
+ it('shows runner list button', () => {
+ expect(findBtn().attributes('href')).toBe(mockRunnersPath);
+ expect(findBtn().props('variant')).toBe('confirm');
+ });
+ });
+ });
+
+ describe('When another platform has been selected', () => {
+ beforeEach(() => {
+ setWindowLocation(`?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`);
+
+ createComponent();
+ });
+
+ it('shows registration instructions for the platform', () => {
+ expect(findRegistrationInstructions().props('platform')).toBe(WINDOWS_PLATFORM);
+ });
+ });
+
+ describe('When opening install instructions', () => {
+ beforeEach(() => {
+ createComponent();
+
+ findRegistrationInstructions().vm.$emit('toggleDrawer');
+ });
+
+ it('opens platform drawer', () => {
+ expect(findPlatformsDrawer().props('open')).toBe(true);
+ });
+
+ it('closes platform drawer', async () => {
+ findRegistrationInstructions().vm.$emit('toggleDrawer');
+ await nextTick();
+
+ expect(findPlatformsDrawer().props('open')).toBe(false);
+ });
+
+ it('closes platform drawer from drawer', async () => {
+ findPlatformsDrawer().vm.$emit('close');
+ await nextTick();
+
+ expect(findPlatformsDrawer().props('open')).toBe(false);
+ });
+
+ describe('when selecting a platform', () => {
+ beforeEach(() => {
+ findPlatformsDrawer().vm.$emit('selectPlatform', WINDOWS_PLATFORM);
+ });
+
+ it('updates the url', () => {
+ expect(updateHistory).toHaveBeenCalledTimes(1);
+ expect(updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`,
+ });
+ });
+
+ it('updates the registration instructions', () => {
+ expect(findRegistrationInstructions().props('platform')).toBe(WINDOWS_PLATFORM);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js b/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js
index a9369a5e626..79bbf95f8f0 100644
--- a/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js
+++ b/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerHeader from '~/ci/runner/components/runner_header.vue';
@@ -15,7 +15,7 @@ import { I18N_STATUS_NEVER_CONTACTED, I18N_INSTANCE_TYPE } from '~/ci/runner/con
import { runnerFormData } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
const mockRunner = runnerFormData.data.runner;
@@ -51,7 +51,6 @@ describe('RunnerEditApp', () => {
afterEach(() => {
mockRunnerQuery.mockReset();
- wrapper.destroy();
});
it('expect GraphQL ID to be requested', async () => {
diff --git a/spec/frontend/ci/runner/runner_search_utils_spec.js b/spec/frontend/ci/runner/runner_search_utils_spec.js
index f64b89d47fd..9a4a6139198 100644
--- a/spec/frontend/ci/runner/runner_search_utils_spec.js
+++ b/spec/frontend/ci/runner/runner_search_utils_spec.js
@@ -7,6 +7,7 @@ import {
isSearchFiltered,
} from 'ee_else_ce/ci/runner/runner_search_utils';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
+import { DEFAULT_SORT } from '~/ci/runner/constants';
import { mockSearchExamples } from './mock_data';
describe('search_params.js', () => {
@@ -68,7 +69,7 @@ describe('search_params.js', () => {
'http://test.host/?paused[]=true',
'http://test.host/?search=my_text',
])('When a filter is removed, it is removed from the URL', (initialUrl) => {
- const search = { filters: [], sort: 'CREATED_DESC' };
+ const search = { filters: [], sort: DEFAULT_SORT };
const expectedUrl = `http://test.host/`;
expect(fromSearchToUrl(search, initialUrl)).toBe(expectedUrl);
@@ -76,7 +77,7 @@ describe('search_params.js', () => {
it('When unrelated search parameter is present, it does not get removed', () => {
const initialUrl = `http://test.host/?unrelated=UNRELATED&status[]=ACTIVE`;
- const search = { filters: [], sort: 'CREATED_DESC' };
+ const search = { filters: [], sort: DEFAULT_SORT };
const expectedUrl = `http://test.host/?unrelated=UNRELATED`;
expect(fromSearchToUrl(search, initialUrl)).toBe(expectedUrl);
diff --git a/spec/frontend/ci/runner/sentry_utils_spec.js b/spec/frontend/ci/runner/sentry_utils_spec.js
index f7b689272ce..2f17cc43ac5 100644
--- a/spec/frontend/ci/runner/sentry_utils_spec.js
+++ b/spec/frontend/ci/runner/sentry_utils_spec.js
@@ -6,7 +6,7 @@ jest.mock('@sentry/browser');
describe('~/ci/runner/sentry_utils', () => {
let mockSetTag;
- beforeEach(async () => {
+ beforeEach(() => {
mockSetTag = jest.fn();
Sentry.withScope.mockImplementation((fn) => {