From 9f46488805e86b1bc341ea1620b866016c2ce5ed Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 20 May 2020 14:34:42 +0000 Subject: Add latest changes from gitlab-org/gitlab@13-0-stable-ee --- spec/frontend/pipelines/pipelines_spec.js | 710 ++++++++++++++++++++++++++++++ 1 file changed, 710 insertions(+) create mode 100644 spec/frontend/pipelines/pipelines_spec.js (limited to 'spec/frontend/pipelines/pipelines_spec.js') diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js new file mode 100644 index 00000000000..2ddd2116e2c --- /dev/null +++ b/spec/frontend/pipelines/pipelines_spec.js @@ -0,0 +1,710 @@ +import Api from '~/api'; +import { mount } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import waitForPromises from 'helpers/wait_for_promises'; +import PipelinesComponent from '~/pipelines/components/pipelines.vue'; +import Store from '~/pipelines/stores/pipelines_store'; +import { pipelineWithStages, stageReply, users, mockSearch, branches } from './mock_data'; +import { RAW_TEXT_WARNING } from '~/pipelines/constants'; +import { GlFilteredSearch } from '@gitlab/ui'; +import createFlash from '~/flash'; + +jest.mock('~/flash', () => jest.fn()); + +describe('Pipelines', () => { + const jsonFixtureName = 'pipelines/pipelines.json'; + + preloadFixtures(jsonFixtureName); + + let pipelines; + let wrapper; + let mock; + + const paths = { + endpoint: 'twitter/flight/pipelines.json', + autoDevopsPath: '/help/topics/autodevops/index.md', + helpPagePath: '/help/ci/quick_start/README', + emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg', + errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg', + noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg', + ciLintPath: '/ci/lint', + resetCachePath: '/twitter/flight/settings/ci_cd/reset_cache', + newPipelinePath: '/twitter/flight/pipelines/new', + }; + + const noPermissions = { + endpoint: 'twitter/flight/pipelines.json', + autoDevopsPath: '/help/topics/autodevops/index.md', + helpPagePath: '/help/ci/quick_start/README', + emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg', + errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg', + noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg', + }; + + const defaultProps = { + hasGitlabCi: true, + canCreatePipeline: true, + ...paths, + }; + + const findFilteredSearch = () => wrapper.find(GlFilteredSearch); + + const createComponent = (props = defaultProps, methods) => { + wrapper = mount(PipelinesComponent, { + provide: { glFeatures: { filterPipelinesSearch: true } }, + propsData: { + store: new Store(), + projectId: '21', + ...props, + }, + methods: { + ...methods, + }, + }); + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + pipelines = getJSONFixture(jsonFixtureName); + + jest.spyOn(Api, 'projectUsers').mockResolvedValue(users); + jest.spyOn(Api, 'branches').mockResolvedValue({ data: branches }); + }); + + afterEach(() => { + wrapper.destroy(); + mock.restore(); + }); + + describe('With permission', () => { + describe('With pipelines in main tab', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); + createComponent(); + return waitForPromises(); + }); + + it('renders tabs', () => { + expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + }); + + it('renders Run Pipeline link', () => { + expect(wrapper.find('.js-run-pipeline').attributes('href')).toBe(paths.newPipelinePath); + }); + + it('renders CI Lint link', () => { + expect(wrapper.find('.js-ci-lint').attributes('href')).toBe(paths.ciLintPath); + }); + + it('renders Clear Runner Cache button', () => { + expect(wrapper.find('.js-clear-cache').text()).toBe('Clear Runner Caches'); + }); + + it('renders pipelines table', () => { + expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength( + pipelines.pipelines.length + 1, + ); + }); + }); + + describe('Without pipelines on main tab with CI', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(200, { + pipelines: [], + count: { + all: 0, + pending: 0, + running: 0, + finished: 0, + }, + }); + + createComponent(); + + return waitForPromises(); + }); + + it('renders tabs', () => { + expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + }); + + it('renders Run Pipeline link', () => { + expect(wrapper.find('.js-run-pipeline').attributes('href')).toEqual(paths.newPipelinePath); + }); + + it('renders CI Lint link', () => { + expect(wrapper.find('.js-ci-lint').attributes('href')).toEqual(paths.ciLintPath); + }); + + it('renders Clear Runner Cache button', () => { + expect(wrapper.find('.js-clear-cache').text()).toEqual('Clear Runner Caches'); + }); + + it('renders tab empty state', () => { + expect(wrapper.find('.empty-state h4').text()).toEqual('There are currently no pipelines.'); + }); + }); + + describe('Without pipelines nor CI', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(200, { + pipelines: [], + count: { + all: 0, + pending: 0, + running: 0, + finished: 0, + }, + }); + + createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths }); + + return waitForPromises(); + }); + + it('renders empty state', () => { + expect(wrapper.find('.js-empty-state h4').text()).toEqual('Build with confidence'); + + expect(wrapper.find('.js-get-started-pipelines').attributes('href')).toEqual( + paths.helpPagePath, + ); + }); + + it('does not render tabs nor buttons', () => { + expect(wrapper.find('.js-pipelines-tab-all').exists()).toBeFalsy(); + expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy(); + expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy(); + expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy(); + }); + }); + + describe('When API returns error', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(500, {}); + createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths }); + + return waitForPromises(); + }); + + it('renders tabs', () => { + expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + }); + + it('renders buttons', () => { + expect(wrapper.find('.js-run-pipeline').attributes('href')).toEqual(paths.newPipelinePath); + + expect(wrapper.find('.js-ci-lint').attributes('href')).toEqual(paths.ciLintPath); + expect(wrapper.find('.js-clear-cache').text()).toEqual('Clear Runner Caches'); + }); + + it('renders error state', () => { + expect(wrapper.find('.empty-state').text()).toContain( + 'There was an error fetching the pipelines.', + ); + }); + }); + }); + + describe('Without permission', () => { + describe('With pipelines in main tab', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); + + createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions }); + + return waitForPromises(); + }); + + it('renders tabs', () => { + expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + }); + + it('does not render buttons', () => { + expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy(); + expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy(); + expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy(); + }); + + it('renders pipelines table', () => { + expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength( + pipelines.pipelines.length + 1, + ); + }); + }); + + describe('Without pipelines on main tab with CI', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(200, { + pipelines: [], + count: { + all: 0, + pending: 0, + running: 0, + finished: 0, + }, + }); + + createComponent({ hasGitlabCi: true, canCreatePipeline: false, ...noPermissions }); + + return waitForPromises(); + }); + + it('renders tabs', () => { + expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + }); + + it('does not render buttons', () => { + expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy(); + expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy(); + expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy(); + }); + + it('renders tab empty state', () => { + expect(wrapper.find('.empty-state h4').text()).toEqual('There are currently no pipelines.'); + }); + }); + + describe('Without pipelines nor CI', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(200, { + pipelines: [], + count: { + all: 0, + pending: 0, + running: 0, + finished: 0, + }, + }); + + createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions }); + + return waitForPromises(); + }); + + it('renders empty state without button to set CI', () => { + expect(wrapper.find('.js-empty-state').text()).toEqual( + 'This project is not currently set up to run pipelines.', + ); + + expect(wrapper.find('.js-get-started-pipelines').exists()).toBeFalsy(); + }); + + it('does not render tabs or buttons', () => { + expect(wrapper.find('.js-pipelines-tab-all').exists()).toBeFalsy(); + expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy(); + expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy(); + expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy(); + }); + }); + + describe('When API returns error', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(500, {}); + + createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...noPermissions }); + + return waitForPromises(); + }); + + it('renders tabs', () => { + expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + }); + + it('does not renders buttons', () => { + expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy(); + expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy(); + expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy(); + }); + + it('renders error state', () => { + expect(wrapper.find('.empty-state').text()).toContain( + 'There was an error fetching the pipelines.', + ); + }); + }); + }); + + describe('successful request', () => { + describe('with pipelines', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); + + createComponent(); + return waitForPromises(); + }); + + it('should render table', () => { + expect(wrapper.find('.table-holder').exists()).toBe(true); + expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength( + pipelines.pipelines.length + 1, + ); + }); + + it('should render navigation tabs', () => { + expect(wrapper.find('.js-pipelines-tab-pending').text()).toContain('Pending'); + + expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + + expect(wrapper.find('.js-pipelines-tab-running').text()).toContain('Running'); + + expect(wrapper.find('.js-pipelines-tab-finished').text()).toContain('Finished'); + + expect(wrapper.find('.js-pipelines-tab-branches').text()).toContain('Branches'); + + expect(wrapper.find('.js-pipelines-tab-tags').text()).toContain('Tags'); + }); + + it('should make an API request when using tabs', () => { + const updateContentMock = jest.fn(() => {}); + createComponent( + { hasGitlabCi: true, canCreatePipeline: true, ...paths }, + { + updateContent: updateContentMock, + }, + ); + + return waitForPromises().then(() => { + wrapper.find('.js-pipelines-tab-finished').trigger('click'); + + expect(updateContentMock).toHaveBeenCalledWith({ scope: 'finished', page: '1' }); + }); + }); + + describe('with pagination', () => { + it('should make an API request when using pagination', () => { + const updateContentMock = jest.fn(() => {}); + createComponent( + { hasGitlabCi: true, canCreatePipeline: true, ...paths }, + { + updateContent: updateContentMock, + }, + ); + + return waitForPromises() + .then(() => { + // Mock pagination + wrapper.vm.store.state.pageInfo = { + page: 1, + total: 10, + perPage: 2, + nextPage: 2, + totalPages: 5, + }; + + return wrapper.vm.$nextTick(); + }) + .then(() => { + wrapper.find('.next-page-item').trigger('click'); + + expect(updateContentMock).toHaveBeenCalledWith({ scope: 'all', page: '2' }); + }); + }); + }); + }); + }); + + describe('methods', () => { + beforeEach(() => { + jest.spyOn(window.history, 'pushState').mockImplementation(() => null); + }); + + describe('onChangeTab', () => { + it('should set page to 1', () => { + const updateContentMock = jest.fn(() => {}); + createComponent( + { hasGitlabCi: true, canCreatePipeline: true, ...paths }, + { + updateContent: updateContentMock, + }, + ); + + wrapper.vm.onChangeTab('running'); + + expect(updateContentMock).toHaveBeenCalledWith({ scope: 'running', page: '1' }); + }); + }); + + describe('onChangePage', () => { + it('should update page and keep scope', () => { + const updateContentMock = jest.fn(() => {}); + createComponent( + { hasGitlabCi: true, canCreatePipeline: true, ...paths }, + { + updateContent: updateContentMock, + }, + ); + + wrapper.vm.onChangePage(4); + + expect(updateContentMock).toHaveBeenCalledWith({ scope: wrapper.vm.scope, page: '4' }); + }); + }); + }); + + describe('computed properties', () => { + beforeEach(() => { + createComponent(); + }); + + describe('tabs', () => { + it('returns default tabs', () => { + expect(wrapper.vm.tabs).toEqual([ + { name: 'All', scope: 'all', count: undefined, isActive: true }, + { name: 'Pending', scope: 'pending', count: undefined, isActive: false }, + { name: 'Running', scope: 'running', count: undefined, isActive: false }, + { name: 'Finished', scope: 'finished', count: undefined, isActive: false }, + { name: 'Branches', scope: 'branches', isActive: false }, + { name: 'Tags', scope: 'tags', isActive: false }, + ]); + }); + }); + + describe('emptyTabMessage', () => { + it('returns message with scope', () => { + wrapper.vm.scope = 'pending'; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.emptyTabMessage).toEqual('There are currently no pending pipelines.'); + }); + }); + + it('returns message without scope when scope is `all`', () => { + expect(wrapper.vm.emptyTabMessage).toEqual('There are currently no pipelines.'); + }); + }); + + describe('stateToRender', () => { + it('returns loading state when the app is loading', () => { + expect(wrapper.vm.stateToRender).toEqual('loading'); + }); + + it('returns error state when app has error', () => { + wrapper.vm.hasError = true; + wrapper.vm.isLoading = false; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.stateToRender).toEqual('error'); + }); + }); + + it('returns table list when app has pipelines', () => { + wrapper.vm.isLoading = false; + wrapper.vm.hasError = false; + wrapper.vm.state.pipelines = pipelines.pipelines; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.stateToRender).toEqual('tableList'); + }); + }); + + it('returns empty tab when app does not have pipelines but project has pipelines', () => { + wrapper.vm.state.count.all = 10; + wrapper.vm.isLoading = false; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.stateToRender).toEqual('emptyTab'); + }); + }); + + it('returns empty tab when project has CI', () => { + wrapper.vm.isLoading = false; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.stateToRender).toEqual('emptyTab'); + }); + }); + + it('returns empty state when project does not have pipelines nor CI', () => { + createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths }); + + wrapper.vm.isLoading = false; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.stateToRender).toEqual('emptyState'); + }); + }); + }); + + describe('shouldRenderTabs', () => { + it('returns true when state is loading & has already made the first request', () => { + wrapper.vm.isLoading = true; + wrapper.vm.hasMadeRequest = true; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.shouldRenderTabs).toEqual(true); + }); + }); + + it('returns true when state is tableList & has already made the first request', () => { + wrapper.vm.isLoading = false; + wrapper.vm.state.pipelines = pipelines.pipelines; + wrapper.vm.hasMadeRequest = true; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.shouldRenderTabs).toEqual(true); + }); + }); + + it('returns true when state is error & has already made the first request', () => { + wrapper.vm.isLoading = false; + wrapper.vm.hasError = true; + wrapper.vm.hasMadeRequest = true; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.shouldRenderTabs).toEqual(true); + }); + }); + + it('returns true when state is empty tab & has already made the first request', () => { + wrapper.vm.isLoading = false; + wrapper.vm.state.count.all = 10; + wrapper.vm.hasMadeRequest = true; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.shouldRenderTabs).toEqual(true); + }); + }); + + it('returns false when has not made first request', () => { + wrapper.vm.hasMadeRequest = false; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.shouldRenderTabs).toEqual(false); + }); + }); + + it('returns false when state is empty state', () => { + createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths }); + + wrapper.vm.isLoading = false; + wrapper.vm.hasMadeRequest = true; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.shouldRenderTabs).toEqual(false); + }); + }); + }); + + describe('shouldRenderButtons', () => { + it('returns true when it has paths & has made the first request', () => { + wrapper.vm.hasMadeRequest = true; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.shouldRenderButtons).toEqual(true); + }); + }); + + it('returns false when it has not made the first request', () => { + wrapper.vm.hasMadeRequest = false; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.shouldRenderButtons).toEqual(false); + }); + }); + }); + }); + + describe('updates results when a staged is clicked', () => { + beforeEach(() => { + const copyPipeline = { ...pipelineWithStages }; + copyPipeline.id += 1; + mock + .onGet('twitter/flight/pipelines.json') + .reply( + 200, + { + pipelines: [pipelineWithStages], + count: { + all: 1, + finished: 1, + pending: 0, + running: 0, + }, + }, + { + 'POLL-INTERVAL': 100, + }, + ) + .onGet(pipelineWithStages.details.stages[0].dropdown_path) + .reply(200, stageReply); + + createComponent(); + }); + + describe('when a request is being made', () => { + it('stops polling, cancels the request, & restarts polling', () => { + const stopMock = jest.spyOn(wrapper.vm.poll, 'stop'); + const restartMock = jest.spyOn(wrapper.vm.poll, 'restart'); + const cancelMock = jest.spyOn(wrapper.vm.service.cancelationSource, 'cancel'); + mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); + + return waitForPromises() + .then(() => { + wrapper.vm.isMakingRequest = true; + wrapper.find('.js-builds-dropdown-button').trigger('click'); + }) + .then(() => { + expect(cancelMock).toHaveBeenCalled(); + expect(stopMock).toHaveBeenCalled(); + expect(restartMock).toHaveBeenCalled(); + }); + }); + }); + + describe('when no request is being made', () => { + it('stops polling & restarts polling', () => { + const stopMock = jest.spyOn(wrapper.vm.poll, 'stop'); + const restartMock = jest.spyOn(wrapper.vm.poll, 'restart'); + mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); + + return waitForPromises() + .then(() => { + wrapper.find('.js-builds-dropdown-button').trigger('click'); + expect(stopMock).toHaveBeenCalled(); + }) + .then(() => { + expect(restartMock).toHaveBeenCalled(); + }); + }); + }); + }); + + describe('Pipeline filters', () => { + let updateContentMock; + + beforeEach(() => { + mock.onGet(paths.endpoint).reply(200, pipelines); + createComponent(); + + updateContentMock = jest.spyOn(wrapper.vm, 'updateContent'); + + return waitForPromises(); + }); + + it('updates request data and query params on filter submit', () => { + const expectedQueryParams = { page: '1', scope: 'all', username: 'root', ref: 'master' }; + + findFilteredSearch().vm.$emit('submit', mockSearch); + + expect(wrapper.vm.requestData).toEqual(expectedQueryParams); + expect(updateContentMock).toHaveBeenCalledWith(expectedQueryParams); + }); + + it('does not add query params if raw text search is used', () => { + const expectedQueryParams = { page: '1', scope: 'all' }; + + findFilteredSearch().vm.$emit('submit', ['rawText']); + + expect(wrapper.vm.requestData).toEqual(expectedQueryParams); + expect(updateContentMock).toHaveBeenCalledWith(expectedQueryParams); + }); + + it('displays a warning message if raw text search is used', () => { + findFilteredSearch().vm.$emit('submit', ['rawText']); + + expect(createFlash).toHaveBeenCalledTimes(1); + expect(createFlash).toHaveBeenCalledWith(RAW_TEXT_WARNING, 'warning'); + }); + }); +}); -- cgit v1.2.1