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 ...@@ -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');
});
});
}); });
...@@ -22492,7 +22492,10 @@ msgstr "" ...@@ -22492,7 +22492,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)"
...@@ -22507,7 +22510,7 @@ msgstr "" ...@@ -22507,7 +22510,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