diff options
Diffstat (limited to 'spec/frontend/boards')
18 files changed, 332 insertions, 220 deletions
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js index e0446811f64..677978d31ca 100644 --- a/spec/frontend/boards/board_card_inner_spec.js +++ b/spec/frontend/boards/board_card_inner_spec.js @@ -1,15 +1,13 @@ import { GlLabel, GlLoadingIcon, GlTooltip } from '@gitlab/ui'; import { range } from 'lodash'; import Vuex from 'vuex'; -import setWindowLocation from 'helpers/set_window_location_helper'; +import { nextTick } from 'vue'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import BoardBlockedIcon from '~/boards/components/board_blocked_icon.vue'; import BoardCardInner from '~/boards/components/board_card_inner.vue'; import { issuableTypes } from '~/boards/constants'; -import eventHub from '~/boards/eventhub'; import defaultStore from '~/boards/stores'; -import { updateHistory } from '~/lib/utils/url_utility'; import { mockLabelList, mockIssue, mockIssueFullPath } from './mock_data'; jest.mock('~/lib/utils/url_utility'); @@ -53,6 +51,7 @@ describe('Board card component', () => { state: { ...defaultStore.state, issuableType: issuableTypes.issue, + isShowingLabels: true, }, getters: { isGroupBoard: () => true, @@ -261,7 +260,7 @@ describe('Board card component', () => { ], }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.find('.board-card-assignee img').attributes('src')).toBe( 'test_image_from_avatar_url?width=24', @@ -376,7 +375,7 @@ describe('Board card component', () => { }, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.find('.board-card-assignee .avatar-counter').text().trim()).toEqual('99+'); }); @@ -399,58 +398,17 @@ describe('Board card component', () => { it('does not render label if label does not have an ID', async () => { wrapper.setProps({ item: { ...issue, labels: [label1, { title: 'closed' }] } }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.findAll(GlLabel).length).toBe(1); expect(wrapper.text()).not.toContain('closed'); }); - }); - - describe('filterByLabel method', () => { - beforeEach(() => { - wrapper.setProps({ - updateFilters: true, - }); - }); - - describe('when selected label is not in the filter', () => { - beforeEach(() => { - jest.spyOn(wrapper.vm, 'performSearch').mockImplementation(() => {}); - setWindowLocation('?'); - wrapper.vm.filterByLabel(label1); - }); - - it('calls updateHistory', () => { - expect(updateHistory).toHaveBeenCalledTimes(1); - }); - - it('dispatches performSearch vuex action', () => { - expect(wrapper.vm.performSearch).toHaveBeenCalledTimes(1); - }); - - it('emits updateTokens event', () => { - expect(eventHub.$emit).toHaveBeenCalledTimes(1); - expect(eventHub.$emit).toHaveBeenCalledWith('updateTokens'); - }); - }); - - describe('when selected label is already in the filter', () => { - beforeEach(() => { - jest.spyOn(wrapper.vm, 'performSearch').mockImplementation(() => {}); - setWindowLocation('?label_name[]=testing%20123'); - wrapper.vm.filterByLabel(label1); - }); - - it('does not call updateHistory', () => { - expect(updateHistory).not.toHaveBeenCalled(); - }); - it('does not dispatch performSearch vuex action', () => { - expect(wrapper.vm.performSearch).not.toHaveBeenCalled(); - }); - - it('does not emit updateTokens event', () => { - expect(eventHub.$emit).not.toHaveBeenCalled(); + describe('when label params arent set', () => { + it('passes the right target to GlLabel', () => { + expect(wrapper.findAll(GlLabel).at(0).props('target')).toEqual( + '?label_name[]=testing%20123', + ); }); }); }); diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js index d0f14bd37c1..04192489817 100644 --- a/spec/frontend/boards/board_list_helper.js +++ b/spec/frontend/boards/board_list_helper.js @@ -1,4 +1,5 @@ -import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; import VueApollo from 'vue-apollo'; import Vuex from 'vuex'; @@ -33,9 +34,8 @@ export default function createComponent({ }, issuesCount, } = {}) { - const localVue = createLocalVue(); - localVue.use(VueApollo); - localVue.use(Vuex); + Vue.use(VueApollo); + Vue.use(Vuex); const fakeApollo = createMockApollo([ [listQuery, jest.fn().mockResolvedValue(boardListQueryResponse(issuesCount))], @@ -85,7 +85,6 @@ export default function createComponent({ const component = shallowMount(BoardList, { apolloProvider: fakeApollo, - localVue, store, propsData: { disabled: false, diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js index 1981ed5ab7f..fd9d2b6823d 100644 --- a/spec/frontend/boards/board_list_spec.js +++ b/spec/frontend/boards/board_list_spec.js @@ -1,6 +1,8 @@ import Draggable from 'vuedraggable'; +import { nextTick } from 'vue'; import { DraggableItemTypes } from 'ee_else_ce/boards/constants'; import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame'; +import waitForPromises from 'helpers/wait_for_promises'; import createComponent from 'jest/boards/board_list_helper'; import BoardCard from '~/boards/components/board_card.vue'; import eventHub from '~/boards/eventhub'; @@ -64,14 +66,14 @@ describe('Board list component', () => { it('shows new issue form', async () => { wrapper.vm.toggleForm(); - await wrapper.vm.$nextTick(); + await 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(); + await nextTick(); expect(wrapper.find('.board-new-issue-form').exists()).toBe(true); }); @@ -85,7 +87,7 @@ describe('Board list component', () => { it('shows count list item', async () => { wrapper.vm.showCount = true; - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.find('.board-list-count').exists()).toBe(true); expect(wrapper.find('.board-list-count').text()).toBe('Showing all issues'); @@ -94,7 +96,7 @@ describe('Board list component', () => { it('sets data attribute with invalid id', async () => { wrapper.vm.showCount = true; - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.find('.board-list-count').attributes('data-issue-id')).toBe('-1'); }); }); @@ -104,10 +106,6 @@ describe('Board list component', () => { fetchItemsForList: jest.fn(), }; - beforeEach(() => { - wrapper = createComponent(); - }); - it('does not load issues if already loading', () => { wrapper = createComponent({ actions, @@ -126,20 +124,23 @@ describe('Board list component', () => { }, }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findIssueCountLoadingIcon().exists()).toBe(true); }); it('shows how many more issues to load', async () => { - // wrapper.vm.showCount = true; wrapper = createComponent({ data: { showCount: true, }, }); - await wrapper.vm.$nextTick(); + await nextTick(); + await waitForPromises(); + await nextTick(); + await nextTick(); + expect(wrapper.find('.board-list-count').text()).toBe('Showing 1 of 20 issues'); }); }); @@ -155,7 +156,7 @@ describe('Board list component', () => { it('sets background to bg-danger-100', async () => { wrapper.setProps({ list: { issuesCount: 4, maxIssueCount: 3 } }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.find('.bg-danger-100').exists()).toBe(true); }); }); diff --git a/spec/frontend/boards/components/board_add_new_column_trigger_spec.js b/spec/frontend/boards/components/board_add_new_column_trigger_spec.js index c35f2463f69..7dd02bf1d35 100644 --- a/spec/frontend/boards/components/board_add_new_column_trigger_spec.js +++ b/spec/frontend/boards/components/board_add_new_column_trigger_spec.js @@ -1,5 +1,5 @@ import { GlButton } from '@gitlab/ui'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue'; @@ -49,7 +49,7 @@ describe('BoardAddNewColumnTrigger', () => { it('shows the tooltip', async () => { wrapper.find(GlButton).vm.$emit('click'); - await wrapper.vm.$nextTick(); + await nextTick(); const tooltip = findTooltipText(); diff --git a/spec/frontend/boards/components/board_blocked_icon_spec.js b/spec/frontend/boards/components/board_blocked_icon_spec.js index 7b04942f056..7a5c49bd488 100644 --- a/spec/frontend/boards/components/board_blocked_icon_spec.js +++ b/spec/frontend/boards/components/board_blocked_icon_spec.js @@ -1,6 +1,6 @@ import { GlIcon, GlLink, GlPopover, GlLoadingIcon } from '@gitlab/ui'; import { shallowMount, mount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; @@ -39,7 +39,7 @@ describe('BoardBlockedIcon', () => { const mouseenter = async () => { findGlIcon().vm.$emit('mouseenter'); - await wrapper.vm.$nextTick(); + await nextTick(); await waitForApollo(); }; diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js index 3af173aa18c..aad89cf8261 100644 --- a/spec/frontend/boards/components/board_card_spec.js +++ b/spec/frontend/boards/components/board_card_spec.js @@ -1,6 +1,6 @@ import { GlLabel } from '@gitlab/ui'; import { shallowMount, mount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import BoardCard from '~/boards/components/board_card.vue'; @@ -65,12 +65,12 @@ describe('Board card', () => { const selectCard = async () => { wrapper.trigger('click'); - await wrapper.vm.$nextTick(); + await nextTick(); }; const multiSelectCard = async () => { wrapper.trigger('click', { ctrlKey: true }); - await wrapper.vm.$nextTick(); + await nextTick(); }; beforeEach(() => { diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js index a8398a138ba..85ba703a6ee 100644 --- a/spec/frontend/boards/components/board_filtered_search_spec.js +++ b/spec/frontend/boards/components/board_filtered_search_spec.js @@ -120,7 +120,7 @@ describe('BoardFilteredSearch', () => { { type: 'author', value: { data: 'root', operator: '=' } }, { type: 'assignee', value: { data: 'root', operator: '=' } }, { type: 'label', value: { data: 'label', operator: '=' } }, - { type: 'label', value: { data: 'label2', operator: '=' } }, + { type: 'label', value: { data: 'label&2', operator: '=' } }, { type: 'milestone', value: { data: 'New Milestone', operator: '=' } }, { type: 'type', value: { data: 'INCIDENT', operator: '=' } }, { type: 'weight', value: { data: '2', operator: '=' } }, @@ -134,7 +134,7 @@ describe('BoardFilteredSearch', () => { title: '', replace: true, url: - 'http://test.host/?author_username=root&label_name[]=label&label_name[]=label2&assignee_username=root&milestone_title=New+Milestone&iteration_id=3341&types=INCIDENT&weight=2&release_tag=v1.0.0', + 'http://test.host/?author_username=root&label_name[]=label&label_name[]=label%262&assignee_username=root&milestone_title=New%20Milestone&iteration_id=3341&types=INCIDENT&weight=2&release_tag=v1.0.0', }); }); diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js index 692fd3ec555..5678da2a246 100644 --- a/spec/frontend/boards/components/board_form_spec.js +++ b/spec/frontend/boards/components/board_form_spec.js @@ -130,7 +130,7 @@ describe('BoardForm', () => { it('passes correct primary action text and variant', () => { expect(findModalActionPrimary().text).toBe('Create board'); - expect(findModalActionPrimary().attributes[0].variant).toBe('success'); + expect(findModalActionPrimary().attributes[0].variant).toBe('confirm'); }); it('does not render delete confirmation message', () => { diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js index 8cc0ad5f30c..14870ec76a2 100644 --- a/spec/frontend/boards/components/board_list_header_spec.js +++ b/spec/frontend/boards/components/board_list_header_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import Vuex from 'vuex'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -148,7 +148,7 @@ describe('Board List Header Component', () => { findCaret().vm.$emit('click'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(toggleListCollapsedSpy).toHaveBeenCalledTimes(1); }); @@ -156,7 +156,7 @@ describe('Board List Header Component', () => { createComponent({ withLocalStorage: false, currentUserId: 1 }); findCaret().vm.$emit('click'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(updateListSpy).toHaveBeenCalledTimes(1); expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.collapsed`)).toBe(null); @@ -168,7 +168,7 @@ describe('Board List Header Component', () => { }); findCaret().vm.$emit('click'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(updateListSpy).not.toHaveBeenCalled(); expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.collapsed`)).toBe(String(isCollapsed())); diff --git a/spec/frontend/boards/components/board_new_issue_spec.js b/spec/frontend/boards/components/board_new_issue_spec.js index 57ccebf3676..8b0100d069a 100644 --- a/spec/frontend/boards/components/board_new_issue_spec.js +++ b/spec/frontend/boards/components/board_new_issue_spec.js @@ -1,15 +1,14 @@ -import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import BoardNewIssue from '~/boards/components/board_new_issue.vue'; import BoardNewItem from '~/boards/components/board_new_item.vue'; import ProjectSelect from '~/boards/components/project_select.vue'; import eventHub from '~/boards/eventhub'; -import { mockList, mockGroupProjects } from '../mock_data'; +import { mockList, mockGroupProjects, mockIssue, mockIssue2 } from '../mock_data'; -const localVue = createLocalVue(); - -localVue.use(Vuex); +Vue.use(Vuex); const addListNewIssuesSpy = jest.fn().mockResolvedValue(); const mockActions = { addListNewIssue: addListNewIssuesSpy }; @@ -17,10 +16,9 @@ const mockActions = { addListNewIssue: addListNewIssuesSpy }; const createComponent = ({ state = { selectedProject: mockGroupProjects[0], fullPath: mockGroupProjects[0].fullPath }, actions = mockActions, - getters = { isGroupBoard: () => true, isProjectBoard: () => false }, + getters = { isGroupBoard: () => true, getBoardItemsByList: () => () => [] }, } = {}) => shallowMount(BoardNewIssue, { - localVue, store: new Vuex.Store({ state, actions, @@ -47,7 +45,7 @@ describe('Issue boards new issue form', () => { beforeEach(async () => { wrapper = createComponent(); - await wrapper.vm.$nextTick(); + await nextTick(); }); afterEach(() => { @@ -68,7 +66,7 @@ describe('Issue boards new issue form', () => { it('calls addListNewIssue action when `board-new-item` emits form-submit event', async () => { findBoardNewItem().vm.$emit('form-submit', { title: 'Foo' }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(addListNewIssuesSpy).toHaveBeenCalledWith(expect.any(Object), { list: mockList, issueInput: { @@ -77,15 +75,44 @@ describe('Issue boards new issue form', () => { assigneeIds: [], milestoneId: undefined, projectPath: mockGroupProjects[0].fullPath, + moveAfterId: undefined, }, }); }); + describe('when list has an existing issues', () => { + beforeEach(() => { + wrapper = createComponent({ + getters: { + isGroupBoard: () => true, + getBoardItemsByList: () => () => [mockIssue, mockIssue2], + }, + }); + }); + + it('it uses the first issue ID as moveAfterId', async () => { + findBoardNewItem().vm.$emit('form-submit', { title: 'Foo' }); + + await nextTick(); + expect(addListNewIssuesSpy).toHaveBeenCalledWith(expect.any(Object), { + list: mockList, + issueInput: { + title: 'Foo', + labelIds: [], + assigneeIds: [], + milestoneId: undefined, + projectPath: mockGroupProjects[0].fullPath, + moveAfterId: mockIssue.id, + }, + }); + }); + }); + it('emits event `toggle-issue-form` with current list Id suffix on eventHub when `board-new-item` emits form-cancel event', async () => { jest.spyOn(eventHub, '$emit').mockImplementation(); findBoardNewItem().vm.$emit('form-cancel'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(eventHub.$emit).toHaveBeenCalledWith(`toggle-issue-form-${mockList.id}`); }); @@ -101,7 +128,7 @@ describe('Issue boards new issue form', () => { describe('when in project issue board', () => { beforeEach(() => { wrapper = createComponent({ - getters: { isGroupBoard: () => false, isProjectBoard: () => true }, + getters: { isGroupBoard: () => false }, }); }); diff --git a/spec/frontend/boards/components/board_new_item_spec.js b/spec/frontend/boards/components/board_new_item_spec.js index 0151d9c1c14..86cebc8a719 100644 --- a/spec/frontend/boards/components/board_new_item_spec.js +++ b/spec/frontend/boards/components/board_new_item_spec.js @@ -1,4 +1,5 @@ import { GlForm, GlFormInput, GlButton } from '@gitlab/ui'; +import { nextTick } from 'vue'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import BoardNewItem from '~/boards/components/board_new_item.vue'; @@ -39,6 +40,27 @@ describe('BoardNewItem', () => { }); describe('template', () => { + describe('when the user provides a valid input', () => { + it('finds an enabled create button', async () => { + expect(wrapper.findByTestId('create-button').props('disabled')).toBe(true); + + wrapper.find(GlFormInput).vm.$emit('input', 'hello'); + await nextTick(); + + expect(wrapper.findByTestId('create-button').props('disabled')).toBe(false); + }); + }); + + describe('when the user types in a string with only spaces', () => { + it('disables the Create Issue button', async () => { + wrapper.find(GlFormInput).vm.$emit('input', ' '); + + await nextTick(); + + expect(wrapper.findByTestId('create-button').props('disabled')).toBe(true); + }); + }); + it('renders gl-form component', () => { expect(wrapper.findComponent(GlForm).exists()).toBe(true); }); @@ -80,6 +102,19 @@ describe('BoardNewItem', () => { ]); }); + it('emits `form-submit` event with trimmed title', async () => { + titleInput().setValue(' Foo '); + + await glForm().trigger('submit'); + + expect(wrapper.emitted('form-submit')[0]).toEqual([ + { + title: 'Foo', + list: mockList, + }, + ]); + }); + it('emits `scroll-board-list-` event with list.id on eventHub when `submit` is triggered on gl-form', async () => { jest.spyOn(eventHub, '$emit').mockImplementation(); await glForm().trigger('submit'); @@ -90,7 +125,7 @@ describe('BoardNewItem', () => { it('emits `form-cancel` event and clears title value when `reset` is triggered on gl-form', async () => { titleInput().setValue('Foo'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(titleInput().element.value).toBe('Foo'); await glForm().trigger('reset'); diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js index 46dd109ffb1..7f40c426b30 100644 --- a/spec/frontend/boards/components/board_settings_sidebar_spec.js +++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js @@ -1,8 +1,9 @@ -import { GlDrawer, GlLabel } from '@gitlab/ui'; +import { GlDrawer, GlLabel, GlModal, GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { MountingPortal } from 'portal-vue'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { stubComponent } from 'helpers/stub_component'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue'; @@ -20,8 +21,7 @@ describe('BoardSettingsSidebar', () => { const labelTitle = mockLabelList.label.title; const labelColor = mockLabelList.label.color; const listId = mockLabelList.id; - - const findRemoveButton = () => wrapper.findByTestId('remove-list'); + const modalID = 'board-settings-sidebar-modal'; const createComponent = ({ canAdminList = false, @@ -46,6 +46,9 @@ describe('BoardSettingsSidebar', () => { canAdminList, scopedLabelsAvailable: false, }, + directives: { + GlModal: createMockDirective(), + }, stubs: { GlDrawer: stubComponent(GlDrawer, { template: '<div><slot name="header"></slot><slot></slot></div>', @@ -56,6 +59,8 @@ describe('BoardSettingsSidebar', () => { }; const findLabel = () => wrapper.find(GlLabel); const findDrawer = () => wrapper.find(GlDrawer); + const findModal = () => wrapper.find(GlModal); + const findRemoveButton = () => wrapper.find(GlButton); afterEach(() => { jest.restoreAllMocks(); @@ -86,7 +91,7 @@ describe('BoardSettingsSidebar', () => { findDrawer().vm.$emit('close'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.find(GlDrawer).exists()).toBe(false); }); @@ -96,7 +101,7 @@ describe('BoardSettingsSidebar', () => { sidebarEventHub.$emit('sidebar.closeAll'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.find(GlDrawer).exists()).toBe(false); }); @@ -161,5 +166,16 @@ describe('BoardSettingsSidebar', () => { expect(findRemoveButton().exists()).toBe(true); }); + + it('has the correct ID on the button', () => { + createComponent({ canAdminList: true, activeId: listId, list: mockLabelList }); + const binding = getBinding(findRemoveButton().element, 'gl-modal'); + expect(binding.value).toBe(modalID); + }); + + it('has the correct ID on the modal', () => { + createComponent({ canAdminList: true, activeId: listId, list: mockLabelList }); + expect(findModal().props('modalId')).toBe(modalID); + }); }); }); diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js index 9cf7c5774bf..26a5bf34595 100644 --- a/spec/frontend/boards/components/boards_selector_spec.js +++ b/spec/frontend/boards/components/boards_selector_spec.js @@ -1,43 +1,40 @@ import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; -import MockAdapter from 'axios-mock-adapter'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import Vuex from 'vuex'; import { TEST_HOST } from 'spec/test_constants'; import BoardsSelector from '~/boards/components/boards_selector.vue'; +import { BoardType } from '~/boards/constants'; import groupBoardQuery from '~/boards/graphql/group_board.query.graphql'; import projectBoardQuery from '~/boards/graphql/project_board.query.graphql'; +import groupBoardsQuery from '~/boards/graphql/group_boards.query.graphql'; +import projectBoardsQuery from '~/boards/graphql/project_boards.query.graphql'; +import groupRecentBoardsQuery from '~/boards/graphql/group_recent_boards.query.graphql'; +import projectRecentBoardsQuery from '~/boards/graphql/project_recent_boards.query.graphql'; import defaultStore from '~/boards/stores'; -import axios from '~/lib/utils/axios_utils'; import createMockApollo from 'helpers/mock_apollo_helper'; -import { mockGroupBoardResponse, mockProjectBoardResponse } from '../mock_data'; +import { + mockGroupBoardResponse, + mockProjectBoardResponse, + mockGroupAllBoardsResponse, + mockProjectAllBoardsResponse, + mockGroupRecentBoardsResponse, + mockProjectRecentBoardsResponse, + mockSmallProjectAllBoardsResponse, + mockEmptyProjectRecentBoardsResponse, + boards, + recentIssueBoards, +} from '../mock_data'; const throttleDuration = 1; Vue.use(VueApollo); -function boardGenerator(n) { - return new Array(n).fill().map((board, index) => { - const id = `${index}`; - const name = `board${id}`; - - return { - id, - name, - }; - }); -} - describe('BoardsSelector', () => { let wrapper; - let allBoardsResponse; - let recentBoardsResponse; - let mock; let fakeApollo; let store; - const boards = boardGenerator(20); - const recentBoards = boardGenerator(5); const createStore = ({ isGroupBoard = false, isProjectBoard = false } = {}) => { store = new Vuex.Store({ @@ -63,17 +60,43 @@ describe('BoardsSelector', () => { }; const getDropdownItems = () => wrapper.findAll('.js-dropdown-item'); - const getDropdownHeaders = () => wrapper.findAll(GlDropdownSectionHeader); - const getLoadingIcon = () => wrapper.find(GlLoadingIcon); - const findDropdown = () => wrapper.find(GlDropdown); + const getDropdownHeaders = () => wrapper.findAllComponents(GlDropdownSectionHeader); + const getLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findDropdown = () => wrapper.findComponent(GlDropdown); const projectBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockProjectBoardResponse); const groupBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupBoardResponse); - const createComponent = () => { + const projectBoardsQueryHandlerSuccess = jest + .fn() + .mockResolvedValue(mockProjectAllBoardsResponse); + const groupBoardsQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupAllBoardsResponse); + + const projectRecentBoardsQueryHandlerSuccess = jest + .fn() + .mockResolvedValue(mockProjectRecentBoardsResponse); + const groupRecentBoardsQueryHandlerSuccess = jest + .fn() + .mockResolvedValue(mockGroupRecentBoardsResponse); + + const smallBoardsQueryHandlerSuccess = jest + .fn() + .mockResolvedValue(mockSmallProjectAllBoardsResponse); + const emptyRecentBoardsQueryHandlerSuccess = jest + .fn() + .mockResolvedValue(mockEmptyProjectRecentBoardsResponse); + + const createComponent = ({ + projectBoardsQueryHandler = projectBoardsQueryHandlerSuccess, + projectRecentBoardsQueryHandler = projectRecentBoardsQueryHandlerSuccess, + } = {}) => { fakeApollo = createMockApollo([ [projectBoardQuery, projectBoardQueryHandlerSuccess], [groupBoardQuery, groupBoardQueryHandlerSuccess], + [projectBoardsQuery, projectBoardsQueryHandler], + [groupBoardsQuery, groupBoardsQueryHandlerSuccess], + [projectRecentBoardsQuery, projectRecentBoardsQueryHandler], + [groupRecentBoardsQuery, groupRecentBoardsQueryHandlerSuccess], ]); wrapper = mount(BoardsSelector, { @@ -91,67 +114,34 @@ describe('BoardsSelector', () => { attachTo: document.body, provide: { fullPath: '', - recentBoardsEndpoint: `${TEST_HOST}/recent`, }, }); - - wrapper.vm.$apollo.addSmartQuery = jest.fn((_, options) => { - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - [options.loadingKey]: true, - }); - }); }; afterEach(() => { wrapper.destroy(); - wrapper = null; - mock.restore(); + fakeApollo = null; }); - describe('fetching all boards', () => { + describe('template', () => { beforeEach(() => { - mock = new MockAdapter(axios); - - allBoardsResponse = Promise.resolve({ - data: { - group: { - boards: { - edges: boards.map((board) => ({ node: board })), - }, - }, - }, - }); - recentBoardsResponse = Promise.resolve({ - data: recentBoards, - }); - - createStore(); + createStore({ isProjectBoard: true }); createComponent(); - - mock.onGet(`${TEST_HOST}/recent`).replyOnce(200, recentBoards); }); describe('loading', () => { - beforeEach(async () => { - // Wait for current board to be loaded - await nextTick(); - - // Emits gl-dropdown show event to simulate the dropdown is opened at initialization time - findDropdown().vm.$emit('show'); - }); - // we are testing loading state, so don't resolve responses until after the tests afterEach(async () => { - await Promise.all([allBoardsResponse, recentBoardsResponse]); await nextTick(); }); it('shows loading spinner', () => { + // Emits gl-dropdown show event to simulate the dropdown is opened at initialization time + findDropdown().vm.$emit('show'); + + expect(getLoadingIcon().exists()).toBe(true); expect(getDropdownHeaders()).toHaveLength(0); expect(getDropdownItems()).toHaveLength(0); - expect(getLoadingIcon().exists()).toBe(true); }); }); @@ -163,16 +153,13 @@ describe('BoardsSelector', () => { // Emits gl-dropdown show event to simulate the dropdown is opened at initialization time findDropdown().vm.$emit('show'); - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - await wrapper.setData({ - loadingBoards: false, - loadingRecentBoards: false, - }); - await Promise.all([allBoardsResponse, recentBoardsResponse]); await nextTick(); }); + it('fetches all issue boards', () => { + expect(projectBoardsQueryHandlerSuccess).toHaveBeenCalled(); + }); + it('hides loading spinner', async () => { await nextTick(); expect(getLoadingIcon().exists()).toBe(false); @@ -180,22 +167,17 @@ describe('BoardsSelector', () => { describe('filtering', () => { beforeEach(async () => { - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - boards, - }); - await nextTick(); }); it('shows all boards without filtering', () => { - expect(getDropdownItems()).toHaveLength(boards.length + recentBoards.length); + expect(getDropdownItems()).toHaveLength(boards.length + recentIssueBoards.length); }); it('shows only matching boards when filtering', async () => { const filterTerm = 'board1'; - const expectedCount = boards.filter((board) => board.name.includes(filterTerm)).length; + const expectedCount = boards.filter((board) => board.node.name.includes(filterTerm)) + .length; fillSearchBox(filterTerm); @@ -214,32 +196,21 @@ describe('BoardsSelector', () => { describe('recent boards section', () => { it('shows only when boards are greater than 10', async () => { - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - boards, - }); - await nextTick(); + expect(projectRecentBoardsQueryHandlerSuccess).toHaveBeenCalled(); expect(getDropdownHeaders()).toHaveLength(2); }); it('does not show when boards are less than 10', async () => { - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - boards: boards.slice(0, 5), - }); + createComponent({ projectBoardsQueryHandler: smallBoardsQueryHandlerSuccess }); await nextTick(); expect(getDropdownHeaders()).toHaveLength(0); }); - it('does not show when recentBoards api returns empty array', async () => { - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - recentBoards: [], + it('does not show when recentIssueBoards api returns empty array', async () => { + createComponent({ + projectRecentBoardsQueryHandler: emptyRecentBoardsQueryHandlerSuccess, }); await nextTick(); @@ -256,15 +227,39 @@ describe('BoardsSelector', () => { }); }); + describe('fetching all boards', () => { + it.each` + boardType | queryHandler | notCalledHandler + ${BoardType.group} | ${groupBoardsQueryHandlerSuccess} | ${projectBoardsQueryHandlerSuccess} + ${BoardType.project} | ${projectBoardsQueryHandlerSuccess} | ${groupBoardsQueryHandlerSuccess} + `('fetches $boardType boards', async ({ boardType, queryHandler, notCalledHandler }) => { + createStore({ + isProjectBoard: boardType === BoardType.project, + isGroupBoard: boardType === BoardType.group, + }); + createComponent(); + + await nextTick(); + + // Emits gl-dropdown show event to simulate the dropdown is opened at initialization time + findDropdown().vm.$emit('show'); + + await nextTick(); + + expect(queryHandler).toHaveBeenCalled(); + expect(notCalledHandler).not.toHaveBeenCalled(); + }); + }); + describe('fetching current board', () => { it.each` - boardType | queryHandler | notCalledHandler - ${'group'} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess} - ${'project'} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess} + boardType | queryHandler | notCalledHandler + ${BoardType.group} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess} + ${BoardType.project} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess} `('fetches $boardType board', async ({ boardType, queryHandler, notCalledHandler }) => { createStore({ - isProjectBoard: boardType === 'project', - isGroupBoard: boardType === 'group', + isProjectBoard: boardType === BoardType.project, + isGroupBoard: boardType === BoardType.group, }); createComponent(); 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 12e9a9ba365..0c76c711b3a 100644 --- a/spec/frontend/boards/components/sidebar/board_editable_item_spec.js +++ b/spec/frontend/boards/components/sidebar/board_editable_item_spec.js @@ -1,5 +1,6 @@ import { GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import BoardSidebarItem from '~/boards/components/sidebar/board_editable_item.vue'; describe('boards sidebar remove issue', () => { @@ -79,17 +80,16 @@ describe('boards sidebar remove issue', () => { createComponent({ canUpdate: true, slots }); findEditButton().vm.$emit('click'); - return wrapper.vm.$nextTick().then(() => { - expect(findCollapsed().isVisible()).toBe(false); - expect(findExpanded().isVisible()).toBe(true); - }); + await nextTick(); + expect(findCollapsed().isVisible()).toBe(false); + expect(findExpanded().isVisible()).toBe(true); }); it('hides the header while editing if `toggleHeader` is true', async () => { createComponent({ canUpdate: true, props: { toggleHeader: true } }); findEditButton().vm.$emit('click'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findEditButton().isVisible()).toBe(false); expect(findTitle().isVisible()).toBe(false); @@ -101,14 +101,14 @@ describe('boards sidebar remove issue', () => { beforeEach(async () => { createComponent({ canUpdate: true }); findEditButton().vm.$emit('click'); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('hides expanded section and displays collapsed section', async () => { expect(findExpanded().isVisible()).toBe(true); document.body.click(); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findCollapsed().isVisible()).toBe(true); expect(findExpanded().isVisible()).toBe(false); @@ -117,7 +117,7 @@ describe('boards sidebar remove issue', () => { it('emits events', async () => { document.body.click(); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted().close).toHaveLength(1); expect(wrapper.emitted()['off-click']).toHaveLength(1); @@ -129,7 +129,7 @@ describe('boards sidebar remove issue', () => { findEditButton().vm.$emit('click'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(wrapper.emitted().open.length).toBe(1); }); @@ -139,7 +139,7 @@ describe('boards sidebar remove issue', () => { findEditButton().vm.$emit('click'); - await wrapper.vm.$nextTick(); + await nextTick(); wrapper.vm.collapse({ emitEvent: false }); diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js index 4a8eda298f2..5364d929c38 100644 --- a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js +++ b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js @@ -1,5 +1,6 @@ import { GlAlert, GlFormInput, GlForm } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue'; import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue'; import { createStore } from '~/boards/stores'; @@ -75,7 +76,7 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => { }); findFormInput().vm.$emit('input', TEST_TITLE); findForm().vm.$emit('submit', { preventDefault: () => {} }); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('collapses sidebar and renders new title', () => { @@ -98,7 +99,7 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => { jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {}); findFormInput().vm.$emit('input', ''); findForm().vm.$emit('submit', { preventDefault: () => {} }); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('commits change to the server', () => { @@ -113,7 +114,7 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => { wrapper.vm.$refs.sidebarItem.expand(); findFormInput().vm.$emit('input', TEST_TITLE); findEditableItem().vm.$emit('off-click'); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('does not collapses sidebar and shows alert', () => { @@ -148,7 +149,7 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => { }); findFormInput().vm.$emit('input', TEST_TITLE); findCancelButton().vm.$emit('click'); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('collapses sidebar and render former title', () => { @@ -168,7 +169,7 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => { jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {}); findFormInput().vm.$emit('input', 'Invalid title'); findForm().vm.$emit('submit', { preventDefault: () => {} }); - await wrapper.vm.$nextTick(); + await nextTick(); }); it('collapses sidebar and renders former item title', () => { diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js index a081a60166b..24096fddea6 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -29,6 +29,85 @@ export const listObj = { }, }; +function boardGenerator(n) { + return new Array(n).fill().map((board, index) => { + const id = `${index}`; + const name = `board${id}`; + + return { + node: { + id, + name, + weight: 0, + __typename: 'Board', + }, + }; + }); +} + +export const boards = boardGenerator(20); +export const recentIssueBoards = boardGenerator(5); + +export const mockSmallProjectAllBoardsResponse = { + data: { + project: { + id: 'gid://gitlab/Project/114', + boards: { edges: boardGenerator(3) }, + __typename: 'Project', + }, + }, +}; + +export const mockEmptyProjectRecentBoardsResponse = { + data: { + project: { + id: 'gid://gitlab/Project/114', + recentIssueBoards: { edges: [] }, + __typename: 'Project', + }, + }, +}; + +export const mockGroupAllBoardsResponse = { + data: { + group: { + id: 'gid://gitlab/Group/114', + boards: { edges: boards }, + __typename: 'Group', + }, + }, +}; + +export const mockProjectAllBoardsResponse = { + data: { + project: { + id: 'gid://gitlab/Project/1', + boards: { edges: boards }, + __typename: 'Project', + }, + }, +}; + +export const mockGroupRecentBoardsResponse = { + data: { + group: { + id: 'gid://gitlab/Group/114', + recentIssueBoards: { edges: recentIssueBoards }, + __typename: 'Group', + }, + }, +}; + +export const mockProjectRecentBoardsResponse = { + data: { + project: { + id: 'gid://gitlab/Project/1', + recentIssueBoards: { edges: recentIssueBoards }, + __typename: 'Project', + }, + }, +}; + export const mockGroupBoardResponse = { data: { workspace: { @@ -612,6 +691,7 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, isSignedI title: __('Milestone'), symbol: '%', type: 'milestone', + shouldSkipSort: true, token: MilestoneToken, unique: true, fetchMilestones, diff --git a/spec/frontend/boards/project_select_spec.js b/spec/frontend/boards/project_select_spec.js index de823094630..05dc7d28eaa 100644 --- a/spec/frontend/boards/project_select_spec.js +++ b/spec/frontend/boards/project_select_spec.js @@ -1,6 +1,6 @@ import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import ProjectSelect from '~/boards/components/project_select.vue'; import defaultState from '~/boards/stores/state'; @@ -88,7 +88,7 @@ describe('ProjectSelect component', () => { expect(findGlDropdownLoadingIcon().exists()).toBe(true); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findGlDropdownLoadingIcon().exists()).toBe(false); }); diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index 7c842d71688..0eca0cb3ee5 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -315,14 +315,14 @@ describe('fetchMilestones', () => { 'project', { query: projectBoardMilestones, - variables: { fullPath: 'gitlab-org/gitlab', state: 'active' }, + variables: { fullPath: 'gitlab-org/gitlab' }, }, ], [ 'group', { query: groupBoardMilestones, - variables: { fullPath: 'gitlab-org/gitlab', state: 'active' }, + variables: { fullPath: 'gitlab-org/gitlab' }, }, ], ])( |