diff options
Diffstat (limited to 'spec/frontend/boards/components')
9 files changed, 584 insertions, 66 deletions
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(); + }); + }); +}); |