diff options
Diffstat (limited to 'spec/frontend/pipelines')
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); + }); + }); +}); |