import { GlLoadingIcon, GlModal, GlTableLite } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; import fixture from 'test_fixtures/pipelines/pipelines.json'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { stubComponent } from 'helpers/stub_component'; import waitForPromises from 'helpers/wait_for_promises'; import Api from '~/api'; import PipelinesTable from '~/commit/pipelines/pipelines_table.vue'; import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK, HTTP_STATUS_UNAUTHORIZED, } from '~/lib/utils/http_status'; import { createAlert } from '~/alert'; import { TOAST_MESSAGE } from '~/ci/pipeline_details/constants'; import axios from '~/lib/utils/axios_utils'; const $toast = { show: jest.fn(), }; jest.mock('~/alert'); describe('Pipelines table in Commits and Merge requests', () => { let wrapper; let pipeline; let mock; const showMock = jest.fn(); const findRunPipelineBtn = () => wrapper.findByTestId('run_pipeline_button'); const findRunPipelineBtnMobile = () => wrapper.findByTestId('run_pipeline_button_mobile'); const findLoadingState = () => wrapper.findComponent(GlLoadingIcon); const findErrorEmptyState = () => wrapper.findByTestId('pipeline-error-empty-state'); const findEmptyState = () => wrapper.findByTestId('pipeline-empty-state'); const findTable = () => wrapper.findComponent(GlTableLite); const findTableRows = () => wrapper.findAllByTestId('pipeline-table-row'); const findModal = () => wrapper.findComponent(GlModal); const findMrPipelinesDocsLink = () => wrapper.findByTestId('mr-pipelines-docs-link'); const createComponent = ({ props = {} } = {}) => { wrapper = extendedWrapper( mount(PipelinesTable, { propsData: { endpoint: 'endpoint.json', emptyStateSvgPath: 'foo', errorStateSvgPath: 'foo', ...props, }, mocks: { $toast, }, stubs: { GlModal: stubComponent(GlModal, { template: '
', methods: { show: showMock }, }), }, }), ); }; beforeEach(() => { mock = new MockAdapter(axios); const { pipelines } = fixture; pipeline = pipelines.find((p) => p.user !== null && p.commit !== null); }); describe('successful request', () => { describe('without pipelines', () => { beforeEach(async () => { mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, []); createComponent(); await waitForPromises(); }); it('should render the empty state', () => { expect(findTableRows()).toHaveLength(0); expect(findLoadingState().exists()).toBe(false); expect(findErrorEmptyState().exists()).toBe(false); expect(findEmptyState().exists()).toBe(true); }); it('should render correct empty state content', () => { expect(findRunPipelineBtn().exists()).toBe(true); expect(findMrPipelinesDocsLink().attributes('href')).toBe( '/help/ci/pipelines/merge_request_pipelines.md#prerequisites', ); expect(findEmptyState().text()).toContain( 'To run a merge request pipeline, the jobs in the CI/CD configuration file must be configured to run in merge request pipelines.', ); }); }); describe('with pagination', () => { beforeEach(async () => { mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipeline], { 'X-TOTAL': 10, 'X-PER-PAGE': 2, 'X-PAGE': 1, 'X-TOTAL-PAGES': 5, 'X-NEXT-PAGE': 2, 'X-PREV-PAGE': 2, }); createComponent(); await waitForPromises(); }); it('should make an API request when using pagination', async () => { expect(mock.history.get).toHaveLength(1); expect(mock.history.get[0].params.page).toBe('1'); wrapper.find('.next-page-item').trigger('click'); await waitForPromises(); expect(mock.history.get).toHaveLength(2); expect(mock.history.get[1].params.page).toBe('2'); }); }); describe('with pipelines', () => { beforeEach(async () => { mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipeline], { 'x-total': 10 }); createComponent(); await waitForPromises(); }); it('should render a table with the received pipelines', () => { expect(findTable().exists()).toBe(true); expect(findTableRows()).toHaveLength(1); expect(findLoadingState().exists()).toBe(false); expect(findErrorEmptyState().exists()).toBe(false); }); describe('pipeline badge counts', () => { it('should receive update-pipelines-count event', () => { const element = document.createElement('div'); document.body.appendChild(element); return new Promise((resolve) => { element.addEventListener('update-pipelines-count', (event) => { expect(event.detail.pipelineCount).toEqual(10); resolve(); }); createComponent(); element.appendChild(wrapper.vm.$el); }); }); }); }); }); describe('run pipeline button', () => { let pipelineCopy; beforeEach(() => { pipelineCopy = { ...pipeline }; }); describe('when latest pipeline has detached flag', () => { it('renders the run pipeline button', async () => { pipelineCopy.flags.detached_merge_request_pipeline = true; pipelineCopy.flags.merge_request_pipeline = true; mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipelineCopy]); createComponent(); await waitForPromises(); expect(findRunPipelineBtn().exists()).toBe(true); expect(findRunPipelineBtnMobile().exists()).toBe(true); }); }); describe('when latest pipeline does not have detached flag', () => { it('does not render the run pipeline button', async () => { pipelineCopy.flags.detached_merge_request_pipeline = false; pipelineCopy.flags.merge_request_pipeline = false; mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipelineCopy]); createComponent(); await waitForPromises(); expect(findRunPipelineBtn().exists()).toBe(false); expect(findRunPipelineBtnMobile().exists()).toBe(false); }); }); describe('on click', () => { beforeEach(async () => { pipelineCopy.flags.detached_merge_request_pipeline = true; mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipelineCopy]); createComponent({ props: { canRunPipeline: true, projectId: '5', mergeRequestId: 3, }, }); await waitForPromises(); }); describe('success', () => { beforeEach(() => { jest.spyOn(Api, 'postMergeRequestPipeline').mockResolvedValue(); }); it('displays a toast message during pipeline creation', async () => { await findRunPipelineBtn().trigger('click'); expect($toast.show).toHaveBeenCalledWith(TOAST_MESSAGE); }); it('on desktop, shows a loading button', async () => { await findRunPipelineBtn().trigger('click'); expect(findRunPipelineBtn().props('loading')).toBe(true); await waitForPromises(); expect(findRunPipelineBtn().props('loading')).toBe(false); }); it('on mobile, shows a loading button', async () => { await findRunPipelineBtnMobile().trigger('click'); expect(findRunPipelineBtn().props('loading')).toBe(true); await waitForPromises(); expect(findRunPipelineBtn().props('disabled')).toBe(false); expect(findRunPipelineBtn().props('loading')).toBe(false); }); }); describe('failure', () => { const permissionsMsg = 'You do not have permission to run a pipeline on this branch.'; const defaultMsg = 'An error occurred while trying to run a new pipeline for this merge request.'; it.each` status | message ${HTTP_STATUS_BAD_REQUEST} | ${defaultMsg} ${HTTP_STATUS_UNAUTHORIZED} | ${permissionsMsg} ${HTTP_STATUS_INTERNAL_SERVER_ERROR} | ${defaultMsg} `('displays permissions error message', async ({ status, message }) => { const response = { response: { status } }; jest.spyOn(Api, 'postMergeRequestPipeline').mockRejectedValue(response); await findRunPipelineBtn().trigger('click'); await waitForPromises(); expect(createAlert).toHaveBeenCalledWith({ message, primaryButton: { text: 'Learn more', link: '/help/ci/pipelines/merge_request_pipelines.md', }, }); }); }); }); describe('on click for fork merge request', () => { beforeEach(async () => { pipelineCopy.flags.detached_merge_request_pipeline = true; mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipelineCopy]); createComponent({ props: { projectId: '5', mergeRequestId: 3, canCreatePipelineInTargetProject: true, sourceProjectFullPath: 'test/parent-project', targetProjectFullPath: 'test/fork-project', }, }); jest.spyOn(Api, 'postMergeRequestPipeline').mockResolvedValue(); await waitForPromises(); }); it('on desktop, shows a security warning modal', async () => { await findRunPipelineBtn().trigger('click'); await nextTick(); expect(findModal()).not.toBeNull(); }); it('on mobile, shows a security warning modal', async () => { await findRunPipelineBtnMobile().trigger('click'); expect(findModal()).not.toBeNull(); }); }); describe('when no pipelines were created on a forked merge request', () => { beforeEach(async () => { mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, []); createComponent({ props: { projectId: '5', mergeRequestId: 3, canCreatePipelineInTargetProject: true, sourceProjectFullPath: 'test/parent-project', targetProjectFullPath: 'test/fork-project', }, }); await waitForPromises(); }); it('should show security modal from empty state run pipeline button', () => { expect(findEmptyState().exists()).toBe(true); expect(findModal().exists()).toBe(true); findRunPipelineBtn().trigger('click'); expect(showMock).toHaveBeenCalled(); }); }); }); describe('unsuccessfull request', () => { beforeEach(async () => { mock.onGet('endpoint.json').reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, []); createComponent(); await waitForPromises(); }); it('should render error state', () => { expect(findErrorEmptyState().text()).toBe( 'There was an error fetching the pipelines. Try again in a few moments or contact your support team.', ); }); }); });