Commit 61f27ddc authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '215883-refactor-pipelines-spec-pagination' into 'master'

Refactor pipeline list specs

See merge request gitlab-org/gitlab!54005
parents 3e3c1c03 5b40320a
...@@ -141,8 +141,8 @@ describe('Commit pipeline status component', () => { ...@@ -141,8 +141,8 @@ describe('Commit pipeline status component', () => {
expect(findLink().attributes('href')).toEqual(mockCiStatus.details_path); expect(findLink().attributes('href')).toEqual(mockCiStatus.details_path);
}); });
it('renders CI icon', () => { it('renders CI icon with the correct title and status', () => {
expect(findCiIcon().attributes('title')).toEqual('Pipeline: pending'); expect(findCiIcon().attributes('title')).toEqual('Pipeline: passed');
expect(findCiIcon().props('status')).toEqual(mockCiStatus); expect(findCiIcon().props('status')).toEqual(mockCiStatus);
}); });
}); });
......
...@@ -12,7 +12,7 @@ RSpec.describe Projects::PipelinesController, '(JavaScript fixtures)', type: :co ...@@ -12,7 +12,7 @@ RSpec.describe Projects::PipelinesController, '(JavaScript fixtures)', type: :co
let!(:user) { create(:user, developer_projects: [project], email: commit.author_email) } let!(:user) { create(:user, developer_projects: [project], email: commit.author_email) }
let!(:pipeline) { create(:ci_pipeline, project: project, sha: commit.id, user: user) } let!(:pipeline) { create(:ci_pipeline, project: project, sha: commit.id, user: user) }
let!(:pipeline_without_author) { create(:ci_pipeline, project: project, sha: commit_without_author.id) } let!(:pipeline_without_author) { create(:ci_pipeline, project: project, sha: commit_without_author.id) }
let!(:pipeline_without_commit) { create(:ci_pipeline, project: project, sha: '0000') } let!(:pipeline_without_commit) { create(:ci_pipeline, status: :success, project: project, sha: '0000') }
render_views render_views
......
import { GlFilteredSearch, GlButton, GlLoadingIcon } from '@gitlab/ui'; import { GlFilteredSearch, GlButton, GlLoadingIcon, GlPagination } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { chunk } from 'lodash';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api'; import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
...@@ -20,29 +22,29 @@ import { pipelineWithStages, stageReply, users, mockSearch, branches } from './m ...@@ -20,29 +22,29 @@ import { pipelineWithStages, stageReply, users, mockSearch, branches } from './m
jest.mock('~/flash'); jest.mock('~/flash');
describe('Pipelines', () => { const mockProjectPath = 'twitter/flight';
const jsonFixtureName = 'pipelines/pipelines.json'; const mockProjectId = '21';
const mockPipelinesEndpoint = `/${mockProjectPath}/pipelines.json`;
preloadFixtures(jsonFixtureName); const mockPipelinesResponse = getJSONFixture('pipelines/pipelines.json');
const mockPipelinesIds = mockPipelinesResponse.pipelines.map(({ id }) => id);
let pipelines; describe('Pipelines', () => {
let wrapper; let wrapper;
let mock; let mock;
let origWindowLocation;
const paths = { const paths = {
endpoint: 'twitter/flight/pipelines.json',
autoDevopsHelpPath: '/help/topics/autodevops/index.md', autoDevopsHelpPath: '/help/topics/autodevops/index.md',
helpPagePath: '/help/ci/quick_start/README', helpPagePath: '/help/ci/quick_start/README',
emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg', emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg', errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg', noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg',
ciLintPath: '/ci/lint', ciLintPath: '/ci/lint',
resetCachePath: '/twitter/flight/settings/ci_cd/reset_cache', resetCachePath: `${mockProjectPath}/settings/ci_cd/reset_cache`,
newPipelinePath: '/twitter/flight/pipelines/new', newPipelinePath: `${mockProjectPath}/pipelines/new`,
}; };
const noPermissions = { const noPermissions = {
endpoint: 'twitter/flight/pipelines.json',
autoDevopsHelpPath: '/help/topics/autodevops/index.md', autoDevopsHelpPath: '/help/topics/autodevops/index.md',
helpPagePath: '/help/ci/quick_start/README', helpPagePath: '/help/ci/quick_start/README',
emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg', emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
...@@ -56,101 +58,140 @@ describe('Pipelines', () => { ...@@ -56,101 +58,140 @@ describe('Pipelines', () => {
...paths, ...paths,
}; };
const findFilteredSearch = () => wrapper.find(GlFilteredSearch); const findFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
const findByTestId = (id) => wrapper.find(`[data-testid="${id}"]`); const findNavigationTabs = () => wrapper.findComponent(NavigationTabs);
const findNavigationTabs = () => wrapper.find(NavigationTabs); const findNavigationControls = () => wrapper.findComponent(NavigationControls);
const findNavigationControls = () => wrapper.find(NavigationControls); const findPipelinesTable = () => wrapper.findComponent(PipelinesTableComponent);
const findTab = (tab) => findByTestId(`pipelines-tab-${tab}`); const findEmptyState = () => wrapper.findComponent(EmptyState);
const findBlankState = () => wrapper.findComponent(BlankState);
const findRunPipelineButton = () => findByTestId('run-pipeline-button'); const findTablePagination = () => wrapper.findComponent(TablePagination);
const findCiLintButton = () => findByTestId('ci-lint-button');
const findCleanCacheButton = () => findByTestId('clear-cache-button'); const findTab = (tab) => wrapper.findByTestId(`pipelines-tab-${tab}`);
const findStagesDropdown = () => findByTestId('mini-pipeline-graph-dropdown-toggle'); const findRunPipelineButton = () => wrapper.findByTestId('run-pipeline-button');
const findCiLintButton = () => wrapper.findByTestId('ci-lint-button');
const findEmptyState = () => wrapper.find(EmptyState); const findCleanCacheButton = () => wrapper.findByTestId('clear-cache-button');
const findBlankState = () => wrapper.find(BlankState); const findStagesDropdown = () => wrapper.findByTestId('mini-pipeline-graph-dropdown-toggle');
const findPipelineUrlLinks = () => wrapper.findAll('[data-testid="pipeline-url-link"]');
const findTablePagination = () => wrapper.find(TablePagination);
const createComponent = (props = defaultProps) => { const createComponent = (props = defaultProps) => {
wrapper = mount(PipelinesComponent, { wrapper = extendedWrapper(
propsData: { mount(PipelinesComponent, {
store: new Store(), propsData: {
projectId: '21', store: new Store(),
params: {}, projectId: mockProjectId,
...props, endpoint: mockPipelinesEndpoint,
}, params: {},
}); ...props,
},
}),
);
}; };
beforeEach(() => { beforeAll(() => {
origWindowLocation = window.location;
delete window.location; delete window.location;
window.location = { search: '' };
});
afterAll(() => {
window.location = origWindowLocation;
}); });
beforeEach(() => { beforeEach(() => {
window.location = { search: '' };
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
pipelines = getJSONFixture(jsonFixtureName);
jest.spyOn(window.history, 'pushState');
jest.spyOn(Api, 'projectUsers').mockResolvedValue(users); jest.spyOn(Api, 'projectUsers').mockResolvedValue(users);
jest.spyOn(Api, 'branches').mockResolvedValue({ data: branches }); jest.spyOn(Api, 'branches').mockResolvedValue({ data: branches });
}); });
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
mock.restore(); mock.reset();
window.history.pushState.mockReset();
}); });
describe('With permission', () => { describe('when pipelines are not yet loaded', () => {
describe('With pipelines in main tab', () => { beforeEach(async () => {
beforeEach(() => { createComponent();
mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); await nextTick();
createComponent(); });
return waitForPromises();
});
it('renders tabs', () => { it('shows loading state when the app is loading', () => {
expect(findTab('all').text()).toContain('All'); expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
}); });
it('renders Run Pipeline link', () => { it('does not display tabs when the first request has not yet been made', () => {
expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath); 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', () => { it('renders "All" tab with count different from "0"', () => {
expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath); expect(findTab('all').text()).toMatchInterpolatedText('All 3');
}); });
it('renders Clear Runner Cache button', () => { it('does not render buttons', () => {
expect(findCleanCacheButton().text()).toBe('Clear Runner Caches'); expect(findNavigationControls().exists()).toBe(false);
expect(findRunPipelineButton().exists()).toBe(false);
expect(findCiLintButton().exists()).toBe(false);
expect(findCleanCacheButton().exists()).toBe(false);
}); });
it('renders pipelines table', () => { it('renders pipelines in a table', () => {
expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength( expect(findPipelinesTable().exists()).toBe(true);
pipelines.pipelines.length + 1,
); 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', () => { describe('when user has permissions', () => {
beforeEach(() => { beforeEach(async () => {
mock.onGet('twitter/flight/pipelines.json').reply(200, {
pipelines: [],
count: {
all: 0,
pending: 0,
running: 0,
finished: 0,
},
});
createComponent(); 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', () => { it('renders "All" tab with count different from "0"', () => {
expect(findTab('all').text()).toContain('All'); 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', () => { it('renders Run Pipeline link', () => {
...@@ -165,549 +206,513 @@ describe('Pipelines', () => { ...@@ -165,549 +206,513 @@ describe('Pipelines', () => {
expect(findCleanCacheButton().text()).toBe('Clear Runner Caches'); expect(findCleanCacheButton().text()).toBe('Clear Runner Caches');
}); });
it('renders tab empty state', () => { it('renders pipelines in a table', () => {
expect(findBlankState().text()).toBe('There are currently no pipelines.'); expect(findPipelinesTable().exists()).toBe(true);
});
it('renders tab empty state finished scope', () => {
wrapper.vm.scope = 'finished';
return nextTick().then(() => { expect(findPipelineUrlLinks()).toHaveLength(mockPipelinesIds.length);
expect(findBlankState().text()).toBe('There are currently no finished pipelines.'); 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', () => { beforeEach(async () => {
expect(findEmptyState().find('h4').text()).toBe('Build with confidence'); mock
expect(findEmptyState().find(GlButton).attributes('href')).toBe(paths.helpPagePath); .onGet(mockPipelinesEndpoint, { params: { scope: 'finished', page: '1' } })
}); .reply(200, {
pipelines: [mockFinishedPipeline],
count: mockPipelinesResponse.count,
});
it('does not render tabs nor buttons', () => { goToTab('finished');
expect(findTab('all').exists()).toBe(false);
expect(findRunPipelineButton().exists()).toBeFalsy();
expect(findCiLintButton().exists()).toBeFalsy();
expect(findCleanCacheButton().exists()).toBeFalsy();
});
});
describe('When API returns error', () => { await waitForPromises();
beforeEach(() => { });
mock.onGet('twitter/flight/pipelines.json').reply(500, {});
createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
return waitForPromises(); it('should filter pipelines', async () => {
}); expect(findPipelinesTable().exists()).toBe(true);
it('renders tabs', () => { expect(findPipelineUrlLinks()).toHaveLength(1);
expect(findTab('all').text()).toContain('All'); expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockFinishedPipeline.id}`);
}); });
it('renders buttons', () => { it('should update browser bar', () => {
expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath); 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); describe('when the scope in the tab is empty', () => {
expect(findCleanCacheButton().text()).toBe('Clear Runner Caches'); beforeEach(async () => {
}); mock
.onGet(mockPipelinesEndpoint, { params: { scope: 'branches', page: '1' } })
.reply(200, {
pipelines: [],
count: mockPipelinesResponse.count,
});
it('renders error state', () => { goToTab('branches');
expect(findBlankState().text()).toContain('There was an error fetching the pipelines.');
});
});
});
describe('Without permission', () => { await waitForPromises();
describe('With pipelines in main tab', () => { });
beforeEach(() => {
mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
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', () => { describe('when user triggers a filtered search', () => {
expect(findTab('all').text()).toContain('All'); const mockFilteredPipeline = mockPipelinesResponse.pipelines[1];
});
it('does not render buttons', () => { let expectedParams;
expect(findRunPipelineButton().exists()).toBeFalsy();
expect(findCiLintButton().exists()).toBeFalsy();
expect(findCleanCacheButton().exists()).toBeFalsy();
});
it('renders pipelines table', () => { beforeEach(async () => {
expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength( expectedParams = {
pipelines.pipelines.length + 1, 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', () => { findFilteredSearch().vm.$emit('submit', mockSearch);
beforeEach(() => {
mock.onGet('twitter/flight/pipelines.json').reply(200, { await waitForPromises();
pipelines: [],
count: {
all: 0,
pending: 0,
running: 0,
finished: 0,
},
}); });
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', () => { it('should update browser bar', () => {
expect(findTab('all').text()).toContain('All'); 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', () => { describe('when user triggers a filtered search with raw text', () => {
expect(findRunPipelineButton().exists()).toBeFalsy(); beforeEach(async () => {
expect(findCiLintButton().exists()).toBeFalsy(); findFilteredSearch().vm.$emit('submit', ['rawText']);
expect(findCleanCacheButton().exists()).toBeFalsy();
});
it('renders tab empty state', () => { await waitForPromises();
expect(wrapper.find('.empty-state h4').text()).toBe('There are currently no pipelines.'); });
});
});
describe('Without pipelines nor CI', () => { it('requests data with query params on filter submit', async () => {
beforeEach(() => { expect(mock.history.get[1].params).toEqual({ page: '1', scope: 'all' });
mock.onGet('twitter/flight/pipelines.json').reply(200, {
pipelines: [],
count: {
all: 0,
pending: 0,
running: 0,
finished: 0,
},
}); });
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', () => { describe('when there are multiple pages of pipelines', () => {
expect(findEmptyState().text()).toBe( const mockPageSize = 2;
'This project is not currently set up to run pipelines.', 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', () => { await waitForPromises();
expect(findTab('all').exists()).toBe(false);
expect(findRunPipelineButton().exists()).toBeFalsy();
expect(findCiLintButton().exists()).toBeFalsy();
expect(findCleanCacheButton().exists()).toBeFalsy();
});
}); });
describe('When API returns error', () => { it('shows the first page of pipelines', () => {
beforeEach(() => { expect(findPipelineUrlLinks()).toHaveLength(firstPage.length);
mock.onGet('twitter/flight/pipelines.json').reply(500, {}); expect(findPipelineUrlLinks().at(0).text()).toBe(`#${firstPage[0].id}`);
expect(findPipelineUrlLinks().at(1).text()).toBe(`#${firstPage[1].id}`);
createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...noPermissions }); });
return waitForPromises(); it('should not update browser bar', () => {
}); expect(window.history.pushState).not.toHaveBeenCalled();
});
it('renders tabs', () => { describe('when user goes to next page', () => {
expect(findTab('all').text()).toContain('All'); beforeEach(async () => {
goToPage(2);
await waitForPromises();
}); });
it('does not renders buttons', () => { it('should update page and keep scope the same scope', () => {
expect(findRunPipelineButton().exists()).toBeFalsy(); expect(findPipelineUrlLinks()).toHaveLength(secondPage.length);
expect(findCiLintButton().exists()).toBeFalsy(); expect(findPipelineUrlLinks().at(0).text()).toBe(`#${secondPage[0].id}`);
expect(findCleanCacheButton().exists()).toBeFalsy();
}); });
it('renders error state', () => { it('should update browser bar', () => {
expect(wrapper.find('.empty-state').text()).toContain( expect(window.history.pushState).toHaveBeenCalledTimes(1);
'There was an error fetching the pipelines.', expect(window.history.pushState).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
`${window.location.pathname}?page=2&scope=all`,
); );
}); });
}); });
}); });
describe('successful request', () => { describe('when pipelines can be polled', () => {
describe('with pipelines', () => { beforeEach(() => {
beforeEach(() => { const emptyResponse = {
mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); pipelines: [],
count: { all: '0' },
};
createComponent(); // Mock no pipelines in the first attempt
return waitForPromises(); 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,
});
});
it('should render table', () => { describe('data is loaded for the first time', () => {
expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength( beforeEach(async () => {
pipelines.pipelines.length + 1, createComponent();
); await waitForPromises();
}); });
it('should set up navigation tabs', () => { it('shows tabs', () => {
expect(findNavigationTabs().props('tabs')).toEqual([ expect(findNavigationTabs().exists()).toBe(true);
{ 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 render navigation tabs', () => { it('should update page and keep scope the same scope', () => {
expect(findTab('all').html()).toContain('All'); expect(findPipelineUrlLinks()).toHaveLength(0);
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', () => { describe('data is loaded for a second time', () => {
createComponent({ hasGitlabCi: true, canCreatePipeline: true, ...paths }); beforeEach(async () => {
jest.spyOn(wrapper.vm.service, 'getPipelines'); jest.runOnlyPendingTimers();
await waitForPromises();
return waitForPromises().then(() => {
findTab('finished').trigger('click');
expect(wrapper.vm.service.getPipelines).toHaveBeenCalledWith({
scope: 'finished',
page: '1',
});
}); });
});
describe('with pagination', () => { it('shows tabs', () => {
it('should make an API request when using pagination', () => { expect(findNavigationTabs().exists()).toBe(true);
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,
};
return nextTick(); it('is loading after a time', async () => {
}) expect(findPipelineUrlLinks()).toHaveLength(mockPipelinesIds.length);
.then(() => { expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockPipelinesIds[0]}`);
wrapper.find('.next-page-item').trigger('click'); expect(findPipelineUrlLinks().at(1).text()).toBe(`#${mockPipelinesIds[1]}`);
expect(wrapper.vm.service.getPipelines).toHaveBeenCalledWith({ expect(findPipelineUrlLinks().at(2).text()).toBe(`#${mockPipelinesIds[2]}`);
scope: 'all',
page: '2',
});
});
}); });
}); });
}); });
}); });
describe('User Interaction', () => { describe('when no pipelines exist', () => {
let updateContentMock;
beforeEach(() => {
jest.spyOn(window.history, 'pushState').mockImplementation(() => null);
});
beforeEach(() => { beforeEach(() => {
mock.onGet(paths.endpoint).reply(200, pipelines); mock.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } }).reply(200, {
createComponent(); pipelines: [],
count: { all: '0' },
updateContentMock = jest.spyOn(wrapper.vm, 'updateContent'); });
return waitForPromises();
}); });
describe('when user changes tabs', () => { describe('when CI is enabled and user has permissions', () => {
it('should set page to 1', () => { beforeEach(async () => {
findNavigationTabs().vm.$emit('onChangeTab', 'running'); 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('renders Run Pipeline link', () => {
it('should update page and keep scope', () => { expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath);
findTablePagination().vm.change(4); });
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', () => { it('renders Clear Runner Cache button', () => {
beforeEach(() => { expect(findCleanCacheButton().text()).toBe('Clear Runner Caches');
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(); it('renders empty state', () => {
expect(findBlankState().text()).toBe('There are currently no pipelines.');
}); });
describe('when a request is being made', () => { it('renders tab empty state finished scope', async () => {
it('stops polling, cancels the request, & restarts polling', () => { mock.onGet(mockPipelinesEndpoint, { params: { scope: 'finished', page: '1' } }).reply(200, {
const stopMock = jest.spyOn(wrapper.vm.poll, 'stop'); pipelines: [],
const restartMock = jest.spyOn(wrapper.vm.poll, 'restart'); count: { all: '0' },
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();
});
}); });
});
describe('when no request is being made', () => { findNavigationTabs().vm.$emit('onChangeTab', 'finished');
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() await waitForPromises();
.then(() => {
findStagesDropdown().trigger('click');
expect(stopMock).toHaveBeenCalled();
})
.then(() => {
expect(restartMock).toHaveBeenCalled();
});
});
});
});
});
describe('Rendered content', () => { expect(findBlankState().text()).toBe('There are currently no finished pipelines.');
beforeEach(() => { });
createComponent();
}); });
describe('displays different content', () => { describe('when CI is not enabled and user has permissions', () => {
it('shows loading state when the app is loading', () => { beforeEach(async () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
await waitForPromises();
}); });
it('shows error state when app has error', () => { it('renders empty state', () => {
wrapper.vm.hasError = true; expect(findEmptyState().find('[data-testid="header-text"]').text()).toBe(
wrapper.vm.isLoading = false; 'Build with confidence',
);
return nextTick().then(() => { expect(findEmptyState().find('[data-testid="info-text"]').text()).toContain(
expect(findBlankState().props('message')).toBe( 'GitLab CI/CD can automatically build, test, and deploy your code.',
'There was an error fetching the pipelines. Try again in a few moments or contact your support team.', );
); 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', () => { it('does not render tabs nor buttons', () => {
wrapper.vm.isLoading = false; expect(findNavigationTabs().exists()).toBe(false);
wrapper.vm.hasError = false; expect(findTab('all').exists()).toBe(false);
wrapper.vm.state.pipelines = pipelines.pipelines; expect(findRunPipelineButton().exists()).toBe(false);
expect(findCiLintButton().exists()).toBe(false);
return nextTick().then(() => { expect(findCleanCacheButton().exists()).toBe(false);
expect(wrapper.find(PipelinesTableComponent).exists()).toBe(true);
});
}); });
});
it('shows empty tab when app does not have pipelines but project has pipelines', () => { describe('when CI is not enabled and user has no permissions', () => {
wrapper.vm.state.count.all = 10; beforeEach(async () => {
wrapper.vm.isLoading = false; createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions });
await waitForPromises();
return nextTick().then(() => {
expect(findBlankState().exists()).toBe(true);
expect(findBlankState().props('message')).toBe('There are currently no pipelines.');
});
}); });
it('shows empty tab when project has CI', () => { it('renders empty state without button to set CI', () => {
wrapper.vm.isLoading = false; expect(findEmptyState().text()).toBe(
'This project is not currently set up to run pipelines.',
);
return nextTick().then(() => { expect(findEmptyState().find(GlButton).exists()).toBe(false);
expect(findBlankState().exists()).toBe(true);
expect(findBlankState().props('message')).toBe('There are currently no pipelines.');
});
}); });
it('shows empty state when project does not have pipelines nor CI', () => { it('does not render tabs or buttons', () => {
createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths }); expect(findTab('all').exists()).toBe(false);
expect(findRunPipelineButton().exists()).toBe(false);
wrapper.vm.isLoading = false; expect(findCiLintButton().exists()).toBe(false);
expect(findCleanCacheButton().exists()).toBe(false);
return nextTick().then(() => {
expect(wrapper.find(EmptyState).exists()).toBe(true);
});
}); });
}); });
describe('displays tabs', () => { describe('when CI is enabled and user has no permissions', () => {
it('returns true when state is loading & has already made the first request', () => { beforeEach(() => {
wrapper.vm.isLoading = true; createComponent({ hasGitlabCi: true, canCreatePipeline: false, ...noPermissions });
wrapper.vm.hasMadeRequest = true;
return nextTick().then(() => { return waitForPromises();
expect(findNavigationTabs().exists()).toBe(true);
});
}); });
it('returns true when state is tableList & has already made the first request', () => { it('renders tab with count of "0"', () => {
wrapper.vm.isLoading = false; expect(findTab('all').text()).toMatchInterpolatedText('All 0');
wrapper.vm.state.pipelines = pipelines.pipelines;
wrapper.vm.hasMadeRequest = true;
return nextTick().then(() => {
expect(findNavigationTabs().exists()).toBe(true);
});
}); });
it('returns true when state is error & has already made the first request', () => { it('does not render buttons', () => {
wrapper.vm.isLoading = false; expect(findRunPipelineButton().exists()).toBe(false);
wrapper.vm.hasError = true; expect(findCiLintButton().exists()).toBe(false);
wrapper.vm.hasMadeRequest = true; expect(findCleanCacheButton().exists()).toBe(false);
});
return nextTick().then(() => { it('renders empty state', () => {
expect(findNavigationTabs().exists()).toBe(true); expect(findBlankState().text()).toBe('There are currently no pipelines.');
});
}); });
});
});
it('returns true when state is empty tab & has already made the first request', () => { describe('when a pipeline with stages exists', () => {
wrapper.vm.isLoading = false; describe('updates results when a staged is clicked', () => {
wrapper.vm.state.count.all = 10; let stopMock;
wrapper.vm.hasMadeRequest = true; let restartMock;
let cancelMock;
return nextTick().then(() => { beforeEach(() => {
expect(findNavigationTabs().exists()).toBe(true); 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', () => { createComponent();
wrapper.vm.hasMadeRequest = false;
return nextTick().then(() => { stopMock = jest.spyOn(wrapper.vm.poll, 'stop');
expect(findNavigationTabs().exists()).toBe(false); restartMock = jest.spyOn(wrapper.vm.poll, 'restart');
}); cancelMock = jest.spyOn(wrapper.vm.service.cancelationSource, 'cancel');
}); });
it('returns false when state is empty state', () => { describe('when a request is being made', () => {
createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths }); beforeEach(async () => {
mock.onGet(mockPipelinesEndpoint).reply(200, mockPipelinesResponse);
wrapper.vm.isLoading = false;
wrapper.vm.hasMadeRequest = true;
return nextTick().then(() => { await waitForPromises();
expect(findNavigationTabs().exists()).toBe(false);
}); });
});
});
describe('displays buttons', () => { it('stops polling, cancels the request, & restarts polling', async () => {
it('returns true when it has paths & has made the first request', () => { // Mock init a polling cycle
wrapper.vm.hasMadeRequest = true; wrapper.vm.poll.options.notificationCallback(true);
findStagesDropdown().trigger('click');
await waitForPromises();
return nextTick().then(() => { expect(cancelMock).toHaveBeenCalled();
expect(findNavigationControls().exists()).toBe(true); expect(stopMock).toHaveBeenCalled();
expect(restartMock).toHaveBeenCalled();
}); });
});
it('returns false when it has not made the first request', () => { it('stops polling & restarts polling', async () => {
wrapper.vm.hasMadeRequest = false; findStagesDropdown().trigger('click');
return nextTick().then(() => { expect(cancelMock).not.toHaveBeenCalled();
expect(findNavigationControls().exists()).toBe(false); expect(stopMock).toHaveBeenCalled();
expect(restartMock).toHaveBeenCalled();
}); });
}); });
}); });
}); });
describe('Pipeline filters', () => { describe('when pipelines cannot be loaded', () => {
let updateContentMock; beforeEach(async () => {
mock.onGet(mockPipelinesEndpoint).reply(500, {});
beforeEach(() => { });
mock.onGet(paths.endpoint).reply(200, pipelines);
createComponent();
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 () => { it('renders tabs', () => {
const expectedQueryParams = { expect(findNavigationTabs().exists()).toBe(true);
page: '1', expect(findTab('all').text()).toBe('All');
scope: 'all', });
username: 'root',
ref: 'master',
status: 'pending',
};
findFilteredSearch().vm.$emit('submit', mockSearch); it('does not render buttons', () => {
await nextTick(); expect(findRunPipelineButton().exists()).toBe(false);
expect(findCiLintButton().exists()).toBe(false);
expect(findCleanCacheButton().exists()).toBe(false);
});
expect(wrapper.vm.requestData).toEqual(expectedQueryParams); it('shows error state', () => {
expect(updateContentMock).toHaveBeenCalledWith(expectedQueryParams); 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 () => { describe('when user has permissions', () => {
const expectedQueryParams = { page: '1', scope: 'all' }; beforeEach(async () => {
createComponent();
findFilteredSearch().vm.$emit('submit', ['rawText']); await waitForPromises();
await nextTick(); });
expect(wrapper.vm.requestData).toEqual(expectedQueryParams); it('renders tabs', () => {
expect(updateContentMock).toHaveBeenCalledWith(expectedQueryParams); 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', () => { expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath);
findFilteredSearch().vm.$emit('submit', ['rawText']); expect(findCleanCacheButton().text()).toBe('Clear Runner Caches');
});
expect(createFlash).toHaveBeenCalledTimes(1); it('shows error state', () => {
expect(createFlash).toHaveBeenCalledWith(RAW_TEXT_WARNING, 'warning'); expect(findBlankState().text()).toBe(
'There was an error fetching the pipelines. Try again in a few moments or contact your support team.',
);
});
}); });
}); });
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment