summaryrefslogtreecommitdiff
path: root/spec/frontend/analytics
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-10-20 08:43:02 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-10-20 08:43:02 +0000
commitd9ab72d6080f594d0b3cae15f14b3ef2c6c638cb (patch)
tree2341ef426af70ad1e289c38036737e04b0aa5007 /spec/frontend/analytics
parentd6e514dd13db8947884cd58fe2a9c2a063400a9b (diff)
downloadgitlab-ce-d9ab72d6080f594d0b3cae15f14b3ef2c6c638cb.tar.gz
Add latest changes from gitlab-org/gitlab@14-4-stable-eev14.4.0-rc42
Diffstat (limited to 'spec/frontend/analytics')
-rw-r--r--spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js139
-rw-r--r--spec/frontend/analytics/shared/utils_spec.js156
2 files changed, 278 insertions, 17 deletions
diff --git a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
index 2537b8fb816..5d681c7da4f 100644
--- a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
+++ b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
@@ -1,6 +1,8 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
import { TEST_HOST } from 'helpers/test_constants';
+import waitForPromises from 'helpers/wait_for_promises';
import ProjectsDropdownFilter from '~/analytics/shared/components/projects_dropdown_filter.vue';
import getProjects from '~/analytics/shared/graphql/projects.query.graphql';
@@ -25,6 +27,17 @@ const projects = [
},
];
+const MockGlDropdown = stubComponent(GlDropdown, {
+ template: `
+ <div>
+ <div data-testid="vsa-highlighted-items">
+ <slot name="highlighted-items"></slot>
+ </div>
+ <div data-testid="vsa-default-items"><slot></slot></div>
+ </div>
+ `,
+});
+
const defaultMocks = {
$apollo: {
query: jest.fn().mockResolvedValue({
@@ -38,22 +51,33 @@ let spyQuery;
describe('ProjectsDropdownFilter component', () => {
let wrapper;
- const createComponent = (props = {}) => {
+ const createComponent = (props = {}, stubs = {}) => {
spyQuery = defaultMocks.$apollo.query;
- wrapper = mount(ProjectsDropdownFilter, {
+ wrapper = mountExtended(ProjectsDropdownFilter, {
mocks: { ...defaultMocks },
propsData: {
groupId: 1,
groupNamespace: 'gitlab-org',
...props,
},
+ stubs,
});
};
+ const createWithMockDropdown = (props) => {
+ createComponent(props, { GlDropdown: MockGlDropdown });
+ return waitForPromises();
+ };
+
afterEach(() => {
wrapper.destroy();
});
+ const findHighlightedItems = () => wrapper.findByTestId('vsa-highlighted-items');
+ const findUnhighlightedItems = () => wrapper.findByTestId('vsa-default-items');
+ const findHighlightedItemsTitle = () => wrapper.findByText('Selected');
+ const findClearAllButton = () => wrapper.findByText('Clear all');
+
const findDropdown = () => wrapper.find(GlDropdown);
const findDropdownItems = () =>
@@ -75,8 +99,19 @@ describe('ProjectsDropdownFilter component', () => {
const findDropdownFullPathAtIndex = (index) =>
findDropdownAtIndex(index).find('[data-testid="project-full-path"]');
- const selectDropdownItemAtIndex = (index) =>
+ const selectDropdownItemAtIndex = (index) => {
findDropdownAtIndex(index).find('button').trigger('click');
+ return wrapper.vm.$nextTick();
+ };
+
+ // NOTE: Selected items are now visually separated from unselected items
+ const findSelectedDropdownItems = () => findHighlightedItems().findAll(GlDropdownItem);
+
+ const findSelectedDropdownAtIndex = (index) => findSelectedDropdownItems().at(index);
+ const findSelectedButtonIdentIconAtIndex = (index) =>
+ findSelectedDropdownAtIndex(index).find('div.gl-avatar-identicon');
+ const findSelectedButtonAvatarItemAtIndex = (index) =>
+ findSelectedDropdownAtIndex(index).find('img.gl-avatar');
const selectedIds = () => wrapper.vm.selectedProjects.map(({ id }) => id);
@@ -109,7 +144,80 @@ describe('ProjectsDropdownFilter component', () => {
});
});
- describe('when passed a an array of defaultProject as prop', () => {
+ describe('highlighted items', () => {
+ const blockDefaultProps = { multiSelect: true };
+ beforeEach(() => {
+ createComponent(blockDefaultProps);
+ });
+
+ describe('with no project selected', () => {
+ it('does not render the highlighted items', async () => {
+ await createWithMockDropdown(blockDefaultProps);
+ expect(findSelectedDropdownItems().length).toBe(0);
+ });
+
+ it('does not render the highlighted items title', () => {
+ expect(findHighlightedItemsTitle().exists()).toBe(false);
+ });
+
+ it('does not render the clear all button', () => {
+ expect(findClearAllButton().exists()).toBe(false);
+ });
+ });
+
+ describe('with a selected project', () => {
+ beforeEach(async () => {
+ await selectDropdownItemAtIndex(0);
+ });
+
+ it('renders the highlighted items', async () => {
+ await createWithMockDropdown(blockDefaultProps);
+ await selectDropdownItemAtIndex(0);
+
+ expect(findSelectedDropdownItems().length).toBe(1);
+ });
+
+ it('renders the highlighted items title', () => {
+ expect(findHighlightedItemsTitle().exists()).toBe(true);
+ });
+
+ it('renders the clear all button', () => {
+ expect(findClearAllButton().exists()).toBe(true);
+ });
+
+ it('clears all selected items when the clear all button is clicked', async () => {
+ await selectDropdownItemAtIndex(1);
+
+ expect(wrapper.text()).toContain('2 projects selected');
+
+ findClearAllButton().trigger('click');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.text()).not.toContain('2 projects selected');
+ expect(wrapper.text()).toContain('Select projects');
+ });
+ });
+ });
+
+ describe('with a selected project and search term', () => {
+ beforeEach(async () => {
+ await createWithMockDropdown({ multiSelect: true });
+
+ selectDropdownItemAtIndex(0);
+ wrapper.setData({ searchTerm: 'this is a very long search string' });
+ });
+
+ it('renders the highlighted items', async () => {
+ expect(findUnhighlightedItems().findAll('li').length).toBe(1);
+ });
+
+ it('hides the unhighlighted items that do not match the string', async () => {
+ expect(findUnhighlightedItems().findAll('li').length).toBe(1);
+ expect(findUnhighlightedItems().text()).toContain('No matching results');
+ });
+ });
+
+ describe('when passed an array of defaultProject as prop', () => {
beforeEach(() => {
createComponent({
defaultProjects: [projects[0]],
@@ -130,8 +238,9 @@ describe('ProjectsDropdownFilter component', () => {
});
describe('when multiSelect is false', () => {
+ const blockDefaultProps = { multiSelect: false };
beforeEach(() => {
- createComponent({ multiSelect: false });
+ createComponent(blockDefaultProps);
});
describe('displays the correct information', () => {
@@ -183,21 +292,19 @@ describe('ProjectsDropdownFilter component', () => {
});
it('renders an avatar in the dropdown button when the project has an avatarUrl', async () => {
- selectDropdownItemAtIndex(0);
+ await createWithMockDropdown(blockDefaultProps);
+ await selectDropdownItemAtIndex(0);
- await wrapper.vm.$nextTick().then(() => {
- expect(findDropdownButtonAvatarAtIndex(0).exists()).toBe(true);
- expect(findDropdownButtonIdentIconAtIndex(0).exists()).toBe(false);
- });
+ expect(findSelectedButtonAvatarItemAtIndex(0).exists()).toBe(true);
+ expect(findSelectedButtonIdentIconAtIndex(0).exists()).toBe(false);
});
it("renders an identicon in the dropdown button when the project doesn't have an avatarUrl", async () => {
- selectDropdownItemAtIndex(1);
+ await createWithMockDropdown(blockDefaultProps);
+ await selectDropdownItemAtIndex(1);
- await wrapper.vm.$nextTick().then(() => {
- expect(findDropdownButtonAvatarAtIndex(1).exists()).toBe(false);
- expect(findDropdownButtonIdentIconAtIndex(1).exists()).toBe(true);
- });
+ expect(findSelectedButtonAvatarItemAtIndex(0).exists()).toBe(false);
+ expect(findSelectedButtonIdentIconAtIndex(0).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/analytics/shared/utils_spec.js b/spec/frontend/analytics/shared/utils_spec.js
index e3293f2d8bd..0513ccb2890 100644
--- a/spec/frontend/analytics/shared/utils_spec.js
+++ b/spec/frontend/analytics/shared/utils_spec.js
@@ -1,4 +1,10 @@
-import { filterBySearchTerm } from '~/analytics/shared/utils';
+import {
+ filterBySearchTerm,
+ extractFilterQueryParameters,
+ extractPaginationQueryParameters,
+ getDataZoomOption,
+} from '~/analytics/shared/utils';
+import { objectToQuery } from '~/lib/utils/url_utility';
describe('filterBySearchTerm', () => {
const data = [
@@ -22,3 +28,151 @@ describe('filterBySearchTerm', () => {
expect(filterBySearchTerm(data, 'ne', 'title')).toEqual([data[0]]);
});
});
+
+describe('extractFilterQueryParameters', () => {
+ const selectedAuthor = 'Author 1';
+ const selectedMilestone = 'Milestone 1.0';
+ const selectedSourceBranch = 'main';
+ const selectedTargetBranch = 'feature-1';
+ const selectedAssigneeList = ['Alice', 'Bob'];
+ const selectedLabelList = ['Label 1', 'Label 2'];
+
+ const queryParamsString = objectToQuery({
+ source_branch_name: selectedSourceBranch,
+ target_branch_name: selectedTargetBranch,
+ author_username: selectedAuthor,
+ milestone_title: selectedMilestone,
+ assignee_username: selectedAssigneeList,
+ label_name: selectedLabelList,
+ });
+
+ it('extracts the correct filter parameters from a url', () => {
+ const result = extractFilterQueryParameters(queryParamsString);
+ const operator = '=';
+ const expectedFilters = {
+ selectedAssigneeList: { operator, value: selectedAssigneeList.join(',') },
+ selectedLabelList: { operator, value: selectedLabelList.join(',') },
+ selectedAuthor: { operator, value: selectedAuthor },
+ selectedMilestone: { operator, value: selectedMilestone },
+ selectedSourceBranch: { operator, value: selectedSourceBranch },
+ selectedTargetBranch: { operator, value: selectedTargetBranch },
+ };
+ expect(result).toMatchObject(expectedFilters);
+ });
+
+ it('returns null for missing parameters', () => {
+ const result = extractFilterQueryParameters('');
+ const expectedFilters = {
+ selectedAuthor: null,
+ selectedMilestone: null,
+ selectedSourceBranch: null,
+ selectedTargetBranch: null,
+ };
+ expect(result).toMatchObject(expectedFilters);
+ });
+
+ it('only returns the parameters we expect', () => {
+ const result = extractFilterQueryParameters('foo="one"&bar="two"');
+ const resultKeys = Object.keys(result);
+ ['foo', 'bar'].forEach((key) => {
+ expect(resultKeys).not.toContain(key);
+ });
+
+ [
+ 'selectedAuthor',
+ 'selectedMilestone',
+ 'selectedSourceBranch',
+ 'selectedTargetBranch',
+ 'selectedAssigneeList',
+ 'selectedLabelList',
+ ].forEach((key) => {
+ expect(resultKeys).toContain(key);
+ });
+ });
+
+ it('returns an empty array for missing list parameters', () => {
+ const result = extractFilterQueryParameters('');
+ const expectedFilters = { selectedAssigneeList: [], selectedLabelList: [] };
+ expect(result).toMatchObject(expectedFilters);
+ });
+});
+
+describe('extractPaginationQueryParameters', () => {
+ const sort = 'title';
+ const direction = 'asc';
+ const page = '1';
+ const queryParamsString = objectToQuery({ sort, direction, page });
+
+ it('extracts the correct filter parameters from a url', () => {
+ const result = extractPaginationQueryParameters(queryParamsString);
+ const expectedFilters = { sort, page, direction };
+ expect(result).toMatchObject(expectedFilters);
+ });
+
+ it('returns null for missing parameters', () => {
+ const result = extractPaginationQueryParameters('');
+ const expectedFilters = { sort: null, direction: null, page: null };
+ expect(result).toMatchObject(expectedFilters);
+ });
+
+ it('only returns the parameters we expect', () => {
+ const result = extractPaginationQueryParameters('foo="one"&bar="two"&qux="three"');
+ const resultKeys = Object.keys(result);
+ ['foo', 'bar', 'qux'].forEach((key) => {
+ expect(resultKeys).not.toContain(key);
+ });
+
+ ['sort', 'page', 'direction'].forEach((key) => {
+ expect(resultKeys).toContain(key);
+ });
+ });
+});
+
+describe('getDataZoomOption', () => {
+ it('returns an empty object when totalItems <= maxItemsPerPage', () => {
+ const totalItems = 10;
+ const maxItemsPerPage = 20;
+
+ expect(getDataZoomOption({ totalItems, maxItemsPerPage })).toEqual({});
+ });
+
+ describe('when totalItems > maxItemsPerPage', () => {
+ const totalItems = 30;
+ const maxItemsPerPage = 20;
+
+ it('properly computes the end interval for the default datazoom config', () => {
+ const expected = [
+ {
+ type: 'slider',
+ bottom: 10,
+ start: 0,
+ end: 67,
+ },
+ ];
+
+ expect(getDataZoomOption({ totalItems, maxItemsPerPage })).toEqual(expected);
+ });
+
+ it('properly computes the end interval for a custom datazoom config', () => {
+ const dataZoom = [
+ { type: 'slider', bottom: 0, start: 0 },
+ { type: 'inside', start: 0 },
+ ];
+ const expected = [
+ {
+ type: 'slider',
+ bottom: 0,
+ start: 0,
+ end: 67,
+ },
+ {
+ type: 'inside',
+ start: 0,
+ end: 67,
+ },
+ ];
+
+ expect(getDataZoomOption({ totalItems, maxItemsPerPage, dataZoom })).toEqual(expected);
+ });
+ });
+});