diff options
Diffstat (limited to 'spec/frontend/boards')
13 files changed, 1400 insertions, 63 deletions
diff --git a/spec/frontend/boards/board_list_new_spec.js b/spec/frontend/boards/board_list_new_spec.js index 163611c2197..55516e3fd56 100644 --- a/spec/frontend/boards/board_list_new_spec.js +++ b/spec/frontend/boards/board_list_new_spec.js @@ -77,6 +77,8 @@ const createComponent = ({ provide: { groupId: null, rootPath: '/', + weightFeatureAvailable: false, + boardWeight: null, }, }); diff --git a/spec/frontend/boards/components/board_assignee_dropdown_spec.js b/spec/frontend/boards/components/board_assignee_dropdown_spec.js new file mode 100644 index 00000000000..e185a6d5419 --- /dev/null +++ b/spec/frontend/boards/components/board_assignee_dropdown_spec.js @@ -0,0 +1,308 @@ +import { mount, createLocalVue } from '@vue/test-utils'; +import { GlDropdownItem, GlAvatarLink, GlAvatarLabeled, GlSearchBoxByType } from '@gitlab/ui'; +import createMockApollo from 'jest/helpers/mock_apollo_helper'; +import VueApollo from 'vue-apollo'; +import BoardAssigneeDropdown from '~/boards/components/board_assignee_dropdown.vue'; +import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue'; +import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue'; +import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue'; +import store from '~/boards/stores'; +import getIssueParticipants from '~/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql'; +import searchUsers from '~/boards/queries/users_search.query.graphql'; +import { participants } from '../mock_data'; + +const localVue = createLocalVue(); + +localVue.use(VueApollo); + +describe('BoardCardAssigneeDropdown', () => { + let wrapper; + let fakeApollo; + let getIssueParticipantsSpy; + let getSearchUsersSpy; + + const iid = '111'; + const activeIssueName = 'test'; + const anotherIssueName = 'hello'; + + const createComponent = (search = '') => { + wrapper = mount(BoardAssigneeDropdown, { + data() { + return { + search, + selected: store.getters.activeIssue.assignees, + participants, + }; + }, + store, + provide: { + canUpdate: true, + rootPath: '', + }, + }); + }; + + const createComponentWithApollo = (search = '') => { + fakeApollo = createMockApollo([ + [getIssueParticipants, getIssueParticipantsSpy], + [searchUsers, getSearchUsersSpy], + ]); + + wrapper = mount(BoardAssigneeDropdown, { + localVue, + apolloProvider: fakeApollo, + data() { + return { + search, + selected: store.getters.activeIssue.assignees, + participants, + }; + }, + store, + provide: { + canUpdate: true, + rootPath: '', + }, + }); + }; + + const unassign = async () => { + wrapper.find('[data-testid="unassign"]').trigger('click'); + + await wrapper.vm.$nextTick(); + }; + + const openDropdown = async () => { + wrapper.find('[data-testid="edit-button"]').trigger('click'); + + await wrapper.vm.$nextTick(); + }; + + const findByText = text => { + return wrapper.findAll(GlDropdownItem).wrappers.find(node => node.text().indexOf(text) === 0); + }; + + beforeEach(() => { + store.state.activeId = '1'; + store.state.issues = { + '1': { + iid, + assignees: [{ username: activeIssueName, name: activeIssueName, id: activeIssueName }], + }, + }; + + jest.spyOn(store, 'dispatch').mockResolvedValue(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('when mounted', () => { + beforeEach(() => { + createComponent(); + }); + + it.each` + text + ${anotherIssueName} + ${activeIssueName} + `('finds item with $text', ({ text }) => { + const item = findByText(text); + + expect(item.exists()).toBe(true); + }); + + it('renders gl-avatar-link in gl-dropdown-item', () => { + const item = findByText('hello'); + + expect(item.find(GlAvatarLink).exists()).toBe(true); + }); + + it('renders gl-avatar-labeled in gl-avatar-link', () => { + const item = findByText('hello'); + + expect( + item + .find(GlAvatarLink) + .find(GlAvatarLabeled) + .exists(), + ).toBe(true); + }); + }); + + describe('when selected users are present', () => { + it('renders a divider', () => { + createComponent(); + + expect(wrapper.find('[data-testid="selected-user-divider"]').exists()).toBe(true); + }); + }); + + describe('when collapsed', () => { + it('renders IssuableAssignees', () => { + createComponent(); + + expect(wrapper.find(IssuableAssignees).isVisible()).toBe(true); + expect(wrapper.find(MultiSelectDropdown).isVisible()).toBe(false); + }); + }); + + describe('when dropdown is open', () => { + beforeEach(async () => { + createComponent(); + + await openDropdown(); + }); + + it('shows assignees dropdown', async () => { + expect(wrapper.find(IssuableAssignees).isVisible()).toBe(false); + expect(wrapper.find(MultiSelectDropdown).isVisible()).toBe(true); + }); + + it('shows the issue returned as the activeIssue', async () => { + expect(findByText(activeIssueName).props('isChecked')).toBe(true); + }); + + describe('when "Unassign" is clicked', () => { + it('unassigns assignees', async () => { + await unassign(); + + expect(findByText('Unassign').props('isChecked')).toBe(true); + }); + }); + + describe('when an unselected item is clicked', () => { + beforeEach(async () => { + await unassign(); + }); + + it('assigns assignee in the dropdown', async () => { + wrapper.find('[data-testid="item_test"]').trigger('click'); + + await wrapper.vm.$nextTick(); + + expect(findByText(activeIssueName).props('isChecked')).toBe(true); + }); + + it('calls setAssignees with username list', async () => { + wrapper.find('[data-testid="item_test"]').trigger('click'); + + await wrapper.vm.$nextTick(); + + document.body.click(); + + await wrapper.vm.$nextTick(); + + expect(store.dispatch).toHaveBeenCalledWith('setAssignees', [activeIssueName]); + }); + }); + + describe('when the user off clicks', () => { + beforeEach(async () => { + await unassign(); + + document.body.click(); + + await wrapper.vm.$nextTick(); + }); + + it('calls setAssignees with username list', async () => { + expect(store.dispatch).toHaveBeenCalledWith('setAssignees', []); + }); + + it('closes the dropdown', async () => { + expect(wrapper.find(IssuableAssignees).isVisible()).toBe(true); + }); + }); + }); + + it('renders divider after unassign', () => { + createComponent(); + + expect(wrapper.find('[data-testid="unassign-divider"]').exists()).toBe(true); + }); + + it.each` + assignees | expected + ${[{ id: 5, username: '', name: '' }]} | ${'Assignee'} + ${[{ id: 6, username: '', name: '' }, { id: 7, username: '', name: '' }]} | ${'2 Assignees'} + `( + 'when assignees have a length of $assignees.length, it renders $expected', + ({ assignees, expected }) => { + store.state.issues['1'].assignees = assignees; + + createComponent(); + + expect(wrapper.find(BoardEditableItem).props('title')).toBe(expected); + }, + ); + + describe('Apollo', () => { + beforeEach(() => { + getIssueParticipantsSpy = jest.fn().mockResolvedValue({ + data: { + issue: { + participants: { + nodes: [ + { + username: 'participant', + name: 'participant', + webUrl: '', + avatarUrl: '', + id: '', + }, + ], + }, + }, + }, + }); + getSearchUsersSpy = jest.fn().mockResolvedValue({ + data: { + users: { + nodes: [{ username: 'root', name: 'root', webUrl: '', avatarUrl: '', id: '' }], + }, + }, + }); + }); + + describe('when search is empty', () => { + beforeEach(() => { + createComponentWithApollo(); + }); + + it('calls getIssueParticipants', async () => { + jest.runOnlyPendingTimers(); + await wrapper.vm.$nextTick(); + + expect(getIssueParticipantsSpy).toHaveBeenCalledWith({ id: 'gid://gitlab/Issue/111' }); + }); + }); + + describe('when search is not empty', () => { + beforeEach(() => { + createComponentWithApollo('search term'); + }); + + it('calls searchUsers', async () => { + jest.runOnlyPendingTimers(); + await wrapper.vm.$nextTick(); + + expect(getSearchUsersSpy).toHaveBeenCalledWith({ search: 'search term' }); + }); + }); + }); + + it('finds GlSearchBoxByType', async () => { + createComponent(); + + await openDropdown(); + + expect(wrapper.find(GlSearchBoxByType).exists()).toBe(true); + }); +}); diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js index a3ddcdf01b7..5e23c781eae 100644 --- a/spec/frontend/boards/components/board_card_spec.js +++ b/spec/frontend/boards/components/board_card_spec.js @@ -175,7 +175,7 @@ describe('BoardCard', () => { wrapper.trigger('mousedown'); wrapper.trigger('mouseup'); - expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', wrapper.vm.issue, undefined); + expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', wrapper.vm.issue, false); expect(boardsStore.detail.list).toEqual(wrapper.vm.list); }); @@ -188,7 +188,7 @@ describe('BoardCard', () => { wrapper.trigger('mousedown'); wrapper.trigger('mouseup'); - expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue', undefined); + expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue', false); }); }); diff --git a/spec/frontend/boards/components/board_column_new_spec.js b/spec/frontend/boards/components/board_column_new_spec.js new file mode 100644 index 00000000000..4aafc3a867a --- /dev/null +++ b/spec/frontend/boards/components/board_column_new_spec.js @@ -0,0 +1,72 @@ +import { shallowMount } from '@vue/test-utils'; + +import { listObj } from 'jest/boards/mock_data'; +import BoardColumn from '~/boards/components/board_column_new.vue'; +import List from '~/boards/models/list'; +import { ListType } from '~/boards/constants'; +import { createStore } from '~/boards/stores'; + +describe('Board Column Component', () => { + let wrapper; + let store; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + const createComponent = ({ listType = ListType.backlog, collapsed = false } = {}) => { + const boardId = '1'; + + const listMock = { + ...listObj, + list_type: listType, + collapsed, + }; + + if (listType === ListType.assignee) { + delete listMock.label; + listMock.user = {}; + } + + const list = new List({ ...listMock, doNotFetchIssues: true }); + + store = createStore(); + + wrapper = shallowMount(BoardColumn, { + store, + propsData: { + disabled: false, + list, + }, + provide: { + boardId, + }, + }); + }; + + const isExpandable = () => wrapper.classes('is-expandable'); + const isCollapsed = () => wrapper.classes('is-collapsed'); + + describe('Given different list types', () => { + it('is expandable when List Type is `backlog`', () => { + createComponent({ listType: ListType.backlog }); + + expect(isExpandable()).toBe(true); + }); + }); + + describe('expanded / collapsed column', () => { + it('has class is-collapsed when list is collapsed', () => { + createComponent({ collapsed: false }); + + expect(wrapper.vm.list.isExpanded).toBe(true); + }); + + it('does not have class is-collapsed when list is expanded', () => { + createComponent({ collapsed: true }); + + expect(isCollapsed()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/boards/components/board_column_spec.js b/spec/frontend/boards/components/board_column_spec.js index 2a4dbbb989e..ba11225676b 100644 --- a/spec/frontend/boards/components/board_column_spec.js +++ b/spec/frontend/boards/components/board_column_spec.js @@ -78,7 +78,7 @@ describe('Board Column Component', () => { }); }); - describe('expanded / collaped column', () => { + describe('expanded / collapsed column', () => { it('has class is-collapsed when list is collapsed', () => { createComponent({ collapsed: false }); diff --git a/spec/frontend/boards/components/board_list_header_new_spec.js b/spec/frontend/boards/components/board_list_header_new_spec.js new file mode 100644 index 00000000000..80786d82620 --- /dev/null +++ b/spec/frontend/boards/components/board_list_header_new_spec.js @@ -0,0 +1,169 @@ +import Vuex from 'vuex'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; + +import { listObj } from 'jest/boards/mock_data'; +import BoardListHeader from '~/boards/components/board_list_header_new.vue'; +import List from '~/boards/models/list'; +import { ListType } from '~/boards/constants'; + +const localVue = createLocalVue(); + +localVue.use(Vuex); + +describe('Board List Header Component', () => { + let wrapper; + let store; + + const updateListSpy = jest.fn(); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + + localStorage.clear(); + }); + + const createComponent = ({ + listType = ListType.backlog, + collapsed = false, + withLocalStorage = true, + currentUserId = null, + } = {}) => { + const boardId = '1'; + + const listMock = { + ...listObj, + list_type: listType, + collapsed, + }; + + if (listType === ListType.assignee) { + delete listMock.label; + listMock.user = {}; + } + + const list = new List({ ...listMock, doNotFetchIssues: true }); + + if (withLocalStorage) { + localStorage.setItem( + `boards.${boardId}.${list.type}.${list.id}.expanded`, + (!collapsed).toString(), + ); + } + + store = new Vuex.Store({ + state: {}, + actions: { updateList: updateListSpy }, + getters: {}, + }); + + wrapper = shallowMount(BoardListHeader, { + store, + localVue, + propsData: { + disabled: false, + list, + }, + provide: { + boardId, + weightFeatureAvailable: false, + currentUserId, + }, + }); + }; + + const isExpanded = () => wrapper.vm.list.isExpanded; + const isCollapsed = () => !isExpanded(); + + const findAddIssueButton = () => wrapper.find({ ref: 'newIssueBtn' }); + const findCaret = () => wrapper.find('.board-title-caret'); + + describe('Add issue button', () => { + const hasNoAddButton = [ListType.promotion, ListType.blank, ListType.closed]; + const hasAddButton = [ListType.backlog, ListType.label, ListType.milestone, ListType.assignee]; + + it.each(hasNoAddButton)('does not render when List Type is `%s`', listType => { + createComponent({ listType }); + + expect(findAddIssueButton().exists()).toBe(false); + }); + + it.each(hasAddButton)('does render when List Type is `%s`', listType => { + createComponent({ listType }); + + expect(findAddIssueButton().exists()).toBe(true); + }); + + it('has a test for each list type', () => { + createComponent(); + + Object.values(ListType).forEach(value => { + expect([...hasAddButton, ...hasNoAddButton]).toContain(value); + }); + }); + + it('does render when logged out', () => { + createComponent(); + + expect(findAddIssueButton().exists()).toBe(true); + }); + }); + + describe('expanding / collapsing the column', () => { + it('does not collapse when clicking the header', async () => { + createComponent(); + + expect(isCollapsed()).toBe(false); + + wrapper.find('[data-testid="board-list-header"]').trigger('click'); + + await wrapper.vm.$nextTick(); + + expect(isCollapsed()).toBe(false); + }); + + it('collapses expanded Column when clicking the collapse icon', async () => { + createComponent(); + + expect(isExpanded()).toBe(true); + + findCaret().vm.$emit('click'); + + await wrapper.vm.$nextTick(); + + expect(isCollapsed()).toBe(true); + }); + + it('expands collapsed Column when clicking the expand icon', async () => { + createComponent({ collapsed: true }); + + expect(isCollapsed()).toBe(true); + + findCaret().vm.$emit('click'); + + await wrapper.vm.$nextTick(); + + expect(isCollapsed()).toBe(false); + }); + + it("when logged in it calls list update and doesn't set localStorage", async () => { + createComponent({ withLocalStorage: false, currentUserId: 1 }); + + findCaret().vm.$emit('click'); + await wrapper.vm.$nextTick(); + + expect(updateListSpy).toHaveBeenCalledTimes(1); + expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(null); + }); + + it("when logged out it doesn't call list update and sets localStorage", async () => { + createComponent(); + + findCaret().vm.$emit('click'); + await wrapper.vm.$nextTick(); + + expect(updateListSpy).not.toHaveBeenCalled(); + expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(String(isExpanded())); + }); + }); +}); diff --git a/spec/frontend/boards/components/board_new_issue_new_spec.js b/spec/frontend/boards/components/board_new_issue_new_spec.js new file mode 100644 index 00000000000..af4bad65121 --- /dev/null +++ b/spec/frontend/boards/components/board_new_issue_new_spec.js @@ -0,0 +1,115 @@ +import Vuex from 'vuex'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import BoardNewIssue from '~/boards/components/board_new_issue_new.vue'; + +import '~/boards/models/list'; +import { mockListsWithModel } from '../mock_data'; + +const localVue = createLocalVue(); + +localVue.use(Vuex); + +describe('Issue boards new issue form', () => { + let wrapper; + let vm; + + const addListNewIssuesSpy = jest.fn(); + + const findSubmitButton = () => wrapper.find({ ref: 'submitButton' }); + const findCancelButton = () => wrapper.find({ ref: 'cancelButton' }); + const findSubmitForm = () => wrapper.find({ ref: 'submitForm' }); + + const submitIssue = () => { + const dummySubmitEvent = { + preventDefault() {}, + }; + + return findSubmitForm().trigger('submit', dummySubmitEvent); + }; + + beforeEach(() => { + const store = new Vuex.Store({ + state: {}, + actions: { addListNewIssue: addListNewIssuesSpy }, + getters: {}, + }); + + wrapper = shallowMount(BoardNewIssue, { + propsData: { + disabled: false, + list: mockListsWithModel[0], + }, + store, + localVue, + provide: { + groupId: null, + weightFeatureAvailable: false, + boardWeight: null, + }, + }); + + vm = wrapper.vm; + + return vm.$nextTick(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('calls submit if submit button is clicked', async () => { + jest.spyOn(wrapper.vm, 'submit').mockImplementation(); + wrapper.setData({ title: 'Testing Title' }); + + await vm.$nextTick(); + await submitIssue(); + expect(wrapper.vm.submit).toHaveBeenCalled(); + }); + + it('disables submit button if title is empty', () => { + expect(findSubmitButton().props().disabled).toBe(true); + }); + + it('enables submit button if title is not empty', async () => { + wrapper.setData({ title: 'Testing Title' }); + + await vm.$nextTick(); + expect(wrapper.find({ ref: 'input' }).element.value).toBe('Testing Title'); + expect(findSubmitButton().props().disabled).toBe(false); + }); + + it('clears title after clicking cancel', async () => { + findCancelButton().trigger('click'); + + await vm.$nextTick(); + expect(vm.title).toBe(''); + }); + + describe('submit success', () => { + it('creates new issue', async () => { + wrapper.setData({ title: 'submit issue' }); + + await vm.$nextTick(); + await submitIssue(); + expect(addListNewIssuesSpy).toHaveBeenCalled(); + }); + + it('enables button after submit', async () => { + jest.spyOn(wrapper.vm, 'submit').mockImplementation(); + wrapper.setData({ title: 'submit issue' }); + + await vm.$nextTick(); + await submitIssue(); + expect(findSubmitButton().props().disabled).toBe(false); + }); + + it('clears title after submit', async () => { + wrapper.setData({ title: 'submit issue' }); + + await vm.$nextTick(); + await submitIssue(); + await vm.$nextTick(); + expect(vm.title).toBe(''); + }); + }); +}); diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js new file mode 100644 index 00000000000..b034c8cb11d --- /dev/null +++ b/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js @@ -0,0 +1,137 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlDatepicker } from '@gitlab/ui'; +import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue'; +import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue'; +import { createStore } from '~/boards/stores'; +import createFlash from '~/flash'; + +const TEST_DUE_DATE = '2020-02-20'; +const TEST_FORMATTED_DUE_DATE = 'Feb 20, 2020'; +const TEST_PARSED_DATE = new Date(2020, 1, 20); +const TEST_ISSUE = { id: 'gid://gitlab/Issue/1', iid: 9, dueDate: null, referencePath: 'h/b#2' }; + +jest.mock('~/flash'); + +describe('~/boards/components/sidebar/board_sidebar_due_date.vue', () => { + let wrapper; + let store; + + afterEach(() => { + wrapper.destroy(); + store = null; + wrapper = null; + }); + + const createWrapper = ({ dueDate = null } = {}) => { + store = createStore(); + store.state.issues = { [TEST_ISSUE.id]: { ...TEST_ISSUE, dueDate } }; + store.state.activeId = TEST_ISSUE.id; + + wrapper = shallowMount(BoardSidebarDueDate, { + store, + provide: { + canUpdate: true, + }, + stubs: { + 'board-editable-item': BoardEditableItem, + }, + }); + }; + + const findDatePicker = () => wrapper.find(GlDatepicker); + const findResetButton = () => wrapper.find('[data-testid="reset-button"]'); + const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]'); + + it('renders "None" when no due date is set', () => { + createWrapper(); + + expect(findCollapsed().text()).toBe('None'); + expect(findResetButton().exists()).toBe(false); + }); + + it('renders formatted due date with reset button when set', () => { + createWrapper({ dueDate: TEST_DUE_DATE }); + + expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE); + expect(findResetButton().exists()).toBe(true); + }); + + describe('when due date is submitted', () => { + beforeEach(async () => { + createWrapper(); + + jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => { + store.state.issues[TEST_ISSUE.id].dueDate = TEST_DUE_DATE; + }); + findDatePicker().vm.$emit('input', TEST_PARSED_DATE); + await wrapper.vm.$nextTick(); + }); + + it('collapses sidebar and renders formatted due date with reset button', () => { + expect(findCollapsed().isVisible()).toBe(true); + expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE); + expect(findResetButton().exists()).toBe(true); + }); + + it('commits change to the server', () => { + expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalledWith({ + dueDate: TEST_DUE_DATE, + projectPath: 'h/b', + }); + }); + }); + + describe('when due date is cleared', () => { + beforeEach(async () => { + createWrapper(); + + jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => { + store.state.issues[TEST_ISSUE.id].dueDate = null; + }); + findDatePicker().vm.$emit('clear'); + await wrapper.vm.$nextTick(); + }); + + it('collapses sidebar and renders "None"', () => { + expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalled(); + expect(findCollapsed().isVisible()).toBe(true); + expect(findCollapsed().text()).toBe('None'); + }); + }); + + describe('when due date is resetted', () => { + beforeEach(async () => { + createWrapper({ dueDate: TEST_DUE_DATE }); + + jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => { + store.state.issues[TEST_ISSUE.id].dueDate = null; + }); + findResetButton().vm.$emit('click'); + await wrapper.vm.$nextTick(); + }); + + it('collapses sidebar and renders "None"', () => { + expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalled(); + expect(findCollapsed().isVisible()).toBe(true); + expect(findCollapsed().text()).toBe('None'); + }); + }); + + describe('when the mutation fails', () => { + beforeEach(async () => { + createWrapper({ dueDate: TEST_DUE_DATE }); + + jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => { + throw new Error(['failed mutation']); + }); + findDatePicker().vm.$emit('input', 'Invalid date'); + await wrapper.vm.$nextTick(); + }); + + it('collapses sidebar and renders former issue due date', () => { + expect(findCollapsed().isVisible()).toBe(true); + expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE); + expect(createFlash).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js new file mode 100644 index 00000000000..ee54c662167 --- /dev/null +++ b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js @@ -0,0 +1,157 @@ +import Vuex from 'vuex'; +import { mount, createLocalVue } from '@vue/test-utils'; +import { GlToggle, GlLoadingIcon } from '@gitlab/ui'; +import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue'; +import * as types from '~/boards/stores/mutation_types'; +import { createStore } from '~/boards/stores'; +import { mockActiveIssue } from '../../mock_data'; +import createFlash from '~/flash'; + +jest.mock('~/flash.js'); + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () => { + let wrapper; + let store; + + const findNotificationHeader = () => wrapper.find("[data-testid='notification-header-text']"); + const findToggle = () => wrapper.find(GlToggle); + const findGlLoadingIcon = () => wrapper.find(GlLoadingIcon); + + const createComponent = (activeIssue = { ...mockActiveIssue }) => { + store = createStore(); + store.state.issues = { [activeIssue.id]: activeIssue }; + store.state.activeId = activeIssue.id; + + wrapper = mount(BoardSidebarSubscription, { + localVue, + store, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + store = null; + jest.clearAllMocks(); + }); + + describe('Board sidebar subscription component template', () => { + it('displays "notifications" heading', () => { + createComponent(); + + expect(findNotificationHeader().text()).toBe('Notifications'); + }); + + it('renders toggle as "off" when currently not subscribed', () => { + createComponent(); + + expect(findToggle().exists()).toBe(true); + expect(findToggle().props('value')).toBe(false); + }); + + it('renders toggle as "on" when currently subscribed', () => { + createComponent({ + ...mockActiveIssue, + subscribed: true, + }); + + expect(findToggle().exists()).toBe(true); + expect(findToggle().props('value')).toBe(true); + }); + + describe('when notification emails have been disabled', () => { + beforeEach(() => { + createComponent({ + ...mockActiveIssue, + emailsDisabled: true, + }); + }); + + it('displays a message that notification have been disabled', () => { + expect(findNotificationHeader().text()).toBe( + 'Notifications have been disabled by the project or group owner', + ); + }); + + it('does not render the toggle button', () => { + expect(findToggle().exists()).toBe(false); + }); + }); + }); + + describe('Board sidebar subscription component `behavior`', () => { + const mockSetActiveIssueSubscribed = subscribedState => { + jest.spyOn(wrapper.vm, 'setActiveIssueSubscribed').mockImplementation(async () => { + store.commit(types.UPDATE_ISSUE_BY_ID, { + issueId: mockActiveIssue.id, + prop: 'subscribed', + value: subscribedState, + }); + }); + }; + + it('subscribing to notification', async () => { + createComponent(); + mockSetActiveIssueSubscribed(true); + + expect(findGlLoadingIcon().exists()).toBe(false); + + findToggle().trigger('click'); + + await wrapper.vm.$nextTick(); + + expect(findGlLoadingIcon().exists()).toBe(true); + expect(wrapper.vm.setActiveIssueSubscribed).toHaveBeenCalledWith({ + subscribed: true, + projectPath: 'gitlab-org/test-subgroup/gitlab-test', + }); + + await wrapper.vm.$nextTick(); + + expect(findGlLoadingIcon().exists()).toBe(false); + expect(findToggle().props('value')).toBe(true); + }); + + it('unsubscribing from notification', async () => { + createComponent({ + ...mockActiveIssue, + subscribed: true, + }); + mockSetActiveIssueSubscribed(false); + + expect(findGlLoadingIcon().exists()).toBe(false); + + findToggle().trigger('click'); + + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.setActiveIssueSubscribed).toHaveBeenCalledWith({ + subscribed: false, + projectPath: 'gitlab-org/test-subgroup/gitlab-test', + }); + expect(findGlLoadingIcon().exists()).toBe(true); + + await wrapper.vm.$nextTick(); + + expect(findGlLoadingIcon().exists()).toBe(false); + expect(findToggle().props('value')).toBe(false); + }); + + it('flashes an error message when setting the subscribed state fails', async () => { + createComponent(); + jest.spyOn(wrapper.vm, 'setActiveIssueSubscribed').mockImplementation(async () => { + throw new Error(); + }); + + findToggle().trigger('click'); + + await wrapper.vm.$nextTick(); + expect(createFlash).toHaveBeenNthCalledWith(1, { + message: wrapper.vm.$options.i18n.updateSubscribedErrorMessage, + }); + }); + }); +}); diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js index 50c0a85fc70..58f67231d55 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -2,6 +2,7 @@ /* global List */ import Vue from 'vue'; +import { keyBy } from 'lodash'; import '~/boards/models/list'; import '~/boards/models/issue'; import boardsStore from '~/boards/stores/boards_store'; @@ -175,6 +176,14 @@ export const mockIssue = { }, }; +export const mockActiveIssue = { + ...mockIssue, + id: 436, + iid: '27', + subscribed: false, + emailsDisabled: false, +}; + export const mockIssueWithModel = new ListIssue(mockIssue); export const mockIssue2 = { @@ -290,6 +299,7 @@ export const mockLists = [ assignee: null, milestone: null, loading: false, + issuesSize: 1, }, { id: 'gid://gitlab/List/2', @@ -307,9 +317,12 @@ export const mockLists = [ assignee: null, milestone: null, loading: false, + issuesSize: 0, }, ]; +export const mockListsById = keyBy(mockLists, 'id'); + export const mockListsWithModel = mockLists.map(listMock => Vue.observable(new List({ ...listMock, doNotFetchIssues: true })), ); @@ -319,6 +332,23 @@ export const mockIssuesByListId = { 'gid://gitlab/List/2': mockIssues.map(({ id }) => id), }; +export const participants = [ + { + id: '1', + username: 'test', + name: 'test', + avatar: '', + avatarUrl: '', + }, + { + id: '2', + username: 'hello', + name: 'hello', + avatar: '', + avatarUrl: '', + }, +]; + export const issues = { [mockIssue.id]: mockIssue, [mockIssue2.id]: mockIssue2, diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index 78e70161121..4d529580a7a 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -2,17 +2,21 @@ import testAction from 'helpers/vuex_action_helper'; import { mockListsWithModel, mockLists, + mockListsById, mockIssue, mockIssueWithModel, mockIssue2WithModel, rawIssue, mockIssues, labels, + mockActiveIssue, } from '../mock_data'; import actions, { gqlClient } from '~/boards/stores/actions'; import * as types from '~/boards/stores/mutation_types'; -import { inactiveId, ListType } from '~/boards/constants'; +import { inactiveId } from '~/boards/constants'; import issueMoveListMutation from '~/boards/queries/issue_move_list.mutation.graphql'; +import destroyBoardListMutation from '~/boards/queries/board_list_destroy.mutation.graphql'; +import updateAssignees from '~/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql'; import { fullBoardId, formatListIssues, formatBoardLists } from '~/boards/boards_util'; const expectNotImplemented = action => { @@ -116,7 +120,7 @@ describe('fetchLists', () => { payload: formattedLists, }, ], - [{ type: 'showWelcomeList' }], + [{ type: 'generateDefaultLists' }], done, ); }); @@ -146,14 +150,15 @@ describe('fetchLists', () => { payload: formattedLists, }, ], - [{ type: 'createList', payload: { backlog: true } }, { type: 'showWelcomeList' }], + [{ type: 'createList', payload: { backlog: true } }, { type: 'generateDefaultLists' }], done, ); }); }); -describe('showWelcomeList', () => { - it('should dispatch addList action', done => { +describe('generateDefaultLists', () => { + let store; + beforeEach(() => { const state = { endpoints: { fullPath: 'gitlab-org', boardId: '1' }, boardType: 'group', @@ -161,26 +166,19 @@ describe('showWelcomeList', () => { boardLists: [{ type: 'backlog' }, { type: 'closed' }], }; - const blankList = { - id: 'blank', - listType: ListType.blank, - title: 'Welcome to your issue board!', - position: 0, - }; - - testAction( - actions.showWelcomeList, - {}, + store = { + commit: jest.fn(), + dispatch: jest.fn(() => Promise.resolve()), state, - [], - [{ type: 'addList', payload: blankList }], - done, - ); + }; }); -}); -describe('generateDefaultLists', () => { - expectNotImplemented(actions.generateDefaultLists); + it('should dispatch fetchLabels', () => { + return actions.generateDefaultLists(store).then(() => { + expect(store.dispatch.mock.calls[0]).toEqual(['fetchLabels', 'to do']); + expect(store.dispatch.mock.calls[1]).toEqual(['fetchLabels', 'doing']); + }); + }); }); describe('createList', () => { @@ -323,8 +321,82 @@ describe('updateList', () => { }); }); -describe('deleteList', () => { - expectNotImplemented(actions.deleteList); +describe('removeList', () => { + let state; + const list = mockLists[0]; + const listId = list.id; + const mutationVariables = { + mutation: destroyBoardListMutation, + variables: { + listId, + }, + }; + + beforeEach(() => { + state = { + boardLists: mockListsById, + }; + }); + + afterEach(() => { + state = null; + }); + + it('optimistically deletes the list', () => { + const commit = jest.fn(); + + actions.removeList({ commit, state }, listId); + + expect(commit.mock.calls).toEqual([[types.REMOVE_LIST, listId]]); + }); + + it('keeps the updated list if remove succeeds', async () => { + const commit = jest.fn(); + jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ + data: { + destroyBoardList: { + errors: [], + }, + }, + }); + + await actions.removeList({ commit, state }, listId); + + expect(gqlClient.mutate).toHaveBeenCalledWith(mutationVariables); + expect(commit.mock.calls).toEqual([[types.REMOVE_LIST, listId]]); + }); + + it('restores the list if update fails', async () => { + const commit = jest.fn(); + jest.spyOn(gqlClient, 'mutate').mockResolvedValue(Promise.reject()); + + await actions.removeList({ commit, state }, listId); + + expect(gqlClient.mutate).toHaveBeenCalledWith(mutationVariables); + expect(commit.mock.calls).toEqual([ + [types.REMOVE_LIST, listId], + [types.REMOVE_LIST_FAILURE, mockListsById], + ]); + }); + + it('restores the list if update response has errors', async () => { + const commit = jest.fn(); + jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ + data: { + destroyBoardList: { + errors: ['update failed, ID invalid'], + }, + }, + }); + + await actions.removeList({ commit, state }, listId); + + expect(gqlClient.mutate).toHaveBeenCalledWith(mutationVariables); + expect(commit.mock.calls).toEqual([ + [types.REMOVE_LIST, listId], + [types.REMOVE_LIST_FAILURE, mockListsById], + ]); + }); }); describe('fetchIssuesForList', () => { @@ -560,41 +632,106 @@ describe('moveIssue', () => { }); }); -describe('createNewIssue', () => { - expectNotImplemented(actions.createNewIssue); +describe('setAssignees', () => { + const node = { username: 'name' }; + const name = 'username'; + const projectPath = 'h/h'; + const refPath = `${projectPath}#3`; + const iid = '1'; + + beforeEach(() => { + jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ + data: { issueSetAssignees: { issue: { assignees: { nodes: [{ ...node }] } } } }, + }); + }); + + it('calls mutate with the correct values', async () => { + await actions.setAssignees( + { commit: () => {}, getters: { activeIssue: { iid, referencePath: refPath } } }, + [name], + ); + + expect(gqlClient.mutate).toHaveBeenCalledWith({ + mutation: updateAssignees, + variables: { iid, assigneeUsernames: [name], projectPath }, + }); + }); + + it('calls the correct mutation with the correct values', done => { + testAction( + actions.setAssignees, + {}, + { activeIssue: { iid, referencePath: refPath }, commit: () => {} }, + [ + { + type: 'UPDATE_ISSUE_BY_ID', + payload: { prop: 'assignees', issueId: undefined, value: [node] }, + }, + ], + [], + done, + ); + }); }); -describe('addListIssue', () => { - it('should commit UPDATE_LIST_FAILURE mutation when API returns an error', done => { - const payload = { - list: mockLists[0], - issue: mockIssue, - position: 0, - }; +describe('createNewIssue', () => { + const state = { + boardType: 'group', + endpoints: { + fullPath: 'gitlab-org/gitlab', + }, + }; + + it('should return issue from API on success', async () => { + jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ + data: { + createIssue: { + issue: mockIssue, + errors: [], + }, + }, + }); + + const result = await actions.createNewIssue({ state }, mockIssue); + expect(result).toEqual(mockIssue); + }); + + it('should commit CREATE_ISSUE_FAILURE mutation when API returns an error', done => { + jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ + data: { + createIssue: { + issue: {}, + errors: [{ foo: 'bar' }], + }, + }, + }); + + const payload = mockIssue; testAction( - actions.addListIssue, + actions.createNewIssue, payload, - {}, - [{ type: types.ADD_ISSUE_TO_LIST, payload }], + state, + [{ type: types.CREATE_ISSUE_FAILURE }], [], done, ); }); }); -describe('addListIssueFailure', () => { - it('should commit UPDATE_LIST_FAILURE mutation when API returns an error', done => { +describe('addListIssue', () => { + it('should commit ADD_ISSUE_TO_LIST mutation', done => { const payload = { list: mockLists[0], issue: mockIssue, + position: 0, }; testAction( - actions.addListIssueFailure, + actions.addListIssue, payload, {}, - [{ type: types.ADD_ISSUE_TO_LIST_FAILURE, payload }], + [{ type: types.ADD_ISSUE_TO_LIST, payload }], [], done, ); @@ -603,7 +740,7 @@ describe('addListIssueFailure', () => { describe('setActiveIssueLabels', () => { const state = { issues: { [mockIssue.id]: mockIssue } }; - const getters = { getActiveIssue: mockIssue }; + const getters = { activeIssue: mockIssue }; const testLabelIds = labels.map(label => label.id); const input = { addLabelIds: testLabelIds, @@ -617,7 +754,7 @@ describe('setActiveIssueLabels', () => { .mockResolvedValue({ data: { updateIssue: { issue: { labels: { nodes: labels } } } } }); const payload = { - issueId: getters.getActiveIssue.id, + issueId: getters.activeIssue.id, prop: 'labels', value: labels, }; @@ -646,6 +783,108 @@ describe('setActiveIssueLabels', () => { }); }); +describe('setActiveIssueDueDate', () => { + const state = { issues: { [mockIssue.id]: mockIssue } }; + const getters = { activeIssue: mockIssue }; + const testDueDate = '2020-02-20'; + const input = { + dueDate: testDueDate, + projectPath: 'h/b', + }; + + it('should commit due date after setting the issue', done => { + jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ + data: { + updateIssue: { + issue: { + dueDate: testDueDate, + }, + errors: [], + }, + }, + }); + + const payload = { + issueId: getters.activeIssue.id, + prop: 'dueDate', + value: testDueDate, + }; + + testAction( + actions.setActiveIssueDueDate, + input, + { ...state, ...getters }, + [ + { + type: types.UPDATE_ISSUE_BY_ID, + payload, + }, + ], + [], + done, + ); + }); + + it('throws error if fails', async () => { + jest + .spyOn(gqlClient, 'mutate') + .mockResolvedValue({ data: { updateIssue: { errors: ['failed mutation'] } } }); + + await expect(actions.setActiveIssueDueDate({ getters }, input)).rejects.toThrow(Error); + }); +}); + +describe('setActiveIssueSubscribed', () => { + const state = { issues: { [mockActiveIssue.id]: mockActiveIssue } }; + const getters = { activeIssue: mockActiveIssue }; + const subscribedState = true; + const input = { + subscribedState, + projectPath: 'gitlab-org/gitlab-test', + }; + + it('should commit subscribed status', done => { + jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ + data: { + issueSetSubscription: { + issue: { + subscribed: subscribedState, + }, + errors: [], + }, + }, + }); + + const payload = { + issueId: getters.activeIssue.id, + prop: 'subscribed', + value: subscribedState, + }; + + testAction( + actions.setActiveIssueSubscribed, + input, + { ...state, ...getters }, + [ + { + type: types.UPDATE_ISSUE_BY_ID, + payload, + }, + ], + [], + done, + ); + }); + + it('throws error if fails', async () => { + jest + .spyOn(gqlClient, 'mutate') + .mockResolvedValue({ data: { issueSetSubscription: { errors: ['failed mutation'] } } }); + + await expect(actions.setActiveIssueSubscribed({ getters }, input)).rejects.toThrow(Error); + }); +}); + describe('fetchBacklog', () => { expectNotImplemented(actions.fetchBacklog); }); diff --git a/spec/frontend/boards/stores/getters_spec.js b/spec/frontend/boards/stores/getters_spec.js index b987080abab..64025726dd1 100644 --- a/spec/frontend/boards/stores/getters_spec.js +++ b/spec/frontend/boards/stores/getters_spec.js @@ -10,13 +10,13 @@ import { } from '../mock_data'; describe('Boards - Getters', () => { - describe('getLabelToggleState', () => { + describe('labelToggleState', () => { it('should return "on" when isShowingLabels is true', () => { const state = { isShowingLabels: true, }; - expect(getters.getLabelToggleState(state)).toBe('on'); + expect(getters.labelToggleState(state)).toBe('on'); }); it('should return "off" when isShowingLabels is false', () => { @@ -24,7 +24,7 @@ describe('Boards - Getters', () => { isShowingLabels: false, }; - expect(getters.getLabelToggleState(state)).toBe('off'); + expect(getters.labelToggleState(state)).toBe('off'); }); }); @@ -112,7 +112,7 @@ describe('Boards - Getters', () => { }); }); - describe('getActiveIssue', () => { + describe('activeIssue', () => { it.each` id | expected ${'1'} | ${'issue'} @@ -120,11 +120,27 @@ describe('Boards - Getters', () => { `('returns $expected when $id is passed to state', ({ id, expected }) => { const state = { issues: { '1': 'issue' }, activeId: id }; - expect(getters.getActiveIssue(state)).toEqual(expected); + expect(getters.activeIssue(state)).toEqual(expected); }); }); - describe('getIssues', () => { + describe('projectPathByIssueId', () => { + it('returns project path for the active issue', () => { + const mockActiveIssue = { + referencePath: 'gitlab-org/gitlab-test#1', + }; + expect(getters.projectPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual( + 'gitlab-org/gitlab-test', + ); + }); + + it('returns empty string as project when active issue is an empty object', () => { + const mockActiveIssue = {}; + expect(getters.projectPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual(''); + }); + }); + + describe('getIssuesByList', () => { const boardsState = { issuesByListId: mockIssuesByListId, issues, @@ -132,7 +148,7 @@ describe('Boards - Getters', () => { it('returns issues for a given listId', () => { const getIssueById = issueId => [mockIssue, mockIssue2].find(({ id }) => id === issueId); - expect(getters.getIssues(boardsState, { getIssueById })('gid://gitlab/List/2')).toEqual( + expect(getters.getIssuesByList(boardsState, { getIssueById })('gid://gitlab/List/2')).toEqual( mockIssues, ); }); diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js index 6e53f184bb3..e1e57a8fd43 100644 --- a/spec/frontend/boards/stores/mutations_spec.js +++ b/spec/frontend/boards/stores/mutations_spec.js @@ -82,7 +82,7 @@ describe('Board Store Mutations', () => { mutations.SET_ACTIVE_ID(state, expected); }); - it('updates aciveListId to be the value that is passed', () => { + it('updates activeListId to be the value that is passed', () => { expect(state.activeId).toBe(expected.id); }); @@ -101,6 +101,34 @@ describe('Board Store Mutations', () => { }); }); + describe('CREATE_LIST_FAILURE', () => { + it('sets error message', () => { + mutations.CREATE_LIST_FAILURE(state); + + expect(state.error).toEqual('An error occurred while creating the list. Please try again.'); + }); + }); + + describe('RECEIVE_LABELS_FAILURE', () => { + it('sets error message', () => { + mutations.RECEIVE_LABELS_FAILURE(state); + + expect(state.error).toEqual( + 'An error occurred while fetching labels. Please reload the page.', + ); + }); + }); + + describe('GENERATE_DEFAULT_LISTS_FAILURE', () => { + it('sets error message', () => { + mutations.GENERATE_DEFAULT_LISTS_FAILURE(state); + + expect(state.error).toEqual( + 'An error occurred while generating lists. Please reload the page.', + ); + }); + }); + describe('REQUEST_ADD_LIST', () => { expectNotImplemented(mutations.REQUEST_ADD_LIST); }); @@ -156,16 +184,43 @@ describe('Board Store Mutations', () => { }); }); - describe('REQUEST_REMOVE_LIST', () => { - expectNotImplemented(mutations.REQUEST_REMOVE_LIST); - }); + describe('REMOVE_LIST', () => { + it('removes list from boardLists', () => { + const [list, secondList] = mockListsWithModel; + const expected = { + [secondList.id]: secondList, + }; + state = { + ...state, + boardLists: { ...initialBoardListsState }, + }; - describe('RECEIVE_REMOVE_LIST_SUCCESS', () => { - expectNotImplemented(mutations.RECEIVE_REMOVE_LIST_SUCCESS); + mutations[types.REMOVE_LIST](state, list.id); + + expect(state.boardLists).toEqual(expected); + }); }); - describe('RECEIVE_REMOVE_LIST_ERROR', () => { - expectNotImplemented(mutations.RECEIVE_REMOVE_LIST_ERROR); + describe('REMOVE_LIST_FAILURE', () => { + it('restores lists from backup', () => { + const backupLists = { ...initialBoardListsState }; + + mutations[types.REMOVE_LIST_FAILURE](state, backupLists); + + expect(state.boardLists).toEqual(backupLists); + }); + + it('sets error state', () => { + const backupLists = { ...initialBoardListsState }; + state = { + ...state, + error: undefined, + }; + + mutations[types.REMOVE_LIST_FAILURE](state, backupLists); + + expect(state.error).toEqual('An error occurred while removing the list. Please try again.'); + }); }); describe('RESET_ISSUES', () => { @@ -387,6 +442,14 @@ describe('Board Store Mutations', () => { expectNotImplemented(mutations.RECEIVE_UPDATE_ISSUE_ERROR); }); + describe('CREATE_ISSUE_FAILURE', () => { + it('sets error message on state', () => { + mutations.CREATE_ISSUE_FAILURE(state); + + expect(state.error).toBe('An error occurred while creating the issue. Please try again.'); + }); + }); + describe('ADD_ISSUE_TO_LIST', () => { it('adds issue to issues state and issue id in list in issuesByListId', () => { const listIssues = { @@ -400,17 +463,45 @@ describe('Board Store Mutations', () => { ...state, issuesByListId: listIssues, issues, + boardLists: initialBoardListsState, }; - mutations.ADD_ISSUE_TO_LIST(state, { list: mockLists[0], issue: mockIssue2 }); + expect(state.boardLists['gid://gitlab/List/1'].issuesSize).toBe(1); + + mutations.ADD_ISSUE_TO_LIST(state, { list: mockListsWithModel[0], issue: mockIssue2 }); expect(state.issuesByListId['gid://gitlab/List/1']).toContain(mockIssue2.id); expect(state.issues[mockIssue2.id]).toEqual(mockIssue2); + expect(state.boardLists['gid://gitlab/List/1'].issuesSize).toBe(2); }); }); describe('ADD_ISSUE_TO_LIST_FAILURE', () => { - it('removes issue id from list in issuesByListId', () => { + it('removes issue id from list in issuesByListId and sets error message', () => { + const listIssues = { + 'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id], + }; + const issues = { + '1': mockIssue, + '2': mockIssue2, + }; + + state = { + ...state, + issuesByListId: listIssues, + issues, + boardLists: initialBoardListsState, + }; + + mutations.ADD_ISSUE_TO_LIST_FAILURE(state, { list: mockLists[0], issueId: mockIssue2.id }); + + expect(state.issuesByListId['gid://gitlab/List/1']).not.toContain(mockIssue2.id); + expect(state.error).toBe('An error occurred while creating the issue. Please try again.'); + }); + }); + + describe('REMOVE_ISSUE_FROM_LIST', () => { + it('removes issue id from list in issuesByListId and deletes issue from state', () => { const listIssues = { 'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id], }; @@ -426,9 +517,10 @@ describe('Board Store Mutations', () => { boardLists: initialBoardListsState, }; - mutations.ADD_ISSUE_TO_LIST_FAILURE(state, { list: mockLists[0], issue: mockIssue2 }); + mutations.ADD_ISSUE_TO_LIST_FAILURE(state, { list: mockLists[0], issueId: mockIssue2.id }); expect(state.issuesByListId['gid://gitlab/List/1']).not.toContain(mockIssue2.id); + expect(state.issues).not.toContain(mockIssue2); }); }); |