Commit ab0eab66 authored by Mark Florian's avatar Mark Florian

Merge branch '235202-test-report-docs-link' into 'master'

Add test report empty state

See merge request gitlab-org/gitlab!59812
parents b2e48b5b 83f5ba73
<script>
import { GlEmptyState } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
export const i18n = {
noTestsButton: s__('TestReports|Learn more about pipeline test reports'),
noTestsDescription: s__('TestReports|No test cases were found in the test report.'),
noTestsTitle: s__('TestReports|There are no tests to display'),
noReportsButton: s__('TestReports|Learn how to upload pipeline test reports'),
noReportsDescription: s__(
'TestReports|You can configure your job to use unit test reports, and GitLab displays a report here and in the related merge request.',
),
noReportsTitle: s__('TestReports|There are no test reports for this pipeline'),
};
export default {
i18n,
components: {
GlEmptyState,
},
inject: {
emptyStateImagePath: {
default: '',
},
hasTestReport: {
default: false,
},
},
computed: {
emptyStateText() {
if (this.hasTestReport) {
return {
button: this.$options.i18n.noTestsButton,
description: this.$options.i18n.noTestsDescription,
title: this.$options.i18n.noTestsTitle,
};
}
return {
button: this.$options.i18n.noReportsButton,
description: this.$options.i18n.noReportsDescription,
title: this.$options.i18n.noReportsTitle,
};
},
testReportDocPath() {
return helpPagePath('ci/unit_test_reports');
},
},
};
</script>
<template>
<gl-empty-state
:title="emptyStateText.title"
:description="emptyStateText.description"
:svg-path="emptyStateImagePath"
:primary-button-link="testReportDocPath"
:primary-button-text="emptyStateText.button"
/>
</template>
<script> <script>
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import EmptyState from './empty_state.vue';
import TestSuiteTable from './test_suite_table.vue'; import TestSuiteTable from './test_suite_table.vue';
import TestSummary from './test_summary.vue'; import TestSummary from './test_summary.vue';
import TestSummaryTable from './test_summary_table.vue'; import TestSummaryTable from './test_summary_table.vue';
...@@ -8,11 +9,17 @@ import TestSummaryTable from './test_summary_table.vue'; ...@@ -8,11 +9,17 @@ import TestSummaryTable from './test_summary_table.vue';
export default { export default {
name: 'TestReports', name: 'TestReports',
components: { components: {
EmptyState,
GlLoadingIcon, GlLoadingIcon,
TestSuiteTable, TestSuiteTable,
TestSummary, TestSummary,
TestSummaryTable, TestSummaryTable,
}, },
inject: {
hasTestReport: {
default: false,
},
},
computed: { computed: {
...mapState(['isLoading', 'selectedSuiteIndex', 'testReports']), ...mapState(['isLoading', 'selectedSuiteIndex', 'testReports']),
...mapGetters(['getSelectedSuite']), ...mapGetters(['getSelectedSuite']),
...@@ -25,7 +32,9 @@ export default { ...@@ -25,7 +32,9 @@ export default {
}, },
}, },
created() { created() {
if (this.hasTestReport) {
this.fetchSummary(); this.fetchSummary();
}
}, },
methods: { methods: {
...mapActions([ ...mapActions([
...@@ -83,11 +92,5 @@ export default { ...@@ -83,11 +92,5 @@ export default {
</transition> </transition>
</div> </div>
<div v-else> <empty-state v-else />
<div class="row gl-mt-3">
<div class="col-12">
<p data-testid="no-tests-to-show">{{ s__('TestReports|There are no tests to show.') }}</p>
</div>
</div>
</div>
</template> </template>
import Vue from 'vue'; import Vue from 'vue';
import { deprecatedCreateFlash as Flash } from '~/flash'; import { deprecatedCreateFlash as Flash } from '~/flash';
import { parseBoolean } from '~/lib/utils/common_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import PipelineGraphLegacy from './components/graph/graph_component_legacy.vue'; import PipelineGraphLegacy from './components/graph/graph_component_legacy.vue';
...@@ -63,7 +64,8 @@ const createLegacyPipelinesDetailApp = (mediator) => { ...@@ -63,7 +64,8 @@ const createLegacyPipelinesDetailApp = (mediator) => {
const createTestDetails = () => { const createTestDetails = () => {
const el = document.querySelector(SELECTORS.PIPELINE_TESTS); const el = document.querySelector(SELECTORS.PIPELINE_TESTS);
const { blobPath, summaryEndpoint, suiteEndpoint } = el?.dataset || {}; const { blobPath, emptyStateImagePath, hasTestReport, summaryEndpoint, suiteEndpoint } =
el?.dataset || {};
const testReportsStore = createTestReportsStore({ const testReportsStore = createTestReportsStore({
blobPath, blobPath,
summaryEndpoint, summaryEndpoint,
...@@ -76,6 +78,10 @@ const createTestDetails = () => { ...@@ -76,6 +78,10 @@ const createTestDetails = () => {
components: { components: {
TestReports, TestReports,
}, },
provide: {
emptyStateImagePath,
hasTestReport: parseBoolean(hasTestReport),
},
store: testReportsStore, store: testReportsStore,
render(createElement) { render(createElement) {
return createElement('test-reports'); return createElement('test-reports');
......
...@@ -83,5 +83,7 @@ ...@@ -83,5 +83,7 @@
#js-tab-tests.tab-pane #js-tab-tests.tab-pane
#js-pipeline-tests-detail{ data: { summary_endpoint: summary_project_pipeline_tests_path(@project, @pipeline, format: :json), #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) } } blob_path: project_blob_path(@project, @pipeline.sha),
has_test_report: @pipeline.has_reports?(Ci::JobArtifact.test_reports).to_s,
empty_state_image_path: image_path('illustrations/empty-state/empty-test-cases-lg.svg') } }
= render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project = render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project
---
title: Add link to documentation in empty pipeline test reports
merge_request: 59812
author:
type: added
...@@ -31175,16 +31175,28 @@ msgstr "" ...@@ -31175,16 +31175,28 @@ msgstr ""
msgid "TestReports|Jobs" msgid "TestReports|Jobs"
msgstr "" msgstr ""
msgid "TestReports|Learn how to upload pipeline test reports"
msgstr ""
msgid "TestReports|Learn more about pipeline test reports"
msgstr ""
msgid "TestReports|No test cases were found in the test report."
msgstr ""
msgid "TestReports|Tests" msgid "TestReports|Tests"
msgstr "" msgstr ""
msgid "TestReports|There are no test cases to display." msgid "TestReports|There are no test cases to display."
msgstr "" msgstr ""
msgid "TestReports|There are no test reports for this pipeline"
msgstr ""
msgid "TestReports|There are no test suites to show." msgid "TestReports|There are no test suites to show."
msgstr "" msgstr ""
msgid "TestReports|There are no tests to show." msgid "TestReports|There are no tests to display"
msgstr "" msgstr ""
msgid "TestReports|There was an error fetching the summary." msgid "TestReports|There was an error fetching the summary."
...@@ -31193,6 +31205,9 @@ msgstr "" ...@@ -31193,6 +31205,9 @@ msgstr ""
msgid "TestReports|There was an error fetching the test suite." msgid "TestReports|There was an error fetching the test suite."
msgstr "" msgstr ""
msgid "TestReports|You can configure your job to use unit test reports, and GitLab displays a report here and in the related merge request."
msgstr ""
msgid "Tests" msgid "Tests"
msgstr "" msgstr ""
......
import { GlEmptyState } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import EmptyState, { i18n } from '~/pipelines/components/test_reports/empty_state.vue';
describe('Test report empty state', () => {
let wrapper;
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const createComponent = ({ hasTestReport = true } = {}) => {
wrapper = shallowMount(EmptyState, {
provide: {
emptyStateImagePath: '/image/path',
hasTestReport,
},
stubs: {
GlEmptyState,
},
});
};
describe('when pipeline has a test report', () => {
it('should render empty test report message', () => {
createComponent();
expect(findEmptyState().props()).toMatchObject({
primaryButtonText: i18n.noTestsButton,
description: i18n.noTestsDescription,
title: i18n.noTestsTitle,
});
});
});
describe('when pipeline does not have a test report', () => {
it('should render no test report message', () => {
createComponent({ hasTestReport: false });
expect(findEmptyState().props()).toMatchObject({
primaryButtonText: i18n.noReportsButton,
description: i18n.noReportsDescription,
title: i18n.noReportsTitle,
});
});
});
});
...@@ -2,6 +2,8 @@ import { GlLoadingIcon } from '@gitlab/ui'; ...@@ -2,6 +2,8 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { getJSONFixture } from 'helpers/fixtures'; import { getJSONFixture } from 'helpers/fixtures';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import EmptyState from '~/pipelines/components/test_reports/empty_state.vue';
import TestReports from '~/pipelines/components/test_reports/test_reports.vue'; import TestReports from '~/pipelines/components/test_reports/test_reports.vue';
import TestSummary from '~/pipelines/components/test_reports/test_summary.vue'; import TestSummary from '~/pipelines/components/test_reports/test_summary.vue';
import TestSummaryTable from '~/pipelines/components/test_reports/test_summary_table.vue'; import TestSummaryTable from '~/pipelines/components/test_reports/test_summary_table.vue';
...@@ -16,11 +18,11 @@ describe('Test reports app', () => { ...@@ -16,11 +18,11 @@ describe('Test reports app', () => {
const testReports = getJSONFixture('pipelines/test_report.json'); const testReports = getJSONFixture('pipelines/test_report.json');
const loadingSpinner = () => wrapper.find(GlLoadingIcon); const loadingSpinner = () => wrapper.findComponent(GlLoadingIcon);
const testsDetail = () => wrapper.find('[data-testid="tests-detail"]'); const testsDetail = () => wrapper.findByTestId('tests-detail');
const noTestsToShow = () => wrapper.find('[data-testid="no-tests-to-show"]'); const emptyState = () => wrapper.findComponent(EmptyState);
const testSummary = () => wrapper.find(TestSummary); const testSummary = () => wrapper.findComponent(TestSummary);
const testSummaryTable = () => wrapper.find(TestSummaryTable); const testSummaryTable = () => wrapper.findComponent(TestSummaryTable);
const actionSpies = { const actionSpies = {
fetchTestSuite: jest.fn(), fetchTestSuite: jest.fn(),
...@@ -29,7 +31,7 @@ describe('Test reports app', () => { ...@@ -29,7 +31,7 @@ describe('Test reports app', () => {
removeSelectedSuiteIndex: jest.fn(), removeSelectedSuiteIndex: jest.fn(),
}; };
const createComponent = (state = {}) => { const createComponent = ({ state = {}, hasTestReport = true } = {}) => {
store = new Vuex.Store({ store = new Vuex.Store({
state: { state: {
isLoading: false, isLoading: false,
...@@ -41,10 +43,15 @@ describe('Test reports app', () => { ...@@ -41,10 +43,15 @@ describe('Test reports app', () => {
getters, getters,
}); });
wrapper = shallowMount(TestReports, { wrapper = extendedWrapper(
shallowMount(TestReports, {
store, store,
localVue, localVue,
}); provide: {
hasTestReport,
},
}),
);
}; };
afterEach(() => { afterEach(() => {
...@@ -52,33 +59,34 @@ describe('Test reports app', () => { ...@@ -52,33 +59,34 @@ describe('Test reports app', () => {
}); });
describe('when component is created', () => { describe('when component is created', () => {
beforeEach(() => { it('should call fetchSummary when pipeline has test report', () => {
createComponent(); createComponent();
});
it('should call fetchSummary', () => {
expect(actionSpies.fetchSummary).toHaveBeenCalled(); expect(actionSpies.fetchSummary).toHaveBeenCalled();
}); });
it('should not call fetchSummary when pipeline does not have test report', () => {
createComponent({ state: {}, hasTestReport: false });
expect(actionSpies.fetchSummary).not.toHaveBeenCalled();
});
}); });
describe('when loading', () => { describe('when loading', () => {
beforeEach(() => createComponent({ isLoading: true })); beforeEach(() => createComponent({ state: { isLoading: true } }));
it('shows the loading spinner', () => { it('shows the loading spinner', () => {
expect(noTestsToShow().exists()).toBe(false); expect(emptyState().exists()).toBe(false);
expect(testsDetail().exists()).toBe(false); expect(testsDetail().exists()).toBe(false);
expect(loadingSpinner().exists()).toBe(true); expect(loadingSpinner().exists()).toBe(true);
}); });
}); });
describe('when the api returns no data', () => { describe('when the api returns no data', () => {
beforeEach(() => createComponent({ testReports: {} })); it('displays empty state component', () => {
createComponent({ state: { testReports: {} } });
it('displays that there are no tests to show', () => {
const noTests = noTestsToShow();
expect(noTests.exists()).toBe(true); expect(emptyState().exists()).toBe(true);
expect(noTests.text()).toBe('There are no tests to show.');
}); });
}); });
...@@ -97,7 +105,7 @@ describe('Test reports app', () => { ...@@ -97,7 +105,7 @@ describe('Test reports app', () => {
describe('when a suite is clicked', () => { describe('when a suite is clicked', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ hasFullReport: true }); createComponent({ state: { hasFullReport: true } });
testSummaryTable().vm.$emit('row-click', 0); testSummaryTable().vm.$emit('row-click', 0);
}); });
...@@ -109,7 +117,7 @@ describe('Test reports app', () => { ...@@ -109,7 +117,7 @@ describe('Test reports app', () => {
describe('when clicking back to summary', () => { describe('when clicking back to summary', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ selectedSuiteIndex: 0 }); createComponent({ state: { selectedSuiteIndex: 0 } });
testSummary().vm.$emit('on-back-click'); testSummary().vm.$emit('on-back-click');
}); });
......
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