Commit d61780e9 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '284381-group-code-coverage-query' into 'master'

Update group code coverage projects query

See merge request gitlab-org/gitlab!56124
parents c82217d8 11533405
...@@ -22,23 +22,29 @@ export default { ...@@ -22,23 +22,29 @@ export default {
TimeAgoTooltip, TimeAgoTooltip,
}, },
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
inject: {
groupFullPath: {
default: '',
},
},
apollo: { apollo: {
projects: { projects: {
query: getProjectsTestCoverage, query: getProjectsTestCoverage,
debounce: 500, debounce: 500,
variables() { variables() {
return { return {
projectIds: this.selectedProjectIds, groupFullPath: this.groupFullPath,
projectIds: this.projectIdsToFetch,
}; };
}, },
result({ data }) { result({ data }) {
const projects = data.group.projects.nodes;
// 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 = [
...this.allCoverageData, ...this.allCoverageData,
// Remove the projects that don't have any code coverage ...projects
...data.projects.nodes .filter(({ id }) => !this.allCoverageData.some((project) => project.id === id))
.filter(({ codeCoverageSummary }) => Boolean(codeCoverageSummary))
.map((project) => ({ .map((project) => ({
...project, ...project,
codeCoveragePath: joinPaths( codeCoveragePath: joinPaths(
...@@ -48,6 +54,9 @@ export default { ...@@ -48,6 +54,9 @@ export default {
})), })),
]; ];
}, },
update(data) {
return data.group.projects.nodes;
},
error() { error() {
this.handleError(); this.handleError();
}, },
...@@ -62,10 +71,11 @@ export default { ...@@ -62,10 +71,11 @@ export default {
data() { data() {
return { return {
allProjectsSelected: false, allProjectsSelected: false,
allCoverageData: [], allCoverageData: [], // All data we have ever received whether selected or not
hasError: false, hasError: false,
isLoading: false, isLoading: false,
projectIds: {}, selectedProjectIds: {},
projects: {},
}; };
}, },
computed: { computed: {
...@@ -74,16 +84,28 @@ export default { ...@@ -74,16 +84,28 @@ export default {
}, },
skipQuery() { skipQuery() {
// Skip if we haven't selected any projects yet // Skip if we haven't selected any projects yet
return !this.selectedProjectIds.length; return !this.allProjectsSelected && !this.projectIdsToFetch.length;
}, },
selectedProjectIds() { /**
* projectIdsToFetch is a subset of selectedProjectIds
* The difference is that it only returns the projects
* that we have selected but haven't requested yet
*/
projectIdsToFetch() {
if (this.allProjectsSelected) {
return null;
}
// Get the IDs of the projects that we haven't requested yet // Get the IDs of the projects that we haven't requested yet
return Object.keys(this.projectIds).filter( return Object.keys(this.selectedProjectIds).filter(
(id) => !this.allCoverageData.some((project) => project.id === id), (id) => !this.allCoverageData.some((project) => project.id === id),
); );
}, },
selectedCoverageData() { selectedCoverageData() {
return this.allCoverageData.filter(({ id }) => this.projectIds[id]); if (this.allProjectsSelected) {
return this.allCoverageData;
}
return this.allCoverageData.filter(({ id }) => this.selectedProjectIds[id]);
}, },
sortedCoverageData() { sortedCoverageData() {
// Sort the table by most recently updated coverage report // Sort the table by most recently updated coverage report
...@@ -106,25 +128,24 @@ export default { ...@@ -106,25 +128,24 @@ export default {
api.trackRedisHllUserEvent(this.$options.usagePingProjectEvent); api.trackRedisHllUserEvent(this.$options.usagePingProjectEvent);
} }
}, },
selectAllProjects(allProjects) { selectAllProjects() {
this.projectIds = Object.fromEntries(allProjects.map(({ id }) => [id, true]));
this.allProjectsSelected = true; this.allProjectsSelected = true;
}, },
toggleProject({ id }) { toggleProject({ id }) {
if (this.allProjectsSelected) { if (this.allProjectsSelected) {
// Reset all project selections to false // Reset all project selections to false
this.allProjectsSelected = false; this.allProjectsSelected = false;
this.projectIds = Object.fromEntries( this.selectedProjectIds = Object.fromEntries(
Object.entries(this.projectIds).map(([key]) => [key, false]), Object.entries(this.selectedProjectIds).map(([key]) => [key, false]),
); );
} }
if (Object.prototype.hasOwnProperty.call(this.projectIds, id)) { if (Object.prototype.hasOwnProperty.call(this.selectedProjectIds, id)) {
Vue.set(this.projectIds, id, !this.projectIds[id]); Vue.set(this.selectedProjectIds, id, !this.selectedProjectIds[id]);
return; return;
} }
Vue.set(this.projectIds, id, true); Vue.set(this.selectedProjectIds, id, true);
}, },
}, },
tableFields: [ tableFields: [
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
query getGroupProjects($groupFullPath: ID!, $after: String) { query getGroupProjects($groupFullPath: ID!, $after: String) {
group(fullPath: $groupFullPath) { group(fullPath: $groupFullPath) {
projects(after: $after, first: 100) { projects(hasCodeCoverage: true, after: $after, first: 100) {
nodes { nodes {
name name
id id
......
query getProjectsTestCoverage($projectIds: [ID!]) { query getProjectsTestCoverage($groupFullPath: ID!, $projectIds: [ID!]) {
projects(ids: $projectIds) { group(fullPath: $groupFullPath) {
nodes { projects(hasCodeCoverage: true, ids: $projectIds) {
fullPath nodes {
id fullPath
name id
repository { name
rootRef repository {
} rootRef
codeCoverageSummary { }
averageCoverage codeCoverageSummary {
coverageCount averageCoverage
lastUpdatedOn coverageCount
lastUpdatedOn
}
} }
} }
} }
......
---
title: Fix group code coverage table to show all projects correctly
merge_request: 56124
author:
type: fixed
import { GlTable } from '@gitlab/ui';
import { mount, shallowMount, createLocalVue } from '@vue/test-utils'; import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import SelectProjectsDropdown from 'ee/analytics/repository_analytics/components/select_projects_dropdown.vue';
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 getGroupProjects from 'ee/analytics/repository_analytics/graphql/queries/get_group_projects.query.graphql'; import getGroupProjects from 'ee/analytics/repository_analytics/graphql/queries/get_group_projects.query.graphql';
import getProjectsTestCoverage from 'ee/analytics/repository_analytics/graphql/queries/get_projects_test_coverage.query.graphql'; import getProjectsTestCoverage from 'ee/analytics/repository_analytics/graphql/queries/get_projects_test_coverage.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import Api from '~/api'; import Api from '~/api';
import { getTimeago } from '~/lib/utils/datetime_utility'; import { getTimeago } from '~/lib/utils/datetime_utility';
import { defaultTestCoverageTable, projects } from '../mock_data'; import { defaultTestCoverageTable, projects } from '../mock_data';
...@@ -12,54 +15,60 @@ import { defaultTestCoverageTable, projects } from '../mock_data'; ...@@ -12,54 +15,60 @@ import { defaultTestCoverageTable, projects } from '../mock_data';
jest.mock('~/api.js'); jest.mock('~/api.js');
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(VueApollo);
describe('Test coverage table component', () => { describe('Test coverage table component', () => {
let wrapper; let wrapper;
let getProjectsTestCoverageSpy;
const timeago = getTimeago(); const timeago = getTimeago();
const findEmptyState = () => wrapper.find('[data-testid="test-coverage-table-empty-state"]'); const findProjectsDropdown = () => wrapper.findComponent(SelectProjectsDropdown);
const findLoadingState = () => wrapper.find('[data-testid="test-coverage-loading-state"'); const findEmptyState = () => wrapper.findByTestId('test-coverage-table-empty-state');
const findTable = () => wrapper.find('[data-testid="test-coverage-data-table"'); const findLoadingState = () => wrapper.findByTestId('test-coverage-loading-state');
const findTable = () => wrapper.findComponent(GlTable);
const findTableRows = () => findTable().findAll('tbody tr'); const findTableRows = () => findTable().findAll('tbody tr');
const findProjectNameById = (id) => wrapper.find(`[data-testid="${id}-name"`); const findProjectNameById = (id) => wrapper.findByTestId(`${id}-name`);
const findProjectAverageById = (id) => wrapper.find(`[data-testid="${id}-average"`); const findProjectAverageById = (id) => wrapper.findByTestId(`${id}-average`);
const findProjectCountById = (id) => wrapper.find(`[data-testid="${id}-count"`); const findProjectCountById = (id) => wrapper.findByTestId(`${id}-count`);
const findProjectDateById = (id) => wrapper.find(`[data-testid="${id}-date"`); const findProjectDateById = (id) => wrapper.findByTestId(`${id}-date`);
const createMockApolloProvider = () => { const clickSelectAllProjects = async () => {
localVue.use(VueApollo); findProjectsDropdown().vm.$emit('select-all-projects');
return createMockApollo([ await nextTick();
jest.runOnlyPendingTimers();
await nextTick();
};
const createComponent = ({ glFeatures = {}, mockData = {}, mountFn = shallowMount } = {}) => {
const mockApollo = createMockApollo([
[getGroupProjects, jest.fn().mockResolvedValue()], [getGroupProjects, jest.fn().mockResolvedValue()],
[ [getProjectsTestCoverage, getProjectsTestCoverageSpy],
getProjectsTestCoverage,
jest.fn().mockResolvedValue({
data: { projects: { nodes: projects } },
}),
],
]); ]);
wrapper = extendedWrapper(
mountFn(TestCoverageTable, {
localVue,
apolloProvider: mockApollo,
data() {
return {
...defaultTestCoverageTable,
...mockData,
};
},
provide: {
glFeatures,
groupFullPath: 'gitlab-org',
},
}),
);
}; };
const createComponent = ({ beforeEach(() => {
glFeatures = {}, getProjectsTestCoverageSpy = jest.fn().mockResolvedValue({
mockApollo, data: { group: { projects: { nodes: projects } } },
mockData = {},
mountFn = shallowMount,
} = {}) => {
wrapper = mountFn(TestCoverageTable, {
localVue,
data() {
return {
...defaultTestCoverageTable,
...mockData,
};
},
apolloProvider: mockApollo,
provide: {
glFeatures,
},
}); });
}; });
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -83,19 +92,17 @@ describe('Test coverage table component', () => { ...@@ -83,19 +92,17 @@ describe('Test coverage table component', () => {
}); });
describe('when code coverage is available', () => { describe('when code coverage is available', () => {
it('renders coverage table', () => { it('renders coverage table', async () => {
const { const {
id, id,
name, name,
codeCoverageSummary: { averageCoverage, coverageCount, lastUpdatedOn }, codeCoverageSummary: { averageCoverage, coverageCount, lastUpdatedOn },
} = projects[0]; } = projects[0];
createComponent({ createComponent({ mountFn: mount });
mockData: {
allCoverageData: projects, await clickSelectAllProjects();
projectIds: { [id]: true },
}, expect(getProjectsTestCoverageSpy).toHaveBeenCalled();
mountFn: mount,
});
expect(findTable().exists()).toBe(true); expect(findTable().exists()).toBe(true);
expect(findProjectNameById(id).text()).toBe(name); expect(findProjectNameById(id).text()).toBe(name);
...@@ -104,40 +111,43 @@ describe('Test coverage table component', () => { ...@@ -104,40 +111,43 @@ describe('Test coverage table component', () => {
expect(findProjectDateById(id).text()).toBe(timeago.format(lastUpdatedOn)); expect(findProjectDateById(id).text()).toBe(timeago.format(lastUpdatedOn));
}); });
it('sorts the table by the most recently updated report', () => { it('sorts the table by the most recently updated report', async () => {
const project = projects[0]; const project = projects[0];
const today = '2021-01-30T20:34:14.302Z'; const today = '2021-01-30T20:34:14.302Z';
const yesterday = '2021-01-29T20:34:14.302Z'; const yesterday = '2021-01-29T20:34:14.302Z';
createComponent({ getProjectsTestCoverageSpy = jest.fn().mockResolvedValue({
mockData: { data: {
allCoverageData: [ group: {
{ projects: {
...project, nodes: [
name: 'should be last', {
id: 1, ...project,
codeCoverageSummary: { name: 'should be last',
...project.codeCoverageSummary, id: 1,
lastUpdatedOn: yesterday, codeCoverageSummary: {
}, ...project.codeCoverageSummary,
lastUpdatedOn: yesterday,
},
},
{
...project,
name: 'should be first',
id: 2,
codeCoverageSummary: {
...project.codeCoverageSummary,
lastUpdatedOn: today,
},
},
],
}, },
{
...project,
name: 'should be first',
id: 2,
codeCoverageSummary: {
...project.codeCoverageSummary,
lastUpdatedOn: today,
},
},
],
projectIds: {
1: true,
2: true,
}, },
}, },
mountFn: mount,
}); });
createComponent({ mountFn: mount });
await clickSelectAllProjects();
expect(findTable().exists()).toBe(true); expect(findTable().exists()).toBe(true);
expect(findTableRows().at(0).text()).toContain('should be first'); expect(findTableRows().at(0).text()).toContain('should be first');
expect(findTableRows().at(1).text()).toContain('should be last'); expect(findTableRows().at(1).text()).toContain('should be last');
...@@ -150,69 +160,48 @@ describe('Test coverage table component', () => { ...@@ -150,69 +160,48 @@ describe('Test coverage table component', () => {
repository: { rootRef }, repository: { rootRef },
} = projects[0]; } = projects[0];
const expectedPath = `/${fullPath}/-/graphs/${rootRef}/charts`; const expectedPath = `/${fullPath}/-/graphs/${rootRef}/charts`;
createComponent({ createComponent({ mountFn: mount });
mockApollo: createMockApolloProvider(),
mockData: { await clickSelectAllProjects();
projectIds: { [id]: true },
},
mountFn: mount,
});
// We have to wait for apollo to make the mock query and fill the table before
// we can click on the project link inside the table. Neither `runOnlyPendingTimers`
// nor `waitForPromises` work on their own to accomplish this.
jest.runOnlyPendingTimers();
await waitForPromises();
expect(findTable().exists()).toBe(true); expect(findTable().exists()).toBe(true);
expect(findProjectNameById(id).attributes('href')).toBe(expectedPath); expect(findProjectNameById(id).attributes('href')).toBe(expectedPath);
}); });
});
describe('with usage metrics', () => { describe('with usage metrics', () => {
describe('with :usageDataITestingGroupCodeCoverageProjectClickTotal enabled', () => { describe('with :usageDataITestingGroupCodeCoverageProjectClickTotal enabled', () => {
it('tracks i_testing_group_code_coverage_project_click_total metric', async () => { it('tracks i_testing_group_code_coverage_project_click_total metric', async () => {
const { id } = projects[0]; const { id } = projects[0];
createComponent({ createComponent({
glFeatures: { usageDataITestingGroupCodeCoverageProjectClickTotal: true }, glFeatures: { usageDataITestingGroupCodeCoverageProjectClickTotal: true },
mockApollo: createMockApolloProvider(), mountFn: mount,
mockData: {
projectIds: { [id]: true },
},
mountFn: mount,
});
// We have to wait for apollo to make the mock query and fill the table before
// we can click on the project link inside the table. Neither `runOnlyPendingTimers`
// nor `waitForPromises` work on their own to accomplish this.
jest.runOnlyPendingTimers();
await waitForPromises();
findProjectNameById(id).trigger('click');
expect(Api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
expect(Api.trackRedisHllUserEvent).toHaveBeenCalledWith(
wrapper.vm.$options.usagePingProjectEvent,
);
}); });
await clickSelectAllProjects();
findProjectNameById(id).trigger('click');
expect(Api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
expect(Api.trackRedisHllUserEvent).toHaveBeenCalledWith(
wrapper.vm.$options.usagePingProjectEvent,
);
}); });
});
describe('with :usageDataITestingGroupCodeCoverageProjectClickTotal disabled', () => { describe('with :usageDataITestingGroupCodeCoverageProjectClickTotal disabled', () => {
it('does not track i_testing_group_code_coverage_project_click_total metric', async () => { it('does not track i_testing_group_code_coverage_project_click_total metric', async () => {
const { id } = projects[0]; const { id } = projects[0];
createComponent({ createComponent({
glFeatures: { usageDataITestingGroupCodeCoverageProjectClickTotal: false }, glFeatures: { usageDataITestingGroupCodeCoverageProjectClickTotal: false },
mockApollo: createMockApolloProvider(), mountFn: mount,
mockData: {
projectIds: { [id]: true },
},
mountFn: mount,
});
// We have to wait for apollo to make the mock query and fill the table before
// we can click on the project link inside the table. Neither `runOnlyPendingTimers`
// nor `waitForPromises` work on their own to accomplish this.
jest.runOnlyPendingTimers();
await waitForPromises();
findProjectNameById(id).trigger('click');
expect(Api.trackRedisHllUserEvent).not.toHaveBeenCalled();
}); });
await clickSelectAllProjects();
findProjectNameById(id).trigger('click');
expect(Api.trackRedisHllUserEvent).not.toHaveBeenCalled();
}); });
}); });
}); });
......
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