Commit 5f0f1aa6 authored by Scott Hampton's avatar Scott Hampton

Ensure we fetch each project only once

Because we were incrementally adding
projects to our query, each query was
different and thus we couldn't take
advantage of caching. By keeping track
of all projects we receive, we can ensure
we only fetch the projects we haven't
already fetched.

Also refactored:
- Loading state
- Spec finders
- Spec mount params
- Spec tests to use variables
parent 42c895d9
<script>
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';
......@@ -15,64 +16,81 @@ export default {
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 {
coverageData: [],
hasError: false,
allProjectsSelected: false,
selectedProjectIds: [],
allCoverageData: [],
hasError: false,
isLoading: false,
projectIds: {},
};
},
computed: {
hasCoverageData() {
return this.coverageData.length;
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: {
getCoverageData() {
this.$apollo.addSmartQuery('coverageData', {
query: getProjectsTestCoverage,
debounce: 500,
variables() {
return {
projectIds: this.selectedProjectIds,
};
},
update(data) {
return data.projects.nodes;
},
error() {
this.handleError();
},
watchLoading(isLoading) {
this.isLoading = isLoading;
},
});
},
handleError() {
this.hasError = true;
},
selectAllProjects(allProjects) {
this.selectedProjectIds = allProjects.map(project => project.id);
this.projectIds = Object.fromEntries(allProjects.map(({ id }) => [id, true]));
this.allProjectsSelected = true;
this.getCoverageData();
},
selectProject({ id }) {
toggleProject({ id }) {
if (this.allProjectsSelected) {
// Clear out all the selected projects
// Reset all project selections to false
this.allProjectsSelected = false;
this.selectedProjectIds = [];
this.projectIds = Object.fromEntries(
Object.entries(this.projectIds).map(([key]) => [key, false]),
);
}
const index = this.selectedProjectIds.indexOf(id);
if (index < 0) {
this.selectedProjectIds.push(id);
} else {
this.selectedProjectIds.splice(index, 1);
if (Object.prototype.hasOwnProperty.call(this.projectIds, id)) {
Vue.set(this.projectIds, id, !this.projectIds[id]);
return;
}
this.getCoverageData();
Vue.set(this.projectIds, id, true);
},
},
tableFields: [
......@@ -99,6 +117,15 @@ export default {
'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>
......@@ -108,43 +135,36 @@ export default {
class="gl-w-quarter"
@projects-query-error="handleError"
@select-all-projects="selectAllProjects"
@select-project="selectProject"
@select-project="toggleProject"
/>
</template>
<gl-skeleton-loader
v-if="isLoading"
:width="430"
:height="55"
data-testid="test-coverage-loading-state"
>
<rect width="90" height="10" x="0" y="0" rx="4" />
<rect width="80" height="10" x="95" y="0" rx="4" />
<rect width="145" height="10" x="180" y="0" rx="4" />
<rect width="100" height="10" x="330" y="0" rx="4" />
<rect width="90" height="10" x="0" y="15" rx="4" />
<rect width="80" height="10" x="95" y="15" rx="4" />
<rect width="145" height="10" x="180" y="15" rx="4" />
<rect width="100" height="10" x="330" y="15" rx="4" />
<rect width="90" height="10" x="0" y="30" rx="4" />
<rect width="80" height="10" x="95" y="30" rx="4" />
<rect width="145" height="10" x="180" y="30" rx="4" />
<rect width="100" height="10" x="330" y="30" rx="4" />
<rect width="90" height="10" x="0" y="45" rx="4" />
<rect width="80" height="10" x="95" y="45" rx="4" />
<rect width="145" height="10" x="180" y="45" rx="4" />
<rect width="100" height="10" x="330" y="45" rx="4" />
</gl-skeleton-loader>
<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="coverageData"
:items="selectedCoverageData"
>
<template #head(project)="data">
<div>{{ data.label }}</div>
......
......@@ -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');
......@@ -80,24 +82,30 @@ describe('Select projects dropdown component', () => {
selectAllProjects();
expect(wrapper.vm.$emit).toHaveBeenCalledWith('select-all-projects', [
{ id: 1, name: '1', isSelected: false },
{ ...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');
......@@ -105,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(
......@@ -117,12 +125,12 @@ 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', {
id: 1,
name: '1',
...project,
isSelected: true,
});
});
......
......@@ -8,16 +8,24 @@ describe('Test coverage table component', () => {
useFakeDate();
let wrapper;
const createComponent = (mountFn = shallowMount, data = {}) => {
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 {
coverageData: [],
hasError: false,
allCoverageData: [],
allProjectsSelected: false,
selectedProjectIds: [],
hasError: false,
isLoading: false,
projectIds: {},
...data,
};
},
......@@ -41,18 +49,15 @@ describe('Test coverage table component', () => {
describe('when code coverage is empty', () => {
it('renders empty state', () => {
createComponent();
const emptyState = wrapper.find('[data-testid="test-coverage-table-empty-state"]');
expect(emptyState.exists()).toBe(true);
expect(findEmptyState().exists()).toBe(true);
});
});
describe('when query is loading', () => {
it('renders loading state', () => {
createComponent(shallowMount, { isLoading: true });
const loadingState = wrapper.find('[data-testid="test-coverage-loading-state"');
createComponent({ isLoading: true });
expect(loadingState.exists()).toBe(true);
expect(findLoadingState().exists()).toBe(true);
});
});
......@@ -65,30 +70,31 @@ describe('Test coverage table component', () => {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
createComponent(mount, {
coverageData: [
{
id,
name,
codeCoverage: {
average,
count,
lastUpdatedAt: yesterday.toISOString(),
createComponent(
{
allCoverageData: [
{
id,
name,
codeCoverage: {
average,
count,
lastUpdatedAt: yesterday.toISOString(),
},
},
],
projectIds: {
[id]: true,
},
],
});
const coverageTable = wrapper.find('[data-testid="test-coverage-data-table"');
const expectedName = wrapper.find(`[data-testid="${id}-name"`);
const expectedAverage = wrapper.find(`[data-testid="${id}-average"`);
const expectedCount = wrapper.find(`[data-testid="${id}-count"`);
const expectedDate = wrapper.find(`[data-testid="${id}-date"`);
},
mount,
);
expect(coverageTable.exists()).toBe(true);
expect(expectedName.text()).toBe(name);
expect(expectedAverage.text()).toBe(`${average}%`);
expect(expectedCount.text()).toBe(count);
expect(expectedDate.text()).toBe('1 day ago');
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');
});
});
});
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