Commit 75771778 authored by Scott Hampton's avatar Scott Hampton Committed by Miguel Rincon

Use code coverage graphql API

Now that the graphql API has been implemented
for group code coverage, we can switch from
the client resolver.
parent 3e343213
......@@ -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');
});
});
});
......@@ -22453,7 +22453,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)"
......@@ -22468,7 +22471,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