diff options
Diffstat (limited to 'spec/frontend/boards')
16 files changed, 893 insertions, 455 deletions
diff --git a/spec/frontend/boards/board_list_new_spec.js b/spec/frontend/boards/board_list_new_spec.js index 55516e3fd56..96b03ed927e 100644 --- a/spec/frontend/boards/board_list_new_spec.js +++ b/spec/frontend/boards/board_list_new_spec.js @@ -1,15 +1,11 @@ -/* 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 { mockList, mockIssuesByListId, issues, mockIssues } from './mock_data'; import defaultState from '~/boards/stores/state'; const localVue = createLocalVue(); @@ -46,13 +42,11 @@ const createComponent = ({ ...state, }); - const list = new List({ - ...listObj, - id: 'gid://gitlab/List/1', + const list = { + ...mockList, ...listProps, - doNotFetchIssues: true, - }); - const issue = new ListIssue({ + }; + const issue = { title: 'Testing', id: 1, iid: 1, @@ -60,9 +54,9 @@ const createComponent = ({ labels: [], assignees: [], ...listIssueProps, - }); - if (!Object.prototype.hasOwnProperty.call(listProps, 'issuesSize')) { - list.issuesSize = 1; + }; + if (!Object.prototype.hasOwnProperty.call(listProps, 'issuesCount')) { + list.issuesCount = 1; } const component = mount(BoardList, { @@ -71,6 +65,7 @@ const createComponent = ({ disabled: false, list, issues: [issue], + canAdminList: true, ...componentProps, }, store, @@ -87,17 +82,19 @@ const createComponent = ({ describe('Board list component', () => { let wrapper; + const findByTestId = testId => wrapper.find(`[data-testid="${testId}"]`); useFakeRequestAnimationFrame(); + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + describe('When Expanded', () => { beforeEach(() => { wrapper = createComponent(); }); - afterEach(() => { - wrapper.destroy(); - }); - it('renders component', () => { expect(wrapper.find('.board-list-component').exists()).toBe(true); }); @@ -107,7 +104,7 @@ describe('Board list component', () => { state: { listsFlags: { 'gid://gitlab/List/1': { isLoading: true } } }, }); - expect(wrapper.find('[data-testid="board_list_loading"').exists()).toBe(true); + expect(findByTestId('board_list_loading').exists()).toBe(true); }); it('renders issues', () => { @@ -157,7 +154,7 @@ describe('Board list component', () => { it('shows how many more issues to load', async () => { wrapper.vm.showCount = true; - wrapper.setProps({ list: { issuesSize: 20 } }); + wrapper.setProps({ list: { issuesCount: 20 } }); await wrapper.vm.$nextTick(); expect(wrapper.find('.board-list-count').text()).toBe('Showing 1 of 20 issues'); @@ -167,30 +164,30 @@ describe('Board list component', () => { describe('load more issues', () => { beforeEach(() => { wrapper = createComponent({ - listProps: { issuesSize: 25 }, + listProps: { issuesCount: 25 }, }); }); - afterEach(() => { - wrapper.destroy(); - }); - it('loads more issues after scrolling', () => { - wrapper.vm.$refs.list.dispatchEvent(new Event('scroll')); + wrapper.vm.listRef.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')); + wrapper = createComponent({ + state: { listsFlags: { 'gid://gitlab/List/1': { isLoadingMore: true } } }, + }); + wrapper.vm.listRef.dispatchEvent(new Event('scroll')); - expect(actions.fetchIssuesForList).toHaveBeenCalledTimes(1); + expect(actions.fetchIssuesForList).not.toHaveBeenCalled(); }); it('shows loading more spinner', async () => { + wrapper = createComponent({ + state: { listsFlags: { 'gid://gitlab/List/1': { isLoadingMore: true } } }, + }); wrapper.vm.showCount = true; - wrapper.vm.list.loadingMore = true; await wrapper.vm.$nextTick(); expect(wrapper.find('.board-list-count .gl-spinner').exists()).toBe(true); @@ -200,17 +197,13 @@ describe('Board list component', () => { describe('max issue count warning', () => { beforeEach(() => { wrapper = createComponent({ - listProps: { issuesSize: 50 }, + listProps: { issuesCount: 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 } }); + wrapper.setProps({ list: { issuesCount: 4, maxIssueCount: 3 } }); await wrapper.vm.$nextTick(); expect(wrapper.find('.bg-danger-100').exists()).toBe(true); @@ -219,7 +212,7 @@ describe('Board list component', () => { 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 } }); + wrapper.setProps({ list: { issuesCount: 2, maxIssueCount: 3 } }); expect(wrapper.find('.bg-danger-100').exists()).toBe(false); }); @@ -233,4 +226,43 @@ describe('Board list component', () => { }); }); }); + + describe('drag & drop issue', () => { + beforeEach(() => { + wrapper = createComponent(); + }); + + describe('handleDragOnStart', () => { + it('adds a class `is-dragging` to document body', () => { + expect(document.body.classList.contains('is-dragging')).toBe(false); + + findByTestId('tree-root-wrapper').vm.$emit('start'); + + expect(document.body.classList.contains('is-dragging')).toBe(true); + }); + }); + + describe('handleDragOnEnd', () => { + it('removes class `is-dragging` from document body', () => { + jest.spyOn(wrapper.vm, 'moveIssue').mockImplementation(() => {}); + document.body.classList.add('is-dragging'); + + findByTestId('tree-root-wrapper').vm.$emit('end', { + oldIndex: 1, + newIndex: 0, + item: { + dataset: { + issueId: mockIssues[0].id, + issueIid: mockIssues[0].iid, + issuePath: mockIssues[0].referencePath, + }, + }, + to: { children: [], dataset: { listId: 'gid://gitlab/List/1' } }, + from: { dataset: { listId: 'gid://gitlab/List/2' } }, + }); + + expect(document.body.classList.contains('is-dragging')).toBe(false); + }); + }); + }); }); diff --git a/spec/frontend/boards/boards_store_spec.js b/spec/frontend/boards/boards_store_spec.js index e7c1cf79fdc..c89f6d22ef2 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, { gqlClient } from '~/boards/stores/boards_store'; +import boardsStore from '~/boards/stores/boards_store'; import eventHub from '~/boards/eventhub'; import { listObj, listObjDuplicate } from './mock_data'; @@ -66,23 +66,6 @@ describe('boardsStore', () => { }); }); - describe('generateDefaultLists', () => { - const listsEndpointGenerate = `${endpoints.listsEndpoint}/generate.json`; - - it('makes a request to generate default lists', () => { - axiosMock.onPost(listsEndpointGenerate).replyOnce(200, dummyResponse); - const expectedResponse = expect.objectContaining({ data: dummyResponse }); - - return expect(boardsStore.generateDefaultLists()).resolves.toEqual(expectedResponse); - }); - - it('fails for error response', () => { - axiosMock.onPost(listsEndpointGenerate).replyOnce(500); - - return expect(boardsStore.generateDefaultLists()).rejects.toThrow(); - }); - }); - describe('createList', () => { const entityType = 'moorhen'; const entityId = 'quack'; @@ -473,118 +456,6 @@ describe('boardsStore', () => { }); }); - describe('createBoard', () => { - const labelIds = ['first label', 'second label']; - const assigneeId = 'as sign ee'; - const milestoneId = 'vegetable soup'; - const board = { - labels: labelIds.map(id => ({ id })), - assignee: { id: assigneeId }, - milestone: { id: milestoneId }, - }; - - describe('for existing board', () => { - const id = 'skate-board'; - const url = `${endpoints.boardsEndpoint}/${id}.json`; - const expectedRequest = expect.objectContaining({ - data: JSON.stringify({ - board: { - ...board, - id, - label_ids: labelIds, - assignee_id: assigneeId, - milestone_id: milestoneId, - }, - }), - }); - - let requestSpy; - - 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 }), - expect.objectContaining({}), - ]; - - return expect( - boardsStore.createBoard({ - ...board, - id, - }), - ) - .resolves.toEqual(expectedResponse) - .then(() => { - expect(requestSpy).toHaveBeenCalledWith(expectedRequest); - }); - }); - - it('fails for error response', () => { - requestSpy.mockReturnValue([500]); - - return expect( - boardsStore.createBoard({ - ...board, - id, - }), - ) - .rejects.toThrow() - .then(() => { - expect(requestSpy).toHaveBeenCalledWith(expectedRequest); - }); - }); - }); - - describe('for new board', () => { - const url = `${endpoints.boardsEndpoint}.json`; - const expectedRequest = expect.objectContaining({ - data: JSON.stringify({ - board: { - ...board, - label_ids: labelIds, - assignee_id: assigneeId, - milestone_id: milestoneId, - }, - }), - }); - - let requestSpy; - - 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 = dummyResponse; - - return expect(boardsStore.createBoard(board)) - .resolves.toEqual(expectedResponse) - .then(() => { - expect(requestSpy).toHaveBeenCalledWith(expectedRequest); - }); - }); - - it('fails for error response', () => { - requestSpy.mockReturnValue([500]); - - return expect(boardsStore.createBoard(board)) - .rejects.toThrow() - .then(() => { - expect(requestSpy).toHaveBeenCalledWith(expectedRequest); - }); - }); - }); - }); - describe('deleteBoard', () => { const id = 'capsized'; const url = `${endpoints.boardsEndpoint}/${id}.json`; @@ -727,24 +598,6 @@ describe('boardsStore', () => { }); }); - it('check for blank state adding', () => { - expect(boardsStore.shouldAddBlankState()).toBe(true); - }); - - it('check for blank state not adding', () => { - boardsStore.addList(listObj); - - expect(boardsStore.shouldAddBlankState()).toBe(false); - }); - - it('check for blank state adding when closed list exist', () => { - boardsStore.addList({ - list_type: 'closed', - }); - - expect(boardsStore.shouldAddBlankState()).toBe(true); - }); - it('removes list from state', () => { boardsStore.addList(listObj); diff --git a/spec/frontend/boards/components/board_assignee_dropdown_spec.js b/spec/frontend/boards/components/board_assignee_dropdown_spec.js index e185a6d5419..bbdcc707f09 100644 --- a/spec/frontend/boards/components/board_assignee_dropdown_spec.js +++ b/spec/frontend/boards/components/board_assignee_dropdown_spec.js @@ -1,5 +1,11 @@ import { mount, createLocalVue } from '@vue/test-utils'; -import { GlDropdownItem, GlAvatarLink, GlAvatarLabeled, GlSearchBoxByType } from '@gitlab/ui'; +import { + GlDropdownItem, + GlAvatarLink, + GlAvatarLabeled, + GlSearchBoxByType, + GlLoadingIcon, +} from '@gitlab/ui'; import createMockApollo from 'jest/helpers/mock_apollo_helper'; import VueApollo from 'vue-apollo'; import BoardAssigneeDropdown from '~/boards/components/board_assignee_dropdown.vue'; @@ -8,7 +14,7 @@ import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dro import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue'; import store from '~/boards/stores'; import getIssueParticipants from '~/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql'; -import searchUsers from '~/boards/queries/users_search.query.graphql'; +import searchUsers from '~/boards/graphql/users_search.query.graphql'; import { participants } from '../mock_data'; const localVue = createLocalVue(); @@ -20,17 +26,18 @@ describe('BoardCardAssigneeDropdown', () => { let fakeApollo; let getIssueParticipantsSpy; let getSearchUsersSpy; + let dispatchSpy; const iid = '111'; const activeIssueName = 'test'; const anotherIssueName = 'hello'; - const createComponent = (search = '') => { + const createComponent = (search = '', loading = false) => { wrapper = mount(BoardAssigneeDropdown, { data() { return { search, - selected: store.getters.activeIssue.assignees, + selected: [], participants, }; }, @@ -39,6 +46,15 @@ describe('BoardCardAssigneeDropdown', () => { canUpdate: true, rootPath: '', }, + mocks: { + $apollo: { + queries: { + participants: { + loading, + }, + }, + }, + }, }); }; @@ -47,14 +63,13 @@ describe('BoardCardAssigneeDropdown', () => { [getIssueParticipants, getIssueParticipantsSpy], [searchUsers, getSearchUsersSpy], ]); - wrapper = mount(BoardAssigneeDropdown, { localVue, apolloProvider: fakeApollo, data() { return { search, - selected: store.getters.activeIssue.assignees, + selected: [], participants, }; }, @@ -82,6 +97,8 @@ describe('BoardCardAssigneeDropdown', () => { return wrapper.findAll(GlDropdownItem).wrappers.find(node => node.text().indexOf(text) === 0); }; + const findLoadingIcon = () => wrapper.find(GlLoadingIcon); + beforeEach(() => { store.state.activeId = '1'; store.state.issues = { @@ -91,10 +108,11 @@ describe('BoardCardAssigneeDropdown', () => { }, }; - jest.spyOn(store, 'dispatch').mockResolvedValue(); + dispatchSpy = jest.spyOn(store, 'dispatch').mockResolvedValue(); }); afterEach(() => { + window.gon = {}; jest.restoreAllMocks(); }); @@ -243,6 +261,30 @@ describe('BoardCardAssigneeDropdown', () => { }, ); + describe('when participants is loading', () => { + beforeEach(() => { + createComponent('', true); + }); + + it('finds a loading icon in the dropdown', () => { + expect(findLoadingIcon().exists()).toBe(true); + }); + }); + + describe('when participants is loading is false', () => { + beforeEach(() => { + createComponent(); + }); + + it('does not find GlLoading icon in the dropdown', () => { + expect(findLoadingIcon().exists()).toBe(false); + }); + + it('finds at least 1 GlDropdownItem', () => { + expect(wrapper.findAll(GlDropdownItem).length).toBeGreaterThan(0); + }); + }); + describe('Apollo', () => { beforeEach(() => { getIssueParticipantsSpy = jest.fn().mockResolvedValue({ @@ -305,4 +347,39 @@ describe('BoardCardAssigneeDropdown', () => { expect(wrapper.find(GlSearchBoxByType).exists()).toBe(true); }); + + describe('when assign-self is emitted from IssuableAssignees', () => { + const currentUser = { username: 'self', name: '', id: '' }; + + beforeEach(() => { + window.gon = { current_username: currentUser.username }; + + dispatchSpy.mockResolvedValue([currentUser]); + createComponent(); + + wrapper.find(IssuableAssignees).vm.$emit('assign-self'); + }); + + it('calls setAssignees with currentUser', () => { + expect(store.dispatch).toHaveBeenCalledWith('setAssignees', currentUser.username); + }); + + it('adds the user to the selected list', async () => { + expect(findByText(currentUser.username).exists()).toBe(true); + }); + }); + + describe('when setting an assignee', () => { + beforeEach(() => { + createComponent(); + }); + + it('passes loading state from Vuex to BoardEditableItem', async () => { + store.state.isSettingAssignees = true; + + await wrapper.vm.$nextTick(); + + expect(wrapper.find(BoardEditableItem).props('loading')).toBe(true); + }); + }); }); diff --git a/spec/frontend/boards/components/board_column_new_spec.js b/spec/frontend/boards/components/board_column_new_spec.js index 4aafc3a867a..81c0e60f931 100644 --- a/spec/frontend/boards/components/board_column_new_spec.js +++ b/spec/frontend/boards/components/board_column_new_spec.js @@ -2,7 +2,6 @@ import { shallowMount } from '@vue/test-utils'; import { listObj } from 'jest/boards/mock_data'; import BoardColumn from '~/boards/components/board_column_new.vue'; -import List from '~/boards/models/list'; import { ListType } from '~/boards/constants'; import { createStore } from '~/boards/stores'; @@ -20,24 +19,22 @@ describe('Board Column Component', () => { const listMock = { ...listObj, - list_type: listType, + listType, collapsed, }; if (listType === ListType.assignee) { delete listMock.label; - listMock.user = {}; + listMock.assignee = {}; } - const list = new List({ ...listMock, doNotFetchIssues: true }); - store = createStore(); wrapper = shallowMount(BoardColumn, { store, propsData: { disabled: false, - list, + list: listMock, }, provide: { boardId, @@ -60,7 +57,7 @@ describe('Board Column Component', () => { it('has class is-collapsed when list is collapsed', () => { createComponent({ collapsed: false }); - expect(wrapper.vm.list.isExpanded).toBe(true); + expect(isCollapsed()).toBe(false); }); it('does not have class is-collapsed when list is expanded', () => { diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js index 09e38001e2e..291013c561e 100644 --- a/spec/frontend/boards/components/board_content_spec.js +++ b/spec/frontend/boards/components/board_content_spec.js @@ -1,32 +1,38 @@ import Vuex from 'vuex'; import { createLocalVue, shallowMount } from '@vue/test-utils'; import { GlAlert } from '@gitlab/ui'; +import Draggable from 'vuedraggable'; 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 BoardColumn from '~/boards/components/board_column.vue'; +import { mockLists, mockListsWithModel } from '../mock_data'; import BoardContent from '~/boards/components/board_content.vue'; const localVue = createLocalVue(); localVue.use(Vuex); +const actions = { + moveList: jest.fn(), +}; + describe('BoardContent', () => { let wrapper; const defaultState = { isShowingEpicsSwimlanes: false, - boardLists: mockListsWithModel, + boardLists: mockLists, error: undefined, }; const createStore = (state = defaultState) => { return new Vuex.Store({ + actions, getters, state, }); }; - const createComponent = state => { + const createComponent = ({ state, props = {}, graphqlBoardListsEnabled = false } = {}) => { const store = createStore({ ...defaultState, ...state, @@ -37,25 +43,61 @@ describe('BoardContent', () => { lists: mockListsWithModel, canAdminList: true, disabled: false, + ...props, + }, + provide: { + glFeatures: { graphqlBoardLists: graphqlBoardListsEnabled }, }, store, }); }; - beforeEach(() => { - createComponent(); - }); - afterEach(() => { wrapper.destroy(); }); it('renders a BoardColumn component per list', () => { - expect(wrapper.findAll(BoardColumn)).toHaveLength(mockListsWithModel.length); + createComponent(); + + expect(wrapper.findAll(BoardColumn)).toHaveLength(mockLists.length); }); it('does not display EpicsSwimlanes component', () => { + createComponent(); + expect(wrapper.find(EpicsSwimlanes).exists()).toBe(false); expect(wrapper.find(GlAlert).exists()).toBe(false); }); + + describe('graphqlBoardLists feature flag enabled', () => { + describe('can admin list', () => { + beforeEach(() => { + createComponent({ graphqlBoardListsEnabled: true, props: { canAdminList: true } }); + }); + + it('renders draggable component', () => { + expect(wrapper.find(Draggable).exists()).toBe(true); + }); + }); + + describe('can not admin list', () => { + beforeEach(() => { + createComponent({ graphqlBoardListsEnabled: true, props: { canAdminList: false } }); + }); + + it('renders draggable component', () => { + expect(wrapper.find(Draggable).exists()).toBe(false); + }); + }); + }); + + describe('graphqlBoardLists feature flag disabled', () => { + beforeEach(() => { + createComponent({ graphqlBoardListsEnabled: false }); + }); + + it('does not render draggable component', () => { + expect(wrapper.find(Draggable).exists()).toBe(false); + }); + }); }); diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js index 65d8070192c..3b15cbb6b7e 100644 --- a/spec/frontend/boards/components/board_form_spec.js +++ b/spec/frontend/boards/components/board_form_spec.js @@ -1,47 +1,275 @@ -import { mount } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; +import AxiosMockAdapter from 'axios-mock-adapter'; import { TEST_HOST } from 'jest/helpers/test_constants'; +import { GlModal } from '@gitlab/ui'; +import waitForPromises from 'helpers/wait_for_promises'; + +import axios from '~/lib/utils/axios_utils'; +import { visitUrl } from '~/lib/utils/url_utility'; import boardsStore from '~/boards/stores/boards_store'; -import boardForm from '~/boards/components/board_form.vue'; -import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; +import BoardForm from '~/boards/components/board_form.vue'; +import BoardConfigurationOptions from '~/boards/components/board_configuration_options.vue'; +import createBoardMutation from '~/boards/graphql/board.mutation.graphql'; + +jest.mock('~/lib/utils/url_utility', () => ({ + visitUrl: jest.fn().mockName('visitUrlMock'), +})); + +const currentBoard = { + id: 1, + name: 'test', + labels: [], + milestone_id: undefined, + assignee: {}, + assignee_id: undefined, + weight: null, + hide_backlog_list: false, + hide_closed_list: false, +}; + +const boardDefaults = { + id: false, + name: '', + labels: [], + milestone_id: undefined, + assignee: {}, + assignee_id: undefined, + weight: null, + hide_backlog_list: false, + hide_closed_list: false, +}; + +const defaultProps = { + canAdminBoard: false, + labelsPath: `${TEST_HOST}/labels/path`, + labelsWebUrl: `${TEST_HOST}/-/labels`, + currentBoard, +}; -describe('board_form.vue', () => { +const endpoints = { + boardsEndpoint: 'test-endpoint', +}; + +const mutate = jest.fn().mockResolvedValue({}); + +describe('BoardForm', () => { let wrapper; + let axiosMock; - const propsData = { - canAdminBoard: false, - labelsPath: `${TEST_HOST}/labels/path`, - labelsWebUrl: `${TEST_HOST}/-/labels`, - }; + const findModal = () => wrapper.find(GlModal); + const findModalActionPrimary = () => findModal().props('actionPrimary'); + const findForm = () => wrapper.find('[data-testid="board-form"]'); + const findFormWrapper = () => wrapper.find('[data-testid="board-form-wrapper"]'); + const findDeleteConfirmation = () => wrapper.find('[data-testid="delete-confirmation-message"]'); + const findConfigurationOptions = () => wrapper.find(BoardConfigurationOptions); + const findInput = () => wrapper.find('#board-new-name'); - const findModal = () => wrapper.find(DeprecatedModal); + const createComponent = (props, data) => { + wrapper = shallowMount(BoardForm, { + propsData: { ...defaultProps, ...props }, + data() { + return { + ...data, + }; + }, + provide: { + endpoints, + }, + mocks: { + $apollo: { + mutate, + }, + }, + attachToDocument: true, + }); + }; beforeEach(() => { - boardsStore.state.currentPage = 'edit'; - wrapper = mount(boardForm, { propsData }); + axiosMock = new AxiosMockAdapter(axios); }); afterEach(() => { wrapper.destroy(); wrapper = null; + axiosMock.restore(); + boardsStore.state.currentPage = null; }); - describe('methods', () => { - describe('cancel', () => { - it('resets currentPage', () => { - wrapper.vm.cancel(); - expect(boardsStore.state.currentPage).toBe(''); + describe('when user can not admin the board', () => { + beforeEach(() => { + boardsStore.state.currentPage = 'new'; + createComponent(); + }); + + it('hides modal footer when user is not a board admin', () => { + expect(findModal().attributes('hide-footer')).toBeDefined(); + }); + + it('displays board scope title', () => { + expect(findModal().attributes('title')).toBe('Board scope'); + }); + + it('does not display a form', () => { + expect(findForm().exists()).toBe(false); + }); + }); + + describe('when user can admin the board', () => { + beforeEach(() => { + boardsStore.state.currentPage = 'new'; + createComponent({ canAdminBoard: true }); + }); + + it('shows modal footer when user is a board admin', () => { + expect(findModal().attributes('hide-footer')).toBeUndefined(); + }); + + it('displays a form', () => { + expect(findForm().exists()).toBe(true); + }); + + it('focuses an input field', async () => { + expect(document.activeElement).toBe(wrapper.vm.$refs.name); + }); + }); + + describe('when creating a new board', () => { + beforeEach(() => { + boardsStore.state.currentPage = 'new'; + }); + + describe('on non-scoped-board', () => { + beforeEach(() => { + createComponent({ canAdminBoard: true }); + }); + + it('clears the form', () => { + expect(findConfigurationOptions().props('board')).toEqual(boardDefaults); + }); + + it('shows a correct title about creating a board', () => { + expect(findModal().attributes('title')).toBe('Create new board'); + }); + + it('passes correct primary action text and variant', () => { + expect(findModalActionPrimary().text).toBe('Create board'); + expect(findModalActionPrimary().attributes[0].variant).toBe('success'); + }); + + it('does not render delete confirmation message', () => { + expect(findDeleteConfirmation().exists()).toBe(false); + }); + + it('renders form wrapper', () => { + expect(findFormWrapper().exists()).toBe(true); + }); + + it('passes a true isNewForm prop to BoardConfigurationOptions component', () => { + expect(findConfigurationOptions().props('isNewForm')).toBe(true); + }); + }); + + describe('when submitting a create event', () => { + beforeEach(() => { + const url = `${endpoints.boardsEndpoint}.json`; + axiosMock.onPost(url).reply(200, { id: '2', board_path: 'new path' }); + }); + + it('does not call API if board name is empty', async () => { + createComponent({ canAdminBoard: true }); + findInput().trigger('keyup.enter', { metaKey: true }); + + await waitForPromises(); + + expect(mutate).not.toHaveBeenCalled(); + }); + + it('calls REST and GraphQL API and redirects to correct page', async () => { + createComponent({ canAdminBoard: true }); + + findInput().value = 'Test name'; + findInput().trigger('input'); + findInput().trigger('keyup.enter', { metaKey: true }); + + await waitForPromises(); + + expect(axiosMock.history.post[0].data).toBe( + JSON.stringify({ board: { ...boardDefaults, name: 'test', label_ids: [''] } }), + ); + + expect(mutate).toHaveBeenCalledWith({ + mutation: createBoardMutation, + variables: { + id: 'gid://gitlab/Board/2', + }, + }); + + await waitForPromises(); + expect(visitUrl).toHaveBeenCalledWith('new path'); }); }); }); - describe('buttons', () => { - it('cancel button triggers cancel()', () => { - wrapper.setMethods({ cancel: jest.fn() }); - findModal().vm.$emit('cancel'); + describe('when editing a board', () => { + beforeEach(() => { + boardsStore.state.currentPage = 'edit'; + }); + + describe('on non-scoped-board', () => { + beforeEach(() => { + createComponent({ canAdminBoard: true }); + }); + + it('clears the form', () => { + expect(findConfigurationOptions().props('board')).toEqual(currentBoard); + }); + + it('shows a correct title about creating a board', () => { + expect(findModal().attributes('title')).toBe('Edit board'); + }); + + it('passes correct primary action text and variant', () => { + expect(findModalActionPrimary().text).toBe('Save changes'); + expect(findModalActionPrimary().attributes[0].variant).toBe('info'); + }); + + it('does not render delete confirmation message', () => { + expect(findDeleteConfirmation().exists()).toBe(false); + }); + + it('renders form wrapper', () => { + expect(findFormWrapper().exists()).toBe(true); + }); + + it('passes a false isNewForm prop to BoardConfigurationOptions component', () => { + expect(findConfigurationOptions().props('isNewForm')).toBe(false); + }); + }); + + describe('when submitting an update event', () => { + beforeEach(() => { + const url = endpoints.boardsEndpoint; + axiosMock.onPut(url).reply(200, { board_path: 'new path' }); + }); + + it('calls REST and GraphQL API with correct parameters', async () => { + createComponent({ canAdminBoard: true }); + + findInput().trigger('keyup.enter', { metaKey: true }); + + await waitForPromises(); + + expect(axiosMock.history.put[0].data).toBe( + JSON.stringify({ board: { ...currentBoard, label_ids: [''] } }), + ); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.cancel).toHaveBeenCalled(); + expect(mutate).toHaveBeenCalledWith({ + mutation: createBoardMutation, + variables: { + id: `gid://gitlab/Board/${currentBoard.id}`, + }, + }); }); }); }); diff --git a/spec/frontend/boards/components/board_list_header_new_spec.js b/spec/frontend/boards/components/board_list_header_new_spec.js index 80786d82620..7428dfae83f 100644 --- a/spec/frontend/boards/components/board_list_header_new_spec.js +++ b/spec/frontend/boards/components/board_list_header_new_spec.js @@ -1,9 +1,8 @@ import Vuex from 'vuex'; import { shallowMount, createLocalVue } from '@vue/test-utils'; -import { listObj } from 'jest/boards/mock_data'; +import { mockLabelList } from 'jest/boards/mock_data'; import BoardListHeader from '~/boards/components/board_list_header_new.vue'; -import List from '~/boards/models/list'; import { ListType } from '~/boards/constants'; const localVue = createLocalVue(); @@ -32,21 +31,19 @@ describe('Board List Header Component', () => { const boardId = '1'; const listMock = { - ...listObj, - list_type: listType, + ...mockLabelList, + listType, collapsed, }; if (listType === ListType.assignee) { delete listMock.label; - listMock.user = {}; + listMock.assignee = {}; } - const list = new List({ ...listMock, doNotFetchIssues: true }); - if (withLocalStorage) { localStorage.setItem( - `boards.${boardId}.${list.type}.${list.id}.expanded`, + `boards.${boardId}.${listMock.listType}.${listMock.id}.expanded`, (!collapsed).toString(), ); } @@ -62,7 +59,7 @@ describe('Board List Header Component', () => { localVue, propsData: { disabled: false, - list, + list: listMock, }, provide: { boardId, @@ -72,14 +69,15 @@ describe('Board List Header Component', () => { }); }; - const isExpanded = () => wrapper.vm.list.isExpanded; - const isCollapsed = () => !isExpanded(); + const isCollapsed = () => wrapper.vm.list.collapsed; + const isExpanded = () => !isCollapsed; const findAddIssueButton = () => wrapper.find({ ref: 'newIssueBtn' }); + const findTitle = () => wrapper.find('.board-title'); const findCaret = () => wrapper.find('.board-title-caret'); describe('Add issue button', () => { - const hasNoAddButton = [ListType.promotion, ListType.blank, ListType.closed]; + const hasNoAddButton = [ListType.closed]; const hasAddButton = [ListType.backlog, ListType.label, ListType.milestone, ListType.assignee]; it.each(hasNoAddButton)('does not render when List Type is `%s`', listType => { @@ -125,7 +123,7 @@ describe('Board List Header Component', () => { it('collapses expanded Column when clicking the collapse icon', async () => { createComponent(); - expect(isExpanded()).toBe(true); + expect(isCollapsed()).toBe(false); findCaret().vm.$emit('click'); @@ -166,4 +164,24 @@ describe('Board List Header Component', () => { expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(String(isExpanded())); }); }); + + describe('user can drag', () => { + const cannotDragList = [ListType.backlog, ListType.closed]; + const canDragList = [ListType.label, ListType.milestone, ListType.assignee]; + + it.each(cannotDragList)( + 'does not have user-can-drag-class so user cannot drag list', + listType => { + createComponent({ listType }); + + expect(findTitle().classes()).not.toContain('user-can-drag'); + }, + ); + + it.each(canDragList)('has user-can-drag-class so user can drag list', listType => { + createComponent({ listType }); + + expect(findTitle().classes()).toContain('user-can-drag'); + }); + }); }); diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js index 2439c347bf0..656a503bb86 100644 --- a/spec/frontend/boards/components/board_list_header_spec.js +++ b/spec/frontend/boards/components/board_list_header_spec.js @@ -73,7 +73,7 @@ describe('Board List Header Component', () => { const findCaret = () => wrapper.find('.board-title-caret'); describe('Add issue button', () => { - const hasNoAddButton = [ListType.promotion, ListType.blank, ListType.closed]; + const hasNoAddButton = [ListType.closed]; const hasAddButton = [ListType.backlog, ListType.label, ListType.milestone, ListType.assignee]; it.each(hasNoAddButton)('does not render when List Type is `%s`', listType => { diff --git a/spec/frontend/boards/components/board_new_issue_new_spec.js b/spec/frontend/boards/components/board_new_issue_new_spec.js index af4bad65121..ee1c4f31cf0 100644 --- a/spec/frontend/boards/components/board_new_issue_new_spec.js +++ b/spec/frontend/boards/components/board_new_issue_new_spec.js @@ -3,7 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import BoardNewIssue from '~/boards/components/board_new_issue_new.vue'; import '~/boards/models/list'; -import { mockListsWithModel } from '../mock_data'; +import { mockList } from '../mock_data'; const localVue = createLocalVue(); @@ -37,7 +37,7 @@ describe('Issue boards new issue form', () => { wrapper = shallowMount(BoardNewIssue, { propsData: { disabled: false, - list: mockListsWithModel[0], + list: mockList, }, store, localVue, diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js index 2b7605a3f7c..db3c8c22950 100644 --- a/spec/frontend/boards/components/boards_selector_spec.js +++ b/spec/frontend/boards/components/boards_selector_spec.js @@ -1,6 +1,6 @@ import { nextTick } from 'vue'; import { mount } from '@vue/test-utils'; -import { GlDeprecatedDropdown, GlLoadingIcon } from '@gitlab/ui'; +import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui'; import { TEST_HOST } from 'spec/test_constants'; import BoardsSelector from '~/boards/components/boards_selector.vue'; import boardsStore from '~/boards/stores/boards_store'; @@ -34,8 +34,9 @@ describe('BoardsSelector', () => { }; const getDropdownItems = () => wrapper.findAll('.js-dropdown-item'); - const getDropdownHeaders = () => wrapper.findAll('.dropdown-bold-header'); + const getDropdownHeaders = () => wrapper.findAll(GlDropdownSectionHeader); const getLoadingIcon = () => wrapper.find(GlLoadingIcon); + const findDropdown = () => wrapper.find(GlDropdown); beforeEach(() => { const $apollo = { @@ -103,7 +104,7 @@ describe('BoardsSelector', () => { }); // Emits gl-dropdown show event to simulate the dropdown is opened at initialization time - wrapper.find(GlDeprecatedDropdown).vm.$emit('show'); + findDropdown().vm.$emit('show'); }); afterEach(() => { @@ -125,7 +126,10 @@ describe('BoardsSelector', () => { }); describe('loaded', () => { - beforeEach(() => { + beforeEach(async () => { + await wrapper.setData({ + loadingBoards: false, + }); return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick()); }); diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js new file mode 100644 index 00000000000..74d88d9f34c --- /dev/null +++ b/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js @@ -0,0 +1,152 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlLoadingIcon } from '@gitlab/ui'; +import { mockMilestone as TEST_MILESTONE } from 'jest/boards/mock_data'; +import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue'; +import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue'; +import { createStore } from '~/boards/stores'; +import createFlash from '~/flash'; + +const TEST_ISSUE = { id: 'gid://gitlab/Issue/1', iid: 9, referencePath: 'h/b#2' }; + +jest.mock('~/flash'); + +describe('~/boards/components/sidebar/board_sidebar_milestone_select.vue', () => { + let wrapper; + let store; + + afterEach(() => { + wrapper.destroy(); + store = null; + wrapper = null; + }); + + const createWrapper = ({ milestone = null } = {}) => { + store = createStore(); + store.state.issues = { [TEST_ISSUE.id]: { ...TEST_ISSUE, milestone } }; + store.state.activeId = TEST_ISSUE.id; + + wrapper = shallowMount(BoardSidebarMilestoneSelect, { + store, + provide: { + canUpdate: true, + }, + data: () => ({ + milestones: [TEST_MILESTONE], + }), + stubs: { + 'board-editable-item': BoardEditableItem, + }, + mocks: { + $apollo: { + loading: false, + }, + }, + }); + }; + + const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]'); + const findLoader = () => wrapper.find(GlLoadingIcon); + const findDropdownItem = () => wrapper.find('[data-testid="milestone-item"]'); + const findUnsetMilestoneItem = () => wrapper.find('[data-testid="no-milestone-item"]'); + const findNoMilestonesFoundItem = () => wrapper.find('[data-testid="no-milestones-found"]'); + + it('renders "None" when no milestone is selected', () => { + createWrapper(); + + expect(findCollapsed().text()).toBe('None'); + }); + + it('renders milestone title when set', () => { + createWrapper({ milestone: TEST_MILESTONE }); + + expect(findCollapsed().text()).toContain(TEST_MILESTONE.title); + }); + + it('shows loader while Apollo is loading', async () => { + createWrapper({ milestone: TEST_MILESTONE }); + + expect(findLoader().exists()).toBe(false); + + wrapper.vm.$apollo.loading = true; + await wrapper.vm.$nextTick(); + + expect(findLoader().exists()).toBe(true); + }); + + it('shows message when error or no milestones found', async () => { + createWrapper(); + + wrapper.setData({ milestones: [] }); + await wrapper.vm.$nextTick(); + + expect(findNoMilestonesFoundItem().text()).toBe('No milestones found'); + }); + + describe('when milestone is selected', () => { + beforeEach(async () => { + createWrapper(); + + jest.spyOn(wrapper.vm, 'setActiveIssueMilestone').mockImplementation(() => { + store.state.issues[TEST_ISSUE.id].milestone = TEST_MILESTONE; + }); + findDropdownItem().vm.$emit('click'); + await wrapper.vm.$nextTick(); + }); + + it('collapses sidebar and renders selected milestone', () => { + expect(findCollapsed().isVisible()).toBe(true); + expect(findCollapsed().text()).toContain(TEST_MILESTONE.title); + }); + + it('commits change to the server', () => { + expect(wrapper.vm.setActiveIssueMilestone).toHaveBeenCalledWith({ + milestoneId: TEST_MILESTONE.id, + projectPath: 'h/b', + }); + }); + }); + + describe('when milestone is set to "None"', () => { + beforeEach(async () => { + createWrapper({ milestone: TEST_MILESTONE }); + + jest.spyOn(wrapper.vm, 'setActiveIssueMilestone').mockImplementation(() => { + store.state.issues[TEST_ISSUE.id].milestone = null; + }); + findUnsetMilestoneItem().vm.$emit('click'); + await wrapper.vm.$nextTick(); + }); + + it('collapses sidebar and renders "None"', () => { + expect(findCollapsed().isVisible()).toBe(true); + expect(findCollapsed().text()).toBe('None'); + }); + + it('commits change to the server', () => { + expect(wrapper.vm.setActiveIssueMilestone).toHaveBeenCalledWith({ + milestoneId: null, + projectPath: 'h/b', + }); + }); + }); + + describe('when the mutation fails', () => { + const testMilestone = { id: '1', title: 'Former milestone' }; + + beforeEach(async () => { + createWrapper({ milestone: testMilestone }); + + jest.spyOn(wrapper.vm, 'setActiveIssueMilestone').mockImplementation(() => { + throw new Error(['failed mutation']); + }); + findDropdownItem().vm.$emit('click'); + await wrapper.vm.$nextTick(); + }); + + it('collapses sidebar and renders former milestone', () => { + expect(findCollapsed().isVisible()).toBe(true); + expect(findCollapsed().text()).toContain(testMilestone.title); + expect(createFlash).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/boards/list_spec.js b/spec/frontend/boards/list_spec.js index 9c3a6e66ef4..b731bb6e474 100644 --- a/spec/frontend/boards/list_spec.js +++ b/spec/frontend/boards/list_spec.js @@ -184,7 +184,6 @@ 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 58f67231d55..ea6c52c6830 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -1,10 +1,8 @@ -/* global ListIssue */ /* global List */ import Vue from 'vue'; import { keyBy } from 'lodash'; import '~/boards/models/list'; -import '~/boards/models/issue'; import boardsStore from '~/boards/stores/boards_store'; export const boardObj = { @@ -99,7 +97,7 @@ export const mockMilestone = { due_date: '2019-12-31', }; -const assignees = [ +export const assignees = [ { id: 'gid://gitlab/User/2', username: 'angelina.herman', @@ -184,8 +182,6 @@ export const mockActiveIssue = { emailsDisabled: false, }; -export const mockIssueWithModel = new ListIssue(mockIssue); - export const mockIssue2 = { id: 'gid://gitlab/Issue/437', iid: 28, @@ -203,8 +199,6 @@ export const mockIssue2 = { }, }; -export const mockIssue2WithModel = new ListIssue(mockIssue2); - export const mockIssue3 = { id: 'gid://gitlab/Issue/438', iid: 29, @@ -288,38 +282,39 @@ export const setMockEndpoints = (opts = {}) => { }); }; -export const mockLists = [ - { - id: 'gid://gitlab/List/1', - title: 'Backlog', - position: null, - listType: 'backlog', - collapsed: false, - label: null, - assignee: null, - milestone: null, - loading: false, - issuesSize: 1, - }, - { - id: 'gid://gitlab/List/2', +export const mockList = { + id: 'gid://gitlab/List/1', + title: 'Backlog', + position: null, + listType: 'backlog', + collapsed: false, + label: null, + assignee: null, + milestone: null, + loading: false, + issuesCount: 1, +}; + +export const mockLabelList = { + id: 'gid://gitlab/List/2', + title: 'To Do', + position: 0, + listType: 'label', + collapsed: false, + label: { + id: 'gid://gitlab/GroupLabel/121', 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, - issuesSize: 0, + color: '#F0AD4E', + textColor: '#FFFFFF', + description: null, }, -]; + assignee: null, + milestone: null, + loading: false, + issuesCount: 0, +}; + +export const mockLists = [mockList, mockLabelList]; export const mockListsById = keyBy(mockLists, 'id'); diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index 4d529580a7a..0cae6456887 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -1,23 +1,25 @@ import testAction from 'helpers/vuex_action_helper'; import { - mockListsWithModel, mockLists, mockListsById, mockIssue, - mockIssueWithModel, - mockIssue2WithModel, + mockIssue2, rawIssue, mockIssues, + mockMilestone, labels, mockActiveIssue, } from '../mock_data'; import actions, { gqlClient } from '~/boards/stores/actions'; import * as types from '~/boards/stores/mutation_types'; import { inactiveId } from '~/boards/constants'; -import issueMoveListMutation from '~/boards/queries/issue_move_list.mutation.graphql'; -import destroyBoardListMutation from '~/boards/queries/board_list_destroy.mutation.graphql'; +import issueMoveListMutation from '~/boards/graphql/issue_move_list.mutation.graphql'; +import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql'; import updateAssignees from '~/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql'; import { fullBoardId, formatListIssues, formatBoardLists } from '~/boards/boards_util'; +import createFlash from '~/flash'; + +jest.mock('~/flash'); const expectNotImplemented = action => { it('is not implemented', () => { @@ -29,6 +31,10 @@ const expectNotImplemented = action => { // subgroups when the movIssue action is called. const getProjectPath = path => path.split('#')[0]; +beforeEach(() => { + window.gon = { features: {} }; +}); + describe('setInitialBoardData', () => { it('sets data object', () => { const mockData = { @@ -65,6 +71,24 @@ describe('setFilters', () => { }); }); +describe('performSearch', () => { + it('should dispatch setFilters action', done => { + testAction(actions.performSearch, {}, {}, [], [{ type: 'setFilters', payload: {} }], done); + }); + + it('should dispatch setFilters, fetchLists and resetIssues action when graphqlBoardLists FF is on', done => { + window.gon = { features: { graphqlBoardLists: true } }; + testAction( + actions.performSearch, + {}, + {}, + [], + [{ type: 'setFilters', payload: {} }, { type: 'fetchLists' }, { type: 'resetIssues' }], + done, + ); + }); +}); + describe('setActiveId', () => { it('should commit mutation SET_ACTIVE_ID', done => { const state = { @@ -120,7 +144,7 @@ describe('fetchLists', () => { payload: formattedLists, }, ], - [{ type: 'generateDefaultLists' }], + [], done, ); }); @@ -150,37 +174,12 @@ describe('fetchLists', () => { payload: formattedLists, }, ], - [{ type: 'createList', payload: { backlog: true } }, { type: 'generateDefaultLists' }], + [{ type: 'createList', payload: { backlog: true } }], done, ); }); }); -describe('generateDefaultLists', () => { - let store; - beforeEach(() => { - const state = { - endpoints: { fullPath: 'gitlab-org', boardId: '1' }, - boardType: 'group', - disabled: false, - boardLists: [{ type: 'backlog' }, { type: 'closed' }], - }; - - store = { - commit: jest.fn(), - dispatch: jest.fn(() => Promise.resolve()), - state, - }; - }); - - it('should dispatch fetchLabels', () => { - return actions.generateDefaultLists(store).then(() => { - expect(store.dispatch.mock.calls[0]).toEqual(['fetchLabels', 'to do']); - expect(store.dispatch.mock.calls[1]).toEqual(['fetchLabels', 'doing']); - }); - }); -}); - describe('createList', () => { it('should dispatch addList action when creating backlog list', done => { const backlogList = { @@ -251,8 +250,8 @@ 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], + 'gid://gitlab/List/1': mockLists[0], + 'gid://gitlab/List/2': mockLists[1], }; const state = { @@ -274,7 +273,7 @@ describe('moveList', () => { [ { type: types.MOVE_LIST, - payload: { movedList: mockListsWithModel[0], listAtNewIndex: mockListsWithModel[1] }, + payload: { movedList: mockLists[0], listAtNewIndex: mockLists[1] }, }, ], [ @@ -290,6 +289,33 @@ describe('moveList', () => { done, ); }); + + it('should not commit MOVE_LIST or dispatch updateList if listId and replacedListId are the same', () => { + const initialBoardListsState = { + 'gid://gitlab/List/1': mockLists[0], + 'gid://gitlab/List/2': mockLists[1], + }; + + const state = { + endpoints: { fullPath: 'gitlab-org', boardId: '1' }, + boardType: 'group', + disabled: false, + boardLists: initialBoardListsState, + }; + + testAction( + actions.moveList, + { + listId: 'gid://gitlab/List/1', + replacedListId: 'gid://gitlab/List/1', + newIndex: 1, + adjustmentValue: 1, + }, + state, + [], + [], + ); + }); }); describe('updateList', () => { @@ -499,15 +525,15 @@ describe('moveIssue', () => { }; const issues = { - '436': mockIssueWithModel, - '437': mockIssue2WithModel, + '436': mockIssue, + '437': mockIssue2, }; const state = { endpoints: { fullPath: 'gitlab-org', boardId: '1' }, boardType: 'group', disabled: false, - boardLists: mockListsWithModel, + boardLists: mockLists, issuesByListId: listIssues, issues, }; @@ -536,7 +562,7 @@ describe('moveIssue', () => { { type: types.MOVE_ISSUE, payload: { - originalIssue: mockIssueWithModel, + originalIssue: mockIssue, fromListId: 'gid://gitlab/List/1', toListId: 'gid://gitlab/List/2', }, @@ -611,7 +637,7 @@ describe('moveIssue', () => { { type: types.MOVE_ISSUE, payload: { - originalIssue: mockIssueWithModel, + originalIssue: mockIssue, fromListId: 'gid://gitlab/List/1', toListId: 'gid://gitlab/List/2', }, @@ -619,7 +645,7 @@ describe('moveIssue', () => { { type: types.MOVE_ISSUE_FAILURE, payload: { - originalIssue: mockIssueWithModel, + originalIssue: mockIssue, fromListId: 'gid://gitlab/List/1', toListId: 'gid://gitlab/List/2', originalIndex: 0, @@ -639,38 +665,59 @@ describe('setAssignees', () => { const refPath = `${projectPath}#3`; const iid = '1'; - beforeEach(() => { - jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ - data: { issueSetAssignees: { issue: { assignees: { nodes: [{ ...node }] } } } }, + describe('when succeeds', () => { + beforeEach(() => { + jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ + data: { issueSetAssignees: { issue: { assignees: { nodes: [{ ...node }] } } } }, + }); }); - }); - it('calls mutate with the correct values', async () => { - await actions.setAssignees( - { commit: () => {}, getters: { activeIssue: { iid, referencePath: refPath } } }, - [name], - ); + it('calls mutate with the correct values', async () => { + await actions.setAssignees( + { commit: () => {}, getters: { activeIssue: { iid, referencePath: refPath } } }, + [name], + ); + + expect(gqlClient.mutate).toHaveBeenCalledWith({ + mutation: updateAssignees, + variables: { iid, assigneeUsernames: [name], projectPath }, + }); + }); - expect(gqlClient.mutate).toHaveBeenCalledWith({ - mutation: updateAssignees, - variables: { iid, assigneeUsernames: [name], projectPath }, + it('calls the correct mutation with the correct values', done => { + testAction( + actions.setAssignees, + {}, + { activeIssue: { iid, referencePath: refPath }, commit: () => {} }, + [ + { type: types.SET_ASSIGNEE_LOADING, payload: true }, + { + type: 'UPDATE_ISSUE_BY_ID', + payload: { prop: 'assignees', issueId: undefined, value: [node] }, + }, + { type: types.SET_ASSIGNEE_LOADING, payload: false }, + ], + [], + done, + ); }); }); - it('calls the correct mutation with the correct values', done => { - testAction( - actions.setAssignees, - {}, - { activeIssue: { iid, referencePath: refPath }, commit: () => {} }, - [ - { - type: 'UPDATE_ISSUE_BY_ID', - payload: { prop: 'assignees', issueId: undefined, value: [node] }, - }, - ], - [], - done, - ); + describe('when fails', () => { + beforeEach(() => { + jest.spyOn(gqlClient, 'mutate').mockRejectedValue(); + }); + + it('calls createFlash', async () => { + await actions.setAssignees({ + commit: () => {}, + getters: { activeIssue: { iid, referencePath: refPath } }, + }); + + expect(createFlash).toHaveBeenCalledWith({ + message: 'An error occurred while updating assignees.', + }); + }); }); }); @@ -885,6 +932,60 @@ describe('setActiveIssueSubscribed', () => { }); }); +describe('setActiveIssueMilestone', () => { + const state = { issues: { [mockIssue.id]: mockIssue } }; + const getters = { activeIssue: mockIssue }; + const testMilestone = { + ...mockMilestone, + id: 'gid://gitlab/Milestone/1', + }; + const input = { + milestoneId: testMilestone.id, + projectPath: 'h/b', + }; + + it('should commit milestone after setting the issue', done => { + jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ + data: { + updateIssue: { + issue: { + milestone: testMilestone, + }, + errors: [], + }, + }, + }); + + const payload = { + issueId: getters.activeIssue.id, + prop: 'milestone', + value: testMilestone, + }; + + testAction( + actions.setActiveIssueMilestone, + 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.setActiveIssueMilestone({ 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 64025726dd1..6ceb8867d1f 100644 --- a/spec/frontend/boards/stores/getters_spec.js +++ b/spec/frontend/boards/stores/getters_spec.js @@ -6,28 +6,10 @@ import { mockIssues, mockIssuesByListId, issues, - mockListsWithModel, + mockLists, } from '../mock_data'; describe('Boards - Getters', () => { - describe('labelToggleState', () => { - it('should return "on" when isShowingLabels is true', () => { - const state = { - isShowingLabels: true, - }; - - expect(getters.labelToggleState(state)).toBe('on'); - }); - - it('should return "off" when isShowingLabels is false', () => { - const state = { - isShowingLabels: false, - }; - - expect(getters.labelToggleState(state)).toBe('off'); - }); - }); - describe('isSidebarOpen', () => { it('returns true when activeId is not equal to 0', () => { const state = { @@ -51,52 +33,8 @@ describe('Boards - Getters', () => { 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); - }); - }); + it('returns false', () => { + expect(getters.isSwimlanesOn()).toBe(false); }); }); @@ -156,22 +94,22 @@ describe('Boards - Getters', () => { const boardsState = { boardLists: { - 'gid://gitlab/List/1': mockListsWithModel[0], - 'gid://gitlab/List/2': mockListsWithModel[1], + 'gid://gitlab/List/1': mockLists[0], + 'gid://gitlab/List/2': mockLists[1], }, }; describe('getListByLabelId', () => { it('returns list for a given label id', () => { expect(getters.getListByLabelId(boardsState)('gid://gitlab/GroupLabel/121')).toEqual( - mockListsWithModel[1], + mockLists[1], ); }); }); describe('getListByTitle', () => { it('returns list for a given list title', () => { - expect(getters.getListByTitle(boardsState)('To Do')).toEqual(mockListsWithModel[1]); + expect(getters.getListByTitle(boardsState)('To Do')).toEqual(mockLists[1]); }); }); }); diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js index e1e57a8fd43..d93119ede3d 100644 --- a/spec/frontend/boards/stores/mutations_spec.js +++ b/spec/frontend/boards/stores/mutations_spec.js @@ -1,15 +1,7 @@ import mutations from '~/boards/stores/mutations'; import * as types from '~/boards/stores/mutation_types'; import defaultState from '~/boards/stores/state'; -import { - mockListsWithModel, - mockLists, - rawIssue, - mockIssue, - mockIssue2, - mockIssueWithModel, - mockIssue2WithModel, -} from '../mock_data'; +import { mockLists, rawIssue, mockIssue, mockIssue2 } from '../mock_data'; const expectNotImplemented = action => { it('is not implemented', () => { @@ -21,8 +13,8 @@ describe('Board Store Mutations', () => { let state; const initialBoardListsState = { - 'gid://gitlab/List/1': mockListsWithModel[0], - 'gid://gitlab/List/2': mockListsWithModel[1], + 'gid://gitlab/List/1': mockLists[0], + 'gid://gitlab/List/2': mockLists[1], }; beforeEach(() => { @@ -41,19 +33,21 @@ describe('Board Store Mutations', () => { }; const boardType = 'group'; const disabled = false; - const showPromotion = false; + const boardConfig = { + milestoneTitle: 'Milestone 1', + }; mutations[types.SET_INITIAL_BOARD_DATA](state, { ...endpoints, boardType, disabled, - showPromotion, + boardConfig, }); expect(state.endpoints).toEqual(endpoints); expect(state.boardType).toEqual(boardType); expect(state.disabled).toEqual(disabled); - expect(state.showPromotion).toEqual(showPromotion); + expect(state.boardConfig).toEqual(boardConfig); }); }); @@ -135,10 +129,10 @@ describe('Board Store Mutations', () => { describe('RECEIVE_ADD_LIST_SUCCESS', () => { it('adds list to boardLists state', () => { - mutations.RECEIVE_ADD_LIST_SUCCESS(state, mockListsWithModel[0]); + mutations.RECEIVE_ADD_LIST_SUCCESS(state, mockLists[0]); expect(state.boardLists).toEqual({ - [mockListsWithModel[0].id]: mockListsWithModel[0], + [mockLists[0].id]: mockLists[0], }); }); }); @@ -155,13 +149,13 @@ describe('Board Store Mutations', () => { }; mutations.MOVE_LIST(state, { - movedList: mockListsWithModel[0], - listAtNewIndex: mockListsWithModel[1], + movedList: mockLists[0], + listAtNewIndex: mockLists[1], }); expect(state.boardLists).toEqual({ - 'gid://gitlab/List/2': mockListsWithModel[1], - 'gid://gitlab/List/1': mockListsWithModel[0], + 'gid://gitlab/List/2': mockLists[1], + 'gid://gitlab/List/1': mockLists[0], }); }); }); @@ -171,8 +165,8 @@ describe('Board Store Mutations', () => { state = { ...state, boardLists: { - 'gid://gitlab/List/2': mockListsWithModel[1], - 'gid://gitlab/List/1': mockListsWithModel[0], + 'gid://gitlab/List/2': mockLists[1], + 'gid://gitlab/List/1': mockLists[0], }, error: undefined, }; @@ -186,7 +180,7 @@ describe('Board Store Mutations', () => { describe('REMOVE_LIST', () => { it('removes list from boardLists', () => { - const [list, secondList] = mockListsWithModel; + const [list, secondList] = mockLists; const expected = { [secondList.id]: secondList, }; @@ -355,8 +349,8 @@ describe('Board Store Mutations', () => { }; const issues = { - '1': mockIssueWithModel, - '2': mockIssue2WithModel, + '1': mockIssue, + '2': mockIssue2, }; state = { @@ -367,7 +361,7 @@ describe('Board Store Mutations', () => { }; mutations.MOVE_ISSUE(state, { - originalIssue: mockIssue2WithModel, + originalIssue: mockIssue2, fromListId: 'gid://gitlab/List/1', toListId: 'gid://gitlab/List/2', }); @@ -396,7 +390,7 @@ describe('Board Store Mutations', () => { issue: rawIssue, }); - expect(state.issues).toEqual({ '436': { ...mockIssueWithModel, id: 436 } }); + expect(state.issues).toEqual({ '436': { ...mockIssue, id: 436 } }); }); }); @@ -466,13 +460,13 @@ describe('Board Store Mutations', () => { boardLists: initialBoardListsState, }; - expect(state.boardLists['gid://gitlab/List/1'].issuesSize).toBe(1); + expect(state.boardLists['gid://gitlab/List/1'].issuesCount).toBe(1); - mutations.ADD_ISSUE_TO_LIST(state, { list: mockListsWithModel[0], issue: mockIssue2 }); + 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); - expect(state.boardLists['gid://gitlab/List/1'].issuesSize).toBe(2); + expect(state.boardLists['gid://gitlab/List/1'].issuesCount).toBe(2); }); }); @@ -524,6 +518,14 @@ describe('Board Store Mutations', () => { }); }); + describe('SET_ASSIGNEE_LOADING', () => { + it('sets isSettingAssignees to the value passed', () => { + mutations.SET_ASSIGNEE_LOADING(state, true); + + expect(state.isSettingAssignees).toBe(true); + }); + }); + describe('SET_CURRENT_PAGE', () => { expectNotImplemented(mutations.SET_CURRENT_PAGE); }); |