diff options
Diffstat (limited to 'spec/frontend/boards')
-rw-r--r-- | spec/frontend/boards/board_blank_state_spec.js | 95 | ||||
-rw-r--r-- | spec/frontend/boards/board_list_new_spec.js | 234 | ||||
-rw-r--r-- | spec/frontend/boards/board_list_spec.js | 3 | ||||
-rw-r--r-- | spec/frontend/boards/boards_store_spec.js | 19 | ||||
-rw-r--r-- | spec/frontend/boards/components/board_configuration_options_spec.js | 59 | ||||
-rw-r--r-- | spec/frontend/boards/components/board_content_spec.js | 3 | ||||
-rw-r--r-- | spec/frontend/boards/components/sidebar/board_editable_item_spec.js | 26 | ||||
-rw-r--r-- | spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js | 143 | ||||
-rw-r--r-- | spec/frontend/boards/mock_data.js | 8 | ||||
-rw-r--r-- | spec/frontend/boards/stores/actions_spec.js | 236 | ||||
-rw-r--r-- | spec/frontend/boards/stores/getters_spec.js | 30 | ||||
-rw-r--r-- | spec/frontend/boards/stores/mutations_spec.js | 135 |
12 files changed, 804 insertions, 187 deletions
diff --git a/spec/frontend/boards/board_blank_state_spec.js b/spec/frontend/boards/board_blank_state_spec.js deleted file mode 100644 index 3ffdda52f58..00000000000 --- a/spec/frontend/boards/board_blank_state_spec.js +++ /dev/null @@ -1,95 +0,0 @@ -import Vue from 'vue'; -import boardsStore from '~/boards/stores/boards_store'; -import BoardBlankState from '~/boards/components/board_blank_state.vue'; - -describe('Boards blank state', () => { - let vm; - let fail = false; - - beforeEach(done => { - const Comp = Vue.extend(BoardBlankState); - - boardsStore.create(); - - jest.spyOn(boardsStore, 'addList').mockImplementation(); - jest.spyOn(boardsStore, 'removeList').mockImplementation(); - jest.spyOn(boardsStore, 'generateDefaultLists').mockImplementation( - () => - new Promise((resolve, reject) => { - if (fail) { - reject(); - } else { - resolve({ - data: [ - { - id: 1, - title: 'To Do', - label: { id: 1 }, - }, - { - id: 2, - title: 'Doing', - label: { id: 2 }, - }, - ], - }); - } - }), - ); - - vm = new Comp(); - - setImmediate(() => { - vm.$mount(); - done(); - }); - }); - - it('renders pre-defined labels', () => { - expect(vm.$el.querySelectorAll('.board-blank-state-list li').length).toBe(2); - - expect(vm.$el.querySelectorAll('.board-blank-state-list li')[0].textContent.trim()).toEqual( - 'To Do', - ); - - expect(vm.$el.querySelectorAll('.board-blank-state-list li')[1].textContent.trim()).toEqual( - 'Doing', - ); - }); - - it('clears blank state', done => { - vm.$el.querySelector('.btn-default').click(); - - setImmediate(() => { - expect(boardsStore.welcomeIsHidden()).toBeTruthy(); - - done(); - }); - }); - - it('creates pre-defined labels', done => { - vm.$el.querySelector('.btn-success').click(); - - setImmediate(() => { - expect(boardsStore.addList).toHaveBeenCalledTimes(2); - expect(boardsStore.addList).toHaveBeenCalledWith(expect.objectContaining({ title: 'To Do' })); - - expect(boardsStore.addList).toHaveBeenCalledWith(expect.objectContaining({ title: 'Doing' })); - - done(); - }); - }); - - it('resets the store if request fails', done => { - fail = true; - - vm.$el.querySelector('.btn-success').click(); - - setImmediate(() => { - expect(boardsStore.welcomeIsHidden()).toBeFalsy(); - expect(boardsStore.removeList).toHaveBeenCalledWith(undefined, 'label'); - - done(); - }); - }); -}); diff --git a/spec/frontend/boards/board_list_new_spec.js b/spec/frontend/boards/board_list_new_spec.js new file mode 100644 index 00000000000..163611c2197 --- /dev/null +++ b/spec/frontend/boards/board_list_new_spec.js @@ -0,0 +1,234 @@ +/* global List */ +/* global ListIssue */ + +import Vuex from 'vuex'; +import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame'; +import { createLocalVue, mount } from '@vue/test-utils'; +import eventHub from '~/boards/eventhub'; +import BoardList from '~/boards/components/board_list_new.vue'; +import BoardCard from '~/boards/components/board_card.vue'; +import '~/boards/models/issue'; +import '~/boards/models/list'; +import { listObj, mockIssuesByListId, issues } from './mock_data'; +import defaultState from '~/boards/stores/state'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +const actions = { + fetchIssuesForList: jest.fn(), +}; + +const createStore = (state = defaultState) => { + return new Vuex.Store({ + state, + actions, + }); +}; + +const createComponent = ({ + listIssueProps = {}, + componentProps = {}, + listProps = {}, + state = {}, +} = {}) => { + const store = createStore({ + issuesByListId: mockIssuesByListId, + issues, + pageInfoByListId: { + 'gid://gitlab/List/1': { hasNextPage: true }, + 'gid://gitlab/List/2': {}, + }, + listsFlags: { + 'gid://gitlab/List/1': {}, + 'gid://gitlab/List/2': {}, + }, + ...state, + }); + + const list = new List({ + ...listObj, + id: 'gid://gitlab/List/1', + ...listProps, + doNotFetchIssues: true, + }); + const issue = new ListIssue({ + title: 'Testing', + id: 1, + iid: 1, + confidential: false, + labels: [], + assignees: [], + ...listIssueProps, + }); + if (!Object.prototype.hasOwnProperty.call(listProps, 'issuesSize')) { + list.issuesSize = 1; + } + + const component = mount(BoardList, { + localVue, + propsData: { + disabled: false, + list, + issues: [issue], + ...componentProps, + }, + store, + provide: { + groupId: null, + rootPath: '/', + }, + }); + + return component; +}; + +describe('Board list component', () => { + let wrapper; + useFakeRequestAnimationFrame(); + + describe('When Expanded', () => { + beforeEach(() => { + wrapper = createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders component', () => { + expect(wrapper.find('.board-list-component').exists()).toBe(true); + }); + + it('renders loading icon', () => { + wrapper = createComponent({ + state: { listsFlags: { 'gid://gitlab/List/1': { isLoading: true } } }, + }); + + expect(wrapper.find('[data-testid="board_list_loading"').exists()).toBe(true); + }); + + it('renders issues', () => { + expect(wrapper.findAll(BoardCard).length).toBe(1); + }); + + it('sets data attribute with issue id', () => { + expect(wrapper.find('.board-card').attributes('data-issue-id')).toBe('1'); + }); + + it('shows new issue form', async () => { + wrapper.vm.toggleForm(); + + await wrapper.vm.$nextTick(); + expect(wrapper.find('.board-new-issue-form').exists()).toBe(true); + }); + + it('shows new issue form after eventhub event', async () => { + eventHub.$emit(`toggle-issue-form-${wrapper.vm.list.id}`); + + await wrapper.vm.$nextTick(); + expect(wrapper.find('.board-new-issue-form').exists()).toBe(true); + }); + + it('does not show new issue form for closed list', () => { + wrapper.setProps({ list: { type: 'closed' } }); + wrapper.vm.toggleForm(); + + expect(wrapper.find('.board-new-issue-form').exists()).toBe(false); + }); + + it('shows count list item', async () => { + wrapper.vm.showCount = true; + + await wrapper.vm.$nextTick(); + expect(wrapper.find('.board-list-count').exists()).toBe(true); + + expect(wrapper.find('.board-list-count').text()).toBe('Showing all issues'); + }); + + it('sets data attribute with invalid id', async () => { + wrapper.vm.showCount = true; + + await wrapper.vm.$nextTick(); + expect(wrapper.find('.board-list-count').attributes('data-issue-id')).toBe('-1'); + }); + + it('shows how many more issues to load', async () => { + wrapper.vm.showCount = true; + wrapper.setProps({ list: { issuesSize: 20 } }); + + await wrapper.vm.$nextTick(); + expect(wrapper.find('.board-list-count').text()).toBe('Showing 1 of 20 issues'); + }); + }); + + describe('load more issues', () => { + beforeEach(() => { + wrapper = createComponent({ + listProps: { issuesSize: 25 }, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('loads more issues after scrolling', () => { + wrapper.vm.$refs.list.dispatchEvent(new Event('scroll')); + + expect(actions.fetchIssuesForList).toHaveBeenCalled(); + }); + + it('does not load issues if already loading', () => { + wrapper.vm.$refs.list.dispatchEvent(new Event('scroll')); + wrapper.vm.$refs.list.dispatchEvent(new Event('scroll')); + + expect(actions.fetchIssuesForList).toHaveBeenCalledTimes(1); + }); + + it('shows loading more spinner', async () => { + wrapper.vm.showCount = true; + wrapper.vm.list.loadingMore = true; + + await wrapper.vm.$nextTick(); + expect(wrapper.find('.board-list-count .gl-spinner').exists()).toBe(true); + }); + }); + + describe('max issue count warning', () => { + beforeEach(() => { + wrapper = createComponent({ + listProps: { issuesSize: 50 }, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when issue count exceeds max issue count', () => { + it('sets background to bg-danger-100', async () => { + wrapper.setProps({ list: { issuesSize: 4, maxIssueCount: 3 } }); + + await wrapper.vm.$nextTick(); + expect(wrapper.find('.bg-danger-100').exists()).toBe(true); + }); + }); + + describe('when list issue count does NOT exceed list max issue count', () => { + it('does not sets background to bg-danger-100', () => { + wrapper.setProps({ list: { issuesSize: 2, maxIssueCount: 3 } }); + + expect(wrapper.find('.bg-danger-100').exists()).toBe(false); + }); + }); + + describe('when list max issue count is 0', () => { + it('does not sets background to bg-danger-100', () => { + wrapper.setProps({ list: { maxIssueCount: 0 } }); + + expect(wrapper.find('.bg-danger-100').exists()).toBe(false); + }); + }); + }); +}); diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js index 88883ae61d4..0fe3c88f518 100644 --- a/spec/frontend/boards/board_list_spec.js +++ b/spec/frontend/boards/board_list_spec.js @@ -44,7 +44,6 @@ const createComponent = ({ done, listIssueProps = {}, componentProps = {}, listP disabled: false, list, issues: list.issues, - loading: false, ...componentProps, }, provide: { @@ -94,7 +93,7 @@ describe('Board list component', () => { }); it('renders loading icon', () => { - component.loading = true; + component.list.loading = true; return Vue.nextTick().then(() => { expect(component.$el.querySelector('.board-list-loading')).not.toBeNull(); diff --git a/spec/frontend/boards/boards_store_spec.js b/spec/frontend/boards/boards_store_spec.js index 41971137b95..e7c1cf79fdc 100644 --- a/spec/frontend/boards/boards_store_spec.js +++ b/spec/frontend/boards/boards_store_spec.js @@ -1,7 +1,7 @@ import AxiosMockAdapter from 'axios-mock-adapter'; import { TEST_HOST } from 'helpers/test_constants'; import axios from '~/lib/utils/axios_utils'; -import boardsStore from '~/boards/stores/boards_store'; +import boardsStore, { gqlClient } from '~/boards/stores/boards_store'; import eventHub from '~/boards/eventhub'; import { listObj, listObjDuplicate } from './mock_data'; @@ -503,11 +503,15 @@ describe('boardsStore', () => { beforeEach(() => { requestSpy = jest.fn(); axiosMock.onPut(url).replyOnce(config => requestSpy(config)); + jest.spyOn(gqlClient, 'mutate').mockReturnValue(Promise.resolve({})); }); it('makes a request to update the board', () => { requestSpy.mockReturnValue([200, dummyResponse]); - const expectedResponse = expect.objectContaining({ data: dummyResponse }); + const expectedResponse = [ + expect.objectContaining({ data: dummyResponse }), + expect.objectContaining({}), + ]; return expect( boardsStore.createBoard({ @@ -555,11 +559,12 @@ describe('boardsStore', () => { beforeEach(() => { requestSpy = jest.fn(); axiosMock.onPost(url).replyOnce(config => requestSpy(config)); + jest.spyOn(gqlClient, 'mutate').mockReturnValue(Promise.resolve({})); }); it('makes a request to create a new board', () => { requestSpy.mockReturnValue([200, dummyResponse]); - const expectedResponse = expect.objectContaining({ data: dummyResponse }); + const expectedResponse = dummyResponse; return expect(boardsStore.createBoard(board)) .resolves.toEqual(expectedResponse) @@ -740,14 +745,6 @@ describe('boardsStore', () => { expect(boardsStore.shouldAddBlankState()).toBe(true); }); - it('adds the blank state', () => { - boardsStore.addBlankState(); - - const list = boardsStore.findList('type', 'blank', 'blank'); - - expect(list).toBeDefined(); - }); - it('removes list from state', () => { boardsStore.addList(listObj); diff --git a/spec/frontend/boards/components/board_configuration_options_spec.js b/spec/frontend/boards/components/board_configuration_options_spec.js new file mode 100644 index 00000000000..e9a1cb6a4e8 --- /dev/null +++ b/spec/frontend/boards/components/board_configuration_options_spec.js @@ -0,0 +1,59 @@ +import { shallowMount } from '@vue/test-utils'; +import BoardConfigurationOptions from '~/boards/components/board_configuration_options.vue'; + +describe('BoardConfigurationOptions', () => { + let wrapper; + const board = { hide_backlog_list: false, hide_closed_list: false }; + + const defaultProps = { + currentBoard: board, + board, + isNewForm: false, + }; + + const createComponent = () => { + wrapper = shallowMount(BoardConfigurationOptions, { + propsData: { ...defaultProps }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + const backlogListCheckbox = el => el.find('[data-testid="backlog-list-checkbox"]'); + const closedListCheckbox = el => el.find('[data-testid="closed-list-checkbox"]'); + + const checkboxAssert = (backlogCheckbox, closedCheckbox) => { + expect(backlogListCheckbox(wrapper).attributes('checked')).toEqual( + backlogCheckbox ? undefined : 'true', + ); + expect(closedListCheckbox(wrapper).attributes('checked')).toEqual( + closedCheckbox ? undefined : 'true', + ); + }; + + it.each` + backlogCheckboxValue | closedCheckboxValue + ${true} | ${true} + ${true} | ${false} + ${false} | ${true} + ${false} | ${false} + `( + 'renders two checkbox when one is $backlogCheckboxValue and other is $closedCheckboxValue', + async ({ backlogCheckboxValue, closedCheckboxValue }) => { + await wrapper.setData({ + hideBacklogList: backlogCheckboxValue, + hideClosedList: closedCheckboxValue, + }); + + return wrapper.vm.$nextTick().then(() => { + checkboxAssert(backlogCheckboxValue, closedCheckboxValue); + }); + }, + ); +}); diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js index df117d06cdf..09e38001e2e 100644 --- a/spec/frontend/boards/components/board_content_spec.js +++ b/spec/frontend/boards/components/board_content_spec.js @@ -23,9 +23,6 @@ describe('BoardContent', () => { return new Vuex.Store({ getters, state, - actions: { - fetchIssuesForAllLists: () => {}, - }, }); }; diff --git a/spec/frontend/boards/components/sidebar/board_editable_item_spec.js b/spec/frontend/boards/components/sidebar/board_editable_item_spec.js index 1dbcbd06407..d7df2ff1563 100644 --- a/spec/frontend/boards/components/sidebar/board_editable_item_spec.js +++ b/spec/frontend/boards/components/sidebar/board_editable_item_spec.js @@ -96,12 +96,34 @@ describe('boards sidebar remove issue', () => { expect(findExpanded().isVisible()).toBe(false); }); - it('emits changed event', async () => { + it('emits close event', async () => { document.body.click(); await wrapper.vm.$nextTick(); - expect(wrapper.emitted().changed[1][0]).toBe(false); + expect(wrapper.emitted().close.length).toBe(1); }); }); + + it('emits open when edit button is clicked and edit is initailized to false', async () => { + createComponent({ canUpdate: true }); + + findEditButton().vm.$emit('click'); + + await wrapper.vm.$nextTick(); + + expect(wrapper.emitted().open.length).toBe(1); + }); + + it('does not emits events when collapsing with false `emitEvent`', async () => { + createComponent({ canUpdate: true }); + + findEditButton().vm.$emit('click'); + + await wrapper.vm.$nextTick(); + + wrapper.vm.collapse({ emitEvent: false }); + + expect(wrapper.emitted().close).toBeUndefined(); + }); }); 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 new file mode 100644 index 00000000000..da000d21f6a --- /dev/null +++ b/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js @@ -0,0 +1,143 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlLabel } from '@gitlab/ui'; +import { TEST_HOST } from 'helpers/test_constants'; +import { labels as TEST_LABELS, mockIssue as TEST_ISSUE } from 'jest/boards/mock_data'; +import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue'; +import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { createStore } from '~/boards/stores'; +import createFlash from '~/flash'; + +jest.mock('~/flash'); + +const TEST_LABELS_PAYLOAD = TEST_LABELS.map(label => ({ ...label, set: true })); +const TEST_LABELS_TITLES = TEST_LABELS.map(label => label.title); + +describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => { + let wrapper; + let store; + + afterEach(() => { + wrapper.destroy(); + store = null; + wrapper = null; + }); + + const createWrapper = ({ labels = [] } = {}) => { + store = createStore(); + store.state.issues = { [TEST_ISSUE.id]: { ...TEST_ISSUE, labels } }; + store.state.activeId = TEST_ISSUE.id; + + wrapper = shallowMount(BoardSidebarLabelsSelect, { + store, + provide: { + canUpdate: true, + labelsFetchPath: TEST_HOST, + labelsManagePath: TEST_HOST, + labelsFilterBasePath: TEST_HOST, + }, + stubs: { + 'board-editable-item': BoardEditableItem, + 'labels-select': '<div></div>', + }, + }); + }; + + const findLabelsSelect = () => wrapper.find({ ref: 'labelsSelect' }); + const findLabelsTitles = () => wrapper.findAll(GlLabel).wrappers.map(item => item.props('title')); + const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]'); + + it('renders "None" when no labels are selected', () => { + createWrapper(); + + expect(findCollapsed().text()).toBe('None'); + }); + + it('renders labels when set', () => { + createWrapper({ labels: TEST_LABELS }); + + expect(findLabelsTitles()).toEqual(TEST_LABELS_TITLES); + }); + + describe('when labels are submitted', () => { + beforeEach(async () => { + createWrapper(); + + jest.spyOn(wrapper.vm, 'setActiveIssueLabels').mockImplementation(() => TEST_LABELS); + findLabelsSelect().vm.$emit('updateSelectedLabels', TEST_LABELS_PAYLOAD); + store.state.issues[TEST_ISSUE.id].labels = TEST_LABELS; + await wrapper.vm.$nextTick(); + }); + + it('collapses sidebar and renders labels', () => { + expect(findCollapsed().isVisible()).toBe(true); + expect(findLabelsTitles()).toEqual(TEST_LABELS_TITLES); + }); + + it('commits change to the server', () => { + expect(wrapper.vm.setActiveIssueLabels).toHaveBeenCalledWith({ + addLabelIds: TEST_LABELS.map(label => label.id), + projectPath: 'gitlab-org/test-subgroup/gitlab-test', + removeLabelIds: [], + }); + }); + }); + + describe('when labels are updated over existing labels', () => { + const testLabelsPayload = [{ id: 5, set: true }, { id: 7, set: true }]; + const expectedLabels = [{ id: 5 }, { id: 7 }]; + + beforeEach(async () => { + createWrapper({ labels: TEST_LABELS }); + + jest.spyOn(wrapper.vm, 'setActiveIssueLabels').mockImplementation(() => expectedLabels); + findLabelsSelect().vm.$emit('updateSelectedLabels', testLabelsPayload); + await wrapper.vm.$nextTick(); + }); + + it('commits change to the server', () => { + expect(wrapper.vm.setActiveIssueLabels).toHaveBeenCalledWith({ + addLabelIds: [5, 7], + removeLabelIds: [6], + projectPath: 'gitlab-org/test-subgroup/gitlab-test', + }); + }); + }); + + describe('when removing individual labels', () => { + const testLabel = TEST_LABELS[0]; + + beforeEach(async () => { + createWrapper({ labels: [testLabel] }); + + jest.spyOn(wrapper.vm, 'setActiveIssueLabels').mockImplementation(() => {}); + }); + + it('commits change to the server', () => { + wrapper.find(GlLabel).vm.$emit('close', testLabel); + + expect(wrapper.vm.setActiveIssueLabels).toHaveBeenCalledWith({ + removeLabelIds: [getIdFromGraphQLId(testLabel.id)], + projectPath: 'gitlab-org/test-subgroup/gitlab-test', + }); + }); + }); + + describe('when the mutation fails', () => { + beforeEach(async () => { + createWrapper({ labels: TEST_LABELS }); + + jest.spyOn(wrapper.vm, 'setActiveIssueLabels').mockImplementation(() => { + throw new Error(['failed mutation']); + }); + findLabelsSelect().vm.$emit('updateSelectedLabels', [{ id: '?' }]); + await wrapper.vm.$nextTick(); + }); + + it('collapses sidebar and renders former issue weight', () => { + expect(findCollapsed().isVisible()).toBe(true); + expect(findLabelsTitles()).toEqual(TEST_LABELS_TITLES); + expect(createFlash).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js index 5776332c499..50c0a85fc70 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -108,13 +108,19 @@ const assignees = [ }, ]; -const labels = [ +export const labels = [ { id: 'gid://gitlab/GroupLabel/5', title: 'Cosync', color: '#34ebec', description: null, }, + { + id: 'gid://gitlab/GroupLabel/6', + title: 'Brock', + color: '#e082b6', + description: null, + }, ]; export const rawIssue = { diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index bdbcd435708..78e70161121 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -6,12 +6,14 @@ import { mockIssueWithModel, mockIssue2WithModel, rawIssue, + mockIssues, + labels, } from '../mock_data'; import actions, { gqlClient } from '~/boards/stores/actions'; import * as types from '~/boards/stores/mutation_types'; import { inactiveId, ListType } from '~/boards/constants'; import issueMoveListMutation from '~/boards/queries/issue_move_list.mutation.graphql'; -import { fullBoardId } from '~/boards/boards_util'; +import { fullBoardId, formatListIssues, formatBoardLists } from '~/boards/boards_util'; const expectNotImplemented = action => { it('is not implemented', () => { @@ -76,6 +78,80 @@ describe('setActiveId', () => { }); }); +describe('fetchLists', () => { + const state = { + endpoints: { + fullPath: 'gitlab-org', + boardId: 1, + }, + filterParams: {}, + boardType: 'group', + }; + + let queryResponse = { + data: { + group: { + board: { + hideBacklogList: true, + lists: { + nodes: [mockLists[1]], + }, + }, + }, + }, + }; + + const formattedLists = formatBoardLists(queryResponse.data.group.board.lists); + + it('should commit mutations RECEIVE_BOARD_LISTS_SUCCESS on success', done => { + jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse); + + testAction( + actions.fetchLists, + {}, + state, + [ + { + type: types.RECEIVE_BOARD_LISTS_SUCCESS, + payload: formattedLists, + }, + ], + [{ type: 'showWelcomeList' }], + done, + ); + }); + + it('dispatch createList action when backlog list does not exist and is not hidden', done => { + queryResponse = { + data: { + group: { + board: { + hideBacklogList: false, + lists: { + nodes: [mockLists[1]], + }, + }, + }, + }, + }; + jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse); + + testAction( + actions.fetchLists, + {}, + state, + [ + { + type: types.RECEIVE_BOARD_LISTS_SUCCESS, + payload: formattedLists, + }, + ], + [{ type: 'createList', payload: { backlog: true } }, { type: 'showWelcomeList' }], + done, + ); + }); +}); + describe('showWelcomeList', () => { it('should dispatch addList action', done => { const state = { @@ -176,16 +252,26 @@ describe('createList', () => { describe('moveList', () => { it('should commit MOVE_LIST mutation and dispatch updateList action', done => { + const initialBoardListsState = { + 'gid://gitlab/List/1': mockListsWithModel[0], + 'gid://gitlab/List/2': mockListsWithModel[1], + }; + const state = { endpoints: { fullPath: 'gitlab-org', boardId: '1' }, boardType: 'group', disabled: false, - boardLists: mockListsWithModel, + boardLists: initialBoardListsState, }; testAction( actions.moveList, - { listId: 'gid://gitlab/List/1', newIndex: 1, adjustmentValue: 1 }, + { + listId: 'gid://gitlab/List/1', + replacedListId: 'gid://gitlab/List/2', + newIndex: 1, + adjustmentValue: 1, + }, state, [ { @@ -196,7 +282,11 @@ describe('moveList', () => { [ { type: 'updateList', - payload: { listId: 'gid://gitlab/List/1', position: 0, backupList: mockListsWithModel }, + payload: { + listId: 'gid://gitlab/List/1', + position: 0, + backupList: initialBoardListsState, + }, }, ], done, @@ -237,6 +327,99 @@ describe('deleteList', () => { expectNotImplemented(actions.deleteList); }); +describe('fetchIssuesForList', () => { + const listId = mockLists[0].id; + + const state = { + endpoints: { + fullPath: 'gitlab-org', + boardId: 1, + }, + filterParams: {}, + boardType: 'group', + }; + + const mockIssuesNodes = mockIssues.map(issue => ({ node: issue })); + + const pageInfo = { + endCursor: '', + hasNextPage: false, + }; + + const queryResponse = { + data: { + group: { + board: { + lists: { + nodes: [ + { + id: listId, + issues: { + edges: mockIssuesNodes, + pageInfo, + }, + }, + ], + }, + }, + }, + }, + }; + + const formattedIssues = formatListIssues(queryResponse.data.group.board.lists); + + const listPageInfo = { + [listId]: pageInfo, + }; + + it('should commit mutations REQUEST_ISSUES_FOR_LIST and RECEIVE_ISSUES_FOR_LIST_SUCCESS on success', done => { + jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse); + + testAction( + actions.fetchIssuesForList, + { listId }, + state, + [ + { + type: types.REQUEST_ISSUES_FOR_LIST, + payload: { listId, fetchNext: false }, + }, + { + type: types.RECEIVE_ISSUES_FOR_LIST_SUCCESS, + payload: { listIssues: formattedIssues, listPageInfo, listId }, + }, + ], + [], + done, + ); + }); + + it('should commit mutations REQUEST_ISSUES_FOR_LIST and RECEIVE_ISSUES_FOR_LIST_FAILURE on failure', done => { + jest.spyOn(gqlClient, 'query').mockResolvedValue(Promise.reject()); + + testAction( + actions.fetchIssuesForList, + { listId }, + state, + [ + { + type: types.REQUEST_ISSUES_FOR_LIST, + payload: { listId, fetchNext: false }, + }, + { type: types.RECEIVE_ISSUES_FOR_LIST_FAILURE, payload: listId }, + ], + [], + done, + ); + }); +}); + +describe('resetIssues', () => { + it('commits RESET_ISSUES mutation', () => { + return testAction(actions.resetIssues, {}, {}, [{ type: types.RESET_ISSUES }], []); + }); +}); + describe('moveIssue', () => { const listIssues = { 'gid://gitlab/List/1': [436, 437], @@ -418,6 +601,51 @@ describe('addListIssueFailure', () => { }); }); +describe('setActiveIssueLabels', () => { + const state = { issues: { [mockIssue.id]: mockIssue } }; + const getters = { getActiveIssue: mockIssue }; + const testLabelIds = labels.map(label => label.id); + const input = { + addLabelIds: testLabelIds, + removeLabelIds: [], + projectPath: 'h/b', + }; + + it('should assign labels on success', done => { + jest + .spyOn(gqlClient, 'mutate') + .mockResolvedValue({ data: { updateIssue: { issue: { labels: { nodes: labels } } } } }); + + const payload = { + issueId: getters.getActiveIssue.id, + prop: 'labels', + value: labels, + }; + + testAction( + actions.setActiveIssueLabels, + input, + { ...state, ...getters }, + [ + { + type: types.UPDATE_ISSUE_BY_ID, + payload, + }, + ], + [], + done, + ); + }); + + it('throws error if fails', async () => { + jest + .spyOn(gqlClient, 'mutate') + .mockResolvedValue({ data: { updateIssue: { errors: ['failed mutation'] } } }); + + await expect(actions.setActiveIssueLabels({ getters }, input)).rejects.toThrow(Error); + }); +}); + describe('fetchBacklog', () => { expectNotImplemented(actions.fetchBacklog); }); diff --git a/spec/frontend/boards/stores/getters_spec.js b/spec/frontend/boards/stores/getters_spec.js index 288143a0f21..b987080abab 100644 --- a/spec/frontend/boards/stores/getters_spec.js +++ b/spec/frontend/boards/stores/getters_spec.js @@ -1,6 +1,13 @@ import getters from '~/boards/stores/getters'; import { inactiveId } from '~/boards/constants'; -import { mockIssue, mockIssue2, mockIssues, mockIssuesByListId, issues } from '../mock_data'; +import { + mockIssue, + mockIssue2, + mockIssues, + mockIssuesByListId, + issues, + mockListsWithModel, +} from '../mock_data'; describe('Boards - Getters', () => { describe('getLabelToggleState', () => { @@ -130,4 +137,25 @@ describe('Boards - Getters', () => { ); }); }); + + const boardsState = { + boardLists: { + 'gid://gitlab/List/1': mockListsWithModel[0], + 'gid://gitlab/List/2': mockListsWithModel[1], + }, + }; + + describe('getListByLabelId', () => { + it('returns list for a given label id', () => { + expect(getters.getListByLabelId(boardsState)('gid://gitlab/GroupLabel/121')).toEqual( + mockListsWithModel[1], + ); + }); + }); + + describe('getListByTitle', () => { + it('returns list for a given list title', () => { + expect(getters.getListByTitle(boardsState)('To Do')).toEqual(mockListsWithModel[1]); + }); + }); }); diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js index a13a99a507e..6e53f184bb3 100644 --- a/spec/frontend/boards/stores/mutations_spec.js +++ b/spec/frontend/boards/stores/mutations_spec.js @@ -2,8 +2,6 @@ import mutations from '~/boards/stores/mutations'; import * as types from '~/boards/stores/mutation_types'; import defaultState from '~/boards/stores/state'; import { - listObj, - listObjDuplicate, mockListsWithModel, mockLists, rawIssue, @@ -22,6 +20,11 @@ const expectNotImplemented = action => { describe('Board Store Mutations', () => { let state; + const initialBoardListsState = { + 'gid://gitlab/List/1': mockListsWithModel[0], + 'gid://gitlab/List/2': mockListsWithModel[1], + }; + beforeEach(() => { state = defaultState(); }); @@ -56,11 +59,19 @@ describe('Board Store Mutations', () => { describe('RECEIVE_BOARD_LISTS_SUCCESS', () => { it('Should set boardLists to state', () => { - const lists = [listObj, listObjDuplicate]; + mutations[types.RECEIVE_BOARD_LISTS_SUCCESS](state, initialBoardListsState); + + expect(state.boardLists).toEqual(initialBoardListsState); + }); + }); - mutations[types.RECEIVE_BOARD_LISTS_SUCCESS](state, lists); + describe('RECEIVE_BOARD_LISTS_FAILURE', () => { + it('Should set error in state', () => { + mutations[types.RECEIVE_BOARD_LISTS_FAILURE](state); - expect(state.boardLists).toEqual(lists); + expect(state.error).toEqual( + 'An error occurred while fetching the board lists. Please reload the page.', + ); }); }); @@ -95,7 +106,13 @@ describe('Board Store Mutations', () => { }); describe('RECEIVE_ADD_LIST_SUCCESS', () => { - expectNotImplemented(mutations.RECEIVE_ADD_LIST_SUCCESS); + it('adds list to boardLists state', () => { + mutations.RECEIVE_ADD_LIST_SUCCESS(state, mockListsWithModel[0]); + + expect(state.boardLists).toEqual({ + [mockListsWithModel[0].id]: mockListsWithModel[0], + }); + }); }); describe('RECEIVE_ADD_LIST_ERROR', () => { @@ -106,7 +123,7 @@ describe('Board Store Mutations', () => { it('updates boardLists state with reordered lists', () => { state = { ...state, - boardLists: mockListsWithModel, + boardLists: initialBoardListsState, }; mutations.MOVE_LIST(state, { @@ -114,7 +131,10 @@ describe('Board Store Mutations', () => { listAtNewIndex: mockListsWithModel[1], }); - expect(state.boardLists).toEqual([mockListsWithModel[1], mockListsWithModel[0]]); + expect(state.boardLists).toEqual({ + 'gid://gitlab/List/2': mockListsWithModel[1], + 'gid://gitlab/List/1': mockListsWithModel[0], + }); }); }); @@ -122,13 +142,16 @@ describe('Board Store Mutations', () => { it('updates boardLists state with previous order and sets error message', () => { state = { ...state, - boardLists: [mockListsWithModel[1], mockListsWithModel[0]], + boardLists: { + 'gid://gitlab/List/2': mockListsWithModel[1], + 'gid://gitlab/List/1': mockListsWithModel[0], + }, error: undefined, }; - mutations.UPDATE_LIST_FAILURE(state, mockListsWithModel); + mutations.UPDATE_LIST_FAILURE(state, initialBoardListsState); - expect(state.boardLists).toEqual(mockListsWithModel); + expect(state.boardLists).toEqual(initialBoardListsState); expect(state.error).toEqual('An error occurred while updating the list. Please try again.'); }); }); @@ -145,6 +168,23 @@ describe('Board Store Mutations', () => { expectNotImplemented(mutations.RECEIVE_REMOVE_LIST_ERROR); }); + describe('RESET_ISSUES', () => { + it('should remove issues from issuesByListId state', () => { + const issuesByListId = { + 'gid://gitlab/List/1': [mockIssue.id], + }; + + state = { + ...state, + issuesByListId, + }; + + mutations[types.RESET_ISSUES](state); + + expect(state.issuesByListId).toEqual({ 'gid://gitlab/List/1': [] }); + }); + }); + describe('RECEIVE_ISSUES_FOR_LIST_SUCCESS', () => { it('updates issuesByListId and issues on state', () => { const listIssues = { @@ -156,14 +196,23 @@ describe('Board Store Mutations', () => { state = { ...state, - isLoadingIssues: true, - issuesByListId: {}, + issuesByListId: { + 'gid://gitlab/List/1': [], + }, issues: {}, - boardLists: mockListsWithModel, + boardLists: initialBoardListsState, + }; + + const listPageInfo = { + 'gid://gitlab/List/1': { + endCursor: '', + hasNextPage: false, + }, }; mutations.RECEIVE_ISSUES_FOR_LIST_SUCCESS(state, { listIssues: { listData: listIssues, issues }, + listPageInfo, listId: 'gid://gitlab/List/1', }); @@ -172,21 +221,11 @@ describe('Board Store Mutations', () => { }); }); - describe('REQUEST_ISSUES_FOR_ALL_LISTS', () => { - it('sets isLoadingIssues to true', () => { - expect(state.isLoadingIssues).toBe(false); - - mutations.REQUEST_ISSUES_FOR_ALL_LISTS(state); - - expect(state.isLoadingIssues).toBe(true); - }); - }); - describe('RECEIVE_ISSUES_FOR_LIST_FAILURE', () => { it('sets error message', () => { state = { ...state, - boardLists: mockListsWithModel, + boardLists: initialBoardListsState, error: undefined, }; @@ -200,51 +239,10 @@ describe('Board Store Mutations', () => { }); }); - describe('RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS', () => { - it('sets isLoadingIssues to false and updates issuesByListId object', () => { - const listIssues = { - 'gid://gitlab/List/1': [mockIssue.id], - }; - const issues = { - '1': mockIssue, - }; - - state = { - ...state, - isLoadingIssues: true, - issuesByListId: {}, - issues: {}, - }; - - 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); - }); - }); - describe('REQUEST_ADD_ISSUE', () => { 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'; @@ -254,7 +252,6 @@ describe('Board Store Mutations', () => { beforeEach(() => { state = { ...state, - isLoadingIssues: true, error: undefined, issues: { ...issue, @@ -310,7 +307,7 @@ describe('Board Store Mutations', () => { state = { ...state, issuesByListId: listIssues, - boardLists: mockListsWithModel, + boardLists: initialBoardListsState, issues, }; @@ -358,6 +355,7 @@ describe('Board Store Mutations', () => { state = { ...state, issuesByListId: listIssues, + boardLists: initialBoardListsState, }; mutations.MOVE_ISSUE_FAILURE(state, { @@ -425,6 +423,7 @@ describe('Board Store Mutations', () => { ...state, issuesByListId: listIssues, issues, + boardLists: initialBoardListsState, }; mutations.ADD_ISSUE_TO_LIST_FAILURE(state, { list: mockLists[0], issue: mockIssue2 }); |