summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/reports/components/grouped_test_reports_app.vue114
-rw-r--r--app/assets/javascripts/reports/components/modal.vue73
-rw-r--r--app/assets/javascripts/reports/components/test_issue_body.vue48
-rw-r--r--app/assets/javascripts/reports/constants.js16
-rw-r--r--app/assets/javascripts/reports/store/actions.js9
-rw-r--r--app/assets/javascripts/reports/store/getters.js24
-rw-r--r--app/assets/javascripts/reports/store/index.js2
-rw-r--r--app/assets/javascripts/reports/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/reports/store/mutations.js15
-rw-r--r--app/assets/javascripts/reports/store/state.js35
-rw-r--r--app/assets/javascripts/reports/store/utils.js60
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/code_block.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/reports/issue_body.js10
-rw-r--r--app/assets/javascripts/vue_shared/components/reports/issues_list.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/reports/report_issues.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/reports/summary_row.vue9
-rw-r--r--app/assets/stylesheets/framework/blocks.scss24
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss41
-rw-r--r--changelogs/unreleased/45318-junit-FE.yml5
-rw-r--r--locale/gitlab.pot42
-rw-r--r--spec/javascripts/reports/components/grouped_test_reports_app_spec.js143
-rw-r--r--spec/javascripts/reports/components/modal_spec.js45
-rw-r--r--spec/javascripts/reports/components/test_issue_body_spec.js72
-rw-r--r--spec/javascripts/reports/mock_data/mock_data.js8
-rw-r--r--spec/javascripts/reports/mock_data/new_and_fixed_failures_report.json1
-rw-r--r--spec/javascripts/reports/mock_data/new_failures_report.json1
-rw-r--r--spec/javascripts/reports/mock_data/no_failures_report.json1
-rw-r--r--spec/javascripts/reports/store/actions_spec.js29
-rw-r--r--spec/javascripts/reports/store/mutations_spec.js27
-rw-r--r--spec/javascripts/reports/store/utils_spec.js138
-rw-r--r--spec/javascripts/vue_shared/components/code_block_spec.js33
33 files changed, 1067 insertions, 4 deletions
diff --git a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
new file mode 100644
index 00000000000..d2e72e72421
--- /dev/null
+++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
@@ -0,0 +1,114 @@
+<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>
diff --git a/app/assets/javascripts/reports/components/modal.vue b/app/assets/javascripts/reports/components/modal.vue
new file mode 100644
index 00000000000..6257ebf71fc
--- /dev/null
+++ b/app/assets/javascripts/reports/components/modal.vue
@@ -0,0 +1,73 @@
+<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>
diff --git a/app/assets/javascripts/reports/components/test_issue_body.vue b/app/assets/javascripts/reports/components/test_issue_body.vue
new file mode 100644
index 00000000000..13b7fdf58bc
--- /dev/null
+++ b/app/assets/javascripts/reports/components/test_issue_body.vue
@@ -0,0 +1,48 @@
+<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>
diff --git a/app/assets/javascripts/reports/constants.js b/app/assets/javascripts/reports/constants.js
new file mode 100644
index 00000000000..807ecb1039e
--- /dev/null
+++ b/app/assets/javascripts/reports/constants.js
@@ -0,0 +1,16 @@
+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';
diff --git a/app/assets/javascripts/reports/store/actions.js b/app/assets/javascripts/reports/store/actions.js
index 15c077b0fd8..edbf860ecc6 100644
--- a/app/assets/javascripts/reports/store/actions.js
+++ b/app/assets/javascripts/reports/store/actions.js
@@ -1,4 +1,5 @@
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';
@@ -63,5 +64,13 @@ export const receiveReportsSuccess = ({ commit }, response) =>
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 () => {};
diff --git a/app/assets/javascripts/reports/store/getters.js b/app/assets/javascripts/reports/store/getters.js
new file mode 100644
index 00000000000..5fbafa201b5
--- /dev/null
+++ b/app/assets/javascripts/reports/store/getters.js
@@ -0,0 +1,24 @@
+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 () => {};
diff --git a/app/assets/javascripts/reports/store/index.js b/app/assets/javascripts/reports/store/index.js
index af4f9688fb4..9d8f7dc3b74 100644
--- a/app/assets/javascripts/reports/store/index.js
+++ b/app/assets/javascripts/reports/store/index.js
@@ -1,6 +1,7 @@
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(),
});
diff --git a/app/assets/javascripts/reports/store/mutation_types.js b/app/assets/javascripts/reports/store/mutation_types.js
index 77722974c45..82bda31df5d 100644
--- a/app/assets/javascripts/reports/store/mutation_types.js
+++ b/app/assets/javascripts/reports/store/mutation_types.js
@@ -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';
+
diff --git a/app/assets/javascripts/reports/store/mutations.js b/app/assets/javascripts/reports/store/mutations.js
index d9d301826cf..6b900356bfc 100644
--- a/app/assets/javascripts/reports/store/mutations.js
+++ b/app/assets/javascripts/reports/store/mutations.js
@@ -16,11 +16,26 @@ 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 = [];
+ },
+ [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],
+ };
+ }
+ });
},
};
diff --git a/app/assets/javascripts/reports/store/state.js b/app/assets/javascripts/reports/store/state.js
index 97f9d0a6859..fd1819dbfea 100644
--- a/app/assets/javascripts/reports/store/state.js
+++ b/app/assets/javascripts/reports/store/state.js
@@ -1,9 +1,14 @@
+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,34 @@ export default () => ({
* }
*/
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,
+ },
+ },
+ },
+
});
diff --git a/app/assets/javascripts/reports/store/utils.js b/app/assets/javascripts/reports/store/utils.js
new file mode 100644
index 00000000000..caa4039af72
--- /dev/null
+++ b/app/assets/javascripts/reports/store/utils.js
@@ -0,0 +1,60 @@
+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;
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index b5de3dd6d73..80593d1f34a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -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"
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index e84c436905d..672e5280b5e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -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);
}
diff --git a/app/assets/javascripts/vue_shared/components/code_block.vue b/app/assets/javascripts/vue_shared/components/code_block.vue
new file mode 100644
index 00000000000..30920a2a86d
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/code_block.vue
@@ -0,0 +1,16 @@
+<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>
diff --git a/app/assets/javascripts/vue_shared/components/reports/issue_body.js b/app/assets/javascripts/vue_shared/components/reports/issue_body.js
index f2141e519da..54dfb7b16bf 100644
--- a/app/assets/javascripts/vue_shared/components/reports/issue_body.js
+++ b/app/assets/javascripts/vue_shared/components/reports/issue_body.js
@@ -1,3 +1,9 @@
-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,
+};
diff --git a/app/assets/javascripts/vue_shared/components/reports/issues_list.vue b/app/assets/javascripts/vue_shared/components/reports/issues_list.vue
index c01f77c2509..2545e84f932 100644
--- a/app/assets/javascripts/vue_shared/components/reports/issues_list.vue
+++ b/app/assets/javascripts/vue_shared/components/reports/issues_list.vue
@@ -18,6 +18,11 @@ export default {
failed: STATUS_FAILED,
neutral: STATUS_NEUTRAL,
props: {
+ newIssues: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
unresolvedIssues: {
type: Array,
required: false,
@@ -45,6 +50,15 @@ export default {
<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"
:issues="unresolvedIssues"
diff --git a/app/assets/javascripts/vue_shared/components/reports/report_issues.vue b/app/assets/javascripts/vue_shared/components/reports/report_issues.vue
index 2d1f3d82234..1f13e555b31 100644
--- a/app/assets/javascripts/vue_shared/components/reports/report_issues.vue
+++ b/app/assets/javascripts/vue_shared/components/reports/report_issues.vue
@@ -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>
diff --git a/app/assets/javascripts/vue_shared/components/reports/summary_row.vue b/app/assets/javascripts/vue_shared/components/reports/summary_row.vue
index 997bad960e2..28156d7c983 100644
--- a/app/assets/javascripts/vue_shared/components/reports/summary_row.vue
+++ b/app/assets/javascripts/vue_shared/components/reports/summary_row.vue
@@ -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>
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 340fddd398b..12fc307130a 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -353,3 +353,27 @@
.flex-right {
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;
+ }
+}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 7bd0f0bf1e0..a355ceea7a0 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -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;
diff --git a/changelogs/unreleased/45318-junit-FE.yml b/changelogs/unreleased/45318-junit-FE.yml
new file mode 100644
index 00000000000..bbc08f54484
--- /dev/null
+++ b/changelogs/unreleased/45318-junit-FE.yml
@@ -0,0 +1,5 @@
+---
+title: Adds frontend support to render test reports on the MR widget
+merge_request: 20936
+author:
+type: added
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 2d700fe8773..21a7c62debe 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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] ""
@@ -4381,6 +4391,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 ""
@@ -6264,6 +6301,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] ""
diff --git a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js
new file mode 100644
index 00000000000..6ca03620ca3
--- /dev/null
+++ b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js
@@ -0,0 +1,143 @@
+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);
+ });
+ });
+});
diff --git a/spec/javascripts/reports/components/modal_spec.js b/spec/javascripts/reports/components/modal_spec.js
new file mode 100644
index 00000000000..3a567c40eca
--- /dev/null
+++ b/spec/javascripts/reports/components/modal_spec.js
@@ -0,0 +1,45 @@
+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');
+ });
+});
diff --git a/spec/javascripts/reports/components/test_issue_body_spec.js b/spec/javascripts/reports/components/test_issue_body_spec.js
new file mode 100644
index 00000000000..242eed114d9
--- /dev/null
+++ b/spec/javascripts/reports/components/test_issue_body_spec.js
@@ -0,0 +1,72 @@
+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);
+ });
+ });
+});
diff --git a/spec/javascripts/reports/mock_data/mock_data.js b/spec/javascripts/reports/mock_data/mock_data.js
new file mode 100644
index 00000000000..0d90253bad2
--- /dev/null
+++ b/spec/javascripts/reports/mock_data/mock_data.js
@@ -0,0 +1,8 @@
+// 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'",
+};
diff --git a/spec/javascripts/reports/mock_data/new_and_fixed_failures_report.json b/spec/javascripts/reports/mock_data/new_and_fixed_failures_report.json
new file mode 100644
index 00000000000..ceaf894375a
--- /dev/null
+++ b/spec/javascripts/reports/mock_data/new_and_fixed_failures_report.json
@@ -0,0 +1 @@
+{"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
diff --git a/spec/javascripts/reports/mock_data/new_failures_report.json b/spec/javascripts/reports/mock_data/new_failures_report.json
new file mode 100644
index 00000000000..930efe16f65
--- /dev/null
+++ b/spec/javascripts/reports/mock_data/new_failures_report.json
@@ -0,0 +1 @@
+{"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
diff --git a/spec/javascripts/reports/mock_data/no_failures_report.json b/spec/javascripts/reports/mock_data/no_failures_report.json
new file mode 100644
index 00000000000..6c0675ff7dc
--- /dev/null
+++ b/spec/javascripts/reports/mock_data/no_failures_report.json
@@ -0,0 +1 @@
+{"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
diff --git a/spec/javascripts/reports/store/actions_spec.js b/spec/javascripts/reports/store/actions_spec.js
index c714c5af156..3b2fcd0edba 100644
--- a/spec/javascripts/reports/store/actions_spec.js
+++ b/spec/javascripts/reports/store/actions_spec.js
@@ -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';
@@ -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,
+ );
+ });
+ });
+
});
diff --git a/spec/javascripts/reports/store/mutations_spec.js b/spec/javascripts/reports/store/mutations_spec.js
index 3e0b15438c3..c3e2bb8e00c 100644
--- a/spec/javascripts/reports/store/mutations_spec.js
+++ b/spec/javascripts/reports/store/mutations_spec.js
@@ -1,6 +1,7 @@
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;
@@ -89,6 +90,7 @@ describe('Reports Store Mutations', () => {
beforeEach(() => {
mutations[types.RECEIVE_REPORTS_ERROR](stateCopy);
});
+
it('should reset isLoading', () => {
expect(stateCopy.isLoading).toEqual(false);
});
@@ -97,5 +99,30 @@ 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,
+ 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);
+ });
});
});
diff --git a/spec/javascripts/reports/store/utils_spec.js b/spec/javascripts/reports/store/utils_spec.js
new file mode 100644
index 00000000000..1679d120db2
--- /dev/null
+++ b/spec/javascripts/reports/store/utils_spec.js
@@ -0,0 +1,138 @@
+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);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/code_block_spec.js b/spec/javascripts/vue_shared/components/code_block_spec.js
new file mode 100644
index 00000000000..6b91a20ff76
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/code_block_spec.js
@@ -0,0 +1,33 @@
+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);
+ });
+});