Commit def98a4c authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '267344-test-report-file-name-link' into 'master'

[RUN-AS-IF-FOSS]Link test report file name to file

See merge request gitlab-org/gitlab!53650
parents d2ec8be6 43d74305
......@@ -5,6 +5,7 @@ import {
GlTooltipDirective,
GlFriendlyWrap,
GlIcon,
GlLink,
GlButton,
GlPagination,
} from '@gitlab/ui';
......@@ -16,6 +17,7 @@ export default {
components: {
GlIcon,
GlFriendlyWrap,
GlLink,
GlButton,
GlPagination,
TestCaseDetails,
......@@ -97,11 +99,9 @@ export default {
<div class="table-section section-10 section-wrap">
<div role="rowheader" class="table-mobile-header">{{ __('Filename') }}</div>
<div class="table-mobile-content gl-md-pr-2 gl-overflow-wrap-break">
<gl-friendly-wrap
v-if="testCase.file"
:symbols="$options.wrapSymbols"
:text="testCase.file"
/>
<gl-link v-if="testCase.file" :href="testCase.filePath" target="_blank">
<gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.file" />
</gl-link>
<gl-button
v-if="testCase.file"
v-gl-tooltip
......
......@@ -58,8 +58,9 @@ const createLegacyPipelinesDetailApp = (mediator) => {
const createTestDetails = () => {
const el = document.querySelector(SELECTORS.PIPELINE_TESTS);
const { summaryEndpoint, suiteEndpoint } = el?.dataset || {};
const { blobPath, summaryEndpoint, suiteEndpoint } = el?.dataset || {};
const testReportsStore = createTestReportsStore({
blobPath,
summaryEndpoint,
suiteEndpoint,
});
......
import { addIconStatus, formattedTime } from './utils';
import { addIconStatus, formatFilePath, formattedTime } from './utils';
export const getTestSuites = (state) => {
const { test_suites: testSuites = [] } = state.testReports;
......@@ -17,7 +17,13 @@ export const getSuiteTests = (state) => {
const { page, perPage } = state.pageInfo;
const start = (page - 1) * perPage;
return testCases.map(addIconStatus).slice(start, start + perPage);
return testCases
.map((testCase) => ({
...testCase,
filePath: testCase.file ? `${state.blobPath}/${formatFilePath(testCase.file)}` : null,
}))
.map(addIconStatus)
.slice(start, start + perPage);
};
export const getSuiteTestCount = (state) => getSelectedSuite(state)?.test_cases?.length || 0;
export default ({ summaryEndpoint = '', suiteEndpoint = '' }) => ({
export default ({ blobPath = '', summaryEndpoint = '', suiteEndpoint = '' }) => ({
blobPath,
summaryEndpoint,
suiteEndpoint,
testReports: {},
......
import { __, sprintf } from '../../../locale';
import { TestStatus } from '../../constants';
/**
* 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 function formatFilePath(file) {
return file.replace(/^\.?\/*/, '');
}
export function iconForTestStatus(status) {
switch (status) {
case TestStatus.SUCCESS:
......
......@@ -82,5 +82,6 @@
#js-tab-tests.tab-pane
#js-pipeline-tests-detail{ data: { summary_endpoint: summary_project_pipeline_tests_path(@project, @pipeline, format: :json),
suite_endpoint: project_pipeline_test_path(@project, @pipeline, suite_name: 'suite', format: :json) } }
suite_endpoint: project_pipeline_test_path(@project, @pipeline, suite_name: 'suite', format: :json),
blob_path: project_blob_path(@project, @pipeline.sha) } }
= render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project
---
title: Add link to test case file in pipeline test report
merge_request: 53650
author:
type: added
import { getJSONFixture } from 'helpers/fixtures';
import * as getters from '~/pipelines/stores/test_reports/getters';
import { iconForTestStatus, formattedTime } from '~/pipelines/stores/test_reports/utils';
import {
iconForTestStatus,
formatFilePath,
formattedTime,
} from '~/pipelines/stores/test_reports/utils';
describe('Getters TestReports Store', () => {
let state;
......@@ -8,6 +12,7 @@ describe('Getters TestReports Store', () => {
const testReports = getJSONFixture('pipelines/test_report.json');
const defaultState = {
blobPath: '/test/blob/path',
testReports,
selectedSuiteIndex: 0,
pageInfo: {
......@@ -17,6 +22,7 @@ describe('Getters TestReports Store', () => {
};
const emptyState = {
blobPath: '',
testReports: {},
selectedSuite: null,
pageInfo: {
......@@ -74,6 +80,7 @@ describe('Getters TestReports Store', () => {
const expected = testReports.test_suites[0].test_cases
.map((x) => ({
...x,
filePath: `${state.blobPath}/${formatFilePath(x.file)}`,
formattedTime: formattedTime(x.execution_time),
icon: iconForTestStatus(x.status),
}))
......
import { formattedTime } from '~/pipelines/stores/test_reports/utils';
import { formatFilePath, formattedTime } from '~/pipelines/stores/test_reports/utils';
describe('Test reports 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(formatFilePath(file)).toBe(expected);
});
});
describe('formattedTime', () => {
describe('when time is smaller than a second', () => {
it('should return time in milliseconds fixed to 2 decimals', () => {
......
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlButton, GlFriendlyWrap, GlPagination } from '@gitlab/ui';
import { GlButton, GlFriendlyWrap, GlLink, GlPagination } from '@gitlab/ui';
import { getJSONFixture } from 'helpers/fixtures';
import SuiteTable from '~/pipelines/components/test_reports/test_suite_table.vue';
import * as getters from '~/pipelines/stores/test_reports/getters';
import { formatFilePath } from '~/pipelines/stores/test_reports/utils';
import { TestStatus } from '~/pipelines/constants';
import skippedTestCases from './mock_data';
......@@ -20,15 +21,18 @@ describe('Test reports suite table', () => {
testSuite.test_cases = [...testSuite.test_cases, ...skippedTestCases];
const testCases = testSuite.test_cases;
const blobPath = '/test/blob/path';
const noCasesMessage = () => wrapper.find('.js-no-test-cases');
const allCaseRows = () => wrapper.findAll('.js-case-row');
const findCaseRowAtIndex = (index) => wrapper.findAll('.js-case-row').at(index);
const findLinkForRow = (row) => row.find(GlLink);
const findIconForRow = (row, status) => row.find(`.ci-status-icon-${status}`);
const createComponent = (suite = testSuite, perPage = 20) => {
store = new Vuex.Store({
state: {
blobPath,
testReports: {
test_suites: [suite],
},
......@@ -82,9 +86,13 @@ describe('Test reports suite table', () => {
it('renders the file name for the test with a copy button', () => {
const { file } = testCases[0];
const relativeFile = formatFilePath(file);
const filePath = `${blobPath}/${relativeFile}`;
const row = findCaseRowAtIndex(0);
const fileLink = findLinkForRow(row);
const button = row.find(GlButton);
expect(fileLink.attributes('href')).toBe(filePath);
expect(row.text()).toContain(file);
expect(button.exists()).toBe(true);
expect(button.attributes('data-clipboard-text')).toBe(file);
......
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