Commit 3f078050 authored by Scott Hampton's avatar Scott Hampton

Link to test file in test report widget

Add a link to the test case file in the test report
MR widget modal.
parent c84d9415
export const fieldTypes = { export const fieldTypes = {
codeBock: 'codeBlock', codeBlock: 'codeBlock',
link: 'link', link: 'link',
seconds: 'seconds', seconds: 'seconds',
text: 'text', text: 'text',
......
...@@ -25,6 +25,14 @@ export default { ...@@ -25,6 +25,14 @@ export default {
required: true, required: true,
}, },
}, },
computed: {
filteredModalData() {
// Filter out the properties that don't have a value
return Object.fromEntries(
Object.entries(this.modalData).filter((data) => Boolean(data[1].value)),
);
},
},
fieldTypes, fieldTypes,
}; };
</script> </script>
...@@ -36,23 +44,18 @@ export default { ...@@ -36,23 +44,18 @@ export default {
:hide-footer="true" :hide-footer="true"
@hide="$emit('hide')" @hide="$emit('hide')"
> >
<div <div v-for="(field, key, index) in filteredModalData" :key="index" class="row gl-mt-3 gl-mb-3">
v-for="(field, key, index) in modalData"
v-if="field.value"
:key="index"
class="row gl-mt-3 gl-mb-3"
>
<strong class="col-sm-3 text-right"> {{ field.text }}: </strong> <strong class="col-sm-3 text-right"> {{ field.text }}: </strong>
<div class="col-sm-9 text-secondary"> <div class="col-sm-9 text-secondary">
<code-block v-if="field.type === $options.fieldTypes.codeBock" :code="field.value" /> <code-block v-if="field.type === $options.fieldTypes.codeBlock" :code="field.value" />
<gl-link <gl-link
v-else-if="field.type === $options.fieldTypes.link" v-else-if="field.type === $options.fieldTypes.link"
:href="field.value" :href="field.value.path"
target="_blank" target="_blank"
> >
{{ field.value }} {{ field.value.text }}
</gl-link> </gl-link>
<gl-sprintf <gl-sprintf
......
...@@ -39,6 +39,10 @@ export default { ...@@ -39,6 +39,10 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
headBlobPath: {
type: String,
required: true,
},
}, },
componentNames, componentNames,
computed: { computed: {
...@@ -73,12 +77,15 @@ export default { ...@@ -73,12 +77,15 @@ export default {
}, },
}, },
created() { created() {
this.setEndpoint(this.endpoint); this.setPaths({
endpoint: this.endpoint,
headBlobPath: this.headBlobPath,
});
this.fetchReports(); this.fetchReports();
}, },
methods: { methods: {
...mapActions(['setEndpoint', 'fetchReports', 'closeModal']), ...mapActions(['setPaths', 'fetchReports', 'closeModal']),
reportText(report) { reportText(report) {
const { name, summary } = report || {}; const { name, summary } = report || {};
......
...@@ -4,7 +4,7 @@ import httpStatusCodes from '../../../lib/utils/http_status'; ...@@ -4,7 +4,7 @@ import httpStatusCodes from '../../../lib/utils/http_status';
import Poll from '../../../lib/utils/poll'; import Poll from '../../../lib/utils/poll';
import * as types from './mutation_types'; import * as types from './mutation_types';
export const setEndpoint = ({ commit }, endpoint) => commit(types.SET_ENDPOINT, endpoint); export const setPaths = ({ commit }, paths) => commit(types.SET_PATHS, paths);
export const requestReports = ({ commit }) => commit(types.REQUEST_REPORTS); export const requestReports = ({ commit }) => commit(types.REQUEST_REPORTS);
......
export const SET_ENDPOINT = 'SET_ENDPOINT'; export const SET_PATHS = 'SET_PATHS';
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';
......
import * as types from './mutation_types'; import * as types from './mutation_types';
import { countRecentlyFailedTests } from './utils'; import { countRecentlyFailedTests, formatFilePath } from './utils';
export default { export default {
[types.SET_ENDPOINT](state, endpoint) { [types.SET_PATHS](state, { endpoint, headBlobPath }) {
state.endpoint = endpoint; state.endpoint = endpoint;
state.headBlobPath = headBlobPath;
}, },
[types.REQUEST_REPORTS](state) { [types.REQUEST_REPORTS](state) {
state.isLoading = true; state.isLoading = true;
...@@ -42,17 +43,25 @@ export default { ...@@ -42,17 +43,25 @@ export default {
state.status = null; state.status = null;
}, },
[types.SET_ISSUE_MODAL_DATA](state, payload) { [types.SET_ISSUE_MODAL_DATA](state, payload) {
state.modal.title = payload.issue.name; const { issue } = payload;
state.modal.title = issue.name;
Object.keys(payload.issue).forEach((key) => { Object.keys(issue).forEach((key) => {
if (Object.prototype.hasOwnProperty.call(state.modal.data, key)) { if (Object.prototype.hasOwnProperty.call(state.modal.data, key)) {
state.modal.data[key] = { state.modal.data[key] = {
...state.modal.data[key], ...state.modal.data[key],
value: payload.issue[key], value: issue[key],
}; };
} }
}); });
if (issue.file) {
state.modal.data.filename.value = {
text: issue.file,
path: `${state.headBlobPath}/${formatFilePath(issue.file)}`,
};
}
state.modal.open = true; state.modal.open = true;
}, },
[types.RESET_ISSUE_MODAL_DATA](state) { [types.RESET_ISSUE_MODAL_DATA](state) {
......
...@@ -41,16 +41,16 @@ export default () => ({ ...@@ -41,16 +41,16 @@ export default () => ({
open: false, open: false,
data: { data: {
class: {
value: null,
text: s__('Reports|Class'),
type: fieldTypes.link,
},
classname: { classname: {
value: null, value: null,
text: s__('Reports|Classname'), text: s__('Reports|Classname'),
type: fieldTypes.text, type: fieldTypes.text,
}, },
filename: {
value: null,
text: s__('Reports|Filename'),
type: fieldTypes.link,
},
execution_time: { execution_time: {
value: null, value: null,
text: s__('Reports|Execution time'), text: s__('Reports|Execution time'),
...@@ -59,12 +59,12 @@ export default () => ({ ...@@ -59,12 +59,12 @@ export default () => ({
failure: { failure: {
value: null, value: null,
text: s__('Reports|Failure'), text: s__('Reports|Failure'),
type: fieldTypes.codeBock, type: fieldTypes.codeBlock,
}, },
system_output: { system_output: {
value: null, value: null,
text: s__('Reports|System output'), text: s__('Reports|System output'),
type: fieldTypes.codeBock, type: fieldTypes.codeBlock,
}, },
}, },
}, },
......
...@@ -100,3 +100,12 @@ export const statusIcon = (status) => { ...@@ -100,3 +100,12 @@ export const statusIcon = (status) => {
return ICON_NOTFOUND; return ICON_NOTFOUND;
}; };
/**
* Removes `./` from the beginning of a file path so it can be appended onto a blob path
* @param {String} file
* @returns {String} - formatted value
*/
export const formatFilePath = (file) => {
return file.replace(/^\.?\/*/, '');
};
...@@ -480,6 +480,7 @@ export default { ...@@ -480,6 +480,7 @@ export default {
v-if="mr.testResultsPath" v-if="mr.testResultsPath"
class="js-reports-container" class="js-reports-container"
:endpoint="mr.testResultsPath" :endpoint="mr.testResultsPath"
:head-blob-path="mr.headBlobPath"
:pipeline-path="mr.pipeline.path" :pipeline-path="mr.pipeline.path"
/> />
......
---
title: Add link to test case file in the test report for merge requests
merge_request: 57911
author:
type: added
...@@ -380,6 +380,7 @@ export default { ...@@ -380,6 +380,7 @@ export default {
v-if="mr.testResultsPath" v-if="mr.testResultsPath"
class="js-reports-container" class="js-reports-container"
:endpoint="mr.testResultsPath" :endpoint="mr.testResultsPath"
:head-blob-path="mr.headBlobPath"
:pipeline-path="mr.pipeline.path" :pipeline-path="mr.pipeline.path"
/> />
......
...@@ -25837,9 +25837,6 @@ msgstr "" ...@@ -25837,9 +25837,6 @@ msgstr ""
msgid "Reports|Base report parsing error:" msgid "Reports|Base report parsing error:"
msgstr "" msgstr ""
msgid "Reports|Class"
msgstr ""
msgid "Reports|Classname" msgid "Reports|Classname"
msgstr "" msgstr ""
...@@ -25859,6 +25856,9 @@ msgstr[1] "" ...@@ -25859,6 +25856,9 @@ msgstr[1] ""
msgid "Reports|Failure" msgid "Reports|Failure"
msgstr "" msgstr ""
msgid "Reports|Filename"
msgstr ""
msgid "Reports|Head report parsing error:" msgid "Reports|Head report parsing error:"
msgstr "" msgstr ""
......
...@@ -15,7 +15,10 @@ describe('Grouped Test Reports Modal', () => { ...@@ -15,7 +15,10 @@ describe('Grouped Test Reports Modal', () => {
// populate data // populate data
modalDataStructure.execution_time.value = 0.009411; modalDataStructure.execution_time.value = 0.009411;
modalDataStructure.system_output.value = 'Failure/Error: is_expected.to eq(3)\n\n'; modalDataStructure.system_output.value = 'Failure/Error: is_expected.to eq(3)\n\n';
modalDataStructure.class.value = 'link'; modalDataStructure.filename.value = {
text: 'link',
path: '/file/path',
};
let wrapper; let wrapper;
...@@ -43,9 +46,9 @@ describe('Grouped Test Reports Modal', () => { ...@@ -43,9 +46,9 @@ describe('Grouped Test Reports Modal', () => {
it('renders link', () => { it('renders link', () => {
const link = wrapper.findComponent(GlLink); const link = wrapper.findComponent(GlLink);
expect(link.attributes().href).toEqual(modalDataStructure.class.value); expect(link.attributes().href).toEqual(modalDataStructure.filename.value.path);
expect(link.text()).toEqual(modalDataStructure.class.value); expect(link.text()).toEqual(modalDataStructure.filename.value.text);
}); });
it('renders seconds', () => { it('renders seconds', () => {
......
...@@ -17,6 +17,7 @@ localVue.use(Vuex); ...@@ -17,6 +17,7 @@ localVue.use(Vuex);
describe('Grouped test reports app', () => { describe('Grouped test reports app', () => {
const endpoint = 'endpoint.json'; const endpoint = 'endpoint.json';
const headBlobPath = '/blob/path';
const pipelinePath = '/path/to/pipeline'; const pipelinePath = '/path/to/pipeline';
let wrapper; let wrapper;
let mockStore; let mockStore;
...@@ -27,6 +28,7 @@ describe('Grouped test reports app', () => { ...@@ -27,6 +28,7 @@ describe('Grouped test reports app', () => {
localVue, localVue,
propsData: { propsData: {
endpoint, endpoint,
headBlobPath,
pipelinePath, pipelinePath,
...props, ...props,
}, },
...@@ -56,7 +58,7 @@ describe('Grouped test reports app', () => { ...@@ -56,7 +58,7 @@ describe('Grouped test reports app', () => {
...getStoreConfig(), ...getStoreConfig(),
actions: { actions: {
fetchReports: () => {}, fetchReports: () => {},
setEndpoint: () => {}, setPaths: () => {},
}, },
}); });
mountComponent(); mountComponent();
......
...@@ -3,7 +3,7 @@ import { TEST_HOST } from 'helpers/test_constants'; ...@@ -3,7 +3,7 @@ import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { import {
setEndpoint, setPaths,
requestReports, requestReports,
fetchReports, fetchReports,
stopPolling, stopPolling,
...@@ -23,13 +23,18 @@ describe('Reports Store Actions', () => { ...@@ -23,13 +23,18 @@ describe('Reports Store Actions', () => {
mockedState = state(); mockedState = state();
}); });
describe('setEndpoint', () => { describe('setPaths', () => {
it('should commit SET_ENDPOINT mutation', (done) => { it('should commit SET_PATHS mutation', (done) => {
testAction( testAction(
setEndpoint, setPaths,
'endpoint.json', { endpoint: 'endpoint.json', headBlobPath: '/blob/path' },
mockedState, mockedState,
[{ type: types.SET_ENDPOINT, payload: 'endpoint.json' }], [
{
type: types.SET_PATHS,
payload: { endpoint: 'endpoint.json', headBlobPath: '/blob/path' },
},
],
[], [],
done, done,
); );
......
...@@ -10,11 +10,15 @@ describe('Reports Store Mutations', () => { ...@@ -10,11 +10,15 @@ describe('Reports Store Mutations', () => {
stateCopy = state(); stateCopy = state();
}); });
describe('SET_ENDPOINT', () => { describe('SET_PATHS', () => {
it('should set endpoint', () => { it('should set endpoint', () => {
mutations[types.SET_ENDPOINT](stateCopy, 'endpoint.json'); mutations[types.SET_PATHS](stateCopy, {
endpoint: 'endpoint.json',
headBlobPath: '/blob/path',
});
expect(stateCopy.endpoint).toEqual('endpoint.json'); expect(stateCopy.endpoint).toEqual('endpoint.json');
expect(stateCopy.headBlobPath).toEqual('/blob/path');
}); });
}); });
......
...@@ -238,4 +238,18 @@ describe('Reports store utils', () => { ...@@ -238,4 +238,18 @@ describe('Reports store utils', () => {
}); });
}); });
}); });
describe('formatFilePath', () => {
it.each`
file | expected
${'./test.js'} | ${'test.js'}
${'/test.js'} | ${'test.js'}
${'.//////////////test.js'} | ${'test.js'}
${'test.js'} | ${'test.js'}
${'mock/path./test.js'} | ${'mock/path./test.js'}
${'./mock/path./test.js'} | ${'mock/path./test.js'}
`('should format $file to be $expected', ({ file, expected }) => {
expect(utils.formatFilePath(file)).toBe(expected);
});
});
}); });
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