diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-19 11:27:35 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-19 11:27:35 +0300 |
commit | 7e9c479f7de77702622631cff2628a9c8dcbc627 (patch) | |
tree | c8f718a08e110ad7e1894510980d2155a6549197 /spec/frontend/milestones | |
parent | e852b0ae16db4052c1c567d9efa4facc81146e88 (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.js | 518 | ||||
-rw-r--r-- | spec/frontend/milestones/mock_data.js | 94 | ||||
-rw-r--r-- | spec/frontend/milestones/project_milestone_combobox_spec.js | 186 | ||||
-rw-r--r-- | spec/frontend/milestones/stores/actions_spec.js | 173 | ||||
-rw-r--r-- | spec/frontend/milestones/stores/getter_spec.js | 18 | ||||
-rw-r--r-- | spec/frontend/milestones/stores/mutations_spec.js | 101 |
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, + }); + }); + }); + }); }); |