Commit bfeb7c2c authored by Filipa Lacerda's avatar Filipa Lacerda

Adds frontend support to render test reports on theMR widget

Creates an app to render grouped test reports in the MR widget
Ports CSS from EE into CE
Creates a reusable code component
Adds getters and utils to the existing reports store
parent 28c15d4f
<script>
import { mapActions, mapGetters } 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: {
...mapGetters([
'reports',
'summaryStatus',
'isLoading',
'hasError',
'summaryCounts',
'modalTitle',
'modalData',
'isCreatingNewIssue',
]),
groupedSummaryText() {
if (this.isLoading) {
return s__('Reports|Test summary results are being parsed');
}
if (this.hasError || !this.summaryCounts) {
return s__('Reports|Test summary failed loading results');
}
return summaryTextBuilder(s__('Reports|Test summary'), this.summaryCounts);
},
},
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"
>
<label class="col-sm-2 text-right font-weight-bold">
{{ field.text }}:
</label>
<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']),
handleIssueClick() {
const { issue, status, openModal } = this;
openModal({ issue, status });
},
},
};
</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="handleIssueClick()"
>
<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 Visibility from 'visibilityjs';
import $ from 'jquery';
import axios from '../../lib/utils/axios_utils'; import axios from '../../lib/utils/axios_utils';
import Poll from '../../lib/utils/poll'; import Poll from '../../lib/utils/poll';
import * as types from './mutation_types'; import * as types from './mutation_types';
...@@ -63,5 +64,13 @@ export const receiveReportsSuccess = ({ commit }, response) => ...@@ -63,5 +64,13 @@ export const receiveReportsSuccess = ({ commit }, response) =>
export const receiveReportsError = ({ commit }) => commit(types.RECEIVE_REPORTS_ERROR); 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 // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
import { LOADING, ERROR, SUCCESS } from '../constants';
export const reports = state => state.reports;
export const summaryCounts = state => state.summary;
export const isLoading = state => state.isLoading;
export const hasError = state => state.hasError;
export const modalTitle = state => state.modal.title || '';
export const modalData = state => state.modal.data || {};
export const isCreatingNewIssue = state => state.modal.isLoading;
export const summaryStatus = state => {
if (state.isLoading) {
return LOADING;
}
if (state.hasError) {
return ERROR;
}
return SUCCESS;
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import * as actions from './actions'; import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations'; import mutations from './mutations';
import state from './state'; import state from './state';
...@@ -9,5 +10,6 @@ Vue.use(Vuex); ...@@ -9,5 +10,6 @@ Vue.use(Vuex);
export default () => new Vuex.Store({ export default () => new Vuex.Store({
actions, actions,
mutations, mutations,
getters,
state: state(), state: state(),
}); });
...@@ -3,3 +3,5 @@ export const SET_ENDPOINT = 'SET_ENDPOINT'; ...@@ -3,3 +3,5 @@ export const SET_ENDPOINT = 'SET_ENDPOINT';
export const REQUEST_REPORTS = 'REQUEST_REPORTS'; export const REQUEST_REPORTS = 'REQUEST_REPORTS';
export const RECEIVE_REPORTS_SUCCESS = 'RECEIVE_REPORTS_SUCCESS'; export const RECEIVE_REPORTS_SUCCESS = 'RECEIVE_REPORTS_SUCCESS';
export const RECEIVE_REPORTS_ERROR = 'RECEIVE_REPORTS_ERROR'; export const RECEIVE_REPORTS_ERROR = 'RECEIVE_REPORTS_ERROR';
export const SET_ISSUE_MODAL_DATA = 'SET_ISSUE_MODAL_DATA';
...@@ -16,11 +16,26 @@ export default { ...@@ -16,11 +16,26 @@ export default {
state.summary.resolved = response.summary.resolved; state.summary.resolved = response.summary.resolved;
state.summary.failed = response.summary.failed; state.summary.failed = response.summary.failed;
state.status = response.status;
state.reports = response.suites; state.reports = response.suites;
}, },
[types.RECEIVE_REPORTS_ERROR](state) { [types.RECEIVE_REPORTS_ERROR](state) {
state.isLoading = false; state.isLoading = false;
state.hasError = true; state.hasError = true;
state.reports = [];
},
[types.SET_ISSUE_MODAL_DATA](state, payload) {
state.modal.title = payload.issue.name;
state.modal.status = payload.status;
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 () => ({ export default () => ({
endpoint: null, endpoint: null,
isLoading: false, isLoading: false,
hasError: false, hasError: false,
status: null,
summary: { summary: {
total: 0, total: 0,
resolved: 0, resolved: 0,
...@@ -25,4 +30,34 @@ export default () => ({ ...@@ -25,4 +30,34 @@ export default () => ({
* } * }
*/ */
reports: [], reports: [],
modal: {
title: null,
status: 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;
if (failed) {
if (resolved) {
resultsString = sprintf(s__('Reports|%{failedString} and %{resolvedString}'), {
failedString,
resolvedString,
});
} else {
resultsString = failedString;
}
} else if (resolved) {
resultsString = resolvedString;
} else {
resultsString = s__('Reports|no changed test results');
}
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 { ...@@ -36,6 +36,7 @@ import {
notify, notify,
SourceBranchRemovalStatus, SourceBranchRemovalStatus,
} from './dependencies'; } from './dependencies';
import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue';
import { setFaviconOverlay } from '../lib/utils/common_utils'; import { setFaviconOverlay } from '../lib/utils/common_utils';
export default { export default {
...@@ -68,6 +69,7 @@ export default { ...@@ -68,6 +69,7 @@ export default {
'mr-widget-auto-merge-failed': AutoMergeFailed, 'mr-widget-auto-merge-failed': AutoMergeFailed,
'mr-widget-rebase': RebaseState, 'mr-widget-rebase': RebaseState,
SourceBranchRemovalStatus, SourceBranchRemovalStatus,
GroupedTestReportsApp,
}, },
props: { props: {
mrData: { mrData: {
...@@ -260,6 +262,10 @@ export default { ...@@ -260,6 +262,10 @@ export default {
:deployment="deployment" :deployment="deployment"
/> />
<div class="mr-section-container"> <div class="mr-section-container">
<grouped-test-reports-app
v-if="mr.testResultsPath"
:endpoint="mr.testResultsPath"
/>
<div class="mr-widget-section"> <div class="mr-widget-section">
<component <component
:is="componentName" :is="componentName"
......
...@@ -108,6 +108,8 @@ export default class MergeRequestStore { ...@@ -108,6 +108,8 @@ export default class MergeRequestStore {
this.isPipelineBlocked = pipelineStatus ? pipelineStatus.group === 'manual' : false; this.isPipelineBlocked = pipelineStatus ? pipelineStatus.group === 'manual' : false;
this.ciStatusFaviconPath = pipelineStatus ? pipelineStatus.favicon : null; this.ciStatusFaviconPath = pipelineStatus ? pipelineStatus.favicon : null;
this.testResultsPath = data.test_reports_path;
this.setState(data); this.setState(data);
} }
......
<script>
export default {
name: 'CodeBlock',
props: {
code: {
type: String,
required: true,
},
},
};
</script>
<template>
<pre class="code-block build-trace-rounded">
<code class="bash">{{ 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 { ...@@ -18,6 +18,11 @@ export default {
failed: STATUS_FAILED, failed: STATUS_FAILED,
neutral: STATUS_NEUTRAL, neutral: STATUS_NEUTRAL,
props: { props: {
newIssues: {
type: Array,
required: false,
default: () => [],
},
unresolvedIssues: { unresolvedIssues: {
type: Array, type: Array,
required: false, required: false,
...@@ -44,6 +49,15 @@ export default { ...@@ -44,6 +49,15 @@ export default {
<template> <template>
<div class="report-block-container"> <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 <issues-block
v-if="unresolvedIssues.length" v-if="unresolvedIssues.length"
:component="component" :component="component"
......
...@@ -24,6 +24,11 @@ export default { ...@@ -24,6 +24,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
isNew: {
type: Boolean,
required: false,
default: false,
},
}, },
}; };
</script> </script>
...@@ -46,6 +51,7 @@ export default { ...@@ -46,6 +51,7 @@ export default {
:is="component" :is="component"
:issue="issue" :issue="issue"
:status="issue.status || status" :status="issue.status || status"
:is-new="isNew"
/> />
</li> </li>
</ul> </ul>
......
...@@ -29,7 +29,8 @@ export default { ...@@ -29,7 +29,8 @@ export default {
}, },
popoverOptions: { popoverOptions: {
type: Object, type: Object,
required: true, required: false,
default: null,
}, },
}, },
computed: { computed: {
...@@ -60,7 +61,11 @@ export default { ...@@ -60,7 +61,11 @@ export default {
{{ summary }} {{ summary }}
</div> </div>
<popover :options="popoverOptions" /> <popover
v-if="popoverOptions"
:options="popoverOptions"
/>
</div> </div>
</div> </div>
</template> </template>
...@@ -353,3 +353,27 @@ ...@@ -353,3 +353,27 @@
.flex-right { .flex-right {
margin-left: auto; margin-left: auto;
} }
.code-block {
background: $black;
color: $gray-darkest;
white-space: pre;
overflow-x: auto;
font-size: 12px;
border-radius: 0;
border: 0;
padding: $grid-size;
code {
background-color: inherit;
padding: inherit;
}
.bash {
display: block;
}
&.build-trace-rounded {
border-radius: $border-radius-base;
}
}
...@@ -15,6 +15,39 @@ ...@@ -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 { .mr-widget-heading {
position: relative; position: relative;
border: 1px solid $border-color; border: 1px solid $border-color;
...@@ -54,6 +87,14 @@ ...@@ -54,6 +87,14 @@
padding: 0; padding: 0;
} }
.grouped-security-reports {
padding: 0;
> .media {
padding: $gl-padding;
}
}
form { form {
margin-bottom: 0; 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" ...@@ -36,6 +36,16 @@ msgid_plural "%d exporters"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" 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 "%d issue"
msgid_plural "%d issues" msgid_plural "%d issues"
msgstr[0] "" msgstr[0] ""
...@@ -4381,6 +4391,33 @@ msgstr "" ...@@ -4381,6 +4391,33 @@ msgstr ""
msgid "Reply to this email directly or %{view_it_on_gitlab}." msgid "Reply to this email directly or %{view_it_on_gitlab}."
msgstr "" 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" msgid "Repository"
msgstr "" msgstr ""
...@@ -6264,6 +6301,11 @@ msgstr "" ...@@ -6264,6 +6301,11 @@ msgstr ""
msgid "or" msgid "or"
msgstr "" msgstr ""
msgid "out of %d total test"
msgid_plural "out of %d total tests"
msgstr[0] ""
msgstr[1] ""
msgid "parent" msgid "parent"
msgid_plural "parents" msgid_plural "parents"
msgstr[0] "" 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 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,
status: commonProps.status,
});
});
});
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 { ...@@ -8,6 +8,8 @@ import {
clearEtagPoll, clearEtagPoll,
receiveReportsSuccess, receiveReportsSuccess,
receiveReportsError, receiveReportsError,
openModal,
setModalData,
} from '~/reports/store/actions'; } from '~/reports/store/actions';
import state from '~/reports/store/state'; import state from '~/reports/store/state';
import * as types from '~/reports/store/mutation_types'; import * as types from '~/reports/store/mutation_types';
...@@ -127,4 +129,31 @@ describe('Reports Store Actions', () => { ...@@ -127,4 +129,31 @@ 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 state from '~/reports/store/state';
import mutations from '~/reports/store/mutations'; import mutations from '~/reports/store/mutations';
import * as types from '~/reports/store/mutation_types'; import * as types from '~/reports/store/mutation_types';
import { issue } from '../mock_data/mock_data';
describe('Reports Store Mutations', () => { describe('Reports Store Mutations', () => {
let stateCopy; let stateCopy;
...@@ -89,6 +90,7 @@ describe('Reports Store Mutations', () => { ...@@ -89,6 +90,7 @@ describe('Reports Store Mutations', () => {
beforeEach(() => { beforeEach(() => {
mutations[types.RECEIVE_REPORTS_ERROR](stateCopy); mutations[types.RECEIVE_REPORTS_ERROR](stateCopy);
}); });
it('should reset isLoading', () => { it('should reset isLoading', () => {
expect(stateCopy.isLoading).toEqual(false); expect(stateCopy.isLoading).toEqual(false);
}); });
...@@ -97,5 +99,30 @@ describe('Reports Store Mutations', () => { ...@@ -97,5 +99,30 @@ describe('Reports Store Mutations', () => {
expect(stateCopy.hasError).toEqual(true); 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,
status: 'failed',
});
});
it('should set modal title', () => {
expect(stateCopy.modal.title).toEqual(issue.name);
});
it('should set modal status', () => {
expect(stateCopy.modal.status).toEqual('failed');
});
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