summaryrefslogtreecommitdiff
path: root/spec/frontend/jobs/components
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/jobs/components')
-rw-r--r--spec/frontend/jobs/components/job_app_spec.js1
-rw-r--r--spec/frontend/jobs/components/table/cells.vue/duration_cell_spec.js81
-rw-r--r--spec/frontend/jobs/components/table/cells.vue/job_cell_spec.js140
-rw-r--r--spec/frontend/jobs/components/table/cells.vue/pipeline_cell_spec.js82
-rw-r--r--spec/frontend/jobs/components/table/job_table_app_spec.js110
-rw-r--r--spec/frontend/jobs/components/table/jobs_table_empty_state_spec.js37
-rw-r--r--spec/frontend/jobs/components/table/jobs_table_spec.js49
7 files changed, 492 insertions, 8 deletions
diff --git a/spec/frontend/jobs/components/job_app_spec.js b/spec/frontend/jobs/components/job_app_spec.js
index 2974e91e46d..3fcefde1aba 100644
--- a/spec/frontend/jobs/components/job_app_spec.js
+++ b/spec/frontend/jobs/components/job_app_spec.js
@@ -35,6 +35,7 @@ describe('Job App', () => {
const props = {
artifactHelpUrl: 'help/artifact',
deploymentHelpUrl: 'help/deployment',
+ codeQualityHelpPath: '/help/code_quality',
runnerSettingsUrl: 'settings/ci-cd/runners',
variablesSettingsUrl: 'settings/ci-cd/variables',
terminalPath: 'jobs/123/terminal',
diff --git a/spec/frontend/jobs/components/table/cells.vue/duration_cell_spec.js b/spec/frontend/jobs/components/table/cells.vue/duration_cell_spec.js
new file mode 100644
index 00000000000..763a4b0eaa2
--- /dev/null
+++ b/spec/frontend/jobs/components/table/cells.vue/duration_cell_spec.js
@@ -0,0 +1,81 @@
+import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import DurationCell from '~/jobs/components/table/cells/duration_cell.vue';
+
+describe('Duration Cell', () => {
+ let wrapper;
+
+ const findJobDuration = () => wrapper.findByTestId('job-duration');
+ const findJobFinishedTime = () => wrapper.findByTestId('job-finished-time');
+ const findDurationIcon = () => wrapper.findByTestId('duration-icon');
+ const findFinishedTimeIcon = () => wrapper.findByTestId('finished-time-icon');
+
+ const createComponent = (props) => {
+ wrapper = extendedWrapper(
+ shallowMount(DurationCell, {
+ propsData: {
+ job: {
+ ...props,
+ },
+ },
+ }),
+ );
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('does not display duration or finished time when no properties are present', () => {
+ createComponent();
+
+ expect(findJobDuration().exists()).toBe(false);
+ expect(findJobFinishedTime().exists()).toBe(false);
+ });
+
+ it('displays duration and finished time when both properties are present', () => {
+ const props = {
+ duration: 7,
+ finishedAt: '2021-04-26T13:37:52Z',
+ };
+
+ createComponent(props);
+
+ expect(findJobDuration().exists()).toBe(true);
+ expect(findJobFinishedTime().exists()).toBe(true);
+ });
+
+ it('displays only the duration of the job when the duration property is present', () => {
+ const props = {
+ duration: 7,
+ };
+
+ createComponent(props);
+
+ expect(findJobDuration().exists()).toBe(true);
+ expect(findJobFinishedTime().exists()).toBe(false);
+ });
+
+ it('displays only the finished time of the job when the finshedAt property is present', () => {
+ const props = {
+ finishedAt: '2021-04-26T13:37:52Z',
+ };
+
+ createComponent(props);
+
+ expect(findJobFinishedTime().exists()).toBe(true);
+ expect(findJobDuration().exists()).toBe(false);
+ });
+
+ it('displays icons for finished time and duration', () => {
+ const props = {
+ duration: 7,
+ finishedAt: '2021-04-26T13:37:52Z',
+ };
+
+ createComponent(props);
+
+ expect(findFinishedTimeIcon().props('name')).toBe('calendar');
+ expect(findDurationIcon().props('name')).toBe('timer');
+ });
+});
diff --git a/spec/frontend/jobs/components/table/cells.vue/job_cell_spec.js b/spec/frontend/jobs/components/table/cells.vue/job_cell_spec.js
new file mode 100644
index 00000000000..fc4e5586349
--- /dev/null
+++ b/spec/frontend/jobs/components/table/cells.vue/job_cell_spec.js
@@ -0,0 +1,140 @@
+import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import JobCell from '~/jobs/components/table/cells/job_cell.vue';
+import { mockJobsInTable } from '../../../mock_data';
+
+const mockJob = mockJobsInTable[0];
+const mockJobCreatedByTag = mockJobsInTable[1];
+const mockJobLimitedAccess = mockJobsInTable[2];
+const mockStuckJob = mockJobsInTable[3];
+
+describe('Job Cell', () => {
+ let wrapper;
+
+ const findJobIdLink = () => wrapper.findByTestId('job-id-link');
+ const findJobIdNoLink = () => wrapper.findByTestId('job-id-limited-access');
+ const findJobRef = () => wrapper.findByTestId('job-ref');
+ const findJobSha = () => wrapper.findByTestId('job-sha');
+ const findLabelIcon = () => wrapper.findByTestId('label-icon');
+ const findForkIcon = () => wrapper.findByTestId('fork-icon');
+ const findStuckIcon = () => wrapper.findByTestId('stuck-icon');
+ const findAllTagBadges = () => wrapper.findAllByTestId('job-tag-badge');
+
+ const findBadgeById = (id) => wrapper.findByTestId(id);
+
+ const createComponent = (jobData = mockJob) => {
+ wrapper = extendedWrapper(
+ shallowMount(JobCell, {
+ propsData: {
+ job: jobData,
+ },
+ }),
+ );
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('Job Id', () => {
+ it('displays the job id and links to the job', () => {
+ createComponent();
+
+ const expectedJobId = `#${getIdFromGraphQLId(mockJob.id)}`;
+
+ 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', () => {
+ createComponent(mockJobLimitedAccess);
+
+ const expectedJobId = `#${getIdFromGraphQLId(mockJobLimitedAccess.id)}`;
+
+ expect(findJobIdNoLink().text()).toBe(expectedJobId);
+ expect(findJobIdNoLink().exists()).toBe(true);
+ expect(findJobIdLink().exists()).toBe(false);
+ });
+ });
+
+ describe('Ref of the job', () => {
+ it('displays the ref name and links to the ref', () => {
+ createComponent();
+
+ expect(findJobRef().text()).toBe(mockJob.refName);
+ expect(findJobRef().attributes('href')).toBe(mockJob.refPath);
+ });
+
+ it('displays fork icon when job is not created by tag', () => {
+ createComponent();
+
+ expect(findForkIcon().exists()).toBe(true);
+ expect(findLabelIcon().exists()).toBe(false);
+ });
+
+ it('displays label icon when job is created by a tag', () => {
+ createComponent(mockJobCreatedByTag);
+
+ expect(findLabelIcon().exists()).toBe(true);
+ expect(findForkIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('Commit of the job', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('displays the sha and links to the commit', () => {
+ expect(findJobSha().text()).toBe(mockJob.shortSha);
+ expect(findJobSha().attributes('href')).toBe(mockJob.commitPath);
+ });
+ });
+
+ describe('Job badges', () => {
+ it('displays tags of the job', () => {
+ const mockJobWithTags = {
+ tags: ['tag-1', 'tag-2', 'tag-3'],
+ };
+
+ createComponent(mockJobWithTags);
+
+ expect(findAllTagBadges()).toHaveLength(mockJobWithTags.tags.length);
+ });
+
+ it.each`
+ testId | text
+ ${'manual-job-badge'} | ${'manual'}
+ ${'triggered-job-badge'} | ${'triggered'}
+ ${'fail-job-badge'} | ${'allowed to fail'}
+ ${'delayed-job-badge'} | ${'delayed'}
+ `('displays the static $text badge', ({ testId, text }) => {
+ createComponent({
+ manualJob: true,
+ triggered: true,
+ allowFailure: true,
+ scheduledAt: '2021-03-09T14:58:50+00:00',
+ });
+
+ expect(findBadgeById(testId).exists()).toBe(true);
+ expect(findBadgeById(testId).text()).toBe(text);
+ });
+ });
+
+ describe('Job icons', () => {
+ it('stuck icon is not shown if job is not stuck', () => {
+ createComponent();
+
+ expect(findStuckIcon().exists()).toBe(false);
+ });
+
+ it('stuck icon is shown if job is stuck', () => {
+ createComponent(mockStuckJob);
+
+ expect(findStuckIcon().exists()).toBe(true);
+ expect(findStuckIcon().attributes('name')).toBe('warning');
+ });
+ });
+});
diff --git a/spec/frontend/jobs/components/table/cells.vue/pipeline_cell_spec.js b/spec/frontend/jobs/components/table/cells.vue/pipeline_cell_spec.js
new file mode 100644
index 00000000000..1f5e0a7aa21
--- /dev/null
+++ b/spec/frontend/jobs/components/table/cells.vue/pipeline_cell_spec.js
@@ -0,0 +1,82 @@
+import { GlAvatar } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import PipelineCell from '~/jobs/components/table/cells/pipeline_cell.vue';
+
+const mockJobWithoutUser = {
+ id: 'gid://gitlab/Ci::Build/2264',
+ pipeline: {
+ id: 'gid://gitlab/Ci::Pipeline/460',
+ path: '/root/ci-project/-/pipelines/460',
+ },
+};
+
+const mockJobWithUser = {
+ id: 'gid://gitlab/Ci::Build/2264',
+ pipeline: {
+ id: 'gid://gitlab/Ci::Pipeline/460',
+ path: '/root/ci-project/-/pipelines/460',
+ user: {
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ webPath: '/root',
+ },
+ },
+};
+
+describe('Pipeline Cell', () => {
+ let wrapper;
+
+ const findPipelineId = () => wrapper.findByTestId('pipeline-id');
+ const findPipelineUserLink = () => wrapper.findByTestId('pipeline-user-link');
+ const findUserAvatar = () => wrapper.findComponent(GlAvatar);
+
+ const createComponent = (props = mockJobWithUser) => {
+ wrapper = extendedWrapper(
+ shallowMount(PipelineCell, {
+ propsData: {
+ job: props,
+ },
+ }),
+ );
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('Pipeline Id', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('displays the pipeline id and links to the pipeline', () => {
+ const expectedPipelineId = `#${getIdFromGraphQLId(mockJobWithUser.pipeline.id)}`;
+
+ expect(findPipelineId().text()).toBe(expectedPipelineId);
+ expect(findPipelineId().attributes('href')).toBe(mockJobWithUser.pipeline.path);
+ });
+ });
+
+ describe('Pipeline created by', () => {
+ const apiWrapperText = 'API';
+
+ it('shows and links to the pipeline user', () => {
+ createComponent();
+
+ expect(findPipelineUserLink().exists()).toBe(true);
+ expect(findPipelineUserLink().attributes('href')).toBe(mockJobWithUser.pipeline.user.webPath);
+ expect(findUserAvatar().attributes('src')).toBe(mockJobWithUser.pipeline.user.avatarUrl);
+ expect(wrapper.text()).not.toContain(apiWrapperText);
+ });
+
+ it('shows pipeline was created by the API', () => {
+ createComponent(mockJobWithoutUser);
+
+ expect(findPipelineUserLink().exists()).toBe(false);
+ expect(findUserAvatar().exists()).toBe(false);
+ expect(wrapper.text()).toContain(apiWrapperText);
+ });
+ });
+});
diff --git a/spec/frontend/jobs/components/table/job_table_app_spec.js b/spec/frontend/jobs/components/table/job_table_app_spec.js
new file mode 100644
index 00000000000..9d1135e26c8
--- /dev/null
+++ b/spec/frontend/jobs/components/table/job_table_app_spec.js
@@ -0,0 +1,110 @@
+import { GlSkeletonLoader, GlAlert, GlEmptyState } from '@gitlab/ui';
+import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import getJobsQuery from '~/jobs/components/table/graphql/queries/get_jobs.query.graphql';
+import JobsTable from '~/jobs/components/table/jobs_table.vue';
+import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue';
+import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue';
+import { mockJobsQueryResponse, mockJobsQueryEmptyResponse } from '../../mock_data';
+
+const projectPath = 'gitlab-org/gitlab';
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+describe('Job table app', () => {
+ let wrapper;
+
+ const successHandler = jest.fn().mockResolvedValue(mockJobsQueryResponse);
+ const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+ const emptyHandler = jest.fn().mockResolvedValue(mockJobsQueryEmptyResponse);
+
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+ const findTable = () => wrapper.findComponent(JobsTable);
+ const findTabs = () => wrapper.findComponent(JobsTableTabs);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+
+ const createMockApolloProvider = (handler) => {
+ const requestHandlers = [[getJobsQuery, handler]];
+
+ return createMockApollo(requestHandlers);
+ };
+
+ const createComponent = (handler = successHandler, mountFn = shallowMount) => {
+ wrapper = mountFn(JobsTableApp, {
+ provide: {
+ projectPath,
+ },
+ localVue,
+ apolloProvider: createMockApolloProvider(handler),
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('loading state', () => {
+ it('should display skeleton loader when loading', () => {
+ createComponent();
+
+ expect(findSkeletonLoader().exists()).toBe(true);
+ expect(findTable().exists()).toBe(false);
+ });
+ });
+
+ describe('loaded state', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('should display the jobs table with data', () => {
+ expect(findTable().exists()).toBe(true);
+ expect(findSkeletonLoader().exists()).toBe(false);
+ });
+
+ it('should retfech jobs query on fetchJobsByStatus event', async () => {
+ jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn());
+
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
+
+ await findTabs().vm.$emit('fetchJobsByStatus');
+
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('error state', () => {
+ it('should show an alert if there is an error fetching the data', async () => {
+ createComponent(failedHandler);
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ });
+ });
+
+ describe('empty state', () => {
+ it('should display empty state if there are no jobs and tab scope is null', async () => {
+ createComponent(emptyHandler, mount);
+
+ await waitForPromises();
+
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findTable().exists()).toBe(false);
+ });
+
+ it('should not display empty state if there are jobs and tab scope is not null', async () => {
+ createComponent(successHandler, mount);
+
+ await waitForPromises();
+
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findTable().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/jobs/components/table/jobs_table_empty_state_spec.js b/spec/frontend/jobs/components/table/jobs_table_empty_state_spec.js
new file mode 100644
index 00000000000..05b066a9edc
--- /dev/null
+++ b/spec/frontend/jobs/components/table/jobs_table_empty_state_spec.js
@@ -0,0 +1,37 @@
+import { GlEmptyState } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import JobsTableEmptyState from '~/jobs/components/table/jobs_table_empty_state.vue';
+
+describe('Jobs table empty state', () => {
+ let wrapper;
+
+ const pipelineEditorPath = '/root/project/-/ci/editor';
+ const emptyStateSvgPath = 'assets/jobs-empty-state.svg';
+
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+
+ const createComponent = () => {
+ wrapper = shallowMount(JobsTableEmptyState, {
+ provide: {
+ pipelineEditorPath,
+ emptyStateSvgPath,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('displays empty state', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ it('links to the pipeline editor', () => {
+ expect(findEmptyState().props('primaryButtonLink')).toBe(pipelineEditorPath);
+ });
+
+ it('shows an empty state image', () => {
+ expect(findEmptyState().props('svgPath')).toBe(emptyStateSvgPath);
+ });
+});
diff --git a/spec/frontend/jobs/components/table/jobs_table_spec.js b/spec/frontend/jobs/components/table/jobs_table_spec.js
index db057efbfb4..ac8bef675f8 100644
--- a/spec/frontend/jobs/components/table/jobs_table_spec.js
+++ b/spec/frontend/jobs/components/table/jobs_table_spec.js
@@ -1,20 +1,29 @@
import { GlTable } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import JobsTable from '~/jobs/components/table/jobs_table.vue';
+import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
import { mockJobsInTable } from '../../mock_data';
describe('Jobs Table', () => {
let wrapper;
const findTable = () => wrapper.findComponent(GlTable);
+ const findStatusBadge = () => wrapper.findComponent(CiBadge);
+ const findTableRows = () => wrapper.findAllByTestId('jobs-table-row');
+ const findJobStage = () => wrapper.findByTestId('job-stage-name');
+ const findJobName = () => wrapper.findByTestId('job-name');
+ const findAllCoverageJobs = () => wrapper.findAllByTestId('job-coverage');
const createComponent = (props = {}) => {
- wrapper = shallowMount(JobsTable, {
- propsData: {
- jobs: mockJobsInTable,
- ...props,
- },
- });
+ wrapper = extendedWrapper(
+ mount(JobsTable, {
+ propsData: {
+ jobs: mockJobsInTable,
+ ...props,
+ },
+ }),
+ );
};
beforeEach(() => {
@@ -25,7 +34,31 @@ describe('Jobs Table', () => {
wrapper.destroy();
});
- it('displays a table', () => {
+ it('displays the jobs table', () => {
expect(findTable().exists()).toBe(true);
});
+
+ it('displays correct number of job rows', () => {
+ expect(findTableRows()).toHaveLength(mockJobsInTable.length);
+ });
+
+ it('displays job status', () => {
+ expect(findStatusBadge().exists()).toBe(true);
+ });
+
+ it('displays the job stage and name', () => {
+ const firstJob = mockJobsInTable[0];
+
+ expect(findJobStage().text()).toBe(firstJob.stage.name);
+ expect(findJobName().text()).toBe(firstJob.name);
+ });
+
+ it('displays the coverage for only jobs that have coverage', () => {
+ const jobsThatHaveCoverage = mockJobsInTable.filter((job) => job.coverage !== null);
+
+ jobsThatHaveCoverage.forEach((job, index) => {
+ expect(findAllCoverageJobs().at(index).text()).toBe(`${job.coverage}%`);
+ });
+ expect(findAllCoverageJobs()).toHaveLength(jobsThatHaveCoverage.length);
+ });
});