diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-22 03:10:13 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-22 03:10:13 +0300 |
commit | 72023ba299df4aede839a0846688c3d8f0b096a1 (patch) | |
tree | 56f7fd59dc800979b3f10729e03c83b2735b5d8c /spec/frontend | |
parent | aa10c5ed22ed20df96b5d12b5e8921971408e410 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend')
3 files changed, 235 insertions, 54 deletions
diff --git a/spec/frontend/pipelines/components/pipelines_list/failure_widget/mock.js b/spec/frontend/pipelines/components/pipelines_list/failure_widget/mock.js index a4c90fa3876..fc8e4c06019 100644 --- a/spec/frontend/pipelines/components/pipelines_list/failure_widget/mock.js +++ b/spec/frontend/pipelines/components/pipelines_list/failure_widget/mock.js @@ -13,13 +13,17 @@ export const job = { }, name: 'job-name', retried: false, + retryable: true, stage: { id: '1', name: 'build', }, trace: { - htmlSummary: - '<span>To install the missing version, run `gem install bundler:2.4.13`<br/>\tfrom /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/rubygems.rb:302:in `activate_bin_path\'<br/>\tfrom /usr/bin/bundle:23:in `<main>\'<br/></span><div class="section-start" data-timestamp="1685044123" data-section="upload-artifacts-on-failure" role="button"></div><span class="term-fg-l-cyan term-bold section section-header js-s-upload-artifacts-on-failure">Uploading artifacts for failed job</span><span class="section section-header js-s-upload-artifacts-on-failure"><br/></span><span class="term-fg-l-green term-bold section line js-s-upload-artifacts-on-failure">Uploading artifacts...</span><span class="section line js-s-upload-artifacts-on-failure"><br/>Runtime platform </span><span class="section line js-s-upload-artifacts-on-failure"> arch</span><span class="section line js-s-upload-artifacts-on-failure">=arm64 os</span><span class="section line js-s-upload-artifacts-on-failure">=darwin pid</span><span class="section line js-s-upload-artifacts-on-failure">=16706 revision</span><span class="section line js-s-upload-artifacts-on-failure">=43b2dc3d version</span><span class="section line js-s-upload-artifacts-on-failure">=15.4.0<br/></span><span class="term-fg-yellow section line js-s-upload-artifacts-on-failure">WARNING: rspec.xml: no matching files. Ensure that the artifact path is relative to the working directory</span><span class="section line js-s-upload-artifacts-on-failure"> <br/></span><span class="term-fg-l-red term-bold section line js-s-upload-artifacts-on-failure">ERROR: No files to upload </span><span class="section line js-s-upload-artifacts-on-failure"> <br/></span><div class="section-end" data-section="upload-artifacts-on-failure"></div><span class="term-fg-l-red term-bold">ERROR: Job failed: exit status 1<br/></span><span><br/></span>', + htmlSummary: '<h1>Hello</h1>', + }, + userPermissions: { + readBuild: true, + updateBuild: true, }, webPath: '/', }; @@ -43,3 +47,17 @@ export const failedJobsMock = { }, }, }; + +export const failedJobsMock2 = { + data: { + project: { + id: 'gid://gitlab/Project/20', + pipeline: { + id: 'gid://gitlab/Pipeline/20', + jobs: { + nodes: [job], + }, + }, + }, + }, +}; diff --git a/spec/frontend/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget_spec.js b/spec/frontend/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget_spec.js index df6d114f683..8e096b98030 100644 --- a/spec/frontend/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget_spec.js +++ b/spec/frontend/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import { GlButton, GlIcon, GlLoadingIcon, GlPopover } from '@gitlab/ui'; +import { GlButton, GlIcon, GlLoadingIcon, GlPopover, GlToast } from '@gitlab/ui'; import createMockApollo from 'helpers/mock_apollo_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; @@ -10,14 +10,17 @@ import { createAlert } from '~/alert'; import WidgetFailedJobRow from '~/pipelines/components/pipelines_list/failure_widget/widget_failed_job_row.vue'; import * as utils from '~/pipelines/components/pipelines_list/failure_widget/utils'; import getPipelineFailedJobs from '~/pipelines/graphql/queries/get_pipeline_failed_jobs.query.graphql'; -import { failedJobsMock } from './mock'; +import { failedJobsMock, failedJobsMock2 } from './mock'; Vue.use(VueApollo); +Vue.use(GlToast); + jest.mock('~/alert'); describe('PipelineFailedJobsWidget component', () => { let wrapper; let mockFailedJobsResponse; + const showToast = jest.fn(); const defaultProps = { pipelineIid: 1, @@ -42,6 +45,11 @@ describe('PipelineFailedJobsWidget component', () => { ...provide, }, apolloProvider: mockApollo, + mocks: { + $toast: { + show: showToast, + }, + }, }); }; @@ -101,12 +109,13 @@ describe('PipelineFailedJobsWidget component', () => { await findFailedJobsButton().vm.$emit('click'); await waitForPromises(); }); + it('does not renders a loading icon', () => { expect(findLoadingIcon().exists()).toBe(false); }); it('renders table column', () => { - expect(findAllHeaders()).toHaveLength(3); + expect(findAllHeaders()).toHaveLength(4); }); it('shows the list of failed jobs', () => { @@ -141,4 +150,36 @@ describe('PipelineFailedJobsWidget component', () => { expect(createAlert).toHaveBeenCalledWith({ message: errorMessage, variant: 'danger' }); }); }); + + describe('when `refetch-jobs` job is fired from the widget', () => { + beforeEach(async () => { + mockFailedJobsResponse.mockResolvedValueOnce(failedJobsMock); + mockFailedJobsResponse.mockResolvedValueOnce(failedJobsMock2); + + createComponent(); + + await findFailedJobsButton().vm.$emit('click'); + await waitForPromises(); + }); + + it('refetches all failed jobs', async () => { + expect(findFailedJobRows()).not.toHaveLength( + failedJobsMock2.data.project.pipeline.jobs.nodes.length, + ); + + await findFailedJobRows().at(0).vm.$emit('job-retried', 'job-name'); + await waitForPromises(); + + expect(findFailedJobRows()).toHaveLength( + failedJobsMock2.data.project.pipeline.jobs.nodes.length, + ); + }); + + it('shows a toast message', async () => { + await findFailedJobRows().at(0).vm.$emit('job-retried', 'job-name'); + await waitForPromises(); + + expect(showToast).toHaveBeenCalledWith('job-name job is being retried'); + }); + }); }); diff --git a/spec/frontend/pipelines/components/pipelines_list/failure_widget/widget_failed_job_row_spec.js b/spec/frontend/pipelines/components/pipelines_list/failure_widget/widget_failed_job_row_spec.js index dfc2806840f..a15edcc1fe8 100644 --- a/spec/frontend/pipelines/components/pipelines_list/failure_widget/widget_failed_job_row_spec.js +++ b/spec/frontend/pipelines/components/pipelines_list/failure_widget/widget_failed_job_row_spec.js @@ -1,36 +1,46 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; import { GlIcon, GlLink } from '@gitlab/ui'; +import createMockApollo from 'helpers/mock_apollo_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { createAlert } from '~/alert'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import WidgetFailedJobRow from '~/pipelines/components/pipelines_list/failure_widget/widget_failed_job_row.vue'; +import RetryMrFailedJobMutation from '~/pipelines/graphql/mutations/retry_mr_failed_job.mutation.graphql'; +import { job } from './mock'; + +Vue.use(VueApollo); +jest.mock('~/alert'); + +const createFakeEvent = () => ({ stopPropagation: jest.fn() }); describe('WidgetFailedJobRow component', () => { let wrapper; + let mockRetryResponse; - const defaultProps = { - job: { - id: 'gid://gitlab/Ci::Build/5240', - detailedStatus: { - group: 'running', - icon: 'icon_status_running', - }, - name: 'my-job', - stage: { - name: 'build', - }, - trace: { - htmlSummary: '<h1>job log</h1>', + const retrySuccessResponse = { + data: { + jobRetry: { + errors: [], }, - webpath: '/', }, }; + const defaultProps = { + job, + }; + const createComponent = ({ props = {} } = {}) => { + const handlers = [[RetryMrFailedJobMutation, mockRetryResponse]]; + wrapper = shallowMountExtended(WidgetFailedJobRow, { propsData: { ...defaultProps, ...props, }, + apolloProvider: createMockApollo(handlers), }); }; @@ -40,9 +50,15 @@ describe('WidgetFailedJobRow component', () => { const findHiddenJobLog = () => wrapper.findByTestId('log-is-hidden'); const findVisibleJobLog = () => wrapper.findByTestId('log-is-visible'); const findJobName = () => wrapper.findByText(defaultProps.job.name); + const findRetryButton = () => wrapper.findByLabelText('Retry'); const findRow = () => wrapper.findByTestId('widget-row'); const findStageName = () => wrapper.findByText(defaultProps.job.stage.name); + beforeEach(() => { + mockRetryResponse = jest.fn(); + mockRetryResponse.mockResolvedValue(retrySuccessResponse); + }); + describe('ui', () => { beforeEach(() => { createComponent(); @@ -77,63 +93,169 @@ describe('WidgetFailedJobRow component', () => { }); }); - describe('Job log', () => { - beforeEach(() => { - createComponent(); - }); + describe('Retry action', () => { + describe('when the job is not retryable', () => { + beforeEach(() => { + createComponent({ props: { job: { ...job, retryable: false } } }); + }); - describe('when clicking on the row', () => { - beforeEach(async () => { - await findRow().trigger('click'); + it('disables the retry button', () => { + expect(findRetryButton().props().disabled).toBe(true); }); + }); - describe('while collapsed', () => { - it('expands the job log', () => { - expect(findHiddenJobLog().exists()).toBe(false); - expect(findVisibleJobLog().exists()).toBe(true); + describe('when the job is retryable', () => { + describe('and user has permission to update the build', () => { + beforeEach(() => { + createComponent(); }); - it('renders the down arrow', () => { - expect(findArrowIcon().props().name).toBe('chevron-down'); + it('enables the retry button', () => { + expect(findRetryButton().props().disabled).toBe(false); }); - it('renders the received html', () => { - expect(findVisibleJobLog().html()).toContain(defaultProps.job.trace.htmlSummary); + describe('when clicking on the retry button', () => { + it('passes the loading state to the button', async () => { + await findRetryButton().vm.$emit('click', createFakeEvent()); + + expect(findRetryButton().props().loading).toBe(true); + }); + + describe('and it succeeds', () => { + beforeEach(async () => { + findRetryButton().vm.$emit('click', createFakeEvent()); + await waitForPromises(); + }); + + it('is no longer loading', () => { + expect(findRetryButton().props().loading).toBe(false); + }); + + it('calls the retry mutation', () => { + expect(mockRetryResponse).toHaveBeenCalled(); + expect(mockRetryResponse).toHaveBeenCalledWith({ + id: job.id, + }); + }); + + it('emits the `retried-job` event', () => { + expect(wrapper.emitted('job-retried')).toStrictEqual([[job.name]]); + }); + }); + + describe('and it fails', () => { + const customErrorMsg = 'Custom error message from API'; + + beforeEach(async () => { + mockRetryResponse.mockResolvedValue({ + data: { jobRetry: { errors: [customErrorMsg] } }, + }); + findRetryButton().vm.$emit('click', createFakeEvent()); + + await waitForPromises(); + }); + + it('shows an error message', () => { + expect(createAlert).toHaveBeenCalledWith({ message: customErrorMsg }); + }); + + it('does not emits the `refetch-jobs` event', () => { + expect(wrapper.emitted('refetch-jobs')).toBeUndefined(); + }); + }); }); }); - describe('while expanded', () => { - it('collapes the job log', async () => { - expect(findHiddenJobLog().exists()).toBe(false); - expect(findVisibleJobLog().exists()).toBe(true); + describe('and user does not have permission to update the build', () => { + beforeEach(() => { + createComponent({ + props: { job: { ...job, retryable: true, userPermissions: { updateBuild: false } } }, + }); + }); + it('disables the retry button', () => { + expect(findRetryButton().props().disabled).toBe(true); + }); + }); + }); + }); + + describe('Job log', () => { + describe('without permissions', () => { + beforeEach(async () => { + createComponent({ props: { job: { ...job, userPermissions: { readBuild: false } } } }); + await findRow().trigger('click'); + }); + + it('does not renders the received html of the job log', () => { + expect(findVisibleJobLog().html()).not.toContain(defaultProps.job.trace.htmlSummary); + }); + + it('shows a permission error message', () => { + expect(findVisibleJobLog().text()).toBe( + "You do not have permission to read this job's log", + ); + }); + }); + + describe('with permissions', () => { + beforeEach(() => { + createComponent(); + }); + + describe('when clicking on the row', () => { + beforeEach(async () => { await findRow().trigger('click'); + }); - expect(findHiddenJobLog().exists()).toBe(true); - expect(findVisibleJobLog().exists()).toBe(false); + describe('while collapsed', () => { + it('expands the job log', () => { + expect(findHiddenJobLog().exists()).toBe(false); + expect(findVisibleJobLog().exists()).toBe(true); + }); + + it('renders the down arrow', () => { + expect(findArrowIcon().props().name).toBe('chevron-down'); + }); + + it('renders the received html of the job log', () => { + expect(findVisibleJobLog().html()).toContain(defaultProps.job.trace.htmlSummary); + }); }); - it('renders the right arrow', async () => { - expect(findArrowIcon().props().name).toBe('chevron-down'); + describe('while expanded', () => { + it('collapes the job log', async () => { + expect(findHiddenJobLog().exists()).toBe(false); + expect(findVisibleJobLog().exists()).toBe(true); - await findRow().trigger('click'); + await findRow().trigger('click'); - expect(findArrowIcon().props().name).toBe('chevron-right'); + expect(findHiddenJobLog().exists()).toBe(true); + expect(findVisibleJobLog().exists()).toBe(false); + }); + + it('renders the right arrow', async () => { + expect(findArrowIcon().props().name).toBe('chevron-down'); + + await findRow().trigger('click'); + + expect(findArrowIcon().props().name).toBe('chevron-right'); + }); }); }); - }); - describe('when clicking on a link element within the row', () => { - it('does not expands/collapse the job log', async () => { - expect(findHiddenJobLog().exists()).toBe(true); - expect(findVisibleJobLog().exists()).toBe(false); - expect(findArrowIcon().props().name).toBe('chevron-right'); + describe('when clicking on a link element within the row', () => { + it('does not expands/collapse the job log', async () => { + expect(findHiddenJobLog().exists()).toBe(true); + expect(findVisibleJobLog().exists()).toBe(false); + expect(findArrowIcon().props().name).toBe('chevron-right'); - await findJobId().vm.$emit('click'); + await findJobId().vm.$emit('click'); - expect(findHiddenJobLog().exists()).toBe(true); - expect(findVisibleJobLog().exists()).toBe(false); - expect(findArrowIcon().props().name).toBe('chevron-right'); + expect(findHiddenJobLog().exists()).toBe(true); + expect(findVisibleJobLog().exists()).toBe(false); + expect(findArrowIcon().props().name).toBe('chevron-right'); + }); }); }); }); |