Commit 78497467 authored by Sarah Groff Hennigh-Palermo's avatar Sarah Groff Hennigh-Palermo

Merge branch '215135-group-test-coverage-table' into 'master'

Display the current coverage data for each selected project

See merge request gitlab-org/gitlab!44477
parents 769d341a 5f0f1aa6
......@@ -100,11 +100,11 @@ export default {
this.allProjectsSelected = true;
this.selectedProjectIds = [];
},
selectProject(id) {
selectProject({ parsedId }) {
this.allProjectsSelected = false;
const index = this.selectedProjectIds.indexOf(id);
const index = this.selectedProjectIds.indexOf(parsedId);
if (index < 0) {
this.selectedProjectIds.push(id);
this.selectedProjectIds.push(parsedId);
return;
}
this.selectedProjectIds.splice(index, 1);
......
......@@ -43,7 +43,7 @@ export default {
update(data) {
return data.group.projects.nodes.map(project => ({
...project,
id: getIdFromGraphQLId(project.id),
parsedId: getIdFromGraphQLId(project.id),
isSelected: false,
}));
},
......@@ -87,7 +87,7 @@ export default {
const index = this.groupProjects.map(project => project.id).indexOf(id);
this.groupProjects[index].isSelected = !this.groupProjects[index].isSelected;
this.selectAllProjects = false;
this.$emit('select-project', id);
this.$emit('select-project', this.groupProjects[index]);
},
clickSelectAllProjects() {
this.selectAllProjects = true;
......@@ -95,7 +95,7 @@ export default {
...project,
isSelected: false,
}));
this.$emit('select-all-projects');
this.$emit('select-all-projects', this.groupProjects);
},
handleError() {
this.$emit('projects-query-error');
......
<script>
import { GlCard } from '@gitlab/ui';
import { s__ } from '~/locale';
import Vue from 'vue';
import { GlCard, GlEmptyState, GlSkeletonLoader, GlTable } from '@gitlab/ui';
import { __, s__ } from '~/locale';
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';
export default {
name: 'TestCoverageTable',
components: {
GlCard,
GlEmptyState,
GlSkeletonLoader,
GlTable,
SelectProjectsDropdown,
TimeAgoTooltip,
},
apollo: {
coverageData: {
query: getProjectsTestCoverage,
debounce: 500,
variables() {
return {
projectIds: this.selectedProjectIds,
};
},
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];
},
error() {
this.handleError();
},
watchLoading(isLoading) {
this.isLoading = isLoading;
},
skip() {
return this.skipQuery;
},
},
},
data() {
return {
allProjectsSelected: false,
allCoverageData: [],
hasError: false,
isLoading: false,
projectIds: {},
};
},
computed: {
hasCoverageData() {
return Boolean(this.selectedCoverageData.length);
},
skipQuery() {
// Skip if we haven't selected any projects yet
return !this.selectedProjectIds.length;
},
selectedProjectIds() {
// Get the IDs of the projects that we haven't requested yet
return Object.keys(this.projectIds).filter(
id => !this.allCoverageData.some(project => project.id === id),
);
},
selectedCoverageData() {
return this.allCoverageData.filter(({ id }) => this.projectIds[id]);
},
},
methods: {
handleError() {
this.hasError = true;
},
selectAllProjects(allProjects) {
this.projectIds = Object.fromEntries(allProjects.map(({ id }) => [id, true]));
this.allProjectsSelected = true;
},
toggleProject({ id }) {
if (this.allProjectsSelected) {
// Reset all project selections to false
this.allProjectsSelected = false;
this.projectIds = Object.fromEntries(
Object.entries(this.projectIds).map(([key]) => [key, false]),
);
}
if (Object.prototype.hasOwnProperty.call(this.projectIds, id)) {
Vue.set(this.projectIds, id, !this.projectIds[id]);
return;
}
Vue.set(this.projectIds, id, true);
},
},
tableFields: [
{
key: 'project',
label: __('Project'),
},
{
key: 'coverage',
label: s__('RepositoriesAnalytics|Coverage'),
},
{
key: 'numberOfCoverages',
label: s__('RepositoriesAnalytics|Number of Coverages'),
},
{
key: 'lastUpdate',
label: s__('RepositoriesAnalytics|Last Update'),
},
],
text: {
// This is a temporary placeholder until we actually implement the feature
header: s__('RepositoriesAnalytics|Test Code Coverage'),
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.',
),
},
LOADING_STATE: {
rows: 4,
height: 10,
rx: 4,
groupXs: [0, 95, 180, 330],
widths: [90, 80, 145, 100],
totalWidth: 430,
totalHeight: 15,
},
};
</script>
<template>
<gl-card>
<template #header>
<h5>{{ $options.text.header }}</h5>
<select-projects-dropdown
class="gl-w-quarter"
@projects-query-error="handleError"
@select-all-projects="selectAllProjects"
@select-project="toggleProject"
/>
</template>
<template v-if="isLoading">
<gl-skeleton-loader
v-for="index in $options.LOADING_STATE.rows"
:key="index"
:width="$options.LOADING_STATE.totalWidth"
:height="$options.LOADING_STATE.totalHeight"
data-testid="test-coverage-loading-state"
>
<rect
v-for="(x, xIndex) in $options.LOADING_STATE.groupXs"
:key="`x-skeleton-${x}`"
:width="$options.LOADING_STATE.widths[xIndex]"
:height="$options.LOADING_STATE.height"
:x="x"
:y="0"
:rx="$options.LOADING_STATE.rx"
/>
</gl-skeleton-loader>
</template>
<gl-table
v-else-if="hasCoverageData"
data-testid="test-coverage-data-table"
thead-class="thead-white"
:fields="$options.tableFields"
:items="selectedCoverageData"
>
<template #head(project)="data">
<div>{{ data.label }}</div>
</template>
<template #head(coverage)="data">
<div>{{ data.label }}</div>
</template>
<template #head(numberOfCoverages)="data">
<div>{{ data.label }}</div>
</template>
<template #head(lastUpdate)="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>
<template #cell(numberOfCoverages)="{ item }">
<div :data-testid="`${item.id}-count`">{{ item.codeCoverage.count }}</div>
</template>
<template #cell(lastUpdate)="{ item }">
<time-ago-tooltip
:time="item.codeCoverage.lastUpdatedAt"
:data-testid="`${item.id}-date`"
/>
</template>
</gl-table>
<gl-empty-state
v-else
class="gl-mt-3"
:title="$options.text.emptyStateTitle"
:description="$options.text.emptyStateDescription"
data-testid="test-coverage-table-empty-state"
/>
</gl-card>
</template>
query getProjectsTestCoverage($projectIds: [ID!]) {
projects(ids: $projectIds) {
nodes {
id
name
codeCoverage @client {
average
count
lastUpdatedAt
}
}
}
}
......@@ -6,7 +6,21 @@ import GroupRepositoryAnalytics from './components/group_repository_analytics.vu
Vue.use(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 () => {
......
......@@ -59,8 +59,10 @@ describe('Select projects dropdown component', () => {
});
describe('when selecting all project', () => {
const initialData = { groupProjects: [{ id: 1, name: '1', isSelected: true }] };
beforeEach(() => {
createComponent({ data: { groupProjects: [{ id: 1, name: '1', isSelected: true }] } });
createComponent({ data: initialData });
});
it('should reset all selected projects', () => {
......@@ -68,7 +70,7 @@ describe('Select projects dropdown component', () => {
return wrapper.vm.$nextTick().then(() => {
expect(
findProjectById(1)
findProjectById(initialData.groupProjects[0].id)
.find(GlIcon)
.classes(),
).toContain('gl-visibility-hidden');
......@@ -79,23 +81,31 @@ describe('Select projects dropdown component', () => {
jest.spyOn(wrapper.vm, '$emit');
selectAllProjects();
expect(wrapper.vm.$emit).toHaveBeenCalledWith('select-all-projects');
expect(wrapper.vm.$emit).toHaveBeenCalledWith('select-all-projects', [
{ ...initialData.groupProjects[0], isSelected: false },
]);
});
});
describe('when selecting a project', () => {
const initialData = {
groupProjects: [{ id: 1, name: '1', isSelected: false }],
selectAllProjects: true,
};
beforeEach(() => {
createComponent({
data: { groupProjects: [{ id: 1, name: '1', isSelected: false }], selectAllProjects: true },
data: initialData,
});
});
it('should check selected project', () => {
selectProjectById(1);
const project = initialData.groupProjects[0];
selectProjectById(project.id);
return wrapper.vm.$nextTick().then(() => {
expect(
findProjectById(1)
findProjectById(project.id)
.find(GlIcon)
.classes(),
).not.toContain('gl-visibility-hidden');
......@@ -103,7 +113,7 @@ describe('Select projects dropdown component', () => {
});
it('should uncheck select all projects', () => {
selectProjectById(1);
selectProjectById(initialData.groupProjects[0].id);
return wrapper.vm.$nextTick().then(() => {
expect(
......@@ -115,10 +125,14 @@ describe('Select projects dropdown component', () => {
});
it('should emit select-project event', () => {
const project = initialData.groupProjects[0];
jest.spyOn(wrapper.vm, '$emit');
selectProjectById(1);
selectProjectById(project.id);
expect(wrapper.vm.$emit).toHaveBeenCalledWith('select-project', 1);
expect(wrapper.vm.$emit).toHaveBeenCalledWith('select-project', {
...project,
isSelected: true,
});
});
});
......
import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import { useFakeDate } from 'helpers/fake_date';
import TestCoverageTable from 'ee/analytics/repository_analytics/components/test_coverage_table.vue';
const localVue = createLocalVue();
describe('Test coverage table component', () => {
useFakeDate();
let wrapper;
const findEmptyState = () => wrapper.find('[data-testid="test-coverage-table-empty-state"]');
const findLoadingState = () => wrapper.find('[data-testid="test-coverage-loading-state"');
const findTable = () => wrapper.find('[data-testid="test-coverage-data-table"');
const findProjectNameById = id => wrapper.find(`[data-testid="${id}-name"`);
const findProjectAverageById = id => wrapper.find(`[data-testid="${id}-average"`);
const findProjectCountById = id => wrapper.find(`[data-testid="${id}-count"`);
const findProjectDateById = id => wrapper.find(`[data-testid="${id}-date"`);
const createComponent = (data = {}, mountFn = shallowMount) => {
wrapper = mountFn(TestCoverageTable, {
localVue,
data() {
return {
allCoverageData: [],
allProjectsSelected: false,
hasError: false,
isLoading: false,
projectIds: {},
...data,
};
},
mocks: {
$apollo: {
queries: {
coverageData: {
query: jest.fn().mockResolvedValue(),
},
},
},
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when code coverage is empty', () => {
it('renders empty state', () => {
createComponent();
expect(findEmptyState().exists()).toBe(true);
});
});
describe('when query is loading', () => {
it('renders loading state', () => {
createComponent({ isLoading: true });
expect(findLoadingState().exists()).toBe(true);
});
});
describe('when code coverage is available', () => {
it('renders coverage table', () => {
const id = 'gid://gitlab/Project/1';
const name = 'GitLab';
const average = '74.35';
const count = '5';
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
createComponent(
{
allCoverageData: [
{
id,
name,
codeCoverage: {
average,
count,
lastUpdatedAt: yesterday.toISOString(),
},
},
],
projectIds: {
[id]: true,
},
},
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(findProjectDateById(id).text()).toBe('1 day ago');
});
});
});
......@@ -22022,6 +22022,9 @@ msgstr ""
msgid "Repositories Analytics"
msgstr ""
msgid "RepositoriesAnalytics|Coverage"
msgstr ""
msgid "RepositoriesAnalytics|Download Historic Test Coverage Data"
msgstr ""
......@@ -22034,6 +22037,18 @@ msgstr ""
msgid "RepositoriesAnalytics|Historic Test Coverage Data is available in raw format (.csv) for further analysis."
msgstr ""
msgid "RepositoriesAnalytics|Last Update"
msgstr ""
msgid "RepositoriesAnalytics|Number of Coverages"
msgstr ""
msgid "RepositoriesAnalytics|Please select a project or multiple projects to display their most recent test coverage data."
msgstr ""
msgid "RepositoriesAnalytics|Please select projects to display."
msgstr ""
msgid "RepositoriesAnalytics|Test Code Coverage"
msgstr ""
......
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