diff options
Diffstat (limited to 'spec/frontend/jobs/components')
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); + }); }); |