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 ...@@ -12,6 +12,29 @@ info: To determine the technical writer assigned to the Stage/Group associated w
CAUTION: **Warning:** CAUTION: **Warning:**
This feature might not be available to you. Check the **version history** note above for details. 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: 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 1. Go to your group's **Analytics > Repositories** page
......
...@@ -110,11 +110,10 @@ export default { ...@@ -110,11 +110,10 @@ export default {
}, },
}, },
text: { 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)'), downloadCSVButton: s__('RepositoriesAnalytics|Download historic test coverage data (.csv)'),
dateRangeHeader: __('Date range'), dateRangeHeader: __('Date range'),
downloadCSVModalButton: s__('RepositoriesAnalytics|Download test coverage data (.csv)'), downloadCSVModalButton: s__('RepositoriesAnalytics|Download test coverage data (.csv)'),
downloadCSVModalTitle: s__('RepositoriesAnalytics|Download Historic Test Coverage Data'),
downloadCSVModalDescription: s__( downloadCSVModalDescription: s__(
'RepositoriesAnalytics|Historic Test Coverage Data is available in raw format (.csv) for further analysis.', 'RepositoriesAnalytics|Historic Test Coverage Data is available in raw format (.csv) for further analysis.',
), ),
...@@ -148,7 +147,7 @@ export default { ...@@ -148,7 +147,7 @@ export default {
<gl-modal <gl-modal
modal-id="download-csv-modal" modal-id="download-csv-modal"
:title="$options.text.downloadCSVModalTitle" :title="$options.text.downloadTestCoverageHeader"
no-fade no-fade
:action-primary="downloadCSVModalButton" :action-primary="downloadCSVModalButton"
:action-cancel="cancelModalButton" :action-cancel="cancelModalButton"
......
...@@ -40,11 +40,13 @@ export default { ...@@ -40,11 +40,13 @@ export default {
}; };
}, },
update(data) { update(data) {
return data.group.projects.nodes.map(project => ({ return (
...project, data.group?.projects?.nodes?.map(project => ({
parsedId: getIdFromGraphQLId(project.id), ...project,
isSelected: false, parsedId: getIdFromGraphQLId(project.id),
})); isSelected: false,
})) || []
);
}, },
result({ data }) { result({ data }) {
this.projectsPageInfo = data?.group?.projects?.pageInfo || {}; this.projectsPageInfo = data?.group?.projects?.pageInfo || {};
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import Vue from 'vue'; import Vue from 'vue';
import { GlCard, GlEmptyState, GlSkeletonLoader, GlTable } from '@gitlab/ui'; import { GlCard, GlEmptyState, GlSkeletonLoader, GlTable } from '@gitlab/ui';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import SelectProjectsDropdown from './select_projects_dropdown.vue'; import SelectProjectsDropdown from './select_projects_dropdown.vue';
import getProjectsTestCoverage from '../graphql/queries/get_projects_test_coverage.query.graphql'; import getProjectsTestCoverage from '../graphql/queries/get_projects_test_coverage.query.graphql';
...@@ -17,7 +18,7 @@ export default { ...@@ -17,7 +18,7 @@ export default {
TimeAgoTooltip, TimeAgoTooltip,
}, },
apollo: { apollo: {
coverageData: { projects: {
query: getProjectsTestCoverage, query: getProjectsTestCoverage,
debounce: 500, debounce: 500,
variables() { variables() {
...@@ -28,7 +29,15 @@ export default { ...@@ -28,7 +29,15 @@ export default {
result({ data }) { result({ data }) {
// Keep data from all queries so that we don't // Keep data from all queries so that we don't
// fetch the same data more than once // 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() { error() {
this.handleError(); this.handleError();
...@@ -99,24 +108,30 @@ export default { ...@@ -99,24 +108,30 @@ export default {
label: __('Project'), label: __('Project'),
}, },
{ {
key: 'coverage', key: 'averageCoverage',
label: s__('RepositoriesAnalytics|Coverage'), label: s__('RepositoriesAnalytics|Coverage'),
}, },
{ {
key: 'numberOfCoverages', key: 'coverageCount',
label: s__('RepositoriesAnalytics|Number of Coverages'), label: s__('RepositoriesAnalytics|Coverage Jobs'),
}, },
{ {
key: 'lastUpdate', key: 'lastUpdatedAt',
label: s__('RepositoriesAnalytics|Last Update'), label: s__('RepositoriesAnalytics|Last Update'),
}, },
], ],
text: { text: {
header: s__('RepositoriesAnalytics|Latest test coverage results'),
emptyStateTitle: s__('RepositoriesAnalytics|Please select projects to display.'), emptyStateTitle: s__('RepositoriesAnalytics|Please select projects to display.'),
emptyStateDescription: s__( emptyStateDescription: s__(
'RepositoriesAnalytics|Please select a project or multiple projects to display their most recent test coverage data.', '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: { LOADING_STATE: {
rows: 4, rows: 4,
height: 10, height: 10,
...@@ -126,17 +141,21 @@ export default { ...@@ -126,17 +141,21 @@ export default {
totalWidth: 430, totalWidth: 430,
totalHeight: 15, totalHeight: 15,
}, },
averageCoverageFormatter: getFormatter(SUPPORTED_FORMATS.percentHundred),
}; };
</script> </script>
<template> <template>
<gl-card> <gl-card>
<template #header> <template #header>
<select-projects-dropdown <div class="gl-display-flex gl-justify-content-space-between">
class="gl-w-quarter" <h5>{{ $options.text.header }}</h5>
@projects-query-error="handleError" <select-projects-dropdown
@select-all-projects="selectAllProjects" class="gl-w-quarter"
@select-project="toggleProject" @projects-query-error="handleError"
/> @select-all-projects="selectAllProjects"
@select-project="toggleProject"
/>
</div>
</template> </template>
<template v-if="isLoading"> <template v-if="isLoading">
...@@ -169,28 +188,30 @@ export default { ...@@ -169,28 +188,30 @@ export default {
<template #head(project)="data"> <template #head(project)="data">
<div>{{ data.label }}</div> <div>{{ data.label }}</div>
</template> </template>
<template #head(coverage)="data"> <template #head(averageCoverage)="data">
<div>{{ data.label }}</div> <div>{{ data.label }}</div>
</template> </template>
<template #head(numberOfCoverages)="data"> <template #head(coverageCount)="data">
<div>{{ data.label }}</div> <div>{{ data.label }}</div>
</template> </template>
<template #head(lastUpdate)="data"> <template #head(lastUpdatedAt)="data">
<div>{{ data.label }}</div> <div>{{ data.label }}</div>
</template> </template>
<template #cell(project)="{ item }"> <template #cell(project)="{ item }">
<div :data-testid="`${item.id}-name`">{{ item.name }}</div> <div :data-testid="`${item.id}-name`">{{ item.name }}</div>
</template> </template>
<template #cell(coverage)="{ item }"> <template #cell(averageCoverage)="{ item }">
<div :data-testid="`${item.id}-average`">{{ item.codeCoverage.average }}%</div> <div :data-testid="`${item.id}-average`">
{{ $options.averageCoverageFormatter(item.codeCoverageSummary.averageCoverage, 2) }}
</div>
</template> </template>
<template #cell(numberOfCoverages)="{ item }"> <template #cell(coverageCount)="{ item }">
<div :data-testid="`${item.id}-count`">{{ item.codeCoverage.count }}</div> <div :data-testid="`${item.id}-count`">{{ item.codeCoverageSummary.coverageCount }}</div>
</template> </template>
<template #cell(lastUpdate)="{ item }"> <template #cell(lastUpdatedAt)="{ item }">
<time-ago-tooltip <time-ago-tooltip
:time="item.codeCoverage.lastUpdatedAt" :time="item.codeCoverageSummary.lastUpdatedAt"
:data-testid="`${item.id}-date`" :data-testid="`${item.id}-date`"
/> />
</template> </template>
......
...@@ -3,9 +3,9 @@ query getProjectsTestCoverage($projectIds: [ID!]) { ...@@ -3,9 +3,9 @@ query getProjectsTestCoverage($projectIds: [ID!]) {
nodes { nodes {
id id
name name
codeCoverage @client { codeCoverageSummary {
average averageCoverage
count coverageCount
lastUpdatedAt lastUpdatedAt
} }
} }
......
...@@ -6,21 +6,7 @@ import GroupRepositoryAnalytics from './components/group_repository_analytics.vu ...@@ -6,21 +6,7 @@ import GroupRepositoryAnalytics from './components/group_repository_analytics.vu
Vue.use(VueApollo); Vue.use(VueApollo);
const apolloProvider = new VueApollo({ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient({ 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',
}),
},
}),
}); });
export default () => { export default () => {
......
import VueApollo from 'vue-apollo';
import { mount, shallowMount, createLocalVue } from '@vue/test-utils'; import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import { useFakeDate } from 'helpers/fake_date'; 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 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(); const localVue = createLocalVue();
describe('Test coverage table component', () => { describe('Test coverage table component', () => {
useFakeDate(); useFakeDate();
let wrapper; let wrapper;
let fakeApollo;
const findEmptyState = () => wrapper.find('[data-testid="test-coverage-table-empty-state"]'); const findEmptyState = () => wrapper.find('[data-testid="test-coverage-table-empty-state"]');
const findLoadingState = () => wrapper.find('[data-testid="test-coverage-loading-state"'); const findLoadingState = () => wrapper.find('[data-testid="test-coverage-loading-state"');
...@@ -16,7 +22,7 @@ describe('Test coverage table component', () => { ...@@ -16,7 +22,7 @@ describe('Test coverage table component', () => {
const findProjectCountById = id => wrapper.find(`[data-testid="${id}-count"`); const findProjectCountById = id => wrapper.find(`[data-testid="${id}-count"`);
const findProjectDateById = id => wrapper.find(`[data-testid="${id}-date"`); const findProjectDateById = id => wrapper.find(`[data-testid="${id}-date"`);
const createComponent = (data = {}, mountFn = shallowMount) => { const createComponent = ({ data = {}, mountFn = shallowMount } = {}) => {
wrapper = mountFn(TestCoverageTable, { wrapper = mountFn(TestCoverageTable, {
localVue, localVue,
data() { data() {
...@@ -32,7 +38,7 @@ describe('Test coverage table component', () => { ...@@ -32,7 +38,7 @@ describe('Test coverage table component', () => {
mocks: { mocks: {
$apollo: { $apollo: {
queries: { queries: {
coverageData: { projects: {
query: jest.fn().mockResolvedValue(), query: jest.fn().mockResolvedValue(),
}, },
}, },
...@@ -41,6 +47,33 @@ describe('Test coverage table component', () => { ...@@ -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(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
...@@ -55,7 +88,7 @@ describe('Test coverage table component', () => { ...@@ -55,7 +88,7 @@ describe('Test coverage table component', () => {
describe('when query is loading', () => { describe('when query is loading', () => {
it('renders loading state', () => { it('renders loading state', () => {
createComponent({ isLoading: true }); createComponent({ data: { isLoading: true } });
expect(findLoadingState().exists()).toBe(true); expect(findLoadingState().exists()).toBe(true);
}); });
...@@ -65,20 +98,20 @@ describe('Test coverage table component', () => { ...@@ -65,20 +98,20 @@ describe('Test coverage table component', () => {
it('renders coverage table', () => { it('renders coverage table', () => {
const id = 'gid://gitlab/Project/1'; const id = 'gid://gitlab/Project/1';
const name = 'GitLab'; const name = 'GitLab';
const average = '74.35'; const averageCoverage = '74.35';
const count = '5'; const coverageCount = '5';
const yesterday = new Date(); const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1); yesterday.setDate(yesterday.getDate() - 1);
createComponent( createComponent({
{ data: {
allCoverageData: [ allCoverageData: [
{ {
id, id,
name, name,
codeCoverage: { codeCoverageSummary: {
average, averageCoverage,
count, coverageCount,
lastUpdatedAt: yesterday.toISOString(), lastUpdatedAt: yesterday.toISOString(),
}, },
}, },
...@@ -87,14 +120,48 @@ describe('Test coverage table component', () => { ...@@ -87,14 +120,48 @@ describe('Test coverage table component', () => {
[id]: true, [id]: true,
}, },
}, },
mount, mountFn: mount,
); });
expect(findTable().exists()).toBe(true); expect(findTable().exists()).toBe(true);
expect(findProjectNameById(id).text()).toBe(name); expect(findProjectNameById(id).text()).toBe(name);
expect(findProjectAverageById(id).text()).toBe(`${average}%`); expect(findProjectAverageById(id).text()).toBe(`${averageCoverage}%`);
expect(findProjectCountById(id).text()).toBe(count); expect(findProjectCountById(id).text()).toBe(coverageCount);
expect(findProjectDateById(id).text()).toBe('1 day ago'); 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 "" ...@@ -22453,7 +22453,10 @@ msgstr ""
msgid "RepositoriesAnalytics|Coverage" msgid "RepositoriesAnalytics|Coverage"
msgstr "" msgstr ""
msgid "RepositoriesAnalytics|Download Historic Test Coverage Data" msgid "RepositoriesAnalytics|Coverage Jobs"
msgstr ""
msgid "RepositoriesAnalytics|Download historic test coverage data"
msgstr "" msgstr ""
msgid "RepositoriesAnalytics|Download historic test coverage data (.csv)" msgid "RepositoriesAnalytics|Download historic test coverage data (.csv)"
...@@ -22468,7 +22471,7 @@ msgstr "" ...@@ -22468,7 +22471,7 @@ msgstr ""
msgid "RepositoriesAnalytics|Last Update" msgid "RepositoriesAnalytics|Last Update"
msgstr "" msgstr ""
msgid "RepositoriesAnalytics|Number of Coverages" msgid "RepositoriesAnalytics|Latest test coverage results"
msgstr "" msgstr ""
msgid "RepositoriesAnalytics|Please select a project or multiple projects to display their most recent test coverage data." 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