diff options
Diffstat (limited to 'spec/frontend/boards')
21 files changed, 1406 insertions, 205 deletions
diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js index b51a82f2a35..80d7a72151d 100644 --- a/spec/frontend/boards/board_list_helper.js +++ b/spec/frontend/boards/board_list_helper.js @@ -52,10 +52,12 @@ export default function createComponent({ list, issues: list.issues, loading: false, - issueLinkBase: '/issues', - rootPath: '/', ...componentProps, }, + provide: { + groupId: null, + rootPath: '/', + }, }).$mount(); Vue.nextTick(() => { diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js index 3a64b004847..88883ae61d4 100644 --- a/spec/frontend/boards/board_list_spec.js +++ b/spec/frontend/boards/board_list_spec.js @@ -45,10 +45,12 @@ const createComponent = ({ done, listIssueProps = {}, componentProps = {}, listP list, issues: list.issues, loading: false, - issueLinkBase: '/issues', - rootPath: '/', ...componentProps, }, + provide: { + groupId: null, + rootPath: '/', + }, }).$mount(); Vue.nextTick(() => { diff --git a/spec/frontend/boards/board_new_issue_spec.js b/spec/frontend/boards/board_new_issue_spec.js index 94afc8a2b45..3eebfeca965 100644 --- a/spec/frontend/boards/board_new_issue_spec.js +++ b/spec/frontend/boards/board_new_issue_spec.js @@ -1,6 +1,7 @@ /* global List */ import Vue from 'vue'; +import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import boardNewIssue from '~/boards/components/board_new_issue.vue'; @@ -10,6 +11,7 @@ import '~/boards/models/list'; import { listObj, boardsMockInterceptor } from './mock_data'; describe('Issue boards new issue form', () => { + let wrapper; let vm; let list; let mock; @@ -24,13 +26,11 @@ describe('Issue boards new issue form', () => { const dummySubmitEvent = { preventDefault() {}, }; - vm.$refs.submitButton = vm.$el.querySelector('.btn-success'); - return vm.submit(dummySubmitEvent); + wrapper.vm.$refs.submitButton = wrapper.find({ ref: 'submitButton' }); + return wrapper.vm.submit(dummySubmitEvent); }; beforeEach(() => { - setFixtures('<div class="test-container"></div>'); - const BoardNewIssueComp = Vue.extend(boardNewIssue); mock = new MockAdapter(axios); @@ -43,46 +43,52 @@ describe('Issue boards new issue form', () => { newIssueMock = Promise.resolve(promiseReturn); jest.spyOn(list, 'newIssue').mockImplementation(() => newIssueMock); - vm = new BoardNewIssueComp({ + wrapper = mount(BoardNewIssueComp, { propsData: { + disabled: false, list, }, - }).$mount(document.querySelector('.test-container')); + provide: { + groupId: null, + }, + }); + + vm = wrapper.vm; return Vue.nextTick(); }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); mock.restore(); }); it('calls submit if submit button is clicked', () => { - jest.spyOn(vm, 'submit').mockImplementation(e => e.preventDefault()); + jest.spyOn(wrapper.vm, 'submit').mockImplementation(); vm.title = 'Testing Title'; - return Vue.nextTick().then(() => { - vm.$el.querySelector('.btn-success').click(); - - expect(vm.submit.mock.calls.length).toBe(1); - }); + return Vue.nextTick() + .then(submitIssue) + .then(() => { + expect(wrapper.vm.submit).toHaveBeenCalled(); + }); }); it('disables submit button if title is empty', () => { - expect(vm.$el.querySelector('.btn-success').disabled).toBe(true); + expect(wrapper.find({ ref: 'submitButton' }).props().disabled).toBe(true); }); it('enables submit button if title is not empty', () => { - vm.title = 'Testing Title'; + wrapper.setData({ title: 'Testing Title' }); return Vue.nextTick().then(() => { - expect(vm.$el.querySelector('.form-control').value).toBe('Testing Title'); - expect(vm.$el.querySelector('.btn-success').disabled).not.toBe(true); + expect(wrapper.find({ ref: 'input' }).element.value).toBe('Testing Title'); + expect(wrapper.find({ ref: 'submitButton' }).props().disabled).toBe(false); }); }); it('clears title after clicking cancel', () => { - vm.$el.querySelector('.btn-default').click(); + wrapper.find({ ref: 'cancelButton' }).trigger('click'); return Vue.nextTick().then(() => { expect(vm.title).toBe(''); @@ -97,7 +103,7 @@ describe('Issue boards new issue form', () => { describe('submit success', () => { it('creates new issue', () => { - vm.title = 'submit title'; + wrapper.setData({ title: 'submit issue' }); return Vue.nextTick() .then(submitIssue) @@ -107,17 +113,18 @@ describe('Issue boards new issue form', () => { }); it('enables button after submit', () => { - vm.title = 'submit issue'; + jest.spyOn(wrapper.vm, 'submit').mockImplementation(); + wrapper.setData({ title: 'submit issue' }); return Vue.nextTick() .then(submitIssue) .then(() => { - expect(vm.$el.querySelector('.btn-success').disabled).toBe(false); + expect(wrapper.vm.$refs.submitButton.props().disabled).toBe(false); }); }); it('clears title after submit', () => { - vm.title = 'submit issue'; + wrapper.setData({ title: 'submit issue' }); return Vue.nextTick() .then(submitIssue) @@ -128,7 +135,7 @@ describe('Issue boards new issue form', () => { it('sets detail issue after submit', () => { expect(boardsStore.detail.issue.title).toBe(undefined); - vm.title = 'submit issue'; + wrapper.setData({ title: 'submit issue' }); return Vue.nextTick() .then(submitIssue) @@ -138,7 +145,7 @@ describe('Issue boards new issue form', () => { }); it('sets detail list after submit', () => { - vm.title = 'submit issue'; + wrapper.setData({ title: 'submit issue' }); return Vue.nextTick() .then(submitIssue) @@ -149,7 +156,7 @@ describe('Issue boards new issue form', () => { it('sets detail weight after submit', () => { boardsStore.weightFeatureAvailable = true; - vm.title = 'submit issue'; + wrapper.setData({ title: 'submit issue' }); return Vue.nextTick() .then(submitIssue) @@ -160,7 +167,7 @@ describe('Issue boards new issue form', () => { it('does not set detail weight after submit', () => { boardsStore.weightFeatureAvailable = false; - vm.title = 'submit issue'; + wrapper.setData({ title: 'submit issue' }); return Vue.nextTick() .then(submitIssue) diff --git a/spec/frontend/boards/boards_store_spec.js b/spec/frontend/boards/boards_store_spec.js index 29cc8f981bd..41971137b95 100644 --- a/spec/frontend/boards/boards_store_spec.js +++ b/spec/frontend/boards/boards_store_spec.js @@ -312,7 +312,7 @@ describe('boardsStore', () => { }); describe('newIssue', () => { - const id = 'not-creative'; + const id = 1; const issue = { some: 'issue data' }; const url = `${endpoints.listsEndpoint}/${id}/issues`; const expectedRequest = expect.objectContaining({ diff --git a/spec/frontend/boards/components/board_card_layout_spec.js b/spec/frontend/boards/components/board_card_layout_spec.js new file mode 100644 index 00000000000..80f649a1a96 --- /dev/null +++ b/spec/frontend/boards/components/board_card_layout_spec.js @@ -0,0 +1,95 @@ +/* global List */ +/* global ListLabel */ + +import { shallowMount } from '@vue/test-utils'; + +import MockAdapter from 'axios-mock-adapter'; +import waitForPromises from 'helpers/wait_for_promises'; +import axios from '~/lib/utils/axios_utils'; + +import '~/boards/models/label'; +import '~/boards/models/assignee'; +import '~/boards/models/list'; +import store from '~/boards/stores'; +import boardsStore from '~/boards/stores/boards_store'; +import BoardCardLayout from '~/boards/components/board_card_layout.vue'; +import issueCardInner from '~/boards/components/issue_card_inner.vue'; +import { listObj, boardsMockInterceptor, setMockEndpoints } from '../mock_data'; + +describe('Board card layout', () => { + let wrapper; + let mock; + let list; + + // this particular mount component needs to be used after the root beforeEach because it depends on list being initialized + const mountComponent = propsData => { + wrapper = shallowMount(BoardCardLayout, { + stubs: { + issueCardInner, + }, + store, + propsData: { + list, + issue: list.issues[0], + disabled: false, + index: 0, + ...propsData, + }, + provide: { + groupId: null, + rootPath: '/', + }, + }); + }; + + const setupData = () => { + list = new List(listObj); + boardsStore.create(); + boardsStore.detail.issue = {}; + const label1 = new ListLabel({ + id: 3, + title: 'testing 123', + color: '#000cff', + text_color: 'white', + description: 'test', + }); + return waitForPromises().then(() => { + list.issues[0].labels.push(label1); + }); + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onAny().reply(boardsMockInterceptor); + setMockEndpoints(); + return setupData(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + list = null; + mock.restore(); + }); + + describe('mouse events', () => { + it('sets showDetail to true on mousedown', async () => { + mountComponent(); + + wrapper.trigger('mousedown'); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.showDetail).toBe(true); + }); + + it('sets showDetail to false on mousemove', async () => { + mountComponent(); + wrapper.trigger('mousedown'); + await wrapper.vm.$nextTick(); + expect(wrapper.vm.showDetail).toBe(true); + wrapper.trigger('mousemove'); + await wrapper.vm.$nextTick(); + expect(wrapper.vm.showDetail).toBe(false); + }); + }); +}); diff --git a/spec/frontend/boards/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js index d01b895f996..a3ddcdf01b7 100644 --- a/spec/frontend/boards/board_card_spec.js +++ b/spec/frontend/boards/components/board_card_spec.js @@ -2,7 +2,7 @@ /* global ListAssignee */ /* global ListLabel */ -import { shallowMount } from '@vue/test-utils'; +import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import waitForPromises from 'helpers/wait_for_promises'; @@ -15,12 +15,12 @@ import '~/boards/models/assignee'; import '~/boards/models/list'; import store from '~/boards/stores'; import boardsStore from '~/boards/stores/boards_store'; -import boardCard from '~/boards/components/board_card.vue'; +import BoardCard from '~/boards/components/board_card.vue'; import issueCardInner from '~/boards/components/issue_card_inner.vue'; import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; -import { listObj, boardsMockInterceptor, setMockEndpoints } from './mock_data'; +import { listObj, boardsMockInterceptor, setMockEndpoints } from '../mock_data'; -describe('Board card', () => { +describe('BoardCard', () => { let wrapper; let mock; let list; @@ -30,7 +30,7 @@ describe('Board card', () => { // this particular mount component needs to be used after the root beforeEach because it depends on list being initialized const mountComponent = propsData => { - wrapper = shallowMount(boardCard, { + wrapper = mount(BoardCard, { stubs: { issueCardInner, }, @@ -38,16 +38,18 @@ describe('Board card', () => { propsData: { list, issue: list.issues[0], - issueLinkBase: '/', disabled: false, index: 0, - rootPath: '/', ...propsData, }, + provide: { + groupId: null, + rootPath: '/', + }, }); }; - const setupData = () => { + const setupData = async () => { list = new List(listObj); boardsStore.create(); boardsStore.detail.issue = {}; @@ -58,9 +60,9 @@ describe('Board card', () => { text_color: 'white', description: 'test', }); - return waitForPromises().then(() => { - list.issues[0].labels.push(label1); - }); + await waitForPromises(); + + list.issues[0].labels.push(label1); }; beforeEach(() => { @@ -79,7 +81,7 @@ describe('Board card', () => { it('when details issue is empty does not show the element', () => { mountComponent(); - expect(wrapper.classes()).not.toContain('is-active'); + expect(wrapper.find('[data-testid="board_card"').classes()).not.toContain('is-active'); }); it('when detailIssue is equal to card issue shows the element', () => { @@ -124,29 +126,6 @@ describe('Board card', () => { }); describe('mouse events', () => { - it('sets showDetail to true on mousedown', () => { - mountComponent(); - wrapper.trigger('mousedown'); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.showDetail).toBe(true); - }); - }); - - it('sets showDetail to false on mousemove', () => { - mountComponent(); - wrapper.trigger('mousedown'); - return wrapper.vm - .$nextTick() - .then(() => { - expect(wrapper.vm.showDetail).toBe(true); - wrapper.trigger('mousemove'); - return wrapper.vm.$nextTick(); - }) - .then(() => { - expect(wrapper.vm.showDetail).toBe(false); - }); - }); - it('does not set detail issue if showDetail is false', () => { mountComponent(); expect(boardsStore.detail.issue).toEqual({}); @@ -219,6 +198,9 @@ describe('Board card', () => { boardsStore.detail.issue = {}; mountComponent(); + // sets conditional so that event is emitted. + wrapper.trigger('mousedown'); + wrapper.trigger('mouseup'); expect(sidebarEventHub.$emit).toHaveBeenCalledWith('sidebar.closeAll'); diff --git a/spec/frontend/boards/components/board_column_spec.js b/spec/frontend/boards/components/board_column_spec.js index c06b7aceaad..2a4dbbb989e 100644 --- a/spec/frontend/boards/components/board_column_spec.js +++ b/spec/frontend/boards/components/board_column_spec.js @@ -59,10 +59,11 @@ describe('Board Column Component', () => { propsData: { boardId, disabled: false, - issueLinkBase: '/', - rootPath: '/', list, }, + provide: { + boardId, + }, }); }; diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js new file mode 100644 index 00000000000..df117d06cdf --- /dev/null +++ b/spec/frontend/boards/components/board_content_spec.js @@ -0,0 +1,64 @@ +import Vuex from 'vuex'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { GlAlert } from '@gitlab/ui'; +import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue'; +import BoardColumn from 'ee_else_ce/boards/components/board_column.vue'; +import getters from 'ee_else_ce/boards/stores/getters'; +import { mockListsWithModel } from '../mock_data'; +import BoardContent from '~/boards/components/board_content.vue'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('BoardContent', () => { + let wrapper; + + const defaultState = { + isShowingEpicsSwimlanes: false, + boardLists: mockListsWithModel, + error: undefined, + }; + + const createStore = (state = defaultState) => { + return new Vuex.Store({ + getters, + state, + actions: { + fetchIssuesForAllLists: () => {}, + }, + }); + }; + + const createComponent = state => { + const store = createStore({ + ...defaultState, + ...state, + }); + wrapper = shallowMount(BoardContent, { + localVue, + propsData: { + lists: mockListsWithModel, + canAdminList: true, + disabled: false, + }, + store, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders a BoardColumn component per list', () => { + expect(wrapper.findAll(BoardColumn)).toHaveLength(mockListsWithModel.length); + }); + + it('does not display EpicsSwimlanes component', () => { + expect(wrapper.find(EpicsSwimlanes).exists()).toBe(false); + expect(wrapper.find(GlAlert).exists()).toBe(false); + }); +}); diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js index b1d277863e8..65d8070192c 100644 --- a/spec/frontend/boards/components/board_form_spec.js +++ b/spec/frontend/boards/components/board_form_spec.js @@ -11,7 +11,7 @@ describe('board_form.vue', () => { const propsData = { canAdminBoard: false, labelsPath: `${TEST_HOST}/labels/path`, - milestonePath: `${TEST_HOST}/milestone/path`, + labelsWebUrl: `${TEST_HOST}/-/labels`, }; const findModal = () => wrapper.find(DeprecatedModal); diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js index 76a3d5e71c8..2439c347bf0 100644 --- a/spec/frontend/boards/components/board_list_header_spec.js +++ b/spec/frontend/boards/components/board_list_header_spec.js @@ -57,12 +57,12 @@ describe('Board List Header Component', () => { wrapper = shallowMount(BoardListHeader, { propsData: { - boardId, disabled: false, - issueLinkBase: '/', - rootPath: '/', list, }, + provide: { + boardId, + }, }); }; @@ -106,7 +106,7 @@ describe('Board List Header Component', () => { createComponent(); expect(isCollapsed()).toBe(false); - wrapper.find('[data-testid="board-list-header"]').vm.$emit('click'); + wrapper.find('[data-testid="board-list-header"]').trigger('click'); return wrapper.vm.$nextTick().then(() => { expect(isCollapsed()).toBe(false); diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js index f39adc0fc49..12c9431f2d4 100644 --- a/spec/frontend/boards/components/board_settings_sidebar_spec.js +++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js @@ -6,8 +6,9 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import { GlDrawer, GlLabel } from '@gitlab/ui'; import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue'; import boardsStore from '~/boards/stores/boards_store'; +import { createStore } from '~/boards/stores'; import sidebarEventHub from '~/sidebar/event_hub'; -import { inactiveId } from '~/boards/constants'; +import { inactiveId, LIST } from '~/boards/constants'; const localVue = createLocalVue(); @@ -16,19 +17,12 @@ localVue.use(Vuex); describe('BoardSettingsSidebar', () => { let wrapper; let mock; - let storeActions; + let store; const labelTitle = 'test'; const labelColor = '#FFFF'; const listId = 1; - const createComponent = (state = { activeId: inactiveId }, actions = {}) => { - storeActions = actions; - - const store = new Vuex.Store({ - state, - actions: storeActions, - }); - + const createComponent = () => { wrapper = shallowMount(BoardSettingsSidebar, { store, localVue, @@ -38,6 +32,9 @@ describe('BoardSettingsSidebar', () => { const findDrawer = () => wrapper.find(GlDrawer); beforeEach(() => { + store = createStore(); + store.state.activeId = inactiveId; + store.state.sidebarType = LIST; boardsStore.create(); }); @@ -46,114 +43,125 @@ describe('BoardSettingsSidebar', () => { wrapper.destroy(); }); - it('finds a GlDrawer component', () => { - createComponent(); + describe('when sidebarType is "list"', () => { + it('finds a GlDrawer component', () => { + createComponent(); - expect(findDrawer().exists()).toBe(true); - }); + expect(findDrawer().exists()).toBe(true); + }); - describe('on close', () => { - it('calls closeSidebar', async () => { - const spy = jest.fn(); - createComponent({ activeId: inactiveId }, { setActiveId: spy }); + describe('on close', () => { + it('closes the sidebar', async () => { + createComponent(); - findDrawer().vm.$emit('close'); + findDrawer().vm.$emit('close'); - await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); - expect(storeActions.setActiveId).toHaveBeenCalledWith( - expect.anything(), - inactiveId, - undefined, - ); - }); + expect(wrapper.find(GlDrawer).exists()).toBe(false); + }); - it('calls closeSidebar on sidebar.closeAll event', async () => { - createComponent({ activeId: inactiveId }, { setActiveId: jest.fn() }); + it('closes the sidebar when emitting the correct event', async () => { + createComponent(); - sidebarEventHub.$emit('sidebar.closeAll'); + sidebarEventHub.$emit('sidebar.closeAll'); - await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); - expect(storeActions.setActiveId).toHaveBeenCalledWith( - expect.anything(), - inactiveId, - undefined, - ); + expect(wrapper.find(GlDrawer).exists()).toBe(false); + }); }); - }); - describe('when activeId is zero', () => { - it('renders GlDrawer with open false', () => { - createComponent(); + describe('when activeId is zero', () => { + it('renders GlDrawer with open false', () => { + createComponent(); - expect(findDrawer().props('open')).toBe(false); + expect(findDrawer().props('open')).toBe(false); + }); }); - }); - describe('when activeId is greater than zero', () => { - beforeEach(() => { - mock = new MockAdapter(axios); + describe('when activeId is greater than zero', () => { + beforeEach(() => { + mock = new MockAdapter(axios); + + boardsStore.addList({ + id: listId, + label: { title: labelTitle, color: labelColor }, + list_type: 'label', + }); + store.state.activeId = 1; + store.state.sidebarType = LIST; + }); - boardsStore.addList({ - id: listId, - label: { title: labelTitle, color: labelColor }, - list_type: 'label', + afterEach(() => { + boardsStore.removeList(listId); }); - }); - afterEach(() => { - boardsStore.removeList(listId); + it('renders GlDrawer with open false', () => { + createComponent(); + + expect(findDrawer().props('open')).toBe(true); + }); }); - it('renders GlDrawer with open false', () => { - createComponent({ activeId: 1 }); + describe('when activeId is in boardsStore', () => { + beforeEach(() => { + mock = new MockAdapter(axios); - expect(findDrawer().props('open')).toBe(true); - }); - }); + boardsStore.addList({ + id: listId, + label: { title: labelTitle, color: labelColor }, + list_type: 'label', + }); - describe('when activeId is in boardsStore', () => { - beforeEach(() => { - mock = new MockAdapter(axios); + store.state.activeId = listId; + store.state.sidebarType = LIST; - boardsStore.addList({ - id: listId, - label: { title: labelTitle, color: labelColor }, - list_type: 'label', + createComponent(); }); - createComponent({ activeId: listId }); - }); + afterEach(() => { + mock.restore(); + }); - afterEach(() => { - mock.restore(); - }); + it('renders label title', () => { + expect(findLabel().props('title')).toBe(labelTitle); + }); - it('renders label title', () => { - expect(findLabel().props('title')).toBe(labelTitle); + it('renders label background color', () => { + expect(findLabel().props('backgroundColor')).toBe(labelColor); + }); }); - it('renders label background color', () => { - expect(findLabel().props('backgroundColor')).toBe(labelColor); - }); - }); + describe('when activeId is not in boardsStore', () => { + beforeEach(() => { + mock = new MockAdapter(axios); - describe('when activeId is not in boardsStore', () => { - beforeEach(() => { - mock = new MockAdapter(axios); + boardsStore.addList({ id: listId, label: { title: labelTitle, color: labelColor } }); + + store.state.activeId = inactiveId; - boardsStore.addList({ id: listId, label: { title: labelTitle, color: labelColor } }); + createComponent(); + }); + + afterEach(() => { + mock.restore(); + }); - createComponent({ activeId: inactiveId }); + it('does not render GlLabel', () => { + expect(findLabel().exists()).toBe(false); + }); }); + }); - afterEach(() => { - mock.restore(); + describe('when sidebarType is not List', () => { + beforeEach(() => { + store.state.sidebarType = ''; + createComponent(); }); - it('does not render GlLabel', () => { - expect(findLabel().exists()).toBe(false); + it('does not render GlDrawer', () => { + expect(findDrawer().exists()).toBe(false); }); }); }); diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js index f2d4de238d1..2b7605a3f7c 100644 --- a/spec/frontend/boards/components/boards_selector_spec.js +++ b/spec/frontend/boards/components/boards_selector_spec.js @@ -81,12 +81,12 @@ describe('BoardsSelector', () => { assignee_id: null, labels: [], }, - milestonePath: `${TEST_HOST}/milestone/path`, boardBaseUrl: `${TEST_HOST}/board/base/url`, hasMissingBoards: false, canAdminBoard: true, multipleIssueBoardsAvailable: true, labelsPath: `${TEST_HOST}/labels/path`, + labelsWebUrl: `${TEST_HOST}/labels`, projectId: 42, groupId: 19, scopedIssueBoardFeatureEnabled: true, diff --git a/spec/frontend/boards/components/issuable_title_spec.js b/spec/frontend/boards/components/issuable_title_spec.js new file mode 100644 index 00000000000..4b7f491b998 --- /dev/null +++ b/spec/frontend/boards/components/issuable_title_spec.js @@ -0,0 +1,33 @@ +import { shallowMount } from '@vue/test-utils'; +import IssuableTitle from '~/boards/components/issuable_title.vue'; + +describe('IssuableTitle', () => { + let wrapper; + const defaultProps = { + title: 'One', + refPath: 'path', + }; + const createComponent = () => { + wrapper = shallowMount(IssuableTitle, { + propsData: { ...defaultProps }, + }); + }; + const findIssueContent = () => wrapper.find('[data-testid="issue-title"]'); + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('renders a title of an issue in the sidebar', () => { + expect(findIssueContent().text()).toContain('One'); + }); + + it('renders a referencePath of an issue in the sidebar', () => { + expect(findIssueContent().text()).toContain('path'); + }); +}); diff --git a/spec/frontend/boards/components/issue_count_spec.js b/spec/frontend/boards/components/issue_count_spec.js index 819d878f4e2..d1ff0bdbf88 100644 --- a/spec/frontend/boards/components/issue_count_spec.js +++ b/spec/frontend/boards/components/issue_count_spec.js @@ -29,7 +29,7 @@ describe('IssueCount', () => { }); it('does not contains maxIssueCount in the template', () => { - expect(vm.contains('.js-max-issue-size')).toBe(false); + expect(vm.find('.js-max-issue-size').exists()).toBe(false); }); }); diff --git a/spec/frontend/boards/components/sidebar/board_editable_item_spec.js b/spec/frontend/boards/components/sidebar/board_editable_item_spec.js new file mode 100644 index 00000000000..1dbcbd06407 --- /dev/null +++ b/spec/frontend/boards/components/sidebar/board_editable_item_spec.js @@ -0,0 +1,107 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlLoadingIcon } from '@gitlab/ui'; +import BoardSidebarItem from '~/boards/components/sidebar/board_editable_item.vue'; + +describe('boards sidebar remove issue', () => { + let wrapper; + + const findLoader = () => wrapper.find(GlLoadingIcon); + const findEditButton = () => wrapper.find('[data-testid="edit-button"]'); + const findTitle = () => wrapper.find('[data-testid="title"]'); + const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]'); + const findExpanded = () => wrapper.find('[data-testid="expanded-content"]'); + + const createComponent = ({ props = {}, slots = {}, canUpdate = false } = {}) => { + wrapper = shallowMount(BoardSidebarItem, { + attachTo: document.body, + provide: { canUpdate }, + propsData: props, + slots, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('template', () => { + it('renders title', () => { + const title = 'Sidebar item title'; + createComponent({ props: { title } }); + + expect(findTitle().text()).toBe(title); + }); + + it('hides edit button, loader and expanded content by default', () => { + createComponent(); + + expect(findEditButton().exists()).toBe(false); + expect(findLoader().exists()).toBe(false); + expect(findExpanded().isVisible()).toBe(false); + }); + + it('shows "None" if empty collapsed slot', () => { + createComponent({}); + + expect(findCollapsed().text()).toBe('None'); + }); + + it('renders collapsed content by default', () => { + const slots = { collapsed: '<div>Collapsed content</div>' }; + createComponent({ slots }); + + expect(findCollapsed().text()).toBe('Collapsed content'); + }); + + it('shows edit button if can update', () => { + createComponent({ canUpdate: true }); + + expect(findEditButton().exists()).toBe(true); + }); + + it('shows loading icon if loading', () => { + createComponent({ props: { loading: true } }); + + expect(findLoader().exists()).toBe(true); + }); + + it('shows expanded content and hides collapsed content when clicking edit button', async () => { + const slots = { default: '<div>Select item</div>' }; + createComponent({ canUpdate: true, slots }); + findEditButton().vm.$emit('click'); + + return wrapper.vm.$nextTick().then(() => { + expect(findCollapsed().isVisible()).toBe(false); + expect(findExpanded().isVisible()).toBe(true); + expect(findExpanded().text()).toBe('Select item'); + }); + }); + }); + + describe('collapsing an item by offclicking', () => { + beforeEach(async () => { + createComponent({ canUpdate: true }); + findEditButton().vm.$emit('click'); + await wrapper.vm.$nextTick(); + }); + + it('hides expanded section and displays collapsed section', async () => { + expect(findExpanded().isVisible()).toBe(true); + document.body.click(); + + await wrapper.vm.$nextTick(); + + expect(findCollapsed().isVisible()).toBe(true); + expect(findExpanded().isVisible()).toBe(false); + }); + + it('emits changed event', async () => { + document.body.click(); + + await wrapper.vm.$nextTick(); + + expect(wrapper.emitted().changed[1][0]).toBe(false); + }); + }); +}); diff --git a/spec/frontend/boards/issue_card_spec.js b/spec/frontend/boards/issue_card_spec.js index dee8cb7b6e5..7e22e9647f0 100644 --- a/spec/frontend/boards/issue_card_spec.js +++ b/spec/frontend/boards/issue_card_spec.js @@ -47,13 +47,15 @@ describe('Issue card component', () => { propsData: { list, issue, - issueLinkBase: '/test', - rootPath: '/', }, store, stubs: { GlLabel: true, }, + provide: { + groupId: null, + rootPath: '/', + }, }); }); diff --git a/spec/frontend/boards/list_spec.js b/spec/frontend/boards/list_spec.js index b731bb6e474..9c3a6e66ef4 100644 --- a/spec/frontend/boards/list_spec.js +++ b/spec/frontend/boards/list_spec.js @@ -184,6 +184,7 @@ describe('List model', () => { }), ); list.issues = []; + global.gon.features = { boardsWithSwimlanes: false }; }); it('adds new issue to top of list', done => { diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js index 8ef6efe23c7..5776332c499 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -1,3 +1,9 @@ +/* global ListIssue */ +/* global List */ + +import Vue from 'vue'; +import '~/boards/models/list'; +import '~/boards/models/issue'; import boardsStore from '~/boards/stores/boards_store'; export const boardObj = { @@ -92,11 +98,64 @@ export const mockMilestone = { due_date: '2019-12-31', }; +const assignees = [ + { + id: 'gid://gitlab/User/2', + username: 'angelina.herman', + name: 'Bernardina Bosco', + avatar: 'https://www.gravatar.com/avatar/eb7b664b13a30ad9f9ba4b61d7075470?s=80&d=identicon', + webUrl: 'http://127.0.0.1:3000/angelina.herman', + }, +]; + +const labels = [ + { + id: 'gid://gitlab/GroupLabel/5', + title: 'Cosync', + color: '#34ebec', + description: null, + }, +]; + +export const rawIssue = { + title: 'Issue 1', + id: 'gid://gitlab/Issue/436', + iid: 27, + dueDate: null, + timeEstimate: 0, + weight: null, + confidential: false, + referencePath: 'gitlab-org/test-subgroup/gitlab-test#27', + path: '/gitlab-org/test-subgroup/gitlab-test/-/issues/27', + labels: { + nodes: [ + { + id: 1, + title: 'test', + color: 'red', + description: 'testing', + }, + ], + }, + assignees: { + nodes: assignees, + }, + epic: { + id: 'gid://gitlab/Epic/41', + }, +}; + export const mockIssue = { - title: 'Testing', - id: 1, - iid: 1, + id: 'gid://gitlab/Issue/436', + iid: 27, + title: 'Issue 1', + dueDate: null, + timeEstimate: 0, + weight: null, confidential: false, + referencePath: 'gitlab-org/test-subgroup/gitlab-test#27', + path: '/gitlab-org/test-subgroup/gitlab-test/-/issues/27', + assignees, labels: [ { id: 1, @@ -105,16 +164,64 @@ export const mockIssue = { description: 'testing', }, ], - assignees: [ - { - id: 1, - name: 'name', - username: 'username', - avatar_url: 'http://avatar_url', - }, - ], + epic: { + id: 'gid://gitlab/Epic/41', + }, }; +export const mockIssueWithModel = new ListIssue(mockIssue); + +export const mockIssue2 = { + id: 'gid://gitlab/Issue/437', + iid: 28, + title: 'Issue 2', + dueDate: null, + timeEstimate: 0, + weight: null, + confidential: false, + referencePath: 'gitlab-org/test-subgroup/gitlab-test#28', + path: '/gitlab-org/test-subgroup/gitlab-test/-/issues/28', + assignees, + labels, + epic: { + id: 'gid://gitlab/Epic/40', + }, +}; + +export const mockIssue2WithModel = new ListIssue(mockIssue2); + +export const mockIssue3 = { + id: 'gid://gitlab/Issue/438', + iid: 29, + title: 'Issue 3', + referencePath: '#29', + dueDate: null, + timeEstimate: 0, + weight: null, + confidential: false, + path: '/gitlab-org/gitlab-test/-/issues/28', + assignees, + labels, + epic: null, +}; + +export const mockIssue4 = { + id: 'gid://gitlab/Issue/439', + iid: 30, + title: 'Issue 4', + referencePath: '#30', + dueDate: null, + timeEstimate: 0, + weight: null, + confidential: false, + path: '/gitlab-org/gitlab-test/-/issues/28', + assignees, + labels, + epic: null, +}; + +export const mockIssues = [mockIssue, mockIssue2]; + export const BoardsMockData = { GET: { '/test/-/boards/1/lists/300/issues?id=300&page=1': { @@ -165,3 +272,50 @@ export const setMockEndpoints = (opts = {}) => { boardId, }); }; + +export const mockLists = [ + { + id: 'gid://gitlab/List/1', + title: 'Backlog', + position: null, + listType: 'backlog', + collapsed: false, + label: null, + assignee: null, + milestone: null, + loading: false, + }, + { + id: 'gid://gitlab/List/2', + title: 'To Do', + position: 0, + listType: 'label', + collapsed: false, + label: { + id: 'gid://gitlab/GroupLabel/121', + title: 'To Do', + color: '#F0AD4E', + textColor: '#FFFFFF', + description: null, + }, + assignee: null, + milestone: null, + loading: false, + }, +]; + +export const mockListsWithModel = mockLists.map(listMock => + Vue.observable(new List({ ...listMock, doNotFetchIssues: true })), +); + +export const mockIssuesByListId = { + 'gid://gitlab/List/1': [mockIssue.id, mockIssue3.id, mockIssue4.id], + 'gid://gitlab/List/2': mockIssues.map(({ id }) => id), +}; + +export const issues = { + [mockIssue.id]: mockIssue, + [mockIssue2.id]: mockIssue2, + [mockIssue3.id]: mockIssue3, + [mockIssue4.id]: mockIssue4, +}; diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index d539cba76ca..bdbcd435708 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -1,7 +1,17 @@ import testAction from 'helpers/vuex_action_helper'; -import actions from '~/boards/stores/actions'; +import { + mockListsWithModel, + mockLists, + mockIssue, + mockIssueWithModel, + mockIssue2WithModel, + rawIssue, +} from '../mock_data'; +import actions, { gqlClient } from '~/boards/stores/actions'; import * as types from '~/boards/stores/mutation_types'; -import { inactiveId } from '~/boards/constants'; +import { inactiveId, ListType } from '~/boards/constants'; +import issueMoveListMutation from '~/boards/queries/issue_move_list.mutation.graphql'; +import { fullBoardId } from '~/boards/boards_util'; const expectNotImplemented = action => { it('is not implemented', () => { @@ -9,6 +19,10 @@ const expectNotImplemented = action => { }); }; +// We need this helper to make sure projectPath is including +// subgroups when the movIssue action is called. +const getProjectPath = path => path.split('#')[0]; + describe('setInitialBoardData', () => { it('sets data object', () => { const mockData = { @@ -26,6 +40,25 @@ describe('setInitialBoardData', () => { }); }); +describe('setFilters', () => { + it('should commit mutation SET_FILTERS', done => { + const state = { + filters: {}, + }; + + const filters = { labelName: 'label' }; + + testAction( + actions.setFilters, + filters, + state, + [{ type: types.SET_FILTERS, payload: filters }], + [], + done, + ); + }); +}); + describe('setActiveId', () => { it('should commit mutation SET_ACTIVE_ID', done => { const state = { @@ -34,17 +67,40 @@ describe('setActiveId', () => { testAction( actions.setActiveId, - 1, + { id: 1, sidebarType: 'something' }, state, - [{ type: types.SET_ACTIVE_ID, payload: 1 }], + [{ type: types.SET_ACTIVE_ID, payload: { id: 1, sidebarType: 'something' } }], [], done, ); }); }); -describe('fetchLists', () => { - expectNotImplemented(actions.fetchLists); +describe('showWelcomeList', () => { + it('should dispatch addList action', done => { + const state = { + endpoints: { fullPath: 'gitlab-org', boardId: '1' }, + boardType: 'group', + disabled: false, + boardLists: [{ type: 'backlog' }, { type: 'closed' }], + }; + + const blankList = { + id: 'blank', + listType: ListType.blank, + title: 'Welcome to your issue board!', + position: 0, + }; + + testAction( + actions.showWelcomeList, + {}, + state, + [], + [{ type: 'addList', payload: blankList }], + done, + ); + }); }); describe('generateDefaultLists', () => { @@ -52,29 +108,316 @@ describe('generateDefaultLists', () => { }); describe('createList', () => { - expectNotImplemented(actions.createList); + it('should dispatch addList action when creating backlog list', done => { + const backlogList = { + id: 'gid://gitlab/List/1', + listType: 'backlog', + title: 'Open', + position: 0, + }; + + jest.spyOn(gqlClient, 'mutate').mockReturnValue( + Promise.resolve({ + data: { + boardListCreate: { + list: backlogList, + errors: [], + }, + }, + }), + ); + + const state = { + endpoints: { fullPath: 'gitlab-org', boardId: '1' }, + boardType: 'group', + disabled: false, + boardLists: [{ type: 'closed' }], + }; + + testAction( + actions.createList, + { backlog: true }, + state, + [], + [{ type: 'addList', payload: backlogList }], + done, + ); + }); + + it('should commit CREATE_LIST_FAILURE mutation when API returns an error', done => { + jest.spyOn(gqlClient, 'mutate').mockReturnValue( + Promise.resolve({ + data: { + boardListCreate: { + list: {}, + errors: [{ foo: 'bar' }], + }, + }, + }), + ); + + const state = { + endpoints: { fullPath: 'gitlab-org', boardId: '1' }, + boardType: 'group', + disabled: false, + boardLists: [{ type: 'closed' }], + }; + + testAction( + actions.createList, + { backlog: true }, + state, + [{ type: types.CREATE_LIST_FAILURE }], + [], + done, + ); + }); +}); + +describe('moveList', () => { + it('should commit MOVE_LIST mutation and dispatch updateList action', done => { + const state = { + endpoints: { fullPath: 'gitlab-org', boardId: '1' }, + boardType: 'group', + disabled: false, + boardLists: mockListsWithModel, + }; + + testAction( + actions.moveList, + { listId: 'gid://gitlab/List/1', newIndex: 1, adjustmentValue: 1 }, + state, + [ + { + type: types.MOVE_LIST, + payload: { movedList: mockListsWithModel[0], listAtNewIndex: mockListsWithModel[1] }, + }, + ], + [ + { + type: 'updateList', + payload: { listId: 'gid://gitlab/List/1', position: 0, backupList: mockListsWithModel }, + }, + ], + done, + ); + }); }); describe('updateList', () => { - expectNotImplemented(actions.updateList); + it('should commit UPDATE_LIST_FAILURE mutation when API returns an error', done => { + jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ + data: { + updateBoardList: { + list: {}, + errors: [{ foo: 'bar' }], + }, + }, + }); + + const state = { + endpoints: { fullPath: 'gitlab-org', boardId: '1' }, + boardType: 'group', + disabled: false, + boardLists: [{ type: 'closed' }], + }; + + testAction( + actions.updateList, + { listId: 'gid://gitlab/List/1', position: 1 }, + state, + [{ type: types.UPDATE_LIST_FAILURE }], + [], + done, + ); + }); }); describe('deleteList', () => { expectNotImplemented(actions.deleteList); }); -describe('fetchIssuesForList', () => { - expectNotImplemented(actions.fetchIssuesForList); -}); - describe('moveIssue', () => { - expectNotImplemented(actions.moveIssue); + const listIssues = { + 'gid://gitlab/List/1': [436, 437], + 'gid://gitlab/List/2': [], + }; + + const issues = { + '436': mockIssueWithModel, + '437': mockIssue2WithModel, + }; + + const state = { + endpoints: { fullPath: 'gitlab-org', boardId: '1' }, + boardType: 'group', + disabled: false, + boardLists: mockListsWithModel, + issuesByListId: listIssues, + issues, + }; + + it('should commit MOVE_ISSUE mutation and MOVE_ISSUE_SUCCESS mutation when successful', done => { + jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ + data: { + issueMoveList: { + issue: rawIssue, + errors: [], + }, + }, + }); + + testAction( + actions.moveIssue, + { + issueId: '436', + issueIid: mockIssue.iid, + issuePath: mockIssue.referencePath, + fromListId: 'gid://gitlab/List/1', + toListId: 'gid://gitlab/List/2', + }, + state, + [ + { + type: types.MOVE_ISSUE, + payload: { + originalIssue: mockIssueWithModel, + fromListId: 'gid://gitlab/List/1', + toListId: 'gid://gitlab/List/2', + }, + }, + { + type: types.MOVE_ISSUE_SUCCESS, + payload: { issue: rawIssue }, + }, + ], + [], + done, + ); + }); + + it('calls mutate with the correct variables', () => { + const mutationVariables = { + mutation: issueMoveListMutation, + variables: { + projectPath: getProjectPath(mockIssue.referencePath), + boardId: fullBoardId(state.endpoints.boardId), + iid: mockIssue.iid, + fromListId: 1, + toListId: 2, + moveBeforeId: undefined, + moveAfterId: undefined, + }, + }; + jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ + data: { + issueMoveList: { + issue: rawIssue, + errors: [], + }, + }, + }); + + actions.moveIssue( + { state, commit: () => {} }, + { + issueId: mockIssue.id, + issueIid: mockIssue.iid, + issuePath: mockIssue.referencePath, + fromListId: 'gid://gitlab/List/1', + toListId: 'gid://gitlab/List/2', + }, + ); + + expect(gqlClient.mutate).toHaveBeenCalledWith(mutationVariables); + }); + + it('should commit MOVE_ISSUE mutation and MOVE_ISSUE_FAILURE mutation when unsuccessful', done => { + jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ + data: { + issueMoveList: { + issue: {}, + errors: [{ foo: 'bar' }], + }, + }, + }); + + testAction( + actions.moveIssue, + { + issueId: '436', + issueIid: mockIssue.iid, + issuePath: mockIssue.referencePath, + fromListId: 'gid://gitlab/List/1', + toListId: 'gid://gitlab/List/2', + }, + state, + [ + { + type: types.MOVE_ISSUE, + payload: { + originalIssue: mockIssueWithModel, + fromListId: 'gid://gitlab/List/1', + toListId: 'gid://gitlab/List/2', + }, + }, + { + type: types.MOVE_ISSUE_FAILURE, + payload: { + originalIssue: mockIssueWithModel, + fromListId: 'gid://gitlab/List/1', + toListId: 'gid://gitlab/List/2', + originalIndex: 0, + }, + }, + ], + [], + done, + ); + }); }); describe('createNewIssue', () => { expectNotImplemented(actions.createNewIssue); }); +describe('addListIssue', () => { + it('should commit UPDATE_LIST_FAILURE mutation when API returns an error', done => { + const payload = { + list: mockLists[0], + issue: mockIssue, + position: 0, + }; + + testAction( + actions.addListIssue, + payload, + {}, + [{ type: types.ADD_ISSUE_TO_LIST, payload }], + [], + done, + ); + }); +}); + +describe('addListIssueFailure', () => { + it('should commit UPDATE_LIST_FAILURE mutation when API returns an error', done => { + const payload = { + list: mockLists[0], + issue: mockIssue, + }; + + testAction( + actions.addListIssueFailure, + payload, + {}, + [{ type: types.ADD_ISSUE_TO_LIST_FAILURE, payload }], + [], + done, + ); + }); +}); + describe('fetchBacklog', () => { expectNotImplemented(actions.fetchBacklog); }); diff --git a/spec/frontend/boards/stores/getters_spec.js b/spec/frontend/boards/stores/getters_spec.js index 38b2333e679..288143a0f21 100644 --- a/spec/frontend/boards/stores/getters_spec.js +++ b/spec/frontend/boards/stores/getters_spec.js @@ -1,4 +1,6 @@ import getters from '~/boards/stores/getters'; +import { inactiveId } from '~/boards/constants'; +import { mockIssue, mockIssue2, mockIssues, mockIssuesByListId, issues } from '../mock_data'; describe('Boards - Getters', () => { describe('getLabelToggleState', () => { @@ -18,4 +20,114 @@ describe('Boards - Getters', () => { expect(getters.getLabelToggleState(state)).toBe('off'); }); }); + + describe('isSidebarOpen', () => { + it('returns true when activeId is not equal to 0', () => { + const state = { + activeId: 1, + }; + + expect(getters.isSidebarOpen(state)).toBe(true); + }); + + it('returns false when activeId is equal to 0', () => { + const state = { + activeId: inactiveId, + }; + + expect(getters.isSidebarOpen(state)).toBe(false); + }); + }); + + describe('isSwimlanesOn', () => { + afterEach(() => { + window.gon = { features: {} }; + }); + + describe('when boardsWithSwimlanes is true', () => { + beforeEach(() => { + window.gon = { features: { boardsWithSwimlanes: true } }; + }); + + describe('when isShowingEpicsSwimlanes is true', () => { + it('returns true', () => { + const state = { + isShowingEpicsSwimlanes: true, + }; + + expect(getters.isSwimlanesOn(state)).toBe(true); + }); + }); + + describe('when isShowingEpicsSwimlanes is false', () => { + it('returns false', () => { + const state = { + isShowingEpicsSwimlanes: false, + }; + + expect(getters.isSwimlanesOn(state)).toBe(false); + }); + }); + }); + + describe('when boardsWithSwimlanes is false', () => { + describe('when isShowingEpicsSwimlanes is true', () => { + it('returns false', () => { + const state = { + isShowingEpicsSwimlanes: true, + }; + + expect(getters.isSwimlanesOn(state)).toBe(false); + }); + }); + + describe('when isShowingEpicsSwimlanes is false', () => { + it('returns false', () => { + const state = { + isShowingEpicsSwimlanes: false, + }; + + expect(getters.isSwimlanesOn(state)).toBe(false); + }); + }); + }); + }); + + describe('getIssueById', () => { + const state = { issues: { '1': 'issue' } }; + + it.each` + id | expected + ${'1'} | ${'issue'} + ${''} | ${{}} + `('returns $expected when $id is passed to state', ({ id, expected }) => { + expect(getters.getIssueById(state)(id)).toEqual(expected); + }); + }); + + describe('getActiveIssue', () => { + it.each` + id | expected + ${'1'} | ${'issue'} + ${''} | ${{}} + `('returns $expected when $id is passed to state', ({ id, expected }) => { + const state = { issues: { '1': 'issue' }, activeId: id }; + + expect(getters.getActiveIssue(state)).toEqual(expected); + }); + }); + + describe('getIssues', () => { + const boardsState = { + issuesByListId: mockIssuesByListId, + issues, + }; + 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( + mockIssues, + ); + }); + }); }); diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js index c1f7f3dda6e..a13a99a507e 100644 --- a/spec/frontend/boards/stores/mutations_spec.js +++ b/spec/frontend/boards/stores/mutations_spec.js @@ -1,6 +1,17 @@ import mutations from '~/boards/stores/mutations'; +import * as types from '~/boards/stores/mutation_types'; import defaultState from '~/boards/stores/state'; -import { mockIssue } from '../mock_data'; +import { + listObj, + listObjDuplicate, + mockListsWithModel, + mockLists, + rawIssue, + mockIssue, + mockIssue2, + mockIssueWithModel, + mockIssue2WithModel, +} from '../mock_data'; const expectNotImplemented = action => { it('is not implemented', () => { @@ -26,21 +37,56 @@ describe('Board Store Mutations', () => { fullPath: 'gitlab-org', }; const boardType = 'group'; + const disabled = false; + const showPromotion = false; - mutations.SET_INITIAL_BOARD_DATA(state, { ...endpoints, boardType }); + mutations[types.SET_INITIAL_BOARD_DATA](state, { + ...endpoints, + boardType, + disabled, + showPromotion, + }); expect(state.endpoints).toEqual(endpoints); expect(state.boardType).toEqual(boardType); + expect(state.disabled).toEqual(disabled); + expect(state.showPromotion).toEqual(showPromotion); + }); + }); + + describe('RECEIVE_BOARD_LISTS_SUCCESS', () => { + it('Should set boardLists to state', () => { + const lists = [listObj, listObjDuplicate]; + + mutations[types.RECEIVE_BOARD_LISTS_SUCCESS](state, lists); + + expect(state.boardLists).toEqual(lists); }); }); describe('SET_ACTIVE_ID', () => { - it('updates activeListId to be the value that is passed', () => { - const expectedId = 1; + const expected = { id: 1, sidebarType: '' }; - mutations.SET_ACTIVE_ID(state, expectedId); + beforeEach(() => { + mutations.SET_ACTIVE_ID(state, expected); + }); + + it('updates aciveListId to be the value that is passed', () => { + expect(state.activeId).toBe(expected.id); + }); - expect(state.activeId).toBe(expectedId); + it('updates sidebarType to be the value that is passed', () => { + expect(state.sidebarType).toBe(expected.sidebarType); + }); + }); + + describe('SET_FILTERS', () => { + it('updates filterParams to be the value that is passed', () => { + const filterParams = { labelName: 'label' }; + + mutations.SET_FILTERS(state, filterParams); + + expect(state.filterParams).toBe(filterParams); }); }); @@ -56,16 +102,35 @@ describe('Board Store Mutations', () => { expectNotImplemented(mutations.RECEIVE_ADD_LIST_ERROR); }); - describe('REQUEST_UPDATE_LIST', () => { - expectNotImplemented(mutations.REQUEST_UPDATE_LIST); - }); + describe('MOVE_LIST', () => { + it('updates boardLists state with reordered lists', () => { + state = { + ...state, + boardLists: mockListsWithModel, + }; - describe('RECEIVE_UPDATE_LIST_SUCCESS', () => { - expectNotImplemented(mutations.RECEIVE_UPDATE_LIST_SUCCESS); + mutations.MOVE_LIST(state, { + movedList: mockListsWithModel[0], + listAtNewIndex: mockListsWithModel[1], + }); + + expect(state.boardLists).toEqual([mockListsWithModel[1], mockListsWithModel[0]]); + }); }); - describe('RECEIVE_UPDATE_LIST_ERROR', () => { - expectNotImplemented(mutations.RECEIVE_UPDATE_LIST_ERROR); + describe('UPDATE_LIST_FAILURE', () => { + it('updates boardLists state with previous order and sets error message', () => { + state = { + ...state, + boardLists: [mockListsWithModel[1], mockListsWithModel[0]], + error: undefined, + }; + + mutations.UPDATE_LIST_FAILURE(state, mockListsWithModel); + + expect(state.boardLists).toEqual(mockListsWithModel); + expect(state.error).toEqual('An error occurred while updating the list. Please try again.'); + }); }); describe('REQUEST_REMOVE_LIST', () => { @@ -80,6 +145,33 @@ describe('Board Store Mutations', () => { expectNotImplemented(mutations.RECEIVE_REMOVE_LIST_ERROR); }); + describe('RECEIVE_ISSUES_FOR_LIST_SUCCESS', () => { + it('updates issuesByListId and issues on state', () => { + const listIssues = { + 'gid://gitlab/List/1': [mockIssue.id], + }; + const issues = { + '1': mockIssue, + }; + + state = { + ...state, + isLoadingIssues: true, + issuesByListId: {}, + issues: {}, + boardLists: mockListsWithModel, + }; + + mutations.RECEIVE_ISSUES_FOR_LIST_SUCCESS(state, { + listIssues: { listData: listIssues, issues }, + listId: 'gid://gitlab/List/1', + }); + + expect(state.issuesByListId).toEqual(listIssues); + expect(state.issues).toEqual(issues); + }); + }); + describe('REQUEST_ISSUES_FOR_ALL_LISTS', () => { it('sets isLoadingIssues to true', () => { expect(state.isLoadingIssues).toBe(false); @@ -90,22 +182,45 @@ describe('Board Store Mutations', () => { }); }); + describe('RECEIVE_ISSUES_FOR_LIST_FAILURE', () => { + it('sets error message', () => { + state = { + ...state, + boardLists: mockListsWithModel, + error: undefined, + }; + + const listId = 'gid://gitlab/List/1'; + + mutations.RECEIVE_ISSUES_FOR_LIST_FAILURE(state, listId); + + expect(state.error).toEqual( + 'An error occurred while fetching the board issues. Please reload the page.', + ); + }); + }); + describe('RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS', () => { it('sets isLoadingIssues to false and updates issuesByListId object', () => { const listIssues = { - '1': [mockIssue], + 'gid://gitlab/List/1': [mockIssue.id], + }; + const issues = { + '1': mockIssue, }; state = { ...state, isLoadingIssues: true, issuesByListId: {}, + issues: {}, }; - mutations.RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS(state, listIssues); + mutations.RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS(state, { listData: listIssues, issues }); expect(state.isLoadingIssues).toBe(false); expect(state.issuesByListId).toEqual(listIssues); + expect(state.issues).toEqual(issues); }); }); @@ -113,6 +228,65 @@ describe('Board Store Mutations', () => { expectNotImplemented(mutations.REQUEST_ADD_ISSUE); }); + describe('RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE', () => { + it('sets isLoadingIssues to false and sets error message', () => { + state = { + ...state, + isLoadingIssues: true, + error: undefined, + }; + + mutations.RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE(state); + + expect(state.isLoadingIssues).toBe(false); + expect(state.error).toEqual( + 'An error occurred while fetching the board issues. Please reload the page.', + ); + }); + }); + + describe('UPDATE_ISSUE_BY_ID', () => { + const issueId = '1'; + const prop = 'id'; + const value = '2'; + const issue = { [issueId]: { id: 1, title: 'Issue' } }; + + beforeEach(() => { + state = { + ...state, + isLoadingIssues: true, + error: undefined, + issues: { + ...issue, + }, + }; + }); + + describe('when the issue is in state', () => { + it('updates the property of the correct issue', () => { + mutations.UPDATE_ISSUE_BY_ID(state, { + issueId, + prop, + value, + }); + + expect(state.issues[issueId]).toEqual({ ...issue[issueId], id: '2' }); + }); + }); + + describe('when the issue is not in state', () => { + it('throws an error', () => { + expect(() => { + mutations.UPDATE_ISSUE_BY_ID(state, { + issueId: '3', + prop, + value, + }); + }).toThrow(new Error('No issue found.')); + }); + }); + }); + describe('RECEIVE_ADD_ISSUE_SUCCESS', () => { expectNotImplemented(mutations.RECEIVE_ADD_ISSUE_SUCCESS); }); @@ -121,16 +295,86 @@ describe('Board Store Mutations', () => { expectNotImplemented(mutations.RECEIVE_ADD_ISSUE_ERROR); }); - describe('REQUEST_MOVE_ISSUE', () => { - expectNotImplemented(mutations.REQUEST_MOVE_ISSUE); + describe('MOVE_ISSUE', () => { + it('updates issuesByListId, moving issue between lists', () => { + const listIssues = { + 'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id], + 'gid://gitlab/List/2': [], + }; + + const issues = { + '1': mockIssueWithModel, + '2': mockIssue2WithModel, + }; + + state = { + ...state, + issuesByListId: listIssues, + boardLists: mockListsWithModel, + issues, + }; + + mutations.MOVE_ISSUE(state, { + originalIssue: mockIssue2WithModel, + 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.issuesByListId).toEqual(updatedListIssues); + }); }); - describe('RECEIVE_MOVE_ISSUE_SUCCESS', () => { - expectNotImplemented(mutations.RECEIVE_MOVE_ISSUE_SUCCESS); + describe('MOVE_ISSUE_SUCCESS', () => { + it('updates issue in issues state', () => { + const issues = { + '436': { id: rawIssue.id }, + }; + + state = { + ...state, + issues, + }; + + mutations.MOVE_ISSUE_SUCCESS(state, { + issue: rawIssue, + }); + + expect(state.issues).toEqual({ '436': { ...mockIssueWithModel, id: 436 } }); + }); }); - describe('RECEIVE_MOVE_ISSUE_ERROR', () => { - expectNotImplemented(mutations.RECEIVE_MOVE_ISSUE_ERROR); + describe('MOVE_ISSUE_FAILURE', () => { + it('updates issuesByListId, reverting moving issue between lists, and sets error message', () => { + const listIssues = { + 'gid://gitlab/List/1': [mockIssue.id], + 'gid://gitlab/List/2': [mockIssue2.id], + }; + + state = { + ...state, + issuesByListId: listIssues, + }; + + mutations.MOVE_ISSUE_FAILURE(state, { + originalIssue: mockIssue2, + fromListId: 'gid://gitlab/List/1', + toListId: 'gid://gitlab/List/2', + originalIndex: 1, + }); + + const updatedListIssues = { + 'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id], + 'gid://gitlab/List/2': [], + }; + + expect(state.issuesByListId).toEqual(updatedListIssues); + expect(state.error).toEqual('An error occurred while moving the issue. Please try again.'); + }); }); describe('REQUEST_UPDATE_ISSUE', () => { @@ -145,6 +389,50 @@ describe('Board Store Mutations', () => { expectNotImplemented(mutations.RECEIVE_UPDATE_ISSUE_ERROR); }); + describe('ADD_ISSUE_TO_LIST', () => { + it('adds issue to issues state and issue id in list in issuesByListId', () => { + const listIssues = { + 'gid://gitlab/List/1': [mockIssue.id], + }; + const issues = { + '1': mockIssue, + }; + + state = { + ...state, + issuesByListId: listIssues, + issues, + }; + + mutations.ADD_ISSUE_TO_LIST(state, { list: mockLists[0], issue: mockIssue2 }); + + expect(state.issuesByListId['gid://gitlab/List/1']).toContain(mockIssue2.id); + expect(state.issues[mockIssue2.id]).toEqual(mockIssue2); + }); + }); + + describe('ADD_ISSUE_TO_LIST_FAILURE', () => { + it('removes issue id from list in issuesByListId', () => { + const listIssues = { + 'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id], + }; + const issues = { + '1': mockIssue, + '2': mockIssue2, + }; + + state = { + ...state, + issuesByListId: listIssues, + issues, + }; + + mutations.ADD_ISSUE_TO_LIST_FAILURE(state, { list: mockLists[0], issue: mockIssue2 }); + + expect(state.issuesByListId['gid://gitlab/List/1']).not.toContain(mockIssue2.id); + }); + }); + describe('SET_CURRENT_PAGE', () => { expectNotImplemented(mutations.SET_CURRENT_PAGE); }); |