Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-11-19 11:27:35 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-11-19 11:27:35 +0300
commit7e9c479f7de77702622631cff2628a9c8dcbc627 (patch)
treec8f718a08e110ad7e1894510980d2155a6549197 /spec/frontend/milestones
parente852b0ae16db4052c1c567d9efa4facc81146e88 (diff)
Add latest changes from gitlab-org/gitlab@13-6-stable-eev13.6.0-rc42
Diffstat (limited to 'spec/frontend/milestones')
-rw-r--r--spec/frontend/milestones/milestone_combobox_spec.js518
-rw-r--r--spec/frontend/milestones/mock_data.js94
-rw-r--r--spec/frontend/milestones/project_milestone_combobox_spec.js186
-rw-r--r--spec/frontend/milestones/stores/actions_spec.js173
-rw-r--r--spec/frontend/milestones/stores/getter_spec.js18
-rw-r--r--spec/frontend/milestones/stores/mutations_spec.js101
6 files changed, 883 insertions, 207 deletions
diff --git a/spec/frontend/milestones/milestone_combobox_spec.js b/spec/frontend/milestones/milestone_combobox_spec.js
new file mode 100644
index 00000000000..047484f117f
--- /dev/null
+++ b/spec/frontend/milestones/milestone_combobox_spec.js
@@ -0,0 +1,518 @@
+import Vuex from 'vuex';
+import { mount, createLocalVue } from '@vue/test-utils';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { ENTER_KEY } from '~/lib/utils/keys';
+import MilestoneCombobox from '~/milestones/components/milestone_combobox.vue';
+import { projectMilestones, groupMilestones } from './mock_data';
+import createStore from '~/milestones/stores/';
+
+const extraLinks = [
+ { text: 'Create new', url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/new' },
+ { text: 'Manage milestones', url: '/h5bp/html5-boilerplate/-/milestones' },
+];
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('Milestone combobox component', () => {
+ const projectId = '8';
+ const groupId = '24';
+ const groupMilestonesAvailable = true;
+ const X_TOTAL_HEADER = 'x-total';
+
+ let wrapper;
+ let projectMilestonesApiCallSpy;
+ let groupMilestonesApiCallSpy;
+ let searchApiCallSpy;
+
+ const createComponent = (props = {}, attrs = {}) => {
+ wrapper = mount(MilestoneCombobox, {
+ propsData: {
+ projectId,
+ groupId,
+ groupMilestonesAvailable,
+ extraLinks,
+ value: [],
+ ...props,
+ },
+ attrs,
+ listeners: {
+ // simulate a parent component v-model binding
+ input: selectedMilestone => {
+ wrapper.setProps({ value: selectedMilestone });
+ },
+ },
+ stubs: {
+ GlSearchBoxByType: true,
+ },
+ localVue,
+ store: createStore(),
+ });
+ };
+
+ beforeEach(() => {
+ const mock = new MockAdapter(axios);
+ gon.api_version = 'v4';
+
+ projectMilestonesApiCallSpy = jest
+ .fn()
+ .mockReturnValue([200, projectMilestones, { [X_TOTAL_HEADER]: '6' }]);
+
+ groupMilestonesApiCallSpy = jest
+ .fn()
+ .mockReturnValue([200, groupMilestones, { [X_TOTAL_HEADER]: '6' }]);
+
+ searchApiCallSpy = jest
+ .fn()
+ .mockReturnValue([200, projectMilestones, { [X_TOTAL_HEADER]: '6' }]);
+
+ mock
+ .onGet(`/api/v4/projects/${projectId}/milestones`)
+ .reply(config => projectMilestonesApiCallSpy(config));
+
+ mock
+ .onGet(`/api/v4/groups/${groupId}/milestones`)
+ .reply(config => groupMilestonesApiCallSpy(config));
+
+ mock.onGet(`/api/v4/projects/${projectId}/search`).reply(config => searchApiCallSpy(config));
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ //
+ // Finders
+ //
+ const findButtonContent = () => wrapper.find('[data-testid="milestone-combobox-button-content"]');
+
+ const findNoResults = () => wrapper.find('[data-testid="milestone-combobox-no-results"]');
+
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+
+ const findSearchBox = () => wrapper.find(GlSearchBoxByType);
+
+ const findProjectMilestonesSection = () =>
+ wrapper.find('[data-testid="project-milestones-section"]');
+ const findProjectMilestonesDropdownItems = () =>
+ findProjectMilestonesSection().findAll(GlDropdownItem);
+ const findFirstProjectMilestonesDropdownItem = () => findProjectMilestonesDropdownItems().at(0);
+
+ const findGroupMilestonesSection = () => wrapper.find('[data-testid="group-milestones-section"]');
+ const findGroupMilestonesDropdownItems = () =>
+ findGroupMilestonesSection().findAll(GlDropdownItem);
+ const findFirstGroupMilestonesDropdownItem = () => findGroupMilestonesDropdownItems().at(0);
+
+ //
+ // Expecters
+ //
+ const projectMilestoneSectionContainsErrorMessage = () => {
+ const projectMilestoneSection = findProjectMilestonesSection();
+
+ return projectMilestoneSection
+ .text()
+ .includes(s__('MilestoneCombobox|An error occurred while searching for milestones'));
+ };
+
+ const groupMilestoneSectionContainsErrorMessage = () => {
+ const groupMilestoneSection = findGroupMilestonesSection();
+
+ return groupMilestoneSection
+ .text()
+ .includes(s__('MilestoneCombobox|An error occurred while searching for milestones'));
+ };
+
+ //
+ // Convenience methods
+ //
+ const updateQuery = newQuery => {
+ findSearchBox().vm.$emit('input', newQuery);
+ };
+
+ const selectFirstProjectMilestone = () => {
+ findFirstProjectMilestonesDropdownItem().vm.$emit('click');
+ };
+
+ const selectFirstGroupMilestone = () => {
+ findFirstGroupMilestonesDropdownItem().vm.$emit('click');
+ };
+
+ const waitForRequests = ({ andClearMocks } = { andClearMocks: false }) =>
+ axios.waitForAll().then(() => {
+ if (andClearMocks) {
+ projectMilestonesApiCallSpy.mockClear();
+ groupMilestonesApiCallSpy.mockClear();
+ }
+ });
+
+ describe('initialization behavior', () => {
+ beforeEach(createComponent);
+
+ it('initializes the dropdown with milestones when mounted', () => {
+ return waitForRequests().then(() => {
+ expect(projectMilestonesApiCallSpy).toHaveBeenCalledTimes(1);
+ expect(groupMilestonesApiCallSpy).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ it('shows a spinner while network requests are in progress', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+
+ return waitForRequests().then(() => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ it('shows additional links', () => {
+ const links = wrapper.findAll('[data-testid="milestone-combobox-extra-links"]');
+ links.wrappers.forEach((item, idx) => {
+ expect(item.text()).toBe(extraLinks[idx].text);
+ expect(item.attributes('href')).toBe(extraLinks[idx].url);
+ });
+ });
+ });
+
+ describe('post-initialization behavior', () => {
+ describe('when the parent component provides an `id` binding', () => {
+ const id = '8';
+
+ beforeEach(() => {
+ createComponent({}, { id });
+
+ return waitForRequests();
+ });
+
+ it('adds the provided ID to the GlDropdown instance', () => {
+ expect(wrapper.attributes().id).toBe(id);
+ });
+ });
+
+ describe('when milestones are pre-selected', () => {
+ beforeEach(() => {
+ createComponent({ value: projectMilestones });
+
+ return waitForRequests();
+ });
+
+ it('renders the pre-selected milestones', () => {
+ expect(findButtonContent().text()).toBe('v0.1 + 5 more');
+ });
+ });
+
+ describe('when the search query is updated', () => {
+ beforeEach(() => {
+ createComponent();
+
+ return waitForRequests({ andClearMocks: true });
+ });
+
+ it('requeries the search when the search query is updated', () => {
+ updateQuery('v1.2.3');
+
+ return waitForRequests().then(() => {
+ expect(searchApiCallSpy).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+
+ describe('when the Enter is pressed', () => {
+ beforeEach(() => {
+ createComponent();
+
+ return waitForRequests({ andClearMocks: true });
+ });
+
+ it('requeries the search when Enter is pressed', () => {
+ findSearchBox().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
+
+ return waitForRequests().then(() => {
+ expect(searchApiCallSpy).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+
+ describe('when no results are found', () => {
+ beforeEach(() => {
+ projectMilestonesApiCallSpy = jest
+ .fn()
+ .mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
+
+ groupMilestonesApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
+
+ createComponent();
+
+ return waitForRequests();
+ });
+
+ describe('when the search query is empty', () => {
+ it('renders a "no results" message', () => {
+ expect(findNoResults().text()).toBe(s__('MilestoneCombobox|No matching results'));
+ });
+ });
+ });
+
+ describe('project milestones', () => {
+ describe('when the project milestones search returns results', () => {
+ beforeEach(() => {
+ createComponent();
+
+ return waitForRequests();
+ });
+
+ it('renders the project milestones section in the dropdown', () => {
+ expect(findProjectMilestonesSection().exists()).toBe(true);
+ });
+
+ it('renders the "Project milestones" heading with a total number indicator', () => {
+ expect(
+ findProjectMilestonesSection()
+ .find('[data-testid="milestone-results-section-header"]')
+ .text(),
+ ).toBe('Project milestones 6');
+ });
+
+ it("does not render an error message in the project milestone section's body", () => {
+ expect(projectMilestoneSectionContainsErrorMessage()).toBe(false);
+ });
+
+ it('renders each project milestones as a selectable item', () => {
+ const dropdownItems = findProjectMilestonesDropdownItems();
+
+ projectMilestones.forEach((milestone, i) => {
+ expect(dropdownItems.at(i).text()).toBe(milestone.title);
+ });
+ });
+ });
+
+ describe('when the project milestones search returns no results', () => {
+ beforeEach(() => {
+ projectMilestonesApiCallSpy = jest
+ .fn()
+ .mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
+
+ createComponent();
+
+ return waitForRequests();
+ });
+
+ it('does not render the project milestones section in the dropdown', () => {
+ expect(findProjectMilestonesSection().exists()).toBe(false);
+ });
+ });
+
+ describe('when the project milestones search returns an error', () => {
+ beforeEach(() => {
+ projectMilestonesApiCallSpy = jest.fn().mockReturnValue([500]);
+ searchApiCallSpy = jest.fn().mockReturnValue([500]);
+
+ createComponent({ value: [] });
+
+ return waitForRequests();
+ });
+
+ it('renders the project milestones section in the dropdown', () => {
+ expect(findProjectMilestonesSection().exists()).toBe(true);
+ });
+
+ it("renders an error message in the project milestones section's body", () => {
+ expect(projectMilestoneSectionContainsErrorMessage()).toBe(true);
+ });
+ });
+
+ describe('selection', () => {
+ beforeEach(() => {
+ createComponent();
+
+ return waitForRequests();
+ });
+
+ it('renders a checkmark by the selected item', async () => {
+ selectFirstProjectMilestone();
+
+ await localVue.nextTick();
+
+ expect(
+ findFirstProjectMilestonesDropdownItem()
+ .find('span')
+ .classes('selected-item'),
+ ).toBe(false);
+
+ selectFirstProjectMilestone();
+
+ await localVue.nextTick();
+
+ expect(
+ findFirstProjectMilestonesDropdownItem()
+ .find('span')
+ .classes('selected-item'),
+ ).toBe(true);
+ });
+
+ describe('when a project milestones is selected', () => {
+ beforeEach(() => {
+ createComponent();
+ projectMilestonesApiCallSpy = jest
+ .fn()
+ .mockReturnValue([200, [{ title: 'v1.0' }], { [X_TOTAL_HEADER]: '1' }]);
+
+ return waitForRequests();
+ });
+
+ it("displays the project milestones name in the dropdown's button", async () => {
+ selectFirstProjectMilestone();
+ await localVue.nextTick();
+
+ expect(findButtonContent().text()).toBe(s__('MilestoneCombobox|No milestone'));
+
+ selectFirstProjectMilestone();
+
+ await localVue.nextTick();
+ expect(findButtonContent().text()).toBe('v1.0');
+ });
+
+ it('updates the v-model binding with the project milestone title', () => {
+ expect(wrapper.vm.value).toEqual([]);
+
+ selectFirstProjectMilestone();
+
+ expect(wrapper.vm.value).toEqual(['v1.0']);
+ });
+ });
+ });
+ });
+
+ describe('group milestones', () => {
+ describe('when the group milestones search returns results', () => {
+ beforeEach(() => {
+ createComponent();
+
+ return waitForRequests();
+ });
+
+ it('renders the group milestones section in the dropdown', () => {
+ expect(findGroupMilestonesSection().exists()).toBe(true);
+ });
+
+ it('renders the "Group milestones" heading with a total number indicator', () => {
+ expect(
+ findGroupMilestonesSection()
+ .find('[data-testid="milestone-results-section-header"]')
+ .text(),
+ ).toBe('Group milestones 6');
+ });
+
+ it("does not render an error message in the group milestone section's body", () => {
+ expect(groupMilestoneSectionContainsErrorMessage()).toBe(false);
+ });
+
+ it('renders each group milestones as a selectable item', () => {
+ const dropdownItems = findGroupMilestonesDropdownItems();
+
+ groupMilestones.forEach((milestone, i) => {
+ expect(dropdownItems.at(i).text()).toBe(milestone.title);
+ });
+ });
+ });
+
+ describe('when the group milestones search returns no results', () => {
+ beforeEach(() => {
+ groupMilestonesApiCallSpy = jest
+ .fn()
+ .mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
+
+ createComponent();
+
+ return waitForRequests();
+ });
+
+ it('does not render the group milestones section in the dropdown', () => {
+ expect(findGroupMilestonesSection().exists()).toBe(false);
+ });
+ });
+
+ describe('when the group milestones search returns an error', () => {
+ beforeEach(() => {
+ groupMilestonesApiCallSpy = jest.fn().mockReturnValue([500]);
+ searchApiCallSpy = jest.fn().mockReturnValue([500]);
+
+ createComponent({ value: [] });
+
+ return waitForRequests();
+ });
+
+ it('renders the group milestones section in the dropdown', () => {
+ expect(findGroupMilestonesSection().exists()).toBe(true);
+ });
+
+ it("renders an error message in the group milestones section's body", () => {
+ expect(groupMilestoneSectionContainsErrorMessage()).toBe(true);
+ });
+ });
+
+ describe('selection', () => {
+ beforeEach(() => {
+ createComponent();
+
+ return waitForRequests();
+ });
+
+ it('renders a checkmark by the selected item', async () => {
+ selectFirstGroupMilestone();
+
+ await localVue.nextTick();
+
+ expect(
+ findFirstGroupMilestonesDropdownItem()
+ .find('span')
+ .classes('selected-item'),
+ ).toBe(false);
+
+ selectFirstGroupMilestone();
+
+ await localVue.nextTick();
+
+ expect(
+ findFirstGroupMilestonesDropdownItem()
+ .find('span')
+ .classes('selected-item'),
+ ).toBe(true);
+ });
+
+ describe('when a group milestones is selected', () => {
+ beforeEach(() => {
+ createComponent();
+ groupMilestonesApiCallSpy = jest
+ .fn()
+ .mockReturnValue([200, [{ title: 'group-v1.0' }], { [X_TOTAL_HEADER]: '1' }]);
+
+ return waitForRequests();
+ });
+
+ it("displays the group milestones name in the dropdown's button", async () => {
+ selectFirstGroupMilestone();
+ await localVue.nextTick();
+
+ expect(findButtonContent().text()).toBe(s__('MilestoneCombobox|No milestone'));
+
+ selectFirstGroupMilestone();
+
+ await localVue.nextTick();
+ expect(findButtonContent().text()).toBe('group-v1.0');
+ });
+
+ it('updates the v-model binding with the group milestone title', () => {
+ expect(wrapper.vm.value).toEqual([]);
+
+ selectFirstGroupMilestone();
+
+ expect(wrapper.vm.value).toEqual(['group-v1.0']);
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/milestones/mock_data.js b/spec/frontend/milestones/mock_data.js
index c64eeeba663..71fbfe54141 100644
--- a/spec/frontend/milestones/mock_data.js
+++ b/spec/frontend/milestones/mock_data.js
@@ -1,4 +1,4 @@
-export const milestones = [
+export const projectMilestones = [
{
id: 41,
iid: 6,
@@ -79,4 +79,94 @@ export const milestones = [
},
];
-export default milestones;
+export const groupMilestones = [
+ {
+ id: 141,
+ iid: 16,
+ project_id: 8,
+ group_id: 12,
+ title: 'group-v0.1',
+ description: '',
+ state: 'active',
+ created_at: '2020-04-04T01:30:40.051Z',
+ updated_at: '2020-04-04T01:30:40.051Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/6',
+ },
+ {
+ id: 140,
+ iid: 15,
+ project_id: 8,
+ group_id: 12,
+ title: 'group-v4.0',
+ description: 'Laboriosam nisi sapiente dolores et magnam nobis ad earum.',
+ state: 'closed',
+ created_at: '2020-01-13T19:39:15.191Z',
+ updated_at: '2020-01-13T19:39:15.191Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/5',
+ },
+ {
+ id: 139,
+ iid: 14,
+ project_id: 8,
+ group_id: 12,
+ title: 'group-v3.0',
+ description: 'Necessitatibus illo alias et repellat dolorum assumenda ut.',
+ state: 'closed',
+ created_at: '2020-01-13T19:39:15.176Z',
+ updated_at: '2020-01-13T19:39:15.176Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/4',
+ },
+ {
+ id: 138,
+ iid: 13,
+ project_id: 8,
+ group_id: 12,
+ title: 'group-v2.0',
+ description: 'Doloribus qui repudiandae iste sit.',
+ state: 'closed',
+ created_at: '2020-01-13T19:39:15.161Z',
+ updated_at: '2020-01-13T19:39:15.161Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/3',
+ },
+ {
+ id: 137,
+ iid: 12,
+ project_id: 8,
+ group_id: 12,
+ title: 'group-v1.0',
+ description: 'Illo sint odio officia ea.',
+ state: 'closed',
+ created_at: '2020-01-13T19:39:15.146Z',
+ updated_at: '2020-01-13T19:39:15.146Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/2',
+ },
+ {
+ id: 136,
+ iid: 11,
+ project_id: 8,
+ group_id: 12,
+ title: 'group-v0.0',
+ description: 'Sed quae facilis deleniti at delectus assumenda nobis veritatis.',
+ state: 'active',
+ created_at: '2020-01-13T19:39:15.127Z',
+ updated_at: '2020-01-13T19:39:15.127Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/1',
+ },
+];
+
+export default {
+ projectMilestones,
+ groupMilestones,
+};
diff --git a/spec/frontend/milestones/project_milestone_combobox_spec.js b/spec/frontend/milestones/project_milestone_combobox_spec.js
deleted file mode 100644
index 60d68aa5816..00000000000
--- a/spec/frontend/milestones/project_milestone_combobox_spec.js
+++ /dev/null
@@ -1,186 +0,0 @@
-import axios from 'axios';
-import MockAdapter from 'axios-mock-adapter';
-import { shallowMount } from '@vue/test-utils';
-import { GlDropdown, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
-import { ENTER_KEY } from '~/lib/utils/keys';
-import MilestoneCombobox from '~/milestones/project_milestone_combobox.vue';
-import { milestones as projectMilestones } from './mock_data';
-
-const TEST_SEARCH_ENDPOINT = '/api/v4/projects/8/search';
-const TEST_SEARCH = 'TEST_SEARCH';
-
-const extraLinks = [
- { text: 'Create new', url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/new' },
- { text: 'Manage milestones', url: '/h5bp/html5-boilerplate/-/milestones' },
-];
-
-const preselectedMilestones = [];
-const projectId = '8';
-
-describe('Milestone selector', () => {
- let wrapper;
- let mock;
-
- const findNoResultsMessage = () => wrapper.find({ ref: 'noResults' });
-
- const findSearchBox = () => wrapper.find(GlSearchBoxByType);
-
- const factory = (options = {}) => {
- wrapper = shallowMount(MilestoneCombobox, {
- ...options,
- });
- };
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- gon.api_version = 'v4';
-
- mock.onGet('/api/v4/projects/8/milestones').reply(200, projectMilestones);
-
- factory({
- propsData: {
- projectId,
- preselectedMilestones,
- extraLinks,
- },
- });
- });
-
- afterEach(() => {
- mock.restore();
- wrapper.destroy();
- wrapper = null;
- });
-
- it('renders the dropdown', () => {
- expect(wrapper.find(GlDropdown)).toExist();
- });
-
- it('renders additional links', () => {
- const links = wrapper.findAll('[href]');
- links.wrappers.forEach((item, idx) => {
- expect(item.text()).toBe(extraLinks[idx].text);
- expect(item.attributes('href')).toBe(extraLinks[idx].url);
- });
- });
-
- describe('before results', () => {
- it('should show a loading icon', () => {
- const request = mock.onGet(TEST_SEARCH_ENDPOINT, {
- params: { search: TEST_SEARCH, scope: 'milestones' },
- });
-
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
-
- return wrapper.vm.$nextTick().then(() => {
- request.reply(200, []);
- });
- });
-
- it('should not show any dropdown items', () => {
- expect(wrapper.findAll('[role="milestone option"]')).toHaveLength(0);
- });
-
- it('should have "No milestone" as the button text', () => {
- expect(wrapper.find({ ref: 'buttonText' }).text()).toBe('No milestone');
- });
- });
-
- describe('with empty results', () => {
- beforeEach(() => {
- mock
- .onGet(TEST_SEARCH_ENDPOINT, { params: { search: TEST_SEARCH, scope: 'milestones' } })
- .reply(200, []);
- findSearchBox().vm.$emit('input', TEST_SEARCH);
- return axios.waitForAll();
- });
-
- it('should display that no matching items are found', () => {
- expect(findNoResultsMessage().exists()).toBe(true);
- });
- });
-
- describe('with results', () => {
- let items;
- beforeEach(() => {
- mock
- .onGet(TEST_SEARCH_ENDPOINT, { params: { search: 'v0.1', scope: 'milestones' } })
- .reply(200, [
- {
- id: 41,
- iid: 6,
- project_id: 8,
- title: 'v0.1',
- description: '',
- state: 'active',
- created_at: '2020-04-04T01:30:40.051Z',
- updated_at: '2020-04-04T01:30:40.051Z',
- due_date: null,
- start_date: null,
- web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/6',
- },
- ]);
- findSearchBox().vm.$emit('input', 'v0.1');
- return axios.waitForAll().then(() => {
- items = wrapper.findAll('[role="milestone option"]');
- });
- });
-
- it('should display one item per result', () => {
- expect(items).toHaveLength(1);
- });
-
- it('should emit a change if an item is clicked', () => {
- items.at(0).vm.$emit('click');
- expect(wrapper.emitted().change.length).toBe(1);
- expect(wrapper.emitted().change[0]).toEqual([[{ title: 'v0.1' }]]);
- });
-
- it('should not have a selecton icon on any item', () => {
- items.wrappers.forEach(item => {
- expect(item.find('.selected-item').exists()).toBe(false);
- });
- });
-
- it('should have a selecton icon if an item is clicked', () => {
- items.at(0).vm.$emit('click');
- expect(wrapper.find('.selected-item').exists()).toBe(true);
- });
-
- it('should not display a message about no results', () => {
- expect(findNoResultsMessage().exists()).toBe(false);
- });
- });
-
- describe('when Enter is pressed', () => {
- beforeEach(() => {
- factory({
- propsData: {
- projectId,
- preselectedMilestones,
- extraLinks,
- },
- data() {
- return {
- searchQuery: 'TEST_SEARCH',
- };
- },
- });
-
- mock
- .onGet(TEST_SEARCH_ENDPOINT, { params: { search: 'TEST_SEARCH', scope: 'milestones' } })
- .reply(200, []);
- });
-
- it('should trigger a search', async () => {
- mock.resetHistory();
-
- findSearchBox().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
-
- await axios.waitForAll();
-
- expect(mock.history.get.length).toBe(1);
- expect(mock.history.get[0].url).toBe(TEST_SEARCH_ENDPOINT);
- });
- });
-});
diff --git a/spec/frontend/milestones/stores/actions_spec.js b/spec/frontend/milestones/stores/actions_spec.js
index ad73d0e4238..a62b0c49a80 100644
--- a/spec/frontend/milestones/stores/actions_spec.js
+++ b/spec/frontend/milestones/stores/actions_spec.js
@@ -4,6 +4,7 @@ import * as actions from '~/milestones/stores/actions';
import * as types from '~/milestones/stores/mutation_types';
let mockProjectMilestonesReturnValue;
+let mockGroupMilestonesReturnValue;
let mockProjectSearchReturnValue;
jest.mock('~/api', () => ({
@@ -13,6 +14,7 @@ jest.mock('~/api', () => ({
default: {
projectMilestones: () => mockProjectMilestonesReturnValue,
projectSearch: () => mockProjectSearchReturnValue,
+ groupMilestones: () => mockGroupMilestonesReturnValue,
},
}));
@@ -32,6 +34,24 @@ describe('Milestone combobox Vuex store actions', () => {
});
});
+ describe('setGroupId', () => {
+ it(`commits ${types.SET_GROUP_ID} with the new group ID`, () => {
+ const groupId = '123';
+ testAction(actions.setGroupId, groupId, state, [
+ { type: types.SET_GROUP_ID, payload: groupId },
+ ]);
+ });
+ });
+
+ describe('setGroupMilestonesAvailable', () => {
+ it(`commits ${types.SET_GROUP_MILESTONES_AVAILABLE} with the boolean indicating if group milestones are available (Premium)`, () => {
+ state.groupMilestonesAvailable = true;
+ testAction(actions.setGroupMilestonesAvailable, state.groupMilestonesAvailable, state, [
+ { type: types.SET_GROUP_MILESTONES_AVAILABLE, payload: state.groupMilestonesAvailable },
+ ]);
+ });
+ });
+
describe('setSelectedMilestones', () => {
it(`commits ${types.SET_SELECTED_MILESTONES} with the new selected milestones name`, () => {
const selectedMilestones = ['v1.2.3'];
@@ -41,6 +61,14 @@ describe('Milestone combobox Vuex store actions', () => {
});
});
+ describe('clearSelectedMilestones', () => {
+ it(`commits ${types.CLEAR_SELECTED_MILESTONES} with the new selected milestones name`, () => {
+ testAction(actions.clearSelectedMilestones, null, state, [
+ { type: types.CLEAR_SELECTED_MILESTONES },
+ ]);
+ });
+ });
+
describe('toggleMilestones', () => {
const selectedMilestone = 'v1.2.3';
it(`commits ${types.ADD_SELECTED_MILESTONE} with the new selected milestone name`, () => {
@@ -58,19 +86,38 @@ describe('Milestone combobox Vuex store actions', () => {
});
describe('search', () => {
- it(`commits ${types.SET_QUERY} with the new search query`, () => {
- const query = 'v1.0';
- testAction(
- actions.search,
- query,
- state,
- [{ type: types.SET_QUERY, payload: query }],
- [{ type: 'searchMilestones' }],
- );
+ describe('when project has license to add group milestones', () => {
+ it(`commits ${types.SET_SEARCH_QUERY} with the new search query to search for project and group milestones`, () => {
+ const getters = {
+ groupMilestonesEnabled: () => true,
+ };
+
+ const searchQuery = 'v1.0';
+ testAction(
+ actions.search,
+ searchQuery,
+ { ...state, ...getters },
+ [{ type: types.SET_SEARCH_QUERY, payload: searchQuery }],
+ [{ type: 'searchProjectMilestones' }, { type: 'searchGroupMilestones' }],
+ );
+ });
+ });
+
+ describe('when project does not have license to add group milestones', () => {
+ it(`commits ${types.SET_SEARCH_QUERY} with the new search query to search for project milestones`, () => {
+ const searchQuery = 'v1.0';
+ testAction(
+ actions.search,
+ searchQuery,
+ state,
+ [{ type: types.SET_SEARCH_QUERY, payload: searchQuery }],
+ [{ type: 'searchProjectMilestones' }],
+ );
+ });
});
});
- describe('searchMilestones', () => {
+ describe('searchProjectMilestones', () => {
describe('when the search is successful', () => {
const projectSearchApiResponse = { data: [{ title: 'v1.0' }] };
@@ -79,7 +126,7 @@ describe('Milestone combobox Vuex store actions', () => {
});
it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => {
- return testAction(actions.searchMilestones, undefined, state, [
+ return testAction(actions.searchProjectMilestones, undefined, state, [
{ type: types.REQUEST_START },
{ type: types.RECEIVE_PROJECT_MILESTONES_SUCCESS, payload: projectSearchApiResponse },
{ type: types.REQUEST_FINISH },
@@ -95,7 +142,7 @@ describe('Milestone combobox Vuex store actions', () => {
});
it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => {
- return testAction(actions.searchMilestones, undefined, state, [
+ return testAction(actions.searchProjectMilestones, undefined, state, [
{ type: types.REQUEST_START },
{ type: types.RECEIVE_PROJECT_MILESTONES_ERROR, payload: error },
{ type: types.REQUEST_FINISH },
@@ -104,7 +151,71 @@ describe('Milestone combobox Vuex store actions', () => {
});
});
+ describe('searchGroupMilestones', () => {
+ describe('when the search is successful', () => {
+ const groupSearchApiResponse = { data: [{ title: 'group-v1.0' }] };
+
+ beforeEach(() => {
+ mockGroupMilestonesReturnValue = Promise.resolve(groupSearchApiResponse);
+ });
+
+ it(`commits ${types.REQUEST_START}, ${types.RECEIVE_GROUP_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => {
+ return testAction(actions.searchGroupMilestones, undefined, state, [
+ { type: types.REQUEST_START },
+ { type: types.RECEIVE_GROUP_MILESTONES_SUCCESS, payload: groupSearchApiResponse },
+ { type: types.REQUEST_FINISH },
+ ]);
+ });
+ });
+
+ describe('when the search fails', () => {
+ const error = new Error('Something went wrong!');
+
+ beforeEach(() => {
+ mockGroupMilestonesReturnValue = Promise.reject(error);
+ });
+
+ it(`commits ${types.REQUEST_START}, ${types.RECEIVE_GROUP_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => {
+ return testAction(actions.searchGroupMilestones, undefined, state, [
+ { type: types.REQUEST_START },
+ { type: types.RECEIVE_GROUP_MILESTONES_ERROR, payload: error },
+ { type: types.REQUEST_FINISH },
+ ]);
+ });
+ });
+ });
+
describe('fetchMilestones', () => {
+ describe('when project has license to add group milestones', () => {
+ it(`dispatchs fetchProjectMilestones and fetchGroupMilestones`, () => {
+ const getters = {
+ groupMilestonesEnabled: () => true,
+ };
+
+ testAction(
+ actions.fetchMilestones,
+ undefined,
+ { ...state, ...getters },
+ [],
+ [{ type: 'fetchProjectMilestones' }, { type: 'fetchGroupMilestones' }],
+ );
+ });
+ });
+
+ describe('when project does not have license to add group milestones', () => {
+ it(`dispatchs fetchProjectMilestones`, () => {
+ testAction(
+ actions.fetchMilestones,
+ undefined,
+ state,
+ [],
+ [{ type: 'fetchProjectMilestones' }],
+ );
+ });
+ });
+ });
+
+ describe('fetchProjectMilestones', () => {
describe('when the fetch is successful', () => {
const projectMilestonesApiResponse = { data: [{ title: 'v1.0' }] };
@@ -113,7 +224,7 @@ describe('Milestone combobox Vuex store actions', () => {
});
it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => {
- return testAction(actions.fetchMilestones, undefined, state, [
+ return testAction(actions.fetchProjectMilestones, undefined, state, [
{ type: types.REQUEST_START },
{ type: types.RECEIVE_PROJECT_MILESTONES_SUCCESS, payload: projectMilestonesApiResponse },
{ type: types.REQUEST_FINISH },
@@ -129,7 +240,7 @@ describe('Milestone combobox Vuex store actions', () => {
});
it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => {
- return testAction(actions.fetchMilestones, undefined, state, [
+ return testAction(actions.fetchProjectMilestones, undefined, state, [
{ type: types.REQUEST_START },
{ type: types.RECEIVE_PROJECT_MILESTONES_ERROR, payload: error },
{ type: types.REQUEST_FINISH },
@@ -137,4 +248,38 @@ describe('Milestone combobox Vuex store actions', () => {
});
});
});
+
+ describe('fetchGroupMilestones', () => {
+ describe('when the fetch is successful', () => {
+ const groupMilestonesApiResponse = { data: [{ title: 'group-v1.0' }] };
+
+ beforeEach(() => {
+ mockGroupMilestonesReturnValue = Promise.resolve(groupMilestonesApiResponse);
+ });
+
+ it(`commits ${types.REQUEST_START}, ${types.RECEIVE_GROUP_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => {
+ return testAction(actions.fetchGroupMilestones, undefined, state, [
+ { type: types.REQUEST_START },
+ { type: types.RECEIVE_GROUP_MILESTONES_SUCCESS, payload: groupMilestonesApiResponse },
+ { type: types.REQUEST_FINISH },
+ ]);
+ });
+ });
+
+ describe('when the fetch fails', () => {
+ const error = new Error('Something went wrong!');
+
+ beforeEach(() => {
+ mockGroupMilestonesReturnValue = Promise.reject(error);
+ });
+
+ it(`commits ${types.REQUEST_START}, ${types.RECEIVE_GROUP_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => {
+ return testAction(actions.fetchGroupMilestones, undefined, state, [
+ { type: types.REQUEST_START },
+ { type: types.RECEIVE_GROUP_MILESTONES_ERROR, payload: error },
+ { type: types.REQUEST_FINISH },
+ ]);
+ });
+ });
+ });
});
diff --git a/spec/frontend/milestones/stores/getter_spec.js b/spec/frontend/milestones/stores/getter_spec.js
index df7c3d28e67..4a6116b642c 100644
--- a/spec/frontend/milestones/stores/getter_spec.js
+++ b/spec/frontend/milestones/stores/getter_spec.js
@@ -12,4 +12,22 @@ describe('Milestone comboxbox Vuex store getters', () => {
expect(getters.isLoading({ requestCount })).toBe(isLoading);
});
});
+
+ describe('groupMilestonesEnabled', () => {
+ it.each`
+ groupId | groupMilestonesAvailable | groupMilestonesEnabled
+ ${'1'} | ${true} | ${true}
+ ${'1'} | ${false} | ${false}
+ ${''} | ${true} | ${false}
+ ${''} | ${false} | ${false}
+ ${null} | ${true} | ${false}
+ `(
+ 'returns true when groupId is a truthy string and groupMilestonesAvailable is true',
+ ({ groupId, groupMilestonesAvailable, groupMilestonesEnabled }) => {
+ expect(getters.groupMilestonesEnabled({ groupId, groupMilestonesAvailable })).toBe(
+ groupMilestonesEnabled,
+ );
+ },
+ );
+ });
});
diff --git a/spec/frontend/milestones/stores/mutations_spec.js b/spec/frontend/milestones/stores/mutations_spec.js
index 8f8ce3c87ad..0b69a9d572d 100644
--- a/spec/frontend/milestones/stores/mutations_spec.js
+++ b/spec/frontend/milestones/stores/mutations_spec.js
@@ -14,13 +14,19 @@ describe('Milestones combobox Vuex store mutations', () => {
expect(state).toEqual({
projectId: null,
groupId: null,
- query: '',
+ groupMilestonesAvailable: false,
+ searchQuery: '',
matches: {
projectMilestones: {
list: [],
totalCount: 0,
error: null,
},
+ groupMilestones: {
+ list: [],
+ totalCount: 0,
+ error: null,
+ },
},
selectedMilestones: [],
requestCount: 0,
@@ -37,6 +43,24 @@ describe('Milestones combobox Vuex store mutations', () => {
});
});
+ describe(`${types.SET_GROUP_ID}`, () => {
+ it('updates the group ID', () => {
+ const newGroupId = '8';
+ mutations[types.SET_GROUP_ID](state, newGroupId);
+
+ expect(state.groupId).toBe(newGroupId);
+ });
+ });
+
+ describe(`${types.SET_GROUP_MILESTONES_AVAILABLE}`, () => {
+ it('sets boolean indicating if group milestones are available', () => {
+ const groupMilestonesAvailable = true;
+ mutations[types.SET_GROUP_MILESTONES_AVAILABLE](state, groupMilestonesAvailable);
+
+ expect(state.groupMilestonesAvailable).toBe(groupMilestonesAvailable);
+ });
+ });
+
describe(`${types.SET_SELECTED_MILESTONES}`, () => {
it('sets the selected milestones', () => {
const selectedMilestones = ['v1.2.3'];
@@ -46,7 +70,21 @@ describe('Milestones combobox Vuex store mutations', () => {
});
});
- describe(`${types.ADD_SELECTED_MILESTONESs}`, () => {
+ describe(`${types.CLEAR_SELECTED_MILESTONES}`, () => {
+ it('clears the selected milestones', () => {
+ const selectedMilestones = ['v1.2.3'];
+
+ // Set state.selectedMilestones
+ mutations[types.SET_SELECTED_MILESTONES](state, selectedMilestones);
+
+ // Clear state.selectedMilestones
+ mutations[types.CLEAR_SELECTED_MILESTONES](state);
+
+ expect(state.selectedMilestones).toEqual([]);
+ });
+ });
+
+ describe(`${types.ADD_SELECTED_MILESTONES}`, () => {
it('adds the selected milestones', () => {
const selectedMilestone = 'v1.2.3';
mutations[types.ADD_SELECTED_MILESTONE](state, selectedMilestone);
@@ -67,12 +105,12 @@ describe('Milestones combobox Vuex store mutations', () => {
});
});
- describe(`${types.SET_QUERY}`, () => {
+ describe(`${types.SET_SEARCH_QUERY}`, () => {
it('updates the search query', () => {
const newQuery = 'hello';
- mutations[types.SET_QUERY](state, newQuery);
+ mutations[types.SET_SEARCH_QUERY](state, newQuery);
- expect(state.query).toBe(newQuery);
+ expect(state.searchQuery).toBe(newQuery);
});
});
@@ -156,4 +194,57 @@ describe('Milestones combobox Vuex store mutations', () => {
});
});
});
+
+ describe(`${types.RECEIVE_GROUP_MILESTONES_SUCCESS}`, () => {
+ it('updates state.matches.groupMilestones based on the provided API response', () => {
+ const response = {
+ data: [
+ {
+ title: 'group-0.1',
+ },
+ {
+ title: 'group-0.2',
+ },
+ ],
+ headers: {
+ 'x-total': 2,
+ },
+ };
+
+ mutations[types.RECEIVE_GROUP_MILESTONES_SUCCESS](state, response);
+
+ expect(state.matches.groupMilestones).toEqual({
+ list: [
+ {
+ title: 'group-0.1',
+ },
+ {
+ title: 'group-0.2',
+ },
+ ],
+ error: null,
+ totalCount: 2,
+ });
+ });
+
+ describe(`${types.RECEIVE_GROUP_MILESTONES_ERROR}`, () => {
+ it('updates state.matches.groupMilestones to an empty state with the error object', () => {
+ const error = new Error('Something went wrong!');
+
+ state.matches.groupMilestones = {
+ list: [{ title: 'group-0.1' }],
+ totalCount: 1,
+ error: null,
+ };
+
+ mutations[types.RECEIVE_GROUP_MILESTONES_ERROR](state, error);
+
+ expect(state.matches.groupMilestones).toEqual({
+ list: [],
+ totalCount: 0,
+ error,
+ });
+ });
+ });
+ });
});