Commit 3ba01aa2 authored by Miguel Rincon's avatar Miguel Rincon

Merge branch '215140-graph-the-average-code-coverage-of-all-of-a-group-s-projects' into 'master'

Resolve "Graph the average code coverage of all of a group's projects"

See merge request gitlab-org/gitlab!53319
parents a617e2f1 78d2f78e
......@@ -854,17 +854,7 @@ With [GitLab Issue Analytics](issues_analytics/index.md), you can see a bar char
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/263478) in GitLab 13.6.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/276003) in GitLab 13.7.
With [GitLab Repositories Analytics](repositories_analytics/index.md), you can download a CSV of the latest coverage data for all the projects in your group.
### Check code coverage for all projects
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/263478) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.7.
See the overall activity of all projects with code coverage with [GitLab Repositories Analytics](repositories_analytics/index.md).
It displays the current code coverage data available for your projects:
![Group repositories analytics](img/group_code_coverage_analytics_v13_7.png)
With [GitLab Repositories Analytics](repositories_analytics/index.md), you can view overall activity of all projects with code coverage.
## Dependency Proxy
......
......@@ -12,6 +12,25 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
![Group repositories analytics](../img/group_code_coverage_analytics_v13_9.png)
## Current group code coverage
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/263478) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.7.
The **Analytics > Repositories** group page displays the overall test coverage of all your projects in your group.
In the **Overall activity** section, you can see:
- The number of projects with coverage reports.
- The average percentage of coverage across all your projects.
- The total number of pipeline jobs that produce coverage reports.
## Average group test coverage from the last 30 days
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/215140) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.9.
The **Analytics > Repositories** group page displays the average test coverage of all your projects in your group in a graph for the last 30 days.
## Latest project test coverage list
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/267624) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.6.
......
......@@ -24,7 +24,7 @@ export default {
<h4 data-testid="test-coverage-header">
{{ $options.text.codeCoverageHeader }}
</h4>
<test-coverage-summary />
<test-coverage-summary class="gl-mb-5" />
<test-coverage-table class="gl-mb-5" />
<download-test-coverage />
</div>
......
<script>
import chartEmptyStateIllustration from '@gitlab/svgs/dist/illustrations/chart-empty-state.svg';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { GlCard, GlSprintf, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import MetricCard from '~/analytics/shared/components/metric_card.vue';
import { formatDate } from '~/lib/utils/datetime_utility';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format';
import getGroupTestCoverage from '../graphql/queries/get_group_test_coverage.query.graphql';
const formatPercent = getFormatter(SUPPORTED_FORMATS.percentHundred);
export default {
name: 'TestCoverageSummary',
components: {
ChartSkeletonLoader,
GlAreaChart,
GlCard,
GlSprintf,
MetricCard,
},
directives: {
SafeHtml,
},
inject: {
groupFullPath: {
default: '',
......@@ -17,26 +32,34 @@ export default {
group: {
query: getGroupTestCoverage,
variables() {
const ONE_WEEK = 7 * 24 * 60 * 60 * 1000; // milliseconds
const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000; // milliseconds
return {
groupFullPath: this.groupFullPath,
startDate: new Date(Date.now() - ONE_WEEK),
startDate: formatDate(new Date(Date.now() - THIRTY_DAYS), 'yyyy-mm-dd'),
};
},
result(res) {
result({ data }) {
const groupCoverage = data.group.codeCoverageActivities.nodes;
const { projectCount, averageCoverage, coverageCount } =
res.data?.group?.codeCoverageActivities?.nodes?.[0] || {};
groupCoverage?.[groupCoverage.length - 1] || {};
this.projectCount = projectCount;
this.averageCoverage = averageCoverage;
this.coverageCount = coverageCount;
this.groupCoverageChartData = [
{
name: this.$options.i18n.graphName,
data: groupCoverage.map((coverage) => [coverage.date, coverage.averageCoverage]),
},
];
},
error() {
this.hasError = true;
this.projectCount = null;
this.averageCoverage = null;
this.coverageCount = null;
this.groupCoverageChartData = [];
},
watchLoading(isLoading) {
this.isLoading = isLoading;
......@@ -48,37 +71,135 @@ export default {
projectCount: null,
averageCoverage: null,
coverageCount: null,
groupCoverageChartData: [],
coveragePercentage: null,
tooltipTitle: null,
hasError: false,
isLoading: false,
};
},
i18n: {
cardTitle: __('Overall Activity'),
},
computed: {
isChartEmpty() {
return !this.groupCoverageChartData?.[0]?.data?.length;
},
metrics() {
return [
{
key: 'projectCount',
value: this.projectCount,
label: s__('RepositoriesAnalytics|Projects with Coverage'),
label: this.$options.i18n.metrics.projectCountLabel,
},
{
key: 'averageCoverage',
value: this.averageCoverage,
unit: '%',
label: s__('RepositoriesAnalytics|Average Coverage by Job'),
label: this.$options.i18n.metrics.averageCoverageLabel,
},
{
key: 'coverageCount',
value: this.coverageCount,
label: s__('RepositoriesAnalytics|Jobs with Coverage'),
label: this.$options.i18n.metrics.coverageCountLabel,
},
];
},
chartOptions() {
return {
xAxis: {
name: this.$options.i18n.xAxisName,
type: 'time',
axisLabel: {
formatter: (value) => formatDate(value, 'mmm dd'),
},
},
yAxis: {
name: this.$options.i18n.yAxisName,
type: 'value',
min: 0,
max: 100,
axisLabel: {
/**
* We can't do `formatter: formatPercent` because
* formatter passes in a second argument of index, which
* formatPercent takes in as the number of decimal points
* we should include after. This formats 100 as 100.00000%
* instead of 100%.
*/
formatter: (value) => formatPercent(value),
},
},
};
},
},
methods: {
formatTooltipText(params) {
this.tooltipTitle = formatDate(params.value, 'mmm dd');
this.coveragePercentage = formatPercent(params.seriesData?.[0]?.data?.[1], 2);
},
},
i18n: {
emptyChart: s__('RepositoriesAnalytics|No test coverage to display'),
graphCardHeader: s__('RepositoriesAnalytics|Average test coverage last 30 days'),
yAxisName: __('Coverage'),
xAxisName: __('Date'),
graphName: s__('RepositoriesAnalytics|Average coverage'),
graphTooltipMessage: __('Code Coverage: %{coveragePercentage}'),
metrics: {
cardTitle: s__('RepositoriesAnalytics|Overall activity'),
projectCountLabel: s__('RepositoriesAnalytics|Projects with Coverage'),
averageCoverageLabel: s__('RepositoriesAnalytics|Average Coverage by Job'),
coverageCountLabel: s__('RepositoriesAnalytics|Jobs with Coverage'),
},
},
chartEmptyStateIllustration,
};
</script>
<template>
<metric-card :title="$options.i18n.cardTitle" :metrics="metrics" :is-loading="isLoading" />
<div>
<metric-card
:title="$options.i18n.metrics.cardTitle"
:metrics="metrics"
:is-loading="isLoading"
/>
<gl-card>
<template #header>
<h5>{{ $options.i18n.graphCardHeader }}</h5>
</template>
<chart-skeleton-loader v-if="isLoading" data-testid="group-coverage-chart-loading" />
<div
v-else-if="isChartEmpty"
class="d-flex flex-column justify-content-center gl-my-7"
data-testid="group-coverage-chart-empty"
>
<div
v-safe-html="$options.chartEmptyStateIllustration"
class="gl-my-5 svg-w-100 d-flex align-items-center"
data-testid="chart-empty-state-illustration"
></div>
<h5 class="text-center">{{ $options.i18n.emptyChart }}</h5>
</div>
<gl-area-chart
v-else
:data="groupCoverageChartData"
:option="chartOptions"
:include-legend-avg-max="false"
:format-tooltip-text="formatTooltipText"
data-testid="group-coverage-chart"
>
<template #tooltip-title>
{{ tooltipTitle }}
</template>
<template #tooltip-content>
<gl-sprintf :message="$options.i18n.graphTooltipMessage">
<template #coveragePercentage>
{{ coveragePercentage }}
</template>
</gl-sprintf>
</template>
</gl-area-chart>
</gl-card>
</div>
</template>
query getGroupTestCoverage($groupFullPath: ID!, $startDate: Date!, $last: Int = 1) {
query getGroupTestCoverage($groupFullPath: ID!, $startDate: Date!) {
group(fullPath: $groupFullPath) {
codeCoverageActivities(startDate: $startDate, last: $last) {
codeCoverageActivities(startDate: $startDate) {
nodes {
projectCount
averageCoverage
......
---
title: Adds a historical coverage graph to the group repositories analytics page
merge_request: 53319
author:
type: added
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Test coverage table component when group code coverage is available renders area chart with correct data 1`] = `
Array [
Object {
"data": Array [
Array [
"2020-01-10",
77.9,
],
Array [
"2020-01-11",
78.7,
],
Array [
"2020-01-12",
79.6,
],
],
"name": "test",
},
]
`;
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import TestCoverageSummary from 'ee/analytics/repository_analytics/components/test_coverage_summary.vue';
import getGroupTestCoverage from 'ee/analytics/repository_analytics/graphql/queries/get_group_test_coverage.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import MetricCard from '~/analytics/shared/components/metric_card.vue';
const localVue = createLocalVue();
describe('Test coverage table component', () => {
let wrapper;
let fakeApollo;
const findProjectsWithTests = () => wrapper.find('.js-metric-card-item:nth-child(1) h3');
const findAverageCoverage = () => wrapper.find('.js-metric-card-item:nth-child(2) h3');
const findTotalCoverages = () => wrapper.find('.js-metric-card-item:nth-child(3) h3');
const findGroupCoverageChart = () => wrapper.findByTestId('group-coverage-chart');
const findChartLoadingState = () => wrapper.findByTestId('group-coverage-chart-loading');
const findChartEmptyState = () => wrapper.findByTestId('group-coverage-chart-empty');
const findLoadingState = () => wrapper.find(GlSkeletonLoading);
const createComponent = ({ data = {} } = {}, withApollo = false) => {
fakeApollo = createMockApollo([[getGroupTestCoverage, jest.fn().mockResolvedValue()]]);
const props = {
localVue,
data() {
return {
projectCount: null,
averageCoverage: null,
coverageCount: null,
hasError: false,
isLoading: false,
...data,
};
},
};
if (withApollo) {
localVue.use(VueApollo);
props.apolloProvider = fakeApollo;
} else {
props.mocks = {
$apollo: {
queries: {
group: {
query: jest.fn().mockResolvedValue(),
const createComponent = ({ data = {} } = {}) => {
wrapper = extendedWrapper(
shallowMount(TestCoverageSummary, {
localVue,
data() {
return {
projectCount: null,
averageCoverage: null,
coverageCount: null,
hasError: false,
isLoading: false,
...data,
};
},
mocks: {
$apollo: {
queries: {
group: {
query: jest.fn().mockResolvedValue(),
},
},
},
},
};
}
wrapper = mount(TestCoverageSummary, props);
stubs: {
MetricCard,
},
}),
);
};
afterEach(() => {
......@@ -64,6 +60,13 @@ describe('Test coverage table component', () => {
expect(findAverageCoverage().text()).toBe('-');
expect(findTotalCoverages().text()).toBe('-');
});
it('renders empty chart state', () => {
createComponent();
expect(findChartEmptyState().exists()).toBe(true);
expect(findGroupCoverageChart().exists()).toBe(false);
});
});
describe('when query is loading', () => {
......@@ -71,6 +74,7 @@ describe('Test coverage table component', () => {
createComponent({ data: { isLoading: true } });
expect(findLoadingState().exists()).toBe(true);
expect(findChartLoadingState().exists()).toBe(true);
});
});
......@@ -92,29 +96,47 @@ describe('Test coverage table component', () => {
expect(findAverageCoverage().text()).toBe(`${averageCoverage} %`);
expect(findTotalCoverages().text()).toBe(coverageCount);
});
});
describe('when group has no coverage', () => {
it('renders empty metrics', async () => {
it('renders area chart with correct data', () => {
createComponent({
withApollo: true,
data: {},
queryData: {
data: {
group: {
codeCoverageActivities: {
nodes: [],
},
data: {
groupCoverageChartData: [
{
name: 'test',
data: [
['2020-01-10', 77.9],
['2020-01-11', 78.7],
['2020-01-12', 79.6],
],
},
},
],
},
});
jest.runOnlyPendingTimers();
await waitForPromises();
expect(findProjectsWithTests().text()).toBe('-');
expect(findAverageCoverage().text()).toBe('-');
expect(findTotalCoverages().text()).toBe('-');
expect(findGroupCoverageChart().exists()).toBe(true);
expect(findGroupCoverageChart().props('data')).toMatchSnapshot();
});
it('formats the area chart labels correctly', () => {
createComponent({
data: {
groupCoverageChartData: [
{
name: 'test',
data: [
['2020-01-10', 77.9],
['2020-01-11', 78.7],
['2020-01-12', 79.6],
],
},
],
},
});
expect(findGroupCoverageChart().props('option').xAxis.axisLabel.formatter('2020-01-10')).toBe(
'Jan 10',
);
expect(findGroupCoverageChart().props('option').yAxis.axisLabel.formatter(80)).toBe('80%');
});
});
});
......@@ -7199,6 +7199,9 @@ msgstr ""
msgid "Code"
msgstr ""
msgid "Code Coverage: %{coveragePercentage}"
msgstr ""
msgid "Code Coverage: %{coveragePercentage}%{percentSymbol}"
msgstr ""
......@@ -20931,9 +20934,6 @@ msgstr ""
msgid "Outdent"
msgstr ""
msgid "Overall Activity"
msgstr ""
msgid "Overridden"
msgstr ""
......@@ -24834,6 +24834,12 @@ msgstr ""
msgid "RepositoriesAnalytics|Average Coverage by Job"
msgstr ""
msgid "RepositoriesAnalytics|Average coverage"
msgstr ""
msgid "RepositoriesAnalytics|Average test coverage last 30 days"
msgstr ""
msgid "RepositoriesAnalytics|Coverage"
msgstr ""
......@@ -24861,6 +24867,12 @@ msgstr ""
msgid "RepositoriesAnalytics|Latest test coverage results"
msgstr ""
msgid "RepositoriesAnalytics|No test coverage to display"
msgstr ""
msgid "RepositoriesAnalytics|Overall activity"
msgstr ""
msgid "RepositoriesAnalytics|Please select a project or multiple projects to display their most recent test coverage data."
msgstr ""
......
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