diff options
Diffstat (limited to 'spec/frontend/pipelines/pipelines_spec.js')
-rw-r--r-- | spec/frontend/pipelines/pipelines_spec.js | 990 |
1 files changed, 497 insertions, 493 deletions
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js index 5d82669b0b8..811303a5624 100644 --- a/spec/frontend/pipelines/pipelines_spec.js +++ b/spec/frontend/pipelines/pipelines_spec.js @@ -1,49 +1,50 @@ -import { nextTick } from 'vue'; +import { GlFilteredSearch, GlButton, GlLoadingIcon, GlPagination } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import { chunk } from 'lodash'; +import { nextTick } from 'vue'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { GlFilteredSearch, GlButton, GlLoadingIcon } from '@gitlab/ui'; import Api from '~/api'; +import { deprecatedCreateFlash as createFlash } from '~/flash'; import axios from '~/lib/utils/axios_utils'; -import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue'; -import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; - -import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue'; -import EmptyState from '~/pipelines/components/pipelines_list/empty_state.vue'; import BlankState from '~/pipelines/components/pipelines_list/blank_state.vue'; -import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue'; - +import EmptyState from '~/pipelines/components/pipelines_list/empty_state.vue'; +import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue'; import PipelinesComponent from '~/pipelines/components/pipelines_list/pipelines.vue'; +import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue'; +import { RAW_TEXT_WARNING } from '~/pipelines/constants'; import Store from '~/pipelines/stores/pipelines_store'; +import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue'; +import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; + import { pipelineWithStages, stageReply, users, mockSearch, branches } from './mock_data'; -import { RAW_TEXT_WARNING } from '~/pipelines/constants'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; jest.mock('~/flash'); -describe('Pipelines', () => { - const jsonFixtureName = 'pipelines/pipelines.json'; +const mockProjectPath = 'twitter/flight'; +const mockProjectId = '21'; +const mockPipelinesEndpoint = `/${mockProjectPath}/pipelines.json`; +const mockPipelinesResponse = getJSONFixture('pipelines/pipelines.json'); +const mockPipelinesIds = mockPipelinesResponse.pipelines.map(({ id }) => id); - preloadFixtures(jsonFixtureName); - - let pipelines; +describe('Pipelines', () => { let wrapper; let mock; + let origWindowLocation; const paths = { - endpoint: 'twitter/flight/pipelines.json', autoDevopsHelpPath: '/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', + resetCachePath: `${mockProjectPath}/settings/ci_cd/reset_cache`, + newPipelinePath: `${mockProjectPath}/pipelines/new`, }; const noPermissions = { - endpoint: 'twitter/flight/pipelines.json', autoDevopsHelpPath: '/help/topics/autodevops/index.md', helpPagePath: '/help/ci/quick_start/README', emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg', @@ -57,101 +58,140 @@ describe('Pipelines', () => { ...paths, }; - const findFilteredSearch = () => wrapper.find(GlFilteredSearch); - const findByTestId = (id) => wrapper.find(`[data-testid="${id}"]`); - const findNavigationTabs = () => wrapper.find(NavigationTabs); - const findNavigationControls = () => wrapper.find(NavigationControls); - const findTab = (tab) => findByTestId(`pipelines-tab-${tab}`); - - const findRunPipelineButton = () => findByTestId('run-pipeline-button'); - const findCiLintButton = () => findByTestId('ci-lint-button'); - const findCleanCacheButton = () => findByTestId('clear-cache-button'); - - const findEmptyState = () => wrapper.find(EmptyState); - const findBlankState = () => wrapper.find(BlankState); - const findStagesDropdown = () => wrapper.find('.js-builds-dropdown-button'); - - const findTablePagination = () => wrapper.find(TablePagination); + const findFilteredSearch = () => wrapper.findComponent(GlFilteredSearch); + const findNavigationTabs = () => wrapper.findComponent(NavigationTabs); + const findNavigationControls = () => wrapper.findComponent(NavigationControls); + const findPipelinesTable = () => wrapper.findComponent(PipelinesTableComponent); + const findEmptyState = () => wrapper.findComponent(EmptyState); + const findBlankState = () => wrapper.findComponent(BlankState); + const findTablePagination = () => wrapper.findComponent(TablePagination); + + const findTab = (tab) => wrapper.findByTestId(`pipelines-tab-${tab}`); + const findRunPipelineButton = () => wrapper.findByTestId('run-pipeline-button'); + const findCiLintButton = () => wrapper.findByTestId('ci-lint-button'); + const findCleanCacheButton = () => wrapper.findByTestId('clear-cache-button'); + const findStagesDropdown = () => wrapper.findByTestId('mini-pipeline-graph-dropdown-toggle'); + const findPipelineUrlLinks = () => wrapper.findAll('[data-testid="pipeline-url-link"]'); const createComponent = (props = defaultProps) => { - wrapper = mount(PipelinesComponent, { - propsData: { - store: new Store(), - projectId: '21', - params: {}, - ...props, - }, - }); + wrapper = extendedWrapper( + mount(PipelinesComponent, { + propsData: { + store: new Store(), + projectId: mockProjectId, + endpoint: mockPipelinesEndpoint, + params: {}, + ...props, + }, + }), + ); }; - beforeEach(() => { + beforeAll(() => { + origWindowLocation = window.location; delete window.location; + window.location = { search: '' }; + }); + + afterAll(() => { + window.location = origWindowLocation; }); beforeEach(() => { - window.location = { search: '' }; mock = new MockAdapter(axios); - pipelines = getJSONFixture(jsonFixtureName); + jest.spyOn(window.history, 'pushState'); jest.spyOn(Api, 'projectUsers').mockResolvedValue(users); jest.spyOn(Api, 'branches').mockResolvedValue({ data: branches }); }); afterEach(() => { wrapper.destroy(); - mock.restore(); + mock.reset(); + window.history.pushState.mockReset(); }); - describe('With permission', () => { - describe('With pipelines in main tab', () => { - beforeEach(() => { - mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); - createComponent(); - return waitForPromises(); - }); + describe('when pipelines are not yet loaded', () => { + beforeEach(async () => { + createComponent(); + await nextTick(); + }); - it('renders tabs', () => { - expect(findTab('all').text()).toContain('All'); - }); + it('shows loading state when the app is loading', () => { + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + }); - it('renders Run Pipeline link', () => { - expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath); + it('does not display tabs when the first request has not yet been made', () => { + expect(findNavigationTabs().exists()).toBe(false); + }); + + it('does not display buttons', () => { + expect(findNavigationControls().exists()).toBe(false); + }); + }); + + describe('when there are pipelines in the project', () => { + beforeEach(() => { + mock + .onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } }) + .reply(200, mockPipelinesResponse); + }); + + describe('when user has no permissions', () => { + beforeEach(async () => { + createComponent({ hasGitlabCi: true, canCreatePipeline: false, ...noPermissions }); + await waitForPromises(); }); - it('renders CI Lint link', () => { - expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath); + it('renders "All" tab with count different from "0"', () => { + expect(findTab('all').text()).toMatchInterpolatedText('All 3'); }); - it('renders Clear Runner Cache button', () => { - expect(findCleanCacheButton().text()).toBe('Clear Runner Caches'); + it('does not render buttons', () => { + expect(findNavigationControls().exists()).toBe(false); + + expect(findRunPipelineButton().exists()).toBe(false); + expect(findCiLintButton().exists()).toBe(false); + expect(findCleanCacheButton().exists()).toBe(false); }); - it('renders pipelines table', () => { - expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength( - pipelines.pipelines.length + 1, - ); + it('renders pipelines in a table', () => { + expect(findPipelinesTable().exists()).toBe(true); + + expect(findPipelineUrlLinks()).toHaveLength(mockPipelinesIds.length); + expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockPipelinesIds[0]}`); + expect(findPipelineUrlLinks().at(1).text()).toBe(`#${mockPipelinesIds[1]}`); + expect(findPipelineUrlLinks().at(2).text()).toBe(`#${mockPipelinesIds[2]}`); }); }); - 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, - }, - }); - + describe('when user has permissions', () => { + beforeEach(async () => { createComponent(); + await waitForPromises(); + }); - return waitForPromises(); + it('should set up navigation tabs', () => { + expect(findNavigationTabs().props('tabs')).toEqual([ + { name: 'All', scope: 'all', count: '3', isActive: true }, + { name: 'Finished', scope: 'finished', count: undefined, isActive: false }, + { name: 'Branches', scope: 'branches', isActive: false }, + { name: 'Tags', scope: 'tags', isActive: false }, + ]); }); - it('renders tabs', () => { - expect(findTab('all').text()).toContain('All'); + it('renders "All" tab with count different from "0"', () => { + expect(findTab('all').text()).toMatchInterpolatedText('All 3'); + }); + + it('should render other navigation tabs', () => { + expect(findTab('finished').text()).toBe('Finished'); + expect(findTab('branches').text()).toBe('Branches'); + expect(findTab('tags').text()).toBe('Tags'); + }); + + it('shows navigation controls', () => { + expect(findNavigationControls().exists()).toBe(true); }); it('renders Run Pipeline link', () => { @@ -166,549 +206,513 @@ describe('Pipelines', () => { expect(findCleanCacheButton().text()).toBe('Clear Runner Caches'); }); - it('renders tab empty state', () => { - expect(findBlankState().text()).toBe('There are currently no pipelines.'); - }); - - it('renders tab empty state finished scope', () => { - wrapper.vm.scope = 'finished'; + it('renders pipelines in a table', () => { + expect(findPipelinesTable().exists()).toBe(true); - return nextTick().then(() => { - expect(findBlankState().text()).toBe('There are currently no finished pipelines.'); - }); + expect(findPipelineUrlLinks()).toHaveLength(mockPipelinesIds.length); + expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockPipelinesIds[0]}`); + expect(findPipelineUrlLinks().at(1).text()).toBe(`#${mockPipelinesIds[1]}`); + expect(findPipelineUrlLinks().at(2).text()).toBe(`#${mockPipelinesIds[2]}`); }); - }); - - 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 }); + describe('when user goes to a tab', () => { + const goToTab = (tab) => { + findNavigationTabs().vm.$emit('onChangeTab', tab); + }; - return waitForPromises(); - }); + describe('when the scope in the tab has pipelines', () => { + const mockFinishedPipeline = mockPipelinesResponse.pipelines[0]; - it('renders empty state', () => { - expect(findEmptyState().find('h4').text()).toBe('Build with confidence'); - expect(findEmptyState().find(GlButton).attributes('href')).toBe(paths.helpPagePath); - }); + beforeEach(async () => { + mock + .onGet(mockPipelinesEndpoint, { params: { scope: 'finished', page: '1' } }) + .reply(200, { + pipelines: [mockFinishedPipeline], + count: mockPipelinesResponse.count, + }); - it('does not render tabs nor buttons', () => { - expect(findTab('all').exists()).toBe(false); - expect(findRunPipelineButton().exists()).toBeFalsy(); - expect(findCiLintButton().exists()).toBeFalsy(); - expect(findCleanCacheButton().exists()).toBeFalsy(); - }); - }); + goToTab('finished'); - describe('When API returns error', () => { - beforeEach(() => { - mock.onGet('twitter/flight/pipelines.json').reply(500, {}); - createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths }); + await waitForPromises(); + }); - return waitForPromises(); - }); + it('should filter pipelines', async () => { + expect(findPipelinesTable().exists()).toBe(true); - it('renders tabs', () => { - expect(findTab('all').text()).toContain('All'); - }); + expect(findPipelineUrlLinks()).toHaveLength(1); + expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockFinishedPipeline.id}`); + }); - it('renders buttons', () => { - expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath); + it('should update browser bar', () => { + expect(window.history.pushState).toHaveBeenCalledTimes(1); + expect(window.history.pushState).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + `${window.location.pathname}?scope=finished&page=1`, + ); + }); + }); - expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath); - expect(findCleanCacheButton().text()).toBe('Clear Runner Caches'); - }); + describe('when the scope in the tab is empty', () => { + beforeEach(async () => { + mock + .onGet(mockPipelinesEndpoint, { params: { scope: 'branches', page: '1' } }) + .reply(200, { + pipelines: [], + count: mockPipelinesResponse.count, + }); - it('renders error state', () => { - expect(findBlankState().text()).toContain('There was an error fetching the pipelines.'); - }); - }); - }); + goToTab('branches'); - describe('Without permission', () => { - describe('With pipelines in main tab', () => { - beforeEach(() => { - mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); + await waitForPromises(); + }); - createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions }); + it('should filter pipelines', async () => { + expect(findBlankState().text()).toBe('There are currently no pipelines.'); + }); - return waitForPromises(); + it('should update browser bar', () => { + expect(window.history.pushState).toHaveBeenCalledTimes(1); + expect(window.history.pushState).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + `${window.location.pathname}?scope=branches&page=1`, + ); + }); + }); }); - it('renders tabs', () => { - expect(findTab('all').text()).toContain('All'); - }); + describe('when user triggers a filtered search', () => { + const mockFilteredPipeline = mockPipelinesResponse.pipelines[1]; - it('does not render buttons', () => { - expect(findRunPipelineButton().exists()).toBeFalsy(); - expect(findCiLintButton().exists()).toBeFalsy(); - expect(findCleanCacheButton().exists()).toBeFalsy(); - }); + let expectedParams; - it('renders pipelines table', () => { - expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength( - pipelines.pipelines.length + 1, - ); - }); - }); + beforeEach(async () => { + expectedParams = { + page: '1', + scope: 'all', + username: 'root', + ref: 'master', + status: 'pending', + }; + + mock + .onGet(mockPipelinesEndpoint, { + params: expectedParams, + }) + .replyOnce(200, { + pipelines: [mockFilteredPipeline], + count: mockPipelinesResponse.count, + }); - 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, - }, + findFilteredSearch().vm.$emit('submit', mockSearch); + + await waitForPromises(); }); - createComponent({ hasGitlabCi: true, canCreatePipeline: false, ...noPermissions }); + it('requests data with query params on filter submit', async () => { + expect(mock.history.get[1].params).toEqual(expectedParams); + }); - return waitForPromises(); - }); + it('renders filtered pipelines', async () => { + expect(findPipelineUrlLinks()).toHaveLength(1); + expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockFilteredPipeline.id}`); + }); - it('renders tabs', () => { - expect(findTab('all').text()).toContain('All'); + it('should update browser bar', () => { + expect(window.history.pushState).toHaveBeenCalledTimes(1); + expect(window.history.pushState).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + `${window.location.pathname}?page=1&scope=all&username=root&ref=master&status=pending`, + ); + }); }); - it('does not render buttons', () => { - expect(findRunPipelineButton().exists()).toBeFalsy(); - expect(findCiLintButton().exists()).toBeFalsy(); - expect(findCleanCacheButton().exists()).toBeFalsy(); - }); + describe('when user triggers a filtered search with raw text', () => { + beforeEach(async () => { + findFilteredSearch().vm.$emit('submit', ['rawText']); - it('renders tab empty state', () => { - expect(wrapper.find('.empty-state h4').text()).toBe('There are currently no pipelines.'); - }); - }); + await waitForPromises(); + }); - describe('Without pipelines nor CI', () => { - beforeEach(() => { - mock.onGet('twitter/flight/pipelines.json').reply(200, { - pipelines: [], - count: { - all: 0, - pending: 0, - running: 0, - finished: 0, - }, + it('requests data with query params on filter submit', async () => { + expect(mock.history.get[1].params).toEqual({ page: '1', scope: 'all' }); }); - createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions }); + it('displays a warning message if raw text search is used', () => { + expect(createFlash).toHaveBeenCalledTimes(1); + expect(createFlash).toHaveBeenCalledWith(RAW_TEXT_WARNING, 'warning'); + }); - return waitForPromises(); + it('should update browser bar', () => { + expect(window.history.pushState).toHaveBeenCalledTimes(1); + expect(window.history.pushState).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + `${window.location.pathname}?page=1&scope=all`, + ); + }); }); + }); + }); - it('renders empty state without button to set CI', () => { - expect(findEmptyState().text()).toBe( - 'This project is not currently set up to run pipelines.', - ); + describe('when there are multiple pages of pipelines', () => { + const mockPageSize = 2; + const mockPageHeaders = ({ page = 1 } = {}) => { + return { + 'X-PER-PAGE': `${mockPageSize}`, + 'X-PREV-PAGE': `${page - 1}`, + 'X-PAGE': `${page}`, + 'X-NEXT-PAGE': `${page + 1}`, + }; + }; + const [firstPage, secondPage] = chunk(mockPipelinesResponse.pipelines, mockPageSize); + + const goToPage = (page) => { + findTablePagination().find(GlPagination).vm.$emit('input', page); + }; + + beforeEach(async () => { + mock.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } }).reply( + 200, + { + pipelines: firstPage, + count: mockPipelinesResponse.count, + }, + mockPageHeaders({ page: 1 }), + ); + mock.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '2' } }).reply( + 200, + { + pipelines: secondPage, + count: mockPipelinesResponse.count, + }, + mockPageHeaders({ page: 2 }), + ); - expect(findEmptyState().find(GlButton).exists()).toBeFalsy(); - }); + createComponent(); - it('does not render tabs or buttons', () => { - expect(findTab('all').exists()).toBe(false); - expect(findRunPipelineButton().exists()).toBeFalsy(); - expect(findCiLintButton().exists()).toBeFalsy(); - expect(findCleanCacheButton().exists()).toBeFalsy(); - }); + await waitForPromises(); }); - describe('When API returns error', () => { - beforeEach(() => { - mock.onGet('twitter/flight/pipelines.json').reply(500, {}); - - createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...noPermissions }); + it('shows the first page of pipelines', () => { + expect(findPipelineUrlLinks()).toHaveLength(firstPage.length); + expect(findPipelineUrlLinks().at(0).text()).toBe(`#${firstPage[0].id}`); + expect(findPipelineUrlLinks().at(1).text()).toBe(`#${firstPage[1].id}`); + }); - return waitForPromises(); - }); + it('should not update browser bar', () => { + expect(window.history.pushState).not.toHaveBeenCalled(); + }); - it('renders tabs', () => { - expect(findTab('all').text()).toContain('All'); + describe('when user goes to next page', () => { + beforeEach(async () => { + goToPage(2); + await waitForPromises(); }); - it('does not renders buttons', () => { - expect(findRunPipelineButton().exists()).toBeFalsy(); - expect(findCiLintButton().exists()).toBeFalsy(); - expect(findCleanCacheButton().exists()).toBeFalsy(); + it('should update page and keep scope the same scope', () => { + expect(findPipelineUrlLinks()).toHaveLength(secondPage.length); + expect(findPipelineUrlLinks().at(0).text()).toBe(`#${secondPage[0].id}`); }); - it('renders error state', () => { - expect(wrapper.find('.empty-state').text()).toContain( - 'There was an error fetching the pipelines.', + it('should update browser bar', () => { + expect(window.history.pushState).toHaveBeenCalledTimes(1); + expect(window.history.pushState).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + `${window.location.pathname}?page=2&scope=all`, ); }); }); }); - describe('successful request', () => { - describe('with pipelines', () => { - beforeEach(() => { - mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); + describe('when pipelines can be polled', () => { + beforeEach(() => { + const emptyResponse = { + pipelines: [], + count: { all: '0' }, + }; + // Mock no pipelines in the first attempt + mock + .onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } }) + .replyOnce(200, emptyResponse, { + 'POLL-INTERVAL': 100, + }); + // Mock pipelines in the next attempt + mock + .onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } }) + .reply(200, mockPipelinesResponse, { + 'POLL-INTERVAL': 100, + }); + }); + + describe('data is loaded for the first time', () => { + beforeEach(async () => { createComponent(); - return waitForPromises(); + await waitForPromises(); }); - it('should render table', () => { - expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength( - pipelines.pipelines.length + 1, - ); + it('shows tabs', () => { + expect(findNavigationTabs().exists()).toBe(true); }); - it('should set up navigation tabs', () => { - expect(findNavigationTabs().props('tabs')).toEqual([ - { name: 'All', scope: 'all', count: '3', isActive: true }, - { name: 'Finished', scope: 'finished', count: undefined, isActive: false }, - { name: 'Branches', scope: 'branches', isActive: false }, - { name: 'Tags', scope: 'tags', isActive: false }, - ]); + it('should update page and keep scope the same scope', () => { + expect(findPipelineUrlLinks()).toHaveLength(0); }); - it('should render navigation tabs', () => { - expect(findTab('all').html()).toContain('All'); - expect(findTab('finished').text()).toContain('Finished'); - expect(findTab('branches').text()).toContain('Branches'); - expect(findTab('tags').text()).toContain('Tags'); - }); - - it('should make an API request when using tabs', () => { - createComponent({ hasGitlabCi: true, canCreatePipeline: true, ...paths }); - jest.spyOn(wrapper.vm.service, 'getPipelines'); - - return waitForPromises().then(() => { - findTab('finished').trigger('click'); - - expect(wrapper.vm.service.getPipelines).toHaveBeenCalledWith({ - scope: 'finished', - page: '1', - }); + describe('data is loaded for a second time', () => { + beforeEach(async () => { + jest.runOnlyPendingTimers(); + await waitForPromises(); }); - }); - - describe('with pagination', () => { - it('should make an API request when using pagination', () => { - createComponent({ hasGitlabCi: true, canCreatePipeline: true, ...paths }); - jest.spyOn(wrapper.vm.service, 'getPipelines'); - return waitForPromises() - .then(() => { - // Mock pagination - wrapper.vm.store.state.pageInfo = { - page: 1, - total: 10, - perPage: 2, - nextPage: 2, - totalPages: 5, - }; + it('shows tabs', () => { + expect(findNavigationTabs().exists()).toBe(true); + }); - return nextTick(); - }) - .then(() => { - wrapper.find('.next-page-item').trigger('click'); - expect(wrapper.vm.service.getPipelines).toHaveBeenCalledWith({ - scope: 'all', - page: '2', - }); - }); + it('is loading after a time', async () => { + expect(findPipelineUrlLinks()).toHaveLength(mockPipelinesIds.length); + expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockPipelinesIds[0]}`); + expect(findPipelineUrlLinks().at(1).text()).toBe(`#${mockPipelinesIds[1]}`); + expect(findPipelineUrlLinks().at(2).text()).toBe(`#${mockPipelinesIds[2]}`); }); }); }); }); - describe('User Interaction', () => { - let updateContentMock; - + describe('when no pipelines exist', () => { beforeEach(() => { - jest.spyOn(window.history, 'pushState').mockImplementation(() => null); - }); - - beforeEach(() => { - mock.onGet(paths.endpoint).reply(200, pipelines); - createComponent(); - - updateContentMock = jest.spyOn(wrapper.vm, 'updateContent'); - - return waitForPromises(); + mock.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } }).reply(200, { + pipelines: [], + count: { all: '0' }, + }); }); - describe('when user changes tabs', () => { - it('should set page to 1', () => { - findNavigationTabs().vm.$emit('onChangeTab', 'running'); + describe('when CI is enabled and user has permissions', () => { + beforeEach(async () => { + createComponent(); + await waitForPromises(); + }); - expect(updateContentMock).toHaveBeenCalledWith({ scope: 'running', page: '1' }); + it('renders tab with count of "0"', () => { + expect(findNavigationTabs().exists()).toBe(true); + expect(findTab('all').text()).toMatchInterpolatedText('All 0'); }); - }); - describe('when user changes page', () => { - it('should update page and keep scope', () => { - findTablePagination().vm.change(4); + it('renders Run Pipeline link', () => { + expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath); + }); - expect(updateContentMock).toHaveBeenCalledWith({ scope: wrapper.vm.scope, page: '4' }); + it('renders CI Lint link', () => { + expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath); }); - }); - 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); + it('renders Clear Runner Cache button', () => { + expect(findCleanCacheButton().text()).toBe('Clear Runner Caches'); + }); - createComponent(); + it('renders empty state', () => { + expect(findBlankState().text()).toBe('There are currently no pipelines.'); }); - 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; - findStagesDropdown().trigger('click'); - }) - .then(() => { - expect(cancelMock).toHaveBeenCalled(); - expect(stopMock).toHaveBeenCalled(); - expect(restartMock).toHaveBeenCalled(); - }); + it('renders tab empty state finished scope', async () => { + mock.onGet(mockPipelinesEndpoint, { params: { scope: 'finished', page: '1' } }).reply(200, { + pipelines: [], + count: { all: '0' }, }); - }); - 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); + findNavigationTabs().vm.$emit('onChangeTab', 'finished'); - return waitForPromises() - .then(() => { - findStagesDropdown().trigger('click'); - expect(stopMock).toHaveBeenCalled(); - }) - .then(() => { - expect(restartMock).toHaveBeenCalled(); - }); - }); - }); - }); - }); + await waitForPromises(); - describe('Rendered content', () => { - beforeEach(() => { - createComponent(); + expect(findBlankState().text()).toBe('There are currently no finished pipelines.'); + }); }); - describe('displays different content', () => { - it('shows loading state when the app is loading', () => { - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + describe('when CI is not enabled and user has permissions', () => { + beforeEach(async () => { + createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths }); + await waitForPromises(); }); - it('shows error state when app has error', () => { - wrapper.vm.hasError = true; - wrapper.vm.isLoading = false; - - return nextTick().then(() => { - expect(findBlankState().props('message')).toBe( - 'There was an error fetching the pipelines. Try again in a few moments or contact your support team.', - ); - }); + it('renders empty state', () => { + expect(findEmptyState().find('[data-testid="header-text"]').text()).toBe( + 'Build with confidence', + ); + expect(findEmptyState().find('[data-testid="info-text"]').text()).toContain( + 'GitLab CI/CD can automatically build, test, and deploy your code.', + ); + expect(findEmptyState().find(GlButton).text()).toBe('Get started with CI/CD'); + expect(findEmptyState().find(GlButton).attributes('href')).toBe(paths.helpPagePath); }); - it('shows table list when app has pipelines', () => { - wrapper.vm.isLoading = false; - wrapper.vm.hasError = false; - wrapper.vm.state.pipelines = pipelines.pipelines; - - return nextTick().then(() => { - expect(wrapper.find(PipelinesTableComponent).exists()).toBe(true); - }); + it('does not render tabs nor buttons', () => { + expect(findNavigationTabs().exists()).toBe(false); + expect(findTab('all').exists()).toBe(false); + expect(findRunPipelineButton().exists()).toBe(false); + expect(findCiLintButton().exists()).toBe(false); + expect(findCleanCacheButton().exists()).toBe(false); }); + }); - it('shows empty tab when app does not have pipelines but project has pipelines', () => { - wrapper.vm.state.count.all = 10; - wrapper.vm.isLoading = false; - - return nextTick().then(() => { - expect(findBlankState().exists()).toBe(true); - expect(findBlankState().props('message')).toBe('There are currently no pipelines.'); - }); + describe('when CI is not enabled and user has no permissions', () => { + beforeEach(async () => { + createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions }); + await waitForPromises(); }); - it('shows empty tab when project has CI', () => { - wrapper.vm.isLoading = false; + it('renders empty state without button to set CI', () => { + expect(findEmptyState().text()).toBe( + 'This project is not currently set up to run pipelines.', + ); - return nextTick().then(() => { - expect(findBlankState().exists()).toBe(true); - expect(findBlankState().props('message')).toBe('There are currently no pipelines.'); - }); + expect(findEmptyState().find(GlButton).exists()).toBe(false); }); - it('shows empty state when project does not have pipelines nor CI', () => { - createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths }); - - wrapper.vm.isLoading = false; - - return nextTick().then(() => { - expect(wrapper.find(EmptyState).exists()).toBe(true); - }); + it('does not render tabs or buttons', () => { + expect(findTab('all').exists()).toBe(false); + expect(findRunPipelineButton().exists()).toBe(false); + expect(findCiLintButton().exists()).toBe(false); + expect(findCleanCacheButton().exists()).toBe(false); }); }); - describe('displays tabs', () => { - it('returns true when state is loading & has already made the first request', () => { - wrapper.vm.isLoading = true; - wrapper.vm.hasMadeRequest = true; + describe('when CI is enabled and user has no permissions', () => { + beforeEach(() => { + createComponent({ hasGitlabCi: true, canCreatePipeline: false, ...noPermissions }); - return nextTick().then(() => { - expect(findNavigationTabs().exists()).toBe(true); - }); + return waitForPromises(); }); - 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 nextTick().then(() => { - expect(findNavigationTabs().exists()).toBe(true); - }); + it('renders tab with count of "0"', () => { + expect(findTab('all').text()).toMatchInterpolatedText('All 0'); }); - 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; + it('does not render buttons', () => { + expect(findRunPipelineButton().exists()).toBe(false); + expect(findCiLintButton().exists()).toBe(false); + expect(findCleanCacheButton().exists()).toBe(false); + }); - return nextTick().then(() => { - expect(findNavigationTabs().exists()).toBe(true); - }); + it('renders empty state', () => { + expect(findBlankState().text()).toBe('There are currently no pipelines.'); }); + }); + }); - 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; + describe('when a pipeline with stages exists', () => { + describe('updates results when a staged is clicked', () => { + let stopMock; + let restartMock; + let cancelMock; - return nextTick().then(() => { - expect(findNavigationTabs().exists()).toBe(true); - }); - }); + beforeEach(() => { + mock.onGet(mockPipelinesEndpoint, { scope: 'all', page: '1' }).reply( + 200, + { + pipelines: [pipelineWithStages], + count: { all: '1' }, + }, + { + 'POLL-INTERVAL': 100, + }, + ); + mock.onGet(pipelineWithStages.details.stages[0].dropdown_path).reply(200, stageReply); - it('returns false when has not made first request', () => { - wrapper.vm.hasMadeRequest = false; + createComponent(); - return nextTick().then(() => { - expect(findNavigationTabs().exists()).toBe(false); - }); + stopMock = jest.spyOn(wrapper.vm.poll, 'stop'); + restartMock = jest.spyOn(wrapper.vm.poll, 'restart'); + cancelMock = jest.spyOn(wrapper.vm.service.cancelationSource, 'cancel'); }); - it('returns false when state is empty state', () => { - createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths }); - - wrapper.vm.isLoading = false; - wrapper.vm.hasMadeRequest = true; + describe('when a request is being made', () => { + beforeEach(async () => { + mock.onGet(mockPipelinesEndpoint).reply(200, mockPipelinesResponse); - return nextTick().then(() => { - expect(findNavigationTabs().exists()).toBe(false); + await waitForPromises(); }); - }); - }); - describe('displays buttons', () => { - it('returns true when it has paths & has made the first request', () => { - wrapper.vm.hasMadeRequest = true; + it('stops polling, cancels the request, & restarts polling', async () => { + // Mock init a polling cycle + wrapper.vm.poll.options.notificationCallback(true); + + findStagesDropdown().trigger('click'); - return nextTick().then(() => { - expect(findNavigationControls().exists()).toBe(true); + await waitForPromises(); + + expect(cancelMock).toHaveBeenCalled(); + expect(stopMock).toHaveBeenCalled(); + expect(restartMock).toHaveBeenCalled(); }); - }); - it('returns false when it has not made the first request', () => { - wrapper.vm.hasMadeRequest = false; + it('stops polling & restarts polling', async () => { + findStagesDropdown().trigger('click'); - return nextTick().then(() => { - expect(findNavigationControls().exists()).toBe(false); + expect(cancelMock).not.toHaveBeenCalled(); + expect(stopMock).toHaveBeenCalled(); + expect(restartMock).toHaveBeenCalled(); }); }); }); }); - describe('Pipeline filters', () => { - let updateContentMock; - - beforeEach(() => { - mock.onGet(paths.endpoint).reply(200, pipelines); - createComponent(); + describe('when pipelines cannot be loaded', () => { + beforeEach(async () => { + mock.onGet(mockPipelinesEndpoint).reply(500, {}); + }); - updateContentMock = jest.spyOn(wrapper.vm, 'updateContent'); + describe('when user has no permissions', () => { + beforeEach(async () => { + createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...noPermissions }); - return waitForPromises(); - }); + await waitForPromises(); + }); - it('updates request data and query params on filter submit', async () => { - const expectedQueryParams = { - page: '1', - scope: 'all', - username: 'root', - ref: 'master', - status: 'pending', - }; + it('renders tabs', () => { + expect(findNavigationTabs().exists()).toBe(true); + expect(findTab('all').text()).toBe('All'); + }); - findFilteredSearch().vm.$emit('submit', mockSearch); - await nextTick(); + it('does not render buttons', () => { + expect(findRunPipelineButton().exists()).toBe(false); + expect(findCiLintButton().exists()).toBe(false); + expect(findCleanCacheButton().exists()).toBe(false); + }); - expect(wrapper.vm.requestData).toEqual(expectedQueryParams); - expect(updateContentMock).toHaveBeenCalledWith(expectedQueryParams); + it('shows error state', () => { + expect(findBlankState().text()).toBe( + 'There was an error fetching the pipelines. Try again in a few moments or contact your support team.', + ); + }); }); - it('does not add query params if raw text search is used', async () => { - const expectedQueryParams = { page: '1', scope: 'all' }; + describe('when user has permissions', () => { + beforeEach(async () => { + createComponent(); - findFilteredSearch().vm.$emit('submit', ['rawText']); - await nextTick(); + await waitForPromises(); + }); - expect(wrapper.vm.requestData).toEqual(expectedQueryParams); - expect(updateContentMock).toHaveBeenCalledWith(expectedQueryParams); - }); + it('renders tabs', () => { + expect(findTab('all').text()).toBe('All'); + }); + + it('renders buttons', () => { + expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath); - it('displays a warning message if raw text search is used', () => { - findFilteredSearch().vm.$emit('submit', ['rawText']); + expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath); + expect(findCleanCacheButton().text()).toBe('Clear Runner Caches'); + }); - expect(createFlash).toHaveBeenCalledTimes(1); - expect(createFlash).toHaveBeenCalledWith(RAW_TEXT_WARNING, 'warning'); + it('shows error state', () => { + expect(findBlankState().text()).toBe( + 'There was an error fetching the pipelines. Try again in a few moments or contact your support team.', + ); + }); }); }); }); |