Commit 4e06bab8 authored by Scott Hampton's avatar Scott Hampton

Add test report empty state

The test report empty state was very basic,
and only had text. This switches over to use
a complete empty state, and checks to see
if the user has an empty test report or no report
at all.
parent 1fd3674a
<script>
import { GlEmptyState } from '@gitlab/ui';
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 will display 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: {
type: String,
default: '',
},
hasTestReport: {
type: Boolean,
default: false,
},
testReportDocPath: {
type: String,
default: '',
},
},
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,
};
},
},
};
</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,18 @@ import TestSummaryTable from './test_summary_table.vue'; ...@@ -8,11 +9,18 @@ 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: {
type: Boolean,
default: false,
},
},
computed: { computed: {
...mapState(['isLoading', 'selectedSuiteIndex', 'testReports']), ...mapState(['isLoading', 'selectedSuiteIndex', 'testReports']),
...mapGetters(['getSelectedSuite']), ...mapGetters(['getSelectedSuite']),
...@@ -25,7 +33,9 @@ export default { ...@@ -25,7 +33,9 @@ export default {
}, },
}, },
created() { created() {
if (this.hasTestReport) {
this.fetchSummary(); this.fetchSummary();
}
}, },
methods: { methods: {
...mapActions([ ...mapActions([
...@@ -83,11 +93,5 @@ export default { ...@@ -83,11 +93,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>
...@@ -63,7 +63,14 @@ const createLegacyPipelinesDetailApp = (mediator) => { ...@@ -63,7 +63,14 @@ 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,
testReportDocPath,
} = el?.dataset || {};
const testReportsStore = createTestReportsStore({ const testReportsStore = createTestReportsStore({
blobPath, blobPath,
summaryEndpoint, summaryEndpoint,
...@@ -76,6 +83,11 @@ const createTestDetails = () => { ...@@ -76,6 +83,11 @@ const createTestDetails = () => {
components: { components: {
TestReports, TestReports,
}, },
provide: {
emptyStateImagePath,
hasTestReport: hasTestReport !== undefined, // if hasTestReport is false that means the attribute isn't included
testReportDocPath,
},
store: testReportsStore, store: testReportsStore,
render(createElement) { render(createElement) {
return createElement('test-reports'); return createElement('test-reports');
......
...@@ -83,5 +83,8 @@ ...@@ -83,5 +83,8 @@
#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),
test_report_doc_path: help_page_path('ci/unit_test_reports'),
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
...@@ -30986,16 +30986,28 @@ msgstr "" ...@@ -30986,16 +30986,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."
...@@ -31004,6 +31016,9 @@ msgstr "" ...@@ -31004,6 +31016,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 will display 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,
testReportDocPath: '/docs/path',
},
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(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({}, false);
expect(actionSpies.fetchSummary).not.toHaveBeenCalled();
});
}); });
describe('when loading', () => { describe('when loading', () => {
beforeEach(() => createComponent({ isLoading: true })); beforeEach(() => createComponent({ 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({ 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.');
}); });
}); });
......
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