Commit 8a942142 authored by Phil Hughes's avatar Phil Hughes

Merge branch '45318-junit-FE' into 'master'

Frontend code for "JUnit XML Test Summary In MR widget"

See merge request gitlab-org/gitlab-ce!20936
parents beda5ca5 94981308
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { s__ } from '~/locale';
import { componentNames } from '~/vue_shared/components/reports/issue_body';
import ReportSection from '~/vue_shared/components/reports/report_section.vue';
import SummaryRow from '~/vue_shared/components/reports/summary_row.vue';
import IssuesList from '~/vue_shared/components/reports/issues_list.vue';
import Modal from './modal.vue';
import createStore from '../store';
import { summaryTextBuilder, reportTextBuilder, statusIcon } from '../store/utils';
export default {
name: 'GroupedTestReportsApp',
store: createStore(),
components: {
ReportSection,
SummaryRow,
IssuesList,
Modal,
},
props: {
endpoint: {
type: String,
required: true,
},
},
componentNames,
computed: {
...mapState([
'reports',
'isLoading',
'hasError',
'summary',
]),
...mapState({
modalTitle: state => state.modal.title || '',
modalData: state => state.modal.data || {},
}),
...mapGetters([
'summaryStatus',
]),
groupedSummaryText() {
if (this.isLoading) {
return s__('Reports|Test summary results are being parsed');
}
if (this.hasError) {
return s__('Reports|Test summary failed loading results');
}
return summaryTextBuilder(s__('Reports|Test summary'), this.summary);
},
},
created() {
this.setEndpoint(this.endpoint);
this.fetchReports();
},
methods: {
...mapActions(['setEndpoint', 'fetchReports']),
reportText(report) {
const summary = report.summary || {};
return reportTextBuilder(report.name, summary);
},
getReportIcon(report) {
return statusIcon(report.status);
},
shouldRenderIssuesList(report) {
return (
report.existing_failures.length > 0 ||
report.new_failures.length > 0 ||
report.resolved_failures > 0
);
},
},
};
</script>
<template>
<report-section
:status="summaryStatus"
:success-text="groupedSummaryText"
:loading-text="groupedSummaryText"
:error-text="groupedSummaryText"
:has-issues="reports.length > 0"
class="mr-widget-border-top grouped-security-reports"
>
<div
slot="body"
class="mr-widget-grouped-section report-block"
>
<template
v-for="(report, i) in reports"
>
<summary-row
:summary="reportText(report)"
:status-icon="getReportIcon(report)"
:key="`summary-row-${i}`"
/>
<issues-list
v-if="shouldRenderIssuesList(report)"
:unresolved-issues="report.existing_failures"
:new-issues="report.new_failures"
:resolved-issues="report.resolved_failures"
:key="`issues-list-${i}`"
:component="$options.componentNames.TestIssueBody"
class="report-block-group-list"
/>
</template>
<modal
:title="modalTitle"
:modal-data="modalData"
/>
</div>
</report-section>
</template>
<script>
import Modal from '~/vue_shared/components/gl_modal.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import CodeBlock from '~/vue_shared/components/code_block.vue';
import { fieldTypes } from '../constants';
export default {
components: {
Modal,
LoadingButton,
CodeBlock,
},
props: {
title: {
type: String,
required: true,
},
modalData: {
type: Object,
required: true,
},
},
fieldTypes,
};
</script>
<template>
<modal
id="modal-mrwidget-reports"
:header-title-text="title"
class="modal-security-report-dast modal-hide-footer"
>
<slot>
<div
v-for="(field, key, index) in modalData"
v-if="field.value"
:key="index"
class="row prepend-top-10 append-bottom-10"
>
<strong class="col-sm-2 text-right">
{{ field.text }}:
</strong>
<div class="col-sm-10 text-secondary">
<code-block
v-if="field.type === $options.fieldTypes.codeBock"
:code="field.value"
/>
<template v-else-if="field.type === $options.fieldTypes.link">
<a
:href="field.value"
target="_blank"
rel="noopener noreferrer"
class="js-modal-link"
>
{{ field.value }}
</a>
</template>
<template v-else-if="field.type === $options.fieldTypes.miliseconds">
{{ field.value }} ms
</template>
<template v-else-if="field.type === $options.fieldTypes.text">
{{ field.value }}
</template>
</div>
</div>
</slot>
<div slot="footer">
</div>
</modal>
</template>
<script>
import { mapActions } from 'vuex';
export default {
name: 'TestIssueBody',
props: {
issue: {
type: Object,
required: true,
},
// failed || success
status: {
type: String,
required: true,
},
isNew: {
type: Boolean,
required: false,
default: false,
},
},
methods: {
...mapActions(['openModal']),
},
};
</script>
<template>
<div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
<div class="report-block-list-issue-description-text">
<button
type="button"
class="btn-link btn-blank text-left break-link vulnerability-name-button"
@click="openModal({ issue })"
>
<div
v-if="isNew"
class="badge badge-danger append-right-5"
>
{{ s__('New') }}
</div>{{ issue.name }}
</button>
</div>
</div>
</template>
export const fieldTypes = {
codeBock: 'codeBlock',
link: 'link',
miliseconds: 'miliseconds',
text: 'text',
};
export const LOADING = 'LOADING';
export const ERROR = 'ERROR';
export const SUCCESS = 'SUCCESS';
export const STATUS_FAILED = 'failed';
export const STATUS_SUCCESS = 'success';
export const ICON_WARNING = 'warning';
export const ICON_SUCCESS = 'success';
export const ICON_NOTFOUND = 'notfound';
import Visibility from 'visibilityjs';
import $ from 'jquery';
import axios from '../../lib/utils/axios_utils';
import Poll from '../../lib/utils/poll';
import * as types from './mutation_types';
import httpStatusCodes from '../../lib/utils/http_status';
export const setEndpoint = ({ commit }, endpoint) => commit(types.SET_ENDPOINT, endpoint);
......@@ -41,12 +43,19 @@ export const fetchReports = ({ state, dispatch }) => {
},
data: state.endpoint,
method: 'getReports',
successCallback: ({ data }) => dispatch('receiveReportsSuccess', data),
successCallback: ({ data, status }) => dispatch('receiveReportsSuccess', {
data, status,
}),
errorCallback: () => dispatch('receiveReportsError'),
});
if (!Visibility.hidden()) {
eTagPoll.makeRequest();
} else {
axios
.get(state.endpoint)
.then(({ data, status }) => dispatch('receiveReportsSuccess', { data, status }))
.catch(() => dispatch('receiveReportsError'));
}
Visibility.change(() => {
......@@ -58,10 +67,22 @@ export const fetchReports = ({ state, dispatch }) => {
});
};
export const receiveReportsSuccess = ({ commit }, response) =>
commit(types.RECEIVE_REPORTS_SUCCESS, response);
export const receiveReportsSuccess = ({ commit }, response) => {
// With 204 we keep polling and don't update the state
if (response.status === httpStatusCodes.OK) {
commit(types.RECEIVE_REPORTS_SUCCESS, response.data);
}
};
export const receiveReportsError = ({ commit }) => commit(types.RECEIVE_REPORTS_ERROR);
export const openModal = ({ dispatch }, payload) => {
dispatch('setModalData', payload);
$('#modal-mrwidget-reports').modal('show');
};
export const setModalData = ({ commit }, payload) => commit(types.SET_ISSUE_MODAL_DATA, payload);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import { LOADING, ERROR, SUCCESS, STATUS_FAILED } from '../constants';
export const summaryStatus = state => {
if (state.isLoading) {
return LOADING;
}
if (state.hasError || state.status === STATUS_FAILED) {
return ERROR;
}
return SUCCESS;
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';
......@@ -9,5 +10,6 @@ Vue.use(Vuex);
export default () => new Vuex.Store({
actions,
mutations,
getters,
state: state(),
});
......@@ -3,3 +3,5 @@ export const SET_ENDPOINT = 'SET_ENDPOINT';
export const REQUEST_REPORTS = 'REQUEST_REPORTS';
export const RECEIVE_REPORTS_SUCCESS = 'RECEIVE_REPORTS_SUCCESS';
export const RECEIVE_REPORTS_ERROR = 'RECEIVE_REPORTS_ERROR';
export const SET_ISSUE_MODAL_DATA = 'SET_ISSUE_MODAL_DATA';
......@@ -16,11 +16,32 @@ export default {
state.summary.resolved = response.summary.resolved;
state.summary.failed = response.summary.failed;
state.status = response.status;
state.reports = response.suites;
},
[types.RECEIVE_REPORTS_ERROR](state) {
state.isLoading = false;
state.hasError = true;
state.reports = [];
state.summary = {
total: 0,
resolved: 0,
failed: 0,
};
state.status = null;
},
[types.SET_ISSUE_MODAL_DATA](state, payload) {
state.modal.title = payload.issue.name;
Object.keys(payload.issue).forEach((key) => {
if (Object.prototype.hasOwnProperty.call(state.modal.data, key)) {
state.modal.data[key] = {
...state.modal.data[key],
value: payload.issue[key],
};
}
});
},
};
import { s__ } from '~/locale';
import { fieldTypes } from '../constants';
export default () => ({
endpoint: null,
isLoading: false,
hasError: false,
status: null,
summary: {
total: 0,
resolved: 0,
......@@ -25,4 +30,32 @@ export default () => ({
* }
*/
reports: [],
modal: {
title: null,
data: {
class: {
value: null,
text: s__('Reports|Class'),
type: fieldTypes.link,
},
execution_time: {
value: null,
text: s__('Reports|Execution time'),
type: fieldTypes.miliseconds,
},
failure: {
value: null,
text: s__('Reports|Failure'),
type: fieldTypes.codeBock,
},
system_output: {
value: null,
text: s__('Reports|System output'),
type: fieldTypes.codeBock,
},
},
},
});
import { sprintf, n__, s__ } from '~/locale';
import {
STATUS_FAILED,
STATUS_SUCCESS,
ICON_WARNING,
ICON_SUCCESS,
ICON_NOTFOUND,
} from '../constants';
const textBuilder = results => {
const { failed, resolved, total } = results;
const failedString = failed
? n__('%d failed test result', '%d failed test results', failed)
: null;
const resolvedString = resolved
? n__('%d fixed test result', '%d fixed test results', resolved)
: null;
const totalString = total ? n__('out of %d total test', 'out of %d total tests', total) : null;
let resultsString = s__('Reports|no changed test results');
if (failed) {
if (resolved) {
resultsString = sprintf(s__('Reports|%{failedString} and %{resolvedString}'), {
failedString,
resolvedString,
});
} else {
resultsString = failedString;
}
} else if (resolved) {
resultsString = resolvedString;
}
return `${resultsString} ${totalString}`;
};
export const summaryTextBuilder = (name = '', results = {}) => {
const resultsString = textBuilder(results);
return `${name} contained ${resultsString}`;
};
export const reportTextBuilder = (name = '', results = {}) => {
const resultsString = textBuilder(results);
return `${name} found ${resultsString}`;
};
export const statusIcon = status => {
if (status === STATUS_FAILED) {
return ICON_WARNING;
}
if (status === STATUS_SUCCESS) {
return ICON_SUCCESS;
}
return ICON_NOTFOUND;
};
......@@ -36,6 +36,7 @@ import {
notify,
SourceBranchRemovalStatus,
} from './dependencies';
import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue';
import { setFaviconOverlay } from '../lib/utils/common_utils';
export default {
......@@ -68,6 +69,7 @@ export default {
'mr-widget-auto-merge-failed': AutoMergeFailed,
'mr-widget-rebase': RebaseState,
SourceBranchRemovalStatus,
GroupedTestReportsApp,
},
props: {
mrData: {
......@@ -260,6 +262,10 @@ export default {
:deployment="deployment"
/>
<div class="mr-section-container">
<grouped-test-reports-app
v-if="mr.testResultsPath"
:endpoint="mr.testResultsPath"
/>
<div class="mr-widget-section">
<component
:is="componentName"
......
......@@ -108,6 +108,8 @@ export default class MergeRequestStore {
this.isPipelineBlocked = pipelineStatus ? pipelineStatus.group === 'manual' : false;
this.ciStatusFaviconPath = pipelineStatus ? pipelineStatus.favicon : null;
this.testResultsPath = data.test_reports_path;
this.setState(data);
}
......
<script>
export default {
name: 'CodeBlock',
props: {
code: {
type: String,
required: true,
},
},
};
</script>
<template>
<pre class="code-block rounded">
<code class="d-block">{{ code }}</code>
</pre>
</template>
export const components = {};
import TestIssueBody from '~/reports/components/test_issue_body.vue';
export const componentNames = {};
export const components = {
TestIssueBody,
};
export const componentNames = {
TestIssueBody: TestIssueBody.name,
};
......@@ -18,6 +18,11 @@ export default {
failed: STATUS_FAILED,
neutral: STATUS_NEUTRAL,
props: {
newIssues: {
type: Array,
required: false,
default: () => [],
},
unresolvedIssues: {
type: Array,
required: false,
......@@ -44,6 +49,15 @@ export default {
<template>
<div class="report-block-container">
<issues-block
v-if="newIssues.length"
:component="component"
:issues="newIssues"
class="js-mr-code-new-issues"
status="failed"
is-new
/>
<issues-block
v-if="unresolvedIssues.length"
:component="component"
......
......@@ -24,6 +24,11 @@ export default {
type: String,
required: true,
},
isNew: {
type: Boolean,
required: false,
default: false,
},
},
};
</script>
......@@ -46,6 +51,7 @@ export default {
:is="component"
:issue="issue"
:status="issue.status || status"
:is-new="isNew"
/>
</li>
</ul>
......
......@@ -29,7 +29,8 @@ export default {
},
popoverOptions: {
type: Object,
required: true,
required: false,
default: null,
},
},
computed: {
......@@ -60,7 +61,11 @@ export default {
{{ summary }}
</div>
<popover :options="popoverOptions" />
<popover
v-if="popoverOptions"
:options="popoverOptions"
/>
</div>
</div>
</template>
......@@ -353,3 +353,18 @@
.flex-right {
margin-left: auto;
}
.code-block {
background: $black;
color: $gray-darkest;
white-space: pre;
overflow-x: auto;
font-size: 12px;
border: 0;
padding: $grid-size;
code {
background-color: inherit;
padding: inherit;
}
}
......@@ -469,3 +469,4 @@ img.emoji {
.inline { display: inline-block; }
.center { text-align: center; }
.vertical-align-middle { vertical-align: middle; }
.flex-align-self-center { align-self: center; }
......@@ -15,6 +15,39 @@
}
}
.mr-widget-border-top {
border-top: 1px solid $border-color;
}
.media-section {
@include media-breakpoint-down(md) {
align-items: flex-start;
.media-body {
flex-direction: column;
align-items: flex-start;
}
}
.code-text {
@include media-breakpoint-up(lg) {
align-self: center;
flex: 1;
}
}
}
.mr-widget-section {
.media {
align-items: center;
}
.code-text {
flex: 1;
}
}
.mr-widget-heading {
position: relative;
border: 1px solid $border-color;
......@@ -54,6 +87,14 @@
padding: 0;
}
.grouped-security-reports {
padding: 0;
> .media {
padding: $gl-padding;
}
}
form {
margin-bottom: 0;
......
---
title: Adds frontend support to render test reports on the MR widget
merge_request: 20936
author:
type: added
......@@ -36,6 +36,16 @@ msgid_plural "%d exporters"
msgstr[0] ""
msgstr[1] ""
msgid "%d failed test result"
msgid_plural "%d failed test results"
msgstr[0] ""
msgstr[1] ""
msgid "%d fixed test result"
msgid_plural "%d fixed test results"
msgstr[0] ""
msgstr[1] ""
msgid "%d issue"
msgid_plural "%d issues"
msgstr[0] ""
......@@ -4390,6 +4400,33 @@ msgstr ""
msgid "Reply to this email directly or %{view_it_on_gitlab}."
msgstr ""
msgid "Reports|%{failedString} and %{resolvedString}"
msgstr ""
msgid "Reports|Class"
msgstr ""
msgid "Reports|Execution time"
msgstr ""
msgid "Reports|Failure"
msgstr ""
msgid "Reports|System output"
msgstr ""
msgid "Reports|Test summary"
msgstr ""
msgid "Reports|Test summary failed loading results"
msgstr ""
msgid "Reports|Test summary results are being parsed"
msgstr ""
msgid "Reports|no changed test results"
msgstr ""
msgid "Repository"
msgstr ""
......@@ -6273,6 +6310,11 @@ msgstr ""
msgid "or"
msgstr ""
msgid "out of %d total test"
msgid_plural "out of %d total tests"
msgstr[0] ""
msgstr[1] ""
msgid "parent"
msgid_plural "parents"
msgstr[0] ""
......
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import state from '~/reports/store/state';
import component from '~/reports/components/grouped_test_reports_app.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import newFailedTestReports from '../mock_data/new_failures_report.json';
import successTestReports from '../mock_data/no_failures_report.json';
import mixedResultsTestReports from '../mock_data/new_and_fixed_failures_report.json';
describe('Grouped Test Reports App', () => {
let vm;
let mock;
const Component = Vue.extend(component);
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
vm.$store.replaceState(state());
vm.$destroy();
mock.restore();
});
describe('with success result', () => {
beforeEach(() => {
mock.onGet('test_results.json').reply(200, successTestReports, {});
vm = mountComponent(Component, {
endpoint: 'test_results.json',
});
});
it('renders success summary text', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.fa-spinner')).toBeNull();
expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
'Test summary contained no changed test results out of 11 total tests',
);
expect(vm.$el.textContent).toContain(
'rspec:pg found no changed test results out of 8 total tests',
);
expect(vm.$el.textContent).toContain(
'java ant found no changed test results out of 3 total tests',
);
done();
}, 0);
});
});
describe('with 204 result', () => {
beforeEach(() => {
mock.onGet('test_results.json').reply(204, {}, {});
vm = mountComponent(Component, {
endpoint: 'test_results.json',
});
});
it('renders success summary text', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.fa-spinner')).not.toBeNull();
expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
'Test summary results are being parsed',
);
done();
}, 0);
});
});
describe('with new failed result', () => {
beforeEach(() => {
mock.onGet('test_results.json').reply(200, newFailedTestReports, {});
vm = mountComponent(Component, {
endpoint: 'test_results.json',
});
});
it('renders failed summary text + new badge', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.fa-spinner')).toBeNull();
expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
'Test summary contained 2 failed test results out of 11 total tests',
);
expect(vm.$el.textContent).toContain(
'rspec:pg found 2 failed test results out of 8 total tests',
);
expect(vm.$el.textContent).toContain('New');
expect(vm.$el.textContent).toContain(
'java ant found no changed test results out of 3 total tests',
);
done();
}, 0);
});
});
describe('with mixed results', () => {
beforeEach(() => {
mock.onGet('test_results.json').reply(200, mixedResultsTestReports, {});
vm = mountComponent(Component, {
endpoint: 'test_results.json',
});
});
it('renders summary text', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.fa-spinner')).toBeNull();
expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
'Test summary contained 2 failed test results and 2 fixed test results out of 11 total tests',
);
expect(vm.$el.textContent).toContain(
'rspec:pg found 1 failed test result and 2 fixed test results out of 8 total tests',
);
expect(vm.$el.textContent).toContain('New');
expect(vm.$el.textContent).toContain(
' java ant found 1 failed test result out of 3 total tests',
);
done();
}, 0);
});
});
describe('with error', () => {
beforeEach(() => {
mock.onGet('test_results.json').reply(500, {}, {});
vm = mountComponent(Component, {
endpoint: 'test_results.json',
});
});
it('renders loading summary text with loading icon', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
'Test summary failed loading results',
);
done();
}, 0);
});
});
describe('while loading', () => {
beforeEach(() => {
mock.onGet('test_results.json').reply(200, {}, {});
vm = mountComponent(Component, {
endpoint: 'test_results.json',
});
});
it('renders loading summary text with loading icon', done => {
expect(vm.$el.querySelector('.fa-spinner')).not.toBeNull();
expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
'Test summary results are being parsed',
);
setTimeout(() => {
done();
}, 0);
});
});
});
import Vue from 'vue';
import component from '~/reports/components/modal.vue';
import state from '~/reports/store/state';
import mountComponent from '../../helpers/vue_mount_component_helper';
import { trimText } from '../../helpers/vue_component_helper';
describe('Grouped Test Reports Modal', () => {
const Component = Vue.extend(component);
const modalDataStructure = state().modal.data;
// populate data
modalDataStructure.execution_time.value = 0.009411;
modalDataStructure.system_output.value = 'Failure/Error: is_expected.to eq(3)\n\n';
modalDataStructure.class.value = 'link';
let vm;
beforeEach(() => {
vm = mountComponent(Component, {
title: 'Test#sum when a is 1 and b is 2 returns summary',
modalData: modalDataStructure,
});
});
afterEach(() => {
vm.$destroy();
});
it('renders code block', () => {
expect(vm.$el.querySelector('code').textContent).toEqual(modalDataStructure.system_output.value);
});
it('renders link', () => {
expect(vm.$el.querySelector('.js-modal-link').getAttribute('href')).toEqual(modalDataStructure.class.value);
expect(trimText(vm.$el.querySelector('.js-modal-link').textContent)).toEqual(modalDataStructure.class.value);
});
it('renders miliseconds', () => {
expect(vm.$el.textContent).toContain(`${modalDataStructure.execution_time.value} ms`);
});
it('render title', () => {
expect(trimText(vm.$el.querySelector('.modal-title').textContent)).toEqual('Test#sum when a is 1 and b is 2 returns summary');
});
});
import Vue from 'vue';
import component from '~/reports/components/test_issue_body.vue';
import createStore from '~/reports/store';
import { mountComponentWithStore } from '../../helpers/vue_mount_component_helper';
import { trimText } from '../../helpers/vue_component_helper';
import { issue } from '../mock_data/mock_data';
describe('Test Issue body', () => {
let vm;
const Component = Vue.extend(component);
const store = createStore();
const commonProps = {
issue,
status: 'failed',
};
afterEach(() => {
vm.$destroy();
});
describe('on click', () => {
it('calls openModal action', () => {
vm = mountComponentWithStore(Component, {
store,
props: commonProps,
});
spyOn(vm, 'openModal');
vm.$el.querySelector('button').click();
expect(vm.openModal).toHaveBeenCalledWith({
issue: commonProps.issue,
});
});
});
describe('is new', () => {
beforeEach(() => {
vm = mountComponentWithStore(Component, {
store,
props: Object.assign({}, commonProps, { isNew: true }),
});
});
it('renders issue name', () => {
expect(vm.$el.textContent).toContain(commonProps.issue.name);
});
it('renders new badge', () => {
expect(trimText(vm.$el.querySelector('.badge').textContent)).toEqual('New');
});
});
describe('not new', () => {
beforeEach(() => {
vm = mountComponentWithStore(Component, {
store,
props: commonProps,
});
});
it('renders issue name', () => {
expect(vm.$el.textContent).toContain(commonProps.issue.name);
});
it('does not renders new badge', () => {
expect(vm.$el.querySelector('.badge')).toEqual(null);
});
});
});
// eslint-disable-next-line import/prefer-default-export
export const issue = {
result: 'failure',
name: 'Test#sum when a is 1 and b is 2 returns summary',
execution_time: 0.009411,
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 \u003ctop (required)\u003e'",
};
{"status":"failed","summary":{"total":11,"resolved":2,"failed":2},"suites":[{"name":"rspec:pg","status":"failed","summary":{"total":8,"resolved":2,"failed":1},"new_failures":[{"status":"failed","name":"Test#subtract when a is 2 and b is 1 returns correct result","execution_time":0.00908,"system_output":"Failure/Error: is_expected.to eq(1)\n\n expected: 1\n got: 3\n\n (compared using ==)\n./spec/test_spec.rb:43:in `block (4 levels) in <top (required)>'"}],"resolved_failures":[{"status":"success","name":"Test#sum when a is 1 and b is 2 returns summary","execution_time":0.000318,"system_output":null},{"status":"success","name":"Test#sum when a is 100 and b is 200 returns summary","execution_time":0.000074,"system_output":null}],"existing_failures":[]},{"name":"java ant","status":"failed","summary":{"total":3,"resolved":0,"failed":1},"new_failures":[],"resolved_failures":[],"existing_failures":[{"status":"failed","name":"sumTest","execution_time":0.004,"system_output":"junit.framework.AssertionFailedError: expected:<3> but was:<-1>\n\tat CalculatorTest.sumTest(Unknown Source)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\n\tat java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n"}]}]}
\ No newline at end of file
{"summary":{"total":11,"resolved":0,"failed":2},"suites":[{"name":"rspec:pg","summary":{"total":8,"resolved":0,"failed":2},"new_failures":[{"result":"failure","name":"Test#sum when a is 1 and b is 2 returns summary","execution_time":0.009411,"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)>'"},{"result":"failure","name":"Test#sum when a is 100 and b is 200 returns summary","execution_time":0.000162,"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)>'"}],"resolved_failures":[],"existing_failures":[]},{"name":"java ant","summary":{"total":3,"resolved":0,"failed":0},"new_failures":[],"resolved_failures":[],"existing_failures":[]}]}
\ No newline at end of file
{"status":"success","summary":{"total":11,"resolved":0,"failed":0},"suites":[{"name":"rspec:pg","status":"success","summary":{"total":8,"resolved":0,"failed":0},"new_failures":[],"resolved_failures":[],"existing_failures":[]},{"name":"java ant","status":"success","summary":{"total":3,"resolved":0,"failed":0},"new_failures":[],"resolved_failures":[],"existing_failures":[]}]}
\ No newline at end of file
......@@ -8,6 +8,8 @@ import {
clearEtagPoll,
receiveReportsSuccess,
receiveReportsError,
openModal,
setModalData,
} from '~/reports/store/actions';
import state from '~/reports/store/state';
import * as types from '~/reports/store/mutation_types';
......@@ -56,7 +58,9 @@ describe('Reports Store Actions', () => {
describe('success', () => {
it('dispatches requestReports and receiveReportsSuccess ', done => {
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, { summary: {}, suites: [{ name: 'rspec' }] });
mock
.onGet(`${TEST_HOST}/endpoint.json`)
.replyOnce(200, { summary: {}, suites: [{ name: 'rspec' }] });
testAction(
fetchReports,
......@@ -68,7 +72,7 @@ describe('Reports Store Actions', () => {
type: 'requestReports',
},
{
payload: { summary: {}, suites: [{ name: 'rspec' }] },
payload: { data: { summary: {}, suites: [{ name: 'rspec' }] }, status: 200 },
type: 'receiveReportsSuccess',
},
],
......@@ -103,16 +107,27 @@ describe('Reports Store Actions', () => {
});
describe('receiveReportsSuccess', () => {
it('should commit RECEIVE_REPORTS_SUCCESS mutation', done => {
it('should commit RECEIVE_REPORTS_SUCCESS mutation with 200', done => {
testAction(
receiveReportsSuccess,
{ summary: {} },
{ data: { summary: {} }, status: 200 },
mockedState,
[{ type: types.RECEIVE_REPORTS_SUCCESS, payload: { summary: {} } }],
[],
done,
);
});
it('should not commit RECEIVE_REPORTS_SUCCESS mutation with 204', done => {
testAction(
receiveReportsSuccess,
{ data: { summary: {} }, status: 204 },
mockedState,
[],
[],
done,
);
});
});
describe('receiveReportsError', () => {
......@@ -127,4 +142,30 @@ describe('Reports Store Actions', () => {
);
});
});
describe('openModal', () => {
it('should dispatch setModalData', done => {
testAction(
openModal,
{ name: 'foo' },
mockedState,
[],
[{ type: 'setModalData', payload: { name: 'foo' } }],
done,
);
});
});
describe('setModalData', () => {
it('should commit SET_ISSUE_MODAL_DATA', done => {
testAction(
setModalData,
{ name: 'foo' },
mockedState,
[{ type: types.SET_ISSUE_MODAL_DATA, payload: { name: 'foo' } }],
[],
done,
);
});
});
});
import state from '~/reports/store/state';
import mutations from '~/reports/store/mutations';
import * as types from '~/reports/store/mutation_types';
import { issue } from '../mock_data/mock_data';
describe('Reports Store Mutations', () => {
let stateCopy;
......@@ -42,24 +43,21 @@ describe('Reports Store Mutations', () => {
{
name: 'StringHelper#concatenate when a is git and b is lab returns summary',
execution_time: 0.0092435,
system_output:
'Failure/Error: is_expected.to eq(\'gitlab\')',
system_output: "Failure/Error: is_expected.to eq('gitlab')",
},
],
resolved_failures: [
{
name: 'StringHelper#concatenate when a is git and b is lab returns summary',
execution_time: 0.009235,
system_output:
'Failure/Error: is_expected.to eq(\'gitlab\')',
system_output: "Failure/Error: is_expected.to eq('gitlab')",
},
],
existing_failures: [
{
name: 'StringHelper#concatenate when a is git and b is lab returns summary',
execution_time: 1232.08,
system_output:
'Failure/Error: is_expected.to eq(\'gitlab\')',
system_output: "Failure/Error: is_expected.to eq('gitlab')",
},
],
},
......@@ -89,6 +87,7 @@ describe('Reports Store Mutations', () => {
beforeEach(() => {
mutations[types.RECEIVE_REPORTS_ERROR](stateCopy);
});
it('should reset isLoading', () => {
expect(stateCopy.isLoading).toEqual(false);
});
......@@ -97,5 +96,25 @@ describe('Reports Store Mutations', () => {
expect(stateCopy.hasError).toEqual(true);
});
it('should reset reports', () => {
expect(stateCopy.reports).toEqual([]);
});
});
describe('SET_ISSUE_MODAL_DATA', () => {
beforeEach(() => {
mutations[types.SET_ISSUE_MODAL_DATA](stateCopy, {
issue,
});
});
it('should set modal title', () => {
expect(stateCopy.modal.title).toEqual(issue.name);
});
it('should set modal data', () => {
expect(stateCopy.modal.data.execution_time.value).toEqual(issue.execution_time);
expect(stateCopy.modal.data.system_output.value).toEqual(issue.system_output);
});
});
});
import * as utils from '~/reports/store/utils';
import {
STATUS_FAILED,
STATUS_SUCCESS,
ICON_WARNING,
ICON_SUCCESS,
ICON_NOTFOUND,
} from '~/reports/constants';
describe('Reports store utils', () => {
describe('summaryTextbuilder', () => {
it('should render text for no changed results in multiple tests', () => {
const name = 'Test summary';
const data = { total: 10 };
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe('Test summary contained no changed test results out of 10 total tests');
});
it('should render text for no changed results in one test', () => {
const name = 'Test summary';
const data = { total: 1 };
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe('Test summary contained no changed test results out of 1 total test');
});
it('should render text for multiple failed results', () => {
const name = 'Test summary';
const data = { failed: 3, total: 10 };
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe('Test summary contained 3 failed test results out of 10 total tests');
});
it('should render text for multiple fixed results', () => {
const name = 'Test summary';
const data = { resolved: 4, total: 10 };
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe('Test summary contained 4 fixed test results out of 10 total tests');
});
it('should render text for multiple fixed, and multiple failed results', () => {
const name = 'Test summary';
const data = { failed: 3, resolved: 4, total: 10 };
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe(
'Test summary contained 3 failed test results and 4 fixed test results out of 10 total tests',
);
});
it('should render text for a singular fixed, and a singular failed result', () => {
const name = 'Test summary';
const data = { failed: 1, resolved: 1, total: 10 };
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe(
'Test summary contained 1 failed test result and 1 fixed test result out of 10 total tests',
);
});
});
describe('reportTextBuilder', () => {
it('should render text for no changed results in multiple tests', () => {
const name = 'Rspec';
const data = { total: 10 };
const result = utils.reportTextBuilder(name, data);
expect(result).toBe('Rspec found no changed test results out of 10 total tests');
});
it('should render text for no changed results in one test', () => {
const name = 'Rspec';
const data = { total: 1 };
const result = utils.reportTextBuilder(name, data);
expect(result).toBe('Rspec found no changed test results out of 1 total test');
});
it('should render text for multiple failed results', () => {
const name = 'Rspec';
const data = { failed: 3, total: 10 };
const result = utils.reportTextBuilder(name, data);
expect(result).toBe('Rspec found 3 failed test results out of 10 total tests');
});
it('should render text for multiple fixed results', () => {
const name = 'Rspec';
const data = { resolved: 4, total: 10 };
const result = utils.reportTextBuilder(name, data);
expect(result).toBe('Rspec found 4 fixed test results out of 10 total tests');
});
it('should render text for multiple fixed, and multiple failed results', () => {
const name = 'Rspec';
const data = { failed: 3, resolved: 4, total: 10 };
const result = utils.reportTextBuilder(name, data);
expect(result).toBe(
'Rspec found 3 failed test results and 4 fixed test results out of 10 total tests',
);
});
it('should render text for a singular fixed, and a singular failed result', () => {
const name = 'Rspec';
const data = { failed: 1, resolved: 1, total: 10 };
const result = utils.reportTextBuilder(name, data);
expect(result).toBe(
'Rspec found 1 failed test result and 1 fixed test result out of 10 total tests',
);
});
});
describe('statusIcon', () => {
describe('with failed status', () => {
it('returns ICON_WARNING', () => {
expect(utils.statusIcon(STATUS_FAILED)).toEqual(ICON_WARNING);
});
});
describe('with success status', () => {
it('returns ICON_SUCCESS', () => {
expect(utils.statusIcon(STATUS_SUCCESS)).toEqual(ICON_SUCCESS);
});
});
describe('without a status', () => {
it('returns ICON_NOTFOUND', () => {
expect(utils.statusIcon()).toEqual(ICON_NOTFOUND);
});
});
});
});
import Vue from 'vue';
import component from '~/vue_shared/components/code_block.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Code Block', () => {
const Component = Vue.extend(component);
let vm;
afterEach(() => {
vm.$destroy();
});
it('renders a code block with the provided code', () => {
const code =
"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 \u003ctop (required)\u003e'";
vm = mountComponent(Component, {
code,
});
expect(vm.$el.querySelector('code').textContent).toEqual(code);
});
it('escapes XSS injections', () => {
const code = 'CCC&lt;img src=x onerror=alert(document.domain)&gt;';
vm = mountComponent(Component, {
code,
});
expect(vm.$el.querySelector('code').textContent).toEqual(code);
});
});
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