Commit 5134cb37 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '215135-refactor-group-repositories-page' into 'master'

Refactored group repository analytics

See merge request gitlab-org/gitlab!43422
parents ae65cadb 762f7f4c
<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,
},
inject: {
groupAnalyticsCoverageReportsPath: {
type: String,
default: '',
},
},
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"
@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,
},
inject: {
groupAnalyticsCoverageReportsPath: {
type: String,
default: '',
},
groupFullPath: {
type: String,
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;
});
},
DownloadTestCoverage,
},
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 data-testid="test-coverage-header">
{{ $options.text.codeCoverageHeader }}
</h4>
<download-test-coverage />
</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,
},
inject: {
groupFullPath: {
type: String,
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.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 injectedProperties = {
groupAnalyticsCoverageReportsPath: '/coverage.csv?ref_path=refs/heads/master',
};
const createComponent = () => {
wrapper = shallowMount(DownloadTestCoverage, {
localVue,
data() {
return {
hasError: false,
allProjectsSelected: false,
selectedProjectIds: [],
};
},
provide: {
...injectedProperties,
},
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', () => {
it('displays an alert for the failed query', () => {
wrapper.setData({ hasError: true });
return wrapper.vm.$nextTick().then(() => {
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', () => {
it('renders primary action as a link with no project_ids param', () => {
wrapper.setData({ allProjectsSelected: true, selectedProjectIds: [] });
return wrapper.vm.$nextTick().then(() => {
expect(findCodeCoverageDownloadButton().attributes('href')).toBe(
groupAnalyticsCoverageReportsPathWithDates,
);
});
});
});
describe('with two or more projects selected without selecting all projects', () => {
it('renders primary action as a link with two project IDs as parameters', () => {
wrapper.setData({ allProjectsSelected: false, selectedProjectIds: [1, 2] });
const projectIdsQueryParam = `project_ids%5B%5D=1&project_ids%5B%5D=2`;
const expectedPath = `${groupAnalyticsCoverageReportsPathWithDates}&${projectIdsQueryParam}`;
return wrapper.vm.$nextTick().then(() => {
expect(findCodeCoverageDownloadButton().attributes('href')).toBe(expectedPath);
});
});
});
describe('with one project selected', () => {
it('renders primary action as a link with one project ID as a parameter', () => {
wrapper.setData({ allProjectsSelected: false, selectedProjectIds: [1] });
const projectIdsQueryParam = `project_ids%5B%5D=1`;
const expectedPath = `${groupAnalyticsCoverageReportsPathWithDates}&${projectIdsQueryParam}`;
return wrapper.vm.$nextTick().then(() => {
expect(findCodeCoverageDownloadButton().attributes('href')).toBe(expectedPath);
});
});
});
describe('with no projects selected', () => {
it('renders a disabled primary action button', () => {
expect(findCodeCoverageDownloadButton().attributes('disabled')).toBe('true');
});
});
describe('when clicking the select all button', () => {
it('selects all projects and removes the disabled attribute from the download button', () => {
wrapper.setData({ allProjectsSelected: false, selectedProjectIds: [] });
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} | ${`${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);
});
},
);
});
});
});
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 = {} } = {}) => {
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 },
});
const createComponent = () => {
wrapper = shallowMount(GroupRepositoryAnalytics, { localVue });
};
beforeEach(() => {
......@@ -82,164 +20,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 } });
});
describe('test coverage', () => {
it('renders test coverage header', () => {
const header = wrapper.find('[data-testid="test-coverage-header"]');
it('displays an alert for the failed query', () => {
expect(findAlert().exists()).toBe(true);
});
expect(header.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('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);
});
});
});
});
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,
};
},
provide: {
groupFullPath: 'gitlab-org',
},
mocks: {
$apollo: {
queries: {
groupProjects: {
fetchMore: jest.fn().mockResolvedValue(),
...apolloGroupProjects,
},
},
},
},
stubs: { GlDropdown, GlDropdownItem, GlIcon },
});
};
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', () => {
createComponent();
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', () => {
it('makes a query to fetch more projects', () => {
jest
.spyOn(wrapper.vm.$apollo.queries.groupProjects, 'fetchMore')
.mockImplementation(jest.fn().mockResolvedValue());
findIntersectionObserver().vm.$emit('appear');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.$apollo.queries.groupProjects.fetchMore).toHaveBeenCalledTimes(1);
});
});
describe('when the fetchMore query throws an error', () => {
it('emits an error event', () => {
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().then(() => {
expect(wrapper.vm.$emit).toHaveBeenCalledWith('projects-query-error');
});
});
});
});
describe('when a query is loading a new page of projects', () => {
it('should render the loading spinner', () => {
createComponent({
data: { projectsPageInfo: { hasNextPage: true } },
apolloGroupProjects: {
loading: true,
},
});
expect(findLoadingIcon().exists()).toBe(true);
});
});
});
});
......@@ -277,6 +277,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] ""
......@@ -2514,6 +2519,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