Commit c485ec58 authored by Miguel Rincon's avatar Miguel Rincon

Merge branch '267624-group-code-coverage-table' into 'master'

Use code coverage graphql API

See merge request gitlab-org/gitlab!46265
parents 50353228 75771778
......@@ -12,6 +12,29 @@ info: To determine the technical writer assigned to the Stage/Group associated w
CAUTION: **Warning:**
This feature might not be available to you. Check the **version history** note above for details.
## Latest project test coverage list
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/267624) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.6.
> - It's [deployed behind a feature flag](../../../user/feature_flags.md), disabled by default.
> - It's disabled on GitLab.com
> - It can be enabled or disabled per-group.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-latest-project-test-coverage).
To see the latest code coverage for each project in your group:
1. Go to **Analytics > Repositories** in the group (not from a project).
1. In the **Latest test coverage results** section, use the **Select projects** dropdown to choose the projects you want to check.
### Enable or disable latest project test coverage
This feature comes with the `:group_coverage_data_report` feature flag disabled by default. It is disabled on GitLab.com.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md) can enable it for your instance.
The group test coverage table can be enabled or disabled per-group.
## Download historic test coverage data
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/215104) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.4.
You can get a CSV of the code coverage data for all of the projects in your group. This report has a maximum of 1000 records. To get the report:
1. Go to your group's **Analytics > Repositories** page
......
......@@ -110,11 +110,10 @@ export default {
},
},
text: {
downloadTestCoverageHeader: s__('RepositoriesAnalytics|Download Historic Test Coverage Data'),
downloadTestCoverageHeader: s__('RepositoriesAnalytics|Download historic test coverage data'),
downloadCSVButton: s__('RepositoriesAnalytics|Download historic test coverage data (.csv)'),
dateRangeHeader: __('Date range'),
downloadCSVModalButton: s__('RepositoriesAnalytics|Download test coverage data (.csv)'),
downloadCSVModalTitle: s__('RepositoriesAnalytics|Download Historic Test Coverage Data'),
downloadCSVModalDescription: s__(
'RepositoriesAnalytics|Historic Test Coverage Data is available in raw format (.csv) for further analysis.',
),
......@@ -148,7 +147,7 @@ export default {
<gl-modal
modal-id="download-csv-modal"
:title="$options.text.downloadCSVModalTitle"
:title="$options.text.downloadTestCoverageHeader"
no-fade
:action-primary="downloadCSVModalButton"
:action-cancel="cancelModalButton"
......
......@@ -40,11 +40,13 @@ export default {
};
},
update(data) {
return data.group.projects.nodes.map(project => ({
...project,
parsedId: getIdFromGraphQLId(project.id),
isSelected: false,
}));
return (
data.group?.projects?.nodes?.map(project => ({
...project,
parsedId: getIdFromGraphQLId(project.id),
isSelected: false,
})) || []
);
},
result({ data }) {
this.projectsPageInfo = data?.group?.projects?.pageInfo || {};
......
......@@ -2,6 +2,7 @@
import Vue from 'vue';
import { GlCard, GlEmptyState, GlSkeletonLoader, GlTable } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import SelectProjectsDropdown from './select_projects_dropdown.vue';
import getProjectsTestCoverage from '../graphql/queries/get_projects_test_coverage.query.graphql';
......@@ -17,7 +18,7 @@ export default {
TimeAgoTooltip,
},
apollo: {
coverageData: {
projects: {
query: getProjectsTestCoverage,
debounce: 500,
variables() {
......@@ -28,7 +29,15 @@ export default {
result({ data }) {
// Keep data from all queries so that we don't
// fetch the same data more than once
this.allCoverageData = [...this.allCoverageData, ...data.projects.nodes];
this.allCoverageData = [
...this.allCoverageData,
...data.projects.nodes.map(project => ({
...project,
// if a project has no code coverage, set to default values
codeCoverageSummary:
project.codeCoverageSummary || this.$options.noCoverageDefaultSummary,
})),
];
},
error() {
this.handleError();
......@@ -99,24 +108,30 @@ export default {
label: __('Project'),
},
{
key: 'coverage',
key: 'averageCoverage',
label: s__('RepositoriesAnalytics|Coverage'),
},
{
key: 'numberOfCoverages',
label: s__('RepositoriesAnalytics|Number of Coverages'),
key: 'coverageCount',
label: s__('RepositoriesAnalytics|Coverage Jobs'),
},
{
key: 'lastUpdate',
key: 'lastUpdatedAt',
label: s__('RepositoriesAnalytics|Last Update'),
},
],
text: {
header: s__('RepositoriesAnalytics|Latest test coverage results'),
emptyStateTitle: s__('RepositoriesAnalytics|Please select projects to display.'),
emptyStateDescription: s__(
'RepositoriesAnalytics|Please select a project or multiple projects to display their most recent test coverage data.',
),
},
noCoverageDefaultSummary: {
averageCoverage: 0,
coverageCount: 0,
lastUpdatedAt: '', // empty string will default to "just now" in table
},
LOADING_STATE: {
rows: 4,
height: 10,
......@@ -126,17 +141,21 @@ export default {
totalWidth: 430,
totalHeight: 15,
},
averageCoverageFormatter: getFormatter(SUPPORTED_FORMATS.percentHundred),
};
</script>
<template>
<gl-card>
<template #header>
<select-projects-dropdown
class="gl-w-quarter"
@projects-query-error="handleError"
@select-all-projects="selectAllProjects"
@select-project="toggleProject"
/>
<div class="gl-display-flex gl-justify-content-space-between">
<h5>{{ $options.text.header }}</h5>
<select-projects-dropdown
class="gl-w-quarter"
@projects-query-error="handleError"
@select-all-projects="selectAllProjects"
@select-project="toggleProject"
/>
</div>
</template>
<template v-if="isLoading">
......@@ -169,28 +188,30 @@ export default {
<template #head(project)="data">
<div>{{ data.label }}</div>
</template>
<template #head(coverage)="data">
<template #head(averageCoverage)="data">
<div>{{ data.label }}</div>
</template>
<template #head(numberOfCoverages)="data">
<template #head(coverageCount)="data">
<div>{{ data.label }}</div>
</template>
<template #head(lastUpdate)="data">
<template #head(lastUpdatedAt)="data">
<div>{{ data.label }}</div>
</template>
<template #cell(project)="{ item }">
<div :data-testid="`${item.id}-name`">{{ item.name }}</div>
</template>
<template #cell(coverage)="{ item }">
<div :data-testid="`${item.id}-average`">{{ item.codeCoverage.average }}%</div>
<template #cell(averageCoverage)="{ item }">
<div :data-testid="`${item.id}-average`">
{{ $options.averageCoverageFormatter(item.codeCoverageSummary.averageCoverage, 2) }}
</div>
</template>
<template #cell(numberOfCoverages)="{ item }">
<div :data-testid="`${item.id}-count`">{{ item.codeCoverage.count }}</div>
<template #cell(coverageCount)="{ item }">
<div :data-testid="`${item.id}-count`">{{ item.codeCoverageSummary.coverageCount }}</div>
</template>
<template #cell(lastUpdate)="{ item }">
<template #cell(lastUpdatedAt)="{ item }">
<time-ago-tooltip
:time="item.codeCoverage.lastUpdatedAt"
:time="item.codeCoverageSummary.lastUpdatedAt"
:data-testid="`${item.id}-date`"
/>
</template>
......
......@@ -3,9 +3,9 @@ query getProjectsTestCoverage($projectIds: [ID!]) {
nodes {
id
name
codeCoverage @client {
average
count
codeCoverageSummary {
averageCoverage
coverageCount
lastUpdatedAt
}
}
......
......@@ -6,21 +6,7 @@ import GroupRepositoryAnalytics from './components/group_repository_analytics.vu
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient({
Project: {
/*
The backend for adding `codeCoverage` the API is being worked on in parallel.
This is a temporary client resolver for this data. This feature is behind
a feature flag (:group_coverage_data_report)
*/
codeCoverage: () => ({
average: (Math.random() * 100).toFixed(2),
count: Math.ceil(Math.random() * Math.floor(10)), // random number between 1 and 10
lastUpdatedAt: '2020-09-29T21:42:00Z',
__typename: 'CodeCoverage',
}),
},
}),
defaultClient: createDefaultClient(),
});
export default () => {
......
import VueApollo from 'vue-apollo';
import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import { useFakeDate } from 'helpers/fake_date';
import createMockApollo from 'jest/helpers/mock_apollo_helper';
import waitForPromises from 'jest/helpers/wait_for_promises';
import TestCoverageTable from 'ee/analytics/repository_analytics/components/test_coverage_table.vue';
import getProjectsTestCoverage from 'ee/analytics/repository_analytics/graphql/queries/get_projects_test_coverage.query.graphql';
import getGroupProjects from 'ee/analytics/repository_analytics/graphql/queries/get_group_projects.query.graphql';
const localVue = createLocalVue();
describe('Test coverage table component', () => {
useFakeDate();
let wrapper;
let fakeApollo;
const findEmptyState = () => wrapper.find('[data-testid="test-coverage-table-empty-state"]');
const findLoadingState = () => wrapper.find('[data-testid="test-coverage-loading-state"');
......@@ -16,7 +22,7 @@ describe('Test coverage table component', () => {
const findProjectCountById = id => wrapper.find(`[data-testid="${id}-count"`);
const findProjectDateById = id => wrapper.find(`[data-testid="${id}-date"`);
const createComponent = (data = {}, mountFn = shallowMount) => {
const createComponent = ({ data = {}, mountFn = shallowMount } = {}) => {
wrapper = mountFn(TestCoverageTable, {
localVue,
data() {
......@@ -32,7 +38,7 @@ describe('Test coverage table component', () => {
mocks: {
$apollo: {
queries: {
coverageData: {
projects: {
query: jest.fn().mockResolvedValue(),
},
},
......@@ -41,6 +47,33 @@ describe('Test coverage table component', () => {
});
};
const createComponentWithApollo = ({
data = {},
mountFn = shallowMount,
queryData = {},
} = {}) => {
localVue.use(VueApollo);
fakeApollo = createMockApollo([
[getGroupProjects, jest.fn().mockResolvedValue()],
[getProjectsTestCoverage, jest.fn().mockResolvedValue(queryData)],
]);
wrapper = mountFn(TestCoverageTable, {
localVue,
data() {
return {
allCoverageData: [],
allProjectsSelected: false,
hasError: false,
isLoading: false,
projectIds: {},
...data,
};
},
apolloProvider: fakeApollo,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
......@@ -55,7 +88,7 @@ describe('Test coverage table component', () => {
describe('when query is loading', () => {
it('renders loading state', () => {
createComponent({ isLoading: true });
createComponent({ data: { isLoading: true } });
expect(findLoadingState().exists()).toBe(true);
});
......@@ -65,20 +98,20 @@ describe('Test coverage table component', () => {
it('renders coverage table', () => {
const id = 'gid://gitlab/Project/1';
const name = 'GitLab';
const average = '74.35';
const count = '5';
const averageCoverage = '74.35';
const coverageCount = '5';
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
createComponent(
{
createComponent({
data: {
allCoverageData: [
{
id,
name,
codeCoverage: {
average,
count,
codeCoverageSummary: {
averageCoverage,
coverageCount,
lastUpdatedAt: yesterday.toISOString(),
},
},
......@@ -87,14 +120,48 @@ describe('Test coverage table component', () => {
[id]: true,
},
},
mount,
);
mountFn: mount,
});
expect(findTable().exists()).toBe(true);
expect(findProjectNameById(id).text()).toBe(name);
expect(findProjectAverageById(id).text()).toBe(`${average}%`);
expect(findProjectCountById(id).text()).toBe(count);
expect(findProjectAverageById(id).text()).toBe(`${averageCoverage}%`);
expect(findProjectCountById(id).text()).toBe(coverageCount);
expect(findProjectDateById(id).text()).toBe('1 day ago');
});
});
describe('when selected project has no coverage', () => {
it('sets coverage to default values', async () => {
const name = 'test';
const id = 1;
createComponentWithApollo({
data: {
projectIds: { [id]: true },
},
queryData: {
data: {
projects: {
nodes: [
{
name,
id,
codeCoverageSummary: null,
},
],
},
},
},
mountFn: mount,
});
jest.runOnlyPendingTimers();
await waitForPromises();
expect(findTable().exists()).toBe(true);
expect(findProjectNameById(id).text()).toBe(name);
expect(findProjectAverageById(id).text()).toBe('0.00%');
expect(findProjectCountById(id).text()).toBe('0');
expect(findProjectDateById(id).text()).toBe('just now');
});
});
});
......@@ -22492,7 +22492,10 @@ msgstr ""
msgid "RepositoriesAnalytics|Coverage"
msgstr ""
msgid "RepositoriesAnalytics|Download Historic Test Coverage Data"
msgid "RepositoriesAnalytics|Coverage Jobs"
msgstr ""
msgid "RepositoriesAnalytics|Download historic test coverage data"
msgstr ""
msgid "RepositoriesAnalytics|Download historic test coverage data (.csv)"
......@@ -22507,7 +22510,7 @@ msgstr ""
msgid "RepositoriesAnalytics|Last Update"
msgstr ""
msgid "RepositoriesAnalytics|Number of Coverages"
msgid "RepositoriesAnalytics|Latest test coverage results"
msgstr ""
msgid "RepositoriesAnalytics|Please select a project or multiple projects to display their most recent test coverage data."
......
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