Commit 767b6af1 authored by Scott Hampton's avatar Scott Hampton

Refactored group repository analytics

Refactored group repository analytics component
to make room for adding more sections to the
test coverage area. Moved the projects dropdown
to its own component so it can be reused in an
upcoming section.

Also moved the modal button to be inside of a
card according to design.
parent a592e3fe
<script>
import {
GlAlert,
GlButton,
GlCard,
GlDropdown,
GlDropdownSectionHeader,
GlDropdownItem,
GlModal,
GlModalDirective,
} from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { pikadayToString } from '~/lib/utils/datetime_utility';
import SelectProjectsDropdown from './select_projects_dropdown.vue';
export default {
name: 'DownloadTestCoverage',
components: {
GlAlert,
GlButton,
GlCard,
GlDropdown,
GlDropdownSectionHeader,
GlDropdownItem,
GlModal,
SelectProjectsDropdown,
},
directives: {
GlModalDirective,
},
props: {
groupAnalyticsCoverageReportsPath: {
type: String,
required: true,
},
groupFullPath: {
type: String,
required: true,
},
},
data() {
return {
hasError: false,
allProjectsSelected: false,
selectedDateRange: this.$options.dateRangeOptions[2],
selectedProjectIds: [],
};
},
computed: {
cancelModalButton() {
return {
text: __('Cancel'),
};
},
csvReportPath() {
const today = new Date();
const endDate = pikadayToString(today);
today.setDate(today.getDate() - this.selectedDateRange.value);
const startDate = pikadayToString(today);
const queryParams = new URLSearchParams({
start_date: startDate,
end_date: endDate,
});
// not including a project_ids param is the same as selecting all the projects
if (!this.allProjectsSelected) {
this.selectedProjectIds.forEach(id => queryParams.append('project_ids[]', id));
}
return `${this.groupAnalyticsCoverageReportsPath}&${queryParams.toString()}`;
},
downloadCSVModalButton() {
return {
text: this.$options.text.downloadCSVModalButton,
attributes: [
{ variant: 'info' },
{ href: this.csvReportPath },
{ rel: 'nofollow' },
{ download: '' },
{ disabled: this.isDownloadButtonDisabled },
{ 'data-testid': 'group-code-coverage-download-button' },
],
};
},
isDownloadButtonDisabled() {
return !this.allProjectsSelected && !this.selectedProjectIds.length;
},
},
methods: {
clickDateRange(dateRange) {
this.selectedDateRange = dateRange;
},
clickSelectAllProjects() {
this.$refs.projectsDropdown.clickSelectAllProjects();
},
dismissError() {
this.hasError = false;
},
projectsQueryError() {
this.hasError = true;
},
selectAllProjects() {
this.allProjectsSelected = true;
this.selectedProjectIds = [];
},
selectProject(id) {
this.allProjectsSelected = false;
const index = this.selectedProjectIds.indexOf(id);
if (index < 0) {
this.selectedProjectIds.push(id);
return;
}
this.selectedProjectIds.splice(index, 1);
},
},
text: {
downloadTestCoverageHeader: s__('RepositoriesAnalytics|Download Historic Test Coverage Data'),
downloadCSVButton: s__('RepositoriesAnalytics|Download historic test coverage data (.csv)'),
dateRangeHeader: __('Date range'),
downloadCSVModalButton: s__('RepositoriesAnalytics|Download test coverage data (.csv)'),
downloadCSVModalTitle: s__('RepositoriesAnalytics|Download Historic Test Coverage Data'),
downloadCSVModalDescription: s__(
'RepositoriesAnalytics|Historic Test Coverage Data is available in raw format (.csv) for further analysis.',
),
projectDropdownHeader: __('Projects'),
projectSelectAll: __('Select all'),
queryErrorMessage: s__('RepositoriesAnalytics|There was an error fetching the projects.'),
},
dateRangeOptions: [
{ value: 7, text: __('Last week') },
{ value: 14, text: __('Last 2 weeks') },
{ value: 30, text: __('Last 30 days') },
{ value: 60, text: __('Last 60 days') },
{ value: 90, text: __('Last 90 days') },
],
};
</script>
<template>
<gl-card>
<template #header>
<h5>{{ $options.text.downloadTestCoverageHeader }}</h5>
</template>
<gl-button
v-gl-modal-directive="'download-csv-modal'"
category="primary"
variant="info"
data-testid="group-code-coverage-modal-button"
>{{ $options.text.downloadCSVButton }}</gl-button
>
<gl-modal
modal-id="download-csv-modal"
:title="$options.text.downloadCSVModalTitle"
no-fade
:action-primary="downloadCSVModalButton"
:action-cancel="cancelModalButton"
>
<gl-alert
v-if="hasError"
variant="danger"
data-testid="group-code-coverage-projects-error"
@dismiss="dismissError"
>{{ $options.text.queryErrorMessage }}</gl-alert
>
<div>{{ $options.text.downloadCSVModalDescription }}</div>
<div class="gl-my-4">
<label class="gl-display-block col-form-label-sm col-form-label">
{{ $options.text.projectDropdownHeader }}
</label>
<select-projects-dropdown
ref="projectsDropdown"
class="gl-w-half"
:group-full-path="groupFullPath"
@projects-query-error="projectsQueryError"
@select-all-projects="selectAllProjects"
@select-project="selectProject"
/>
<gl-button
class="gl-ml-2"
variant="link"
data-testid="group-code-coverage-select-all-projects-button"
@click="clickSelectAllProjects()"
>{{ $options.text.projectSelectAll }}</gl-button
>
</div>
<div class="gl-my-4">
<label class="gl-display-block col-form-label-sm col-form-label">
{{ $options.text.dateRangeHeader }}
</label>
<gl-dropdown :text="selectedDateRange.text" class="gl-w-half">
<gl-dropdown-section-header>
{{ $options.text.dateRangeHeader }}
</gl-dropdown-section-header>
<gl-dropdown-item
v-for="dateRange in $options.dateRangeOptions"
:key="dateRange.value"
:data-testid="`group-code-coverage-download-select-date-${dateRange.value}`"
@click="clickDateRange(dateRange)"
>{{ dateRange.text }}</gl-dropdown-item
>
</gl-dropdown>
</div>
</gl-modal>
</gl-card>
</template>
<script>
import {
GlAlert,
GlButton,
GlDropdown,
GlDropdownSectionHeader,
GlDropdownItem,
GlIntersectionObserver,
GlLoadingIcon,
GlModal,
GlModalDirective,
GlSearchBoxByType,
} from '@gitlab/ui';
import produce from 'immer';
import { __, s__ } from '~/locale';
import { pikadayToString } from '~/lib/utils/datetime_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import getGroupProjects from '../graphql/queries/get_group_projects.query.graphql';
import { s__ } from '~/locale';
import DownloadTestCoverage from './download_test_coverage.vue';
export default {
name: 'GroupRepositoryAnalytics',
components: {
GlAlert,
GlButton,
GlDropdown,
GlDropdownSectionHeader,
GlDropdownItem,
GlIntersectionObserver,
GlLoadingIcon,
GlModal,
GlSearchBoxByType,
},
directives: {
GlModalDirective,
DownloadTestCoverage,
},
inject: {
groupAnalyticsCoverageReportsPath: {
......@@ -43,239 +17,20 @@ export default {
default: '',
},
},
apollo: {
groupProjects: {
query: getGroupProjects,
variables() {
return {
groupFullPath: this.groupFullPath,
};
},
update(data) {
return data.group.projects.nodes.map(project => ({
...project,
id: getIdFromGraphQLId(project.id),
isSelected: false,
}));
},
result({ data }) {
this.projectsPageInfo = data?.group?.projects?.pageInfo || {};
},
error() {
this.hasError = true;
},
},
},
data() {
return {
groupProjects: [],
hasError: false,
projectsPageInfo: {},
projectSearchTerm: '',
selectAllProjects: true,
selectedDateRange: this.$options.dateRangeOptions[2],
};
},
computed: {
cancelModalButton() {
return {
text: __('Cancel'),
};
},
csvReportPath() {
const today = new Date();
const endDate = pikadayToString(today);
today.setDate(today.getDate() - this.selectedDateRange.value);
const startDate = pikadayToString(today);
const queryParams = new URLSearchParams({
start_date: startDate,
end_date: endDate,
});
// not including a project_ids param is the same as selecting all the projects
if (!this.selectAllProjects) {
this.selectedProjectIds.forEach(id => queryParams.append('project_ids[]', id));
}
return `${this.groupAnalyticsCoverageReportsPath}&${queryParams.toString()}`;
},
downloadCSVModalButton() {
return {
text: this.$options.text.downloadCSVModalButton,
attributes: [
{ variant: 'info' },
{ href: this.csvReportPath },
{ rel: 'nofollow' },
{ download: '' },
{ disabled: this.isDownloadButtonDisabled },
{ 'data-testid': 'group-code-coverage-download-button' },
],
};
},
isDownloadButtonDisabled() {
return !this.selectAllProjects && !this.groupProjects.some(project => project.isSelected);
},
filteredProjects() {
return this.groupProjects.filter(project =>
project.name.toLowerCase().includes(this.projectSearchTerm.toLowerCase()),
);
},
selectedProjectIds() {
return this.groupProjects.filter(project => project.isSelected).map(project => project.id);
},
},
methods: {
clickDropdownProject(id) {
const index = this.groupProjects.map(project => project.id).indexOf(id);
this.groupProjects[index].isSelected = !this.groupProjects[index].isSelected;
this.selectAllProjects = false;
},
clickSelectAllProjects() {
this.selectAllProjects = true;
this.groupProjects = this.groupProjects.map(project => ({
...project,
isSelected: false,
}));
},
clickDateRange(dateRange) {
this.selectedDateRange = dateRange;
},
dismissError() {
this.hasError = false;
},
loadMoreProjects() {
this.$apollo.queries.groupProjects
.fetchMore({
variables: {
groupFullPath: this.groupFullPath,
after: this.projectsPageInfo.endCursor,
},
updateQuery(previousResult, { fetchMoreResult }) {
const results = produce(fetchMoreResult, draftData => {
// eslint-disable-next-line no-param-reassign
draftData.group.projects.nodes = [
...previousResult.group.projects.nodes,
...draftData.group.projects.nodes,
];
});
return results;
},
})
.catch(() => {
this.hasError = true;
});
},
},
text: {
codeCoverageHeader: s__('RepositoriesAnalytics|Test Code Coverage'),
downloadCSVButton: s__('RepositoriesAnalytics|Download historic test coverage data (.csv)'),
dateRangeHeader: __('Date range'),
downloadCSVModalButton: s__('RepositoriesAnalytics|Download test coverage data (.csv)'),
downloadCSVModalTitle: s__('RepositoriesAnalytics|Download Historic Test Coverage Data'),
downloadCSVModalDescription: s__(
'RepositoriesAnalytics|Historic Test Coverage Data is available in raw format (.csv) for further analysis.',
),
projectDropdown: __('Select projects'),
projectDropdownHeader: __('Projects'),
projectDropdownAllProjects: __('All projects'),
projectSelectAll: __('Select all'),
queryErrorMessage: s__('RepositoriesAnalytics|There was an error fetching the projects.'),
},
dateRangeOptions: [
{ value: 7, text: __('Last week') },
{ value: 14, text: __('Last 2 weeks') },
{ value: 30, text: __('Last 30 days') },
{ value: 60, text: __('Last 60 days') },
{ value: 90, text: __('Last 90 days') },
],
};
</script>
<template>
<div class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-flex-wrap">
<h4 class="sub-header">{{ $options.text.codeCoverageHeader }}</h4>
<gl-button
v-gl-modal-directive="'download-csv-modal'"
data-testid="group-code-coverage-modal-button"
>{{ $options.text.downloadCSVButton }}</gl-button
>
<gl-modal
modal-id="download-csv-modal"
:title="$options.text.downloadCSVModalTitle"
no-fade
:action-primary="downloadCSVModalButton"
:action-cancel="cancelModalButton"
>
<gl-alert
v-if="hasError"
variant="danger"
data-testid="group-code-coverage-projects-error"
@dismiss="dismissError"
>{{ $options.text.queryErrorMessage }}</gl-alert
>
<div>{{ $options.text.downloadCSVModalDescription }}</div>
<div class="gl-my-4">
<label class="gl-display-block col-form-label-sm col-form-label">
{{ $options.text.projectDropdownHeader }}
</label>
<gl-dropdown
:text="$options.text.projectDropdown"
class="gl-w-half"
data-testid="group-code-coverage-project-dropdown"
>
<gl-dropdown-section-header>
{{ $options.text.projectDropdownHeader }}
</gl-dropdown-section-header>
<gl-search-box-by-type v-model.trim="projectSearchTerm" class="gl-my-2 gl-mx-3" />
<gl-dropdown-item
:is-check-item="true"
:is-checked="selectAllProjects"
data-testid="group-code-coverage-download-select-all-projects"
@click.native.capture.stop="clickSelectAllProjects()"
>{{ $options.text.projectDropdownAllProjects }}</gl-dropdown-item
>
<gl-dropdown-item
v-for="project in filteredProjects"
:key="project.id"
:is-check-item="true"
:is-checked="project.isSelected"
:data-testid="`group-code-coverage-download-select-project-${project.id}`"
@click.native.capture.stop="clickDropdownProject(project.id)"
>{{ project.name }}</gl-dropdown-item
>
<gl-intersection-observer v-if="projectsPageInfo.hasNextPage" @appear="loadMoreProjects">
<gl-loading-icon v-if="$apollo.queries.groupProjects.loading" size="md" />
</gl-intersection-observer>
</gl-dropdown>
<gl-button
class="gl-ml-2"
variant="link"
data-testid="group-code-coverage-select-all-projects-button"
@click="clickSelectAllProjects()"
>{{ $options.text.projectSelectAll }}</gl-button
>
</div>
<div class="gl-my-4">
<label class="gl-display-block col-form-label-sm col-form-label">
{{ $options.text.dateRangeHeader }}
</label>
<gl-dropdown :text="selectedDateRange.text" class="gl-w-half">
<gl-dropdown-section-header>
{{ $options.text.dateRangeHeader }}
</gl-dropdown-section-header>
<gl-dropdown-item
v-for="dateRange in $options.dateRangeOptions"
:key="dateRange.value"
:data-testid="`group-code-coverage-download-select-date-${dateRange.value}`"
@click="clickDateRange(dateRange)"
>{{ dateRange.text }}</gl-dropdown-item
>
</gl-dropdown>
</div>
</gl-modal>
<div>
<h4 class="sub-header" data-testid="test-coverage-header">
{{ $options.text.codeCoverageHeader }}
</h4>
<download-test-coverage
:group-analytics-coverage-reports-path="groupAnalyticsCoverageReportsPath"
:group-full-path="groupFullPath"
/>
</div>
</template>
<script>
import {
GlDropdown,
GlDropdownSectionHeader,
GlDropdownItem,
GlIntersectionObserver,
GlLoadingIcon,
GlModalDirective,
GlSearchBoxByType,
} from '@gitlab/ui';
import produce from 'immer';
import { __, n__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import getGroupProjects from '../graphql/queries/get_group_projects.query.graphql';
export default {
name: 'SelectProjectsDropdown',
components: {
GlDropdown,
GlDropdownSectionHeader,
GlDropdownItem,
GlIntersectionObserver,
GlLoadingIcon,
GlSearchBoxByType,
},
directives: {
GlModalDirective,
},
props: {
groupFullPath: {
type: String,
required: true,
},
},
apollo: {
groupProjects: {
query: getGroupProjects,
variables() {
return {
groupFullPath: this.groupFullPath,
};
},
update(data) {
return data.group.projects.nodes.map(project => ({
...project,
id: getIdFromGraphQLId(project.id),
isSelected: false,
}));
},
result({ data }) {
this.projectsPageInfo = data?.group?.projects?.pageInfo || {};
},
error() {
this.handleError();
},
},
},
data() {
return {
groupProjects: [],
projectsPageInfo: {},
projectSearchTerm: '',
selectAllProjects: false,
};
},
computed: {
filteredProjects() {
return this.groupProjects.filter(project =>
project.name.toLowerCase().includes(this.projectSearchTerm.toLowerCase()),
);
},
dropdownPlaceholder() {
if (this.selectAllProjects) {
return __('All projects selected');
}
if (this.selectedProjectIds.length) {
return n__('%d project selected', '%d projects selected', this.selectedProjectIds.length);
}
return __('Select projects');
},
selectedProjectIds() {
return this.groupProjects.filter(project => project.isSelected).map(project => project.id);
},
},
methods: {
clickDropdownProject(id) {
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);
},
clickSelectAllProjects() {
this.selectAllProjects = true;
this.groupProjects = this.groupProjects.map(project => ({
...project,
isSelected: false,
}));
this.$emit('select-all-projects');
},
handleError() {
this.$emit('projects-query-error');
},
loadMoreProjects() {
this.$apollo.queries.groupProjects
.fetchMore({
variables: {
groupFullPath: this.groupFullPath,
after: this.projectsPageInfo.endCursor,
},
updateQuery(previousResult, { fetchMoreResult }) {
const results = produce(fetchMoreResult, draftData => {
// eslint-disable-next-line no-param-reassign
draftData.group.projects.nodes = [
...previousResult.group.projects.nodes,
...draftData.group.projects.nodes,
];
});
return results;
},
})
.catch(() => {
this.handleError();
});
},
},
text: {
projectDropdownHeader: __('Projects'),
projectDropdownAllProjects: __('All projects'),
},
};
</script>
<template>
<gl-dropdown :text="dropdownPlaceholder" data-testid="select-projects-dropdown">
<gl-dropdown-section-header>
{{ $options.text.projectDropdownHeader }}
</gl-dropdown-section-header>
<gl-search-box-by-type v-model.trim="projectSearchTerm" class="gl-my-2 gl-mx-3" />
<gl-dropdown-item
:is-check-item="true"
:is-checked="selectAllProjects"
data-testid="select-all-projects"
@click.native.capture.stop="clickSelectAllProjects()"
>{{ $options.text.projectDropdownAllProjects }}</gl-dropdown-item
>
<gl-dropdown-item
v-for="project in filteredProjects"
:key="project.id"
:is-check-item="true"
:is-checked="project.isSelected"
:data-testid="`select-project-${project.id}`"
@click.native.capture.stop="clickDropdownProject(project.id)"
>{{ project.name }}</gl-dropdown-item
>
<gl-intersection-observer v-if="projectsPageInfo.hasNextPage" @appear="loadMoreProjects">
<gl-loading-icon v-if="$apollo.queries.groupProjects.loading" size="md" />
</gl-intersection-observer>
</gl-dropdown>
</template>
---
title: Move the download test coverage button to its own section in group repositories
analytics page
merge_request: 43422
author:
type: changed
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlAlert, GlDropdown, GlDropdownItem, GlModal } from '@gitlab/ui';
import { useFakeDate } from 'helpers/fake_date';
import DownloadTestCoverage from 'ee/analytics/repository_analytics/components/download_test_coverage.vue';
import SelectProjectsDropdown from 'ee/analytics/repository_analytics/components/select_projects_dropdown.vue';
const localVue = createLocalVue();
describe('Download test coverage component', () => {
useFakeDate();
let wrapper;
const findCodeCoverageModalButton = () =>
wrapper.find('[data-testid="group-code-coverage-modal-button"]');
const openCodeCoverageModal = () => {
findCodeCoverageModalButton().vm.$emit('click');
};
const findCodeCoverageDownloadButton = () =>
wrapper.find('[data-testid="group-code-coverage-download-button"]');
const clickSelectAllProjectsButton = () =>
wrapper
.find('[data-testid="group-code-coverage-select-all-projects-button"]')
.vm.$emit('click');
const findAlert = () => wrapper.find(GlAlert);
const defaultProps = {
groupAnalyticsCoverageReportsPath: '/coverage.csv?ref_path=refs/heads/master',
groupFullPath: 'gitlab-org',
};
const createComponent = (data = {}) => {
wrapper = shallowMount(DownloadTestCoverage, {
localVue,
data() {
return {
hasError: false,
...data,
};
},
propsData: {
...defaultProps,
},
stubs: { GlDropdown, GlDropdownItem, GlModal, SelectProjectsDropdown },
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders button to open download code coverage modal', () => {
expect(findCodeCoverageModalButton().exists()).toBe(true);
});
describe('when download code coverage modal is displayed', () => {
beforeEach(() => {
openCodeCoverageModal();
});
describe('when there is an error fetching the projects', () => {
beforeEach(() => {
createComponent({ hasError: true });
});
it('displays an alert for the failed query', () => {
expect(findAlert().exists()).toBe(true);
});
});
describe('when selecting a project', () => {
// Due to the fake_date helper, we can always expect today's date to be 2020-07-06
// and the default date 30 days ago to be 2020-06-06
const groupAnalyticsCoverageReportsPathWithDates = `${defaultProps.groupAnalyticsCoverageReportsPath}&start_date=2020-06-06&end_date=2020-07-06`;
describe('with all projects selected', () => {
beforeEach(() => {
createComponent({ allProjectsSelected: true });
});
it('renders primary action as a link with no project_ids param', () => {
expect(findCodeCoverageDownloadButton().attributes('href')).toBe(
groupAnalyticsCoverageReportsPathWithDates,
);
});
});
describe('with two or more projects selected without selecting all projects', () => {
beforeEach(() => {
createComponent({ allProjectsSelected: false, selectedProjectIds: [1, 2] });
});
it('renders primary action as a link with two project IDs as parameters', () => {
const projectIdsQueryParam = `project_ids%5B%5D=1&project_ids%5B%5D=2`;
const expectedPath = `${groupAnalyticsCoverageReportsPathWithDates}&${projectIdsQueryParam}`;
expect(findCodeCoverageDownloadButton().attributes('href')).toBe(expectedPath);
});
});
describe('with one project selected', () => {
beforeEach(() => {
createComponent({ allProjectsSelected: false, selectedProjectIds: [1] });
});
it('renders primary action as a link with one project ID as a parameter', () => {
const projectIdsQueryParam = `project_ids%5B%5D=1`;
const expectedPath = `${groupAnalyticsCoverageReportsPathWithDates}&${projectIdsQueryParam}`;
expect(findCodeCoverageDownloadButton().attributes('href')).toBe(expectedPath);
});
});
describe('with no projects selected', () => {
beforeEach(() => {
createComponent({ allProjectsSelected: false, selectedProjectIds: [] });
});
it('renders a disabled primary action button', () => {
expect(findCodeCoverageDownloadButton().attributes('disabled')).toBe('true');
});
});
describe('when clicking the select all button', () => {
beforeEach(() => {
createComponent({ allProjectsSelected: false, selectedProjectIds: [] });
});
it('selects all projects and removes the disabled attribute from the download button', () => {
clickSelectAllProjectsButton();
return wrapper.vm.$nextTick().then(() => {
expect(findCodeCoverageDownloadButton().attributes('href')).toBe(
groupAnalyticsCoverageReportsPathWithDates,
);
expect(findCodeCoverageDownloadButton().attributes('disabled')).toBeUndefined();
});
});
});
});
describe('when selecting a date range', () => {
it.each`
date | expected
${7} | ${`${defaultProps.groupAnalyticsCoverageReportsPath}&start_date=2020-06-29&end_date=2020-07-06`}
${14} | ${`${defaultProps.groupAnalyticsCoverageReportsPath}&start_date=2020-06-22&end_date=2020-07-06`}
${30} | ${`${defaultProps.groupAnalyticsCoverageReportsPath}&start_date=2020-06-06&end_date=2020-07-06`}
${60} | ${`${defaultProps.groupAnalyticsCoverageReportsPath}&start_date=2020-05-07&end_date=2020-07-06`}
${90} | ${`${defaultProps.groupAnalyticsCoverageReportsPath}&start_date=2020-04-07&end_date=2020-07-06`}
`(
'updates CSV path to have the start date be $date days before today',
({ date, expected }) => {
wrapper
.find(`[data-testid="group-code-coverage-download-select-date-${date}"]`)
.vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(findCodeCoverageDownloadButton().attributes('href')).toBe(expected);
});
},
);
});
});
});
import { shallowMount, createLocalVue } from '@vue/test-utils';
import {
GlAlert,
GlDropdown,
GlDropdownItem,
GlIntersectionObserver,
GlLoadingIcon,
GlModal,
} from '@gitlab/ui';
import { useFakeDate } from 'helpers/fake_date';
import GroupRepositoryAnalytics from 'ee/analytics/repository_analytics/components/group_repository_analytics.vue';
import DownloadTestCoverage from 'ee/analytics/repository_analytics/components/download_test_coverage.vue';
const localVue = createLocalVue();
describe('Group repository analytics app', () => {
useFakeDate();
let wrapper;
const findCodeCoverageModalButton = () =>
wrapper.find('[data-testid="group-code-coverage-modal-button"]');
const openCodeCoverageModal = () => {
findCodeCoverageModalButton().vm.$emit('click');
};
const findCodeCoverageDownloadButton = () =>
wrapper.find('[data-testid="group-code-coverage-download-button"]');
const selectAllCodeCoverageProjects = () =>
wrapper
.find('[data-testid="group-code-coverage-download-select-all-projects"]')
.trigger('click');
const selectCodeCoverageProjectById = id =>
wrapper
.find(`[data-testid="group-code-coverage-download-select-project-${id}"]`)
.trigger('click');
const findIntersectionObserver = () => wrapper.find(GlIntersectionObserver);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findAlert = () => wrapper.find(GlAlert);
const injectedProperties = {
groupAnalyticsCoverageReportsPath: '/coverage.csv?ref_path=refs/heads/master',
groupFullPath: 'gitlab-org',
};
const groupProjectsData = [{ id: 1, name: '1' }, { id: 2, name: '2' }];
const createComponent = ({ data = {}, apolloGroupProjects = {} } = {}) => {
const createComponent = () => {
wrapper = shallowMount(GroupRepositoryAnalytics, {
localVue,
data() {
return {
// Ensure that isSelected is set to false for each project so that every test is reset properly
groupProjects: groupProjectsData.map(project => ({ ...project, isSelected: false })),
hasError: false,
projectsPageInfo: {
hasNextPage: false,
endCursor: null,
},
...data,
};
},
provide: {
...injectedProperties,
},
mocks: {
$apollo: {
queries: {
groupProjects: {
fetchMore: jest.fn().mockResolvedValue(),
...apolloGroupProjects,
},
},
},
},
stubs: { GlDropdown, GlDropdownItem, GlModal },
});
};
......@@ -82,164 +30,15 @@ describe('Group repository analytics app', () => {
wrapper = null;
});
it('renders button to open download code coverage modal', () => {
expect(findCodeCoverageModalButton().exists()).toBe(true);
});
describe('when download code coverage modal is displayed', () => {
beforeEach(() => {
openCodeCoverageModal();
});
describe('when there is an error fetching the projects', () => {
beforeEach(() => {
createComponent({ data: { hasError: true } });
});
it('displays an alert for the failed query', () => {
expect(findAlert().exists()).toBe(true);
});
});
describe('when selecting a project', () => {
// Due to the fake_date helper, we can always expect today's date to be 2020-07-06
// and the default date 30 days ago to be 2020-06-06
const groupAnalyticsCoverageReportsPathWithDates = `${injectedProperties.groupAnalyticsCoverageReportsPath}&start_date=2020-06-06&end_date=2020-07-06`;
describe('with all projects selected', () => {
beforeEach(() => {
selectAllCodeCoverageProjects();
});
it('renders primary action as a link with no project_ids param', () => {
expect(findCodeCoverageDownloadButton().attributes('href')).toBe(
groupAnalyticsCoverageReportsPathWithDates,
);
});
});
describe('with two or more projects selected without selecting all projects', () => {
beforeEach(() => {
selectCodeCoverageProjectById(groupProjectsData[0].id);
selectCodeCoverageProjectById(groupProjectsData[1].id);
});
it('renders primary action as a link with two project IDs as parameters', () => {
const projectIdsQueryParam = `project_ids%5B%5D=${groupProjectsData[0].id}&project_ids%5B%5D=${groupProjectsData[1].id}`;
const expectedPath = `${groupAnalyticsCoverageReportsPathWithDates}&${projectIdsQueryParam}`;
expect(findCodeCoverageDownloadButton().attributes('href')).toBe(expectedPath);
});
});
describe('test coverage', () => {
it('renders test coverage header', () => {
const header = wrapper.find('[data-testid="test-coverage-header"]');
describe('with one project selected', () => {
beforeEach(() => {
selectCodeCoverageProjectById(groupProjectsData[0].id);
});
it('renders primary action as a link with one project ID as a parameter', () => {
const projectIdsQueryParam = `project_ids%5B%5D=${groupProjectsData[0].id}`;
const expectedPath = `${groupAnalyticsCoverageReportsPathWithDates}&${projectIdsQueryParam}`;
expect(findCodeCoverageDownloadButton().attributes('href')).toBe(expectedPath);
});
});
describe('with no projects selected', () => {
beforeEach(() => {
// Select a project to make sure that "Select all" is unchecked
selectCodeCoverageProjectById(groupProjectsData[0].id);
// Click the same project again to unselect it
selectCodeCoverageProjectById(groupProjectsData[0].id);
});
it('renders a disabled primary action button', () => {
expect(findCodeCoverageDownloadButton().attributes('disabled')).toBe('true');
});
});
describe('when there is only one page of projects', () => {
it('should not render the intersection observer component', () => {
expect(findIntersectionObserver().exists()).toBe(false);
});
});
describe('when there is more than a page of projects', () => {
beforeEach(() => {
createComponent({ data: { projectsPageInfo: { hasNextPage: true } } });
});
it('should render the intersection observer component', () => {
expect(findIntersectionObserver().exists()).toBe(true);
});
describe('when the intersection observer component appears in view', () => {
beforeEach(() => {
jest
.spyOn(wrapper.vm.$apollo.queries.groupProjects, 'fetchMore')
.mockImplementation(jest.fn().mockResolvedValue());
findIntersectionObserver().vm.$emit('appear');
return wrapper.vm.$nextTick();
});
it('makes a query to fetch more projects', () => {
expect(wrapper.vm.$apollo.queries.groupProjects.fetchMore).toHaveBeenCalledTimes(1);
});
describe('when the fetchMore query throws an error', () => {
beforeEach(() => {
jest
.spyOn(wrapper.vm.$apollo.queries.groupProjects, 'fetchMore')
.mockImplementation(jest.fn().mockRejectedValue());
findIntersectionObserver().vm.$emit('appear');
return wrapper.vm.$nextTick();
});
it('displays an alert for the failed query', () => {
expect(findAlert().exists()).toBe(true);
});
});
});
describe('when a query is loading a new page of projects', () => {
beforeEach(() => {
createComponent({
data: { projectsPageInfo: { hasNextPage: true } },
apolloGroupProjects: {
loading: true,
},
});
});
it('should render the loading spinner', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
});
});
expect(header.exists()).toBe(true);
});
describe('when selecting a date range', () => {
it.each`
date | expected
${7} | ${`${injectedProperties.groupAnalyticsCoverageReportsPath}&start_date=2020-06-29&end_date=2020-07-06`}
${14} | ${`${injectedProperties.groupAnalyticsCoverageReportsPath}&start_date=2020-06-22&end_date=2020-07-06`}
${30} | ${`${injectedProperties.groupAnalyticsCoverageReportsPath}&start_date=2020-06-06&end_date=2020-07-06`}
${60} | ${`${injectedProperties.groupAnalyticsCoverageReportsPath}&start_date=2020-05-07&end_date=2020-07-06`}
${90} | ${`${injectedProperties.groupAnalyticsCoverageReportsPath}&start_date=2020-04-07&end_date=2020-07-06`}
`(
'updates CSV path to have the start date be $date days before today',
({ date, expected }) => {
wrapper
.find(`[data-testid="group-code-coverage-download-select-date-${date}"]`)
.vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(findCodeCoverageDownloadButton().attributes('href')).toBe(expected);
});
},
);
it('renders the download test coverage component', () => {
expect(wrapper.find(DownloadTestCoverage).exists()).toBe(true);
});
});
});
import { shallowMount, createLocalVue } from '@vue/test-utils';
import {
GlDropdown,
GlDropdownItem,
GlIntersectionObserver,
GlLoadingIcon,
GlIcon,
} from '@gitlab/ui';
import SelectProjectsDropdown from 'ee/analytics/repository_analytics/components/select_projects_dropdown.vue';
const localVue = createLocalVue();
describe('Select projects dropdown component', () => {
let wrapper;
const findSelectAllProjects = () => wrapper.find('[data-testid="select-all-projects"]');
const findProjectById = id => wrapper.find(`[data-testid="select-project-${id}"]`);
const selectAllProjects = () => findSelectAllProjects().trigger('click');
const selectProjectById = id => findProjectById(id).trigger('click');
const findIntersectionObserver = () => wrapper.find(GlIntersectionObserver);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const createComponent = ({ data = {}, apolloGroupProjects = {} } = {}) => {
wrapper = shallowMount(SelectProjectsDropdown, {
localVue,
data() {
return {
groupProjects: [
{ id: 1, name: '1', isSelected: false },
{ id: 2, name: '2', isSelected: false },
],
projectsPageInfo: {
hasNextPage: false,
endCursor: null,
},
...data,
};
},
propsData: {
groupFullPath: 'gitlab-org',
},
mocks: {
$apollo: {
queries: {
groupProjects: {
fetchMore: jest.fn().mockResolvedValue(),
...apolloGroupProjects,
},
},
},
},
stubs: { GlDropdown, GlDropdownItem, GlIcon },
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when selecting all project', () => {
beforeEach(() => {
createComponent({ data: { groupProjects: [{ id: 1, name: '1', isSelected: true }] } });
});
it('should reset all selected projects', () => {
selectAllProjects();
return wrapper.vm.$nextTick().then(() => {
expect(
findProjectById(1)
.find(GlIcon)
.classes(),
).toContain('gl-visibility-hidden');
});
});
it('should emit select-all-projects event', () => {
jest.spyOn(wrapper.vm, '$emit');
selectAllProjects();
expect(wrapper.vm.$emit).toHaveBeenCalledWith('select-all-projects');
});
});
describe('when selecting a project', () => {
beforeEach(() => {
createComponent({
data: { groupProjects: [{ id: 1, name: '1', isSelected: false }], selectAllProjects: true },
});
});
it('should check selected project', () => {
selectProjectById(1);
return wrapper.vm.$nextTick().then(() => {
expect(
findProjectById(1)
.find(GlIcon)
.classes(),
).not.toContain('gl-visibility-hidden');
});
});
it('should uncheck select all projects', () => {
selectProjectById(1);
return wrapper.vm.$nextTick().then(() => {
expect(
findSelectAllProjects()
.find(GlIcon)
.classes(),
).toContain('gl-visibility-hidden');
});
});
it('should emit select-project event', () => {
jest.spyOn(wrapper.vm, '$emit');
selectProjectById(1);
expect(wrapper.vm.$emit).toHaveBeenCalledWith('select-project', 1);
});
});
describe('when there is only one page of projects', () => {
it('should not render the intersection observer component', () => {
expect(findIntersectionObserver().exists()).toBe(false);
});
});
describe('when there is more than a page of projects', () => {
beforeEach(() => {
createComponent({ data: { projectsPageInfo: { hasNextPage: true } } });
});
it('should render the intersection observer component', () => {
expect(findIntersectionObserver().exists()).toBe(true);
});
describe('when the intersection observer component appears in view', () => {
beforeEach(() => {
jest
.spyOn(wrapper.vm.$apollo.queries.groupProjects, 'fetchMore')
.mockImplementation(jest.fn().mockResolvedValue());
findIntersectionObserver().vm.$emit('appear');
return wrapper.vm.$nextTick();
});
it('makes a query to fetch more projects', () => {
expect(wrapper.vm.$apollo.queries.groupProjects.fetchMore).toHaveBeenCalledTimes(1);
});
describe('when the fetchMore query throws an error', () => {
beforeEach(() => {
jest.spyOn(wrapper.vm, '$emit');
jest
.spyOn(wrapper.vm.$apollo.queries.groupProjects, 'fetchMore')
.mockImplementation(jest.fn().mockRejectedValue());
findIntersectionObserver().vm.$emit('appear');
return wrapper.vm.$nextTick();
});
it('emits an error event', () => {
expect(wrapper.vm.$emit).toHaveBeenCalledWith('projects-query-error');
});
});
});
describe('when a query is loading a new page of projects', () => {
beforeEach(() => {
createComponent({
data: { projectsPageInfo: { hasNextPage: true } },
apolloGroupProjects: {
loading: true,
},
});
});
it('should render the loading spinner', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
});
});
});
......@@ -270,6 +270,11 @@ msgid_plural "%d projects"
msgstr[0] ""
msgstr[1] ""
msgid "%d project selected"
msgid_plural "%d projects selected"
msgstr[0] ""
msgstr[1] ""
msgid "%d request with warnings"
msgid_plural "%d requests with warnings"
msgstr[0] ""
......@@ -2510,6 +2515,9 @@ msgstr ""
msgid "All projects"
msgstr ""
msgid "All projects selected"
msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
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