Commit 3ba07f2a authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '241759-test-history-mr-widget-frontend' into 'master'

Add test history to test report MR widget

See merge request gitlab-org/gitlab!45953
parents 7145daf0 9d42f70e
...@@ -11,7 +11,12 @@ import Modal from './modal.vue'; ...@@ -11,7 +11,12 @@ import Modal from './modal.vue';
import createStore from '../store'; import createStore from '../store';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { summaryTextBuilder, reportTextBuilder, statusIcon } from '../store/utils'; import {
summaryTextBuilder,
reportTextBuilder,
statusIcon,
recentFailuresTextBuilder,
} from '../store/utils';
export default { export default {
name: 'GroupedTestReportsApp', name: 'GroupedTestReportsApp',
...@@ -86,6 +91,12 @@ export default { ...@@ -86,6 +91,12 @@ export default {
return reportTextBuilder(name, summary); return reportTextBuilder(name, summary);
}, },
hasRecentFailures(summary) {
return this.glFeatures.testFailureHistory && summary?.recentlyFailed > 0;
},
recentFailuresText(summary) {
return recentFailuresTextBuilder(summary);
},
getReportIcon(report) { getReportIcon(report) {
return statusIcon(report.status); return statusIcon(report.status);
}, },
...@@ -134,14 +145,22 @@ export default { ...@@ -134,14 +145,22 @@ export default {
{{ s__('ciReport|View full report') }} {{ s__('ciReport|View full report') }}
</gl-button> </gl-button>
</template> </template>
<template v-if="hasRecentFailures(summary)" #subHeading>
{{ recentFailuresText(summary) }}
</template>
<template #body> <template #body>
<div class="mr-widget-grouped-section report-block"> <div class="mr-widget-grouped-section report-block">
<template v-for="(report, i) in reports"> <template v-for="(report, i) in reports">
<summary-row <summary-row :key="`summary-row-${i}`" :status-icon="getReportIcon(report)">
:key="`summary-row-${i}`" <template #summary>
:summary="reportText(report)" <div class="gl-display-inline-flex gl-flex-direction-column">
:status-icon="getReportIcon(report)" <div>{{ reportText(report) }}</div>
/> <div v-if="hasRecentFailures(report.summary)">
{{ recentFailuresText(report.summary) }}
</div>
</div>
</template>
</summary-row>
<issues-list <issues-list
v-if="shouldRenderIssuesList(report)" v-if="shouldRenderIssuesList(report)"
:key="`issues-list-${i}`" :key="`issues-list-${i}`"
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { GlBadge } from '@gitlab/ui';
import { n__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default { export default {
name: 'TestIssueBody', name: 'TestIssueBody',
components: {
GlBadge,
},
mixins: [glFeatureFlagsMixin()],
props: { props: {
issue: { issue: {
type: Object, type: Object,
...@@ -19,8 +26,20 @@ export default { ...@@ -19,8 +26,20 @@ export default {
default: false, default: false,
}, },
}, },
computed: {
showRecentFailures() {
return this.glFeatures.testFailureHistory && this.issue.recent_failures;
},
},
methods: { methods: {
...mapActions(['openModal']), ...mapActions(['openModal']),
recentFailuresText(count) {
return n__(
'Failed %d time in the last 14 days',
'Failed %d times in the last 14 days',
count,
);
},
}, },
}; };
</script> </script>
...@@ -32,7 +51,10 @@ export default { ...@@ -32,7 +51,10 @@ export default {
class="btn-link btn-blank text-left break-link vulnerability-name-button" class="btn-link btn-blank text-left break-link vulnerability-name-button"
@click="openModal({ issue })" @click="openModal({ issue })"
> >
<div v-if="isNew" class="badge badge-danger gl-mr-2">{{ s__('New') }}</div> <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">
{{ recentFailuresText(issue.recent_failures) }}
</gl-badge>
{{ issue.name }} {{ issue.name }}
</button> </button>
</div> </div>
......
import * as types from './mutation_types'; import * as types from './mutation_types';
import { countRecentlyFailedTests } from './utils';
export default { export default {
[types.SET_ENDPOINT](state, endpoint) { [types.SET_ENDPOINT](state, endpoint) {
...@@ -16,9 +17,15 @@ export default { ...@@ -16,9 +17,15 @@ export default {
state.summary.resolved = response.summary.resolved; state.summary.resolved = response.summary.resolved;
state.summary.failed = response.summary.failed; state.summary.failed = response.summary.failed;
state.summary.errored = response.summary.errored; state.summary.errored = response.summary.errored;
state.summary.recentlyFailed = countRecentlyFailedTests(response.suites);
state.status = response.status; state.status = response.status;
state.reports = response.suites; state.reports = response.suites;
state.reports.forEach((report, i) => {
if (!state.reports[i].summary) return;
state.reports[i].summary.recentlyFailed = countRecentlyFailedTests(report);
});
}, },
[types.RECEIVE_REPORTS_ERROR](state) { [types.RECEIVE_REPORTS_ERROR](state) {
state.isLoading = false; state.isLoading = false;
...@@ -30,6 +37,7 @@ export default { ...@@ -30,6 +37,7 @@ export default {
resolved: 0, resolved: 0,
failed: 0, failed: 0,
errored: 0, errored: 0,
recentlyFailed: 0,
}; };
state.status = null; state.status = null;
}, },
......
...@@ -48,6 +48,48 @@ export const reportTextBuilder = (name = '', results = {}) => { ...@@ -48,6 +48,48 @@ export const reportTextBuilder = (name = '', results = {}) => {
return sprintf(__('%{name} found %{resultsString}'), { name, resultsString }); return sprintf(__('%{name} found %{resultsString}'), { name, resultsString });
}; };
export const recentFailuresTextBuilder = (summary = {}) => {
const { failed, recentlyFailed } = summary;
if (!failed || !recentlyFailed) return '';
if (failed < 2) {
return sprintf(
s__(
'Reports|%{recentlyFailed} out of %{failed} failed test has failed more than once in the last 14 days',
),
{ recentlyFailed, failed },
);
}
return sprintf(
n__(
s__(
'Reports|%{recentlyFailed} out of %{failed} failed tests has failed more than once in the last 14 days',
),
s__(
'Reports|%{recentlyFailed} out of %{failed} failed tests have failed more than once in the last 14 days',
),
recentlyFailed,
),
{ recentlyFailed, failed },
);
};
export const countRecentlyFailedTests = subject => {
// handle either a single report or an array of reports
const reports = !subject.length ? [subject] : subject;
return reports
.map(report => {
return (
[report.new_failures, report.existing_failures, report.resolved_failures]
// only count tests which have failed more than once
.map(failureArray => failureArray.filter(failure => failure.recent_failures > 1).length)
.reduce((total, count) => total + count, 0)
);
})
.reduce((total, count) => total + count, 0);
};
export const statusIcon = status => { export const statusIcon = status => {
if (status === STATUS_FAILED) { if (status === STATUS_FAILED) {
return ICON_WARNING; return ICON_WARNING;
......
...@@ -42,6 +42,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -42,6 +42,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:default_merge_ref_for_diffs, @project) push_frontend_feature_flag(:default_merge_ref_for_diffs, @project)
push_frontend_feature_flag(:core_security_mr_widget, @project, default_enabled: true) push_frontend_feature_flag(:core_security_mr_widget, @project, default_enabled: true)
push_frontend_feature_flag(:remove_resolve_note, @project, default_enabled: true) push_frontend_feature_flag(:remove_resolve_note, @project, default_enabled: true)
push_frontend_feature_flag(:test_failure_history, @project)
record_experiment_user(:invite_members_version_a) record_experiment_user(:invite_members_version_a)
record_experiment_user(:invite_members_version_b) record_experiment_user(:invite_members_version_b)
......
...@@ -11208,6 +11208,11 @@ msgstr "" ...@@ -11208,6 +11208,11 @@ msgstr ""
msgid "Failed" msgid "Failed"
msgstr "" msgstr ""
msgid "Failed %d time in the last 14 days"
msgid_plural "Failed %d times in the last 14 days"
msgstr[0] ""
msgstr[1] ""
msgid "Failed Jobs" msgid "Failed Jobs"
msgstr "" msgstr ""
...@@ -22688,6 +22693,15 @@ msgstr "" ...@@ -22688,6 +22693,15 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}" msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr "" msgstr ""
msgid "Reports|%{recentlyFailed} out of %{failed} failed test has failed more than once in the last 14 days"
msgstr ""
msgid "Reports|%{recentlyFailed} out of %{failed} failed tests has failed more than once in the last 14 days"
msgstr ""
msgid "Reports|%{recentlyFailed} out of %{failed} failed tests have failed more than once in the last 14 days"
msgstr ""
msgid "Reports|Accessibility scanning detected %d issue for the source branch only" msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only" msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
msgstr[0] "" msgstr[0] ""
......
...@@ -7,6 +7,7 @@ import { getStoreConfig } from '~/reports/store'; ...@@ -7,6 +7,7 @@ import { getStoreConfig } from '~/reports/store';
import { failedReport } from '../mock_data/mock_data'; import { failedReport } from '../mock_data/mock_data';
import successTestReports from '../mock_data/no_failures_report.json'; import successTestReports from '../mock_data/no_failures_report.json';
import newFailedTestReports from '../mock_data/new_failures_report.json'; import newFailedTestReports from '../mock_data/new_failures_report.json';
import recentFailuresTestReports from '../mock_data/recent_failures_report.json';
import newErrorsTestReports from '../mock_data/new_errors_report.json'; import newErrorsTestReports from '../mock_data/new_errors_report.json';
import mixedResultsTestReports from '../mock_data/new_and_fixed_failures_report.json'; import mixedResultsTestReports from '../mock_data/new_and_fixed_failures_report.json';
import resolvedFailures from '../mock_data/resolved_failures.json'; import resolvedFailures from '../mock_data/resolved_failures.json';
...@@ -21,7 +22,7 @@ describe('Grouped test reports app', () => { ...@@ -21,7 +22,7 @@ describe('Grouped test reports app', () => {
let wrapper; let wrapper;
let mockStore; let mockStore;
const mountComponent = ({ props = { pipelinePath } } = {}) => { const mountComponent = ({ props = { pipelinePath }, testFailureHistory = false } = {}) => {
wrapper = mount(Component, { wrapper = mount(Component, {
store: mockStore, store: mockStore,
localVue, localVue,
...@@ -30,6 +31,11 @@ describe('Grouped test reports app', () => { ...@@ -30,6 +31,11 @@ describe('Grouped test reports app', () => {
pipelinePath, pipelinePath,
...props, ...props,
}, },
provide: {
glFeatures: {
testFailureHistory,
},
},
}); });
}; };
...@@ -234,6 +240,77 @@ describe('Grouped test reports app', () => { ...@@ -234,6 +240,77 @@ describe('Grouped test reports app', () => {
}); });
}); });
describe('recent failures counts', () => {
describe('with recent failures counts', () => {
beforeEach(() => {
setReports(recentFailuresTestReports);
});
describe('with feature flag enabled', () => {
beforeEach(() => {
mountComponent({ testFailureHistory: true });
});
it('renders the recently failed tests summary', () => {
expect(findHeader().text()).toContain(
'2 out of 3 failed tests have failed more than once in the last 14 days',
);
});
it('renders the recently failed count on the test suite', () => {
expect(findSummaryDescription().text()).toContain(
'1 out of 2 failed tests has failed more than once in the last 14 days',
);
});
it('renders the recent failures count on the test case', () => {
expect(findIssueDescription().text()).toContain('Failed 8 times in the last 14 days');
});
});
describe('with feature flag disabled', () => {
beforeEach(() => {
mountComponent({ testFailureHistory: false });
});
it('does not render the recently failed tests summary', () => {
expect(findHeader().text()).not.toContain('failed more than once in the last 14 days');
});
it('does not render the recently failed count on the test suite', () => {
expect(findSummaryDescription().text()).not.toContain(
'failed more than once in the last 14 days',
);
});
it('renders the recent failures count on the test case', () => {
expect(findIssueDescription().text()).not.toContain('in the last 14 days');
});
});
});
describe('without recent failures counts', () => {
beforeEach(() => {
setReports(mixedResultsTestReports);
mountComponent();
});
it('does not render the recently failed tests summary', () => {
expect(findHeader().text()).not.toContain('failed more than once in the last 14 days');
});
it('does not render the recently failed count on the test suite', () => {
expect(findSummaryDescription().text()).not.toContain(
'failed more than once in the last 14 days',
);
});
it('does not render the recent failures count on the test case', () => {
expect(findIssueDescription().text()).not.toContain('in the last 14 days');
});
});
});
describe('with a report that failed to load', () => { describe('with a report that failed to load', () => {
beforeEach(() => { beforeEach(() => {
setReports(failedReport); setReports(failedReport);
......
{
"summary": { "total": 11, "resolved": 0, "errored": 0, "failed": 3, "recentlyFailed": 2 },
"suites": [
{
"name": "rspec:pg",
"summary": { "total": 8, "resolved": 0, "errored": 0, "failed": 2, "recentlyFailed": 1 },
"new_failures": [
{
"result": "failure",
"name": "Test#sum when a is 1 and b is 2 returns summary",
"execution_time": 0.009411,
"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 <top (required)>'",
"recent_failures": 8
},
{
"result": "failure",
"name": "Test#sum when a is 100 and b is 200 returns summary",
"execution_time": 0.000162,
"system_output": "Failure/Error: is_expected.to eq(300)\n\n expected: 300\n got: -100\n\n (compared using ==)\n./spec/test_spec.rb:21:in `block (4 levels) in <top (required)>'"
}
],
"resolved_failures": [],
"existing_failures": [],
"new_errors": [],
"resolved_errors": [],
"existing_errors": []
},
{
"name": "java ant",
"summary": { "total": 3, "resolved": 0, "errored": 0, "failed": 1, "recentlyFailed": 1 },
"new_failures": [
{
"result": "failure",
"name": "Test#sum when a is 100 and b is 200 returns summary",
"execution_time": 0.000562,
"recent_failures": 3
}
],
"resolved_failures": [],
"existing_failures": [],
"new_errors": [],
"resolved_errors": [],
"existing_errors": []
}
]
}
...@@ -46,6 +46,7 @@ describe('Reports Store Mutations', () => { ...@@ -46,6 +46,7 @@ describe('Reports Store Mutations', () => {
name: 'StringHelper#concatenate when a is git and b is lab returns summary', name: 'StringHelper#concatenate when a is git and b is lab returns summary',
execution_time: 0.0092435, execution_time: 0.0092435,
system_output: "Failure/Error: is_expected.to eq('gitlab')", system_output: "Failure/Error: is_expected.to eq('gitlab')",
recent_failures: 4,
}, },
], ],
resolved_failures: [ resolved_failures: [
...@@ -82,6 +83,7 @@ describe('Reports Store Mutations', () => { ...@@ -82,6 +83,7 @@ describe('Reports Store Mutations', () => {
expect(stateCopy.summary.total).toEqual(mockedResponse.summary.total); expect(stateCopy.summary.total).toEqual(mockedResponse.summary.total);
expect(stateCopy.summary.resolved).toEqual(mockedResponse.summary.resolved); expect(stateCopy.summary.resolved).toEqual(mockedResponse.summary.resolved);
expect(stateCopy.summary.failed).toEqual(mockedResponse.summary.failed); expect(stateCopy.summary.failed).toEqual(mockedResponse.summary.failed);
expect(stateCopy.summary.recentlyFailed).toEqual(1);
}); });
it('should set reports', () => { it('should set reports', () => {
......
...@@ -168,6 +168,54 @@ describe('Reports store utils', () => { ...@@ -168,6 +168,54 @@ describe('Reports store utils', () => {
}); });
}); });
describe('recentFailuresTextBuilder', () => {
it.each`
recentlyFailed | failed | expected
${0} | ${1} | ${''}
${1} | ${1} | ${'1 out of 1 failed test has failed more than once in the last 14 days'}
${1} | ${2} | ${'1 out of 2 failed tests has failed more than once in the last 14 days'}
${2} | ${3} | ${'2 out of 3 failed tests have failed more than once in the last 14 days'}
`(
'should render summary for $recentlyFailed out of $failed failures',
({ recentlyFailed, failed, expected }) => {
const result = utils.recentFailuresTextBuilder({ recentlyFailed, failed });
expect(result).toBe(expected);
},
);
});
describe('countRecentlyFailedTests', () => {
it('counts tests with more than one recent failure in a report', () => {
const report = {
new_failures: [{ recent_failures: 2 }],
existing_failures: [{ recent_failures: 1 }],
resolved_failures: [{ recent_failures: 20 }, { recent_failures: 5 }],
};
const result = utils.countRecentlyFailedTests(report);
expect(result).toBe(3);
});
it('counts tests with more than one recent failure in an array of reports', () => {
const reports = [
{
new_failures: [{ recent_failures: 2 }],
existing_failures: [{ recent_failures: 20 }, { recent_failures: 5 }],
resolved_failures: [{ recent_failures: 2 }],
},
{
new_failures: [{ recent_failures: 8 }, { recent_failures: 14 }],
existing_failures: [{ recent_failures: 1 }],
resolved_failures: [{ recent_failures: 7 }, { recent_failures: 5 }],
},
];
const result = utils.countRecentlyFailedTests(reports);
expect(result).toBe(8);
});
});
describe('statusIcon', () => { describe('statusIcon', () => {
describe('with failed status', () => { describe('with failed status', () => {
it('returns ICON_WARNING', () => { it('returns ICON_WARNING', () => {
......
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