Commit af81f312 authored by Scott Hampton's avatar Scott Hampton

Add headers to test report widget

Use the grouped_issues_list component to add
section headers to the test report MR widget.

Also refactored the test issue body to handle
it's own status icon and use gitlab-ui more.

Refactored the test issue body spec to use up
to date standards.
parent a2ccb063
...@@ -14,6 +14,12 @@ export default { ...@@ -14,6 +14,12 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
nestedLevel: {
type: Number,
required: false,
default: 0,
validator: (value) => [0, 1, 2].includes(value),
},
resolvedIssues: { resolvedIssues: {
type: Array, type: Array,
required: false, required: false,
...@@ -58,6 +64,12 @@ export default { ...@@ -58,6 +64,12 @@ export default {
return groupsCount + issuesCount; return groupsCount + issuesCount;
}, },
listClasses() {
return {
'gl-pl-7': this.nestedLevel === 1,
'gl-pl-9': this.nestedLevel === 2,
};
},
}, },
}; };
</script> </script>
...@@ -67,6 +79,7 @@ export default { ...@@ -67,6 +79,7 @@ export default {
:length="listLength" :length="listLength"
:remain="$options.maxShownReportItems" :remain="$options.maxShownReportItems"
:size="$options.typicalReportItemHeight" :size="$options.typicalReportItemHeight"
:class="listClasses"
class="report-block-container" class="report-block-container"
wtag="ul" wtag="ul"
wclass="report-block-list" wclass="report-block-list"
......
...@@ -11,8 +11,8 @@ import { ...@@ -11,8 +11,8 @@ import {
statusIcon, statusIcon,
recentFailuresTextBuilder, recentFailuresTextBuilder,
} from '../store/utils'; } from '../store/utils';
import GroupedIssuesList from './grouped_issues_list.vue';
import { componentNames } from './issue_body'; import { componentNames } from './issue_body';
import IssuesList from './issues_list.vue';
import Modal from './modal.vue'; import Modal from './modal.vue';
import ReportSection from './report_section.vue'; import ReportSection from './report_section.vue';
import SummaryRow from './summary_row.vue'; import SummaryRow from './summary_row.vue';
...@@ -23,7 +23,7 @@ export default { ...@@ -23,7 +23,7 @@ export default {
components: { components: {
ReportSection, ReportSection,
SummaryRow, SummaryRow,
IssuesList, GroupedIssuesList,
Modal, Modal,
GlButton, GlButton,
GlIcon, GlIcon,
...@@ -112,10 +112,12 @@ export default { ...@@ -112,10 +112,12 @@ export default {
); );
}, },
unresolvedIssues(report) { unresolvedIssues(report) {
return report.existing_failures.concat(report.existing_errors); return [
}, ...report.new_failures,
newIssues(report) { ...report.new_errors,
return report.new_failures.concat(report.new_errors); ...report.existing_failures,
...report.existing_errors,
];
}, },
resolvedIssues(report) { resolvedIssues(report) {
return report.resolved_failures.concat(report.resolved_errors); return report.resolved_failures.concat(report.resolved_errors);
...@@ -178,11 +180,10 @@ export default { ...@@ -178,11 +180,10 @@ export default {
</div> </div>
</template> </template>
</summary-row> </summary-row>
<issues-list <grouped-issues-list
v-if="shouldRenderIssuesList(report)" v-if="shouldRenderIssuesList(report)"
:key="`issues-list-${i}`" :key="`issues-list-${i}`"
:unresolved-issues="unresolvedIssues(report)" :unresolved-issues="unresolvedIssues(report)"
:new-issues="newIssues(report)"
:resolved-issues="resolvedIssues(report)" :resolved-issues="resolvedIssues(report)"
:component="$options.componentNames.TestIssueBody" :component="$options.componentNames.TestIssueBody"
:nested-level="2" :nested-level="2"
......
<script> <script>
import { GlBadge, GlSprintf } from '@gitlab/ui'; import { GlBadge, GlButton, GlSprintf } from '@gitlab/ui';
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
export default { export default {
name: 'TestIssueBody', name: 'TestIssueBody',
components: { components: {
GlBadge, GlBadge,
GlButton,
GlSprintf, GlSprintf,
IssueStatusIcon,
}, },
props: { props: {
issue: { issue: {
type: Object, type: Object,
required: true, required: true,
}, },
// failed || success
status: {
type: String,
required: true,
},
isNew: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
showRecentFailures() { showRecentFailures() {
return this.issue.recent_failures?.count && this.issue.recent_failures?.base_branch; return this.issue.recent_failures?.count && this.issue.recent_failures?.base_branch;
}, },
status() {
return this.issue.status || 'unknown';
},
}, },
methods: { methods: {
...mapActions(['openModal']), ...mapActions(['openModal']),
...@@ -35,30 +31,30 @@ export default { ...@@ -35,30 +31,30 @@ export default {
}; };
</script> </script>
<template> <template>
<div class="report-block-list-issue-description gl-mt-2 gl-mb-2"> <div class="gl-display-flex gl-mt-2 gl-mb-2">
<div class="report-block-list-issue-description-text" data-testid="test-issue-body-description"> <issue-status-icon :status="status" :status-icon-size="24" class="gl-mr-3" />
<button <div data-testid="test-issue-body-description">
type="button" <gl-badge v-if="showRecentFailures" variant="warning" class="gl-mr-2">
class="btn-link btn-blank text-left break-link vulnerability-name-button" <gl-sprintf
:message="
n__(
'Reports|Failed %{count} time in %{base_branch} in the last 14 days',
'Reports|Failed %{count} times in %{base_branch} in the last 14 days',
issue.recent_failures.count,
)
"
>
<template #count>{{ issue.recent_failures.count }}</template>
<template #base_branch>{{ issue.recent_failures.base_branch }}</template>
</gl-sprintf>
</gl-badge>
<gl-button
button-text-classes="gl-white-space-normal! gl-word-break-all gl-text-left"
variant="link"
@click="openModal({ issue })" @click="openModal({ issue })"
> >
<gl-badge v-if="isNew" variant="danger" class="gl-mr-2">{{ s__('New') }}</gl-badge>
<gl-badge v-if="showRecentFailures" variant="warning" class="gl-mr-2">
<gl-sprintf
:message="
n__(
'Reports|Failed %{count} time in %{base_branch} in the last 14 days',
'Reports|Failed %{count} times in %{base_branch} in the last 14 days',
issue.recent_failures.count,
)
"
>
<template #count>{{ issue.recent_failures.count }}</template>
<template #base_branch>{{ issue.recent_failures.base_branch }}</template>
</gl-sprintf>
</gl-badge>
{{ issue.name }} {{ issue.name }}
</button> </gl-button>
</div> </div>
</div> </div>
</template> </template>
---
title: Add section headers for the test report widget on the merge request page
merge_request: 56252
author:
type: added
...@@ -43,6 +43,8 @@ describe('Grouped test reports app', () => { ...@@ -43,6 +43,8 @@ describe('Grouped test reports app', () => {
const findExpandButton = () => wrapper.find('[data-testid="report-section-expand-button"]'); const findExpandButton = () => wrapper.find('[data-testid="report-section-expand-button"]');
const findFullTestReportLink = () => wrapper.find('[data-testid="group-test-reports-full-link"]'); const findFullTestReportLink = () => wrapper.find('[data-testid="group-test-reports-full-link"]');
const findSummaryDescription = () => wrapper.find('[data-testid="summary-row-description"]'); const findSummaryDescription = () => wrapper.find('[data-testid="summary-row-description"]');
const findIssueListUnresolvedHeading = () => wrapper.find('[data-testid="unresolvedHeading"]');
const findIssueListResolvedHeading = () => wrapper.find('[data-testid="resolvedHeading"]');
const findIssueDescription = () => wrapper.find('[data-testid="test-issue-body-description"]'); const findIssueDescription = () => wrapper.find('[data-testid="test-issue-body-description"]');
const findAllIssueDescriptions = () => const findAllIssueDescriptions = () =>
wrapper.findAll('[data-testid="test-issue-body-description"]'); wrapper.findAll('[data-testid="test-issue-body-description"]');
...@@ -133,6 +135,10 @@ describe('Grouped test reports app', () => { ...@@ -133,6 +135,10 @@ describe('Grouped test reports app', () => {
mountComponent(); mountComponent();
}); });
it('renders New heading', () => {
expect(findIssueListUnresolvedHeading().text()).toBe('New');
});
it('renders failed summary text', () => { it('renders failed summary text', () => {
expect(findHeader().text()).toBe('Test summary contained 2 failed out of 11 total tests'); expect(findHeader().text()).toBe('Test summary contained 2 failed out of 11 total tests');
}); });
...@@ -144,7 +150,6 @@ describe('Grouped test reports app', () => { ...@@ -144,7 +150,6 @@ describe('Grouped test reports app', () => {
}); });
it('renders failed issue in list', () => { it('renders failed issue in list', () => {
expect(findIssueDescription().text()).toContain('New');
expect(findIssueDescription().text()).toContain( expect(findIssueDescription().text()).toContain(
'Test#sum when a is 1 and b is 2 returns summary', 'Test#sum when a is 1 and b is 2 returns summary',
); );
...@@ -157,6 +162,10 @@ describe('Grouped test reports app', () => { ...@@ -157,6 +162,10 @@ describe('Grouped test reports app', () => {
mountComponent(); mountComponent();
}); });
it('renders New heading', () => {
expect(findIssueListUnresolvedHeading().text()).toBe('New');
});
it('renders error summary text', () => { it('renders error summary text', () => {
expect(findHeader().text()).toBe('Test summary contained 2 errors out of 11 total tests'); expect(findHeader().text()).toBe('Test summary contained 2 errors out of 11 total tests');
}); });
...@@ -168,7 +177,6 @@ describe('Grouped test reports app', () => { ...@@ -168,7 +177,6 @@ describe('Grouped test reports app', () => {
}); });
it('renders error issue in list', () => { it('renders error issue in list', () => {
expect(findIssueDescription().text()).toContain('New');
expect(findIssueDescription().text()).toContain( expect(findIssueDescription().text()).toContain(
'Test#sum when a is 1 and b is 2 returns summary', 'Test#sum when a is 1 and b is 2 returns summary',
); );
...@@ -181,6 +189,11 @@ describe('Grouped test reports app', () => { ...@@ -181,6 +189,11 @@ describe('Grouped test reports app', () => {
mountComponent(); mountComponent();
}); });
it('renders New and Fixed headings', () => {
expect(findIssueListUnresolvedHeading().text()).toBe('New');
expect(findIssueListResolvedHeading().text()).toBe('Fixed');
});
it('renders summary text', () => { it('renders summary text', () => {
expect(findHeader().text()).toBe( expect(findHeader().text()).toBe(
'Test summary contained 2 failed and 2 fixed test results out of 11 total tests', 'Test summary contained 2 failed and 2 fixed test results out of 11 total tests',
...@@ -194,7 +207,6 @@ describe('Grouped test reports app', () => { ...@@ -194,7 +207,6 @@ describe('Grouped test reports app', () => {
}); });
it('renders failed issue in list', () => { it('renders failed issue in list', () => {
expect(findIssueDescription().text()).toContain('New');
expect(findIssueDescription().text()).toContain( expect(findIssueDescription().text()).toContain(
'Test#subtract when a is 2 and b is 1 returns correct result', 'Test#subtract when a is 2 and b is 1 returns correct result',
); );
...@@ -207,6 +219,10 @@ describe('Grouped test reports app', () => { ...@@ -207,6 +219,10 @@ describe('Grouped test reports app', () => {
mountComponent(); mountComponent();
}); });
it('renders Fixed heading', () => {
expect(findIssueListResolvedHeading().text()).toBe('Fixed');
});
it('renders summary text', () => { it('renders summary text', () => {
expect(findHeader().text()).toBe( expect(findHeader().text()).toBe(
'Test summary contained 4 fixed test results out of 11 total tests', 'Test summary contained 4 fixed test results out of 11 total tests',
......
import Vue from 'vue'; import { GlBadge, GlButton } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper'; import Vuex from 'vuex';
import component from '~/reports/components/test_issue_body.vue'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import createStore from '~/reports/store'; import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
import { issue } from '../mock_data/mock_data'; import TestIssueBody from '~/reports/components/test_issue_body.vue';
import { failedIssue, successIssue } 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(() => { const localVue = createLocalVue();
vm.$destroy(); localVue.use(Vuex);
});
describe('on click', () => { describe('Test issue body', () => {
it('calls openModal action', () => { let wrapper;
vm = mountComponentWithStore(Component, { let store;
store,
props: commonProps,
});
jest.spyOn(vm, 'openModal').mockImplementation(() => {}); const findDescription = () => wrapper.findByTestId('test-issue-body-description');
const findStatusIcon = () => wrapper.findComponent(IssueStatusIcon);
const findBadge = () => wrapper.findComponent(GlBadge);
vm.$el.querySelector('button').click(); const actionSpies = {
openModal: jest.fn(),
};
expect(vm.openModal).toHaveBeenCalledWith({ const createComponent = ({ issue = failedIssue } = {}) => {
issue: commonProps.issue, store = new Vuex.Store({
}); actions: actionSpies,
}); });
wrapper = extendedWrapper(
shallowMount(TestIssueBody, {
store,
localVue,
propsData: {
issue,
},
stubs: {
GlBadge,
GlButton,
IssueStatusIcon,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
}); });
describe('is new', () => { describe('when issue has failed status', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponentWithStore(Component, { createComponent();
store,
props: { ...commonProps, isNew: true },
});
}); });
it('renders issue name', () => { it('renders issue name', () => {
expect(vm.$el.textContent).toContain(commonProps.issue.name); expect(findDescription().text()).toContain(failedIssue.name);
});
it('renders failed status icon', () => {
expect(findStatusIcon().props('status')).toBe('failed');
}); });
it('renders new badge', () => { describe('when issue has recent failures', () => {
expect(trimText(vm.$el.querySelector('.badge').textContent)).toEqual('New'); it('renders recent failures badge', () => {
expect(findBadge().exists()).toBe(true);
});
}); });
}); });
describe('not new', () => { describe('when issue has success status', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponentWithStore(Component, { createComponent({ issue: successIssue });
store, });
props: commonProps,
}); it('does not render recent failures', () => {
expect(findBadge().exists()).toBe(false);
}); });
it('renders issue name', () => { it('renders issue name', () => {
expect(vm.$el.textContent).toContain(commonProps.issue.name); expect(findDescription().text()).toContain(successIssue.name);
});
it('renders success status icon', () => {
expect(findStatusIcon().props('status')).toBe('success');
}); });
});
describe('when clicking on an issue', () => {
it('calls openModal action', () => {
createComponent();
wrapper.findComponent(GlButton).trigger('click');
it('does not renders new badge', () => { expect(actionSpies.openModal).toHaveBeenCalled();
expect(vm.$el.querySelector('.badge')).toEqual(null);
}); });
}); });
}); });
export const issue = { export const failedIssue = {
result: 'failure', result: 'failure',
name: 'Test#sum when a is 1 and b is 2 returns summary', name: 'Test#sum when a is 1 and b is 2 returns summary',
execution_time: 0.009411, execution_time: 0.009411,
status: 'failed',
system_output: 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'", "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'",
recent_failures: {
count: 3,
base_branch: 'master',
},
};
export const successIssue = {
result: 'success',
name: 'Test#sum when a is 1 and b is 2 returns summary',
execution_time: 0.009411,
status: 'success',
system_output: null,
recent_failures: null,
}; };
export const failedReport = { export const failedReport = {
......
import * as types from '~/reports/store/mutation_types'; import * as types from '~/reports/store/mutation_types';
import mutations from '~/reports/store/mutations'; import mutations from '~/reports/store/mutations';
import state from '~/reports/store/state'; import state from '~/reports/store/state';
import { issue } from '../mock_data/mock_data'; import { failedIssue } from '../mock_data/mock_data';
describe('Reports Store Mutations', () => { describe('Reports Store Mutations', () => {
let stateCopy; let stateCopy;
...@@ -115,17 +115,17 @@ describe('Reports Store Mutations', () => { ...@@ -115,17 +115,17 @@ describe('Reports Store Mutations', () => {
describe('SET_ISSUE_MODAL_DATA', () => { describe('SET_ISSUE_MODAL_DATA', () => {
beforeEach(() => { beforeEach(() => {
mutations[types.SET_ISSUE_MODAL_DATA](stateCopy, { mutations[types.SET_ISSUE_MODAL_DATA](stateCopy, {
issue, issue: failedIssue,
}); });
}); });
it('should set modal title', () => { it('should set modal title', () => {
expect(stateCopy.modal.title).toEqual(issue.name); expect(stateCopy.modal.title).toEqual(failedIssue.name);
}); });
it('should set modal data', () => { it('should set modal data', () => {
expect(stateCopy.modal.data.execution_time.value).toEqual(issue.execution_time); expect(stateCopy.modal.data.execution_time.value).toEqual(failedIssue.execution_time);
expect(stateCopy.modal.data.system_output.value).toEqual(issue.system_output); expect(stateCopy.modal.data.system_output.value).toEqual(failedIssue.system_output);
}); });
it('should open modal', () => { it('should open modal', () => {
...@@ -136,7 +136,7 @@ describe('Reports Store Mutations', () => { ...@@ -136,7 +136,7 @@ describe('Reports Store Mutations', () => {
describe('RESET_ISSUE_MODAL_DATA', () => { describe('RESET_ISSUE_MODAL_DATA', () => {
beforeEach(() => { beforeEach(() => {
mutations[types.SET_ISSUE_MODAL_DATA](stateCopy, { mutations[types.SET_ISSUE_MODAL_DATA](stateCopy, {
issue, issue: failedIssue,
}); });
mutations[types.RESET_ISSUE_MODAL_DATA](stateCopy); mutations[types.RESET_ISSUE_MODAL_DATA](stateCopy);
......
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