summaryrefslogtreecommitdiff
path: root/spec/frontend/pipelines
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/pipelines')
-rw-r--r--spec/frontend/pipelines/graph/action_component_spec.js75
-rw-r--r--spec/frontend/pipelines/pipeline_triggerer_spec.js57
-rw-r--r--spec/frontend/pipelines/pipelines_table_row_spec.js228
-rw-r--r--spec/frontend/pipelines/test_reports/mock_data.js123
-rw-r--r--spec/frontend/pipelines/test_reports/stores/actions_spec.js109
-rw-r--r--spec/frontend/pipelines/test_reports/stores/getters_spec.js54
-rw-r--r--spec/frontend/pipelines/test_reports/stores/mutations_spec.js63
-rw-r--r--spec/frontend/pipelines/test_reports/test_reports_spec.js64
-rw-r--r--spec/frontend/pipelines/test_reports/test_suite_table_spec.js77
-rw-r--r--spec/frontend/pipelines/test_reports/test_summary_spec.js78
-rw-r--r--spec/frontend/pipelines/test_reports/test_summary_table_spec.js54
11 files changed, 982 insertions, 0 deletions
diff --git a/spec/frontend/pipelines/graph/action_component_spec.js b/spec/frontend/pipelines/graph/action_component_spec.js
new file mode 100644
index 00000000000..38ffe98c79b
--- /dev/null
+++ b/spec/frontend/pipelines/graph/action_component_spec.js
@@ -0,0 +1,75 @@
+import { mount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
+import ActionComponent from '~/pipelines/components/graph/action_component.vue';
+
+describe('pipeline graph action component', () => {
+ let wrapper;
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ mock.onPost('foo.json').reply(200);
+
+ wrapper = mount(ActionComponent, {
+ propsData: {
+ tooltipText: 'bar',
+ link: 'foo',
+ actionIcon: 'cancel',
+ },
+ sync: false,
+ });
+ });
+
+ afterEach(() => {
+ mock.restore();
+ wrapper.destroy();
+ });
+
+ it('should render the provided title as a bootstrap tooltip', () => {
+ expect(wrapper.attributes('data-original-title')).toBe('bar');
+ });
+
+ it('should update bootstrap tooltip when title changes', done => {
+ wrapper.setProps({ tooltipText: 'changed' });
+
+ wrapper.vm
+ .$nextTick()
+ .then(() => {
+ expect(wrapper.attributes('data-original-title')).toBe('changed');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('should render an svg', () => {
+ expect(wrapper.find('.ci-action-icon-wrapper')).toBeDefined();
+ expect(wrapper.find('svg')).toBeDefined();
+ });
+
+ describe('on click', () => {
+ it('emits `pipelineActionRequestComplete` after a successful request', done => {
+ jest.spyOn(wrapper.vm, '$emit');
+
+ wrapper.find('button').trigger('click');
+
+ waitForPromises()
+ .then(() => {
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('pipelineActionRequestComplete');
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('renders a loading icon while waiting for request', done => {
+ wrapper.find('button').trigger('click');
+
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.find('.js-action-icon-loading').exists()).toBe(true);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/pipeline_triggerer_spec.js b/spec/frontend/pipelines/pipeline_triggerer_spec.js
new file mode 100644
index 00000000000..45ac278dd38
--- /dev/null
+++ b/spec/frontend/pipelines/pipeline_triggerer_spec.js
@@ -0,0 +1,57 @@
+import { mount } from '@vue/test-utils';
+import pipelineTriggerer from '~/pipelines/components/pipeline_triggerer.vue';
+
+describe('Pipelines Triggerer', () => {
+ let wrapper;
+
+ const mockData = {
+ pipeline: {
+ user: {
+ name: 'foo',
+ avatar_url: '/avatar',
+ path: '/path',
+ },
+ },
+ };
+
+ const createComponent = () => {
+ wrapper = mount(pipelineTriggerer, {
+ propsData: mockData,
+ sync: false,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should render a table cell', () => {
+ expect(wrapper.contains('.table-section')).toBe(true);
+ });
+
+ it('should render triggerer information when triggerer is provided', () => {
+ const link = wrapper.find('.js-pipeline-url-user');
+
+ expect(link.attributes('href')).toEqual(mockData.pipeline.user.path);
+ expect(link.find('.js-user-avatar-image-toolip').text()).toEqual(mockData.pipeline.user.name);
+ expect(link.find('img.avatar').attributes('src')).toEqual(
+ `${mockData.pipeline.user.avatar_url}?width=26`,
+ );
+ });
+
+ it('should render "API" when no triggerer is provided', () => {
+ wrapper.setProps({
+ pipeline: {
+ user: null,
+ },
+ });
+
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.find('.js-pipeline-url-api').text()).toEqual('API');
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/pipelines_table_row_spec.js b/spec/frontend/pipelines/pipelines_table_row_spec.js
new file mode 100644
index 00000000000..1c785ec6ffe
--- /dev/null
+++ b/spec/frontend/pipelines/pipelines_table_row_spec.js
@@ -0,0 +1,228 @@
+import { mount } from '@vue/test-utils';
+import PipelinesTableRowComponent from '~/pipelines/components/pipelines_table_row.vue';
+import eventHub from '~/pipelines/event_hub';
+
+describe('Pipelines Table Row', () => {
+ const jsonFixtureName = 'pipelines/pipelines.json';
+
+ const createWrapper = pipeline =>
+ mount(PipelinesTableRowComponent, {
+ propsData: {
+ pipeline,
+ autoDevopsHelpPath: 'foo',
+ viewType: 'root',
+ },
+ sync: false,
+ });
+
+ let wrapper;
+ let pipeline;
+ let pipelineWithoutAuthor;
+ let pipelineWithoutCommit;
+
+ preloadFixtures(jsonFixtureName);
+
+ beforeEach(() => {
+ const { pipelines } = getJSONFixture(jsonFixtureName);
+
+ pipeline = pipelines.find(p => p.user !== null && p.commit !== null);
+ pipelineWithoutAuthor = pipelines.find(p => p.user === null && p.commit !== null);
+ pipelineWithoutCommit = pipelines.find(p => p.user === null && p.commit === null);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('should render a table row', () => {
+ wrapper = createWrapper(pipeline);
+
+ expect(wrapper.attributes('class')).toContain('gl-responsive-table-row');
+ });
+
+ describe('status column', () => {
+ beforeEach(() => {
+ wrapper = createWrapper(pipeline);
+ });
+
+ it('should render a pipeline link', () => {
+ expect(wrapper.find('.table-section.commit-link a').attributes('href')).toEqual(
+ pipeline.path,
+ );
+ });
+
+ it('should render status text', () => {
+ expect(wrapper.find('.table-section.commit-link a').text()).toContain(
+ pipeline.details.status.text,
+ );
+ });
+ });
+
+ describe('information column', () => {
+ beforeEach(() => {
+ wrapper = createWrapper(pipeline);
+ });
+
+ it('should render a pipeline link', () => {
+ expect(wrapper.find('.table-section:nth-child(2) a').attributes('href')).toEqual(
+ pipeline.path,
+ );
+ });
+
+ it('should render pipeline ID', () => {
+ expect(wrapper.find('.table-section:nth-child(2) a > span').text()).toEqual(
+ `#${pipeline.id}`,
+ );
+ });
+
+ describe('when a user is provided', () => {
+ it('should render user information', () => {
+ expect(
+ wrapper.find('.table-section:nth-child(3) .js-pipeline-url-user').attributes('href'),
+ ).toEqual(pipeline.user.path);
+
+ expect(
+ wrapper
+ .find('.table-section:nth-child(3) .js-user-avatar-image-toolip')
+ .text()
+ .trim(),
+ ).toEqual(pipeline.user.name);
+ });
+ });
+ });
+
+ describe('commit column', () => {
+ it('should render link to commit', () => {
+ wrapper = createWrapper(pipeline);
+
+ const commitLink = wrapper.find('.branch-commit .commit-sha');
+
+ expect(commitLink.attributes('href')).toEqual(pipeline.commit.commit_path);
+ });
+
+ const findElements = () => {
+ const commitTitleElement = wrapper.find('.branch-commit .commit-title');
+ const commitAuthorElement = commitTitleElement.find('a.avatar-image-container');
+
+ if (!commitAuthorElement.exists()) {
+ return {
+ commitAuthorElement,
+ };
+ }
+
+ const commitAuthorLink = commitAuthorElement.attributes('href');
+ const commitAuthorName = commitAuthorElement
+ .find('.js-user-avatar-image-toolip')
+ .text()
+ .trim();
+
+ return {
+ commitAuthorElement,
+ commitAuthorLink,
+ commitAuthorName,
+ };
+ };
+
+ it('renders nothing without commit', () => {
+ expect(pipelineWithoutCommit.commit).toBe(null);
+
+ wrapper = createWrapper(pipelineWithoutCommit);
+ const { commitAuthorElement } = findElements();
+
+ expect(commitAuthorElement.exists()).toBe(false);
+ });
+
+ it('renders commit author', () => {
+ wrapper = createWrapper(pipeline);
+ const { commitAuthorLink, commitAuthorName } = findElements();
+
+ expect(commitAuthorLink).toEqual(pipeline.commit.author.path);
+ expect(commitAuthorName).toEqual(pipeline.commit.author.username);
+ });
+
+ it('renders commit with unregistered author', () => {
+ expect(pipelineWithoutAuthor.commit.author).toBe(null);
+
+ wrapper = createWrapper(pipelineWithoutAuthor);
+ const { commitAuthorLink, commitAuthorName } = findElements();
+
+ expect(commitAuthorLink).toEqual(`mailto:${pipelineWithoutAuthor.commit.author_email}`);
+ expect(commitAuthorName).toEqual(pipelineWithoutAuthor.commit.author_name);
+ });
+ });
+
+ describe('stages column', () => {
+ beforeEach(() => {
+ wrapper = createWrapper(pipeline);
+ });
+
+ it('should render an icon for each stage', () => {
+ expect(
+ wrapper.findAll('.table-section:nth-child(4) .js-builds-dropdown-button').length,
+ ).toEqual(pipeline.details.stages.length);
+ });
+ });
+
+ describe('actions column', () => {
+ const scheduledJobAction = {
+ name: 'some scheduled job',
+ };
+
+ beforeEach(() => {
+ const withActions = Object.assign({}, pipeline);
+ withActions.details.scheduled_actions = [scheduledJobAction];
+ withActions.flags.cancelable = true;
+ withActions.flags.retryable = true;
+ withActions.cancel_path = '/cancel';
+ withActions.retry_path = '/retry';
+
+ wrapper = createWrapper(withActions);
+ });
+
+ it('should render the provided actions', () => {
+ expect(wrapper.find('.js-pipelines-retry-button').exists()).toBe(true);
+ expect(wrapper.find('.js-pipelines-cancel-button').exists()).toBe(true);
+ const dropdownMenu = wrapper.find('.dropdown-menu');
+
+ expect(dropdownMenu.text()).toContain(scheduledJobAction.name);
+ });
+
+ it('emits `retryPipeline` event when retry button is clicked and toggles loading', () => {
+ eventHub.$on('retryPipeline', endpoint => {
+ expect(endpoint).toBe('/retry');
+ });
+
+ wrapper.find('.js-pipelines-retry-button').trigger('click');
+ expect(wrapper.vm.isRetrying).toBe(true);
+ });
+
+ it('emits `openConfirmationModal` event when cancel button is clicked and toggles loading', () => {
+ eventHub.$once('openConfirmationModal', data => {
+ const { id, ref, commit } = pipeline;
+
+ expect(data.endpoint).toBe('/cancel');
+ expect(data.pipeline).toEqual(
+ expect.objectContaining({
+ id,
+ ref,
+ commit,
+ }),
+ );
+ });
+
+ wrapper.find('.js-pipelines-cancel-button').trigger('click');
+ });
+
+ it('renders a loading icon when `cancelingPipeline` matches pipeline id', done => {
+ wrapper.setProps({ cancelingPipeline: pipeline.id });
+ wrapper.vm
+ .$nextTick()
+ .then(() => {
+ expect(wrapper.vm.isCancelling).toBe(true);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/test_reports/mock_data.js b/spec/frontend/pipelines/test_reports/mock_data.js
new file mode 100644
index 00000000000..b0f22bc63fb
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/mock_data.js
@@ -0,0 +1,123 @@
+import { formatTime } from '~/lib/utils/datetime_utility';
+import { TestStatus } from '~/pipelines/constants';
+
+export const testCases = [
+ {
+ classname: 'spec.test_spec',
+ execution_time: 0.000748,
+ name: 'Test#subtract when a is 1 and b is 2 raises an error',
+ stack_trace: null,
+ status: TestStatus.SUCCESS,
+ system_output: null,
+ },
+ {
+ classname: 'spec.test_spec',
+ execution_time: 0.000064,
+ name: 'Test#subtract when a is 2 and b is 1 returns correct result',
+ stack_trace: null,
+ status: TestStatus.SUCCESS,
+ system_output: null,
+ },
+ {
+ classname: 'spec.test_spec',
+ execution_time: 0.009292,
+ name: 'Test#sum when a is 1 and b is 2 returns summary',
+ stack_trace: null,
+ status: TestStatus.FAILED,
+ system_output:
+ "Failure/Error: is_expected.to eq(3)\n\n expected: 3\n got: -1\n\n (compared using ==)\n./spec/test_spec.rb:12:in `block (4 levels) in <top (required)>'",
+ },
+ {
+ classname: 'spec.test_spec',
+ execution_time: 0.00018,
+ name: 'Test#sum when a is 100 and b is 200 returns summary',
+ stack_trace: null,
+ status: TestStatus.FAILED,
+ system_output:
+ "Failure/Error: is_expected.to eq(300)\n\n expected: 300\n got: -100\n\n (compared using ==)\n./spec/test_spec.rb:21:in `block (4 levels) in <top (required)>'",
+ },
+ {
+ classname: 'spec.test_spec',
+ execution_time: 0,
+ name: 'Test#skipped text',
+ stack_trace: null,
+ status: TestStatus.SKIPPED,
+ system_output: null,
+ },
+];
+
+export const testCasesFormatted = [
+ {
+ ...testCases[2],
+ icon: 'status_failed_borderless',
+ formattedTime: formatTime(testCases[0].execution_time * 1000),
+ },
+ {
+ ...testCases[3],
+ icon: 'status_failed_borderless',
+ formattedTime: formatTime(testCases[1].execution_time * 1000),
+ },
+ {
+ ...testCases[4],
+ icon: 'status_skipped_borderless',
+ formattedTime: formatTime(testCases[2].execution_time * 1000),
+ },
+ {
+ ...testCases[0],
+ icon: 'status_success_borderless',
+ formattedTime: formatTime(testCases[3].execution_time * 1000),
+ },
+ {
+ ...testCases[1],
+ icon: 'status_success_borderless',
+ formattedTime: formatTime(testCases[4].execution_time * 1000),
+ },
+];
+
+export const testSuites = [
+ {
+ error_count: 0,
+ failed_count: 2,
+ name: 'rspec:osx',
+ skipped_count: 0,
+ success_count: 2,
+ test_cases: testCases,
+ total_count: 4,
+ total_time: 60,
+ },
+ {
+ error_count: 0,
+ failed_count: 10,
+ name: 'rspec:osx',
+ skipped_count: 0,
+ success_count: 50,
+ test_cases: [],
+ total_count: 60,
+ total_time: 0.010284,
+ },
+];
+
+export const testSuitesFormatted = testSuites.map(x => ({
+ ...x,
+ formattedTime: formatTime(x.total_time * 1000),
+}));
+
+export const testReports = {
+ error_count: 0,
+ failed_count: 2,
+ skipped_count: 0,
+ success_count: 2,
+ test_suites: testSuites,
+ total_count: 4,
+ total_time: 0.010284,
+};
+
+export const testReportsWithNoSuites = {
+ error_count: 0,
+ failed_count: 2,
+ skipped_count: 0,
+ success_count: 2,
+ test_suites: [],
+ total_count: 4,
+ total_time: 0.010284,
+};
diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
new file mode 100644
index 00000000000..c1721e12234
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
@@ -0,0 +1,109 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import * as actions from '~/pipelines/stores/test_reports/actions';
+import * as types from '~/pipelines/stores/test_reports/mutation_types';
+import { TEST_HOST } from '../../../helpers/test_constants';
+import testAction from '../../../helpers/vuex_action_helper';
+import createFlash from '~/flash';
+import { testReports } from '../mock_data';
+
+jest.mock('~/flash.js');
+
+describe('Actions TestReports Store', () => {
+ let mock;
+ let state;
+
+ const endpoint = `${TEST_HOST}/test_reports.json`;
+ const defaultState = {
+ endpoint,
+ testReports: {},
+ selectedSuite: {},
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ state = defaultState;
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('fetch reports', () => {
+ beforeEach(() => {
+ mock.onGet(`${TEST_HOST}/test_reports.json`).replyOnce(200, testReports, {});
+ });
+
+ it('sets testReports and shows tests', done => {
+ testAction(
+ actions.fetchReports,
+ null,
+ state,
+ [{ type: types.SET_REPORTS, payload: testReports }],
+ [{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
+ done,
+ );
+ });
+
+ it('should create flash on API error', done => {
+ testAction(
+ actions.fetchReports,
+ null,
+ {
+ endpoint: null,
+ },
+ [],
+ [{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
+ () => {
+ expect(createFlash).toHaveBeenCalled();
+ done();
+ },
+ );
+ });
+ });
+
+ describe('set selected suite', () => {
+ const selectedSuite = testReports.test_suites[0];
+
+ it('sets selectedSuite', done => {
+ testAction(
+ actions.setSelectedSuite,
+ selectedSuite,
+ state,
+ [{ type: types.SET_SELECTED_SUITE, payload: selectedSuite }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('remove selected suite', () => {
+ it('sets selectedSuite to {}', done => {
+ testAction(
+ actions.removeSelectedSuite,
+ {},
+ state,
+ [{ type: types.SET_SELECTED_SUITE, payload: {} }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('toggles loading', () => {
+ it('sets isLoading to true', done => {
+ testAction(actions.toggleLoading, {}, state, [{ type: types.TOGGLE_LOADING }], [], done);
+ });
+
+ it('toggles isLoading to false', done => {
+ testAction(
+ actions.toggleLoading,
+ {},
+ { ...state, isLoading: true },
+ [{ type: types.TOGGLE_LOADING }],
+ [],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/test_reports/stores/getters_spec.js b/spec/frontend/pipelines/test_reports/stores/getters_spec.js
new file mode 100644
index 00000000000..e630a005409
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/stores/getters_spec.js
@@ -0,0 +1,54 @@
+import * as getters from '~/pipelines/stores/test_reports/getters';
+import { testReports, testSuitesFormatted, testCasesFormatted } from '../mock_data';
+
+describe('Getters TestReports Store', () => {
+ let state;
+
+ const defaultState = {
+ testReports,
+ selectedSuite: testReports.test_suites[0],
+ };
+
+ const emptyState = {
+ testReports: {},
+ selectedSuite: {},
+ };
+
+ beforeEach(() => {
+ state = {
+ testReports,
+ };
+ });
+
+ const setupState = (testState = defaultState) => {
+ state = testState;
+ };
+
+ describe('getTestSuites', () => {
+ it('should return the test suites', () => {
+ setupState();
+
+ expect(getters.getTestSuites(state)).toEqual(testSuitesFormatted);
+ });
+
+ it('should return an empty array when testReports is empty', () => {
+ setupState(emptyState);
+
+ expect(getters.getTestSuites(state)).toEqual([]);
+ });
+ });
+
+ describe('getSuiteTests', () => {
+ it('should return the test cases inside the suite', () => {
+ setupState();
+
+ expect(getters.getSuiteTests(state)).toEqual(testCasesFormatted);
+ });
+
+ it('should return an empty array when testReports is empty', () => {
+ setupState(emptyState);
+
+ expect(getters.getSuiteTests(state)).toEqual([]);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
new file mode 100644
index 00000000000..ad5b7f91163
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
@@ -0,0 +1,63 @@
+import * as types from '~/pipelines/stores/test_reports/mutation_types';
+import mutations from '~/pipelines/stores/test_reports/mutations';
+import { testReports, testSuites } from '../mock_data';
+
+describe('Mutations TestReports Store', () => {
+ let mockState;
+
+ const defaultState = {
+ endpoint: '',
+ testReports: {},
+ selectedSuite: {},
+ isLoading: false,
+ };
+
+ beforeEach(() => {
+ mockState = defaultState;
+ });
+
+ describe('set endpoint', () => {
+ it('should set endpoint', () => {
+ const expectedState = Object.assign({}, mockState, { endpoint: 'foo' });
+ mutations[types.SET_ENDPOINT](mockState, 'foo');
+
+ expect(mockState.endpoint).toEqual(expectedState.endpoint);
+ });
+ });
+
+ describe('set reports', () => {
+ it('should set testReports', () => {
+ const expectedState = Object.assign({}, mockState, { testReports });
+ mutations[types.SET_REPORTS](mockState, testReports);
+
+ expect(mockState.testReports).toEqual(expectedState.testReports);
+ });
+ });
+
+ describe('set selected suite', () => {
+ it('should set selectedSuite', () => {
+ const expectedState = Object.assign({}, mockState, { selectedSuite: testSuites[0] });
+ mutations[types.SET_SELECTED_SUITE](mockState, testSuites[0]);
+
+ expect(mockState.selectedSuite).toEqual(expectedState.selectedSuite);
+ });
+ });
+
+ describe('toggle loading', () => {
+ it('should set to true', () => {
+ const expectedState = Object.assign({}, mockState, { isLoading: true });
+ mutations[types.TOGGLE_LOADING](mockState);
+
+ expect(mockState.isLoading).toEqual(expectedState.isLoading);
+ });
+
+ it('should toggle back to false', () => {
+ const expectedState = Object.assign({}, mockState, { isLoading: false });
+ mockState.isLoading = true;
+
+ mutations[types.TOGGLE_LOADING](mockState);
+
+ expect(mockState.isLoading).toEqual(expectedState.isLoading);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/test_reports/test_reports_spec.js b/spec/frontend/pipelines/test_reports/test_reports_spec.js
new file mode 100644
index 00000000000..4d6422745a9
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/test_reports_spec.js
@@ -0,0 +1,64 @@
+import Vuex from 'vuex';
+import TestReports from '~/pipelines/components/test_reports/test_reports.vue';
+import { shallowMount } from '@vue/test-utils';
+import { testReports } from './mock_data';
+import * as actions from '~/pipelines/stores/test_reports/actions';
+
+describe('Test reports app', () => {
+ let wrapper;
+ let store;
+
+ const loadingSpinner = () => wrapper.find('.js-loading-spinner');
+ const testsDetail = () => wrapper.find('.js-tests-detail');
+ const noTestsToShow = () => wrapper.find('.js-no-tests-to-show');
+
+ const createComponent = (state = {}) => {
+ store = new Vuex.Store({
+ state: {
+ isLoading: false,
+ selectedSuite: {},
+ testReports,
+ ...state,
+ },
+ actions,
+ });
+
+ wrapper = shallowMount(TestReports, {
+ store,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when loading', () => {
+ beforeEach(() => createComponent({ isLoading: true }));
+
+ it('shows the loading spinner', () => {
+ expect(noTestsToShow().exists()).toBe(false);
+ expect(testsDetail().exists()).toBe(false);
+ expect(loadingSpinner().exists()).toBe(true);
+ });
+ });
+
+ describe('when the api returns no data', () => {
+ beforeEach(() => createComponent({ testReports: {} }));
+
+ it('displays that there are no tests to show', () => {
+ const noTests = noTestsToShow();
+
+ expect(noTests.exists()).toBe(true);
+ expect(noTests.text()).toBe('There are no tests to show.');
+ });
+ });
+
+ describe('when the api returns data', () => {
+ beforeEach(() => createComponent());
+
+ it('sets testReports and shows tests', () => {
+ expect(wrapper.vm.testReports).toBeTruthy();
+ expect(wrapper.vm.showTests).toBeTruthy();
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
new file mode 100644
index 00000000000..b4305719ea8
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
@@ -0,0 +1,77 @@
+import Vuex from 'vuex';
+import SuiteTable from '~/pipelines/components/test_reports/test_suite_table.vue';
+import * as getters from '~/pipelines/stores/test_reports/getters';
+import { TestStatus } from '~/pipelines/constants';
+import { shallowMount } from '@vue/test-utils';
+import { testSuites, testCases } from './mock_data';
+
+describe('Test reports suite table', () => {
+ let wrapper;
+ let store;
+
+ const noCasesMessage = () => wrapper.find('.js-no-test-cases');
+ const allCaseRows = () => wrapper.findAll('.js-case-row');
+ const findCaseRowAtIndex = index => wrapper.findAll('.js-case-row').at(index);
+ const findIconForRow = (row, status) => row.find(`.ci-status-icon-${status}`);
+
+ const createComponent = (suite = testSuites[0]) => {
+ store = new Vuex.Store({
+ state: {
+ selectedSuite: suite,
+ },
+ getters,
+ });
+
+ wrapper = shallowMount(SuiteTable, {
+ store,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('should not render', () => {
+ beforeEach(() => createComponent([]));
+
+ it('a table when there are no test cases', () => {
+ expect(noCasesMessage().exists()).toBe(true);
+ });
+ });
+
+ describe('when a test suite is supplied', () => {
+ beforeEach(() => createComponent());
+
+ it('renders the correct number of rows', () => {
+ expect(allCaseRows().length).toBe(testCases.length);
+ });
+
+ it('renders the failed tests first', () => {
+ const failedCaseNames = testCases
+ .filter(x => x.status === TestStatus.FAILED)
+ .map(x => x.name);
+
+ const skippedCaseNames = testCases
+ .filter(x => x.status === TestStatus.SKIPPED)
+ .map(x => x.name);
+
+ expect(findCaseRowAtIndex(0).text()).toContain(failedCaseNames[0]);
+ expect(findCaseRowAtIndex(1).text()).toContain(failedCaseNames[1]);
+ expect(findCaseRowAtIndex(2).text()).toContain(skippedCaseNames[0]);
+ });
+
+ it('renders the correct icon for each status', () => {
+ const failedTest = testCases.findIndex(x => x.status === TestStatus.FAILED);
+ const skippedTest = testCases.findIndex(x => x.status === TestStatus.SKIPPED);
+ const successTest = testCases.findIndex(x => x.status === TestStatus.SUCCESS);
+
+ const failedRow = findCaseRowAtIndex(failedTest);
+ const skippedRow = findCaseRowAtIndex(skippedTest);
+ const successRow = findCaseRowAtIndex(successTest);
+
+ expect(findIconForRow(failedRow, TestStatus.FAILED).exists()).toBe(true);
+ expect(findIconForRow(skippedRow, TestStatus.SKIPPED).exists()).toBe(true);
+ expect(findIconForRow(successRow, TestStatus.SUCCESS).exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/test_reports/test_summary_spec.js b/spec/frontend/pipelines/test_reports/test_summary_spec.js
new file mode 100644
index 00000000000..19a7755dbdc
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/test_summary_spec.js
@@ -0,0 +1,78 @@
+import Summary from '~/pipelines/components/test_reports/test_summary.vue';
+import { mount } from '@vue/test-utils';
+import { testSuites } from './mock_data';
+
+describe('Test reports summary', () => {
+ let wrapper;
+
+ const backButton = () => wrapper.find('.js-back-button');
+ const totalTests = () => wrapper.find('.js-total-tests');
+ const failedTests = () => wrapper.find('.js-failed-tests');
+ const erroredTests = () => wrapper.find('.js-errored-tests');
+ const successRate = () => wrapper.find('.js-success-rate');
+ const duration = () => wrapper.find('.js-duration');
+
+ const defaultProps = {
+ report: testSuites[0],
+ showBack: false,
+ };
+
+ const createComponent = props => {
+ wrapper = mount(Summary, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ describe('should not render', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('a back button by default', () => {
+ expect(backButton().exists()).toBe(false);
+ });
+ });
+
+ describe('should render', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('a back button and emit on-back-click event', () => {
+ createComponent({
+ showBack: true,
+ });
+
+ expect(backButton().exists()).toBe(true);
+ });
+ });
+
+ describe('when a report is supplied', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('displays the correct total', () => {
+ expect(totalTests().text()).toBe('4 jobs');
+ });
+
+ it('displays the correct failure count', () => {
+ expect(failedTests().text()).toBe('2 failures');
+ });
+
+ it('displays the correct error count', () => {
+ expect(erroredTests().text()).toBe('0 errors');
+ });
+
+ it('calculates and displays percentages correctly', () => {
+ expect(successRate().text()).toBe('50% success rate');
+ });
+
+ it('displays the correctly formatted duration', () => {
+ expect(duration().text()).toBe('00:01:00');
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js
new file mode 100644
index 00000000000..e7599d5cdbc
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js
@@ -0,0 +1,54 @@
+import Vuex from 'vuex';
+import SummaryTable from '~/pipelines/components/test_reports/test_summary_table.vue';
+import * as getters from '~/pipelines/stores/test_reports/getters';
+import { mount, createLocalVue } from '@vue/test-utils';
+import { testReports, testReportsWithNoSuites } from './mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('Test reports summary table', () => {
+ let wrapper;
+ let store;
+
+ const allSuitesRows = () => wrapper.findAll('.js-suite-row');
+ const noSuitesToShow = () => wrapper.find('.js-no-tests-suites');
+
+ const defaultProps = {
+ testReports,
+ };
+
+ const createComponent = (reports = null) => {
+ store = new Vuex.Store({
+ state: {
+ testReports: reports || testReports,
+ },
+ getters,
+ });
+
+ wrapper = mount(SummaryTable, {
+ propsData: defaultProps,
+ store,
+ localVue,
+ });
+ };
+
+ describe('when test reports are supplied', () => {
+ beforeEach(() => createComponent());
+
+ it('renders the correct number of rows', () => {
+ expect(noSuitesToShow().exists()).toBe(false);
+ expect(allSuitesRows().length).toBe(testReports.test_suites.length);
+ });
+ });
+
+ describe('when there are no test suites', () => {
+ beforeEach(() => {
+ createComponent({ testReportsWithNoSuites });
+ });
+
+ it('displays the no suites to show message', () => {
+ expect(noSuitesToShow().exists()).toBe(true);
+ });
+ });
+});