Commit 94b009f6 authored by Frédéric Caplette's avatar Frédéric Caplette Committed by Nicolò Maria Mezzopera

Add chart for code coverage over time

In the repository analytics section,
we are adding a graph that show case
the last 3 months of CC values.
parent bf3fcd03
import Vue from 'vue';
import { __ } from '~/locale';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import CodeCoverage from '../components/code_coverage.vue';
import SeriesDataMixin from './series_data_mixin';
document.addEventListener('DOMContentLoaded', () => {
const languagesContainer = document.getElementById('js-languages-chart');
const codeCoverageContainer = document.getElementById('js-code-coverage-chart');
const monthContainer = document.getElementById('js-month-chart');
const weekdayContainer = document.getElementById('js-weekday-chart');
const hourContainer = document.getElementById('js-hour-chart');
......@@ -57,6 +59,18 @@ document.addEventListener('DOMContentLoaded', () => {
},
});
// eslint-disable-next-line no-new
new Vue({
el: codeCoverageContainer,
render(h) {
return h(CodeCoverage, {
props: {
graphEndpoint: codeCoverageContainer.dataset?.graphEndpoint,
},
});
},
});
// eslint-disable-next-line no-new
new Vue({
el: monthContainer,
......
<script>
import { GlAlert, GlDropdown, GlDropdownItem, GlIcon, GlSprintf } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat';
import axios from '~/lib/utils/axios_utils';
import { get } from 'lodash';
import { __ } from '~/locale';
export default {
components: {
GlAlert,
GlAreaChart,
GlDropdown,
GlDropdownItem,
GlIcon,
GlSprintf,
},
props: {
graphEndpoint: {
type: String,
required: true,
},
},
data() {
return {
dailyCoverageData: [],
hasFetchError: false,
isLoading: true,
selectedCoverageIndex: 0,
tooltipTitle: '',
coveragePercentage: '',
chartOptions: {
yAxis: {
name: __('Bi-weekly code coverage'),
type: 'value',
min: 0,
max: 100,
},
xAxis: {
name: '',
type: 'category',
},
},
};
},
computed: {
hasData() {
return this.dailyCoverageData.length > 0;
},
isReady() {
return !this.isLoading && !this.hasFetchError;
},
canShowData() {
return this.isReady && this.hasData;
},
noDataAvailable() {
return this.isReady && !this.hasData;
},
selectedDailyCoverage() {
return this.hasData && this.dailyCoverageData[this.selectedCoverageIndex];
},
selectedDailyCoverageName() {
return this.selectedDailyCoverage?.group_name;
},
formattedData() {
if (this.selectedDailyCoverage?.data) {
return this.selectedDailyCoverage.data.map(value => [
dateFormat(value.date, 'mmm dd'),
value.coverage,
]);
}
// If the fetching failed, we return an empty array which
// allow the graph to render while empty
return [];
},
chartData() {
return [
{
// The default string 'data' will get shown in the legend if we fail to fetch the data
name: this.canShowData ? this.selectedDailyCoverageName : __('data'),
data: this.formattedData,
type: 'line',
smooth: true,
},
];
},
},
created() {
axios
.get(this.graphEndpoint)
.then(({ data }) => {
this.dailyCoverageData = data;
})
.catch(() => {
this.hasFetchError = true;
})
.finally(() => {
this.isLoading = false;
});
},
methods: {
setSelectedCoverage(index) {
this.selectedCoverageIndex = index;
},
formatTooltipText(params) {
this.tooltipTitle = params.value;
this.coveragePercentage = get(params, 'seriesData[0].data[1]', '');
},
},
height: 200,
};
</script>
<template>
<div>
<div class="gl-mt-3 gl-mb-3">
<gl-alert
v-if="hasFetchError"
variant="danger"
:title="s__('Code Coverage|Couldn\'t fetch the code coverage data')"
:dismissible="false"
/>
<gl-alert
v-if="noDataAvailable"
variant="info"
:title="s__('Code Coverage| Empty code coverage data')"
:dismissible="false"
>
<span>
{{ __('It seems that there is currently no available data for code coverage') }}
</span>
</gl-alert>
<gl-dropdown v-if="canShowData" :text="selectedDailyCoverageName">
<gl-dropdown-item
v-for="({ group_name }, index) in dailyCoverageData"
:key="index"
:value="group_name"
@click="setSelectedCoverage(index)"
>
<div class="gl-display-flex">
<gl-icon
v-if="index === selectedCoverageIndex"
name="mobile-issue-close"
class="gl-absolute"
/>
<span class="gl-display-flex align-items-center ml-4">
{{ group_name }}
</span>
</div>
</gl-dropdown-item>
</gl-dropdown>
</div>
<gl-area-chart
v-if="!isLoading"
:height="$options.height"
:data="chartData"
:option="chartOptions"
:format-tooltip-text="formatTooltipText"
>
<template v-if="canShowData" #tooltipTitle>
{{ tooltipTitle }}
</template>
<template v-if="canShowData" #tooltipContent>
<gl-sprintf :message="__('Code Coverage: %{coveragePercentage}%{percentSymbol}')">
<template #coveragePercentage>
{{ coveragePercentage }}
</template>
<template #percentSymbol>
%
</template>
</gl-sprintf>
</template>
</gl-area-chart>
</div>
</template>
......@@ -28,7 +28,7 @@
%a.btn.btn-sm{ href: "#{download_path}?#{@daily_coverage_options[:base_params].to_query}" }
%small
= _("Download raw data (.csv)")
#js-code-coverage-chart{ data: { daily_coverage_options: @daily_coverage_options.to_json.html_safe } }
#js-code-coverage-chart{ data: { graph_endpoint: "#{@daily_coverage_options[:graph_api_path]}?#{@daily_coverage_options[:base_params].to_query}" } }
.repo-charts
.sub-header-block.border-top
......
---
title: Resolve Graph code coverage changes over time for a project
merge_request: 26174
author:
type: added
......@@ -134,15 +134,18 @@ in the jobs table.
A few examples of known coverage tools for a variety of languages can be found
in the pipelines settings page.
### Download test coverage history
### Code Coverage history
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/209121) in GitLab 12.10.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/209121) the ability to download a `.csv` in GitLab 12.10.
> - [Graph introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33743) in GitLab 13.1.
If you want to see the evolution of your project code coverage over time,
you can download a CSV file with this data. From your project:
you can view a graph or download a CSV file with this data. From your project:
1. Go to **{chart}** **Project Analytics > Repository**.
1. Click **Download raw data (`.csv`)**
1. Go to **{chart}** **Project Analytics > Repository** to see the historic data for each job listed in the dropdown above the graph.
1. If you want a CSV file of that data, click **Download raw data (.csv)**
![Code coverage graph of a project over time](img/code_coverage_graph_v13_1.png)
### Removing color codes
......
......@@ -33,6 +33,7 @@ The data in the charts are updated soon after each commit in the default branch.
Available charts:
- Programming languages used in the repository
- Code coverage history (last 3 months) ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33743) in GitLab 13.1)
- Commit statistics (last month)
- Commits per day of month
- Commits per weekday
......
......@@ -3415,6 +3415,9 @@ msgstr ""
msgid "Beta"
msgstr ""
msgid "Bi-weekly code coverage"
msgstr ""
msgid "Billing"
msgstr ""
......@@ -5575,6 +5578,15 @@ msgstr ""
msgid "Code"
msgstr ""
msgid "Code Coverage: %{coveragePercentage}%{percentSymbol}"
msgstr ""
msgid "Code Coverage| Empty code coverage data"
msgstr ""
msgid "Code Coverage|Couldn't fetch the code coverage data"
msgstr ""
msgid "Code Owners"
msgstr ""
......@@ -12512,6 +12524,9 @@ msgstr ""
msgid "It seems like the Dependency Scanning job ran successfully, but no dependencies have been detected in your project."
msgstr ""
msgid "It seems that there is currently no available data for code coverage"
msgstr ""
msgid "It's you"
msgstr ""
......@@ -26707,6 +26722,9 @@ msgstr ""
msgid "customize"
msgstr ""
msgid "data"
msgstr ""
msgid "date must not be after 9999-12-31"
msgstr ""
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Code Coverage when fetching data is successful matches the snapshot 1`] = `
<div>
<div
class="gl-mt-3 gl-mb-3"
>
<!---->
<!---->
<gl-dropdown-stub
text="rspec"
>
<gl-dropdown-item-stub
value="rspec"
>
<div
class="gl-display-flex"
>
<gl-icon-stub
class="gl-absolute"
name="mobile-issue-close"
size="16"
/>
<span
class="gl-display-flex align-items-center ml-4"
>
rspec
</span>
</div>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
value="cypress"
>
<div
class="gl-display-flex"
>
<!---->
<span
class="gl-display-flex align-items-center ml-4"
>
cypress
</span>
</div>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
value="karma"
>
<div
class="gl-display-flex"
>
<!---->
<span
class="gl-display-flex align-items-center ml-4"
>
karma
</span>
</div>
</gl-dropdown-item-stub>
</gl-dropdown-stub>
</div>
<gl-area-chart-stub
annotations=""
data="[object Object]"
formattooltiptext="function () { [native code] }"
height="200"
includelegendavgmax="true"
legendaveragetext="Avg"
legendcurrenttext="Current"
legendlayout="inline"
legendmaxtext="Max"
legendmintext="Min"
option="[object Object]"
thresholds=""
/>
</div>
`;
import MockAdapter from 'axios-mock-adapter';
import { shallowMount } from '@vue/test-utils';
import { GlAlert, GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import axios from '~/lib/utils/axios_utils';
import CodeCoverage from '~/pages/projects/graphs/components/code_coverage.vue';
import codeCoverageMockData from './mock_data';
import waitForPromises from 'helpers/wait_for_promises';
import httpStatusCodes from '~/lib/utils/http_status';
describe('Code Coverage', () => {
let wrapper;
let mockAxios;
const graphEndpoint = '/graph';
const findAlert = () => wrapper.find(GlAlert);
const findAreaChart = () => wrapper.find(GlAreaChart);
const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem);
const findFirstDropdownItem = () => findAllDropdownItems().at(0);
const findSecondDropdownItem = () => findAllDropdownItems().at(1);
const createComponent = () => {
wrapper = shallowMount(CodeCoverage, {
propsData: {
graphEndpoint,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when fetching data is successful', () => {
beforeEach(() => {
mockAxios = new MockAdapter(axios);
mockAxios.onGet().replyOnce(httpStatusCodes.OK, codeCoverageMockData);
createComponent();
return waitForPromises();
});
afterEach(() => {
mockAxios.restore();
});
it('renders the area chart', () => {
expect(findAreaChart().exists()).toBe(true);
});
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('shows no error messages', () => {
expect(findAlert().exists()).toBe(false);
});
});
describe('when fetching data fails', () => {
beforeEach(() => {
mockAxios = new MockAdapter(axios);
mockAxios.onGet().replyOnce(httpStatusCodes.BAD_REQUEST);
createComponent();
return waitForPromises();
});
afterEach(() => {
mockAxios.restore();
});
it('renders an error message', () => {
expect(findAlert().exists()).toBe(true);
expect(findAlert().attributes().variant).toBe('danger');
});
it('still renders an empty graph', () => {
expect(findAreaChart().exists()).toBe(true);
});
});
describe('when fetching data succeed but returns an empty state', () => {
beforeEach(() => {
mockAxios = new MockAdapter(axios);
mockAxios.onGet().replyOnce(httpStatusCodes.OK, []);
createComponent();
return waitForPromises();
});
afterEach(() => {
mockAxios.restore();
});
it('renders an information message', () => {
expect(findAlert().exists()).toBe(true);
expect(findAlert().attributes().variant).toBe('info');
});
it('still renders an empty graph', () => {
expect(findAreaChart().exists()).toBe(true);
});
});
describe('dropdown options', () => {
beforeEach(() => {
mockAxios = new MockAdapter(axios);
mockAxios.onGet().replyOnce(httpStatusCodes.OK, codeCoverageMockData);
createComponent();
return waitForPromises();
});
it('renders the dropdown with all custom names as options', () => {
expect(wrapper.contains(GlDropdown)).toBeDefined();
expect(findAllDropdownItems()).toHaveLength(codeCoverageMockData.length);
expect(findFirstDropdownItem().text()).toBe(codeCoverageMockData[0].group_name);
});
});
describe('interactions', () => {
beforeEach(() => {
mockAxios = new MockAdapter(axios);
mockAxios.onGet().replyOnce(httpStatusCodes.OK, codeCoverageMockData);
createComponent();
return waitForPromises();
});
it('updates the selected dropdown option with an icon', async () => {
findSecondDropdownItem().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(
findFirstDropdownItem()
.find(GlIcon)
.exists(),
).toBe(false);
expect(findSecondDropdownItem().contains(GlIcon)).toBe(true);
});
it('updates the graph data when selecting a different option in dropdown', async () => {
const originalSelectedData = wrapper.vm.selectedDailyCoverage;
const expectedData = codeCoverageMockData[1];
findSecondDropdownItem().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(wrapper.vm.selectedDailyCoverage).not.toBe(originalSelectedData);
expect(wrapper.vm.selectedDailyCoverage).toBe(expectedData);
});
});
});
export default [
{
group_name: 'rspec',
data: [
{ date: '2020-04-30', coverage: 40.0 },
{ date: '2020-05-01', coverage: 80.0 },
{ date: '2020-05-02', coverage: 99.0 },
{ date: '2020-05-10', coverage: 80.0 },
{ date: '2020-05-15', coverage: 70.0 },
{ date: '2020-05-20', coverage: 69.0 },
],
},
{
group_name: 'cypress',
data: [
{ date: '2022-07-30', coverage: 1.0 },
{ date: '2022-08-01', coverage: 2.4 },
{ date: '2022-08-02', coverage: 5.0 },
{ date: '2022-08-10', coverage: 15.0 },
{ date: '2022-08-15', coverage: 30.0 },
{ date: '2022-08-20', coverage: 40.0 },
],
},
{
group_name: 'karma',
data: [
{ date: '2020-05-01', coverage: 94.0 },
{ date: '2020-05-02', coverage: 94.0 },
{ date: '2020-05-03', coverage: 94.0 },
{ date: '2020-05-04', coverage: 94.0 },
{ date: '2020-05-05', coverage: 92.0 },
{ date: '2020-05-06', coverage: 91.0 },
{ date: '2020-05-07', coverage: 78.0 },
{ date: '2020-05-08', coverage: 94.0 },
{ date: '2020-05-09', coverage: 94.0 },
{ date: '2020-05-10', coverage: 94.0 },
{ date: '2020-05-11', coverage: 94.0 },
{ date: '2020-05-12', coverage: 94.0 },
{ date: '2020-05-13', coverage: 92.0 },
{ date: '2020-05-14', coverage: 91.0 },
{ date: '2020-05-15', coverage: 78.0 },
{ date: '2020-05-16', coverage: 94.0 },
{ date: '2020-05-17', coverage: 94.0 },
{ date: '2020-05-18', coverage: 93.0 },
{ date: '2020-05-19', coverage: 92.0 },
{ date: '2020-05-20', coverage: 91.0 },
{ date: '2020-05-21', coverage: 90.0 },
{ date: '2020-05-22', coverage: 91.0 },
{ date: '2020-05-23', coverage: 92.0 },
{ date: '2020-05-24', coverage: 75.0 },
{ date: '2020-05-25', coverage: 74.0 },
{ date: '2020-05-26', coverage: 74.0 },
{ date: '2020-05-27', coverage: 74.0 },
{ date: '2020-05-28', coverage: 80.0 },
{ date: '2020-05-29', coverage: 85.0 },
{ date: '2020-05-30', coverage: 92.0 },
{ date: '2020-05-31', coverage: 91.0 },
],
},
];
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