diff options
Diffstat (limited to 'spec/frontend/reports')
10 files changed, 918 insertions, 35 deletions
diff --git a/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js b/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js new file mode 100644 index 00000000000..3e11af9c9df --- /dev/null +++ b/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js @@ -0,0 +1,62 @@ +import { shallowMount } from '@vue/test-utils'; +import component from '~/reports/codequality_report/components/codequality_issue_body.vue'; +import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants'; + +describe('code quality issue body issue body', () => { + let wrapper; + + const codequalityIssue = { + name: + 'rubygem-rest-client: session fixation vulnerability via Set-Cookie headers in 30x redirection responses', + path: 'Gemfile.lock', + severity: 'normal', + type: 'Issue', + urlPath: '/Gemfile.lock#L22', + }; + + const mountWithStatus = initialStatus => { + wrapper = shallowMount(component, { + propsData: { + issue: codequalityIssue, + status: initialStatus, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('with success', () => { + it('renders fixed label', () => { + mountWithStatus(STATUS_SUCCESS); + + expect(wrapper.text()).toContain('Fixed'); + }); + }); + + describe('without success', () => { + it('renders fixed label', () => { + mountWithStatus(STATUS_FAILED); + + expect(wrapper.text()).not.toContain('Fixed'); + }); + }); + + describe('name', () => { + it('renders name', () => { + mountWithStatus(STATUS_NEUTRAL); + + expect(wrapper.text()).toContain(codequalityIssue.name); + }); + }); + + describe('path', () => { + it('renders the report-link path using the correct code quality issue', () => { + mountWithStatus(STATUS_NEUTRAL); + + expect(wrapper.find('report-link-stub').props('issue')).toBe(codequalityIssue); + }); + }); +}); diff --git a/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js b/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js new file mode 100644 index 00000000000..1905ca0d5e1 --- /dev/null +++ b/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js @@ -0,0 +1,146 @@ +import { mount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; +import GroupedCodequalityReportsApp from '~/reports/codequality_report/grouped_codequality_reports_app.vue'; +import CodequalityIssueBody from '~/reports/codequality_report/components/codequality_issue_body.vue'; +import store from '~/reports/codequality_report/store'; +import { mockParsedHeadIssues, mockParsedBaseIssues } from './mock_data'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('Grouped code quality reports app', () => { + const Component = localVue.extend(GroupedCodequalityReportsApp); + let wrapper; + let mockStore; + + const mountComponent = (props = {}) => { + wrapper = mount(Component, { + store: mockStore, + localVue, + propsData: { + basePath: 'base.json', + headPath: 'head.json', + baseBlobPath: 'base/blob/path/', + headBlobPath: 'head/blob/path/', + codequalityHelpPath: 'codequality_help.html', + ...props, + }, + methods: { + fetchReports: () => {}, + }, + }); + }; + + const findWidget = () => wrapper.find('.js-codequality-widget'); + const findIssueBody = () => wrapper.find(CodequalityIssueBody); + + beforeEach(() => { + mockStore = store(); + mountComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when it is loading reports', () => { + beforeEach(() => { + mockStore.state.isLoading = true; + }); + + it('should render loading text', () => { + expect(findWidget().text()).toEqual('Loading codeclimate report'); + }); + }); + + describe('when base and head reports are loaded and compared', () => { + describe('with no issues', () => { + beforeEach(() => { + mockStore.state.newIssues = []; + mockStore.state.resolvedIssues = []; + }); + + it('renders no changes text', () => { + expect(findWidget().text()).toEqual('No changes to code quality'); + }); + }); + + describe('with issues', () => { + describe('with new issues', () => { + beforeEach(() => { + mockStore.state.newIssues = [mockParsedHeadIssues[0]]; + mockStore.state.resolvedIssues = []; + }); + + it('renders summary text', () => { + expect(findWidget().text()).toContain('Code quality degraded on 1 point'); + }); + + it('renders custom codequality issue body', () => { + expect(findIssueBody().props('issue')).toEqual(mockParsedHeadIssues[0]); + }); + }); + + describe('with resolved issues', () => { + beforeEach(() => { + mockStore.state.newIssues = []; + mockStore.state.resolvedIssues = [mockParsedBaseIssues[0]]; + }); + + it('renders summary text', () => { + expect(findWidget().text()).toContain('Code quality improved on 1 point'); + }); + + it('renders custom codequality issue body', () => { + expect(findIssueBody().props('issue')).toEqual(mockParsedBaseIssues[0]); + }); + }); + + describe('with new and resolved issues', () => { + beforeEach(() => { + mockStore.state.newIssues = [mockParsedHeadIssues[0]]; + mockStore.state.resolvedIssues = [mockParsedBaseIssues[0]]; + }); + + it('renders summary text', () => { + expect(findWidget().text()).toContain( + 'Code quality improved on 1 point and degraded on 1 point', + ); + }); + + it('renders custom codequality issue body', () => { + expect(findIssueBody().props('issue')).toEqual(mockParsedHeadIssues[0]); + }); + }); + }); + }); + + describe('when there is a head report but no base report', () => { + beforeEach(() => { + mockStore.state.basePath = null; + mockStore.state.hasError = true; + }); + + it('renders error text', () => { + expect(findWidget().text()).toEqual('Failed to load codeclimate report'); + }); + + it('renders a help icon with more information', () => { + expect(findWidget().html()).toContain('ic-question'); + }); + }); + + describe('on error', () => { + beforeEach(() => { + mockStore.state.hasError = true; + }); + + it('renders error text', () => { + expect(findWidget().text()).toContain('Failed to load codeclimate report'); + }); + + it('does not render a help icon', () => { + expect(findWidget().html()).not.toContain('ic-question'); + }); + }); +}); diff --git a/spec/frontend/reports/codequality_report/mock_data.js b/spec/frontend/reports/codequality_report/mock_data.js new file mode 100644 index 00000000000..9bd61527d3f --- /dev/null +++ b/spec/frontend/reports/codequality_report/mock_data.js @@ -0,0 +1,90 @@ +export const headIssues = [ + { + check_name: 'Rubocop/Lint/UselessAssignment', + description: 'Insecure Dependency', + location: { + path: 'lib/six.rb', + lines: { + begin: 6, + end: 7, + }, + }, + fingerprint: 'e879dd9bbc0953cad5037cde7ff0f627', + }, + { + categories: ['Security'], + check_name: 'Insecure Dependency', + description: 'Insecure Dependency', + location: { + path: 'Gemfile.lock', + lines: { + begin: 22, + end: 22, + }, + }, + fingerprint: 'ca2e59451e98ae60ba2f54e3857c50e5', + }, +]; + +export const mockParsedHeadIssues = [ + { + ...headIssues[1], + name: 'Insecure Dependency', + path: 'lib/six.rb', + urlPath: 'headPath/lib/six.rb#L6', + line: 6, + }, +]; + +export const baseIssues = [ + { + categories: ['Security'], + check_name: 'Insecure Dependency', + description: 'Insecure Dependency', + location: { + path: 'Gemfile.lock', + lines: { + begin: 22, + end: 22, + }, + }, + fingerprint: 'ca2e59451e98ae60ba2f54e3857c50e5', + }, + { + categories: ['Security'], + check_name: 'Insecure Dependency', + description: 'Insecure Dependency', + location: { + path: 'Gemfile.lock', + lines: { + begin: 21, + end: 21, + }, + }, + fingerprint: 'ca2354534dee94ae60ba2f54e3857c50e5', + }, +]; + +export const mockParsedBaseIssues = [ + { + ...baseIssues[1], + name: 'Insecure Dependency', + path: 'Gemfile.lock', + line: 21, + urlPath: 'basePath/Gemfile.lock#L21', + }, +]; + +export const issueDiff = [ + { + categories: ['Security'], + check_name: 'Insecure Dependency', + description: 'Insecure Dependency', + fingerprint: 'ca2e59451e98ae60ba2f54e3857c50e5', + line: 6, + location: { lines: { begin: 22, end: 22 }, path: 'Gemfile.lock' }, + name: 'Insecure Dependency', + path: 'lib/six.rb', + urlPath: 'headPath/lib/six.rb#L6', + }, +]; diff --git a/spec/frontend/reports/codequality_report/store/actions_spec.js b/spec/frontend/reports/codequality_report/store/actions_spec.js new file mode 100644 index 00000000000..6c30fdb7871 --- /dev/null +++ b/spec/frontend/reports/codequality_report/store/actions_spec.js @@ -0,0 +1,151 @@ +import axios from '~/lib/utils/axios_utils'; +import MockAdapter from 'axios-mock-adapter'; +import * as actions from '~/reports/codequality_report/store/actions'; +import * as types from '~/reports/codequality_report/store/mutation_types'; +import createStore from '~/reports/codequality_report/store'; +import { TEST_HOST } from 'spec/test_constants'; +import testAction from 'helpers/vuex_action_helper'; +import { headIssues, baseIssues, mockParsedHeadIssues, mockParsedBaseIssues } from '../mock_data'; + +// mock codequality comparison worker +jest.mock('~/reports/codequality_report/workers/codequality_comparison_worker', () => + jest.fn().mockImplementation(() => { + return { + addEventListener: (eventName, callback) => { + callback({ + data: { + newIssues: [mockParsedHeadIssues[0]], + resolvedIssues: [mockParsedBaseIssues[0]], + }, + }); + }, + }; + }), +); + +describe('Codequality Reports actions', () => { + let localState; + let localStore; + + beforeEach(() => { + localStore = createStore(); + localState = localStore.state; + }); + + describe('setPaths', () => { + it('should commit SET_PATHS mutation', done => { + const paths = { + basePath: 'basePath', + headPath: 'headPath', + baseBlobPath: 'baseBlobPath', + headBlobPath: 'headBlobPath', + helpPath: 'codequalityHelpPath', + }; + + testAction( + actions.setPaths, + paths, + localState, + [{ type: types.SET_PATHS, payload: paths }], + [], + done, + ); + }); + }); + + describe('fetchReports', () => { + let mock; + + beforeEach(() => { + localState.headPath = `${TEST_HOST}/head.json`; + localState.basePath = `${TEST_HOST}/base.json`; + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('on success', () => { + it('commits REQUEST_REPORTS and dispatches receiveReportsSuccess', done => { + mock.onGet(`${TEST_HOST}/head.json`).reply(200, headIssues); + mock.onGet(`${TEST_HOST}/base.json`).reply(200, baseIssues); + + testAction( + actions.fetchReports, + null, + localState, + [{ type: types.REQUEST_REPORTS }], + [ + { + payload: { + newIssues: [mockParsedHeadIssues[0]], + resolvedIssues: [mockParsedBaseIssues[0]], + }, + type: 'receiveReportsSuccess', + }, + ], + done, + ); + }); + }); + + describe('on error', () => { + it('commits REQUEST_REPORTS and dispatches receiveReportsError', done => { + mock.onGet(`${TEST_HOST}/head.json`).reply(500); + + testAction( + actions.fetchReports, + null, + localState, + [{ type: types.REQUEST_REPORTS }], + [{ type: 'receiveReportsError' }], + done, + ); + }); + }); + + describe('with no base path', () => { + it('commits REQUEST_REPORTS and dispatches receiveReportsError', done => { + localState.basePath = null; + + testAction( + actions.fetchReports, + null, + localState, + [{ type: types.REQUEST_REPORTS }], + [{ type: 'receiveReportsError' }], + done, + ); + }); + }); + }); + + describe('receiveReportsSuccess', () => { + it('commits RECEIVE_REPORTS_SUCCESS', done => { + const data = { issues: [] }; + + testAction( + actions.receiveReportsSuccess, + data, + localState, + [{ type: types.RECEIVE_REPORTS_SUCCESS, payload: data }], + [], + done, + ); + }); + }); + + describe('receiveReportsError', () => { + it('commits RECEIVE_REPORTS_ERROR', done => { + testAction( + actions.receiveReportsError, + null, + localState, + [{ type: types.RECEIVE_REPORTS_ERROR }], + [], + done, + ); + }); + }); +}); diff --git a/spec/frontend/reports/codequality_report/store/getters_spec.js b/spec/frontend/reports/codequality_report/store/getters_spec.js new file mode 100644 index 00000000000..a641e2fe74f --- /dev/null +++ b/spec/frontend/reports/codequality_report/store/getters_spec.js @@ -0,0 +1,95 @@ +import * as getters from '~/reports/codequality_report/store/getters'; +import createStore from '~/reports/codequality_report/store'; +import { LOADING, ERROR, SUCCESS } from '~/reports/constants'; + +describe('Codequality reports store getters', () => { + let localState; + let localStore; + + beforeEach(() => { + localStore = createStore(); + localState = localStore.state; + }); + + describe('hasCodequalityIssues', () => { + describe('when there are issues', () => { + it('returns true', () => { + localState.newIssues = [{ reason: 'repetitive code' }]; + localState.resolvedIssues = []; + + expect(getters.hasCodequalityIssues(localState)).toEqual(true); + + localState.newIssues = []; + localState.resolvedIssues = [{ reason: 'repetitive code' }]; + + expect(getters.hasCodequalityIssues(localState)).toEqual(true); + }); + }); + + describe('when there are no issues', () => { + it('returns false when there are no issues', () => { + expect(getters.hasCodequalityIssues(localState)).toEqual(false); + }); + }); + }); + + describe('codequalityStatus', () => { + describe('when loading', () => { + it('returns loading status', () => { + localState.isLoading = true; + + expect(getters.codequalityStatus(localState)).toEqual(LOADING); + }); + }); + + describe('on error', () => { + it('returns error status', () => { + localState.hasError = true; + + expect(getters.codequalityStatus(localState)).toEqual(ERROR); + }); + }); + + describe('when successfully loaded', () => { + it('returns error status', () => { + expect(getters.codequalityStatus(localState)).toEqual(SUCCESS); + }); + }); + }); + + describe('codequalityText', () => { + it.each` + resolvedIssues | newIssues | expectedText + ${0} | ${0} | ${'No changes to code quality'} + ${0} | ${1} | ${'Code quality degraded on 1 point'} + ${2} | ${0} | ${'Code quality improved on 2 points'} + ${1} | ${2} | ${'Code quality improved on 1 point and degraded on 2 points'} + `( + 'returns a summary containing $resolvedIssues resolved issues and $newIssues new issues', + ({ newIssues, resolvedIssues, expectedText }) => { + localState.newIssues = new Array(newIssues).fill({ reason: 'Repetitive code' }); + localState.resolvedIssues = new Array(resolvedIssues).fill({ reason: 'Repetitive code' }); + + expect(getters.codequalityText(localState)).toEqual(expectedText); + }, + ); + }); + + describe('codequalityPopover', () => { + describe('when head report is available but base report is not', () => { + it('returns a popover with a documentation link', () => { + localState.headPath = 'head.json'; + localState.basePath = undefined; + localState.helpPath = 'codequality_help.html'; + + expect(getters.codequalityPopover(localState).title).toEqual( + 'Base pipeline codequality artifact not found', + ); + expect(getters.codequalityPopover(localState).content).toContain( + 'Learn more about codequality reports', + 'href="codequality_help.html"', + ); + }); + }); + }); +}); diff --git a/spec/frontend/reports/codequality_report/store/mutations_spec.js b/spec/frontend/reports/codequality_report/store/mutations_spec.js new file mode 100644 index 00000000000..658abf3088c --- /dev/null +++ b/spec/frontend/reports/codequality_report/store/mutations_spec.js @@ -0,0 +1,80 @@ +import mutations from '~/reports/codequality_report/store/mutations'; +import createStore from '~/reports/codequality_report/store'; + +describe('Codequality Reports mutations', () => { + let localState; + let localStore; + + beforeEach(() => { + localStore = createStore(); + localState = localStore.state; + }); + + describe('SET_PATHS', () => { + it('sets paths to given values', () => { + const basePath = 'base.json'; + const headPath = 'head.json'; + const baseBlobPath = 'base/blob/path/'; + const headBlobPath = 'head/blob/path/'; + const helpPath = 'help.html'; + + mutations.SET_PATHS(localState, { + basePath, + headPath, + baseBlobPath, + headBlobPath, + helpPath, + }); + + expect(localState.basePath).toEqual(basePath); + expect(localState.headPath).toEqual(headPath); + expect(localState.baseBlobPath).toEqual(baseBlobPath); + expect(localState.headBlobPath).toEqual(headBlobPath); + expect(localState.helpPath).toEqual(helpPath); + }); + }); + + describe('REQUEST_REPORTS', () => { + it('sets isLoading to true', () => { + mutations.REQUEST_REPORTS(localState); + + expect(localState.isLoading).toEqual(true); + }); + }); + + describe('RECEIVE_REPORTS_SUCCESS', () => { + it('sets isLoading to false', () => { + mutations.RECEIVE_REPORTS_SUCCESS(localState, {}); + + expect(localState.isLoading).toEqual(false); + }); + + it('sets hasError to false', () => { + mutations.RECEIVE_REPORTS_SUCCESS(localState, {}); + + expect(localState.hasError).toEqual(false); + }); + + it('sets newIssues and resolvedIssues from response data', () => { + const data = { newIssues: [{ id: 1 }], resolvedIssues: [{ id: 2 }] }; + mutations.RECEIVE_REPORTS_SUCCESS(localState, data); + + expect(localState.newIssues).toEqual(data.newIssues); + expect(localState.resolvedIssues).toEqual(data.resolvedIssues); + }); + }); + + describe('RECEIVE_REPORTS_ERROR', () => { + it('sets isLoading to false', () => { + mutations.RECEIVE_REPORTS_ERROR(localState); + + expect(localState.isLoading).toEqual(false); + }); + + it('sets hasError to true', () => { + mutations.RECEIVE_REPORTS_ERROR(localState); + + expect(localState.hasError).toEqual(true); + }); + }); +}); diff --git a/spec/frontend/reports/codequality_report/store/utils/codequality_comparison_spec.js b/spec/frontend/reports/codequality_report/store/utils/codequality_comparison_spec.js new file mode 100644 index 00000000000..5dd69d3c4d4 --- /dev/null +++ b/spec/frontend/reports/codequality_report/store/utils/codequality_comparison_spec.js @@ -0,0 +1,139 @@ +import { + parseCodeclimateMetrics, + doCodeClimateComparison, +} from '~/reports/codequality_report/store/utils/codequality_comparison'; +import { baseIssues, mockParsedHeadIssues, mockParsedBaseIssues } from '../../mock_data'; + +jest.mock('~/reports/codequality_report/workers/codequality_comparison_worker', () => { + let mockPostMessageCallback; + return jest.fn().mockImplementation(() => { + return { + addEventListener: (_, callback) => { + mockPostMessageCallback = callback; + }, + postMessage: data => { + if (!data.headIssues) return mockPostMessageCallback({ data: {} }); + if (!data.baseIssues) throw new Error(); + const key = 'fingerprint'; + return mockPostMessageCallback({ + data: { + newIssues: data.headIssues.filter( + item => !data.baseIssues.find(el => el[key] === item[key]), + ), + resolvedIssues: data.baseIssues.filter( + item => !data.headIssues.find(el => el[key] === item[key]), + ), + }, + }); + }, + }; + }); +}); + +describe('Codequality report store utils', () => { + let result; + + describe('parseCodeclimateMetrics', () => { + it('should parse the received issues', () => { + [result] = parseCodeclimateMetrics(baseIssues, 'path'); + + expect(result.name).toEqual(baseIssues[0].check_name); + expect(result.path).toEqual(baseIssues[0].location.path); + expect(result.line).toEqual(baseIssues[0].location.lines.begin); + }); + + describe('when an issue has no location or path', () => { + const issue = { description: 'Insecure Dependency' }; + + beforeEach(() => { + [result] = parseCodeclimateMetrics([issue], 'path'); + }); + + it('is parsed', () => { + expect(result.name).toEqual(issue.description); + }); + }); + + describe('when an issue has a path but no line', () => { + const issue = { description: 'Insecure Dependency', location: { path: 'Gemfile.lock' } }; + + beforeEach(() => { + [result] = parseCodeclimateMetrics([issue], 'path'); + }); + + it('is parsed', () => { + expect(result.name).toEqual(issue.description); + expect(result.path).toEqual(issue.location.path); + expect(result.urlPath).toEqual(`path/${issue.location.path}`); + }); + }); + + describe('when an issue has a line nested in positions', () => { + const issue = { + description: 'Insecure Dependency', + location: { + path: 'Gemfile.lock', + positions: { begin: { line: 84 } }, + }, + }; + + beforeEach(() => { + [result] = parseCodeclimateMetrics([issue], 'path'); + }); + + it('is parsed', () => { + expect(result.name).toEqual(issue.description); + expect(result.path).toEqual(issue.location.path); + expect(result.urlPath).toEqual( + `path/${issue.location.path}#L${issue.location.positions.begin.line}`, + ); + }); + }); + + describe('with an empty issue array', () => { + beforeEach(() => { + result = parseCodeclimateMetrics([], 'path'); + }); + + it('returns an empty array', () => { + expect(result).toEqual([]); + }); + }); + }); + + describe('doCodeClimateComparison', () => { + describe('when the comparison worker finds changed issues', () => { + beforeEach(async () => { + result = await doCodeClimateComparison(mockParsedHeadIssues, mockParsedBaseIssues); + }); + + it('returns the new and resolved issues', () => { + expect(result.resolvedIssues[0]).toEqual(mockParsedBaseIssues[0]); + expect(result.newIssues[0]).toEqual(mockParsedHeadIssues[0]); + }); + }); + + describe('when the comparison worker finds no changed issues', () => { + beforeEach(async () => { + result = await doCodeClimateComparison([], []); + }); + + it('returns the empty issue arrays', () => { + expect(result.newIssues).toEqual([]); + expect(result.resolvedIssues).toEqual([]); + }); + }); + + describe('when the comparison worker is given malformed data', () => { + it('rejects the promise', () => { + return expect(doCodeClimateComparison(null)).rejects.toEqual({}); + }); + }); + + describe('when the comparison worker encounters an error', () => { + it('rejects the promise and throws an error', () => { + return expect(doCodeClimateComparison([], null)).rejects.toThrow(); + }); + }); + }); +}); diff --git a/spec/frontend/reports/components/grouped_test_reports_app_spec.js b/spec/frontend/reports/components/grouped_test_reports_app_spec.js index 6a402277f52..017e0335569 100644 --- a/spec/frontend/reports/components/grouped_test_reports_app_spec.js +++ b/spec/frontend/reports/components/grouped_test_reports_app_spec.js @@ -15,20 +15,29 @@ localVue.use(Vuex); describe('Grouped test reports app', () => { const endpoint = 'endpoint.json'; + const pipelinePath = '/path/to/pipeline'; const Component = localVue.extend(GroupedTestReportsApp); let wrapper; let mockStore; - const mountComponent = () => { + const mountComponent = ({ + glFeatures = { junitPipelineView: false }, + props = { pipelinePath }, + } = {}) => { wrapper = mount(Component, { store: mockStore, localVue, propsData: { endpoint, + pipelinePath, + ...props, }, methods: { fetchReports: () => {}, }, + provide: { + glFeatures, + }, }); }; @@ -39,6 +48,7 @@ describe('Grouped test reports app', () => { }; const findHeader = () => wrapper.find('[data-testid="report-section-code-text"]'); + const findFullTestReportLink = () => wrapper.find('[data-testid="group-test-reports-full-link"]'); const findSummaryDescription = () => wrapper.find('[data-testid="test-summary-row-description"]'); const findIssueDescription = () => wrapper.find('[data-testid="test-issue-body-description"]'); const findAllIssueDescriptions = () => @@ -67,6 +77,39 @@ describe('Grouped test reports app', () => { }); }); + describe('`View full report` button', () => { + it('should not render the full test report link', () => { + expect(findFullTestReportLink().exists()).toBe(false); + }); + + describe('With junitPipelineView feature flag enabled', () => { + beforeEach(() => { + mountComponent({ glFeatures: { junitPipelineView: true } }); + }); + + it('should render the full test report link', () => { + const fullTestReportLink = findFullTestReportLink(); + + expect(fullTestReportLink.exists()).toBe(true); + expect(pipelinePath).not.toBe(''); + expect(fullTestReportLink.attributes('href')).toBe(`${pipelinePath}/test_report`); + }); + }); + + describe('Without a pipelinePath', () => { + beforeEach(() => { + mountComponent({ + glFeatures: { junitPipelineView: true }, + props: { pipelinePath: '' }, + }); + }); + + it('should not render the full test report link', () => { + expect(findFullTestReportLink().exists()).toBe(false); + }); + }); + }); + describe('with new failed result', () => { beforeEach(() => { setReports(newFailedTestReports); diff --git a/spec/frontend/reports/components/report_section_spec.js b/spec/frontend/reports/components/report_section_spec.js index eaeb074acaf..a620b5d9afc 100644 --- a/spec/frontend/reports/components/report_section_spec.js +++ b/spec/frontend/reports/components/report_section_spec.js @@ -1,9 +1,11 @@ import Vue from 'vue'; +import { shallowMount } from '@vue/test-utils'; import mountComponent, { mountComponentWithSlots } from 'helpers/vue_mount_component_helper'; import reportSection from '~/reports/components/report_section.vue'; describe('Report section', () => { let vm; + let wrapper; const ReportSection = Vue.extend(reportSection); const resolvedIssues = [ @@ -16,22 +18,41 @@ describe('Report section', () => { }, ]; + const defaultProps = { + component: '', + status: 'SUCCESS', + loadingText: 'Loading codeclimate report', + errorText: 'foo', + successText: 'Code quality improved on 1 point and degraded on 1 point', + resolvedIssues, + hasIssues: false, + alwaysOpen: false, + }; + + const createComponent = props => { + wrapper = shallowMount(reportSection, { + propsData: { + ...defaultProps, + ...props, + }, + }); + return wrapper; + }; + afterEach(() => { - vm.$destroy(); + if (vm) { + vm.$destroy(); + vm = null; + } + if (wrapper) { + wrapper.destroy(); + wrapper = null; + } }); describe('computed', () => { beforeEach(() => { - vm = mountComponent(ReportSection, { - component: '', - status: 'SUCCESS', - loadingText: 'Loading codeclimate report', - errorText: 'foo', - successText: 'Code quality improved on 1 point and degraded on 1 point', - resolvedIssues, - hasIssues: false, - alwaysOpen: false, - }); + vm = mountComponent(ReportSection, defaultProps); }); describe('isCollapsible', () => { @@ -105,12 +126,7 @@ describe('Report section', () => { describe('with success status', () => { beforeEach(() => { vm = mountComponent(ReportSection, { - component: '', - status: 'SUCCESS', - loadingText: 'Loading codeclimate report', - errorText: 'foo', - successText: 'Code quality improved on 1 point and degraded on 1 point', - resolvedIssues, + ...defaultProps, hasIssues: true, }); }); @@ -160,6 +176,50 @@ describe('Report section', () => { }); }); + describe('snowplow events', () => { + it('does emit an event on issue toggle if the shouldEmitToggleEvent prop does exist', done => { + createComponent({ hasIssues: true, shouldEmitToggleEvent: true }); + + expect(wrapper.emitted().toggleEvent).toBeUndefined(); + + wrapper.vm.$el.querySelector('button').click(); + return wrapper.vm + .$nextTick() + .then(() => { + expect(wrapper.emitted().toggleEvent).toHaveLength(1); + }) + .then(done) + .catch(done.fail); + }); + + it('does not emit an event on issue toggle if the shouldEmitToggleEvent prop does not exist', done => { + createComponent({ hasIssues: true }); + + expect(wrapper.emitted().toggleEvent).toBeUndefined(); + + wrapper.vm.$el.querySelector('button').click(); + return wrapper.vm + .$nextTick() + .then(() => { + expect(wrapper.emitted().toggleEvent).toBeUndefined(); + }) + .then(done) + .catch(done.fail); + }); + + it('does not emit an event if always-open is set to true', done => { + createComponent({ alwaysOpen: true, hasIssues: true, shouldEmitToggleEvent: true }); + + wrapper.vm + .$nextTick() + .then(() => { + expect(wrapper.emitted().toggleEvent).toBeUndefined(); + }) + .then(done) + .catch(done.fail); + }); + }); + describe('with failed request', () => { it('should render error indicator', () => { vm = mountComponent(ReportSection, { @@ -199,7 +259,7 @@ describe('Report section', () => { }); describe('Success and Error slots', () => { - const createComponent = status => { + const createComponentWithSlots = status => { vm = mountComponentWithSlots(ReportSection, { props: { status, @@ -214,7 +274,7 @@ describe('Report section', () => { }; it('only renders success slot when status is "SUCCESS"', () => { - createComponent('SUCCESS'); + createComponentWithSlots('SUCCESS'); expect(vm.$el.textContent.trim()).toContain('This is a success'); expect(vm.$el.textContent.trim()).not.toContain('This is an error'); @@ -222,7 +282,7 @@ describe('Report section', () => { }); it('only renders error slot when status is "ERROR"', () => { - createComponent('ERROR'); + createComponentWithSlots('ERROR'); expect(vm.$el.textContent.trim()).toContain('This is an error'); expect(vm.$el.textContent.trim()).not.toContain('This is a success'); @@ -230,7 +290,7 @@ describe('Report section', () => { }); it('only renders loading slot when status is "LOADING"', () => { - createComponent('LOADING'); + createComponentWithSlots('LOADING'); expect(vm.$el.textContent.trim()).toContain('This is loading'); expect(vm.$el.textContent.trim()).not.toContain('This is an error'); diff --git a/spec/frontend/reports/components/summary_row_spec.js b/spec/frontend/reports/components/summary_row_spec.js index cb0cc025e80..85c68ed069b 100644 --- a/spec/frontend/reports/components/summary_row_spec.js +++ b/spec/frontend/reports/components/summary_row_spec.js @@ -1,10 +1,8 @@ -import Vue from 'vue'; -import mountComponent from 'helpers/vue_mount_component_helper'; -import component from '~/reports/components/summary_row.vue'; +import { mount } from '@vue/test-utils'; +import SummaryRow from '~/reports/components/summary_row.vue'; describe('Summary row', () => { - const Component = Vue.extend(component); - let vm; + let wrapper; const props = { summary: 'SAST detected 1 new vulnerability and 1 fixed vulnerability', @@ -15,23 +13,42 @@ describe('Summary row', () => { statusIcon: 'warning', }; - beforeEach(() => { - vm = mountComponent(Component, props); - }); + const createComponent = ({ propsData = {}, slots = {} } = {}) => { + wrapper = mount(SummaryRow, { + propsData: { + ...props, + ...propsData, + }, + slots, + }); + }; + + const findSummary = () => wrapper.find('.report-block-list-issue-description-text'); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); + wrapper = null; }); it('renders provided summary', () => { - expect( - vm.$el.querySelector('.report-block-list-issue-description-text').textContent.trim(), - ).toEqual(props.summary); + createComponent(); + expect(findSummary().text()).toEqual(props.summary); }); it('renders provided icon', () => { - expect(vm.$el.querySelector('.report-block-list-icon span').classList).toContain( + createComponent(); + expect(wrapper.find('.report-block-list-icon span').classes()).toContain( 'js-ci-status-icon-warning', ); }); + + describe('summary slot', () => { + it('replaces the summary prop', () => { + const summarySlotContent = 'Summary slot content'; + createComponent({ slots: { summary: summarySlotContent } }); + + expect(wrapper.text()).not.toContain(props.summary); + expect(findSummary().text()).toEqual(summarySlotContent); + }); + }); }); |