Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/ci')
-rw-r--r--spec/frontend/ci/admin/jobs_table/components/cells/runner_cell_spec.js26
-rw-r--r--spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js32
-rw-r--r--spec/frontend/ci/catalog/components/ci_catalog_home_spec.js46
-rw-r--r--spec/frontend/ci/catalog/components/details/ci_resource_about_spec.js120
-rw-r--r--spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js113
-rw-r--r--spec/frontend/ci/catalog/components/details/ci_resource_details_spec.js83
-rw-r--r--spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js139
-rw-r--r--spec/frontend/ci/catalog/components/details/ci_resource_readme_spec.js96
-rw-r--r--spec/frontend/ci/catalog/components/list/catalog_header_spec.js86
-rw-r--r--spec/frontend/ci/catalog/components/list/catalog_list_skeleton_loader_spec.js22
-rw-r--r--spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js198
-rw-r--r--spec/frontend/ci/catalog/components/list/ci_resources_list_spec.js143
-rw-r--r--spec/frontend/ci/catalog/components/list/empty_state_spec.js27
-rw-r--r--spec/frontend/ci/catalog/components/pages/ci_resource_details_page_spec.js186
-rw-r--r--spec/frontend/ci/catalog/mock.js546
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js118
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js111
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js41
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js39
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js2
-rw-r--r--spec/frontend/ci/ci_variable_list/mocks.js3
-rw-r--r--spec/frontend/ci/ci_variable_list/utils_spec.js53
-rw-r--r--spec/frontend/ci/common/pipelines_table_spec.js241
-rw-r--r--spec/frontend/ci/job_details/components/job_header_spec.js37
-rw-r--r--spec/frontend/ci/job_details/components/log/collapsible_section_spec.js28
-rw-r--r--spec/frontend/ci/job_details/components/log/line_header_spec.js2
-rw-r--r--spec/frontend/ci/job_details/components/log/line_number_spec.js2
-rw-r--r--spec/frontend/ci/job_details/components/log/line_spec.js2
-rw-r--r--spec/frontend/ci/job_details/components/log/log_spec.js33
-rw-r--r--spec/frontend/ci/job_details/components/log/mock_data.js65
-rw-r--r--spec/frontend/ci/job_details/components/sidebar/artifacts_block_spec.js16
-rw-r--r--spec/frontend/ci/job_details/components/sidebar/sidebar_header_spec.js6
-rw-r--r--spec/frontend/ci/job_details/components/sidebar/sidebar_job_details_container_spec.js3
-rw-r--r--spec/frontend/ci/job_details/job_app_spec.js2
-rw-r--r--spec/frontend/ci/job_details/store/actions_spec.js25
-rw-r--r--spec/frontend/ci/job_details/store/mutations_spec.js22
-rw-r--r--spec/frontend/ci/job_details/store/utils_spec.js67
-rw-r--r--spec/frontend/ci/jobs_page/components/job_cells/job_cell_spec.js8
-rw-r--r--spec/frontend/ci/jobs_page/components/job_cells/status_cell_spec.js (renamed from spec/frontend/ci/jobs_page/components/job_cells/duration_cell_spec.js)4
-rw-r--r--spec/frontend/ci/jobs_page/components/jobs_table_empty_state_spec.js2
-rw-r--r--spec/frontend/ci/jobs_page/components/jobs_table_spec.js7
-rw-r--r--spec/frontend/ci/pipeline_details/graph/components/job_item_spec.js26
-rw-r--r--spec/frontend/ci/pipeline_details/graph/components/linked_pipeline_spec.js2
-rw-r--r--spec/frontend/ci/pipeline_details/mock_data.js6
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js15
-rw-r--r--spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mini_list_spec.js26
-rw-r--r--spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js8
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_empty_state_spec.js37
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js73
-rw-r--r--spec/frontend/ci/pipeline_schedules/mock_data.js27
-rw-r--r--spec/frontend/ci/pipelines_page/components/pipeline_labels_spec.js40
-rw-r--r--spec/frontend/ci/pipelines_page/components/pipeline_operations_spec.js69
-rw-r--r--spec/frontend/ci/pipelines_page/components/pipeline_stop_modal_spec.js42
-rw-r--r--spec/frontend/ci/pipelines_page/pipelines_spec.js101
-rw-r--r--spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js5
-rw-r--r--spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_details_spec.js7
-rw-r--r--spec/frontend/ci/runner/components/runner_details_tabs_spec.js14
-rw-r--r--spec/frontend/ci/runner/components/runner_list_spec.js28
-rw-r--r--spec/frontend/ci/runner/components/runner_type_icon_spec.js67
-rw-r--r--spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js5
-rw-r--r--spec/frontend/ci/runner/sentry_utils_spec.js21
63 files changed, 2760 insertions, 669 deletions
diff --git a/spec/frontend/ci/admin/jobs_table/components/cells/runner_cell_spec.js b/spec/frontend/ci/admin/jobs_table/components/cells/runner_cell_spec.js
index 2f1dae71572..c9758c5ab24 100644
--- a/spec/frontend/ci/admin/jobs_table/components/cells/runner_cell_spec.js
+++ b/spec/frontend/ci/admin/jobs_table/components/cells/runner_cell_spec.js
@@ -1,5 +1,6 @@
import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import RunnerTypeIcon from '~/ci/runner/components/runner_type_icon.vue';
import RunnerCell from '~/ci/admin/jobs_table/components/cells/runner_cell.vue';
import { RUNNER_EMPTY_TEXT } from '~/ci/admin/jobs_table/constants';
import { allRunnersData } from 'jest/ci/runner/mock_data';
@@ -61,4 +62,29 @@ describe('Runner Cell', () => {
});
});
});
+
+ describe('Runner Type Icon', () => {
+ const findRunnerTypeIcon = () => wrapper.findComponent(RunnerTypeIcon);
+
+ describe('Job with runner', () => {
+ beforeEach(() => {
+ createComponent({ job: mockJobWithRunner });
+ });
+
+ it('shows the runner type icon', () => {
+ expect(findRunnerTypeIcon().exists()).toBe(true);
+ expect(findRunnerTypeIcon().props('type')).toBe(mockJobWithRunner.runner.runnerType);
+ });
+ });
+
+ describe('Job without runner', () => {
+ beforeEach(() => {
+ createComponent({ job: mockJobWithoutRunner });
+ });
+
+ it('does not show the runner type icon', () => {
+ expect(findRunnerTypeIcon().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
index 1cbb1a714c9..3628af31aa1 100644
--- a/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js
+++ b/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js
@@ -1,16 +1,8 @@
-import {
- GlLoadingIcon,
- GlTable,
- GlLink,
- GlBadge,
- GlPagination,
- GlModal,
- GlFormCheckbox,
-} from '@gitlab/ui';
+import { GlLoadingIcon, GlTable, GlLink, 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 CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import waitForPromises from 'helpers/wait_for_promises';
import JobArtifactsTable from '~/ci/artifacts/components/job_artifacts_table.vue';
import ArtifactsTableRowDetails from '~/ci/artifacts/components/artifacts_table_row_details.vue';
@@ -59,13 +51,13 @@ describe('JobArtifactsTable component', () => {
const findStatuses = () => wrapper.findAllByTestId('job-artifacts-job-status');
const findSuccessfulJobStatus = () => findStatuses().at(0);
- const findFailedJobStatus = () => findStatuses().at(1);
+ const findCiBadgeLink = () => findSuccessfulJobStatus().findComponent(CiBadgeLink);
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 findCommitLink = () => findLinks().at(2);
+ const findRefLink = () => findLinks().at(3);
const findSize = () => wrapper.findByTestId('job-artifacts-size');
const findCreated = () => wrapper.findByTestId('job-artifacts-created');
@@ -209,13 +201,13 @@ describe('JobArtifactsTable component', () => {
});
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);
+ expect(findCiBadgeLink().props()).toMatchObject({
+ status: {
+ group: 'success',
+ },
+ size: 'sm',
+ showText: false,
+ });
});
it('shows links to the job, pipeline, ref, and commit', () => {
diff --git a/spec/frontend/ci/catalog/components/ci_catalog_home_spec.js b/spec/frontend/ci/catalog/components/ci_catalog_home_spec.js
new file mode 100644
index 00000000000..1b5c86c19cb
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/ci_catalog_home_spec.js
@@ -0,0 +1,46 @@
+import { shallowMount } from '@vue/test-utils';
+import { createRouter } from '~/ci/catalog/router';
+import ciResourceDetailsPage from '~/ci/catalog/components/pages/ci_resource_details_page.vue';
+import CiCatalogHome from '~/ci/catalog/components/ci_catalog_home.vue';
+
+describe('CiCatalogHome', () => {
+ const defaultProps = {};
+ const baseRoute = '/';
+ const resourcesPageComponentStub = {
+ name: 'page-component',
+ template: '<div>Hello</div>',
+ };
+ const router = createRouter(baseRoute, resourcesPageComponentStub);
+
+ const createComponent = ({ props = {} } = {}) => {
+ shallowMount(CiCatalogHome, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ router,
+ });
+ };
+
+ describe('when mounted', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('router', () => {
+ it.each`
+ path | component
+ ${baseRoute} | ${resourcesPageComponentStub}
+ ${'/1'} | ${ciResourceDetailsPage}
+ `('when route is $path it renders the right component', async ({ path, component }) => {
+ if (path !== '/') {
+ await router.push(path);
+ }
+
+ const [root] = router.currentRoute.matched;
+
+ expect(root.components.default).toBe(component);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/components/details/ci_resource_about_spec.js b/spec/frontend/ci/catalog/components/details/ci_resource_about_spec.js
new file mode 100644
index 00000000000..658a135534b
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/details/ci_resource_about_spec.js
@@ -0,0 +1,120 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import CiResourceAbout from '~/ci/catalog/components/details/ci_resource_about.vue';
+import { formatDate } from '~/lib/utils/datetime_utility';
+
+describe('CiResourceAbout', () => {
+ let wrapper;
+
+ const defaultProps = {
+ isLoadingSharedData: false,
+ isLoadingDetails: false,
+ openIssuesCount: 4,
+ openMergeRequestsCount: 9,
+ latestVersion: {
+ id: 1,
+ tagName: 'v1.0.0',
+ tagPath: 'path/to/release',
+ releasedAt: '2022-08-23T17:19:09Z',
+ },
+ webPath: 'path/to/project',
+ };
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMountExtended(CiResourceAbout, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ const findProjectLink = () => wrapper.findByText('Go to the project');
+ const findIssueCount = () => wrapper.findByText(`${defaultProps.openIssuesCount} issues`);
+ const findMergeRequestCount = () =>
+ wrapper.findByText(`${defaultProps.openMergeRequestsCount} merge requests`);
+ const findLastRelease = () =>
+ wrapper.findByText(
+ `Last release at ${formatDate(defaultProps.latestVersion.releasedAt, 'yyyy-mm-dd')}`,
+ );
+ const findAllLoadingItems = () => wrapper.findAllByTestId('skeleton-loading-line');
+
+ // Shared data items are items which gets their data from the index page query.
+ const sharedDataItems = [findProjectLink, findLastRelease];
+ // additional details items gets their state only when on the details page
+ const additionalDetailsItems = [findIssueCount, findMergeRequestCount];
+ const allItems = [...sharedDataItems, ...additionalDetailsItems];
+
+ describe('when loading shared data', () => {
+ beforeEach(() => {
+ createComponent({ props: { isLoadingSharedData: true, isLoadingDetails: true } });
+ });
+
+ it('renders all server-side data as loading', () => {
+ allItems.forEach((finder) => {
+ expect(finder().exists()).toBe(false);
+ });
+
+ expect(findAllLoadingItems()).toHaveLength(allItems.length);
+ });
+ });
+
+ describe('when loading additional details', () => {
+ beforeEach(() => {
+ createComponent({ props: { isLoadingDetails: true } });
+ });
+
+ it('renders only the details query as loading', () => {
+ sharedDataItems.forEach((finder) => {
+ expect(finder().exists()).toBe(true);
+ });
+
+ additionalDetailsItems.forEach((finder) => {
+ expect(finder().exists()).toBe(false);
+ });
+
+ expect(findAllLoadingItems()).toHaveLength(additionalDetailsItems.length);
+ });
+ });
+
+ describe('when has loaded', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders project link', () => {
+ expect(findProjectLink().exists()).toBe(true);
+ });
+
+ it('renders the number of issues opened', () => {
+ expect(findIssueCount().exists()).toBe(true);
+ });
+
+ it('renders the number of merge requests opened', () => {
+ expect(findMergeRequestCount().exists()).toBe(true);
+ });
+
+ it('renders the last release date', () => {
+ expect(findLastRelease().exists()).toBe(true);
+ });
+
+ describe('links', () => {
+ it('has the correct project link', () => {
+ expect(findProjectLink().attributes('href')).toBe(defaultProps.webPath);
+ });
+
+ it('has the correct issues link', () => {
+ expect(findIssueCount().attributes('href')).toBe(`${defaultProps.webPath}/issues`);
+ });
+
+ it('has the correct merge request link', () => {
+ expect(findMergeRequestCount().attributes('href')).toBe(
+ `${defaultProps.webPath}/merge_requests`,
+ );
+ });
+
+ it('has no link for release data', () => {
+ expect(findLastRelease().attributes('href')).toBe(undefined);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js b/spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js
new file mode 100644
index 00000000000..a41996d20b3
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js
@@ -0,0 +1,113 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { resolvers } from '~/ci/catalog/graphql/settings';
+import CiResourceComponents from '~/ci/catalog/components/details/ci_resource_components.vue';
+import getCiCatalogcomponentComponents from '~/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
+import { mockComponents } from '../../mock';
+
+Vue.use(VueApollo);
+jest.mock('~/alert');
+
+describe('CiResourceComponents', () => {
+ let wrapper;
+ let mockComponentsResponse;
+
+ const components = mockComponents.data.ciCatalogResource.components.nodes;
+
+ const resourceId = 'gid://gitlab/Ci::Catalog::Resource/1';
+
+ const defaultProps = { resourceId };
+
+ const createComponent = async () => {
+ const handlers = [[getCiCatalogcomponentComponents, mockComponentsResponse]];
+ const mockApollo = createMockApollo(handlers, resolvers);
+
+ wrapper = mountExtended(CiResourceComponents, {
+ propsData: {
+ ...defaultProps,
+ },
+ apolloProvider: mockApollo,
+ });
+
+ await waitForPromises();
+ };
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findComponents = () => wrapper.findAllByTestId('component-section');
+
+ beforeEach(() => {
+ mockComponentsResponse = jest.fn();
+ mockComponentsResponse.mockResolvedValue(mockComponents);
+ });
+
+ describe('when queries are loading', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('render a loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('does not render components', () => {
+ expect(findComponents()).toHaveLength(0);
+ });
+
+ it('does not throw an error', () => {
+ expect(createAlert).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when components query throws an error', () => {
+ beforeEach(async () => {
+ mockComponentsResponse.mockRejectedValue();
+ await createComponent();
+ });
+
+ it('calls createAlert with the correct message', () => {
+ expect(createAlert).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalledWith({
+ message: "There was an error fetching this resource's components",
+ });
+ });
+
+ it('does not render the loading state', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('when queries have loaded', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
+
+ it('renders every component', () => {
+ expect(findComponents()).toHaveLength(components.length);
+ });
+
+ it('renders the component name, description and snippet', () => {
+ components.forEach((component) => {
+ expect(wrapper.text()).toContain(component.name);
+ expect(wrapper.text()).toContain(component.description);
+ expect(wrapper.text()).toContain(component.path);
+ });
+ });
+
+ describe('inputs', () => {
+ it('renders the component parameter attributes', () => {
+ const [firstComponent] = components;
+
+ firstComponent.inputs.nodes.forEach((input) => {
+ expect(findComponents().at(0).text()).toContain(input.name);
+ expect(findComponents().at(0).text()).toContain(input.defaultValue);
+ expect(findComponents().at(0).text()).toContain('Yes');
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/components/details/ci_resource_details_spec.js b/spec/frontend/ci/catalog/components/details/ci_resource_details_spec.js
new file mode 100644
index 00000000000..1f7dcf9d4e5
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/details/ci_resource_details_spec.js
@@ -0,0 +1,83 @@
+import { GlTabs, GlTab } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import CiResourceComponents from '~/ci/catalog/components/details/ci_resource_components.vue';
+import CiResourceDetails from '~/ci/catalog/components/details/ci_resource_details.vue';
+import CiResourceReadme from '~/ci/catalog/components/details/ci_resource_readme.vue';
+
+describe('CiResourceDetails', () => {
+ let wrapper;
+
+ const defaultProps = {
+ resourceId: 'gid://gitlab/Ci::Catalog::Resource/1',
+ };
+ const defaultProvide = {
+ glFeatures: { ciCatalogComponentsTab: true },
+ };
+
+ const createComponent = ({ provide = {}, props = {} } = {}) => {
+ wrapper = shallowMount(CiResourceDetails, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ provide: {
+ ...defaultProvide,
+ ...provide,
+ },
+ stubs: {
+ GlTabs,
+ },
+ });
+ };
+ const findAllTabs = () => wrapper.findAllComponents(GlTab);
+ const findCiResourceReadme = () => wrapper.findComponent(CiResourceReadme);
+ const findCiResourceComponents = () => wrapper.findComponent(CiResourceComponents);
+
+ describe('tabs', () => {
+ describe('when feature flag `ci_catalog_components_tab` is enabled', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the readme and components tab', () => {
+ expect(findAllTabs()).toHaveLength(2);
+ expect(findCiResourceComponents().exists()).toBe(true);
+ expect(findCiResourceReadme().exists()).toBe(true);
+ });
+ });
+
+ describe('when feature flag `ci_catalog_components_tab` is disabled', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: { glFeatures: { ciCatalogComponentsTab: false } },
+ });
+ });
+
+ it('renders only readme tab as default', () => {
+ expect(findCiResourceReadme().exists()).toBe(true);
+ expect(findCiResourceComponents().exists()).toBe(false);
+ expect(findAllTabs()).toHaveLength(1);
+ });
+ });
+
+ describe('UI', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('passes lazy attribute to all tabs', () => {
+ findAllTabs().wrappers.forEach((tab) => {
+ expect(tab.attributes().lazy).not.toBeUndefined();
+ });
+ });
+
+ it('passes the right props to the readme component', () => {
+ expect(findCiResourceReadme().props().resourceId).toBe(defaultProps.resourceId);
+ });
+
+ it('passes the right props to the components tab', () => {
+ expect(findCiResourceComponents().props().resourceId).toBe(defaultProps.resourceId);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js b/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js
new file mode 100644
index 00000000000..6ab9520508d
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js
@@ -0,0 +1,139 @@
+import { GlAvatar, GlAvatarLink, GlBadge } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import CiResourceHeader from '~/ci/catalog/components/details/ci_resource_header.vue';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiResourceAbout from '~/ci/catalog/components/details/ci_resource_about.vue';
+import { catalogSharedDataMock, catalogAdditionalDetailsMock } from '../../mock';
+
+describe('CiResourceHeader', () => {
+ let wrapper;
+
+ const resource = { ...catalogSharedDataMock.data.ciCatalogResource };
+ const resourceAdditionalData = { ...catalogAdditionalDetailsMock.data.ciCatalogResource };
+
+ const defaultProps = {
+ openIssuesCount: resourceAdditionalData.openIssuesCount,
+ openMergeRequestsCount: resourceAdditionalData.openMergeRequestsCount,
+ isLoadingDetails: false,
+ isLoadingSharedData: false,
+ resource,
+ };
+
+ const findAboutComponent = () => wrapper.findComponent(CiResourceAbout);
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
+ const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
+ const findVersionBadge = () => wrapper.findComponent(GlBadge);
+ const findPipelineStatusBadge = () => wrapper.findComponent(CiBadgeLink);
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMountExtended(CiResourceHeader, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ describe('when mounted', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the project name and description', () => {
+ expect(wrapper.html()).toContain(resource.name);
+ expect(wrapper.html()).toContain(resource.description);
+ });
+
+ it('renders the namespace and project path', () => {
+ expect(wrapper.html()).toContain(resource.rootNamespace.fullPath);
+ expect(wrapper.html()).toContain(resource.rootNamespace.name);
+ });
+
+ it('renders the avatar', () => {
+ const { id, name } = resource;
+
+ expect(findAvatar().exists()).toBe(true);
+ expect(findAvatarLink().exists()).toBe(true);
+ expect(findAvatar().props()).toMatchObject({
+ entityId: getIdFromGraphQLId(id),
+ entityName: name,
+ });
+ });
+
+ it('renders the catalog about section and passes props', () => {
+ expect(findAboutComponent().exists()).toBe(true);
+ expect(findAboutComponent().props()).toEqual({
+ isLoadingDetails: false,
+ isLoadingSharedData: false,
+ openIssuesCount: defaultProps.openIssuesCount,
+ openMergeRequestsCount: defaultProps.openMergeRequestsCount,
+ latestVersion: resource.latestVersion,
+ webPath: resource.webPath,
+ });
+ });
+ });
+
+ describe('Version badge', () => {
+ describe('without a version', () => {
+ beforeEach(() => {
+ createComponent({ props: { resource: { ...resource, latestVersion: null } } });
+ });
+
+ it('does not render', () => {
+ expect(findVersionBadge().exists()).toBe(false);
+ });
+ });
+
+ describe('with a version', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders', () => {
+ expect(findVersionBadge().exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('when the project has a release', () => {
+ const pipelineStatus = {
+ detailsPath: 'path/to/pipeline',
+ icon: 'status_success',
+ text: 'passed',
+ group: 'success',
+ };
+
+ describe.each`
+ hasPipelineBadge | describeText | testText | status
+ ${true} | ${'is'} | ${'renders'} | ${pipelineStatus}
+ ${false} | ${'is not'} | ${'does not render'} | ${{}}
+ `('and there $describeText a pipeline', ({ hasPipelineBadge, testText, status }) => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ pipelineStatus: status,
+ latestVersion: { tagName: '1.0.0', tagPath: 'path/to/release' },
+ },
+ });
+ });
+
+ it('renders the version badge', () => {
+ expect(findVersionBadge().exists()).toBe(true);
+ });
+
+ it(`${testText} the pipeline status badge`, () => {
+ expect(findPipelineStatusBadge().exists()).toBe(hasPipelineBadge);
+ if (hasPipelineBadge) {
+ expect(findPipelineStatusBadge().props()).toEqual({
+ showText: true,
+ size: 'sm',
+ status: pipelineStatus,
+ showTooltip: true,
+ useLink: true,
+ });
+ }
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/components/details/ci_resource_readme_spec.js b/spec/frontend/ci/catalog/components/details/ci_resource_readme_spec.js
new file mode 100644
index 00000000000..0dadac236a8
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/details/ci_resource_readme_spec.js
@@ -0,0 +1,96 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import CiResourceReadme from '~/ci/catalog/components/details/ci_resource_readme.vue';
+import getCiCatalogResourceReadme from '~/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
+
+jest.mock('~/alert');
+
+Vue.use(VueApollo);
+
+const readmeHtml = '<h1>This is a readme file</h1>';
+const resourceId = 'gid://gitlab/Ci::Catalog::Resource/1';
+
+describe('CiResourceReadme', () => {
+ let wrapper;
+ let mockReadmeResponse;
+
+ const readmeMockData = {
+ data: {
+ ciCatalogResource: {
+ id: resourceId,
+ readmeHtml,
+ },
+ },
+ };
+
+ const defaultProps = { resourceId };
+
+ const createComponent = ({ props = {} } = {}) => {
+ const handlers = [[getCiCatalogResourceReadme, mockReadmeResponse]];
+
+ wrapper = shallowMountExtended(CiResourceReadme, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ apolloProvider: createMockApollo(handlers),
+ });
+ };
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+
+ beforeEach(() => {
+ mockReadmeResponse = jest.fn();
+ });
+
+ describe('when loading', () => {
+ beforeEach(() => {
+ mockReadmeResponse.mockResolvedValue(readmeMockData);
+ createComponent();
+ });
+
+ it('renders only a loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(wrapper.html()).not.toContain(readmeHtml);
+ });
+ });
+
+ describe('when mounted', () => {
+ beforeEach(async () => {
+ mockReadmeResponse.mockResolvedValue(readmeMockData);
+
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('renders only the received HTML', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(wrapper.html()).toContain(readmeHtml);
+ });
+
+ it('does not render an error', () => {
+ expect(createAlert).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when there is an error loading the readme', () => {
+ beforeEach(async () => {
+ mockReadmeResponse.mockRejectedValue({ errors: [] });
+
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('calls the createAlert function to show an error', () => {
+ expect(createAlert).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalledWith({
+ message: "There was a problem loading this project's readme content.",
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/components/list/catalog_header_spec.js b/spec/frontend/ci/catalog/components/list/catalog_header_spec.js
new file mode 100644
index 00000000000..912fd9e1a93
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/list/catalog_header_spec.js
@@ -0,0 +1,86 @@
+import { GlBanner, GlButton } from '@gitlab/ui';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import CatalogHeader from '~/ci/catalog/components/list/catalog_header.vue';
+import { CATALOG_FEEDBACK_DISMISSED_KEY } from '~/ci/catalog/constants';
+
+describe('CatalogHeader', () => {
+ useLocalStorageSpy();
+
+ let wrapper;
+
+ const defaultProps = {};
+ const defaultProvide = {
+ pageTitle: 'Catalog page',
+ pageDescription: 'This is a nice catalog page',
+ };
+
+ const findBanner = () => wrapper.findComponent(GlBanner);
+ const findFeedbackButton = () => findBanner().findComponent(GlButton);
+ const findTitle = () => wrapper.findByText(defaultProvide.pageTitle);
+ const findDescription = () => wrapper.findByText(defaultProvide.pageDescription);
+
+ const createComponent = ({ props = {}, stubs = {} } = {}) => {
+ wrapper = shallowMountExtended(CatalogHeader, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ provide: defaultProvide,
+ stubs: {
+ ...stubs,
+ },
+ });
+ };
+
+ it('renders the Catalog title and description', () => {
+ createComponent();
+
+ expect(findTitle().exists()).toBe(true);
+ expect(findDescription().exists()).toBe(true);
+ });
+
+ describe('Feedback banner', () => {
+ describe('when user has never dismissed', () => {
+ beforeEach(() => {
+ createComponent({ stubs: { GlBanner } });
+ });
+
+ it('is visible', () => {
+ expect(findBanner().exists()).toBe(true);
+ });
+
+ it('has link to feedback issue', () => {
+ expect(findFeedbackButton().attributes().href).toBe(
+ 'https://gitlab.com/gitlab-org/gitlab/-/issues/407556',
+ );
+ });
+ });
+
+ describe('when user dismisses it', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('sets the local storage and removes the banner', async () => {
+ expect(findBanner().exists()).toBe(true);
+
+ await findBanner().vm.$emit('close');
+
+ expect(localStorage.setItem).toHaveBeenCalledWith(CATALOG_FEEDBACK_DISMISSED_KEY, 'true');
+ expect(findBanner().exists()).toBe(false);
+ });
+ });
+
+ describe('when user has dismissed it before', () => {
+ beforeEach(() => {
+ localStorage.setItem(CATALOG_FEEDBACK_DISMISSED_KEY, 'true');
+ createComponent();
+ });
+
+ it('does not show the banner', () => {
+ expect(findBanner().exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/components/list/catalog_list_skeleton_loader_spec.js b/spec/frontend/ci/catalog/components/list/catalog_list_skeleton_loader_spec.js
new file mode 100644
index 00000000000..d21fd56eb2e
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/list/catalog_list_skeleton_loader_spec.js
@@ -0,0 +1,22 @@
+import { shallowMount } from '@vue/test-utils';
+import CatalogListSkeletonLoader from '~/ci/catalog/components/list/catalog_list_skeleton_loader.vue';
+
+describe('CatalogListSkeletonLoader', () => {
+ let wrapper;
+
+ const findSkeletonLoader = () => wrapper.findComponent(CatalogListSkeletonLoader);
+
+ const createComponent = () => {
+ wrapper = shallowMount(CatalogListSkeletonLoader, {});
+ };
+
+ describe('when mounted', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders skeleton item', () => {
+ expect(findSkeletonLoader().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js b/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js
new file mode 100644
index 00000000000..7f446064366
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js
@@ -0,0 +1,198 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import { GlAvatar, GlBadge, GlButton, GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { createRouter } from '~/ci/catalog/router/index';
+import CiResourcesListItem from '~/ci/catalog/components/list/ci_resources_list_item.vue';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { CI_RESOURCE_DETAILS_PAGE_NAME } from '~/ci/catalog/router/constants';
+import { catalogSinglePageResponse } from '../../mock';
+
+Vue.use(VueRouter);
+
+let router;
+let routerPush;
+
+describe('CiResourcesListItem', () => {
+ let wrapper;
+
+ const resource = catalogSinglePageResponse.data.ciCatalogResources.nodes[0];
+ const release = {
+ author: { name: 'author', webUrl: '/user/1' },
+ releasedAt: Date.now(),
+ tagName: '1.0.0',
+ };
+ const defaultProps = {
+ resource,
+ };
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMountExtended(CiResourcesListItem, {
+ router,
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ RouterLink: true,
+ RouterView: true,
+ },
+ });
+ };
+
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
+ const findBadge = () => wrapper.findComponent(GlBadge);
+ const findResourceName = () => wrapper.findComponent(GlButton);
+ const findResourceDescription = () => wrapper.findByText(defaultProps.resource.description);
+ const findUserLink = () => wrapper.findByTestId('user-link');
+ const findTimeAgoMessage = () => wrapper.findComponent(GlSprintf);
+ const findFavorites = () => wrapper.findByTestId('stats-favorites');
+ const findForks = () => wrapper.findByTestId('stats-forks');
+
+ beforeEach(() => {
+ router = createRouter();
+ routerPush = jest.spyOn(router, 'push').mockImplementation(() => {});
+ });
+
+ describe('when mounted', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the resource avatar and passes the right props', () => {
+ const { icon, id, name } = defaultProps.resource;
+
+ expect(findAvatar().exists()).toBe(true);
+ expect(findAvatar().props()).toMatchObject({
+ entityId: getIdFromGraphQLId(id),
+ entityName: name,
+ src: icon,
+ });
+ });
+
+ it('renders the resource name button', () => {
+ expect(findResourceName().exists()).toBe(true);
+ });
+
+ it('renders the resource version badge', () => {
+ expect(findBadge().exists()).toBe(true);
+ });
+
+ it('renders the resource description', () => {
+ expect(findResourceDescription().exists()).toBe(true);
+ });
+
+ describe('release time', () => {
+ describe('when there is no release data', () => {
+ beforeEach(() => {
+ createComponent({ props: { resource: { ...resource, latestVersion: null } } });
+ });
+
+ it('does not render the release', () => {
+ expect(findTimeAgoMessage().exists()).toBe(false);
+ });
+
+ it('renders the generic `unreleased` badge', () => {
+ expect(findBadge().exists()).toBe(true);
+ expect(findBadge().text()).toBe('Unreleased');
+ });
+ });
+
+ describe('when there is release data', () => {
+ beforeEach(() => {
+ createComponent({ props: { resource: { ...resource, latestVersion: { ...release } } } });
+ });
+
+ it('renders the user link', () => {
+ expect(findUserLink().exists()).toBe(true);
+ expect(findUserLink().attributes('href')).toBe(release.author.webUrl);
+ });
+
+ it('renders the time since the resource was released', () => {
+ expect(findTimeAgoMessage().exists()).toBe(true);
+ });
+
+ it('renders the version badge', () => {
+ expect(findBadge().exists()).toBe(true);
+ expect(findBadge().text()).toBe(release.tagName);
+ });
+ });
+ });
+ });
+
+ describe('when clicking on an item title', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await findResourceName().vm.$emit('click');
+ });
+
+ it('navigates to the details page', () => {
+ expect(routerPush).toHaveBeenCalledWith({
+ name: CI_RESOURCE_DETAILS_PAGE_NAME,
+ params: {
+ id: getIdFromGraphQLId(resource.id),
+ },
+ });
+ });
+ });
+
+ describe('when clicking on an item avatar', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await findAvatar().vm.$emit('click');
+ });
+
+ it('navigates to the details page', () => {
+ expect(routerPush).toHaveBeenCalledWith({
+ name: CI_RESOURCE_DETAILS_PAGE_NAME,
+ params: {
+ id: getIdFromGraphQLId(resource.id),
+ },
+ });
+ });
+ });
+
+ describe('statistics', () => {
+ describe('when there are no statistics', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ resource: {
+ forksCount: 0,
+ starCount: 0,
+ },
+ },
+ });
+ });
+
+ it('render favorites as 0', () => {
+ expect(findFavorites().exists()).toBe(true);
+ expect(findFavorites().text()).toBe('0');
+ });
+
+ it('render forks as 0', () => {
+ expect(findForks().exists()).toBe(true);
+ expect(findForks().text()).toBe('0');
+ });
+ });
+
+ describe('where there are statistics', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('render favorites', () => {
+ expect(findFavorites().exists()).toBe(true);
+ expect(findFavorites().text()).toBe(String(defaultProps.resource.starCount));
+ });
+
+ it('render forks', () => {
+ expect(findForks().exists()).toBe(true);
+ expect(findForks().text()).toBe(String(defaultProps.resource.forksCount));
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/components/list/ci_resources_list_spec.js b/spec/frontend/ci/catalog/components/list/ci_resources_list_spec.js
new file mode 100644
index 00000000000..aca20a83979
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/list/ci_resources_list_spec.js
@@ -0,0 +1,143 @@
+import { GlKeysetPagination } from '@gitlab/ui';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import CiResourcesList from '~/ci/catalog/components/list/ci_resources_list.vue';
+import CiResourcesListItem from '~/ci/catalog/components/list/ci_resources_list_item.vue';
+import { ciCatalogResourcesItemsCount } from '~/ci/catalog/graphql/settings';
+import { catalogResponseBody, catalogSinglePageResponse } from '../../mock';
+
+describe('CiResourcesList', () => {
+ let wrapper;
+
+ const createComponent = ({ props = {} } = {}) => {
+ const { nodes, pageInfo, count } = catalogResponseBody.data.ciCatalogResources;
+
+ const defaultProps = {
+ currentPage: 1,
+ resources: nodes,
+ pageInfo,
+ totalCount: count,
+ };
+
+ wrapper = shallowMountExtended(CiResourcesList, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ stubs: {
+ GlKeysetPagination,
+ },
+ });
+ };
+
+ const findPageCount = () => wrapper.findByTestId('pageCount');
+ const findResourcesListItems = () => wrapper.findAllComponents(CiResourcesListItem);
+ const findPrevBtn = () => wrapper.findByTestId('prevButton');
+ const findNextBtn = () => wrapper.findByTestId('nextButton');
+
+ describe('contains only one page', () => {
+ const { nodes, pageInfo, count } = catalogSinglePageResponse.data.ciCatalogResources;
+
+ beforeEach(async () => {
+ await createComponent({
+ props: { currentPage: 1, resources: nodes, pageInfo, totalCount: count },
+ });
+ });
+
+ it('shows the right number of items', () => {
+ expect(findResourcesListItems()).toHaveLength(nodes.length);
+ });
+
+ it('hides the keyset control for previous page', () => {
+ expect(findPrevBtn().exists()).toBe(false);
+ });
+
+ it('hides the keyset control for next page', () => {
+ expect(findNextBtn().exists()).toBe(false);
+ });
+
+ it('shows the correct count of current page', () => {
+ expect(findPageCount().text()).toContain('1 of 1');
+ });
+ });
+
+ describe.each`
+ hasPreviousPage | hasNextPage | pageText | expectedTotal | currentPage
+ ${false} | ${true} | ${'1 of 3'} | ${ciCatalogResourcesItemsCount} | ${1}
+ ${true} | ${true} | ${'2 of 3'} | ${ciCatalogResourcesItemsCount} | ${2}
+ ${true} | ${false} | ${'3 of 3'} | ${ciCatalogResourcesItemsCount} | ${3}
+ `(
+ 'when on page $pageText',
+ ({ currentPage, expectedTotal, pageText, hasPreviousPage, hasNextPage }) => {
+ const { nodes, pageInfo, count } = catalogResponseBody.data.ciCatalogResources;
+
+ const previousPageState = hasPreviousPage ? 'enabled' : 'disabled';
+ const nextPageState = hasNextPage ? 'enabled' : 'disabled';
+
+ beforeEach(async () => {
+ await createComponent({
+ props: {
+ currentPage,
+ resources: nodes,
+ pageInfo: { ...pageInfo, hasPreviousPage, hasNextPage },
+ totalCount: count,
+ },
+ });
+ });
+
+ it('shows the right number of items', () => {
+ expect(findResourcesListItems()).toHaveLength(expectedTotal);
+ });
+
+ it(`shows the keyset control for previous page as ${previousPageState}`, () => {
+ const disableAttr = findPrevBtn().attributes('disabled');
+
+ if (previousPageState === 'disabled') {
+ expect(disableAttr).toBeDefined();
+ } else {
+ expect(disableAttr).toBeUndefined();
+ }
+ });
+
+ it(`shows the keyset control for next page as ${nextPageState}`, () => {
+ const disableAttr = findNextBtn().attributes('disabled');
+
+ if (nextPageState === 'disabled') {
+ expect(disableAttr).toBeDefined();
+ } else {
+ expect(disableAttr).toBeUndefined();
+ }
+ });
+
+ it('shows the correct count of current page', () => {
+ expect(findPageCount().text()).toContain(pageText);
+ });
+ },
+ );
+
+ describe('when there is an error getting the page count', () => {
+ beforeEach(() => {
+ createComponent({ props: { totalCount: 0 } });
+ });
+
+ it('hides the page count', () => {
+ expect(findPageCount().exists()).toBe(false);
+ });
+ });
+
+ describe('emitted events', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it.each`
+ btnText | elFinder | eventName
+ ${'previous'} | ${findPrevBtn} | ${'onPrevPage'}
+ ${'next'} | ${findNextBtn} | ${'onNextPage'}
+ `('emits $eventName when clicking on the $btnText button', async ({ elFinder, eventName }) => {
+ await elFinder().vm.$emit('click');
+
+ expect(wrapper.emitted(eventName)).toHaveLength(1);
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/components/list/empty_state_spec.js b/spec/frontend/ci/catalog/components/list/empty_state_spec.js
new file mode 100644
index 00000000000..f589ad96a9d
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/list/empty_state_spec.js
@@ -0,0 +1,27 @@
+import { GlEmptyState } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import EmptyState from '~/ci/catalog/components/list/empty_state.vue';
+
+describe('EmptyState', () => {
+ let wrapper;
+
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMountExtended(EmptyState, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ describe('when mounted', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the empty state', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/components/pages/ci_resource_details_page_spec.js b/spec/frontend/ci/catalog/components/pages/ci_resource_details_page_spec.js
new file mode 100644
index 00000000000..40f243ed891
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/pages/ci_resource_details_page_spec.js
@@ -0,0 +1,186 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import VueRouter from 'vue-router';
+import { GlEmptyState } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { CI_CATALOG_RESOURCE_TYPE, cacheConfig } from '~/ci/catalog/graphql/settings';
+
+import getCiCatalogResourceSharedData from '~/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql';
+import getCiCatalogResourceDetails from '~/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql';
+
+import CiResourceDetails from '~/ci/catalog/components/details/ci_resource_details.vue';
+import CiResourceDetailsPage from '~/ci/catalog/components/pages/ci_resource_details_page.vue';
+import CiResourceHeader from '~/ci/catalog/components/details/ci_resource_header.vue';
+import CiResourceHeaderSkeletonLoader from '~/ci/catalog/components/details/ci_resource_header_skeleton_loader.vue';
+
+import { createRouter } from '~/ci/catalog/router/index';
+import { CI_RESOURCE_DETAILS_PAGE_NAME } from '~/ci/catalog/router/constants';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { catalogSharedDataMock, catalogAdditionalDetailsMock } from '../../mock';
+
+Vue.use(VueApollo);
+Vue.use(VueRouter);
+
+let router;
+
+const defaultSharedData = { ...catalogSharedDataMock.data.ciCatalogResource };
+const defaultAdditionalData = { ...catalogAdditionalDetailsMock.data.ciCatalogResource };
+
+describe('CiResourceDetailsPage', () => {
+ let wrapper;
+ let sharedDataResponse;
+ let additionalDataResponse;
+
+ const defaultProps = {};
+
+ const defaultProvide = {
+ ciCatalogPath: '/ci/catalog/resources',
+ };
+
+ const findDetailsComponent = () => wrapper.findComponent(CiResourceDetails);
+ const findHeaderComponent = () => wrapper.findComponent(CiResourceHeader);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findHeaderSkeletonLoader = () => wrapper.findComponent(CiResourceHeaderSkeletonLoader);
+
+ const createComponent = ({ props = {} } = {}) => {
+ const handlers = [
+ [getCiCatalogResourceSharedData, sharedDataResponse],
+ [getCiCatalogResourceDetails, additionalDataResponse],
+ ];
+
+ const mockApollo = createMockApollo(handlers, undefined, cacheConfig);
+
+ wrapper = shallowMount(CiResourceDetailsPage, {
+ router,
+ apolloProvider: mockApollo,
+ provide: {
+ ...defaultProvide,
+ },
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ stubs: {
+ RouterView: true,
+ },
+ });
+ };
+
+ beforeEach(async () => {
+ sharedDataResponse = jest.fn();
+ additionalDataResponse = jest.fn();
+
+ router = createRouter();
+ await router.push({
+ name: CI_RESOURCE_DETAILS_PAGE_NAME,
+ params: { id: defaultSharedData.id },
+ });
+ });
+
+ describe('when the app is loading', () => {
+ describe('and shared data is pre-fetched', () => {
+ beforeEach(() => {
+ // By mocking a return value and not a promise, we skip the loading
+ // to simulate having the pre-fetched query
+ sharedDataResponse.mockReturnValueOnce(catalogSharedDataMock);
+ additionalDataResponse.mockResolvedValue(catalogAdditionalDetailsMock);
+ createComponent();
+ });
+
+ it('renders the header skeleton loader', () => {
+ expect(findHeaderSkeletonLoader().exists()).toBe(false);
+ });
+
+ it('passes down the loading state to the header component', () => {
+ sharedDataResponse.mockReturnValueOnce(catalogSharedDataMock);
+
+ expect(findHeaderComponent().props()).toMatchObject({
+ isLoadingDetails: true,
+ isLoadingSharedData: false,
+ });
+ });
+ });
+
+ describe('and shared data is not pre-fetched', () => {
+ beforeEach(() => {
+ sharedDataResponse.mockResolvedValue(catalogSharedDataMock);
+ additionalDataResponse.mockResolvedValue(catalogAdditionalDetailsMock);
+ createComponent();
+ });
+
+ it('does not render the header skeleton', () => {
+ expect(findHeaderSkeletonLoader().exists()).toBe(false);
+ });
+
+ it('passes all loading state to the header component as true', () => {
+ expect(findHeaderComponent().props()).toMatchObject({
+ isLoadingDetails: true,
+ isLoadingSharedData: true,
+ });
+ });
+ });
+ });
+
+ describe('and there are no resources', () => {
+ beforeEach(async () => {
+ const mockError = new Error('error');
+ sharedDataResponse.mockRejectedValue(mockError);
+ additionalDataResponse.mockRejectedValue(mockError);
+
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('renders the empty state', () => {
+ expect(findDetailsComponent().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findEmptyState().props('primaryButtonLink')).toBe(defaultProvide.ciCatalogPath);
+ });
+ });
+
+ describe('when data has loaded', () => {
+ beforeEach(async () => {
+ sharedDataResponse.mockResolvedValue(catalogSharedDataMock);
+ additionalDataResponse.mockResolvedValue(catalogAdditionalDetailsMock);
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('does not render the header skeleton loader', () => {
+ expect(findHeaderSkeletonLoader().exists()).toBe(false);
+ });
+
+ describe('Catalog header', () => {
+ it('exists', () => {
+ expect(findHeaderComponent().exists()).toBe(true);
+ });
+
+ it('passes expected props', () => {
+ expect(findHeaderComponent().props()).toEqual({
+ isLoadingDetails: false,
+ isLoadingSharedData: false,
+ openIssuesCount: defaultAdditionalData.openIssuesCount,
+ openMergeRequestsCount: defaultAdditionalData.openMergeRequestsCount,
+ pipelineStatus:
+ defaultAdditionalData.versions.nodes[0].commit.pipelines.nodes[0].detailedStatus,
+ resource: defaultSharedData,
+ });
+ });
+ });
+
+ describe('Catalog details', () => {
+ it('exists', () => {
+ expect(findDetailsComponent().exists()).toBe(true);
+ });
+
+ it('passes expected props', () => {
+ expect(findDetailsComponent().props()).toEqual({
+ resourceId: convertToGraphQLId(CI_CATALOG_RESOURCE_TYPE, defaultAdditionalData.id),
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/mock.js b/spec/frontend/ci/catalog/mock.js
new file mode 100644
index 00000000000..21fed6ac8ec
--- /dev/null
+++ b/spec/frontend/ci/catalog/mock.js
@@ -0,0 +1,546 @@
+import { componentsMockData } from '~/ci/catalog/constants';
+
+export const catalogResponseBody = {
+ data: {
+ ciCatalogResources: {
+ pageInfo: {
+ startCursor:
+ 'eyJjcmVhdGVkX2F0IjoiMjAxNS0wNy0wMyAxMDowMDowMC4wMDAwMDAwMDAgKzAwMDAiLCJpZCI6IjEyOSJ9',
+ endCursor:
+ 'eyJjcmVhdGVkX2F0IjoiMjAxNS0wNy0wMyAxMDowMDowMC4wMDAwMDAwMDAgKzAwMDAiLCJpZCI6IjExMCJ9',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ __typename: 'PageInfo',
+ },
+ count: 41,
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/129',
+ icon: null,
+ name: 'Project-42 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-42',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/128',
+ icon: null,
+ name: 'Project-41 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-41',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/127',
+ icon: null,
+ name: 'Project-40 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-40',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/126',
+ icon: null,
+ name: 'Project-39 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-39',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/125',
+ icon: null,
+ name: 'Project-38 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-38',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/124',
+ icon: null,
+ name: 'Project-37 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-37',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/123',
+ icon: null,
+ name: 'Project-36 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-36',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/122',
+ icon: null,
+ name: 'Project-35 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-35',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/121',
+ icon: null,
+ name: 'Project-34 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-34',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/120',
+ icon: null,
+ name: 'Project-33 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-33',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/119',
+ icon: null,
+ name: 'Project-32 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-32',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/118',
+ icon: null,
+ name: 'Project-31 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-31',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/117',
+ icon: null,
+ name: 'Project-30 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-30',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/116',
+ icon: null,
+ name: 'Project-29 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-29',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/115',
+ icon: null,
+ name: 'Project-28 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-28',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/114',
+ icon: null,
+ name: 'Project-27 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-27',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/113',
+ icon: null,
+ name: 'Project-26 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-26',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/112',
+ icon: null,
+ name: 'Project-25 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-25',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/111',
+ icon: null,
+ name: 'Project-24 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-24',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/110',
+ icon: null,
+ name: 'Project-23 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-23',
+ __typename: 'CiCatalogResource',
+ },
+ ],
+ __typename: 'CiCatalogResourceConnection',
+ },
+ },
+};
+
+export const catalogSinglePageResponse = {
+ data: {
+ ciCatalogResources: {
+ pageInfo: {
+ startCursor:
+ 'eyJjcmVhdGVkX2F0IjoiMjAxNS0wNy0wMyAxMDowMDowMC4wMDAwMDAwMDAgKzAwMDAiLCJpZCI6IjEzMiJ9',
+ endCursor:
+ 'eyJjcmVhdGVkX2F0IjoiMjAxNS0wNy0wMyAxMDowMDowMC4wMDAwMDAwMDAgKzAwMDAiLCJpZCI6IjEzMCJ9',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ __typename: 'PageInfo',
+ },
+ count: 3,
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/132',
+ icon: null,
+ name: 'Project-45 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-45',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/131',
+ icon: null,
+ name: 'Project-44 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-44',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/130',
+ icon: null,
+ name: 'Project-43 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-43',
+ __typename: 'CiCatalogResource',
+ },
+ ],
+ __typename: 'CiCatalogResourceConnection',
+ },
+ },
+};
+
+export const catalogSharedDataMock = {
+ data: {
+ ciCatalogResource: {
+ __typename: 'CiCatalogResource',
+ id: `gid://gitlab/CiCatalogResource/1`,
+ icon: null,
+ description: 'This is the description of the repo',
+ name: 'Ruby',
+ rootNamespace: { id: 1, fullPath: '/group/project', name: 'my-dumb-project' },
+ starCount: 1,
+ forksCount: 2,
+ latestVersion: {
+ __typename: 'Release',
+ id: '3',
+ tagName: '1.0.0',
+ tagPath: 'path/to/release',
+ releasedAt: Date.now(),
+ author: { id: 1, webUrl: 'profile/1', name: 'username' },
+ },
+ webPath: 'path/to/project',
+ },
+ },
+};
+
+export const catalogAdditionalDetailsMock = {
+ data: {
+ ciCatalogResource: {
+ __typename: 'CiCatalogResource',
+ id: `gid://gitlab/CiCatalogResource/1`,
+ openIssuesCount: 4,
+ openMergeRequestsCount: 10,
+ readmeHtml: '<h1>Hello world</h1>',
+ versions: {
+ __typename: 'ReleaseConnection',
+ nodes: [
+ {
+ __typename: 'Release',
+ id: 'gid://gitlab/Release/3',
+ commit: {
+ __typename: 'Commit',
+ id: 'gid://gitlab/CommitPresenter/afa936495f20e08c26ed4a67130ee2166f94fa6e',
+ pipelines: {
+ __typename: 'PipelineConnection',
+ nodes: [
+ {
+ __typename: 'Pipeline',
+ id: 'gid://gitlab/Ci::Pipeline/583',
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ id: 'success-583-583',
+ detailsPath: '/root/cicd-circular/-/pipelines/583',
+ icon: 'status_success',
+ text: 'passed',
+ group: 'success',
+ },
+ },
+ ],
+ },
+ },
+ tagName: 'v1.0.2',
+ releasedAt: '2022-08-23T17:19:09Z',
+ },
+ ],
+ },
+ },
+ },
+};
+
+const generateResourcesNodes = (count = 20, startId = 0) => {
+ const nodes = [];
+ for (let i = startId; i < startId + count; i += 1) {
+ nodes.push({
+ __typename: 'CiCatalogResource',
+ id: `gid://gitlab/CiCatalogResource/${i}`,
+ description: `This is a component that does a bunch of stuff and is really just a number: ${i}`,
+ forksCount: 5,
+ icon: 'my-icon',
+ name: `My component #${i}`,
+ rootNamespace: {
+ id: 1,
+ __typename: 'Namespace',
+ name: 'namespaceName',
+ path: 'namespacePath',
+ },
+ starCount: 10,
+ latestVersion: {
+ __typename: 'Release',
+ id: '3',
+ tagName: '1.0.0',
+ tagPath: 'path/to/release',
+ releasedAt: Date.now(),
+ author: { id: 1, webUrl: 'profile/1', name: 'username' },
+ },
+ webPath: 'path/to/project',
+ });
+ }
+
+ return nodes;
+};
+
+export const mockCatalogResourceItem = generateResourcesNodes(1)[0];
+
+export const mockComponents = {
+ data: {
+ ciCatalogResource: {
+ __typename: 'CiCatalogResource',
+ id: `gid://gitlab/CiCatalogResource/1`,
+ components: {
+ ...componentsMockData,
+ },
+ },
+ },
+};
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 64227872af3..353b5fd3c47 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,10 +1,4 @@
-import {
- GlListboxItem,
- GlCollapsibleListbox,
- GlDropdownDivider,
- GlDropdownItem,
- GlIcon,
-} from '@gitlab/ui';
+import { GlListboxItem, GlCollapsibleListbox, GlDropdownDivider, GlIcon } from '@gitlab/ui';
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';
@@ -16,7 +10,6 @@ describe('Ci environments dropdown', () => {
const defaultProps = {
areEnvironmentsLoading: false,
environments: envs,
- hasEnvScopeQuery: false,
selectedEnvironmentScope: '',
};
@@ -25,7 +18,7 @@ describe('Ci environments dropdown', () => {
const findActiveIconByIndex = (index) => findListboxItemByIndex(index).findComponent(GlIcon);
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findListboxText = () => findListbox().props('toggleText');
- const findCreateWildcardButton = () => wrapper.findComponent(GlDropdownItem);
+ const findCreateWildcardButton = () => wrapper.findByTestId('create-wildcard-button');
const findDropdownDivider = () => wrapper.findComponent(GlDropdownDivider);
const findMaxEnvNote = () => wrapper.findByTestId('max-envs-notice');
@@ -57,32 +50,23 @@ describe('Ci environments dropdown', () => {
});
describe('Search term is empty', () => {
- describe.each`
- hasEnvScopeQuery | status | defaultEnvStatus | firstItemValue | envIndices
- ${true} | ${'exists'} | ${'prepends'} | ${'*'} | ${[1, 2, 3]}
- ${false} | ${'does not exist'} | ${'does not prepend'} | ${envs[0]} | ${[0, 1, 2]}
- `(
- 'when query for fetching environment scope $status',
- ({ defaultEnvStatus, firstItemValue, hasEnvScopeQuery, envIndices }) => {
- beforeEach(() => {
- createComponent({ props: { environments: envs, hasEnvScopeQuery } });
- });
-
- it(`${defaultEnvStatus} * in listbox`, () => {
- expect(findListboxItemByIndex(0).text()).toBe(firstItemValue);
- });
-
- 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);
- });
- },
- );
+ beforeEach(() => {
+ createComponent({ props: { environments: envs } });
+ });
+
+ it(`prepends * in listbox`, () => {
+ expect(findListboxItemByIndex(0).text()).toBe('*');
+ });
+
+ it('renders all environments', () => {
+ expect(findListboxItemByIndex(1).text()).toBe(envs[0]);
+ expect(findListboxItemByIndex(2).text()).toBe(envs[1]);
+ expect(findListboxItemByIndex(3).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', () => {
@@ -98,40 +82,13 @@ describe('Ci environments dropdown', () => {
});
});
- describe('when environments are not fetched via graphql', () => {
+ describe('when fetching environments', () => {
const currentEnv = envs[2];
beforeEach(() => {
createComponent();
});
- 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 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();
- });
-
- it('does not display note about max environments shown', () => {
- expect(findMaxEnvNote().exists()).toBe(false);
- });
- });
-
- describe('when fetching environments via graphql', () => {
- const currentEnv = envs[2];
-
- beforeEach(() => {
- createComponent({ props: { hasEnvScopeQuery: true } });
- });
-
it('renders dropdown divider', () => {
expect(findDropdownDivider().exists()).toBe(true);
});
@@ -143,7 +100,7 @@ describe('Ci environments dropdown', () => {
});
it('renders dropdown loading icon while fetch query is loading', () => {
- createComponent({ props: { areEnvironmentsLoading: true, hasEnvScopeQuery: true } });
+ createComponent({ props: { areEnvironmentsLoading: true } });
expect(findListbox().props('loading')).toBe(true);
expect(findListbox().props('searching')).toBe(false);
@@ -151,7 +108,7 @@ describe('Ci environments dropdown', () => {
});
it('renders search loading icon while search query is loading and dropdown is open', async () => {
- createComponent({ props: { areEnvironmentsLoading: true, hasEnvScopeQuery: true } });
+ createComponent({ props: { areEnvironmentsLoading: true } });
await findListbox().vm.$emit('shown');
expect(findListbox().props('loading')).toBe(false);
@@ -188,16 +145,35 @@ describe('Ci environments dropdown', () => {
});
});
- describe('when creating a new environment from a search term', () => {
- const search = 'new-env';
+ describe('when creating a new environment scope from a search term', () => {
+ const searchTerm = 'new-env';
beforeEach(() => {
- createComponent({ searchTerm: search });
+ createComponent({ searchTerm });
});
- it('emits create-environment-scope', () => {
- findCreateWildcardButton().vm.$emit('click');
+ it('sets new environment scope as the selected environment scope', async () => {
+ findCreateWildcardButton().trigger('click');
+
+ await findListbox().vm.$emit('search', searchTerm);
+
+ expect(findListbox().props('selected')).toBe(searchTerm);
+ });
+
+ it('includes new environment scope in search if it matches search term', async () => {
+ findCreateWildcardButton().trigger('click');
+
+ await findListbox().vm.$emit('search', searchTerm);
+
+ expect(findAllListboxItems()).toHaveLength(envs.length + 1);
+ expect(findListboxItemByIndex(1).text()).toBe(searchTerm);
+ });
+
+ it('excludes new environment scope in search if it does not match the search term', async () => {
+ findCreateWildcardButton().trigger('click');
+
+ await findListbox().vm.$emit('search', 'not-new-env');
- expect(wrapper.emitted('create-environment-scope')).toEqual([[search]]);
+ expect(findAllListboxItems()).toHaveLength(envs.length);
});
});
});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js
index ab5d914a6a1..207ea7aa060 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js
@@ -1,4 +1,5 @@
-import { GlDrawer, GlFormCombobox, GlFormInput, GlFormSelect } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { GlDrawer, GlFormCombobox, GlFormInput, GlFormSelect, GlModal } from '@gitlab/ui';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import CiEnvironmentsDropdown from '~/ci/ci_variable_list/components/ci_environments_dropdown.vue';
import CiVariableDrawer from '~/ci/ci_variable_list/components/ci_variable_drawer.vue';
@@ -67,6 +68,8 @@ describe('CI Variable Drawer', () => {
};
const findConfirmBtn = () => wrapper.findByTestId('ci-variable-confirm-btn');
+ const findConfirmDeleteModal = () => wrapper.findComponent(GlModal);
+ const findDeleteBtn = () => wrapper.findByTestId('ci-variable-delete-btn');
const findDisabledEnvironmentScopeDropdown = () => wrapper.findComponent(GlFormInput);
const findDrawer = () => wrapper.findComponent(GlDrawer);
const findEnvironmentScopeDropdown = () => wrapper.findComponent(CiEnvironmentsDropdown);
@@ -363,22 +366,118 @@ describe('CI Variable Drawer', () => {
});
it('title and confirm button renders the correct text', () => {
- expect(findTitle().text()).toBe('Add Variable');
- expect(findConfirmBtn().text()).toBe('Add Variable');
+ expect(findTitle().text()).toBe('Add variable');
+ expect(findConfirmBtn().text()).toBe('Add variable');
+ });
+
+ it('does not render delete button', () => {
+ expect(findDeleteBtn().exists()).toBe(false);
+ });
+
+ it('dispatches the add-variable event', async () => {
+ await findKeyField().vm.$emit('input', 'NEW_VARIABLE');
+ await findProtectedCheckbox().vm.$emit('input', false);
+ await findExpandedCheckbox().vm.$emit('input', true);
+ await findMaskedCheckbox().vm.$emit('input', true);
+ await findValueField().vm.$emit('input', 'NEW_VALUE');
+
+ findConfirmBtn().vm.$emit('click');
+
+ expect(wrapper.emitted('add-variable')).toEqual([
+ [
+ {
+ environmentScope: '*',
+ key: 'NEW_VARIABLE',
+ masked: true,
+ protected: false,
+ raw: false, // opposite of expanded
+ value: 'NEW_VALUE',
+ variableType: 'ENV_VAR',
+ },
+ ],
+ ]);
});
});
describe('when editing a variable', () => {
beforeEach(() => {
createComponent({
- props: { mode: EDIT_VARIABLE_ACTION },
+ props: { mode: EDIT_VARIABLE_ACTION, selectedVariable: mockProjectVariableFileType },
stubs: { GlDrawer },
});
});
it('title and confirm button renders the correct text', () => {
- expect(findTitle().text()).toBe('Edit Variable');
- expect(findConfirmBtn().text()).toBe('Edit Variable');
+ expect(findTitle().text()).toBe('Edit variable');
+ expect(findConfirmBtn().text()).toBe('Edit variable');
+ });
+
+ it('dispatches the edit-variable event', async () => {
+ await findValueField().vm.$emit('input', 'EDITED_VALUE');
+
+ findConfirmBtn().vm.$emit('click');
+
+ expect(wrapper.emitted('update-variable')).toEqual([
+ [
+ {
+ ...mockProjectVariableFileType,
+ value: 'EDITED_VALUE',
+ },
+ ],
+ ]);
+ });
+ });
+
+ describe('when deleting a variable', () => {
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ props: { mode: EDIT_VARIABLE_ACTION, selectedVariable: mockProjectVariableFileType },
+ });
+ });
+
+ it('bubbles up the delete-variable event', async () => {
+ findDeleteBtn().vm.$emit('click');
+
+ await nextTick();
+
+ findConfirmDeleteModal().vm.$emit('primary');
+
+ expect(wrapper.emitted('delete-variable')).toEqual([[mockProjectVariableFileType]]);
+ });
+ });
+
+ describe('environment scope events', () => {
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ props: {
+ mode: EDIT_VARIABLE_ACTION,
+ selectedVariable: mockProjectVariableFileType,
+ areScopedVariablesAvailable: true,
+ hideEnvironmentScope: false,
+ },
+ });
+ });
+
+ it('sets the environment scope', async () => {
+ await findEnvironmentScopeDropdown().vm.$emit('select-environment', 'staging');
+ await findConfirmBtn().vm.$emit('click');
+
+ expect(wrapper.emitted('update-variable')).toEqual([
+ [
+ {
+ ...mockProjectVariableFileType,
+ environmentScope: 'staging',
+ },
+ ],
+ ]);
+ });
+
+ it('bubbles up the search event', async () => {
+ await findEnvironmentScopeDropdown().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_modal_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
index 7dce23f72c0..5ba9b3b8c20 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
@@ -12,12 +12,10 @@ import {
ENVIRONMENT_SCOPE_LINK_TITLE,
AWS_TIP_TITLE,
AWS_TIP_MESSAGE,
- groupString,
instanceString,
- projectString,
variableOptions,
} from '~/ci/ci_variable_list/constants';
-import { mockEnvs, mockVariablesWithScopes, mockVariablesWithUniqueScopes } from '../mocks';
+import { mockVariablesWithScopes } from '../mocks';
import ModalStub from '../stubs';
describe('Ci variable modal', () => {
@@ -46,7 +44,6 @@ describe('Ci variable modal', () => {
areScopedVariablesAvailable: true,
environments: [],
hideEnvironmentScope: false,
- hasEnvScopeQuery: false,
mode: ADD_VARIABLE_ACTION,
selectedVariable: {},
variables: [],
@@ -352,42 +349,6 @@ describe('Ci variable modal', () => {
expect(link.attributes('title')).toBe(ENVIRONMENT_SCOPE_LINK_TITLE);
expect(link.attributes('href')).toBe(defaultProvide.environmentScopeLink);
});
-
- describe('when query for envioronment scope exists', () => {
- beforeEach(() => {
- createComponent({
- props: {
- environments: mockEnvs,
- hasEnvScopeQuery: true,
- variables: mockVariablesWithUniqueScopes(projectString),
- },
- });
- });
-
- 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', () => {
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 79dd638e2bd..04145c2c6aa 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
@@ -23,7 +23,6 @@ describe('Ci variable table', () => {
environments: mapEnvironmentNames(mockEnvs),
hideEnvironmentScope: false,
isLoading: false,
- hasEnvScopeQuery: false,
maxVariableLimit: 5,
pageInfo: { after: '' },
variables: mockVariablesWithScopes(projectString),
@@ -70,7 +69,6 @@ describe('Ci variable table', () => {
areEnvironmentsLoading: defaultProps.areEnvironmentsLoading,
areScopedVariablesAvailable: defaultProps.areScopedVariablesAvailable,
environments: defaultProps.environments,
- hasEnvScopeQuery: defaultProps.hasEnvScopeQuery,
hideEnvironmentScope: defaultProps.hideEnvironmentScope,
variables: defaultProps.variables,
mode: ADD_VARIABLE_ACTION,
@@ -142,7 +140,7 @@ describe('Ci variable table', () => {
});
});
- describe('variable events', () => {
+ describe('variable events for modal', () => {
beforeEach(() => {
createComponent();
});
@@ -161,6 +159,25 @@ describe('Ci variable table', () => {
});
});
+ describe('variable events for drawer', () => {
+ beforeEach(() => {
+ createComponent({ featureFlags: { ciVariableDrawer: true } });
+ });
+
+ it.each`
+ eventName
+ ${'add-variable'}
+ ${'update-variable'}
+ ${'delete-variable'}
+ `('bubbles up the $eventName event', async ({ eventName }) => {
+ await findCiVariableTable().vm.$emit('set-selected-variable');
+
+ await findCiVariableDrawer().vm.$emit(eventName, newVariable);
+
+ expect(wrapper.emitted(eventName)).toEqual([[newVariable]]);
+ });
+ });
+
describe('pages events', () => {
beforeEach(() => {
createComponent();
@@ -178,7 +195,7 @@ describe('Ci variable table', () => {
});
});
- describe('environment events', () => {
+ describe('environment events for modal', () => {
beforeEach(() => {
createComponent();
});
@@ -191,4 +208,18 @@ describe('Ci variable table', () => {
expect(wrapper.emitted('search-environment-scope')).toEqual([['staging']]);
});
});
+
+ describe('environment events for drawer', () => {
+ beforeEach(() => {
+ createComponent({ featureFlags: { ciVariableDrawer: true } });
+ });
+
+ it('bubbles up the search event', async () => {
+ await findCiVariableTable().vm.$emit('set-selected-variable');
+
+ await findCiVariableDrawer().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 6fa1915f3c1..c90ff4cc682 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
@@ -52,7 +52,6 @@ const mockProvide = {
const defaultProps = {
areScopedVariablesAvailable: true,
- hasEnvScopeQuery: false,
pageInfo: {},
hideEnvironmentScope: false,
refetchAfterMutation: false,
@@ -514,7 +513,6 @@ describe('Ci Variable Shared Component', () => {
areEnvironmentsLoading: false,
areScopedVariablesAvailable: wrapper.props().areScopedVariablesAvailable,
hideEnvironmentScope: defaultProps.hideEnvironmentScope,
- hasEnvScopeQuery: props.hasEnvScopeQuery,
pageInfo: defaultProps.pageInfo,
isLoading: false,
maxVariableLimit,
diff --git a/spec/frontend/ci/ci_variable_list/mocks.js b/spec/frontend/ci/ci_variable_list/mocks.js
index 41dfc0ebfda..9c9c99ad5ea 100644
--- a/spec/frontend/ci/ci_variable_list/mocks.js
+++ b/spec/frontend/ci/ci_variable_list/mocks.js
@@ -189,7 +189,6 @@ export const createProjectProps = () => {
componentName: 'ProjectVariable',
entity: 'project',
fullPath: '/namespace/project/',
- hasEnvScopeQuery: true,
id: 'gid://gitlab/Project/20',
mutationData: {
[ADD_MUTATION_ACTION]: addProjectVariable,
@@ -214,7 +213,6 @@ export const createGroupProps = () => {
componentName: 'GroupVariable',
entity: 'group',
fullPath: '/my-group',
- hasEnvScopeQuery: false,
id: 'gid://gitlab/Group/20',
mutationData: {
[ADD_MUTATION_ACTION]: addGroupVariable,
@@ -233,7 +231,6 @@ export const createGroupProps = () => {
export const createInstanceProps = () => {
return {
componentName: 'InstanceVariable',
- hasEnvScopeQuery: false,
entity: '',
mutationData: {
[ADD_MUTATION_ACTION]: addAdminVariable,
diff --git a/spec/frontend/ci/ci_variable_list/utils_spec.js b/spec/frontend/ci/ci_variable_list/utils_spec.js
index beeae71376a..fbcf0e7c5a5 100644
--- a/spec/frontend/ci/ci_variable_list/utils_spec.js
+++ b/spec/frontend/ci/ci_variable_list/utils_spec.js
@@ -1,58 +1,7 @@
-import {
- createJoinedEnvironments,
- convertEnvironmentScope,
- mapEnvironmentNames,
-} from '~/ci/ci_variable_list/utils';
+import { convertEnvironmentScope, mapEnvironmentNames } from '~/ci/ci_variable_list/utils';
import { allEnvironments } from '~/ci/ci_variable_list/constants';
describe('utils', () => {
- const environments = ['dev', 'prod'];
- const newEnvironments = ['staging'];
-
- describe('createJoinedEnvironments', () => {
- it('returns only `environments` if `variables` argument is undefined', () => {
- const variables = undefined;
-
- expect(createJoinedEnvironments(variables, environments, [])).toEqual(environments);
- });
-
- it('returns a list of environments and environment scopes taken from variables in alphabetical order', () => {
- const envScope1 = 'new1';
- const envScope2 = 'new2';
-
- const variables = [{ environmentScope: envScope1 }, { environmentScope: envScope2 }];
-
- expect(createJoinedEnvironments(variables, environments, [])).toEqual([
- environments[0],
- envScope1,
- envScope2,
- environments[1],
- ]);
- });
-
- it('returns combined list with new environments included', () => {
- const variables = undefined;
-
- expect(createJoinedEnvironments(variables, environments, newEnvironments)).toEqual([
- ...environments,
- ...newEnvironments,
- ]);
- });
-
- it('removes duplicate environments', () => {
- const envScope1 = environments[0];
- const envScope2 = 'new2';
-
- const variables = [{ environmentScope: envScope1 }, { environmentScope: envScope2 }];
-
- expect(createJoinedEnvironments(variables, environments, [])).toEqual([
- environments[0],
- envScope2,
- environments[1],
- ]);
- });
- });
-
describe('convertEnvironmentScope', () => {
it('converts the * to the `All environments` text', () => {
expect(convertEnvironmentScope('*')).toBe(allEnvironments.text);
diff --git a/spec/frontend/ci/common/pipelines_table_spec.js b/spec/frontend/ci/common/pipelines_table_spec.js
index 26dd1a2fcc5..6cf391d72ca 100644
--- a/spec/frontend/ci/common/pipelines_table_spec.js
+++ b/spec/frontend/ci/common/pipelines_table_spec.js
@@ -1,9 +1,7 @@
-import '~/commons';
import { GlTableLite } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
import fixture from 'test_fixtures/pipelines/pipelines.json';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
import PipelineFailedJobsWidget from '~/ci/pipelines_page/components/failure_widget/pipeline_failed_jobs_widget.vue';
import PipelineOperations from '~/ci/pipelines_page/components/pipeline_operations.vue';
@@ -12,7 +10,7 @@ import PipelineUrl from '~/ci/pipelines_page/components/pipeline_url.vue';
import PipelinesTable from '~/ci/common/pipelines_table.vue';
import PipelinesTimeago from '~/ci/pipelines_page/components/time_ago.vue';
import {
- PipelineKeyOptions,
+ PIPELINE_ID_KEY,
BUTTON_TOOLTIP_RETRY,
BUTTON_TOOLTIP_CANCEL,
TRACKING_CATEGORIES,
@@ -20,51 +18,43 @@ import {
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
-jest.mock('~/ci/event_hub');
-
describe('Pipelines Table', () => {
- let pipeline;
let wrapper;
let trackingSpy;
const defaultProvide = {
- glFeatures: {},
- withFailedJobsDetails: false,
+ fullPath: '/my-project/',
+ useFailedJobsWidget: false,
};
- const provideWithDetails = {
- glFeatures: {
- ciJobFailuresInMr: true,
- },
- withFailedJobsDetails: true,
+ const provideWithFailedJobsWidget = {
+ useFailedJobsWidget: true,
};
- const defaultProps = {
- pipelines: [],
- viewType: 'root',
- pipelineKeyOption: PipelineKeyOptions[0],
- };
+ const { pipelines } = fixture;
- const createMockPipeline = () => {
- // Clone fixture as it could be modified by tests
- const { pipelines } = JSON.parse(JSON.stringify(fixture));
- return pipelines.find((p) => p.user !== null && p.commit !== null);
+ const defaultProps = {
+ pipelines,
+ pipelineIdType: PIPELINE_ID_KEY,
};
- const createComponent = (props = {}, provide = {}) => {
- wrapper = extendedWrapper(
- mount(PipelinesTable, {
- propsData: {
- ...defaultProps,
- ...props,
- },
- provide: {
- ...defaultProvide,
- ...provide,
- },
- stubs: ['PipelineFailedJobsWidget'],
- }),
- );
+ const [firstPipeline] = pipelines;
+
+ const createComponent = ({ props = {}, provide = {}, stubs = {} } = {}) => {
+ wrapper = mountExtended(PipelinesTable, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ provide: {
+ ...defaultProvide,
+ ...provide,
+ },
+ stubs: {
+ PipelineOperations: true,
+ ...stubs,
+ },
+ });
};
const findGlTableLite = () => wrapper.findComponent(GlTableLite);
@@ -84,13 +74,9 @@ describe('Pipelines Table', () => {
const findRetryBtn = () => wrapper.findByTestId('pipelines-retry-button');
const findCancelBtn = () => wrapper.findByTestId('pipelines-cancel-button');
- beforeEach(() => {
- pipeline = createMockPipeline();
- });
-
describe('Pipelines Table', () => {
beforeEach(() => {
- createComponent({ pipelines: [pipeline], viewType: 'root' });
+ createComponent({ props: { viewType: 'root' } });
});
it('displays table', () => {
@@ -105,7 +91,7 @@ describe('Pipelines Table', () => {
});
it('should display a table row', () => {
- expect(findTableRows()).toHaveLength(1);
+ expect(findTableRows()).toHaveLength(pipelines.length);
});
describe('status cell', () => {
@@ -120,7 +106,7 @@ describe('Pipelines Table', () => {
});
it('should display the pipeline id', () => {
- expect(findPipelineInfo().text()).toContain(`#${pipeline.id}`);
+ expect(findPipelineInfo().text()).toContain(`#${firstPipeline.id}`);
});
});
@@ -130,24 +116,33 @@ describe('Pipelines Table', () => {
});
it('should render the right number of stages', () => {
- const stagesLength = pipeline.details.stages.length;
- expect(findLegacyPipelineMiniGraph().props('stages').length).toBe(stagesLength);
+ const stagesLength = firstPipeline.details.stages.length;
+ expect(findLegacyPipelineMiniGraph().props('stages')).toHaveLength(stagesLength);
});
it('should render the latest downstream pipelines only', () => {
// component receives two downstream pipelines. one of them is already outdated
// because we retried the trigger job, so the mini pipeline graph will only
// render the newly created downstream pipeline instead
- expect(pipeline.triggered).toHaveLength(2);
+ expect(firstPipeline.triggered).toHaveLength(2);
expect(findLegacyPipelineMiniGraph().props('downstreamPipelines')).toHaveLength(1);
});
describe('when pipeline does not have stages', () => {
beforeEach(() => {
- pipeline = createMockPipeline();
- pipeline.details.stages = [];
-
- createComponent({ pipelines: [pipeline] });
+ createComponent({
+ props: {
+ pipelines: [
+ {
+ ...firstPipeline,
+ details: {
+ ...firstPipeline.details,
+ stages: [],
+ },
+ },
+ ],
+ },
+ });
});
it('stages are not rendered', () => {
@@ -163,6 +158,10 @@ describe('Pipelines Table', () => {
});
describe('operations cell', () => {
+ beforeEach(() => {
+ createComponent({ stubs: { PipelineOperations } });
+ });
+
it('should render pipeline operations', () => {
expect(findActions().exists()).toBe(true);
});
@@ -183,97 +182,101 @@ describe('Pipelines Table', () => {
});
describe('failed jobs details', () => {
- describe('row', () => {
- describe('when the FF is disabled', () => {
- beforeEach(() => {
- createComponent({ pipelines: [pipeline] });
- });
+ describe('when `useFailedJobsWidget` value is provided', () => {
+ beforeEach(() => {
+ createComponent({ provide: provideWithFailedJobsWidget });
+ });
- it('does not render', () => {
- expect(findTableRows()).toHaveLength(1);
- expect(findPipelineFailureWidget().exists()).toBe(false);
- });
+ it('renders', () => {
+ // We have 2 rows per pipeline with the widget
+ expect(findTableRows()).toHaveLength(pipelines.length * 2);
+ expect(findPipelineFailureWidget().exists()).toBe(true);
});
- describe('when the FF is enabled', () => {
- describe('and `withFailedJobsDetails` value is provided', () => {
- beforeEach(() => {
- createComponent({ pipelines: [pipeline] }, provideWithDetails);
- });
-
- it('renders', () => {
- expect(findTableRows()).toHaveLength(2);
- expect(findPipelineFailureWidget().exists()).toBe(true);
- });
-
- it('passes the expected props', () => {
- expect(findPipelineFailureWidget().props()).toStrictEqual({
- failedJobsCount: pipeline.failed_builds.length,
- isPipelineActive: pipeline.active,
- pipelineIid: pipeline.iid,
- pipelinePath: pipeline.path,
- // Make sure the forward slash was removed
- projectPath: 'frontend-fixtures/pipelines-project',
- });
- });
+ it('passes the expected props', () => {
+ expect(findPipelineFailureWidget().props()).toStrictEqual({
+ failedJobsCount: firstPipeline.failed_builds_count,
+ isPipelineActive: firstPipeline.active,
+ pipelineIid: firstPipeline.iid,
+ pipelinePath: firstPipeline.path,
+ // Make sure the forward slash was removed
+ projectPath: 'frontend-fixtures/pipelines-project',
});
+ });
+ });
- describe('and `withFailedJobsDetails` value is not provided', () => {
- beforeEach(() => {
- createComponent(
- { pipelines: [pipeline] },
- { glFeatures: { ciJobFailuresInMr: true } },
- );
- });
-
- it('does not render', () => {
- expect(findTableRows()).toHaveLength(1);
- expect(findPipelineFailureWidget().exists()).toBe(false);
- });
- });
+ describe('and `useFailedJobsWidget` value is not provided', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('does not render', () => {
+ expect(findTableRows()).toHaveLength(pipelines.length);
+ expect(findPipelineFailureWidget().exists()).toBe(false);
});
});
});
+ });
- describe('tracking', () => {
- beforeEach(() => {
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ describe('events', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('when confirming to cancel a pipeline', () => {
+ beforeEach(async () => {
+ await findActions().vm.$emit('cancel-pipeline', firstPipeline);
});
- afterEach(() => {
- unmockTracking();
+ it('emits the `cancel-pipeline` event', () => {
+ expect(wrapper.emitted('cancel-pipeline')).toEqual([[firstPipeline]]);
});
+ });
- it('tracks status badge click', () => {
- findCiBadgeLink().vm.$emit('ciStatusBadgeClick');
+ describe('when retrying a pipeline', () => {
+ beforeEach(() => {
+ findActions().vm.$emit('retry-pipeline', firstPipeline);
+ });
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_ci_status_badge', {
- label: TRACKING_CATEGORIES.table,
- });
+ it('emits the `retry-pipeline` event', () => {
+ expect(wrapper.emitted('retry-pipeline')).toEqual([[firstPipeline]]);
});
+ });
- it('tracks retry pipeline button click', () => {
- findRetryBtn().vm.$emit('click');
+ describe('when refreshing pipelines', () => {
+ beforeEach(() => {
+ findActions().vm.$emit('refresh-pipelines-table');
+ });
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_retry_button', {
- label: TRACKING_CATEGORIES.table,
- });
+ it('emits the `refresh-pipelines-table` event', () => {
+ expect(wrapper.emitted('refresh-pipelines-table')).toEqual([[]]);
});
+ });
+ });
- it('tracks cancel pipeline button click', () => {
- findCancelBtn().vm.$emit('click');
+ describe('tracking', () => {
+ beforeEach(() => {
+ createComponent();
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ });
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_cancel_button', {
- label: TRACKING_CATEGORIES.table,
- });
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('tracks status badge click', () => {
+ findCiBadgeLink().vm.$emit('ciStatusBadgeClick');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_ci_status_badge', {
+ label: TRACKING_CATEGORIES.table,
});
+ });
- it('tracks pipeline mini graph stage click', () => {
- findLegacyPipelineMiniGraph().vm.$emit('miniGraphStageClick');
+ it('tracks pipeline mini graph stage click', () => {
+ findLegacyPipelineMiniGraph().vm.$emit('miniGraphStageClick');
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_minigraph', {
- label: TRACKING_CATEGORIES.table,
- });
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_minigraph', {
+ label: TRACKING_CATEGORIES.table,
});
});
});
diff --git a/spec/frontend/ci/job_details/components/job_header_spec.js b/spec/frontend/ci/job_details/components/job_header_spec.js
index 6fc55732353..609369316f5 100644
--- a/spec/frontend/ci/job_details/components/job_header_spec.js
+++ b/spec/frontend/ci/job_details/components/job_header_spec.js
@@ -16,7 +16,7 @@ describe('Header CI Component', () => {
text: 'failed',
details_path: 'path',
},
- name: 'Job build_job',
+ name: 'build_job',
time: '2017-05-08T14:57:39.781Z',
user: {
id: 1234,
@@ -34,17 +34,15 @@ describe('Header CI Component', () => {
const findUserLink = () => wrapper.findComponent(GlAvatarLink);
const findSidebarToggleBtn = () => wrapper.findComponent(GlButton);
const findStatusTooltip = () => wrapper.findComponent(GlTooltip);
- const findActionButtons = () => wrapper.findByTestId('job-header-action-buttons');
const findJobName = () => wrapper.findByTestId('job-name');
- const createComponent = (props, slots) => {
+ const createComponent = (props) => {
wrapper = extendedWrapper(
shallowMount(JobHeader, {
propsData: {
...defaultProps,
...props,
},
- ...slots,
}),
);
};
@@ -54,6 +52,10 @@ describe('Header CI Component', () => {
createComponent();
});
+ it('renders the correct job name', () => {
+ expect(findJobName().text()).toBe(defaultProps.name);
+ });
+
it('should render status badge', () => {
expect(findCiBadgeLink().exists()).toBe(true);
});
@@ -65,10 +67,6 @@ describe('Header CI Component', () => {
it('should render sidebar toggle button', () => {
expect(findSidebarToggleBtn().exists()).toBe(true);
});
-
- it('should not render header action buttons when slot is empty', () => {
- expect(findActionButtons().exists()).toBe(false);
- });
});
describe('user avatar', () => {
@@ -124,31 +122,12 @@ describe('Header CI Component', () => {
});
});
- describe('job name', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('should render the job name', () => {
- expect(findJobName().text()).toBe('Job build_job');
- });
- });
-
- describe('slot', () => {
- it('should render header action buttons', () => {
- createComponent({}, { slots: { default: 'Test Actions' } });
-
- expect(findActionButtons().exists()).toBe(true);
- expect(findActionButtons().text()).toBe('Test Actions');
- });
- });
-
describe('shouldRenderTriggeredLabel', () => {
it('should render created keyword when the shouldRenderTriggeredLabel is false', () => {
createComponent({ shouldRenderTriggeredLabel: false });
- expect(wrapper.text()).toContain('created');
- expect(wrapper.text()).not.toContain('started');
+ expect(wrapper.text()).toContain('Created');
+ expect(wrapper.text()).not.toContain('Started');
});
});
});
diff --git a/spec/frontend/ci/job_details/components/log/collapsible_section_spec.js b/spec/frontend/ci/job_details/components/log/collapsible_section_spec.js
index e3d5c448338..5abf2a5ce53 100644
--- a/spec/frontend/ci/job_details/components/log/collapsible_section_spec.js
+++ b/spec/frontend/ci/job_details/components/log/collapsible_section_spec.js
@@ -1,6 +1,7 @@
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import CollapsibleSection from '~/ci/job_details/components/log/collapsible_section.vue';
+import LogLine from '~/ci/job_details/components/log/line.vue';
import LogLineHeader from '~/ci/job_details/components/log/line_header.vue';
import { collapsibleSectionClosed, collapsibleSectionOpened } from './mock_data';
@@ -9,9 +10,9 @@ describe('Job Log Collapsible Section', () => {
const jobLogEndpoint = 'jobs/335';
- const findCollapsibleLine = () => wrapper.find('.collapsible-line');
- const findCollapsibleLineSvg = () => wrapper.find('.collapsible-line svg');
const findLogLineHeader = () => wrapper.findComponent(LogLineHeader);
+ const findLogLineHeaderSvg = () => findLogLineHeader().find('svg');
+ const findLogLines = () => wrapper.findAllComponents(LogLine);
const createComponent = (props = {}) => {
wrapper = mount(CollapsibleSection, {
@@ -30,11 +31,16 @@ describe('Job Log Collapsible Section', () => {
});
it('renders clickable header line', () => {
- expect(findCollapsibleLine().attributes('role')).toBe('button');
+ expect(findLogLineHeader().text()).toBe('1 foo');
+ expect(findLogLineHeader().attributes('role')).toBe('button');
});
- it('renders an icon with the closed state', () => {
- expect(findCollapsibleLineSvg().attributes('data-testid')).toBe('chevron-lg-right-icon');
+ it('renders an icon with a closed state', () => {
+ expect(findLogLineHeaderSvg().attributes('data-testid')).toBe('chevron-lg-right-icon');
+ });
+
+ it('does not render collapsed lines', () => {
+ expect(findLogLines()).toHaveLength(0);
});
});
@@ -47,15 +53,17 @@ describe('Job Log Collapsible Section', () => {
});
it('renders clickable header line', () => {
- expect(findCollapsibleLine().attributes('role')).toBe('button');
+ expect(findLogLineHeader().text()).toContain('foo');
+ expect(findLogLineHeader().attributes('role')).toBe('button');
});
it('renders an icon with the open state', () => {
- expect(findCollapsibleLineSvg().attributes('data-testid')).toBe('chevron-lg-down-icon');
+ expect(findLogLineHeaderSvg().attributes('data-testid')).toBe('chevron-lg-down-icon');
});
- it('renders collapsible lines content', () => {
- expect(wrapper.findAll('.js-line').length).toEqual(collapsibleSectionOpened.lines.length);
+ it('renders collapsible lines', () => {
+ expect(findLogLines().at(0).text()).toContain('this is a collapsible nested section');
+ expect(findLogLines()).toHaveLength(collapsibleSectionOpened.lines.length);
});
});
@@ -65,7 +73,7 @@ describe('Job Log Collapsible Section', () => {
jobLogEndpoint,
});
- findCollapsibleLine().trigger('click');
+ findLogLineHeader().trigger('click');
await nextTick();
expect(wrapper.emitted('onClickCollapsibleLine').length).toBe(1);
diff --git a/spec/frontend/ci/job_details/components/log/line_header_spec.js b/spec/frontend/ci/job_details/components/log/line_header_spec.js
index 7d1b05346f2..45296e4b6c2 100644
--- a/spec/frontend/ci/job_details/components/log/line_header_spec.js
+++ b/spec/frontend/ci/job_details/components/log/line_header_spec.js
@@ -16,7 +16,7 @@ describe('Job Log Header Line', () => {
style: 'term-fg-l-green',
},
],
- lineNumber: 76,
+ lineNumber: 77,
},
isClosed: true,
path: '/jashkenas/underscore/-/jobs/335',
diff --git a/spec/frontend/ci/job_details/components/log/line_number_spec.js b/spec/frontend/ci/job_details/components/log/line_number_spec.js
index d5c1d0fd985..db964e341b7 100644
--- a/spec/frontend/ci/job_details/components/log/line_number_spec.js
+++ b/spec/frontend/ci/job_details/components/log/line_number_spec.js
@@ -5,7 +5,7 @@ describe('Job Log Line Number', () => {
let wrapper;
const data = {
- lineNumber: 0,
+ lineNumber: 1,
path: '/jashkenas/underscore/-/jobs/335',
};
diff --git a/spec/frontend/ci/job_details/components/log/line_spec.js b/spec/frontend/ci/job_details/components/log/line_spec.js
index b6f3a2b68df..dad41d0cd7f 100644
--- a/spec/frontend/ci/job_details/components/log/line_spec.js
+++ b/spec/frontend/ci/job_details/components/log/line_spec.js
@@ -224,7 +224,7 @@ describe('Job Log Line', () => {
offset: 24526,
content: [{ text: 'job log content' }],
section: 'custom-section',
- lineNumber: 76,
+ lineNumber: 77,
},
path: '/root/ci-project/-/jobs/6353',
});
diff --git a/spec/frontend/ci/job_details/components/log/log_spec.js b/spec/frontend/ci/job_details/components/log/log_spec.js
index cc1621b87d6..1931d5046dc 100644
--- a/spec/frontend/ci/job_details/components/log/log_spec.js
+++ b/spec/frontend/ci/job_details/components/log/log_spec.js
@@ -7,7 +7,7 @@ import { scrollToElement } from '~/lib/utils/common_utils';
import Log from '~/ci/job_details/components/log/log.vue';
import LogLineHeader from '~/ci/job_details/components/log/line_header.vue';
import { logLinesParser } from '~/ci/job_details/store/utils';
-import { jobLog } from './mock_data';
+import { mockJobLog, mockJobLogLineCount } from './mock_data';
jest.mock('~/lib/utils/common_utils', () => ({
...jest.requireActual('~/lib/utils/common_utils'),
@@ -39,7 +39,7 @@ describe('Job Log', () => {
};
state = {
- jobLog: logLinesParser(jobLog),
+ jobLog: logLinesParser(mockJobLog),
jobLogEndpoint: 'jobs/id',
};
@@ -57,15 +57,18 @@ describe('Job Log', () => {
createComponent();
});
- it('renders a line number for each open line', () => {
- expect(wrapper.find('#L1').text()).toBe('1');
- expect(wrapper.find('#L2').text()).toBe('2');
- expect(wrapper.find('#L3').text()).toBe('3');
- });
+ it.each([...Array(mockJobLogLineCount).keys()])(
+ 'renders a line number for each line %d',
+ (index) => {
+ const lineNumber = wrapper
+ .findAll('.js-log-line')
+ .at(index)
+ .find(`#L${index + 1}`);
- it('links to the provided path and correct line number', () => {
- expect(wrapper.find('#L1').attributes('href')).toBe(`${state.jobLogEndpoint}#L1`);
- });
+ expect(lineNumber.text()).toBe(`${index + 1}`);
+ expect(lineNumber.attributes('href')).toBe(`${state.jobLogEndpoint}#L${index + 1}`);
+ },
+ );
});
describe('collapsible sections', () => {
@@ -103,7 +106,7 @@ describe('Job Log', () => {
await waitForPromises();
- expect(wrapper.find('#L6').exists()).toBe(false);
+ expect(wrapper.find('#L9').exists()).toBe(false);
expect(scrollToElement).not.toHaveBeenCalled();
});
});
@@ -116,19 +119,19 @@ describe('Job Log', () => {
it('scrolls to line number', async () => {
createComponent();
- state.jobLog = logLinesParser(jobLog, [], '#L6');
+ state.jobLog = logLinesParser(mockJobLog, [], '#L6');
await waitForPromises();
expect(scrollToElement).toHaveBeenCalledTimes(1);
- state.jobLog = logLinesParser(jobLog, [], '#L7');
+ state.jobLog = logLinesParser(mockJobLog, [], '#L7');
await waitForPromises();
expect(scrollToElement).toHaveBeenCalledTimes(1);
});
it('line number within collapsed section is visible', () => {
- state.jobLog = logLinesParser(jobLog, [], '#L6');
+ state.jobLog = logLinesParser(mockJobLog, [], '#L6');
createComponent();
@@ -148,7 +151,7 @@ describe('Job Log', () => {
],
section: 'prepare-executor',
section_header: true,
- lineNumber: 2,
+ lineNumber: 3,
},
];
diff --git a/spec/frontend/ci/job_details/components/log/mock_data.js b/spec/frontend/ci/job_details/components/log/mock_data.js
index fa51b92a044..14669872cc1 100644
--- a/spec/frontend/ci/job_details/components/log/mock_data.js
+++ b/spec/frontend/ci/job_details/components/log/mock_data.js
@@ -1,4 +1,4 @@
-export const jobLog = [
+export const mockJobLog = [
{
offset: 1000,
content: [{ text: 'Running with gitlab-runner 12.1.0 (de7731dd)' }],
@@ -19,69 +19,50 @@ export const jobLog = [
},
{
offset: 1003,
- content: [{ text: 'Starting service postgres:9.6.14 ...', style: 'text-green' }],
+ content: [{ text: 'Docker executor with image registry.gitlab.com ...' }],
section: 'prepare-executor',
},
{
offset: 1004,
- content: [
- {
- text: 'Restore cache',
- style: 'term-fg-l-cyan term-bold',
- },
- ],
- section: 'restore-cache',
- section_header: true,
- section_options: {
- collapsed: 'true',
- },
+ content: [{ text: 'Starting service ...', style: 'term-fg-l-green' }],
+ section: 'prepare-executor',
},
{
offset: 1005,
- content: [
- {
- text: 'Checking cache for ruby-gems-debian-bullseye-ruby-3.0-16...',
- style: 'term-fg-l-green term-bold',
- },
- ],
- section: 'restore-cache',
- },
-];
-
-export const utilsMockData = [
- {
- offset: 1001,
- content: [{ text: ' on docker-auto-scale-com 8a6210b8' }],
+ content: [],
+ section: 'prepare-executor',
+ section_duration: '00:09',
},
{
- offset: 1002,
+ offset: 1006,
content: [
{
- text:
- 'Using Docker executor with image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.6-golang-1.14-git-2.28-lfs-2.9-chrome-84-node-12.x-yarn-1.21-postgresql-11-graphicsmagick-1.3.34',
+ text: 'Getting source from Git repository',
},
],
- section: 'prepare-executor',
+ section: 'get-sources',
section_header: true,
},
{
- offset: 1003,
- content: [{ text: 'Starting service postgres:9.6.14 ...' }],
- section: 'prepare-executor',
+ offset: 1007,
+ content: [{ text: 'Fetching changes with git depth set to 20...' }],
+ section: 'get-sources',
},
{
- offset: 1004,
- content: [{ text: 'Pulling docker image postgres:9.6.14 ...', style: 'term-fg-l-green' }],
- section: 'prepare-executor',
+ offset: 1008,
+ content: [{ text: 'Initialized empty Git repository', style: 'term-fg-l-green' }],
+ section: 'get-sources',
},
{
- offset: 1005,
+ offset: 1009,
content: [],
- section: 'prepare-executor',
- section_duration: '10:00',
+ section: 'get-sources',
+ section_duration: '00:19',
},
];
+export const mockJobLogLineCount = 8; // `text` entries in mockJobLog
+
export const originalTrace = [
{
offset: 1,
@@ -191,7 +172,7 @@ export const collapsibleSectionClosed = {
offset: 80,
content: [{ text: 'this is a collapsible nested section' }],
section: 'prepare-script',
- lineNumber: 3,
+ lineNumber: 2,
},
],
};
@@ -212,7 +193,7 @@ export const collapsibleSectionOpened = {
offset: 80,
content: [{ text: 'this is a collapsible nested section' }],
section: 'prepare-script',
- lineNumber: 3,
+ lineNumber: 2,
},
],
};
diff --git a/spec/frontend/ci/job_details/components/sidebar/artifacts_block_spec.js b/spec/frontend/ci/job_details/components/sidebar/artifacts_block_spec.js
index 1d61bf3243f..e539be2b220 100644
--- a/spec/frontend/ci/job_details/components/sidebar/artifacts_block_spec.js
+++ b/spec/frontend/ci/job_details/components/sidebar/artifacts_block_spec.js
@@ -30,31 +30,31 @@ describe('Artifacts block', () => {
'These artifacts are the latest. They will not be deleted (even if expired) until newer artifacts are available.';
const expiredArtifact = {
- expire_at: expireAt,
+ expireAt,
expired: true,
locked: false,
};
const nonExpiredArtifact = {
- download_path: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/download',
- browse_path: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/browse',
- keep_path: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/keep',
- expire_at: expireAt,
+ downloadPath: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/download',
+ browsePath: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/browse',
+ keepPath: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/keep',
+ expireAt,
expired: false,
locked: false,
};
const lockedExpiredArtifact = {
...expiredArtifact,
- download_path: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/download',
- browse_path: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/browse',
+ downloadPath: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/download',
+ browsePath: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/browse',
expired: true,
locked: true,
};
const lockedNonExpiredArtifact = {
...nonExpiredArtifact,
- keep_path: undefined,
+ keepPath: undefined,
locked: true,
};
diff --git a/spec/frontend/ci/job_details/components/sidebar/sidebar_header_spec.js b/spec/frontend/ci/job_details/components/sidebar/sidebar_header_spec.js
index 1063bec6f3b..81181fc71b2 100644
--- a/spec/frontend/ci/job_details/components/sidebar/sidebar_header_spec.js
+++ b/spec/frontend/ci/job_details/components/sidebar/sidebar_header_spec.js
@@ -55,15 +55,9 @@ describe('Sidebar Header', () => {
const findEraseButton = () => wrapper.findByTestId('job-log-erase-link');
const findNewIssueButton = () => wrapper.findByTestId('job-new-issue');
const findTerminalLink = () => wrapper.findByTestId('terminal-link');
- const findJobName = () => wrapper.findByTestId('job-name');
const findRetryButton = () => wrapper.findComponent(JobRetryButton);
describe('when rendering contents', () => {
- it('renders the correct job name', async () => {
- await createComponentWithApollo();
- expect(findJobName().text()).toBe(mockJobResponse.data.project.job.name);
- });
-
it('does not render buttons with no paths', async () => {
await createComponentWithApollo();
expect(findCancelButton().exists()).toBe(false);
diff --git a/spec/frontend/ci/job_details/components/sidebar/sidebar_job_details_container_spec.js b/spec/frontend/ci/job_details/components/sidebar/sidebar_job_details_container_spec.js
index e188d99b8b1..37a2ca75df0 100644
--- a/spec/frontend/ci/job_details/components/sidebar/sidebar_job_details_container_spec.js
+++ b/spec/frontend/ci/job_details/components/sidebar/sidebar_job_details_container_spec.js
@@ -53,7 +53,6 @@ describe('Job Sidebar Details Container', () => {
['erased_at', 'Erased: 3 weeks ago'],
['finished_at', 'Finished: 3 weeks ago'],
['queued_duration', 'Queued: 9 seconds'],
- ['id', 'Job ID: #4757'],
['runner', 'Runner: #1 (ABCDEFGH) local ci runner'],
['coverage', 'Coverage: 20%'],
])('uses %s to render job-%s', async (detail, value) => {
@@ -78,7 +77,7 @@ describe('Job Sidebar Details Container', () => {
createWrapper();
await store.dispatch('receiveJobSuccess', job);
- expect(findAllDetailsRow()).toHaveLength(8);
+ expect(findAllDetailsRow()).toHaveLength(7);
});
describe('duration row', () => {
diff --git a/spec/frontend/ci/job_details/job_app_spec.js b/spec/frontend/ci/job_details/job_app_spec.js
index c2d91771495..ff84b2d0283 100644
--- a/spec/frontend/ci/job_details/job_app_spec.js
+++ b/spec/frontend/ci/job_details/job_app_spec.js
@@ -31,8 +31,6 @@ describe('Job App', () => {
const initSettings = {
endpoint: `${TEST_HOST}jobs/123.json`,
pagePath: `${TEST_HOST}jobs/123`,
- logState:
- 'eyJvZmZzZXQiOjE3NDUxLCJuX29wZW5fdGFncyI6MCwiZmdfY29sb3IiOm51bGwsImJnX2NvbG9yIjpudWxsLCJzdHlsZV9tYXNrIjowfQ%3D%3D',
};
const props = {
diff --git a/spec/frontend/ci/job_details/store/actions_spec.js b/spec/frontend/ci/job_details/store/actions_spec.js
index bb5c1fe32bd..2799bc9578c 100644
--- a/spec/frontend/ci/job_details/store/actions_spec.js
+++ b/spec/frontend/ci/job_details/store/actions_spec.js
@@ -2,7 +2,6 @@ import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
import {
- setJobEndpoint,
setJobLogOptions,
clearEtagPoll,
stopPolling,
@@ -39,25 +38,21 @@ describe('Job State actions', () => {
mockedState = state();
});
- describe('setJobEndpoint', () => {
- it('should commit SET_JOB_ENDPOINT mutation', () => {
- return testAction(
- setJobEndpoint,
- 'job/872324.json',
- mockedState,
- [{ type: types.SET_JOB_ENDPOINT, payload: 'job/872324.json' }],
- [],
- );
- });
- });
-
describe('setJobLogOptions', () => {
it('should commit SET_JOB_LOG_OPTIONS mutation', () => {
return testAction(
setJobLogOptions,
- { pagePath: 'job/872324/trace.json' },
+ { endpoint: '/group1/project1/-/jobs/99.json', pagePath: '/group1/project1/-/jobs/99' },
mockedState,
- [{ type: types.SET_JOB_LOG_OPTIONS, payload: { pagePath: 'job/872324/trace.json' } }],
+ [
+ {
+ type: types.SET_JOB_LOG_OPTIONS,
+ payload: {
+ endpoint: '/group1/project1/-/jobs/99.json',
+ pagePath: '/group1/project1/-/jobs/99',
+ },
+ },
+ ],
[],
);
});
diff --git a/spec/frontend/ci/job_details/store/mutations_spec.js b/spec/frontend/ci/job_details/store/mutations_spec.js
index 0835c534fb9..78b29efed68 100644
--- a/spec/frontend/ci/job_details/store/mutations_spec.js
+++ b/spec/frontend/ci/job_details/store/mutations_spec.js
@@ -12,11 +12,17 @@ describe('Jobs Store Mutations', () => {
stateCopy = state();
});
- describe('SET_JOB_ENDPOINT', () => {
+ describe('SET_JOB_LOG_OPTIONS', () => {
it('should set jobEndpoint', () => {
- mutations[types.SET_JOB_ENDPOINT](stateCopy, 'job/21312321.json');
+ mutations[types.SET_JOB_LOG_OPTIONS](stateCopy, {
+ endpoint: '/group1/project1/-/jobs/99.json',
+ pagePath: '/group1/project1/-/jobs/99',
+ });
- expect(stateCopy.jobEndpoint).toEqual('job/21312321.json');
+ expect(stateCopy).toMatchObject({
+ jobLogEndpoint: '/group1/project1/-/jobs/99',
+ jobEndpoint: '/group1/project1/-/jobs/99.json',
+ });
});
});
@@ -39,13 +45,13 @@ describe('Jobs Store Mutations', () => {
describe('RECEIVE_JOB_LOG_SUCCESS', () => {
describe('when job log has state', () => {
it('sets jobLogState', () => {
- const stateLog =
+ const logState =
'eyJvZmZzZXQiOjczNDQ1MSwibl9vcGVuX3RhZ3MiOjAsImZnX2NvbG9yIjpudWxsLCJiZ19jb2xvciI6bnVsbCwic3R5bGVfbWFzayI6MH0=';
mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, {
- state: stateLog,
+ state: logState,
});
- expect(stateCopy.jobLogState).toEqual(stateLog);
+ expect(stateCopy.jobLogState).toEqual(logState);
});
});
@@ -100,7 +106,7 @@ describe('Jobs Store Mutations', () => {
{
offset: 1,
content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }],
- lineNumber: 0,
+ lineNumber: 1,
},
]);
});
@@ -121,7 +127,7 @@ describe('Jobs Store Mutations', () => {
{
offset: 0,
content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }],
- lineNumber: 0,
+ lineNumber: 1,
},
]);
});
diff --git a/spec/frontend/ci/job_details/store/utils_spec.js b/spec/frontend/ci/job_details/store/utils_spec.js
index 4ffba35761e..394ce0ab737 100644
--- a/spec/frontend/ci/job_details/store/utils_spec.js
+++ b/spec/frontend/ci/job_details/store/utils_spec.js
@@ -6,10 +6,10 @@ import {
addDurationToHeader,
isCollapsibleSection,
findOffsetAndRemove,
- getIncrementalLineNumber,
+ getNextLineNumber,
} from '~/ci/job_details/store/utils';
import {
- utilsMockData,
+ mockJobLog,
originalTrace,
regularIncremental,
regularIncrementalRepeated,
@@ -187,39 +187,49 @@ describe('Jobs Store Utils', () => {
let result;
beforeEach(() => {
- result = logLinesParser(utilsMockData);
+ result = logLinesParser(mockJobLog);
});
describe('regular line', () => {
it('adds a lineNumber property with correct index', () => {
- expect(result[0].lineNumber).toEqual(0);
- expect(result[1].line.lineNumber).toEqual(1);
+ expect(result[0].lineNumber).toEqual(1);
+ expect(result[1].lineNumber).toEqual(2);
+ expect(result[2].line.lineNumber).toEqual(3);
+ expect(result[2].lines[0].lineNumber).toEqual(4);
+ expect(result[2].lines[1].lineNumber).toEqual(5);
+ expect(result[3].line.lineNumber).toEqual(6);
+ expect(result[3].lines[0].lineNumber).toEqual(7);
+ expect(result[3].lines[1].lineNumber).toEqual(8);
});
});
describe('collapsible section', () => {
it('adds a `isClosed` property', () => {
- expect(result[1].isClosed).toEqual(false);
+ expect(result[2].isClosed).toEqual(false);
+ expect(result[3].isClosed).toEqual(false);
});
it('adds a `isHeader` property', () => {
- expect(result[1].isHeader).toEqual(true);
+ expect(result[2].isHeader).toEqual(true);
+ expect(result[3].isHeader).toEqual(true);
});
it('creates a lines array property with the content of the collapsible section', () => {
- expect(result[1].lines.length).toEqual(2);
- expect(result[1].lines[0].content).toEqual(utilsMockData[2].content);
- expect(result[1].lines[1].content).toEqual(utilsMockData[3].content);
+ expect(result[2].lines.length).toEqual(2);
+ expect(result[2].lines[0].content).toEqual(mockJobLog[3].content);
+ expect(result[2].lines[1].content).toEqual(mockJobLog[4].content);
});
});
describe('section duration', () => {
it('adds the section information to the header section', () => {
- expect(result[1].line.section_duration).toEqual(utilsMockData[4].section_duration);
+ expect(result[2].line.section_duration).toEqual(mockJobLog[5].section_duration);
+ expect(result[3].line.section_duration).toEqual(mockJobLog[9].section_duration);
});
it('does not add section duration as a line', () => {
- expect(result[1].lines.includes(utilsMockData[4])).toEqual(false);
+ expect(result[2].lines.includes(mockJobLog[5])).toEqual(false);
+ expect(result[3].lines.includes(mockJobLog[9])).toEqual(false);
});
});
});
@@ -316,17 +326,24 @@ describe('Jobs Store Utils', () => {
});
});
- describe('getIncrementalLineNumber', () => {
- describe('when last line is 0', () => {
+ describe('getNextLineNumber', () => {
+ describe('when there is no previous log', () => {
+ it('returns 1', () => {
+ expect(getNextLineNumber([])).toEqual(1);
+ expect(getNextLineNumber(undefined)).toEqual(1);
+ });
+ });
+
+ describe('when last line is 1', () => {
it('returns 1', () => {
const log = [
{
content: [],
- lineNumber: 0,
+ lineNumber: 1,
},
];
- expect(getIncrementalLineNumber(log)).toEqual(1);
+ expect(getNextLineNumber(log)).toEqual(2);
});
});
@@ -343,7 +360,7 @@ describe('Jobs Store Utils', () => {
},
];
- expect(getIncrementalLineNumber(log)).toEqual(102);
+ expect(getNextLineNumber(log)).toEqual(102);
});
});
@@ -364,7 +381,7 @@ describe('Jobs Store Utils', () => {
},
];
- expect(getIncrementalLineNumber(log)).toEqual(102);
+ expect(getNextLineNumber(log)).toEqual(102);
});
});
@@ -391,7 +408,7 @@ describe('Jobs Store Utils', () => {
},
];
- expect(getIncrementalLineNumber(log)).toEqual(104);
+ expect(getNextLineNumber(log)).toEqual(104);
});
});
});
@@ -410,7 +427,7 @@ describe('Jobs Store Utils', () => {
text: 'Downloading',
},
],
- lineNumber: 0,
+ lineNumber: 1,
},
{
offset: 2,
@@ -419,7 +436,7 @@ describe('Jobs Store Utils', () => {
text: 'log line',
},
],
- lineNumber: 1,
+ lineNumber: 2,
},
]);
});
@@ -438,7 +455,7 @@ describe('Jobs Store Utils', () => {
text: 'log line',
},
],
- lineNumber: 0,
+ lineNumber: 1,
},
]);
});
@@ -462,7 +479,7 @@ describe('Jobs Store Utils', () => {
},
],
section: 'section',
- lineNumber: 0,
+ lineNumber: 1,
},
lines: [],
},
@@ -488,7 +505,7 @@ describe('Jobs Store Utils', () => {
},
],
section: 'section',
- lineNumber: 0,
+ lineNumber: 1,
},
lines: [
{
@@ -499,7 +516,7 @@ describe('Jobs Store Utils', () => {
},
],
section: 'section',
- lineNumber: 1,
+ lineNumber: 2,
},
],
},
diff --git a/spec/frontend/ci/jobs_page/components/job_cells/job_cell_spec.js b/spec/frontend/ci/jobs_page/components/job_cells/job_cell_spec.js
index cb8f6ed8f9b..bb44d970bd7 100644
--- a/spec/frontend/ci/jobs_page/components/job_cells/job_cell_spec.js
+++ b/spec/frontend/ci/jobs_page/components/job_cells/job_cell_spec.js
@@ -40,20 +40,20 @@ describe('Job Cell', () => {
};
describe('Job Id', () => {
- it('displays the job id and links to the job', () => {
+ it('displays the job id, job name and links to the job', () => {
createComponent();
- const expectedJobId = `#${getIdFromGraphQLId(mockJob.id)}`;
+ const expectedJobId = `#${getIdFromGraphQLId(mockJob.id)}: ${mockJob.name}`;
expect(findJobIdLink().text()).toBe(expectedJobId);
expect(findJobIdLink().attributes('href')).toBe(mockJob.detailedStatus.detailsPath);
expect(findJobIdNoLink().exists()).toBe(false);
});
- it('display the job id with no link', () => {
+ it('display the job id and job name with no link', () => {
createComponent(jobAsGuest);
- const expectedJobId = `#${getIdFromGraphQLId(jobAsGuest.id)}`;
+ const expectedJobId = `#${getIdFromGraphQLId(jobAsGuest.id)}: ${jobAsGuest.name}`;
expect(findJobIdNoLink().text()).toBe(expectedJobId);
expect(findJobIdNoLink().exists()).toBe(true);
diff --git a/spec/frontend/ci/jobs_page/components/job_cells/duration_cell_spec.js b/spec/frontend/ci/jobs_page/components/job_cells/status_cell_spec.js
index 21f14ba0c98..e66942cc730 100644
--- a/spec/frontend/ci/jobs_page/components/job_cells/duration_cell_spec.js
+++ b/spec/frontend/ci/jobs_page/components/job_cells/status_cell_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import DurationCell from '~/ci/jobs_page/components/job_cells/duration_cell.vue';
+import StatusCell from '~/ci/jobs_page/components/job_cells/status_cell.vue';
describe('Duration Cell', () => {
let wrapper;
@@ -12,7 +12,7 @@ describe('Duration Cell', () => {
const createComponent = (props) => {
wrapper = extendedWrapper(
- shallowMount(DurationCell, {
+ shallowMount(StatusCell, {
propsData: {
job: {
...props,
diff --git a/spec/frontend/ci/jobs_page/components/jobs_table_empty_state_spec.js b/spec/frontend/ci/jobs_page/components/jobs_table_empty_state_spec.js
index f4893c4077f..0f85c4590ec 100644
--- a/spec/frontend/ci/jobs_page/components/jobs_table_empty_state_spec.js
+++ b/spec/frontend/ci/jobs_page/components/jobs_table_empty_state_spec.js
@@ -6,7 +6,7 @@ describe('Jobs table empty state', () => {
let wrapper;
const pipelineEditorPath = '/root/project/-/ci/editor';
- const emptyStateSvgPath = 'assets/jobs-empty-state.svg';
+ const emptyStateSvgPath = 'illustrations/empty-state/empty-pipeline-md.svg';
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
diff --git a/spec/frontend/ci/jobs_page/components/jobs_table_spec.js b/spec/frontend/ci/jobs_page/components/jobs_table_spec.js
index 3adb95bf371..d4e0ce92bc2 100644
--- a/spec/frontend/ci/jobs_page/components/jobs_table_spec.js
+++ b/spec/frontend/ci/jobs_page/components/jobs_table_spec.js
@@ -2,6 +2,7 @@ import { GlTable } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import JobsTable from '~/ci/jobs_page/components/jobs_table.vue';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import { DEFAULT_FIELDS_ADMIN } from '~/ci/admin/jobs_table/constants';
import ProjectCell from '~/ci/admin/jobs_table/components/cells/project_cell.vue';
@@ -47,11 +48,11 @@ describe('Jobs Table', () => {
expect(findCiBadgeLink().exists()).toBe(true);
});
- it('displays the job stage and name', () => {
+ it('displays the job stage, id and name', () => {
const [firstJob] = mockJobsNodes;
- expect(findJobStage().text()).toBe(firstJob.stage.name);
- expect(findJobName().text()).toBe(firstJob.name);
+ expect(findJobStage().text()).toBe(`Stage: ${firstJob.stage.name}`);
+ expect(findJobName().text()).toBe(`#${getIdFromGraphQLId(firstJob.id)}: ${firstJob.name}`);
});
it('displays the coverage for only jobs that have coverage', () => {
diff --git a/spec/frontend/ci/pipeline_details/graph/components/job_item_spec.js b/spec/frontend/ci/pipeline_details/graph/components/job_item_spec.js
index 107f0df5c02..de9ee8a16bf 100644
--- a/spec/frontend/ci/pipeline_details/graph/components/job_item_spec.js
+++ b/spec/frontend/ci/pipeline_details/graph/components/job_item_spec.js
@@ -1,10 +1,11 @@
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
-import { GlBadge, GlModal, GlToast } from '@gitlab/ui';
+import { GlModal, GlToast } from '@gitlab/ui';
import JobItem from '~/ci/pipeline_details/graph/components/job_item.vue';
import axios from '~/lib/utils/axios_utils';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import ActionComponent from '~/ci/common/private/job_action_component.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import {
@@ -27,9 +28,10 @@ describe('pipeline graph job item', () => {
const findJobWithoutLink = () => wrapper.findByTestId('job-without-link');
const findJobWithLink = () => wrapper.findByTestId('job-with-link');
const findActionVueComponent = () => wrapper.findComponent(ActionComponent);
- const findActionComponent = () => wrapper.findByTestId('ci-action-component');
- const findBadge = () => wrapper.findComponent(GlBadge);
+ const findActionComponent = () => wrapper.findByTestId('ci-action-button');
+ const findBadge = () => wrapper.findByTestId('job-bridge-badge');
const findJobLink = () => wrapper.findByTestId('job-with-link');
+ const findJobCiBadge = () => wrapper.findComponent(CiBadgeLink);
const findModal = () => wrapper.findComponent(GlModal);
const clickOnModalPrimaryBtn = () => findModal().vm.$emit('primary');
@@ -57,6 +59,9 @@ describe('pipeline graph job item', () => {
mocks: {
...mocks,
},
+ stubs: {
+ CiBadgeLink,
+ },
});
};
@@ -81,7 +86,8 @@ describe('pipeline graph job item', () => {
expect(link.attributes('title')).toBe(`${mockJob.name} - ${mockJob.status.label}`);
- expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true);
+ expect(findJobCiBadge().exists()).toBe(true);
+ expect(findJobCiBadge().find('.ci-status-icon-success').exists()).toBe(true);
expect(wrapper.text()).toBe(mockJob.name);
});
@@ -99,7 +105,8 @@ describe('pipeline graph job item', () => {
});
it('should render status and name', () => {
- expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true);
+ expect(findJobCiBadge().exists()).toBe(true);
+ expect(findJobCiBadge().find('.ci-status-icon-success').exists()).toBe(true);
expect(findJobLink().exists()).toBe(false);
expect(wrapper.text()).toBe(mockJobWithoutDetails.name);
@@ -110,6 +117,15 @@ describe('pipeline graph job item', () => {
});
});
+ describe('CiBadgeLink', () => {
+ it('should not render a link', () => {
+ createWrapper();
+
+ expect(findJobCiBadge().exists()).toBe(true);
+ expect(findJobCiBadge().props('useLink')).toBe(false);
+ });
+ });
+
describe('action icon', () => {
it('should render the action icon', () => {
createWrapper();
diff --git a/spec/frontend/ci/pipeline_details/graph/components/linked_pipeline_spec.js b/spec/frontend/ci/pipeline_details/graph/components/linked_pipeline_spec.js
index 5541b0db54a..5fe8581e81b 100644
--- a/spec/frontend/ci/pipeline_details/graph/components/linked_pipeline_spec.js
+++ b/spec/frontend/ci/pipeline_details/graph/components/linked_pipeline_spec.js
@@ -37,7 +37,7 @@ describe('Linked pipeline', () => {
const findButton = () => wrapper.findComponent(GlButton);
const findCancelButton = () => wrapper.findByLabelText('Cancel downstream pipeline');
const findCardTooltip = () => wrapper.findComponent(GlTooltip);
- const findDownstreamPipelineTitle = () => wrapper.findByTestId('downstream-title');
+ const findDownstreamPipelineTitle = () => wrapper.findByTestId('downstream-title-content');
const findExpandButton = () => wrapper.findByTestId('expand-pipeline-button');
const findLinkedPipeline = () => wrapper.findComponent({ ref: 'linkedPipeline' });
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
diff --git a/spec/frontend/ci/pipeline_details/mock_data.js b/spec/frontend/ci/pipeline_details/mock_data.js
index e32d0a0df47..56365622544 100644
--- a/spec/frontend/ci/pipeline_details/mock_data.js
+++ b/spec/frontend/ci/pipeline_details/mock_data.js
@@ -640,7 +640,7 @@ export const mockPipeline = (projectPath) => {
triggered_by: null,
triggered: [],
},
- pipelineScheduleUrl: 'foo',
+ pipelineSchedulesPath: 'foo',
pipelineKey: 'id',
viewType: 'root',
};
@@ -865,7 +865,7 @@ export const mockPipelineTag = () => {
triggered_by: null,
triggered: [],
},
- pipelineScheduleUrl: 'foo',
+ pipelineSchedulesPath: 'foo',
pipelineKey: 'id',
viewType: 'root',
};
@@ -1072,7 +1072,7 @@ export const mockPipelineBranch = () => {
triggered_by: null,
triggered: [],
},
- pipelineScheduleUrl: 'foo',
+ pipelineSchedulesPath: 'foo',
pipelineKey: 'id',
viewType: 'root',
};
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 1a2ed60a6f4..9bb0618b758 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
@@ -1,4 +1,4 @@
-import { GlIcon, GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
+import { GlIcon, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
@@ -43,7 +43,7 @@ describe('Pipeline Status', () => {
},
projectFullPath: mockProjectFullPath,
},
- stubs: { GlLink, GlSprintf },
+ stubs: { GlSprintf },
});
};
diff --git a/spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js b/spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js
index 30a0b868c5f..4b357a9fc7c 100644
--- a/spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js
+++ b/spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js
@@ -2,7 +2,7 @@ import { GlDropdown } from '@gitlab/ui';
import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import LegacyPipelineStage from '~/ci/pipeline_mini_graph/legacy_pipeline_stage.vue';
@@ -52,7 +52,7 @@ describe('Pipelines stage component', () => {
});
const findCiActionBtn = () => wrapper.find('.js-ci-action');
- const findCiIcon = () => wrapper.findComponent(CiIcon);
+ const findCiIcon = () => wrapper.findComponent(CiBadgeLink);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownToggle = () => wrapper.find('button.dropdown-toggle');
const findDropdownMenu = () =>
@@ -106,17 +106,6 @@ describe('Pipelines stage component', () => {
expect(findDropdownToggle().exists()).toBe(true);
expect(findCiIcon().exists()).toBe(true);
});
-
- it('renders a borderless ci-icon', () => {
- expect(findCiIcon().exists()).toBe(true);
- expect(findCiIcon().props('isBorderless')).toBe(true);
- expect(findCiIcon().classes('borderless')).toBe(true);
- });
-
- it('renders a ci-icon with a custom border class', () => {
- expect(findCiIcon().exists()).toBe(true);
- expect(findCiIcon().classes('gl-border')).toBe(true);
- });
});
describe('when user opens dropdown and stage request is successful', () => {
diff --git a/spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mini_list_spec.js b/spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mini_list_spec.js
index 0396029cdaf..3c9d235bfcc 100644
--- a/spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mini_list_spec.js
+++ b/spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mini_list_spec.js
@@ -50,19 +50,6 @@ describe('Linked pipeline mini list', () => {
expect(findCiIcon().exists()).toBe(true);
});
- it('should render a borderless ci-icon', () => {
- expect(findCiIcon().exists()).toBe(true);
-
- expect(findCiIcon().props('isBorderless')).toBe(true);
- expect(findCiIcon().classes('borderless')).toBe(true);
- });
-
- it('should render a ci-icon with a custom border class', () => {
- expect(findCiIcon().exists()).toBe(true);
-
- expect(findCiIcon().classes('gl-border')).toBe(true);
- });
-
it('should render the correct ci status icon', () => {
expect(findCiIcon().classes('ci-status-icon-running')).toBe(true);
});
@@ -124,19 +111,6 @@ describe('Linked pipeline mini list', () => {
expect(findLinkedPipelineMiniList().classes('is-downstream')).toBe(true);
});
- it('should render a borderless ci-icon', () => {
- expect(findCiIcon().exists()).toBe(true);
-
- expect(findCiIcon().props('isBorderless')).toBe(true);
- expect(findCiIcon().classes('borderless')).toBe(true);
- });
-
- it('should render a ci-icon with a custom border class', () => {
- expect(findCiIcon().exists()).toBe(true);
-
- expect(findCiIcon().classes('gl-border')).toBe(true);
- });
-
it('should render the pipeline counter', () => {
expect(findLinkedPipelineCounter().exists()).toBe(true);
});
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 1d4ae33c667..2807cc0f2a1 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
@@ -55,12 +55,12 @@ describe('Pipeline New Form', () => {
const findForm = () => wrapper.findComponent(GlForm);
const findRefsDropdown = () => wrapper.findComponent(RefsDropdown);
- const findSubmitButton = () => wrapper.findByTestId('run_pipeline_button');
- const findVariableRows = () => wrapper.findAllByTestId('ci-variable-row');
+ const findSubmitButton = () => wrapper.findByTestId('run-pipeline-button');
+ const findVariableRows = () => wrapper.findAllByTestId('ci-variable-row-container');
const findRemoveIcons = () => wrapper.findAllByTestId('remove-ci-variable-row');
const findVariableTypes = () => wrapper.findAllByTestId('pipeline-form-ci-variable-type');
- const findKeyInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-key');
- const findValueInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-value');
+ const findKeyInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-key-field');
+ const findValueInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-value-field');
const findValueDropdowns = () =>
wrapper.findAllByTestId('pipeline-form-ci-variable-value-dropdown');
const findValueDropdownItems = (dropdown) => dropdown.findAllComponents(GlDropdownItem);
diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_empty_state_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_empty_state_spec.js
new file mode 100644
index 00000000000..5ad0f915f62
--- /dev/null
+++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_empty_state_spec.js
@@ -0,0 +1,37 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
+import PipelineSchedulesEmptyState from '~/ci/pipeline_schedules/components/pipeline_schedules_empty_state.vue';
+
+describe('Pipeline Schedules Empty State', () => {
+ let wrapper;
+
+ const mockSchedulePath = 'root/test/-/pipeline_schedules/new"';
+
+ const createComponent = () => {
+ wrapper = shallowMount(PipelineSchedulesEmptyState, {
+ provide: {
+ newSchedulePath: mockSchedulePath,
+ },
+ stubs: { GlSprintf },
+ });
+ };
+
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findLink = () => wrapper.findComponent(GlLink);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows empty state', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ it('has link to create new schedule', () => {
+ expect(findEmptyState().props('primaryButtonLink')).toBe(mockSchedulePath);
+ });
+
+ it('has link to help documentation', () => {
+ expect(findLink().attributes('href')).toBe('/help/ci/pipelines/schedules');
+ });
+});
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 eb76b0bfbb4..d1844d609f2 100644
--- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlEmptyState, GlLink, GlLoadingIcon, GlTabs } from '@gitlab/ui';
+import { GlAlert, GlEmptyState, GlLink, GlLoadingIcon, GlPagination, GlTabs } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { trimText } from 'helpers/text_helper';
@@ -14,6 +14,7 @@ import deletePipelineScheduleMutation from '~/ci/pipeline_schedules/graphql/muta
import playPipelineScheduleMutation from '~/ci/pipeline_schedules/graphql/mutations/play_pipeline_schedule.mutation.graphql';
import takeOwnershipMutation from '~/ci/pipeline_schedules/graphql/mutations/take_ownership.mutation.graphql';
import getPipelineSchedulesQuery from '~/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql';
+import { SCHEDULES_PER_PAGE } from '~/ci/pipeline_schedules/constants';
import {
mockGetPipelineSchedulesGraphQLResponse,
mockPipelineScheduleNodes,
@@ -22,6 +23,7 @@ import {
playMutationResponse,
takeOwnershipMutationResponse,
emptyPipelineSchedulesResponse,
+ mockPipelineSchedulesResponseWithPagination,
} from '../mock_data';
Vue.use(VueApollo);
@@ -34,6 +36,9 @@ describe('Pipeline schedules app', () => {
let wrapper;
const successHandler = jest.fn().mockResolvedValue(mockGetPipelineSchedulesGraphQLResponse);
+ const successHandlerWithPagination = jest
+ .fn()
+ .mockResolvedValue(mockPipelineSchedulesResponseWithPagination);
const successEmptyHandler = jest.fn().mockResolvedValue(emptyPipelineSchedulesResponse);
const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
@@ -81,6 +86,11 @@ describe('Pipeline schedules app', () => {
const findInactiveTab = () => wrapper.findByTestId('pipeline-schedules-inactive-tab');
const findSchedulesCharacteristics = () =>
wrapper.findByTestId('pipeline-schedules-characteristics');
+ const findPagination = () => wrapper.findComponent(GlPagination);
+ const setPage = async (page) => {
+ findPagination().vm.$emit('input', page);
+ await waitForPromises();
+ };
describe('default', () => {
beforeEach(() => {
@@ -107,6 +117,10 @@ describe('Pipeline schedules app', () => {
it('new schedule button links to new schedule path', () => {
expect(findNewButton().attributes('href')).toBe('/root/ci-project/-/pipeline_schedules/new');
});
+
+ it('does not display pagination when no next page exists', () => {
+ expect(findPagination().exists()).toBe(false);
+ });
});
describe('fetching pipeline schedules', () => {
@@ -333,6 +347,10 @@ describe('Pipeline schedules app', () => {
ids: null,
projectPath: 'gitlab-org/gitlab',
status: null,
+ first: SCHEDULES_PER_PAGE,
+ last: null,
+ nextPageCursor: '',
+ prevPageCursor: '',
});
});
});
@@ -370,4 +388,57 @@ describe('Pipeline schedules app', () => {
});
});
});
+
+ describe('pagination', () => {
+ const { pageInfo } = mockPipelineSchedulesResponseWithPagination.data.project.pipelineSchedules;
+
+ beforeEach(async () => {
+ createComponent([[getPipelineSchedulesQuery, successHandlerWithPagination]]);
+
+ await waitForPromises();
+ });
+
+ it('displays pagination', () => {
+ expect(findPagination().exists()).toBe(true);
+ expect(findPagination().props()).toMatchObject({
+ value: 1,
+ prevPage: Number(pageInfo.hasPreviousPage),
+ nextPage: Number(pageInfo.hasNextPage),
+ });
+ expect(successHandlerWithPagination).toHaveBeenCalledWith({
+ projectPath: 'gitlab-org/gitlab',
+ ids: null,
+ first: SCHEDULES_PER_PAGE,
+ last: null,
+ nextPageCursor: '',
+ prevPageCursor: '',
+ });
+ });
+
+ it('updates query variables when going to next page', async () => {
+ await setPage(2);
+
+ expect(successHandlerWithPagination).toHaveBeenCalledWith({
+ projectPath: 'gitlab-org/gitlab',
+ ids: null,
+ first: SCHEDULES_PER_PAGE,
+ last: null,
+ prevPageCursor: '',
+ nextPageCursor: pageInfo.endCursor,
+ });
+ expect(findPagination().props('value')).toEqual(2);
+ });
+
+ it('when switching tabs pagination should reset', async () => {
+ await setPage(2);
+
+ expect(findPagination().props('value')).toEqual(2);
+
+ await findInactiveTab().trigger('click');
+
+ await waitForPromises();
+
+ expect(findPagination().props('value')).toEqual(1);
+ });
+ });
});
diff --git a/spec/frontend/ci/pipeline_schedules/mock_data.js b/spec/frontend/ci/pipeline_schedules/mock_data.js
index 711b120c61e..1bff296305d 100644
--- a/spec/frontend/ci/pipeline_schedules/mock_data.js
+++ b/spec/frontend/ci/pipeline_schedules/mock_data.js
@@ -48,6 +48,26 @@ export const mockSinglePipelineScheduleNodeNoVars = {
},
};
+export const mockPipelineSchedulesResponseWithPagination = {
+ data: {
+ currentUser: mockGetPipelineSchedulesGraphQLResponse.data.currentUser,
+ project: {
+ id: mockGetPipelineSchedulesGraphQLResponse.data.project.id,
+ pipelineSchedules: {
+ count: 3,
+ nodes: mockGetPipelineSchedulesGraphQLResponse.data.project.pipelineSchedules.nodes,
+ pageInfo: {
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'eyJpZCI6IjQ0In0',
+ endCursor: 'eyJpZCI6IjI4In0',
+ __typename: 'PageInfo',
+ },
+ },
+ },
+ },
+};
+
export const emptyPipelineSchedulesResponse = {
data: {
currentUser: {
@@ -59,6 +79,13 @@ export const emptyPipelineSchedulesResponse = {
pipelineSchedules: {
count: 0,
nodes: [],
+ pageInfo: {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: '',
+ endCursor: '',
+ __typename: 'PageInfo',
+ },
},
},
},
diff --git a/spec/frontend/ci/pipelines_page/components/pipeline_labels_spec.js b/spec/frontend/ci/pipelines_page/components/pipeline_labels_spec.js
index b5c9a3030e0..6b0d5b18f7d 100644
--- a/spec/frontend/ci/pipelines_page/components/pipeline_labels_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/pipeline_labels_spec.js
@@ -15,6 +15,7 @@ describe('Pipeline label component', () => {
const findAutoDevopsTag = () => wrapper.findByTestId('pipeline-url-autodevops');
const findAutoDevopsTagLink = () => wrapper.findByTestId('pipeline-url-autodevops-link');
const findDetachedTag = () => wrapper.findByTestId('pipeline-url-detached');
+ const findMergedResultsTag = () => wrapper.findByTestId('pipeline-url-merged-results');
const findFailureTag = () => wrapper.findByTestId('pipeline-url-failure');
const findForkTag = () => wrapper.findByTestId('pipeline-url-fork');
const findTrainTag = () => wrapper.findByTestId('pipeline-url-train');
@@ -25,6 +26,7 @@ describe('Pipeline label component', () => {
wrapper = shallowMountExtended(PipelineLabelsComponent, {
propsData: { ...defaultProps, ...props },
provide: {
+ pipelineSchedulesPath: 'group/project/-/schedules',
targetProjectFullPath: projectPath,
},
});
@@ -41,6 +43,7 @@ describe('Pipeline label component', () => {
expect(findScheduledTag().exists()).toBe(false);
expect(findForkTag().exists()).toBe(false);
expect(findTrainTag().exists()).toBe(false);
+ expect(findMergedResultsTag().exists()).toBe(false);
});
it('should render the stuck tag when flag is provided', () => {
@@ -140,9 +143,33 @@ describe('Pipeline label component', () => {
expect(findForkTag().text()).toBe('fork');
});
+ it('should render the merged results badge when the pipeline is a merged results pipeline', () => {
+ const mergedResultsPipeline = defaultProps.pipeline;
+ mergedResultsPipeline.flags.merged_result_pipeline = true;
+
+ createComponent({
+ ...mergedResultsPipeline,
+ });
+
+ expect(findMergedResultsTag().text()).toBe('merged results');
+ });
+
+ it('should not render the merged results badge when the pipeline is not a merged results pipeline', () => {
+ const mergedResultsPipeline = defaultProps.pipeline;
+ mergedResultsPipeline.flags.merged_result_pipeline = false;
+
+ createComponent({
+ ...mergedResultsPipeline,
+ });
+
+ expect(findMergedResultsTag().exists()).toBe(false);
+ });
+
it('should render the train badge when the pipeline is a merge train pipeline', () => {
const mergeTrainPipeline = defaultProps.pipeline;
mergeTrainPipeline.flags.merge_train_pipeline = true;
+ // a merge train pipeline is also a merged results pipeline
+ mergeTrainPipeline.flags.merged_result_pipeline = true;
createComponent({
...mergeTrainPipeline,
@@ -161,4 +188,17 @@ describe('Pipeline label component', () => {
expect(findTrainTag().exists()).toBe(false);
});
+
+ it('should not render the merged results badge when the pipeline is a merge train pipeline', () => {
+ const mergeTrainPipeline = defaultProps.pipeline;
+ mergeTrainPipeline.flags.merge_train_pipeline = true;
+ // a merge train pipeline is also a merged results pipeline
+ mergeTrainPipeline.flags.merged_result_pipeline = true;
+
+ createComponent({
+ ...mergeTrainPipeline,
+ });
+
+ expect(findMergedResultsTag().exists()).toBe(false);
+ });
});
diff --git a/spec/frontend/ci/pipelines_page/components/pipeline_operations_spec.js b/spec/frontend/ci/pipelines_page/components/pipeline_operations_spec.js
index d2eab64b317..6205a37e291 100644
--- a/spec/frontend/ci/pipelines_page/components/pipeline_operations_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/pipeline_operations_spec.js
@@ -1,10 +1,13 @@
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PipelinesManualActions from '~/ci/pipelines_page/components/pipelines_manual_actions.vue';
import PipelineMultiActions from '~/ci/pipelines_page/components/pipeline_multi_actions.vue';
import PipelineOperations from '~/ci/pipelines_page/components/pipeline_operations.vue';
-import eventHub from '~/ci/event_hub';
+import PipelineStopModal from '~/ci/pipelines_page/components/pipeline_stop_modal.vue';
+import { TRACKING_CATEGORIES } from '~/ci/constants';
describe('Pipeline operations', () => {
+ let trackingSpy;
let wrapper;
const defaultProps = {
@@ -36,6 +39,7 @@ describe('Pipeline operations', () => {
const findMultiActions = () => wrapper.findComponent(PipelineMultiActions);
const findRetryBtn = () => wrapper.findByTestId('pipelines-retry-button');
const findCancelBtn = () => wrapper.findByTestId('pipelines-cancel-button');
+ const findPipelineStopModal = () => wrapper.findComponent(PipelineStopModal);
it('should display pipeline manual actions', () => {
createComponent();
@@ -49,28 +53,71 @@ describe('Pipeline operations', () => {
expect(findMultiActions().exists()).toBe(true);
});
+ it('does not show the confirmation modal', () => {
+ createComponent();
+
+ expect(findPipelineStopModal().props().showConfirmationModal).toBe(false);
+ });
+
+ describe('when cancelling a pipeline', () => {
+ beforeEach(async () => {
+ createComponent();
+ await findCancelBtn().vm.$emit('click');
+ });
+
+ it('should show a confirmation modal', () => {
+ expect(findPipelineStopModal().props().showConfirmationModal).toBe(true);
+ });
+
+ it('should emit cancel-pipeline event when confirming', async () => {
+ await findPipelineStopModal().vm.$emit('submit');
+
+ expect(wrapper.emitted('cancel-pipeline')).toEqual([[defaultProps.pipeline]]);
+ expect(findPipelineStopModal().props().showConfirmationModal).toBe(false);
+ });
+
+ it('should hide the modal when closing', async () => {
+ await findPipelineStopModal().vm.$emit('close-modal');
+
+ expect(findPipelineStopModal().props().showConfirmationModal).toBe(false);
+ });
+ });
+
describe('events', () => {
beforeEach(() => {
createComponent();
-
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
});
it('should emit retryPipeline event', () => {
findRetryBtn().vm.$emit('click');
- expect(eventHub.$emit).toHaveBeenCalledWith(
- 'retryPipeline',
- defaultProps.pipeline.retry_path,
- );
+ expect(wrapper.emitted('retry-pipeline')).toEqual([[defaultProps.pipeline]]);
+ });
+ });
+
+ describe('tracking', () => {
+ beforeEach(() => {
+ createComponent();
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('tracks retry pipeline button click', () => {
+ findRetryBtn().vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_retry_button', {
+ label: TRACKING_CATEGORIES.table,
+ });
});
- it('should emit openConfirmationModal event', () => {
+ it('tracks cancel pipeline button click', () => {
findCancelBtn().vm.$emit('click');
- expect(eventHub.$emit).toHaveBeenCalledWith('openConfirmationModal', {
- pipeline: defaultProps.pipeline,
- endpoint: defaultProps.pipeline.cancel_path,
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_cancel_button', {
+ label: TRACKING_CATEGORIES.table,
});
});
});
diff --git a/spec/frontend/ci/pipelines_page/components/pipeline_stop_modal_spec.js b/spec/frontend/ci/pipelines_page/components/pipeline_stop_modal_spec.js
index 4d78a923542..1e276840c07 100644
--- a/spec/frontend/ci/pipelines_page/components/pipeline_stop_modal_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/pipeline_stop_modal_spec.js
@@ -1,15 +1,17 @@
import { shallowMount } from '@vue/test-utils';
-import { GlSprintf } from '@gitlab/ui';
+import { GlModal, GlSprintf } from '@gitlab/ui';
import { mockPipelineHeader } from 'jest/ci/pipeline_details/mock_data';
import PipelineStopModal from '~/ci/pipelines_page/components/pipeline_stop_modal.vue';
describe('PipelineStopModal', () => {
let wrapper;
- const createComponent = () => {
+ const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(PipelineStopModal, {
propsData: {
pipeline: mockPipelineHeader,
+ showConfirmationModal: false,
+ ...props,
},
stubs: {
GlSprintf,
@@ -17,11 +19,43 @@ describe('PipelineStopModal', () => {
});
};
+ const findModal = () => wrapper.findComponent(GlModal);
+
beforeEach(() => {
createComponent();
});
- it('should render "stop pipeline" warning', () => {
- expect(wrapper.text()).toMatch(`You’re about to stop pipeline #${mockPipelineHeader.id}.`);
+ describe('when `showConfirmationModal` is false', () => {
+ it('passes the visiblity value to the modal', () => {
+ expect(findModal().props().visible).toBe(false);
+ });
+ });
+
+ describe('when `showConfirmationModal` is true', () => {
+ beforeEach(() => {
+ createComponent({ props: { showConfirmationModal: true } });
+ });
+
+ it('passes the visiblity value to the modal', () => {
+ expect(findModal().props().visible).toBe(true);
+ });
+
+ it('renders "stop pipeline" warning', () => {
+ expect(wrapper.text()).toMatch(`You're about to stop pipeline #${mockPipelineHeader.id}.`);
+ });
+ });
+
+ describe('events', () => {
+ beforeEach(() => {
+ createComponent({ props: { showConfirmationModal: true } });
+ });
+
+ it('emits the close-modal event when the visiblity changes', async () => {
+ expect(wrapper.emitted('close-modal')).toBeUndefined();
+
+ await findModal().vm.$emit('change', false);
+
+ expect(wrapper.emitted('close-modal')).toEqual([[]]);
+ });
});
});
diff --git a/spec/frontend/ci/pipelines_page/pipelines_spec.js b/spec/frontend/ci/pipelines_page/pipelines_spec.js
index 5d1f431e57c..fd95f98e7f8 100644
--- a/spec/frontend/ci/pipelines_page/pipelines_spec.js
+++ b/spec/frontend/ci/pipelines_page/pipelines_spec.js
@@ -28,7 +28,7 @@ import NavigationControls from '~/ci/pipelines_page/components/nav_controls.vue'
import PipelinesComponent from '~/ci/pipelines_page/pipelines.vue';
import PipelinesCiTemplates from '~/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue';
import PipelinesTableComponent from '~/ci/common/pipelines_table.vue';
-import { RAW_TEXT_WARNING, TRACKING_CATEGORIES } from '~/ci/constants';
+import { PIPELINE_IID_KEY, RAW_TEXT_WARNING, TRACKING_CATEGORIES } from '~/ci/constants';
import Store from '~/ci/pipeline_details/stores/pipelines_store';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
@@ -57,28 +57,23 @@ describe('Pipelines', () => {
let mockApollo;
let mock;
let trackingSpy;
+ let mutationMock;
- const paths = {
- emptyStateSvgPath: '/assets/illustrations/empty-state/empty-pipeline-md.svg',
- errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
- noPipelinesSvgPath: '/assets/illustrations/empty-state/empty-pipeline-md.svg',
+ const withPermissionsProps = {
ciLintPath: '/ci/lint',
resetCachePath: `${mockProjectPath}/settings/ci_cd/reset_cache`,
newPipelinePath: `${mockProjectPath}/pipelines/new`,
-
ciRunnerSettingsPath: `${mockProjectPath}/-/settings/ci_cd#js-runners-settings`,
- };
-
- const noPermissions = {
- emptyStateSvgPath: '/assets/illustrations/empty-state/empty-pipeline-md.svg',
- errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
- noPipelinesSvgPath: '/assets/illustrations/empty-state/empty-pipeline-md.svg',
+ canCreatePipeline: true,
};
const defaultProps = {
hasGitlabCi: true,
- canCreatePipeline: true,
- ...paths,
+ canCreatePipeline: false,
+ projectId: mockProjectId,
+ defaultBranchName: mockDefaultBranchName,
+ endpoint: mockPipelinesEndpoint,
+ params: {},
};
const findFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
@@ -87,10 +82,9 @@ describe('Pipelines', () => {
const findNavigationControls = () => wrapper.findComponent(NavigationControls);
const findPipelinesTable = () => wrapper.findComponent(PipelinesTableComponent);
const findTablePagination = () => wrapper.findComponent(TablePagination);
- const findPipelineKeyCollapsibleBoxVue = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findPipelineKeyCollapsibleBox = () => wrapper.findComponent(GlCollapsibleListbox);
const findTab = (tab) => wrapper.findByTestId(`pipelines-tab-${tab}`);
- const findPipelineKeyCollapsibleBox = () => wrapper.findByTestId('pipeline-key-collapsible-box');
const findRunPipelineButton = () => wrapper.findByTestId('run-pipeline-button');
const findCiLintButton = () => wrapper.findByTestId('ci-lint-button');
const findCleanCacheButton = () => wrapper.findByTestId('clear-cache-button');
@@ -98,25 +92,23 @@ describe('Pipelines', () => {
wrapper.find('[data-testid="mini-pipeline-graph-dropdown"] .dropdown-toggle');
const findPipelineUrlLinks = () => wrapper.findAll('[data-testid="pipeline-url-link"]');
- const createComponent = (props = defaultProps) => {
- const { mutationMock, ...restProps } = props;
+ const createComponent = ({ props = {}, withPermissions = true } = {}) => {
mockApollo = createMockApollo([[setSortPreferenceMutation, mutationMock]]);
+ const permissionsProps = withPermissions ? { ...withPermissionsProps } : {};
wrapper = extendedWrapper(
mount(PipelinesComponent, {
provide: {
pipelineEditorPath: '',
suggestedCiTemplates: [],
- ciRunnerSettingsPath: paths.ciRunnerSettingsPath,
+ ciRunnerSettingsPath: defaultProps.ciRunnerSettingsPath,
anyRunnersAvailable: true,
},
propsData: {
+ ...defaultProps,
+ ...permissionsProps,
+ ...props,
store: new Store(),
- projectId: mockProjectId,
- defaultBranchName: mockDefaultBranchName,
- endpoint: mockPipelinesEndpoint,
- params: {},
- ...restProps,
},
apolloProvider: mockApollo,
}),
@@ -124,12 +116,11 @@ describe('Pipelines', () => {
};
beforeEach(() => {
- setWindowLocation(TEST_HOST);
- });
-
- beforeEach(() => {
mock = new MockAdapter(axios);
+ setWindowLocation(TEST_HOST);
+ mutationMock = jest.fn();
+
jest.spyOn(window.history, 'pushState');
jest.spyOn(Api, 'projectUsers').mockResolvedValue(users);
jest.spyOn(Api, 'branches').mockResolvedValue({ data: branches });
@@ -169,7 +160,9 @@ describe('Pipelines', () => {
describe('when user has no permissions', () => {
beforeEach(async () => {
- createComponent({ hasGitlabCi: true, canCreatePipeline: false, ...noPermissions });
+ createComponent({
+ withPermissions: false,
+ });
await waitForPromises();
});
@@ -225,11 +218,13 @@ describe('Pipelines', () => {
});
it('renders Run pipeline link', () => {
- expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath);
+ expect(findRunPipelineButton().attributes('href')).toBe(
+ withPermissionsProps.newPipelinePath,
+ );
});
it('renders CI lint link', () => {
- expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath);
+ expect(findCiLintButton().attributes('href')).toBe(withPermissionsProps.ciLintPath);
});
it('renders Clear runner cache button', () => {
@@ -382,7 +377,7 @@ describe('Pipelines', () => {
it('should change the text to Show Pipeline IID', async () => {
expect(findPipelineKeyCollapsibleBox().exists()).toBe(true);
expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockFilteredPipeline.id}`);
- findPipelineKeyCollapsibleBoxVue().vm.$emit('select', 'iid');
+ findPipelineKeyCollapsibleBox().vm.$emit('select', PIPELINE_IID_KEY);
await waitForPromises();
@@ -390,21 +385,21 @@ describe('Pipelines', () => {
});
it('calls mutation to save idType preference', () => {
- const mutationMock = jest.fn().mockResolvedValue(setIdTypePreferenceMutationResponse);
- createComponent({ ...defaultProps, mutationMock });
+ mutationMock = jest.fn().mockResolvedValue(setIdTypePreferenceMutationResponse);
+ createComponent();
- findPipelineKeyCollapsibleBoxVue().vm.$emit('select', 'iid');
+ findPipelineKeyCollapsibleBox().vm.$emit('select', PIPELINE_IID_KEY);
- expect(mutationMock).toHaveBeenCalledWith({ input: { visibilityPipelineIdType: 'IID' } });
+ expect(mutationMock).toHaveBeenCalledWith({
+ input: { visibilityPipelineIdType: PIPELINE_IID_KEY.toUpperCase() },
+ });
});
it('captures error when mutation response has errors', async () => {
- const mutationMock = jest
- .fn()
- .mockResolvedValue(setIdTypePreferenceMutationResponseWithErrors);
- createComponent({ ...defaultProps, mutationMock });
+ mutationMock = jest.fn().mockResolvedValue(setIdTypePreferenceMutationResponseWithErrors);
+ createComponent();
- findPipelineKeyCollapsibleBoxVue().vm.$emit('select', 'iid');
+ findPipelineKeyCollapsibleBox().vm.$emit('select', PIPELINE_IID_KEY);
await waitForPromises();
expect(Sentry.captureException).toHaveBeenCalledWith(new Error('oh no!'));
@@ -610,11 +605,13 @@ describe('Pipelines', () => {
});
it('renders Run pipeline link', () => {
- expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath);
+ expect(findRunPipelineButton().attributes('href')).toBe(
+ withPermissionsProps.newPipelinePath,
+ );
});
it('renders CI lint link', () => {
- expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath);
+ expect(findCiLintButton().attributes('href')).toBe(withPermissionsProps.ciLintPath);
});
it('renders Clear runner cache button', () => {
@@ -651,7 +648,7 @@ describe('Pipelines', () => {
describe('when CI is not enabled and user has permissions', () => {
beforeEach(async () => {
- createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
+ createComponent({ props: { hasGitlabCi: false } });
await waitForPromises();
});
@@ -678,7 +675,7 @@ describe('Pipelines', () => {
describe('when CI is not enabled and user has no permissions', () => {
beforeEach(async () => {
- createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions });
+ createComponent({ props: { hasGitlabCi: false }, withPermissions: false });
await waitForPromises();
});
@@ -700,7 +697,7 @@ describe('Pipelines', () => {
describe('when CI is enabled and user has no permissions', () => {
beforeEach(() => {
- createComponent({ hasGitlabCi: true, canCreatePipeline: false, ...noPermissions });
+ createComponent({ props: { hasGitlabCi: true }, withPermissions: false });
return waitForPromises();
});
@@ -798,8 +795,10 @@ describe('Pipelines', () => {
describe('when user has no permissions', () => {
beforeEach(async () => {
- createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...noPermissions });
-
+ createComponent({
+ props: { hasGitlabCi: false },
+ withPermissions: false,
+ });
await waitForPromises();
});
@@ -834,9 +833,11 @@ describe('Pipelines', () => {
});
it('renders buttons', () => {
- expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath);
+ expect(findRunPipelineButton().attributes('href')).toBe(
+ withPermissionsProps.newPipelinePath,
+ );
- expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath);
+ expect(findCiLintButton().attributes('href')).toBe(withPermissionsProps.ciLintPath);
expect(findCleanCacheButton().text()).toBe('Clear runner caches');
});
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 c9349c64bfb..4a75c353487 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
@@ -103,11 +103,6 @@ describe('AdminRunnerShowApp', () => {
it('shows basic runner details', () => {
const expected = `Description My Runner
Last contact Never contacted
- Version 1.0.0
- IP Address None
- Executor None
- Architecture None
- Platform darwin
Configuration Runs untagged jobs
Maximum job timeout None
Token expiry
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 1bbcb991619..bc28147db27 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
@@ -156,9 +156,7 @@ describe('AdminRunnersApp', () => {
await createComponent({ mountFn: mountExtended });
});
- // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/414975
- // eslint-disable-next-line jest/no-disabled-tests
- it.skip('fetches counts', () => {
+ it('fetches counts', () => {
expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES);
});
diff --git a/spec/frontend/ci/runner/components/runner_details_spec.js b/spec/frontend/ci/runner/components/runner_details_spec.js
index cc91340655b..9d5f89a2642 100644
--- a/spec/frontend/ci/runner/components/runner_details_spec.js
+++ b/spec/frontend/ci/runner/components/runner_details_spec.js
@@ -49,13 +49,6 @@ describe('RunnerDetails', () => {
${'Description'} | ${{ description: null }} | ${'None'}
${'Last contact'} | ${{ contactedAt: mockOneHourAgo }} | ${'1 hour ago'}
${'Last contact'} | ${{ contactedAt: null }} | ${'Never contacted'}
- ${'Version'} | ${{ version: '12.3' }} | ${'12.3'}
- ${'Version'} | ${{ version: null }} | ${'None'}
- ${'Executor'} | ${{ executorName: 'shell' }} | ${'shell'}
- ${'Architecture'} | ${{ architectureName: 'amd64' }} | ${'amd64'}
- ${'Platform'} | ${{ platformName: 'darwin' }} | ${'darwin'}
- ${'IP Address'} | ${{ ipAddress: '127.0.0.1' }} | ${'127.0.0.1'}
- ${'IP Address'} | ${{ ipAddress: null }} | ${'None'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: true }} | ${'Protected, Runs untagged jobs'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: false }} | ${'Protected'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED, runUntagged: true }} | ${'Runs untagged jobs'}
diff --git a/spec/frontend/ci/runner/components/runner_details_tabs_spec.js b/spec/frontend/ci/runner/components/runner_details_tabs_spec.js
index 689d0575726..516209794ad 100644
--- a/spec/frontend/ci/runner/components/runner_details_tabs_spec.js
+++ b/spec/frontend/ci/runner/components/runner_details_tabs_spec.js
@@ -54,7 +54,7 @@ describe('RunnerDetailsTabs', () => {
...options,
});
- routerPush = jest.spyOn(wrapper.vm.$router, 'push').mockImplementation(() => {});
+ routerPush = jest.spyOn(wrapper.vm.$router, 'push');
return waitForPromises();
};
@@ -67,9 +67,8 @@ describe('RunnerDetailsTabs', () => {
});
it('shows runner jobs', async () => {
- setWindowLocation(`#${JOBS_ROUTE_PATH}`);
-
- await createComponent({ mountFn: mountExtended });
+ createComponent({ mountFn: mountExtended });
+ await wrapper.vm.$router.push({ path: JOBS_ROUTE_PATH });
expect(findRunnerDetails().exists()).toBe(false);
expect(findRunnerJobs().props('runner')).toBe(mockRunner);
@@ -101,10 +100,9 @@ describe('RunnerDetailsTabs', () => {
}
});
- it.each(['#/', '#/unknown-tab'])('shows details when location hash is `%s`', async (hash) => {
- setWindowLocation(hash);
-
- await createComponent({ mountFn: mountExtended });
+ it.each(['#/', '#/unknown-tab'])('shows details when location hash is `%s`', async (path) => {
+ createComponent({ mountFn: mountExtended });
+ await wrapper.vm.$router.push({ path });
expect(findTabs().props('value')).toBe(0);
expect(findRunnerDetails().exists()).toBe(true);
diff --git a/spec/frontend/ci/runner/components/runner_list_spec.js b/spec/frontend/ci/runner/components/runner_list_spec.js
index 9da640afeb7..7c00aa48d31 100644
--- a/spec/frontend/ci/runner/components/runner_list_spec.js
+++ b/spec/frontend/ci/runner/components/runner_list_spec.js
@@ -1,14 +1,11 @@
import { GlTableLite, GlSkeletonLoader } from '@gitlab/ui';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
-import {
- extendedWrapper,
- shallowMountExtended,
- mountExtended,
-} from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { createLocalState } from '~/ci/runner/graphql/list/local_state';
+import { stubComponent } from 'helpers/stub_component';
import RunnerList from '~/ci/runner/components/runner_list.vue';
import RunnerBulkDelete from '~/ci/runner/components/runner_bulk_delete.vue';
@@ -29,14 +26,11 @@ describe('RunnerList', () => {
const findHeaders = () => wrapper.findAll('th');
const findRows = () => wrapper.findAll('[data-testid^="runner-row-"]');
const findCell = ({ row = 0, fieldKey }) =>
- extendedWrapper(findRows().at(row).find(`[data-testid="td-${fieldKey}"]`));
+ findRows().at(row).find(`[data-testid="td-${fieldKey}"]`);
const findRunnerBulkDelete = () => wrapper.findComponent(RunnerBulkDelete);
const findRunnerBulkDeleteCheckbox = () => wrapper.findComponent(RunnerBulkDeleteCheckbox);
- const createComponent = (
- { props = {}, provide = {}, ...options } = {},
- mountFn = shallowMountExtended,
- ) => {
+ const createComponent = ({ props = {}, ...options } = {}, mountFn = shallowMountExtended) => {
({ cacheConfig, localMutations } = createLocalState());
wrapper = mountFn(RunnerList, {
@@ -49,7 +43,6 @@ describe('RunnerList', () => {
localMutations,
onlineContactTimeoutSecs,
staleTimeoutSecs,
- ...provide,
},
...options,
});
@@ -81,7 +74,11 @@ describe('RunnerList', () => {
});
it('Sets runner id as a row key', () => {
- createComponent();
+ createComponent({
+ stubs: {
+ GlTableLite: stubComponent(GlTableLite),
+ },
+ });
expect(findTable().attributes('primary-key')).toBe('id');
});
@@ -220,7 +217,12 @@ describe('RunnerList', () => {
describe('When data is loading', () => {
it('shows a busy state', () => {
- createComponent({ props: { runners: [], loading: true } });
+ createComponent({
+ props: { runners: [], loading: true },
+ stubs: {
+ GlTableLite: stubComponent(GlTableLite),
+ },
+ });
expect(findTable().classes('gl-opacity-6')).toBe(true);
});
diff --git a/spec/frontend/ci/runner/components/runner_type_icon_spec.js b/spec/frontend/ci/runner/components/runner_type_icon_spec.js
new file mode 100644
index 00000000000..01f3de10aa6
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_type_icon_spec.js
@@ -0,0 +1,67 @@
+import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import RunnerTypeIcon from '~/ci/runner/components/runner_type_icon.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { assertProps } from 'helpers/assert_props';
+import {
+ INSTANCE_TYPE,
+ GROUP_TYPE,
+ PROJECT_TYPE,
+ I18N_INSTANCE_TYPE,
+ I18N_GROUP_TYPE,
+ I18N_PROJECT_TYPE,
+} from '~/ci/runner/constants';
+
+describe('RunnerTypeIcon', () => {
+ let wrapper;
+
+ const findIcon = () => wrapper.findComponent(GlIcon);
+ const getTooltip = () => getBinding(findIcon().element, 'gl-tooltip');
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMount(RunnerTypeIcon, {
+ propsData: {
+ ...props,
+ },
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
+ });
+ };
+
+ describe.each`
+ type | tooltipText
+ ${INSTANCE_TYPE} | ${I18N_INSTANCE_TYPE}
+ ${GROUP_TYPE} | ${I18N_GROUP_TYPE}
+ ${PROJECT_TYPE} | ${I18N_PROJECT_TYPE}
+ `('displays $type runner', ({ type, tooltipText }) => {
+ beforeEach(() => {
+ createComponent({ props: { type } });
+ });
+
+ it(`with no text`, () => {
+ expect(findIcon().text()).toBe('');
+ });
+
+ it(`with aria-label`, () => {
+ expect(findIcon().props('ariaLabel')).toBeDefined();
+ });
+
+ it('with a tooltip', () => {
+ expect(getTooltip().value).toBeDefined();
+ expect(getTooltip().value).toContain(tooltipText);
+ });
+ });
+
+ it('validation fails for an incorrect type', () => {
+ expect(() => {
+ assertProps(RunnerTypeIcon, { type: 'AN_UNKNOWN_VALUE' });
+ }).toThrow();
+ });
+
+ it('does not render content when type is missing', () => {
+ createComponent({ props: { type: undefined } });
+
+ expect(findIcon().exists()).toBe(false);
+ });
+});
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 7438c47e32c..8258bd1d507 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
@@ -108,11 +108,6 @@ describe('GroupRunnerShowApp', () => {
it('shows basic runner details', () => {
const expected = `Description My Runner
Last contact Never contacted
- Version 1.0.0
- IP Address None
- Executor None
- Architecture None
- Platform darwin
Configuration Runs untagged jobs
Maximum job timeout None
Token expiry
diff --git a/spec/frontend/ci/runner/sentry_utils_spec.js b/spec/frontend/ci/runner/sentry_utils_spec.js
index 2f17cc43ac5..59d386a5899 100644
--- a/spec/frontend/ci/runner/sentry_utils_spec.js
+++ b/spec/frontend/ci/runner/sentry_utils_spec.js
@@ -4,24 +4,12 @@ import { captureException } from '~/ci/runner/sentry_utils';
jest.mock('@sentry/browser');
describe('~/ci/runner/sentry_utils', () => {
- let mockSetTag;
-
- beforeEach(() => {
- mockSetTag = jest.fn();
-
- Sentry.withScope.mockImplementation((fn) => {
- const scope = { setTag: mockSetTag };
- fn(scope);
- });
- });
-
describe('captureException', () => {
const mockError = new Error('Something went wrong!');
it('error is reported to sentry', () => {
captureException({ error: mockError });
- expect(Sentry.withScope).toHaveBeenCalled();
expect(Sentry.captureException).toHaveBeenCalledWith(mockError);
});
@@ -30,10 +18,11 @@ describe('~/ci/runner/sentry_utils', () => {
captureException({ error: mockError, component: mockComponentName });
- expect(Sentry.withScope).toHaveBeenCalled();
- expect(Sentry.captureException).toHaveBeenCalledWith(mockError);
-
- expect(mockSetTag).toHaveBeenCalledWith('vue_component', mockComponentName);
+ expect(Sentry.captureException).toHaveBeenCalledWith(mockError, {
+ tags: {
+ vue_component: mockComponentName,
+ },
+ });
});
});
});