diff options
Diffstat (limited to 'spec/frontend/boards')
22 files changed, 1449 insertions, 615 deletions
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js index 4487fc15de6..36043b09636 100644 --- a/spec/frontend/boards/board_card_inner_spec.js +++ b/spec/frontend/boards/board_card_inner_spec.js @@ -1,11 +1,14 @@ import { GlLabel } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import { range } from 'lodash'; +import Vuex from 'vuex'; +import BoardBlockedIcon from '~/boards/components/board_blocked_icon.vue'; import BoardCardInner from '~/boards/components/board_card_inner.vue'; +import { issuableTypes } from '~/boards/constants'; import eventHub from '~/boards/eventhub'; import defaultStore from '~/boards/stores'; import { updateHistory } from '~/lib/utils/url_utility'; -import { mockLabelList } from './mock_data'; +import { mockLabelList, mockIssue } from './mock_data'; jest.mock('~/lib/utils/url_utility'); jest.mock('~/boards/eventhub'); @@ -29,8 +32,28 @@ describe('Board card component', () => { let wrapper; let issue; let list; + let store; + + const findBoardBlockedIcon = () => wrapper.find(BoardBlockedIcon); + + const createStore = () => { + store = new Vuex.Store({ + ...defaultStore, + state: { + ...defaultStore.state, + issuableType: issuableTypes.issue, + }, + getters: { + isGroupBoard: () => true, + isEpicBoard: () => false, + isProjectBoard: () => false, + }, + }); + }; + + const createWrapper = (props = {}) => { + createStore(); - const createWrapper = (props = {}, store = defaultStore) => { wrapper = mount(BoardCardInner, { store, propsData: { @@ -41,6 +64,13 @@ describe('Board card component', () => { stubs: { GlLabel: true, }, + mocks: { + $apollo: { + queries: { + blockingIssuables: { loading: false }, + }, + }, + }, provide: { rootPath: '/', scopedLabelsAvailable: false, @@ -51,14 +81,9 @@ describe('Board card component', () => { beforeEach(() => { list = mockLabelList; issue = { - title: 'Testing', - id: 1, - iid: 1, - confidential: false, + ...mockIssue, labels: [list.label], assignees: [], - referencePath: '#1', - webUrl: '/test/1', weight: 1, }; @@ -68,6 +93,7 @@ describe('Board card component', () => { afterEach(() => { wrapper.destroy(); wrapper = null; + store = null; jest.clearAllMocks(); }); @@ -87,18 +113,38 @@ describe('Board card component', () => { expect(wrapper.find('.confidential-icon').exists()).toBe(false); }); - it('does not render blocked icon', () => { - expect(wrapper.find('.issue-blocked-icon').exists()).toBe(false); - }); - it('renders issue ID with #', () => { - expect(wrapper.find('.board-card-number').text()).toContain(`#${issue.id}`); + expect(wrapper.find('.board-card-number').text()).toContain(`#${issue.iid}`); }); it('does not render assignee', () => { expect(wrapper.find('.board-card-assignee .avatar').exists()).toBe(false); }); + describe('blocked', () => { + it('renders blocked icon if issue is blocked', async () => { + createWrapper({ + item: { + ...issue, + blocked: true, + }, + }); + + expect(findBoardBlockedIcon().exists()).toBe(true); + }); + + it('does not show blocked icon if issue is not blocked', () => { + createWrapper({ + item: { + ...issue, + blocked: false, + }, + }); + + expect(findBoardBlockedIcon().exists()).toBe(false); + }); + }); + describe('confidential issue', () => { beforeEach(() => { wrapper.setProps({ @@ -303,21 +349,6 @@ describe('Board card component', () => { }); }); - describe('blocked', () => { - beforeEach(() => { - wrapper.setProps({ - item: { - ...wrapper.props('item'), - blocked: true, - }, - }); - }); - - it('renders blocked icon if issue is blocked', () => { - expect(wrapper.find('.issue-blocked-icon').exists()).toBe(true); - }); - }); - describe('filterByLabel method', () => { beforeEach(() => { delete window.location; diff --git a/spec/frontend/boards/board_new_issue_deprecated_spec.js b/spec/frontend/boards/board_new_issue_deprecated_spec.js index 3903ad201b2..3beaf870bf5 100644 --- a/spec/frontend/boards/board_new_issue_deprecated_spec.js +++ b/spec/frontend/boards/board_new_issue_deprecated_spec.js @@ -111,7 +111,7 @@ describe('Issue boards new issue form', () => { describe('submit success', () => { it('creates new issue', () => { - wrapper.setData({ title: 'submit issue' }); + wrapper.setData({ title: 'create issue' }); return Vue.nextTick() .then(submitIssue) @@ -122,7 +122,7 @@ describe('Issue boards new issue form', () => { it('enables button after submit', () => { jest.spyOn(wrapper.vm, 'submit').mockImplementation(); - wrapper.setData({ title: 'submit issue' }); + wrapper.setData({ title: 'create issue' }); return Vue.nextTick() .then(submitIssue) @@ -132,7 +132,7 @@ describe('Issue boards new issue form', () => { }); it('clears title after submit', () => { - wrapper.setData({ title: 'submit issue' }); + wrapper.setData({ title: 'create issue' }); return Vue.nextTick() .then(submitIssue) @@ -143,17 +143,17 @@ describe('Issue boards new issue form', () => { it('sets detail issue after submit', () => { expect(boardsStore.detail.issue.title).toBe(undefined); - wrapper.setData({ title: 'submit issue' }); + wrapper.setData({ title: 'create issue' }); return Vue.nextTick() .then(submitIssue) .then(() => { - expect(boardsStore.detail.issue.title).toBe('submit issue'); + expect(boardsStore.detail.issue.title).toBe('create issue'); }); }); it('sets detail list after submit', () => { - wrapper.setData({ title: 'submit issue' }); + wrapper.setData({ title: 'create issue' }); return Vue.nextTick() .then(submitIssue) @@ -164,7 +164,7 @@ describe('Issue boards new issue form', () => { it('sets detail weight after submit', () => { boardsStore.weightFeatureAvailable = true; - wrapper.setData({ title: 'submit issue' }); + wrapper.setData({ title: 'create issue' }); return Vue.nextTick() .then(submitIssue) @@ -175,7 +175,7 @@ describe('Issue boards new issue form', () => { it('does not set detail weight after submit', () => { boardsStore.weightFeatureAvailable = false; - wrapper.setData({ title: 'submit issue' }); + wrapper.setData({ title: 'create issue' }); return Vue.nextTick() .then(submitIssue) diff --git a/spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap b/spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap new file mode 100644 index 00000000000..c000f300e4d --- /dev/null +++ b/spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BoardBlockedIcon on mouseenter on blocked icon with more than three blocking issues matches the snapshot 1`] = ` +"<div class=\\"gl-display-inline\\"><svg data-testid=\\"issue-blocked-icon\\" aria-hidden=\\"true\\" class=\\"issue-blocked-icon gl-mr-2 gl-cursor-pointer gl-icon s16\\" id=\\"blocked-icon-uniqueId\\"> + <use href=\\"#issue-block\\"></use> + </svg> + <div class=\\"gl-popover\\"> + <ul class=\\"gl-list-style-none gl-p-0\\"> + <li><a href=\\"http://gdk.test:3000/gitlab-org/my-project-1/-/issues/6\\" class=\\"gl-link gl-text-blue-500! gl-font-sm\\">my-project-1#6</a> + <p data-testid=\\"issuable-title\\" class=\\"gl-mb-3 gl-display-block!\\"> + blocking issue title 1 + </p> + </li> + <li><a href=\\"http://gdk.test:3000/gitlab-org/my-project-1/-/issues/5\\" class=\\"gl-link gl-text-blue-500! gl-font-sm\\">my-project-1#5</a> + <p data-testid=\\"issuable-title\\" class=\\"gl-mb-3 gl-display-block!\\"> + blocking issue title 2 + blocking issue title 2 + blocking issue title 2 + bloc… + </p> + </li> + <li><a href=\\"http://gdk.test:3000/gitlab-org/my-project-1/-/issues/4\\" class=\\"gl-link gl-text-blue-500! gl-font-sm\\">my-project-1#4</a> + <p data-testid=\\"issuable-title\\" class=\\"gl-mb-3 gl-display-block!\\"> + blocking issue title 3 + </p> + </li> + </ul> + <div class=\\"gl-mt-4\\"> + <p data-testid=\\"hidden-blocking-count\\" class=\\"gl-mb-3\\">+ 1 more issue</p> <a data-testid=\\"view-all-issues\\" href=\\"http://gdk.test:3000/gitlab-org/my-project-1/-/issues/0#related-issues\\" class=\\"gl-link gl-text-blue-500! gl-font-sm\\">View all blocking issues</a> + </div><span data-testid=\\"popover-title\\">Blocked by 4 issues</span> + </div> +</div>" +`; diff --git a/spec/frontend/boards/components/board_add_new_column_form_spec.js b/spec/frontend/boards/components/board_add_new_column_form_spec.js index 3702f55f17b..3b26ca57d6f 100644 --- a/spec/frontend/boards/components/board_add_new_column_form_spec.js +++ b/spec/frontend/boards/components/board_add_new_column_form_spec.js @@ -1,6 +1,6 @@ -import { GlFormGroup, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui'; +import { GlDropdown, GlFormGroup, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; +import Vue from 'vue'; import Vuex from 'vuex'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue'; @@ -25,7 +25,7 @@ describe('Board card layout', () => { const mountComponent = ({ loading = false, - formDescription = '', + noneSelected = '', searchLabel = '', searchPlaceholder = '', selectedId, @@ -34,12 +34,9 @@ describe('Board card layout', () => { } = {}) => { wrapper = extendedWrapper( shallowMount(BoardAddNewColumnForm, { - stubs: { - GlFormGroup: true, - }, propsData: { loading, - formDescription, + noneSelected, searchLabel, searchPlaceholder, selectedId, @@ -51,13 +48,15 @@ describe('Board card layout', () => { ...actions, }, }), + stubs: { + GlDropdown, + }, }), ); }; afterEach(() => { wrapper.destroy(); - wrapper = null; }); const formTitle = () => wrapper.findByTestId('board-add-column-form-title').text(); @@ -65,10 +64,13 @@ describe('Board card layout', () => { const findSearchLabel = () => wrapper.find(GlFormGroup); const cancelButton = () => wrapper.findByTestId('cancelAddNewColumn'); const submitButton = () => wrapper.findByTestId('addNewColumnButton'); + const findDropdown = () => wrapper.findComponent(GlDropdown); it('shows form title & search input', () => { mountComponent(); + findDropdown().vm.$emit('show'); + expect(formTitle()).toEqual(BoardAddNewColumnForm.i18n.newList); expect(findSearchInput().exists()).toBe(true); }); @@ -86,16 +88,6 @@ describe('Board card layout', () => { expect(setAddColumnFormVisibility).toHaveBeenCalledWith(expect.anything(), false); }); - it('sets placeholder and description from props', () => { - const props = { - formDescription: 'Some description of a list', - }; - - mountComponent(props); - - expect(wrapper.html()).toHaveText(props.formDescription); - }); - describe('items', () => { const mountWithItems = (loading) => mountComponent({ @@ -151,13 +143,11 @@ describe('Board card layout', () => { expect(submitButton().props('disabled')).toBe(true); }); - it('emits add-list event on click', async () => { + it('emits add-list event on click', () => { mountComponent({ selectedId: mockLabelList.label.id, }); - await nextTick(); - submitButton().vm.$emit('click'); expect(wrapper.emitted('add-list')).toEqual([[]]); diff --git a/spec/frontend/boards/components/board_add_new_column_spec.js b/spec/frontend/boards/components/board_add_new_column_spec.js index 60584eaf6cf..61f210f566b 100644 --- a/spec/frontend/boards/components/board_add_new_column_spec.js +++ b/spec/frontend/boards/components/board_add_new_column_spec.js @@ -1,3 +1,4 @@ +import { GlFormRadioGroup } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; @@ -12,6 +13,10 @@ Vue.use(Vuex); describe('Board card layout', () => { let wrapper; + const selectLabel = (id) => { + wrapper.findComponent(GlFormRadioGroup).vm.$emit('change', id); + }; + const createStore = ({ actions = {}, getters = {}, state = {} } = {}) => { return new Vuex.Store({ state: { @@ -57,6 +62,11 @@ describe('Board card layout', () => { }, }), ); + + // trigger change event + if (selectedId) { + selectLabel(selectedId); + } }; afterEach(() => { diff --git a/spec/frontend/boards/components/board_blocked_icon_spec.js b/spec/frontend/boards/components/board_blocked_icon_spec.js new file mode 100644 index 00000000000..7b04942f056 --- /dev/null +++ b/spec/frontend/boards/components/board_blocked_icon_spec.js @@ -0,0 +1,226 @@ +import { GlIcon, GlLink, GlPopover, GlLoadingIcon } from '@gitlab/ui'; +import { shallowMount, mount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import BoardBlockedIcon from '~/boards/components/board_blocked_icon.vue'; +import { blockingIssuablesQueries, issuableTypes } from '~/boards/constants'; +import { truncate } from '~/lib/utils/text_utility'; +import { + mockIssue, + mockBlockingIssue1, + mockBlockingIssue2, + mockBlockingIssuablesResponse1, + mockBlockingIssuablesResponse2, + mockBlockingIssuablesResponse3, + mockBlockedIssue1, + mockBlockedIssue2, +} from '../mock_data'; + +describe('BoardBlockedIcon', () => { + let wrapper; + let mockApollo; + + const findGlIcon = () => wrapper.find(GlIcon); + const findGlPopover = () => wrapper.find(GlPopover); + const findGlLink = () => wrapper.find(GlLink); + const findPopoverTitle = () => wrapper.findByTestId('popover-title'); + const findIssuableTitle = () => wrapper.findByTestId('issuable-title'); + const findHiddenBlockingCount = () => wrapper.findByTestId('hidden-blocking-count'); + const findViewAllIssuableLink = () => wrapper.findByTestId('view-all-issues'); + + const waitForApollo = async () => { + jest.runOnlyPendingTimers(); + await waitForPromises(); + }; + + const mouseenter = async () => { + findGlIcon().vm.$emit('mouseenter'); + + await wrapper.vm.$nextTick(); + await waitForApollo(); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + const createWrapperWithApollo = ({ + item = mockBlockedIssue1, + blockingIssuablesSpy = jest.fn().mockResolvedValue(mockBlockingIssuablesResponse1), + } = {}) => { + mockApollo = createMockApollo([ + [blockingIssuablesQueries[issuableTypes.issue].query, blockingIssuablesSpy], + ]); + + Vue.use(VueApollo); + wrapper = extendedWrapper( + mount(BoardBlockedIcon, { + apolloProvider: mockApollo, + propsData: { + item: { + ...mockIssue, + ...item, + }, + uniqueId: 'uniqueId', + issuableType: issuableTypes.issue, + }, + attachTo: document.body, + }), + ); + }; + + const createWrapper = ({ item = {}, queries = {}, data = {}, loading = false } = {}) => { + wrapper = extendedWrapper( + shallowMount(BoardBlockedIcon, { + propsData: { + item: { + ...mockIssue, + ...item, + }, + uniqueId: 'uniqueid', + issuableType: issuableTypes.issue, + }, + data() { + return { + ...data, + }; + }, + mocks: { + $apollo: { + queries: { + blockingIssuables: { loading }, + ...queries, + }, + }, + }, + stubs: { + GlPopover, + }, + attachTo: document.body, + }), + ); + }; + + it('should render blocked icon', () => { + createWrapper(); + + expect(findGlIcon().exists()).toBe(true); + }); + + it('should display a loading spinner while loading', () => { + createWrapper({ loading: true }); + + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + }); + + it('should not query for blocking issuables by default', async () => { + createWrapperWithApollo(); + + expect(findGlPopover().text()).not.toContain(mockBlockingIssue1.title); + }); + + describe('on mouseenter on blocked icon', () => { + it('should query for blocking issuables and render the result', async () => { + createWrapperWithApollo(); + + expect(findGlPopover().text()).not.toContain(mockBlockingIssue1.title); + + await mouseenter(); + + expect(findGlPopover().exists()).toBe(true); + expect(findIssuableTitle().text()).toContain(mockBlockingIssue1.title); + expect(wrapper.vm.skip).toBe(true); + }); + + it('should emit "blocking-issuables-error" event on query error', async () => { + const mockError = new Error('mayday'); + createWrapperWithApollo({ blockingIssuablesSpy: jest.fn().mockRejectedValue(mockError) }); + + await mouseenter(); + + const [ + [ + { + message, + error: { networkError }, + }, + ], + ] = wrapper.emitted('blocking-issuables-error'); + expect(message).toBe('Failed to fetch blocking issues'); + expect(networkError).toBe(mockError); + }); + + describe('with a single blocking issue', () => { + beforeEach(async () => { + createWrapperWithApollo(); + + await mouseenter(); + }); + + it('should render a title of the issuable', async () => { + expect(findIssuableTitle().text()).toBe(mockBlockingIssue1.title); + }); + + it('should render issuable reference and link to the issuable', async () => { + const formattedRef = mockBlockingIssue1.reference.split('/')[1]; + + expect(findGlLink().text()).toBe(formattedRef); + expect(findGlLink().attributes('href')).toBe(mockBlockingIssue1.webUrl); + }); + + it('should render popover title with correct blocking issuable count', async () => { + expect(findPopoverTitle().text()).toBe('Blocked by 1 issue'); + }); + }); + + describe('when issue has a long title', () => { + it('should render a truncated title', async () => { + createWrapperWithApollo({ + blockingIssuablesSpy: jest.fn().mockResolvedValue(mockBlockingIssuablesResponse2), + }); + + await mouseenter(); + + const truncatedTitle = truncate( + mockBlockingIssue2.title, + wrapper.vm.$options.textTruncateWidth, + ); + expect(findIssuableTitle().text()).toBe(truncatedTitle); + }); + }); + + describe('with more than three blocking issues', () => { + beforeEach(async () => { + createWrapperWithApollo({ + item: mockBlockedIssue2, + blockingIssuablesSpy: jest.fn().mockResolvedValue(mockBlockingIssuablesResponse3), + }); + + await mouseenter(); + }); + + it('matches the snapshot', () => { + expect(wrapper.html()).toMatchSnapshot(); + }); + + it('should render popover title with correct blocking issuable count', async () => { + expect(findPopoverTitle().text()).toBe('Blocked by 4 issues'); + }); + + it('should render the number of hidden blocking issuables', () => { + expect(findHiddenBlockingCount().text()).toBe('+ 1 more issue'); + }); + + it('should link to the blocked issue page at the related issue anchor', async () => { + expect(findViewAllIssuableLink().text()).toBe('View all blocking issues'); + expect(findViewAllIssuableLink().attributes('href')).toBe( + `${mockBlockedIssue2.webUrl}#related-issues`, + ); + }); + }); + }); +}); diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js new file mode 100644 index 00000000000..7f949739891 --- /dev/null +++ b/spec/frontend/boards/components/board_content_sidebar_spec.js @@ -0,0 +1,140 @@ +import { GlDrawer } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import Vuex from 'vuex'; +import { stubComponent } from 'helpers/stub_component'; +import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue'; +import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue'; +import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue'; +import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue'; +import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue'; +import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue'; +import { ISSUABLE } from '~/boards/constants'; +import { mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data'; + +describe('BoardContentSidebar', () => { + let wrapper; + let store; + + const createStore = ({ mockGetters = {}, mockActions = {} } = {}) => { + store = new Vuex.Store({ + state: { + sidebarType: ISSUABLE, + issues: { [mockIssue.id]: { ...mockIssue, epic: null } }, + activeId: mockIssue.id, + issuableType: 'issue', + }, + getters: { + activeBoardItem: () => { + return { ...mockIssue, epic: null }; + }, + groupPathForActiveIssue: () => mockIssueGroupPath, + projectPathForActiveIssue: () => mockIssueProjectPath, + isSidebarOpen: () => true, + ...mockGetters, + }, + actions: mockActions, + }); + }; + + const createComponent = () => { + /* + Dynamically imported components (in our case ee imports) + aren't stubbed automatically in VTU v1: + https://github.com/vuejs/vue-test-utils/issues/1279. + + This requires us to additionally mock apollo or vuex stores. + */ + wrapper = shallowMount(BoardContentSidebar, { + provide: { + canUpdate: true, + rootPath: '/', + groupId: 1, + }, + store, + stubs: { + GlDrawer: stubComponent(GlDrawer, { + template: '<div><slot name="header"></slot><slot></slot></div>', + }), + }, + mocks: { + $apollo: { + queries: { + participants: { + loading: false, + }, + currentIteration: { + loading: false, + }, + iterations: { + loading: false, + }, + }, + }, + }, + }); + }; + + beforeEach(() => { + createStore(); + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('confirms we render GlDrawer', () => { + expect(wrapper.find(GlDrawer).exists()).toBe(true); + }); + + it('does not render GlDrawer when isSidebarOpen is false', () => { + createStore({ mockGetters: { isSidebarOpen: () => false } }); + createComponent(); + + expect(wrapper.find(GlDrawer).exists()).toBe(false); + }); + + it('applies an open attribute', () => { + expect(wrapper.find(GlDrawer).props('open')).toBe(true); + }); + + it('renders BoardSidebarLabelsSelect', () => { + expect(wrapper.find(BoardSidebarLabelsSelect).exists()).toBe(true); + }); + + it('renders BoardSidebarTitle', () => { + expect(wrapper.find(BoardSidebarTitle).exists()).toBe(true); + }); + + it('renders BoardSidebarDueDate', () => { + expect(wrapper.find(BoardSidebarDueDate).exists()).toBe(true); + }); + + it('renders BoardSidebarSubscription', () => { + expect(wrapper.find(BoardSidebarSubscription).exists()).toBe(true); + }); + + it('renders BoardSidebarMilestoneSelect', () => { + expect(wrapper.find(BoardSidebarMilestoneSelect).exists()).toBe(true); + }); + + describe('when we emit close', () => { + let toggleBoardItem; + + beforeEach(() => { + toggleBoardItem = jest.fn(); + createStore({ mockActions: { toggleBoardItem } }); + createComponent(); + }); + + it('calls toggleBoardItem with correct parameters', async () => { + wrapper.find(GlDrawer).vm.$emit('close'); + + expect(toggleBoardItem).toHaveBeenCalledTimes(1); + expect(toggleBoardItem).toHaveBeenCalledWith(expect.any(Object), { + boardItem: { ...mockIssue, epic: null }, + sidebarType: ISSUABLE, + }); + }); + }); +}); diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js index 159b67ccc67..8c1a7bd3947 100644 --- a/spec/frontend/boards/components/board_content_spec.js +++ b/spec/frontend/boards/components/board_content_spec.js @@ -33,7 +33,12 @@ describe('BoardContent', () => { }); }; - const createComponent = ({ state, props = {}, graphqlBoardListsEnabled = false } = {}) => { + const createComponent = ({ + state, + props = {}, + graphqlBoardListsEnabled = false, + canAdminList = true, + } = {}) => { const store = createStore({ ...defaultState, ...state, @@ -42,11 +47,11 @@ describe('BoardContent', () => { localVue, propsData: { lists: mockListsWithModel, - canAdminList: true, disabled: false, ...props, }, provide: { + canAdminList, glFeatures: { graphqlBoardLists: graphqlBoardListsEnabled }, }, store, @@ -82,7 +87,7 @@ describe('BoardContent', () => { describe('can admin list', () => { beforeEach(() => { - createComponent({ graphqlBoardListsEnabled: true, props: { canAdminList: true } }); + createComponent({ graphqlBoardListsEnabled: true, canAdminList: true }); }); it('renders draggable component', () => { @@ -92,7 +97,7 @@ describe('BoardContent', () => { describe('can not admin list', () => { beforeEach(() => { - createComponent({ graphqlBoardListsEnabled: true, props: { canAdminList: false } }); + createComponent({ graphqlBoardListsEnabled: true, canAdminList: false }); }); it('does not render draggable component', () => { diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js index 32499bd5480..24fcdd528d5 100644 --- a/spec/frontend/boards/components/board_form_spec.js +++ b/spec/frontend/boards/components/board_form_spec.js @@ -226,7 +226,7 @@ describe('BoardForm', () => { it('passes correct primary action text and variant', () => { expect(findModalActionPrimary().text).toBe('Save changes'); - expect(findModalActionPrimary().attributes[0].variant).toBe('info'); + expect(findModalActionPrimary().attributes[0].variant).toBe('confirm'); }); it('does not render delete confirmation message', () => { diff --git a/spec/frontend/boards/components/board_new_issue_spec.js b/spec/frontend/boards/components/board_new_issue_spec.js index 737a18294bc..e6405bbcff3 100644 --- a/spec/frontend/boards/components/board_new_issue_spec.js +++ b/spec/frontend/boards/components/board_new_issue_spec.js @@ -86,7 +86,7 @@ describe('Issue boards new issue form', () => { describe('submit success', () => { it('creates new issue', async () => { - wrapper.setData({ title: 'submit issue' }); + wrapper.setData({ title: 'create issue' }); await vm.$nextTick(); await submitIssue(); @@ -95,7 +95,7 @@ describe('Issue boards new issue form', () => { it('enables button after submit', async () => { jest.spyOn(wrapper.vm, 'submit').mockImplementation(); - wrapper.setData({ title: 'submit issue' }); + wrapper.setData({ title: 'create issue' }); await vm.$nextTick(); await submitIssue(); @@ -103,7 +103,7 @@ describe('Issue boards new issue form', () => { }); it('clears title after submit', async () => { - wrapper.setData({ title: 'submit issue' }); + wrapper.setData({ title: 'create issue' }); await vm.$nextTick(); await submitIssue(); diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js index 52b4d71f7b9..464331b6e30 100644 --- a/spec/frontend/boards/components/board_settings_sidebar_spec.js +++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js @@ -4,6 +4,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import Vuex from 'vuex'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue'; import { inactiveId, LIST } from '~/boards/constants'; import { createStore } from '~/boards/stores'; @@ -22,11 +23,18 @@ describe('BoardSettingsSidebar', () => { const labelColor = '#FFFF'; const listId = 1; - const createComponent = () => { - wrapper = shallowMount(BoardSettingsSidebar, { - store, - localVue, - }); + const findRemoveButton = () => wrapper.findByTestId('remove-list'); + + const createComponent = ({ canAdminList = false } = {}) => { + wrapper = extendedWrapper( + shallowMount(BoardSettingsSidebar, { + store, + localVue, + provide: { + canAdminList, + }, + }), + ); }; const findLabel = () => wrapper.find(GlLabel); const findDrawer = () => wrapper.find(GlDrawer); @@ -164,4 +172,29 @@ describe('BoardSettingsSidebar', () => { expect(findDrawer().exists()).toBe(false); }); }); + + it('does not render "Remove list" when user cannot admin the boards list', () => { + createComponent(); + + expect(findRemoveButton().exists()).toBe(false); + }); + + describe('when user can admin the boards list', () => { + beforeEach(() => { + store.state.activeId = listId; + store.state.sidebarType = LIST; + + boardsStore.addList({ + id: listId, + label: { title: labelTitle, color: labelColor }, + list_type: 'label', + }); + + createComponent({ canAdminList: true }); + }); + + it('renders "Remove list" button', () => { + expect(findRemoveButton().exists()).toBe(true); + }); + }); }); diff --git a/spec/frontend/boards/components/filtered_search_spec.js b/spec/frontend/boards/components/filtered_search_spec.js deleted file mode 100644 index 7f238aa671f..00000000000 --- a/spec/frontend/boards/components/filtered_search_spec.js +++ /dev/null @@ -1,65 +0,0 @@ -import { createLocalVue, shallowMount } from '@vue/test-utils'; -import Vuex from 'vuex'; -import FilteredSearch from '~/boards/components/filtered_search.vue'; -import { createStore } from '~/boards/stores'; -import * as commonUtils from '~/lib/utils/common_utils'; -import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; - -const localVue = createLocalVue(); -localVue.use(Vuex); - -describe('FilteredSearch', () => { - let wrapper; - let store; - - const createComponent = () => { - wrapper = shallowMount(FilteredSearch, { - localVue, - propsData: { search: '' }, - store, - attachTo: document.body, - }); - }; - - beforeEach(() => { - // this needed for actions call for performSearch - window.gon = { features: {} }; - }); - - afterEach(() => { - wrapper.destroy(); - }); - - describe('default', () => { - beforeEach(() => { - store = createStore(); - - jest.spyOn(store, 'dispatch'); - - createComponent(); - }); - - it('finds FilteredSearch', () => { - expect(wrapper.find(FilteredSearchBarRoot).exists()).toBe(true); - }); - - describe('when onFilter is emitted', () => { - it('calls performSearch', () => { - wrapper.find(FilteredSearchBarRoot).vm.$emit('onFilter', [{ value: { data: '' } }]); - - expect(store.dispatch).toHaveBeenCalledWith('performSearch'); - }); - - it('calls historyPushState', () => { - commonUtils.historyPushState = jest.fn(); - wrapper - .find(FilteredSearchBarRoot) - .vm.$emit('onFilter', [{ value: { data: 'searchQuery' } }]); - - expect(commonUtils.historyPushState).toHaveBeenCalledWith( - 'http://test.host/?search=searchQuery', - ); - }); - }); - }); -}); diff --git a/spec/frontend/boards/components/issue_time_estimate_spec.js b/spec/frontend/boards/components/issue_time_estimate_spec.js index 2e253d24125..635964b6b4a 100644 --- a/spec/frontend/boards/components/issue_time_estimate_spec.js +++ b/spec/frontend/boards/components/issue_time_estimate_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { config as vueConfig } from 'vue'; +import Vue from 'vue'; import IssueTimeEstimate from '~/boards/components/issue_time_estimate.vue'; describe('Issue Time Estimate component', () => { @@ -34,10 +34,10 @@ describe('Issue Time Estimate component', () => { try { // This will raise props validating warning by Vue, silencing it - vueConfig.silent = true; + Vue.config.silent = true; await wrapper.setProps({ estimate: 'Foo <script>alert("XSS")</script>' }); } finally { - vueConfig.silent = false; + Vue.config.silent = false; } expect(alertSpy).not.toHaveBeenCalled(); diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js index 98ac211238c..153d0640b23 100644 --- a/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js +++ b/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js @@ -64,7 +64,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => { beforeEach(async () => { createWrapper(); - jest.spyOn(wrapper.vm, 'setActiveIssueLabels').mockImplementation(() => TEST_LABELS); + jest.spyOn(wrapper.vm, 'setActiveBoardItemLabels').mockImplementation(() => TEST_LABELS); findLabelsSelect().vm.$emit('updateSelectedLabels', TEST_LABELS_PAYLOAD); store.state.boardItems[TEST_ISSUE.id].labels = TEST_LABELS; await wrapper.vm.$nextTick(); @@ -76,7 +76,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => { }); it('commits change to the server', () => { - expect(wrapper.vm.setActiveIssueLabels).toHaveBeenCalledWith({ + expect(wrapper.vm.setActiveBoardItemLabels).toHaveBeenCalledWith({ addLabelIds: TEST_LABELS.map((label) => label.id), projectPath: 'gitlab-org/test-subgroup/gitlab-test', removeLabelIds: [], @@ -94,13 +94,13 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => { beforeEach(async () => { createWrapper({ labels: TEST_LABELS }); - jest.spyOn(wrapper.vm, 'setActiveIssueLabels').mockImplementation(() => expectedLabels); + jest.spyOn(wrapper.vm, 'setActiveBoardItemLabels').mockImplementation(() => expectedLabels); findLabelsSelect().vm.$emit('updateSelectedLabels', testLabelsPayload); await wrapper.vm.$nextTick(); }); it('commits change to the server', () => { - expect(wrapper.vm.setActiveIssueLabels).toHaveBeenCalledWith({ + expect(wrapper.vm.setActiveBoardItemLabels).toHaveBeenCalledWith({ addLabelIds: [5, 7], removeLabelIds: [6], projectPath: 'gitlab-org/test-subgroup/gitlab-test', @@ -114,13 +114,13 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => { beforeEach(async () => { createWrapper({ labels: [testLabel] }); - jest.spyOn(wrapper.vm, 'setActiveIssueLabels').mockImplementation(() => {}); + jest.spyOn(wrapper.vm, 'setActiveBoardItemLabels').mockImplementation(() => {}); }); it('commits change to the server', () => { wrapper.find(GlLabel).vm.$emit('close', testLabel); - expect(wrapper.vm.setActiveIssueLabels).toHaveBeenCalledWith({ + expect(wrapper.vm.setActiveBoardItemLabels).toHaveBeenCalledWith({ removeLabelIds: [getIdFromGraphQLId(testLabel.id)], projectPath: 'gitlab-org/test-subgroup/gitlab-test', }); @@ -131,7 +131,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => { beforeEach(async () => { createWrapper({ labels: TEST_LABELS }); - jest.spyOn(wrapper.vm, 'setActiveIssueLabels').mockImplementation(() => { + jest.spyOn(wrapper.vm, 'setActiveBoardItemLabels').mockImplementation(() => { throw new Error(['failed mutation']); }); findLabelsSelect().vm.$emit('updateSelectedLabels', [{ id: '?' }]); diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js index cfd7f32b2cc..7976e73ff2f 100644 --- a/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js +++ b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js @@ -1,5 +1,6 @@ import { GlToggle, GlLoadingIcon } from '@gitlab/ui'; -import { mount, createLocalVue } from '@vue/test-utils'; +import { mount } from '@vue/test-utils'; +import Vue from 'vue'; import Vuex from 'vuex'; import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue'; import { createStore } from '~/boards/stores'; @@ -9,8 +10,7 @@ import { mockActiveIssue } from '../../mock_data'; jest.mock('~/flash.js'); -const localVue = createLocalVue(); -localVue.use(Vuex); +Vue.use(Vuex); describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () => { let wrapper; @@ -20,14 +20,16 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () = const findToggle = () => wrapper.find(GlToggle); const findGlLoadingIcon = () => wrapper.find(GlLoadingIcon); - const createComponent = (activeIssue = { ...mockActiveIssue }) => { + const createComponent = (activeBoardItem = { ...mockActiveIssue }) => { store = createStore(); - store.state.boardItems = { [activeIssue.id]: activeIssue }; - store.state.activeId = activeIssue.id; + store.state.boardItems = { [activeBoardItem.id]: activeBoardItem }; + store.state.activeId = activeBoardItem.id; wrapper = mount(BoardSidebarSubscription, { - localVue, store, + provide: { + emailsDisabled: false, + }, }); }; @@ -90,9 +92,9 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () = 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, + jest.spyOn(wrapper.vm, 'setActiveItemSubscribed').mockImplementation(async () => { + store.commit(types.UPDATE_BOARD_ITEM_BY_ID, { + itemId: mockActiveIssue.id, prop: 'subscribed', value: subscribedState, }); @@ -110,7 +112,7 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () = await wrapper.vm.$nextTick(); expect(findGlLoadingIcon().exists()).toBe(true); - expect(wrapper.vm.setActiveIssueSubscribed).toHaveBeenCalledWith({ + expect(wrapper.vm.setActiveItemSubscribed).toHaveBeenCalledWith({ subscribed: true, projectPath: 'gitlab-org/test-subgroup/gitlab-test', }); @@ -134,7 +136,7 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () = await wrapper.vm.$nextTick(); - expect(wrapper.vm.setActiveIssueSubscribed).toHaveBeenCalledWith({ + expect(wrapper.vm.setActiveItemSubscribed).toHaveBeenCalledWith({ subscribed: false, projectPath: 'gitlab-org/test-subgroup/gitlab-test', }); @@ -148,7 +150,7 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () = it('flashes an error message when setting the subscribed state fails', async () => { createComponent(); - jest.spyOn(wrapper.vm, 'setActiveIssueSubscribed').mockImplementation(async () => { + jest.spyOn(wrapper.vm, 'setActiveItemSubscribed').mockImplementation(async () => { throw new Error(); }); diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js new file mode 100644 index 00000000000..03924bfa8d3 --- /dev/null +++ b/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js @@ -0,0 +1,58 @@ +/* + To avoid duplicating tests in time_tracker.spec, + this spec only contains a simple test to check rendering. + + A detailed feature spec is used to test time tracking feature + in swimlanes sidebar. +*/ + +import { shallowMount } from '@vue/test-utils'; +import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue'; +import { createStore } from '~/boards/stores'; +import IssuableTimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue'; + +describe('BoardSidebarTimeTracker', () => { + let wrapper; + let store; + + const createComponent = (options) => { + wrapper = shallowMount(BoardSidebarTimeTracker, { + store, + ...options, + }); + }; + + beforeEach(() => { + store = createStore(); + store.state.boardItems = { + 1: { + timeEstimate: 3600, + totalTimeSpent: 1800, + humanTimeEstimate: '1h', + humanTotalTimeSpent: '30min', + }, + }; + store.state.activeId = '1'; + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it.each([[true], [false]])( + 'renders IssuableTimeTracker with correct spent and estimated time (timeTrackingLimitToHours=%s)', + (timeTrackingLimitToHours) => { + createComponent({ provide: { timeTrackingLimitToHours } }); + + expect(wrapper.find(IssuableTimeTracker).props()).toEqual({ + timeEstimate: 3600, + timeSpent: 1800, + humanTimeEstimate: '1h', + humanTimeSpent: '30min', + limitToHours: timeTrackingLimitToHours, + showCollapsed: false, + }); + }, + ); +}); diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_issue_title_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js index 723d0345f76..c8ccd4c88a5 100644 --- a/spec/frontend/boards/components/sidebar/board_sidebar_issue_title_spec.js +++ b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js @@ -1,11 +1,11 @@ import { GlAlert, GlFormInput, GlForm } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue'; -import BoardSidebarIssueTitle from '~/boards/components/sidebar/board_sidebar_issue_title.vue'; +import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue'; import { createStore } from '~/boards/stores'; import createFlash from '~/flash'; -const TEST_TITLE = 'New issue title'; +const TEST_TITLE = 'New item title'; const TEST_ISSUE_A = { id: 'gid://gitlab/Issue/1', iid: 8, @@ -21,7 +21,7 @@ const TEST_ISSUE_B = { jest.mock('~/flash'); -describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => { +describe('~/boards/components/sidebar/board_sidebar_title.vue', () => { let wrapper; let store; @@ -32,12 +32,12 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => { wrapper = null; }); - const createWrapper = (issue = TEST_ISSUE_A) => { + const createWrapper = (item = TEST_ISSUE_A) => { store = createStore(); - store.state.boardItems = { [issue.id]: { ...issue } }; - store.dispatch('setActiveId', { id: issue.id }); + store.state.boardItems = { [item.id]: { ...item } }; + store.dispatch('setActiveId', { id: item.id }); - wrapper = shallowMount(BoardSidebarIssueTitle, { + wrapper = shallowMount(BoardSidebarTitle, { store, provide: { canUpdate: true, @@ -53,7 +53,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => { const findFormInput = () => wrapper.find(GlFormInput); const findEditableItem = () => wrapper.find(BoardEditableItem); const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]'); - const findTitle = () => wrapper.find('[data-testid="issue-title"]'); + const findTitle = () => wrapper.find('[data-testid="item-title"]'); const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]'); it('renders title and reference', () => { @@ -73,7 +73,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => { beforeEach(async () => { createWrapper(); - jest.spyOn(wrapper.vm, 'setActiveIssueTitle').mockImplementation(() => { + jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => { store.state.boardItems[TEST_ISSUE_A.id].title = TEST_TITLE; }); findFormInput().vm.$emit('input', TEST_TITLE); @@ -87,7 +87,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => { }); it('commits change to the server', () => { - expect(wrapper.vm.setActiveIssueTitle).toHaveBeenCalledWith({ + expect(wrapper.vm.setActiveItemTitle).toHaveBeenCalledWith({ title: TEST_TITLE, projectPath: 'h/b', }); @@ -98,14 +98,14 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => { beforeEach(async () => { createWrapper(); - jest.spyOn(wrapper.vm, 'setActiveIssueTitle').mockImplementation(() => {}); + jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {}); findFormInput().vm.$emit('input', ''); findForm().vm.$emit('submit', { preventDefault: () => {} }); await wrapper.vm.$nextTick(); }); it('commits change to the server', () => { - expect(wrapper.vm.setActiveIssueTitle).not.toHaveBeenCalled(); + expect(wrapper.vm.setActiveItemTitle).not.toHaveBeenCalled(); }); }); @@ -122,7 +122,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => { it('does not collapses sidebar and shows alert', () => { expect(findCollapsed().isVisible()).toBe(false); expect(findAlert().exists()).toBe(true); - expect(localStorage.getItem(`${TEST_ISSUE_A.id}/issue-title-pending-changes`)).toBe( + expect(localStorage.getItem(`${TEST_ISSUE_A.id}/item-title-pending-changes`)).toBe( TEST_TITLE, ); }); @@ -130,7 +130,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => { describe('when accessing the form with pending changes', () => { beforeAll(() => { - localStorage.setItem(`${TEST_ISSUE_A.id}/issue-title-pending-changes`, TEST_TITLE); + localStorage.setItem(`${TEST_ISSUE_A.id}/item-title-pending-changes`, TEST_TITLE); createWrapper(); }); @@ -146,7 +146,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => { beforeEach(async () => { createWrapper(TEST_ISSUE_B); - jest.spyOn(wrapper.vm, 'setActiveIssueTitle').mockImplementation(() => { + jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => { store.state.boardItems[TEST_ISSUE_B.id].title = TEST_TITLE; }); findFormInput().vm.$emit('input', TEST_TITLE); @@ -155,7 +155,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => { }); it('collapses sidebar and render former title', () => { - expect(wrapper.vm.setActiveIssueTitle).not.toHaveBeenCalled(); + expect(wrapper.vm.setActiveItemTitle).not.toHaveBeenCalled(); expect(findCollapsed().isVisible()).toBe(true); expect(findTitle().text()).toBe(TEST_ISSUE_B.title); }); @@ -165,7 +165,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => { beforeEach(async () => { createWrapper(TEST_ISSUE_B); - jest.spyOn(wrapper.vm, 'setActiveIssueTitle').mockImplementation(() => { + jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => { throw new Error(['failed mutation']); }); findFormInput().vm.$emit('input', 'Invalid title'); @@ -173,7 +173,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => { await wrapper.vm.$nextTick(); }); - it('collapses sidebar and renders former issue title', () => { + it('collapses sidebar and renders former item title', () => { expect(findCollapsed().isVisible()).toBe(true); expect(findTitle().text()).toContain(TEST_ISSUE_B.title); expect(createFlash).toHaveBeenCalled(); diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js index 500240d00fc..1c5b7cf8248 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -3,6 +3,7 @@ import { keyBy } from 'lodash'; import Vue from 'vue'; import '~/boards/models/list'; +import { ListType } from '~/boards/constants'; import boardsStore from '~/boards/stores/boards_store'; export const boardObj = { @@ -125,7 +126,7 @@ export const labels = [ export const rawIssue = { title: 'Issue 1', id: 'gid://gitlab/Issue/436', - iid: 27, + iid: '27', dueDate: null, timeEstimate: 0, weight: null, @@ -152,7 +153,7 @@ export const rawIssue = { export const mockIssue = { id: 'gid://gitlab/Issue/436', - iid: 27, + iid: '27', title: 'Issue 1', dueDate: null, timeEstimate: 0, @@ -398,3 +399,128 @@ export const mockActiveGroupProjects = [ { ...mockGroupProject1, archived: false }, { ...mockGroupProject2, archived: false }, ]; + +export const mockIssueGroupPath = 'gitlab-org'; +export const mockIssueProjectPath = `${mockIssueGroupPath}/gitlab-test`; + +export const mockBlockingIssue1 = { + id: 'gid://gitlab/Issue/525', + iid: '6', + title: 'blocking issue title 1', + reference: 'gitlab-org/my-project-1#6', + webUrl: 'http://gdk.test:3000/gitlab-org/my-project-1/-/issues/6', + __typename: 'Issue', +}; + +export const mockBlockingIssue2 = { + id: 'gid://gitlab/Issue/524', + iid: '5', + title: + 'blocking issue title 2 + blocking issue title 2 + blocking issue title 2 + blocking issue title 2', + reference: 'gitlab-org/my-project-1#5', + webUrl: 'http://gdk.test:3000/gitlab-org/my-project-1/-/issues/5', + __typename: 'Issue', +}; + +export const mockBlockingIssue3 = { + id: 'gid://gitlab/Issue/523', + iid: '4', + title: 'blocking issue title 3', + reference: 'gitlab-org/my-project-1#4', + webUrl: 'http://gdk.test:3000/gitlab-org/my-project-1/-/issues/4', + __typename: 'Issue', +}; + +export const mockBlockingIssue4 = { + id: 'gid://gitlab/Issue/522', + iid: '3', + title: 'blocking issue title 4', + reference: 'gitlab-org/my-project-1#3', + webUrl: 'http://gdk.test:3000/gitlab-org/my-project-1/-/issues/3', + __typename: 'Issue', +}; + +export const mockBlockingIssuablesResponse1 = { + data: { + issuable: { + __typename: 'Issue', + id: 'gid://gitlab/Issue/527', + blockingIssuables: { + __typename: 'IssueConnection', + nodes: [mockBlockingIssue1], + }, + }, + }, +}; + +export const mockBlockingIssuablesResponse2 = { + data: { + issuable: { + __typename: 'Issue', + id: 'gid://gitlab/Issue/527', + blockingIssuables: { + __typename: 'IssueConnection', + nodes: [mockBlockingIssue2], + }, + }, + }, +}; + +export const mockBlockingIssuablesResponse3 = { + data: { + issuable: { + __typename: 'Issue', + id: 'gid://gitlab/Issue/527', + blockingIssuables: { + __typename: 'IssueConnection', + nodes: [mockBlockingIssue1, mockBlockingIssue2, mockBlockingIssue3, mockBlockingIssue4], + }, + }, + }, +}; + +export const mockBlockedIssue1 = { + id: '527', + blockedByCount: 1, +}; + +export const mockBlockedIssue2 = { + id: '527', + blockedByCount: 4, + webUrl: 'http://gdk.test:3000/gitlab-org/my-project-1/-/issues/0', +}; + +export const mockMoveIssueParams = { + itemId: 1, + fromListId: 'gid://gitlab/List/1', + toListId: 'gid://gitlab/List/2', + moveBeforeId: undefined, + moveAfterId: undefined, +}; + +export const mockMoveState = { + boardLists: { + 'gid://gitlab/List/1': { + listType: ListType.backlog, + }, + 'gid://gitlab/List/2': { + listType: ListType.closed, + }, + }, + boardItems: { + [mockMoveIssueParams.itemId]: { foo: 'bar' }, + }, + boardItemsByListId: { + [mockMoveIssueParams.fromListId]: [mockMoveIssueParams.itemId], + [mockMoveIssueParams.toListId]: [], + }, +}; + +export const mockMoveData = { + reordering: false, + shouldClone: false, + itemNotInToList: true, + originalIndex: 0, + originalIssue: { foo: 'bar' }, + ...mockMoveIssueParams, +}; diff --git a/spec/frontend/boards/modal_store_spec.js b/spec/frontend/boards/modal_store_spec.js deleted file mode 100644 index 5b5ae4b6556..00000000000 --- a/spec/frontend/boards/modal_store_spec.js +++ /dev/null @@ -1,134 +0,0 @@ -/* global ListIssue */ - -import '~/boards/models/label'; -import '~/boards/models/assignee'; -import '~/boards/models/issue'; -import '~/boards/models/list'; -import Store from '~/boards/stores/modal_store'; - -describe('Modal store', () => { - let issue; - let issue2; - - beforeEach(() => { - // Set up default state - Store.store.issues = []; - Store.store.selectedIssues = []; - - issue = new ListIssue({ - title: 'Testing', - id: 1, - iid: 1, - confidential: false, - labels: [], - assignees: [], - }); - issue2 = new ListIssue({ - title: 'Testing', - id: 2, - iid: 2, - confidential: false, - labels: [], - assignees: [], - }); - Store.store.issues.push(issue); - Store.store.issues.push(issue2); - }); - - it('returns selected count', () => { - expect(Store.selectedCount()).toBe(0); - }); - - it('toggles the issue as selected', () => { - Store.toggleIssue(issue); - - expect(issue.selected).toBe(true); - expect(Store.selectedCount()).toBe(1); - }); - - it('toggles the issue as un-selected', () => { - Store.toggleIssue(issue); - Store.toggleIssue(issue); - - expect(issue.selected).toBe(false); - expect(Store.selectedCount()).toBe(0); - }); - - it('toggles all issues as selected', () => { - Store.toggleAll(); - - expect(issue.selected).toBe(true); - expect(issue2.selected).toBe(true); - expect(Store.selectedCount()).toBe(2); - }); - - it('toggles all issues as un-selected', () => { - Store.toggleAll(); - Store.toggleAll(); - - expect(issue.selected).toBe(false); - expect(issue2.selected).toBe(false); - expect(Store.selectedCount()).toBe(0); - }); - - it('toggles all if a single issue is selected', () => { - Store.toggleIssue(issue); - Store.toggleAll(); - - expect(issue.selected).toBe(true); - expect(issue2.selected).toBe(true); - expect(Store.selectedCount()).toBe(2); - }); - - it('adds issue to selected array', () => { - issue.selected = true; - Store.addSelectedIssue(issue); - - expect(Store.selectedCount()).toBe(1); - }); - - it('removes issue from selected array', () => { - Store.addSelectedIssue(issue); - Store.removeSelectedIssue(issue); - - expect(Store.selectedCount()).toBe(0); - }); - - it('returns selected issue index if present', () => { - Store.toggleIssue(issue); - - expect(Store.selectedIssueIndex(issue)).toBe(0); - }); - - it('returns -1 if issue is not selected', () => { - expect(Store.selectedIssueIndex(issue)).toBe(-1); - }); - - it('finds the selected issue', () => { - Store.toggleIssue(issue); - - expect(Store.findSelectedIssue(issue)).toBe(issue); - }); - - it('does not find a selected issue', () => { - expect(Store.findSelectedIssue(issue)).toBe(undefined); - }); - - it('does not remove from selected issue if tab is not all', () => { - Store.store.activeTab = 'selected'; - - Store.toggleIssue(issue); - Store.toggleIssue(issue); - - expect(Store.store.selectedIssues.length).toBe(1); - expect(Store.selectedCount()).toBe(0); - }); - - it('gets selected issue array with only selected issues', () => { - Store.toggleIssue(issue); - Store.toggleIssue(issue2); - Store.toggleIssue(issue2); - - expect(Store.getSelectedIssues().length).toBe(1); - }); -}); diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index 69d2c8977fb..460e77a3f03 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -1,16 +1,21 @@ +import * as Sentry from '@sentry/browser'; +import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql'; import testAction from 'helpers/vuex_action_helper'; import { fullBoardId, formatListIssues, formatBoardLists, formatIssueInput, + formatIssue, + getMoveData, } from '~/boards/boards_util'; -import { inactiveId, ISSUABLE } from '~/boards/constants'; +import { inactiveId, ISSUABLE, ListType } from '~/boards/constants'; import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql'; import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql'; -import issueMoveListMutation from '~/boards/graphql/issue_move_list.mutation.graphql'; import actions, { gqlClient } from '~/boards/stores/actions'; import * as types from '~/boards/stores/mutation_types'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; + import { mockLists, mockListsById, @@ -22,6 +27,9 @@ import { labels, mockActiveIssue, mockGroupProjects, + mockMoveIssueParams, + mockMoveState, + mockMoveData, } from '../mock_data'; jest.mock('~/flash'); @@ -638,73 +646,314 @@ describe('resetIssues', () => { }); describe('moveItem', () => { - it('should dispatch moveIssue action', () => { + it('should dispatch moveIssue action with payload', () => { + const payload = { mock: 'payload' }; + testAction({ action: actions.moveItem, - expectedActions: [{ type: 'moveIssue' }], + payload, + expectedActions: [{ type: 'moveIssue', payload }], }); }); }); describe('moveIssue', () => { - const listIssues = { - 'gid://gitlab/List/1': [436, 437], - 'gid://gitlab/List/2': [], - }; - - const issues = { - 436: mockIssue, - 437: mockIssue2, - }; - - const state = { - fullPath: 'gitlab-org', - boardId: '1', - boardType: 'group', - disabled: false, - boardLists: mockLists, - boardItemsByListId: listIssues, - boardItems: issues, - }; + it('should dispatch a correct set of actions', () => { + testAction({ + action: actions.moveIssue, + payload: mockMoveIssueParams, + state: mockMoveState, + expectedActions: [ + { type: 'moveIssueCard', payload: mockMoveData }, + { type: 'updateMovedIssue', payload: mockMoveData }, + { type: 'updateIssueOrder', payload: { moveData: mockMoveData } }, + ], + }); + }); +}); - it('should commit MOVE_ISSUE mutation and MOVE_ISSUE_SUCCESS mutation when successful', (done) => { - jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ - data: { - issueMoveList: { - issue: rawIssue, - errors: [], +describe('moveIssueCard and undoMoveIssueCard', () => { + describe('card should move without clonning', () => { + let state; + let params; + let moveMutations; + let undoMutations; + + describe('when re-ordering card', () => { + beforeEach( + ({ + itemId = 123, + fromListId = 'gid://gitlab/List/1', + toListId = 'gid://gitlab/List/1', + originalIssue = { foo: 'bar' }, + originalIndex = 0, + moveBeforeId = undefined, + moveAfterId = undefined, + } = {}) => { + state = { + boardLists: { + [toListId]: { listType: ListType.backlog }, + [fromListId]: { listType: ListType.backlog }, + }, + boardItems: { [itemId]: originalIssue }, + boardItemsByListId: { [fromListId]: [123] }, + }; + params = { itemId, fromListId, toListId, moveBeforeId, moveAfterId }; + moveMutations = [ + { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } }, + { + type: types.ADD_BOARD_ITEM_TO_LIST, + payload: { itemId, listId: toListId, moveBeforeId, moveAfterId }, + }, + ]; + undoMutations = [ + { type: types.UPDATE_BOARD_ITEM, payload: originalIssue }, + { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } }, + { + type: types.ADD_BOARD_ITEM_TO_LIST, + payload: { itemId, listId: fromListId, atIndex: originalIndex }, + }, + ]; }, - }, + ); + + it('moveIssueCard commits a correct set of actions', () => { + testAction({ + action: actions.moveIssueCard, + state, + payload: getMoveData(state, params), + expectedMutations: moveMutations, + }); + }); + + it('undoMoveIssueCard commits a correct set of actions', () => { + testAction({ + action: actions.undoMoveIssueCard, + state, + payload: getMoveData(state, params), + expectedMutations: undoMutations, + }); + }); }); - testAction( - actions.moveIssue, - { - itemId: '436', - itemIid: mockIssue.iid, - itemPath: mockIssue.referencePath, - fromListId: 'gid://gitlab/List/1', - toListId: 'gid://gitlab/List/2', - }, - state, + describe.each([ [ + 'issue moves out of backlog', { - type: types.MOVE_ISSUE, - payload: { - originalIssue: mockIssue, - fromListId: 'gid://gitlab/List/1', - toListId: 'gid://gitlab/List/2', - }, + fromListType: ListType.backlog, + toListType: ListType.label, }, + ], + [ + 'issue card moves to closed', { - type: types.MOVE_ISSUE_SUCCESS, - payload: { issue: rawIssue }, + fromListType: ListType.label, + toListType: ListType.closed, }, ], - [], - done, - ); + [ + 'issue card moves to non-closed, non-backlog list of the same type', + { + fromListType: ListType.label, + toListType: ListType.label, + }, + ], + ])('when %s', (_, { toListType, fromListType }) => { + beforeEach( + ({ + itemId = 123, + fromListId = 'gid://gitlab/List/1', + toListId = 'gid://gitlab/List/2', + originalIssue = { foo: 'bar' }, + originalIndex = 0, + moveBeforeId = undefined, + moveAfterId = undefined, + } = {}) => { + state = { + boardLists: { + [fromListId]: { listType: fromListType }, + [toListId]: { listType: toListType }, + }, + boardItems: { [itemId]: originalIssue }, + boardItemsByListId: { [fromListId]: [123], [toListId]: [] }, + }; + params = { itemId, fromListId, toListId, moveBeforeId, moveAfterId }; + moveMutations = [ + { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } }, + { + type: types.ADD_BOARD_ITEM_TO_LIST, + payload: { itemId, listId: toListId, moveBeforeId, moveAfterId }, + }, + ]; + undoMutations = [ + { type: types.UPDATE_BOARD_ITEM, payload: originalIssue }, + { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: toListId } }, + { + type: types.ADD_BOARD_ITEM_TO_LIST, + payload: { itemId, listId: fromListId, atIndex: originalIndex }, + }, + ]; + }, + ); + + it('moveIssueCard commits a correct set of actions', () => { + testAction({ + action: actions.moveIssueCard, + state, + payload: getMoveData(state, params), + expectedMutations: moveMutations, + }); + }); + + it('undoMoveIssueCard commits a correct set of actions', () => { + testAction({ + action: actions.undoMoveIssueCard, + state, + payload: getMoveData(state, params), + expectedMutations: undoMutations, + }); + }); + }); + }); + + describe('card should clone on move', () => { + let state; + let params; + let moveMutations; + let undoMutations; + + describe.each([ + [ + 'issue card moves to non-closed, non-backlog list of a different type', + { + fromListType: ListType.label, + toListType: ListType.assignee, + }, + ], + ])('when %s', (_, { toListType, fromListType }) => { + beforeEach( + ({ + itemId = 123, + fromListId = 'gid://gitlab/List/1', + toListId = 'gid://gitlab/List/2', + originalIssue = { foo: 'bar' }, + originalIndex = 0, + moveBeforeId = undefined, + moveAfterId = undefined, + } = {}) => { + state = { + boardLists: { + [fromListId]: { listType: fromListType }, + [toListId]: { listType: toListType }, + }, + boardItems: { [itemId]: originalIssue }, + boardItemsByListId: { [fromListId]: [123], [toListId]: [] }, + }; + params = { itemId, fromListId, toListId, moveBeforeId, moveAfterId }; + moveMutations = [ + { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } }, + { + type: types.ADD_BOARD_ITEM_TO_LIST, + payload: { itemId, listId: toListId, moveBeforeId, moveAfterId }, + }, + { + type: types.ADD_BOARD_ITEM_TO_LIST, + payload: { itemId, listId: fromListId, atIndex: originalIndex }, + }, + ]; + undoMutations = [ + { type: types.UPDATE_BOARD_ITEM, payload: originalIssue }, + { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } }, + { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: toListId } }, + { + type: types.ADD_BOARD_ITEM_TO_LIST, + payload: { itemId, listId: fromListId, atIndex: originalIndex }, + }, + ]; + }, + ); + + it('moveIssueCard commits a correct set of actions', () => { + testAction({ + action: actions.moveIssueCard, + state, + payload: getMoveData(state, params), + expectedMutations: moveMutations, + }); + }); + + it('undoMoveIssueCard commits a correct set of actions', () => { + testAction({ + action: actions.undoMoveIssueCard, + state, + payload: getMoveData(state, params), + expectedMutations: undoMutations, + }); + }); + }); }); +}); + +describe('updateMovedIssueCard', () => { + const label1 = { + id: 'label1', + }; + + it.each([ + [ + 'issue without a label is moved to a label list', + { + state: { + boardLists: { + from: {}, + to: { + listType: ListType.label, + label: label1, + }, + }, + boardItems: { + 1: { + labels: [], + }, + }, + }, + moveData: { + itemId: 1, + fromListId: 'from', + toListId: 'to', + }, + updatedIssue: { labels: [label1] }, + }, + ], + ])( + 'should commit UPDATE_BOARD_ITEM with a correctly updated issue data when %s', + (_, { state, moveData, updatedIssue }) => { + testAction({ + action: actions.updateMovedIssue, + payload: moveData, + state, + expectedMutations: [{ type: types.UPDATE_BOARD_ITEM, payload: updatedIssue }], + }); + }, + ); +}); + +describe('updateIssueOrder', () => { + const issues = { + 436: mockIssue, + 437: mockIssue2, + }; + + const state = { + boardItems: issues, + boardId: 'gid://gitlab/Board/1', + }; + + const moveData = { + itemId: 436, + fromListId: 'gid://gitlab/List/1', + toListId: 'gid://gitlab/List/2', + }; it('calls mutate with the correct variables', () => { const mutationVariables = { @@ -728,61 +977,56 @@ describe('moveIssue', () => { }, }); - actions.moveIssue( - { state, commit: () => {} }, - { - itemId: mockIssue.id, - itemIid: mockIssue.iid, - itemPath: mockIssue.referencePath, - fromListId: 'gid://gitlab/List/1', - toListId: 'gid://gitlab/List/2', - }, - ); + actions.updateIssueOrder({ state, commit: () => {}, dispatch: () => {} }, { moveData }); expect(gqlClient.mutate).toHaveBeenCalledWith(mutationVariables); }); - it('should commit MOVE_ISSUE mutation and MOVE_ISSUE_FAILURE mutation when unsuccessful', (done) => { + it('should commit MUTATE_ISSUE_SUCCESS mutation when successful', () => { jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ data: { issueMoveList: { - issue: {}, - errors: [{ foo: 'bar' }], + issue: rawIssue, + errors: [], }, }, }); testAction( - actions.moveIssue, - { - itemId: '436', - itemIid: mockIssue.iid, - itemPath: mockIssue.referencePath, - fromListId: 'gid://gitlab/List/1', - toListId: 'gid://gitlab/List/2', - }, + actions.updateIssueOrder, + { moveData }, state, [ { - type: types.MOVE_ISSUE, - payload: { - originalIssue: mockIssue, - fromListId: 'gid://gitlab/List/1', - toListId: 'gid://gitlab/List/2', - }, + type: types.MUTATE_ISSUE_SUCCESS, + payload: { issue: rawIssue }, }, + ], + [], + ); + }); + + it('should commit SET_ERROR and dispatch undoMoveIssueCard', () => { + jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ + data: { + issueMoveList: { + issue: {}, + errors: [{ foo: 'bar' }], + }, + }, + }); + + testAction( + actions.updateIssueOrder, + { moveData }, + state, + [ { - type: types.MOVE_ISSUE_FAILURE, - payload: { - originalIssue: mockIssue, - fromListId: 'gid://gitlab/List/1', - toListId: 'gid://gitlab/List/2', - originalIndex: 0, - }, + type: types.SET_ERROR, + payload: 'An error occurred while moving the issue. Please try again.', }, ], - [], - done, + [{ type: 'undoMoveIssueCard', payload: moveData }], ); }); }); @@ -798,11 +1042,11 @@ describe('setAssignees', () => { testAction( actions.setAssignees, [node], - { activeIssue: { iid, referencePath: refPath }, commit: () => {} }, + { activeBoardItem: { iid, referencePath: refPath }, commit: () => {} }, [ { - type: 'UPDATE_ISSUE_BY_ID', - payload: { prop: 'assignees', issueId: undefined, value: [node] }, + type: 'UPDATE_BOARD_ITEM_BY_ID', + payload: { prop: 'assignees', itemId: undefined, value: [node] }, }, ], [], @@ -812,7 +1056,43 @@ describe('setAssignees', () => { }); }); -describe('createNewIssue', () => { +describe('addListItem', () => { + it('should commit ADD_BOARD_ITEM_TO_LIST and UPDATE_BOARD_ITEM mutations', () => { + const payload = { + list: mockLists[0], + item: mockIssue, + position: 0, + }; + + testAction(actions.addListItem, payload, {}, [ + { + type: types.ADD_BOARD_ITEM_TO_LIST, + payload: { + listId: mockLists[0].id, + itemId: mockIssue.id, + atIndex: 0, + }, + }, + { type: types.UPDATE_BOARD_ITEM, payload: mockIssue }, + ]); + }); +}); + +describe('removeListItem', () => { + it('should commit REMOVE_BOARD_ITEM_FROM_LIST and REMOVE_BOARD_ITEM mutations', () => { + const payload = { + listId: mockLists[0].id, + itemId: mockIssue.id, + }; + + testAction(actions.removeListItem, payload, {}, [ + { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload }, + { type: types.REMOVE_BOARD_ITEM, payload: mockIssue.id }, + ]); + }); +}); + +describe('addListNewIssue', () => { const state = { boardType: 'group', fullPath: 'gitlab-org/gitlab', @@ -839,19 +1119,7 @@ describe('createNewIssue', () => { }, }; - 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); - }); + const fakeList = { id: 'gid://gitlab/List/123' }; it('should add board scope to the issue being created', async () => { jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ @@ -863,7 +1131,11 @@ describe('createNewIssue', () => { }, }); - await actions.createNewIssue({ state: stateWithBoardConfig }, mockIssue); + await actions.addListNewIssue( + { dispatch: jest.fn(), commit: jest.fn(), state: stateWithBoardConfig }, + { issueInput: mockIssue, list: fakeList }, + ); + expect(gqlClient.mutate).toHaveBeenCalledWith({ mutation: issueCreateMutation, variables: { @@ -890,7 +1162,11 @@ describe('createNewIssue', () => { const payload = formatIssueInput(issue, stateWithBoardConfig.boardConfig); - await actions.createNewIssue({ state: stateWithBoardConfig }, issue); + await actions.addListNewIssue( + { dispatch: jest.fn(), commit: jest.fn(), state: stateWithBoardConfig }, + { issueInput: issue, list: fakeList }, + ); + expect(gqlClient.mutate).toHaveBeenCalledWith({ mutation: issueCreateMutation, variables: { @@ -901,51 +1177,92 @@ describe('createNewIssue', () => { expect(payload.assigneeIds).toEqual(['gid://gitlab/User/1', 'gid://gitlab/User/2']); }); - it('should commit CREATE_ISSUE_FAILURE mutation when API returns an error', (done) => { - jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ - data: { - createIssue: { - issue: mockIssue, - errors: [{ foo: 'bar' }], + describe('when issue creation mutation request succeeds', () => { + it('dispatches a correct set of mutations', () => { + jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ + data: { + createIssue: { + issue: mockIssue, + errors: [], + }, }, - }, + }); + + testAction({ + action: actions.addListNewIssue, + payload: { + issueInput: mockIssue, + list: fakeList, + placeholderId: 'tmp', + }, + state, + expectedActions: [ + { + type: 'addListItem', + payload: { + list: fakeList, + item: formatIssue({ ...mockIssue, id: 'tmp' }), + position: 0, + }, + }, + { type: 'removeListItem', payload: { listId: fakeList.id, itemId: 'tmp' } }, + { + type: 'addListItem', + payload: { + list: fakeList, + item: formatIssue({ ...mockIssue, id: getIdFromGraphQLId(mockIssue.id) }), + position: 0, + }, + }, + ], + }); }); - - const payload = mockIssue; - - testAction( - actions.createNewIssue, - payload, - state, - [{ type: types.CREATE_ISSUE_FAILURE }], - [], - done, - ); }); -}); - -describe('addListIssue', () => { - it('should commit ADD_ISSUE_TO_LIST mutation', (done) => { - const payload = { - list: mockLists[0], - issue: mockIssue, - position: 0, - }; - testAction( - actions.addListIssue, - payload, - {}, - [{ type: types.ADD_ISSUE_TO_LIST, payload }], - [], - done, - ); + describe('when issue creation mutation request fails', () => { + it('dispatches a correct set of mutations', () => { + jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ + data: { + createIssue: { + issue: mockIssue, + errors: [{ foo: 'bar' }], + }, + }, + }); + + testAction({ + action: actions.addListNewIssue, + payload: { + issueInput: mockIssue, + list: fakeList, + placeholderId: 'tmp', + }, + state, + expectedActions: [ + { + type: 'addListItem', + payload: { + list: fakeList, + item: formatIssue({ ...mockIssue, id: 'tmp' }), + position: 0, + }, + }, + { type: 'removeListItem', payload: { listId: fakeList.id, itemId: 'tmp' } }, + ], + expectedMutations: [ + { + type: types.SET_ERROR, + payload: 'An error occurred while creating the issue. Please try again.', + }, + ], + }); + }); }); }); describe('setActiveIssueLabels', () => { const state = { boardItems: { [mockIssue.id]: mockIssue } }; - const getters = { activeIssue: mockIssue }; + const getters = { activeBoardItem: mockIssue }; const testLabelIds = labels.map((label) => label.id); const input = { addLabelIds: testLabelIds, @@ -959,7 +1276,7 @@ describe('setActiveIssueLabels', () => { .mockResolvedValue({ data: { updateIssue: { issue: { labels: { nodes: labels } } } } }); const payload = { - issueId: getters.activeIssue.id, + itemId: getters.activeBoardItem.id, prop: 'labels', value: labels, }; @@ -970,7 +1287,7 @@ describe('setActiveIssueLabels', () => { { ...state, ...getters }, [ { - type: types.UPDATE_ISSUE_BY_ID, + type: types.UPDATE_BOARD_ITEM_BY_ID, payload, }, ], @@ -990,7 +1307,7 @@ describe('setActiveIssueLabels', () => { describe('setActiveIssueDueDate', () => { const state = { boardItems: { [mockIssue.id]: mockIssue } }; - const getters = { activeIssue: mockIssue }; + const getters = { activeBoardItem: mockIssue }; const testDueDate = '2020-02-20'; const input = { dueDate: testDueDate, @@ -1010,7 +1327,7 @@ describe('setActiveIssueDueDate', () => { }); const payload = { - issueId: getters.activeIssue.id, + itemId: getters.activeBoardItem.id, prop: 'dueDate', value: testDueDate, }; @@ -1021,7 +1338,7 @@ describe('setActiveIssueDueDate', () => { { ...state, ...getters }, [ { - type: types.UPDATE_ISSUE_BY_ID, + type: types.UPDATE_BOARD_ITEM_BY_ID, payload, }, ], @@ -1039,9 +1356,15 @@ describe('setActiveIssueDueDate', () => { }); }); -describe('setActiveIssueSubscribed', () => { - const state = { boardItems: { [mockActiveIssue.id]: mockActiveIssue } }; - const getters = { activeIssue: mockActiveIssue }; +describe('setActiveItemSubscribed', () => { + const state = { + boardItems: { + [mockActiveIssue.id]: mockActiveIssue, + }, + fullPath: 'gitlab-org', + issuableType: 'issue', + }; + const getters = { activeBoardItem: mockActiveIssue, isEpicBoard: false }; const subscribedState = true; const input = { subscribedState, @@ -1051,7 +1374,7 @@ describe('setActiveIssueSubscribed', () => { it('should commit subscribed status', (done) => { jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ data: { - issueSetSubscription: { + updateIssuableSubscription: { issue: { subscribed: subscribedState, }, @@ -1061,18 +1384,18 @@ describe('setActiveIssueSubscribed', () => { }); const payload = { - issueId: getters.activeIssue.id, + itemId: getters.activeBoardItem.id, prop: 'subscribed', value: subscribedState, }; testAction( - actions.setActiveIssueSubscribed, + actions.setActiveItemSubscribed, input, { ...state, ...getters }, [ { - type: types.UPDATE_ISSUE_BY_ID, + type: types.UPDATE_BOARD_ITEM_BY_ID, payload, }, ], @@ -1084,15 +1407,15 @@ describe('setActiveIssueSubscribed', () => { it('throws error if fails', async () => { jest .spyOn(gqlClient, 'mutate') - .mockResolvedValue({ data: { issueSetSubscription: { errors: ['failed mutation'] } } }); + .mockResolvedValue({ data: { updateIssuableSubscription: { errors: ['failed mutation'] } } }); - await expect(actions.setActiveIssueSubscribed({ getters }, input)).rejects.toThrow(Error); + await expect(actions.setActiveItemSubscribed({ getters }, input)).rejects.toThrow(Error); }); }); describe('setActiveIssueMilestone', () => { const state = { boardItems: { [mockIssue.id]: mockIssue } }; - const getters = { activeIssue: mockIssue }; + const getters = { activeBoardItem: mockIssue }; const testMilestone = { ...mockMilestone, id: 'gid://gitlab/Milestone/1', @@ -1115,7 +1438,7 @@ describe('setActiveIssueMilestone', () => { }); const payload = { - issueId: getters.activeIssue.id, + itemId: getters.activeBoardItem.id, prop: 'milestone', value: testMilestone, }; @@ -1126,7 +1449,7 @@ describe('setActiveIssueMilestone', () => { { ...state, ...getters }, [ { - type: types.UPDATE_ISSUE_BY_ID, + type: types.UPDATE_BOARD_ITEM_BY_ID, payload, }, ], @@ -1144,9 +1467,13 @@ describe('setActiveIssueMilestone', () => { }); }); -describe('setActiveIssueTitle', () => { - const state = { boardItems: { [mockIssue.id]: mockIssue } }; - const getters = { activeIssue: mockIssue }; +describe('setActiveItemTitle', () => { + const state = { + boardItems: { [mockIssue.id]: mockIssue }, + issuableType: 'issue', + fullPath: 'path/f', + }; + const getters = { activeBoardItem: mockIssue, isEpicBoard: false }; const testTitle = 'Test Title'; const input = { title: testTitle, @@ -1156,7 +1483,7 @@ describe('setActiveIssueTitle', () => { it('should commit title after setting the issue', (done) => { jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ data: { - updateIssue: { + updateIssuableTitle: { issue: { title: testTitle, }, @@ -1166,18 +1493,18 @@ describe('setActiveIssueTitle', () => { }); const payload = { - issueId: getters.activeIssue.id, + itemId: getters.activeBoardItem.id, prop: 'title', value: testTitle, }; testAction( - actions.setActiveIssueTitle, + actions.setActiveItemTitle, input, { ...state, ...getters }, [ { - type: types.UPDATE_ISSUE_BY_ID, + type: types.UPDATE_BOARD_ITEM_BY_ID, payload, }, ], @@ -1191,7 +1518,7 @@ describe('setActiveIssueTitle', () => { .spyOn(gqlClient, 'mutate') .mockResolvedValue({ data: { updateIssue: { errors: ['failed mutation'] } } }); - await expect(actions.setActiveIssueTitle({ getters }, input)).rejects.toThrow(Error); + await expect(actions.setActiveItemTitle({ getters }, input)).rejects.toThrow(Error); }); }); @@ -1321,7 +1648,7 @@ describe('toggleBoardItemMultiSelection', () => { testAction( actions.toggleBoardItemMultiSelection, boardItem2, - { activeId: mockActiveIssue.id, activeIssue: mockActiveIssue, selectedBoardItems: [] }, + { activeId: mockActiveIssue.id, activeBoardItem: mockActiveIssue, selectedBoardItems: [] }, [ { type: types.ADD_BOARD_ITEM_TO_SELECTION, @@ -1378,6 +1705,51 @@ describe('toggleBoardItem', () => { }); }); +describe('setError', () => { + it('should commit mutation SET_ERROR', () => { + testAction({ + action: actions.setError, + payload: { message: 'mayday' }, + expectedMutations: [ + { + payload: 'mayday', + type: types.SET_ERROR, + }, + ], + }); + }); + + it('should capture error using Sentry when captureError is true', () => { + jest.spyOn(Sentry, 'captureException'); + + const mockError = new Error(); + actions.setError( + { commit: () => {} }, + { + message: 'mayday', + error: mockError, + captureError: true, + }, + ); + + expect(Sentry.captureException).toHaveBeenNthCalledWith(1, mockError); + }); +}); + +describe('unsetError', () => { + it('should commit mutation SET_ERROR with undefined as payload', () => { + testAction({ + action: actions.unsetError, + expectedMutations: [ + { + payload: undefined, + type: types.SET_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 32d73d861bc..6114ba0af5f 100644 --- a/spec/frontend/boards/stores/getters_spec.js +++ b/spec/frontend/boards/stores/getters_spec.js @@ -88,7 +88,7 @@ describe('Boards - Getters', () => { }); }); - describe('activeIssue', () => { + describe('activeBoardItem', () => { it.each` id | expected ${'1'} | ${'issue'} @@ -96,7 +96,7 @@ describe('Boards - Getters', () => { `('returns $expected when $id is passed to state', ({ id, expected }) => { const state = { boardItems: { 1: 'issue' }, activeId: id }; - expect(getters.activeIssue(state)).toEqual(expected); + expect(getters.activeBoardItem(state)).toEqual(expected); }); }); @@ -105,14 +105,14 @@ describe('Boards - Getters', () => { const mockActiveIssue = { referencePath: 'gitlab-org/gitlab-test#1', }; - expect(getters.groupPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual( + expect(getters.groupPathForActiveIssue({}, { activeBoardItem: mockActiveIssue })).toEqual( 'gitlab-org', ); }); it('returns empty string as group path when active issue is an empty object', () => { const mockActiveIssue = {}; - expect(getters.groupPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual(''); + expect(getters.groupPathForActiveIssue({}, { activeBoardItem: mockActiveIssue })).toEqual(''); }); }); @@ -121,14 +121,16 @@ describe('Boards - Getters', () => { const mockActiveIssue = { referencePath: 'gitlab-org/gitlab-test#1', }; - expect(getters.projectPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual( + expect(getters.projectPathForActiveIssue({}, { activeBoardItem: mockActiveIssue })).toEqual( 'gitlab-org/gitlab-test', ); }); it('returns empty string as project path when active issue is an empty object', () => { const mockActiveIssue = {}; - expect(getters.projectPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual(''); + expect(getters.projectPathForActiveIssue({}, { activeBoardItem: mockActiveIssue })).toEqual( + '', + ); }); }); @@ -177,4 +179,31 @@ describe('Boards - Getters', () => { expect(getters.activeGroupProjects(state)).toEqual([mockGroupProject1]); }); }); + + describe('isIssueBoard', () => { + it.each` + issuableType | expected + ${'issue'} | ${true} + ${'epic'} | ${false} + `( + 'returns $expected when issuableType on state is $issuableType', + ({ issuableType, expected }) => { + const state = { + issuableType, + }; + + expect(getters.isIssueBoard(state)).toBe(expected); + }, + ); + }); + + describe('isEpicBoard', () => { + afterEach(() => { + window.gon = { features: {} }; + }); + + it('returns false', () => { + expect(getters.isEpicBoard()).toBe(false); + }); + }); }); diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js index 33897cc0250..af6d439e294 100644 --- a/spec/frontend/boards/stores/mutations_spec.js +++ b/spec/frontend/boards/stores/mutations_spec.js @@ -1,3 +1,4 @@ +import { cloneDeep } from 'lodash'; import { issuableTypes } from '~/boards/constants'; import * as types from '~/boards/stores/mutation_types'; import mutations from '~/boards/stores/mutations'; @@ -9,6 +10,7 @@ import { mockIssue2, mockGroupProjects, labels, + mockList, } from '../mock_data'; const expectNotImplemented = (action) => { @@ -25,6 +27,14 @@ describe('Board Store Mutations', () => { 'gid://gitlab/List/2': mockLists[1], }; + const setBoardsListsState = () => { + state = cloneDeep({ + ...state, + boardItemsByListId: { 'gid://gitlab/List/1': [mockIssue.id] }, + boardLists: { 'gid://gitlab/List/1': mockList }, + }); + }; + beforeEach(() => { state = defaultState(); }); @@ -335,7 +345,7 @@ describe('Board Store Mutations', () => { expectNotImplemented(mutations.REQUEST_ADD_ISSUE); }); - describe('UPDATE_ISSUE_BY_ID', () => { + describe('UPDATE_BOARD_ITEM_BY_ID', () => { const issueId = '1'; const prop = 'id'; const value = '2'; @@ -353,8 +363,8 @@ describe('Board Store Mutations', () => { describe('when the issue is in state', () => { it('updates the property of the correct issue', () => { - mutations.UPDATE_ISSUE_BY_ID(state, { - issueId, + mutations.UPDATE_BOARD_ITEM_BY_ID(state, { + itemId: issueId, prop, value, }); @@ -366,8 +376,8 @@ describe('Board Store Mutations', () => { describe('when the issue is not in state', () => { it('throws an error', () => { expect(() => { - mutations.UPDATE_ISSUE_BY_ID(state, { - issueId: '3', + mutations.UPDATE_BOARD_ITEM_BY_ID(state, { + itemId: '3', prop, value, }); @@ -384,41 +394,7 @@ describe('Board Store Mutations', () => { expectNotImplemented(mutations.RECEIVE_ADD_ISSUE_ERROR); }); - describe('MOVE_ISSUE', () => { - it('updates boardItemsByListId, moving issue between lists', () => { - const listIssues = { - 'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id], - 'gid://gitlab/List/2': [], - }; - - const issues = { - 1: mockIssue, - 2: mockIssue2, - }; - - state = { - ...state, - boardItemsByListId: listIssues, - boardLists: initialBoardListsState, - boardItems: issues, - }; - - mutations.MOVE_ISSUE(state, { - originalIssue: mockIssue2, - fromListId: 'gid://gitlab/List/1', - toListId: 'gid://gitlab/List/2', - }); - - const updatedListIssues = { - 'gid://gitlab/List/1': [mockIssue.id], - 'gid://gitlab/List/2': [mockIssue2.id], - }; - - expect(state.boardItemsByListId).toEqual(updatedListIssues); - }); - }); - - describe('MOVE_ISSUE_SUCCESS', () => { + describe('MUTATE_ISSUE_SUCCESS', () => { it('updates issue in issues state', () => { const issues = { 436: { id: rawIssue.id }, @@ -429,7 +405,7 @@ describe('Board Store Mutations', () => { boardItems: issues, }; - mutations.MOVE_ISSUE_SUCCESS(state, { + mutations.MUTATE_ISSUE_SUCCESS(state, { issue: rawIssue, }); @@ -437,33 +413,24 @@ describe('Board Store Mutations', () => { }); }); - describe('MOVE_ISSUE_FAILURE', () => { - it('updates boardItemsByListId, reverting moving issue between lists, and sets error message', () => { - const listIssues = { - 'gid://gitlab/List/1': [mockIssue.id], - 'gid://gitlab/List/2': [mockIssue2.id], - }; + describe('UPDATE_BOARD_ITEM', () => { + it('updates the given issue in state.boardItems', () => { + const updatedIssue = { id: 'some_gid', foo: 'bar' }; + state = { boardItems: { some_gid: { id: 'some_gid' } } }; - state = { - ...state, - boardItemsByListId: listIssues, - boardLists: initialBoardListsState, - }; + mutations.UPDATE_BOARD_ITEM(state, updatedIssue); - mutations.MOVE_ISSUE_FAILURE(state, { - originalIssue: mockIssue2, - fromListId: 'gid://gitlab/List/1', - toListId: 'gid://gitlab/List/2', - originalIndex: 1, - }); + expect(state.boardItems.some_gid).toEqual(updatedIssue); + }); + }); - const updatedListIssues = { - 'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id], - 'gid://gitlab/List/2': [], - }; + describe('REMOVE_BOARD_ITEM', () => { + it('removes the given issue from state.boardItems', () => { + state = { boardItems: { some_gid: {}, some_gid2: {} } }; + + mutations.REMOVE_BOARD_ITEM(state, 'some_gid'); - expect(state.boardItemsByListId).toEqual(updatedListIssues); - expect(state.error).toEqual('An error occurred while moving the issue. Please try again.'); + expect(state.boardItems).toEqual({ some_gid2: {} }); }); }); @@ -479,85 +446,89 @@ 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); + describe('ADD_BOARD_ITEM_TO_LIST', () => { + beforeEach(() => { + setBoardsListsState(); + }); + + it.each([ + [ + 'at position 0 by default', + { + payload: { + itemId: mockIssue2.id, + listId: mockList.id, + }, + listState: [mockIssue2.id, mockIssue.id], + }, + ], + [ + 'at a given position', + { + payload: { + itemId: mockIssue2.id, + listId: mockList.id, + atIndex: 1, + }, + listState: [mockIssue.id, mockIssue2.id], + }, + ], + [ + "below the issue with id of 'moveBeforeId'", + { + payload: { + itemId: mockIssue2.id, + listId: mockList.id, + moveBeforeId: mockIssue.id, + }, + listState: [mockIssue.id, mockIssue2.id], + }, + ], + [ + "above the issue with id of 'moveAfterId'", + { + payload: { + itemId: mockIssue2.id, + listId: mockList.id, + moveAfterId: mockIssue.id, + }, + listState: [mockIssue2.id, mockIssue.id], + }, + ], + ])(`inserts an item into a list %s`, (_, { payload, listState }) => { + mutations.ADD_BOARD_ITEM_TO_LIST(state, payload); - expect(state.error).toBe('An error occurred while creating the issue. Please try again.'); + expect(state.boardItemsByListId[payload.listId]).toEqual(listState); }); - }); - - describe('ADD_ISSUE_TO_LIST', () => { - it('adds issue to issues state and issue id in list in boardItemsByListId', () => { - const listIssues = { - 'gid://gitlab/List/1': [mockIssue.id], - }; - const issues = { - 1: mockIssue, - }; - - state = { - ...state, - boardItemsByListId: listIssues, - boardItems: issues, - boardLists: initialBoardListsState, - }; + it("updates the list's items count", () => { expect(state.boardLists['gid://gitlab/List/1'].issuesCount).toBe(1); - mutations.ADD_ISSUE_TO_LIST(state, { list: mockLists[0], issue: mockIssue2 }); + mutations.ADD_BOARD_ITEM_TO_LIST(state, { + itemId: mockIssue2.id, + listId: mockList.id, + }); - expect(state.boardItemsByListId['gid://gitlab/List/1']).toContain(mockIssue2.id); - expect(state.boardItems[mockIssue2.id]).toEqual(mockIssue2); expect(state.boardLists['gid://gitlab/List/1'].issuesCount).toBe(2); }); }); - describe('ADD_ISSUE_TO_LIST_FAILURE', () => { - it('removes issue id from list in boardItemsByListId and sets error message', () => { - const listIssues = { - 'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id], - }; - const issues = { - 1: mockIssue, - 2: mockIssue2, - }; - - state = { - ...state, - boardItemsByListId: listIssues, - boardItems: issues, - boardLists: initialBoardListsState, - }; - - mutations.ADD_ISSUE_TO_LIST_FAILURE(state, { list: mockLists[0], issueId: mockIssue2.id }); - - expect(state.boardItemsByListId['gid://gitlab/List/1']).not.toContain(mockIssue2.id); - expect(state.error).toBe('An error occurred while creating the issue. Please try again.'); + describe('REMOVE_BOARD_ITEM_FROM_LIST', () => { + beforeEach(() => { + setBoardsListsState(); }); - }); - describe('REMOVE_ISSUE_FROM_LIST', () => { - it('removes issue id from list in boardItemsByListId and deletes issue from state', () => { - const listIssues = { - 'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id], - }; - const issues = { - 1: mockIssue, - 2: mockIssue2, - }; - - state = { - ...state, - boardItemsByListId: listIssues, - boardItems: issues, - boardLists: initialBoardListsState, - }; + it("removes an item from a list and updates the list's items count", () => { + expect(state.boardLists['gid://gitlab/List/1'].issuesCount).toBe(1); + expect(state.boardItemsByListId['gid://gitlab/List/1']).toContain(mockIssue.id); - mutations.ADD_ISSUE_TO_LIST_FAILURE(state, { list: mockLists[0], issueId: mockIssue2.id }); + mutations.REMOVE_BOARD_ITEM_FROM_LIST(state, { + itemId: mockIssue.id, + listId: mockList.id, + }); - expect(state.boardItemsByListId['gid://gitlab/List/1']).not.toContain(mockIssue2.id); - expect(state.boardItems).not.toContain(mockIssue2); + expect(state.boardItemsByListId['gid://gitlab/List/1']).not.toContain(mockIssue.id); + expect(state.boardLists['gid://gitlab/List/1'].issuesCount).toBe(0); }); }); @@ -666,4 +637,14 @@ describe('Board Store Mutations', () => { expect(state.selectedBoardItems).toEqual([]); }); }); + + describe('SET_ERROR', () => { + it('Should set error state', () => { + state.error = undefined; + + mutations[types.SET_ERROR](state, 'mayday'); + + expect(state.error).toBe('mayday'); + }); + }); }); |