diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-16 18:18:33 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-16 18:18:33 +0000 |
commit | f64a639bcfa1fc2bc89ca7db268f594306edfd7c (patch) | |
tree | a2c3c2ebcc3b45e596949db485d6ed18ffaacfa1 /spec/frontend/boards | |
parent | bfbc3e0d6583ea1a91f627528bedc3d65ba4b10f (diff) | |
download | gitlab-ce-f64a639bcfa1fc2bc89ca7db268f594306edfd7c.tar.gz |
Add latest changes from gitlab-org/gitlab@13-10-stable-eev13.10.0-rc40
Diffstat (limited to 'spec/frontend/boards')
26 files changed, 1184 insertions, 561 deletions
diff --git a/spec/frontend/boards/issue_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js index b9f84fed6b3..4487fc15de6 100644 --- a/spec/frontend/boards/issue_card_inner_spec.js +++ b/spec/frontend/boards/board_card_inner_spec.js @@ -1,7 +1,7 @@ import { GlLabel } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import { range } from 'lodash'; -import IssueCardInner from '~/boards/components/issue_card_inner.vue'; +import BoardCardInner from '~/boards/components/board_card_inner.vue'; import eventHub from '~/boards/eventhub'; import defaultStore from '~/boards/stores'; import { updateHistory } from '~/lib/utils/url_utility'; @@ -10,7 +10,7 @@ import { mockLabelList } from './mock_data'; jest.mock('~/lib/utils/url_utility'); jest.mock('~/boards/eventhub'); -describe('Issue card component', () => { +describe('Board card component', () => { const user = { id: 1, name: 'testing 123', @@ -31,18 +31,17 @@ describe('Issue card component', () => { let list; const createWrapper = (props = {}, store = defaultStore) => { - wrapper = mount(IssueCardInner, { + wrapper = mount(BoardCardInner, { store, propsData: { list, - issue, + item: issue, ...props, }, stubs: { GlLabel: true, }, provide: { - groupId: null, rootPath: '/', scopedLabelsAvailable: false, }, @@ -63,7 +62,7 @@ describe('Issue card component', () => { weight: 1, }; - createWrapper({ issue, list }); + createWrapper({ item: issue, list }); }); afterEach(() => { @@ -103,8 +102,8 @@ describe('Issue card component', () => { describe('confidential issue', () => { beforeEach(() => { wrapper.setProps({ - issue: { - ...wrapper.props('issue'), + item: { + ...wrapper.props('item'), confidential: true, }, }); @@ -119,8 +118,8 @@ describe('Issue card component', () => { describe('with avatar', () => { beforeEach(() => { wrapper.setProps({ - issue: { - ...wrapper.props('issue'), + item: { + ...wrapper.props('item'), assignees: [user], updateData(newData) { Object.assign(this, newData); @@ -146,8 +145,8 @@ describe('Issue card component', () => { }); it('renders the avatar using avatarUrl property', async () => { - wrapper.props('issue').updateData({ - ...wrapper.props('issue'), + wrapper.props('item').updateData({ + ...wrapper.props('item'), assignees: [ { id: '1', @@ -172,8 +171,8 @@ describe('Issue card component', () => { global.gon.default_avatar_url = 'default_avatar'; wrapper.setProps({ - issue: { - ...wrapper.props('issue'), + item: { + ...wrapper.props('item'), assignees: [ { id: 1, @@ -201,8 +200,8 @@ describe('Issue card component', () => { describe('multiple assignees', () => { beforeEach(() => { wrapper.setProps({ - issue: { - ...wrapper.props('issue'), + item: { + ...wrapper.props('item'), assignees: [ { id: 2, @@ -233,7 +232,7 @@ describe('Issue card component', () => { describe('more than three assignees', () => { beforeEach(() => { - const { assignees } = wrapper.props('issue'); + const { assignees } = wrapper.props('item'); assignees.push({ id: 5, name: 'user5', @@ -242,8 +241,8 @@ describe('Issue card component', () => { }); wrapper.setProps({ - issue: { - ...wrapper.props('issue'), + item: { + ...wrapper.props('item'), assignees, }, }); @@ -259,7 +258,7 @@ describe('Issue card component', () => { it('renders 99+ avatar counter', async () => { const assignees = [ - ...wrapper.props('issue').assignees, + ...wrapper.props('item').assignees, ...range(5, 103).map((i) => ({ id: i, name: 'name', @@ -268,8 +267,8 @@ describe('Issue card component', () => { })), ]; wrapper.setProps({ - issue: { - ...wrapper.props('issue'), + item: { + ...wrapper.props('item'), assignees, }, }); @@ -283,7 +282,7 @@ describe('Issue card component', () => { describe('labels', () => { beforeEach(() => { - wrapper.setProps({ issue: { ...issue, labels: [list.label, label1] } }); + wrapper.setProps({ item: { ...issue, labels: [list.label, label1] } }); }); it('does not render list label but renders all other labels', () => { @@ -295,7 +294,7 @@ describe('Issue card component', () => { }); it('does not render label if label does not have an ID', async () => { - wrapper.setProps({ issue: { ...issue, labels: [label1, { title: 'closed' }] } }); + wrapper.setProps({ item: { ...issue, labels: [label1, { title: 'closed' }] } }); await wrapper.vm.$nextTick(); @@ -307,8 +306,8 @@ describe('Issue card component', () => { describe('blocked', () => { beforeEach(() => { wrapper.setProps({ - issue: { - ...wrapper.props('issue'), + item: { + ...wrapper.props('item'), blocked: true, }, }); diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js index 7ed20f20882..bf39c3f3e42 100644 --- a/spec/frontend/boards/board_list_spec.js +++ b/spec/frontend/boards/board_list_spec.js @@ -1,8 +1,9 @@ -import { createLocalVue, mount } from '@vue/test-utils'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; import Vuex from 'vuex'; import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame'; import BoardCard from '~/boards/components/board_card.vue'; import BoardList from '~/boards/components/board_list.vue'; +import BoardNewIssue from '~/boards/components/board_new_issue.vue'; import eventHub from '~/boards/eventhub'; import defaultState from '~/boards/stores/state'; import { mockList, mockIssuesByListId, issues, mockIssues } from './mock_data'; @@ -11,13 +12,18 @@ const localVue = createLocalVue(); localVue.use(Vuex); const actions = { - fetchIssuesForList: jest.fn(), + fetchItemsForList: jest.fn(), }; const createStore = (state = defaultState) => { return new Vuex.Store({ state, actions, + getters: { + isGroupBoard: () => false, + isProjectBoard: () => true, + isEpicBoard: () => false, + }, }); }; @@ -28,8 +34,8 @@ const createComponent = ({ state = {}, } = {}) => { const store = createStore({ - issuesByListId: mockIssuesByListId, - issues, + boardItemsByListId: mockIssuesByListId, + boardItems: issues, pageInfoByListId: { 'gid://gitlab/List/1': { hasNextPage: true }, 'gid://gitlab/List/2': {}, @@ -38,6 +44,7 @@ const createComponent = ({ 'gid://gitlab/List/1': {}, 'gid://gitlab/List/2': {}, }, + selectedBoardItems: [], ...state, }); @@ -58,12 +65,12 @@ const createComponent = ({ list.issuesCount = 1; } - const component = mount(BoardList, { + const component = shallowMount(BoardList, { localVue, propsData: { disabled: false, list, - issues: [issue], + boardItems: [issue], canAdminList: true, ...componentProps, }, @@ -74,6 +81,10 @@ const createComponent = ({ weightFeatureAvailable: false, boardWeight: null, }, + stubs: { + BoardCard, + BoardNewIssue, + }, }); return component; @@ -81,7 +92,10 @@ const createComponent = ({ describe('Board list component', () => { let wrapper; + const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`); + const findIssueCountLoadingIcon = () => wrapper.find('[data-testid="count-loading-icon"]'); + useFakeRequestAnimationFrame(); afterEach(() => { @@ -111,7 +125,7 @@ describe('Board list component', () => { }); it('sets data attribute with issue id', () => { - expect(wrapper.find('.board-card').attributes('data-issue-id')).toBe('1'); + expect(wrapper.find('.board-card').attributes('data-item-id')).toBe('1'); }); it('shows new issue form', async () => { @@ -170,7 +184,7 @@ describe('Board list component', () => { it('loads more issues after scrolling', () => { wrapper.vm.listRef.dispatchEvent(new Event('scroll')); - expect(actions.fetchIssuesForList).toHaveBeenCalled(); + expect(actions.fetchItemsForList).toHaveBeenCalled(); }); it('does not load issues if already loading', () => { @@ -179,7 +193,7 @@ describe('Board list component', () => { }); wrapper.vm.listRef.dispatchEvent(new Event('scroll')); - expect(actions.fetchIssuesForList).not.toHaveBeenCalled(); + expect(actions.fetchItemsForList).not.toHaveBeenCalled(); }); it('shows loading more spinner', async () => { @@ -189,7 +203,8 @@ describe('Board list component', () => { wrapper.vm.showCount = true; await wrapper.vm.$nextTick(); - expect(wrapper.find('.board-list-count .gl-spinner').exists()).toBe(true); + + expect(findIssueCountLoadingIcon().exists()).toBe(true); }); }); @@ -243,7 +258,7 @@ describe('Board list component', () => { describe('handleDragOnEnd', () => { it('removes class `is-dragging` from document body', () => { - jest.spyOn(wrapper.vm, 'moveIssue').mockImplementation(() => {}); + jest.spyOn(wrapper.vm, 'moveItem').mockImplementation(() => {}); document.body.classList.add('is-dragging'); findByTestId('tree-root-wrapper').vm.$emit('end', { @@ -251,9 +266,9 @@ describe('Board list component', () => { newIndex: 0, item: { dataset: { - issueId: mockIssues[0].id, - issueIid: mockIssues[0].iid, - issuePath: mockIssues[0].referencePath, + itemId: mockIssues[0].id, + itemIid: mockIssues[0].iid, + itemPath: mockIssues[0].referencePath, }, }, to: { children: [], dataset: { listId: 'gid://gitlab/List/1' } }, diff --git a/spec/frontend/boards/board_new_issue_deprecated_spec.js b/spec/frontend/boards/board_new_issue_deprecated_spec.js index 1a29f680166..3903ad201b2 100644 --- a/spec/frontend/boards/board_new_issue_deprecated_spec.js +++ b/spec/frontend/boards/board_new_issue_deprecated_spec.js @@ -3,6 +3,7 @@ import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import Vue from 'vue'; +import Vuex from 'vuex'; import boardNewIssue from '~/boards/components/board_new_issue_deprecated.vue'; import boardsStore from '~/boards/stores/boards_store'; import axios from '~/lib/utils/axios_utils'; @@ -10,6 +11,8 @@ import axios from '~/lib/utils/axios_utils'; import '~/boards/models/list'; import { listObj, boardsMockInterceptor } from './mock_data'; +Vue.use(Vuex); + describe('Issue boards new issue form', () => { let wrapper; let vm; @@ -43,11 +46,16 @@ describe('Issue boards new issue form', () => { newIssueMock = Promise.resolve(promiseReturn); jest.spyOn(list, 'newIssue').mockImplementation(() => newIssueMock); + const store = new Vuex.Store({ + getters: { isGroupBoard: () => false }, + }); + wrapper = mount(BoardNewIssueComp, { propsData: { disabled: false, list, }, + store, provide: { groupId: null, }, diff --git a/spec/frontend/boards/components/board_add_new_column_form_spec.js b/spec/frontend/boards/components/board_add_new_column_form_spec.js new file mode 100644 index 00000000000..3702f55f17b --- /dev/null +++ b/spec/frontend/boards/components/board_add_new_column_form_spec.js @@ -0,0 +1,166 @@ +import { GlFormGroup, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; +import Vuex from 'vuex'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue'; +import defaultState from '~/boards/stores/state'; +import { mockLabelList } from '../mock_data'; + +Vue.use(Vuex); + +describe('Board card layout', () => { + let wrapper; + + const createStore = ({ actions = {}, getters = {}, state = {} } = {}) => { + return new Vuex.Store({ + state: { + ...defaultState, + ...state, + }, + actions, + getters, + }); + }; + + const mountComponent = ({ + loading = false, + formDescription = '', + searchLabel = '', + searchPlaceholder = '', + selectedId, + actions, + slots, + } = {}) => { + wrapper = extendedWrapper( + shallowMount(BoardAddNewColumnForm, { + stubs: { + GlFormGroup: true, + }, + propsData: { + loading, + formDescription, + searchLabel, + searchPlaceholder, + selectedId, + }, + slots, + store: createStore({ + actions: { + setAddColumnFormVisibility: jest.fn(), + ...actions, + }, + }), + }), + ); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + const formTitle = () => wrapper.findByTestId('board-add-column-form-title').text(); + const findSearchInput = () => wrapper.find(GlSearchBoxByType); + const findSearchLabel = () => wrapper.find(GlFormGroup); + const cancelButton = () => wrapper.findByTestId('cancelAddNewColumn'); + const submitButton = () => wrapper.findByTestId('addNewColumnButton'); + + it('shows form title & search input', () => { + mountComponent(); + + expect(formTitle()).toEqual(BoardAddNewColumnForm.i18n.newList); + expect(findSearchInput().exists()).toBe(true); + }); + + it('clicking cancel hides the form', () => { + const setAddColumnFormVisibility = jest.fn(); + mountComponent({ + actions: { + setAddColumnFormVisibility, + }, + }); + + cancelButton().vm.$emit('click'); + + expect(setAddColumnFormVisibility).toHaveBeenCalledWith(expect.anything(), false); + }); + + it('sets placeholder and description from props', () => { + const props = { + formDescription: 'Some description of a list', + }; + + mountComponent(props); + + expect(wrapper.html()).toHaveText(props.formDescription); + }); + + describe('items', () => { + const mountWithItems = (loading) => + mountComponent({ + loading, + slots: { + items: '<div class="item-slot">Some kind of list</div>', + }, + }); + + it('hides items slot and shows skeleton while loading', () => { + mountWithItems(true); + + expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true); + expect(wrapper.find('.item-slot').exists()).toBe(false); + }); + + it('shows items slot and hides skeleton while not loading', () => { + mountWithItems(false); + + expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false); + expect(wrapper.find('.item-slot').exists()).toBe(true); + }); + }); + + describe('search box', () => { + it('sets label and placeholder text from props', () => { + const props = { + searchLabel: 'Some items', + searchPlaceholder: 'Search for an item', + }; + + mountComponent(props); + + expect(findSearchLabel().attributes('label')).toEqual(props.searchLabel); + expect(findSearchInput().attributes('placeholder')).toEqual(props.searchPlaceholder); + }); + + it('emits filter event on input', () => { + mountComponent(); + + const searchText = 'some text'; + + findSearchInput().vm.$emit('input', searchText); + + expect(wrapper.emitted('filter-items')).toEqual([[searchText]]); + }); + }); + + describe('Add list button', () => { + it('is disabled if no item is selected', () => { + mountComponent(); + + expect(submitButton().props('disabled')).toBe(true); + }); + + it('emits add-list event on click', async () => { + mountComponent({ + selectedId: mockLabelList.label.id, + }); + + await nextTick(); + + submitButton().vm.$emit('click'); + + expect(wrapper.emitted('add-list')).toEqual([[]]); + }); + }); +}); diff --git a/spec/frontend/boards/components/board_add_new_column_spec.js b/spec/frontend/boards/components/board_add_new_column_spec.js new file mode 100644 index 00000000000..60584eaf6cf --- /dev/null +++ b/spec/frontend/boards/components/board_add_new_column_spec.js @@ -0,0 +1,115 @@ +import { shallowMount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; +import Vuex from 'vuex'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import BoardAddNewColumn from '~/boards/components/board_add_new_column.vue'; +import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue'; +import defaultState from '~/boards/stores/state'; +import { mockLabelList } from '../mock_data'; + +Vue.use(Vuex); + +describe('Board card layout', () => { + let wrapper; + + const createStore = ({ actions = {}, getters = {}, state = {} } = {}) => { + return new Vuex.Store({ + state: { + ...defaultState, + ...state, + }, + actions, + getters, + }); + }; + + const mountComponent = ({ + selectedId, + labels = [], + getListByLabelId = jest.fn(), + actions = {}, + } = {}) => { + wrapper = extendedWrapper( + shallowMount(BoardAddNewColumn, { + data() { + return { + selectedId, + }; + }, + store: createStore({ + actions: { + fetchLabels: jest.fn(), + setAddColumnFormVisibility: jest.fn(), + ...actions, + }, + getters: { + shouldUseGraphQL: () => true, + getListByLabelId: () => getListByLabelId, + }, + state: { + labels, + labelsLoading: false, + isEpicBoard: false, + }, + }), + provide: { + scopedLabelsAvailable: true, + }, + }), + ); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('Add list button', () => { + it('calls addList', async () => { + const getListByLabelId = jest.fn().mockReturnValue(null); + const highlightList = jest.fn(); + const createList = jest.fn(); + + mountComponent({ + labels: [mockLabelList.label], + selectedId: mockLabelList.label.id, + getListByLabelId, + actions: { + createList, + highlightList, + }, + }); + + wrapper.findComponent(BoardAddNewColumnForm).vm.$emit('add-list'); + + await nextTick(); + + expect(highlightList).not.toHaveBeenCalled(); + expect(createList).toHaveBeenCalledWith(expect.anything(), { + labelId: mockLabelList.label.id, + }); + }); + + it('highlights existing list if trying to re-add', async () => { + const getListByLabelId = jest.fn().mockReturnValue(mockLabelList); + const highlightList = jest.fn(); + const createList = jest.fn(); + + mountComponent({ + labels: [mockLabelList.label], + selectedId: mockLabelList.label.id, + getListByLabelId, + actions: { + createList, + highlightList, + }, + }); + + wrapper.findComponent(BoardAddNewColumnForm).vm.$emit('add-list'); + + await nextTick(); + + expect(highlightList).toHaveBeenCalledWith(expect.anything(), mockLabelList.id); + expect(createList).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/boards/components/board_card_deprecated_spec.js b/spec/frontend/boards/components/board_card_deprecated_spec.js new file mode 100644 index 00000000000..266cbc7106d --- /dev/null +++ b/spec/frontend/boards/components/board_card_deprecated_spec.js @@ -0,0 +1,219 @@ +/* global List */ +/* global ListAssignee */ +/* global ListLabel */ + +import { mount } from '@vue/test-utils'; + +import MockAdapter from 'axios-mock-adapter'; +import waitForPromises from 'helpers/wait_for_promises'; +import BoardCardDeprecated from '~/boards/components/board_card_deprecated.vue'; +import issueCardInner from '~/boards/components/issue_card_inner_deprecated.vue'; +import eventHub from '~/boards/eventhub'; +import store from '~/boards/stores'; +import boardsStore from '~/boards/stores/boards_store'; +import axios from '~/lib/utils/axios_utils'; + +import sidebarEventHub from '~/sidebar/event_hub'; +import '~/boards/models/label'; +import '~/boards/models/assignee'; +import '~/boards/models/list'; +import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; +import { listObj, boardsMockInterceptor, setMockEndpoints } from '../mock_data'; + +describe('BoardCard', () => { + let wrapper; + let mock; + let list; + + const findIssueCardInner = () => wrapper.find(issueCardInner); + const findUserAvatarLink = () => wrapper.find(userAvatarLink); + + // this particular mount component needs to be used after the root beforeEach because it depends on list being initialized + const mountComponent = (propsData) => { + wrapper = mount(BoardCardDeprecated, { + stubs: { + issueCardInner, + }, + store, + propsData: { + list, + issue: list.issues[0], + disabled: false, + index: 0, + ...propsData, + }, + provide: { + groupId: null, + rootPath: '/', + scopedLabelsAvailable: false, + }, + }); + }; + + const setupData = async () => { + list = new List(listObj); + boardsStore.create(); + boardsStore.detail.issue = {}; + const label1 = new ListLabel({ + id: 3, + title: 'testing 123', + color: '#000cff', + text_color: 'white', + description: 'test', + }); + await waitForPromises(); + + list.issues[0].labels.push(label1); + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onAny().reply(boardsMockInterceptor); + setMockEndpoints(); + return setupData(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + list = null; + mock.restore(); + }); + + it('when details issue is empty does not show the element', () => { + mountComponent(); + expect(wrapper.find('[data-testid="board_card"').classes()).not.toContain('is-active'); + }); + + it('when detailIssue is equal to card issue shows the element', () => { + [boardsStore.detail.issue] = list.issues; + mountComponent(); + + expect(wrapper.classes()).toContain('is-active'); + }); + + it('when multiSelect does not contain issue removes multi select class', () => { + mountComponent(); + expect(wrapper.classes()).not.toContain('multi-select'); + }); + + it('when multiSelect contain issue add multi select class', () => { + boardsStore.multiSelect.list = [list.issues[0]]; + mountComponent(); + + expect(wrapper.classes()).toContain('multi-select'); + }); + + it('adds user-can-drag class if not disabled', () => { + mountComponent(); + expect(wrapper.classes()).toContain('user-can-drag'); + }); + + it('does not add user-can-drag class disabled', () => { + mountComponent({ disabled: true }); + + expect(wrapper.classes()).not.toContain('user-can-drag'); + }); + + it('does not add disabled class', () => { + mountComponent(); + expect(wrapper.classes()).not.toContain('is-disabled'); + }); + + it('adds disabled class is disabled is true', () => { + mountComponent({ disabled: true }); + + expect(wrapper.classes()).toContain('is-disabled'); + }); + + describe('mouse events', () => { + it('does not set detail issue if showDetail is false', () => { + mountComponent(); + expect(boardsStore.detail.issue).toEqual({}); + }); + + it('does not set detail issue if link is clicked', () => { + mountComponent(); + findIssueCardInner().find('a').trigger('mouseup'); + + expect(boardsStore.detail.issue).toEqual({}); + }); + + it('does not set detail issue if img is clicked', () => { + mountComponent({ + issue: { + ...list.issues[0], + assignees: [ + new ListAssignee({ + id: 1, + name: 'testing 123', + username: 'test', + avatar: 'test_image', + }), + ], + }, + }); + + findUserAvatarLink().trigger('mouseup'); + + expect(boardsStore.detail.issue).toEqual({}); + }); + + it('does not set detail issue if showDetail is false after mouseup', () => { + mountComponent(); + wrapper.trigger('mouseup'); + + expect(boardsStore.detail.issue).toEqual({}); + }); + + it('sets detail issue to card issue on mouse up', () => { + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + + mountComponent(); + + wrapper.trigger('mousedown'); + wrapper.trigger('mouseup'); + + expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', wrapper.vm.issue, false); + expect(boardsStore.detail.list).toEqual(wrapper.vm.list); + }); + + it('resets detail issue to empty if already set', () => { + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + const [issue] = list.issues; + boardsStore.detail.issue = issue; + mountComponent(); + + wrapper.trigger('mousedown'); + wrapper.trigger('mouseup'); + + expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue', false); + }); + }); + + describe('sidebarHub events', () => { + it('closes all sidebars before showing an issue if no issues are opened', () => { + jest.spyOn(sidebarEventHub, '$emit').mockImplementation(() => {}); + boardsStore.detail.issue = {}; + mountComponent(); + + // sets conditional so that event is emitted. + wrapper.trigger('mousedown'); + + wrapper.trigger('mouseup'); + + expect(sidebarEventHub.$emit).toHaveBeenCalledWith('sidebar.closeAll'); + }); + + it('it does not closes all sidebars before showing an issue if an issue is opened', () => { + jest.spyOn(sidebarEventHub, '$emit').mockImplementation(() => {}); + const [issue] = list.issues; + boardsStore.detail.issue = issue; + mountComponent(); + + wrapper.trigger('mousedown'); + + expect(sidebarEventHub.$emit).not.toHaveBeenCalledWith('sidebar.closeAll'); + }); + }); +}); diff --git a/spec/frontend/boards/components/board_card_layout_deprecated_spec.js b/spec/frontend/boards/components/board_card_layout_deprecated_spec.js index 426c5289ba6..9853c9f434f 100644 --- a/spec/frontend/boards/components/board_card_layout_deprecated_spec.js +++ b/spec/frontend/boards/components/board_card_layout_deprecated_spec.js @@ -11,7 +11,7 @@ import '~/boards/models/label'; import '~/boards/models/assignee'; import '~/boards/models/list'; import BoardCardLayout from '~/boards/components/board_card_layout_deprecated.vue'; -import issueCardInner from '~/boards/components/issue_card_inner.vue'; +import issueCardInner from '~/boards/components/issue_card_inner_deprecated.vue'; import { ISSUABLE } from '~/boards/constants'; import boardsVuexStore from '~/boards/stores'; import boardsStore from '~/boards/stores/boards_store'; diff --git a/spec/frontend/boards/components/board_card_layout_spec.js b/spec/frontend/boards/components/board_card_layout_spec.js deleted file mode 100644 index 3fa8714807c..00000000000 --- a/spec/frontend/boards/components/board_card_layout_spec.js +++ /dev/null @@ -1,116 +0,0 @@ -import { createLocalVue, shallowMount } from '@vue/test-utils'; -import Vuex from 'vuex'; - -import BoardCardLayout from '~/boards/components/board_card_layout.vue'; -import IssueCardInner from '~/boards/components/issue_card_inner.vue'; -import { ISSUABLE } from '~/boards/constants'; -import defaultState from '~/boards/stores/state'; -import { mockLabelList, mockIssue } from '../mock_data'; - -describe('Board card layout', () => { - let wrapper; - let store; - - const localVue = createLocalVue(); - localVue.use(Vuex); - - const createStore = ({ getters = {}, actions = {} } = {}) => { - store = new Vuex.Store({ - state: defaultState, - actions, - getters, - }); - }; - - // this particular mount component needs to be used after the root beforeEach because it depends on list being initialized - const mountComponent = ({ propsData = {}, provide = {} } = {}) => { - wrapper = shallowMount(BoardCardLayout, { - localVue, - stubs: { - IssueCardInner, - }, - store, - propsData: { - list: mockLabelList, - issue: mockIssue, - disabled: false, - index: 0, - ...propsData, - }, - provide: { - groupId: null, - rootPath: '/', - scopedLabelsAvailable: false, - ...provide, - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - describe('mouse events', () => { - it('sets showDetail to true on mousedown', async () => { - createStore(); - mountComponent(); - - wrapper.trigger('mousedown'); - await wrapper.vm.$nextTick(); - - expect(wrapper.vm.showDetail).toBe(true); - }); - - it('sets showDetail to false on mousemove', async () => { - createStore(); - mountComponent(); - wrapper.trigger('mousedown'); - await wrapper.vm.$nextTick(); - expect(wrapper.vm.showDetail).toBe(true); - wrapper.trigger('mousemove'); - await wrapper.vm.$nextTick(); - expect(wrapper.vm.showDetail).toBe(false); - }); - - it("calls 'setActiveId'", async () => { - const setActiveId = jest.fn(); - createStore({ - actions: { - setActiveId, - }, - }); - mountComponent(); - - wrapper.trigger('mouseup'); - await wrapper.vm.$nextTick(); - - expect(setActiveId).toHaveBeenCalledTimes(1); - expect(setActiveId).toHaveBeenCalledWith(expect.any(Object), { - id: mockIssue.id, - sidebarType: ISSUABLE, - }); - }); - - it("calls 'setActiveId' when epic swimlanes is active", async () => { - const setActiveId = jest.fn(); - const isSwimlanesOn = () => true; - createStore({ - getters: { isSwimlanesOn }, - actions: { - setActiveId, - }, - }); - mountComponent(); - - wrapper.trigger('mouseup'); - await wrapper.vm.$nextTick(); - - expect(setActiveId).toHaveBeenCalledTimes(1); - expect(setActiveId).toHaveBeenCalledWith(expect.any(Object), { - id: mockIssue.id, - sidebarType: ISSUABLE, - }); - }); - }); -}); diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js index 5f26ae1bb3b..022f8c05e1e 100644 --- a/spec/frontend/boards/components/board_card_spec.js +++ b/spec/frontend/boards/components/board_card_spec.js @@ -1,43 +1,50 @@ -/* global List */ -/* global ListAssignee */ -/* global ListLabel */ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import Vuex from 'vuex'; -import { mount } from '@vue/test-utils'; - -import MockAdapter from 'axios-mock-adapter'; -import waitForPromises from 'helpers/wait_for_promises'; import BoardCard from '~/boards/components/board_card.vue'; -import issueCardInner from '~/boards/components/issue_card_inner.vue'; -import eventHub from '~/boards/eventhub'; -import store from '~/boards/stores'; -import boardsStore from '~/boards/stores/boards_store'; -import axios from '~/lib/utils/axios_utils'; - -import sidebarEventHub from '~/sidebar/event_hub'; -import '~/boards/models/label'; -import '~/boards/models/assignee'; -import '~/boards/models/list'; -import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; -import { listObj, boardsMockInterceptor, setMockEndpoints } from '../mock_data'; - -describe('BoardCard', () => { - let wrapper; - let mock; - let list; +import BoardCardInner from '~/boards/components/board_card_inner.vue'; +import { inactiveId } from '~/boards/constants'; +import { mockLabelList, mockIssue } from '../mock_data'; - const findIssueCardInner = () => wrapper.find(issueCardInner); - const findUserAvatarLink = () => wrapper.find(userAvatarLink); +describe('Board card', () => { + let wrapper; + let store; + let mockActions; + + const localVue = createLocalVue(); + localVue.use(Vuex); + + const createStore = ({ initialState = {}, isSwimlanesOn = false } = {}) => { + mockActions = { + toggleBoardItem: jest.fn(), + toggleBoardItemMultiSelection: jest.fn(), + }; + + store = new Vuex.Store({ + state: { + activeId: inactiveId, + selectedBoardItems: [], + ...initialState, + }, + actions: mockActions, + getters: { + isSwimlanesOn: () => isSwimlanesOn, + isEpicBoard: () => false, + }, + }); + }; // this particular mount component needs to be used after the root beforeEach because it depends on list being initialized - const mountComponent = (propsData) => { - wrapper = mount(BoardCard, { + const mountComponent = ({ propsData = {}, provide = {} } = {}) => { + wrapper = shallowMount(BoardCard, { + localVue, stubs: { - issueCardInner, + BoardCardInner, }, store, propsData: { - list, - issue: list.issues[0], + list: mockLabelList, + item: mockIssue, disabled: false, index: 0, ...propsData, @@ -46,174 +53,94 @@ describe('BoardCard', () => { groupId: null, rootPath: '/', scopedLabelsAvailable: false, + ...provide, }, }); }; - const setupData = async () => { - list = new List(listObj); - boardsStore.create(); - boardsStore.detail.issue = {}; - const label1 = new ListLabel({ - id: 3, - title: 'testing 123', - color: '#000cff', - text_color: 'white', - description: 'test', - }); - await waitForPromises(); - - list.issues[0].labels.push(label1); + const selectCard = async () => { + wrapper.trigger('mouseup'); + await wrapper.vm.$nextTick(); }; - beforeEach(() => { - mock = new MockAdapter(axios); - mock.onAny().reply(boardsMockInterceptor); - setMockEndpoints(); - return setupData(); - }); + const multiSelectCard = async () => { + wrapper.trigger('mouseup', { ctrlKey: true }); + await wrapper.vm.$nextTick(); + }; afterEach(() => { wrapper.destroy(); wrapper = null; - list = null; - mock.restore(); - }); - - it('when details issue is empty does not show the element', () => { - mountComponent(); - expect(wrapper.find('[data-testid="board_card"').classes()).not.toContain('is-active'); - }); - - it('when detailIssue is equal to card issue shows the element', () => { - [boardsStore.detail.issue] = list.issues; - mountComponent(); - - expect(wrapper.classes()).toContain('is-active'); - }); - - it('when multiSelect does not contain issue removes multi select class', () => { - mountComponent(); - expect(wrapper.classes()).not.toContain('multi-select'); - }); - - it('when multiSelect contain issue add multi select class', () => { - boardsStore.multiSelect.list = [list.issues[0]]; - mountComponent(); - - expect(wrapper.classes()).toContain('multi-select'); - }); - - it('adds user-can-drag class if not disabled', () => { - mountComponent(); - expect(wrapper.classes()).toContain('user-can-drag'); - }); - - it('does not add user-can-drag class disabled', () => { - mountComponent({ disabled: true }); - - expect(wrapper.classes()).not.toContain('user-can-drag'); - }); - - it('does not add disabled class', () => { - mountComponent(); - expect(wrapper.classes()).not.toContain('is-disabled'); + store = null; }); - it('adds disabled class is disabled is true', () => { - mountComponent({ disabled: true }); - - expect(wrapper.classes()).toContain('is-disabled'); - }); - - describe('mouse events', () => { - it('does not set detail issue if showDetail is false', () => { + describe.each` + isSwimlanesOn + ${true} | ${false} + `('when isSwimlanesOn is $isSwimlanesOn', ({ isSwimlanesOn }) => { + it('should not highlight the card by default', async () => { + createStore({ isSwimlanesOn }); mountComponent(); - expect(boardsStore.detail.issue).toEqual({}); - }); - it('does not set detail issue if link is clicked', () => { - mountComponent(); - findIssueCardInner().find('a').trigger('mouseup'); - - expect(boardsStore.detail.issue).toEqual({}); + expect(wrapper.classes()).not.toContain('is-active'); + expect(wrapper.classes()).not.toContain('multi-select'); }); - it('does not set detail issue if img is clicked', () => { - mountComponent({ - issue: { - ...list.issues[0], - assignees: [ - new ListAssignee({ - id: 1, - name: 'testing 123', - username: 'test', - avatar: 'test_image', - }), - ], + it('should highlight the card with a correct style when selected', async () => { + createStore({ + initialState: { + activeId: mockIssue.id, }, + isSwimlanesOn, }); - - findUserAvatarLink().trigger('mouseup'); - - expect(boardsStore.detail.issue).toEqual({}); - }); - - it('does not set detail issue if showDetail is false after mouseup', () => { - mountComponent(); - wrapper.trigger('mouseup'); - - expect(boardsStore.detail.issue).toEqual({}); - }); - - it('sets detail issue to card issue on mouse up', () => { - jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - mountComponent(); - wrapper.trigger('mousedown'); - wrapper.trigger('mouseup'); - - expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', wrapper.vm.issue, false); - expect(boardsStore.detail.list).toEqual(wrapper.vm.list); + expect(wrapper.classes()).toContain('is-active'); + expect(wrapper.classes()).not.toContain('multi-select'); }); - it('resets detail issue to empty if already set', () => { - jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - const [issue] = list.issues; - boardsStore.detail.issue = issue; + it('should highlight the card with a correct style when multi-selected', async () => { + createStore({ + initialState: { + activeId: inactiveId, + selectedBoardItems: [mockIssue], + }, + isSwimlanesOn, + }); mountComponent(); - wrapper.trigger('mousedown'); - wrapper.trigger('mouseup'); - - expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue', false); + expect(wrapper.classes()).toContain('multi-select'); + expect(wrapper.classes()).not.toContain('is-active'); }); - }); - - describe('sidebarHub events', () => { - it('closes all sidebars before showing an issue if no issues are opened', () => { - jest.spyOn(sidebarEventHub, '$emit').mockImplementation(() => {}); - boardsStore.detail.issue = {}; - mountComponent(); - - // sets conditional so that event is emitted. - wrapper.trigger('mousedown'); - wrapper.trigger('mouseup'); + describe('when mouseup event is called on the card', () => { + beforeEach(() => { + createStore({ isSwimlanesOn }); + mountComponent(); + }); - expect(sidebarEventHub.$emit).toHaveBeenCalledWith('sidebar.closeAll'); - }); + describe('when not using multi-select', () => { + it('should call vuex action "toggleBoardItem" with correct parameters', async () => { + await selectCard(); - it('it does not closes all sidebars before showing an issue if an issue is opened', () => { - jest.spyOn(sidebarEventHub, '$emit').mockImplementation(() => {}); - const [issue] = list.issues; - boardsStore.detail.issue = issue; - mountComponent(); + expect(mockActions.toggleBoardItem).toHaveBeenCalledTimes(1); + expect(mockActions.toggleBoardItem).toHaveBeenCalledWith(expect.any(Object), { + boardItem: mockIssue, + }); + }); + }); - wrapper.trigger('mousedown'); + describe('when using multi-select', () => { + it('should call vuex action "multiSelectBoardItem" with correct parameters', async () => { + await multiSelectCard(); - expect(sidebarEventHub.$emit).not.toHaveBeenCalledWith('sidebar.closeAll'); + expect(mockActions.toggleBoardItemMultiSelection).toHaveBeenCalledTimes(1); + expect(mockActions.toggleBoardItemMultiSelection).toHaveBeenCalledWith( + expect.any(Object), + mockIssue, + ); + }); + }); }); }); }); diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js index 858efea99ad..32499bd5480 100644 --- a/spec/frontend/boards/components/board_form_spec.js +++ b/spec/frontend/boards/components/board_form_spec.js @@ -8,6 +8,7 @@ import { formType } from '~/boards/constants'; import createBoardMutation from '~/boards/graphql/board_create.mutation.graphql'; import destroyBoardMutation from '~/boards/graphql/board_destroy.mutation.graphql'; import updateBoardMutation from '~/boards/graphql/board_update.mutation.graphql'; +import { createStore } from '~/boards/stores'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { visitUrl } from '~/lib/utils/url_utility'; @@ -48,6 +49,13 @@ describe('BoardForm', () => { const findDeleteConfirmation = () => wrapper.find('[data-testid="delete-confirmation-message"]'); const findInput = () => wrapper.find('#board-new-name'); + const store = createStore({ + getters: { + isGroupBoard: () => true, + isProjectBoard: () => false, + }, + }); + const createComponent = (props, data) => { wrapper = shallowMount(BoardForm, { propsData: { ...defaultProps, ...props }, @@ -64,6 +72,7 @@ describe('BoardForm', () => { mutate, }, }, + store, attachTo: document.body, }); }; diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js index f30e3792435..d2dfb4148b3 100644 --- a/spec/frontend/boards/components/board_list_header_spec.js +++ b/spec/frontend/boards/components/board_list_header_spec.js @@ -1,5 +1,6 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { mockLabelList } from 'jest/boards/mock_data'; import BoardListHeader from '~/boards/components/board_list_header.vue'; @@ -14,6 +15,7 @@ describe('Board List Header Component', () => { let store; const updateListSpy = jest.fn(); + const toggleListCollapsedSpy = jest.fn(); afterEach(() => { wrapper.destroy(); @@ -43,38 +45,39 @@ describe('Board List Header Component', () => { if (withLocalStorage) { localStorage.setItem( - `boards.${boardId}.${listMock.listType}.${listMock.id}.expanded`, - (!collapsed).toString(), + `boards.${boardId}.${listMock.listType}.${listMock.id}.collapsed`, + collapsed.toString(), ); } store = new Vuex.Store({ state: {}, - actions: { updateList: updateListSpy }, - getters: {}, + actions: { updateList: updateListSpy, toggleListCollapsed: toggleListCollapsedSpy }, + getters: { isEpicBoard: () => false }, }); - wrapper = shallowMount(BoardListHeader, { - store, - localVue, - propsData: { - disabled: false, - list: listMock, - }, - provide: { - boardId, - weightFeatureAvailable: false, - currentUserId, - }, - }); + wrapper = extendedWrapper( + shallowMount(BoardListHeader, { + store, + localVue, + propsData: { + disabled: false, + list: listMock, + }, + provide: { + boardId, + weightFeatureAvailable: false, + currentUserId, + }, + }), + ); }; 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'); + const findCaret = () => wrapper.findByTestId('board-title-caret'); describe('Add issue button', () => { const hasNoAddButton = [ListType.closed]; @@ -114,40 +117,29 @@ describe('Board List Header Component', () => { }); describe('expanding / collapsing the column', () => { - it('does not collapse when clicking the header', async () => { + it('should display collapse icon when column is expanded', async () => { createComponent(); - expect(isCollapsed()).toBe(false); - - wrapper.find('[data-testid="board-list-header"]').trigger('click'); + const icon = findCaret(); - await wrapper.vm.$nextTick(); - - expect(isCollapsed()).toBe(false); + expect(icon.props('icon')).toBe('chevron-right'); }); - it('collapses expanded Column when clicking the collapse icon', async () => { - createComponent(); - - expect(isCollapsed()).toBe(false); - - findCaret().vm.$emit('click'); + it('should display expand icon when column is collapsed', async () => { + createComponent({ collapsed: true }); - await wrapper.vm.$nextTick(); + const icon = findCaret(); - expect(isCollapsed()).toBe(true); + expect(icon.props('icon')).toBe('chevron-down'); }); - it('expands collapsed Column when clicking the expand icon', async () => { - createComponent({ collapsed: true }); - - expect(isCollapsed()).toBe(true); + it('should dispatch toggleListCollapse when clicking the collapse icon', async () => { + createComponent(); findCaret().vm.$emit('click'); await wrapper.vm.$nextTick(); - - expect(isCollapsed()).toBe(false); + expect(toggleListCollapsedSpy).toHaveBeenCalledTimes(1); }); it("when logged in it calls list update and doesn't set localStorage", async () => { @@ -157,7 +149,7 @@ describe('Board List Header Component', () => { await wrapper.vm.$nextTick(); expect(updateListSpy).toHaveBeenCalledTimes(1); - expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(null); + expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.collapsed`)).toBe(null); }); it("when logged out it doesn't call list update and sets localStorage", async () => { @@ -167,7 +159,7 @@ describe('Board List Header Component', () => { await wrapper.vm.$nextTick(); expect(updateListSpy).not.toHaveBeenCalled(); - expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(String(isExpanded())); + 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 ce8c95527e9..737a18294bc 100644 --- a/spec/frontend/boards/components/board_new_issue_spec.js +++ b/spec/frontend/boards/components/board_new_issue_spec.js @@ -2,7 +2,6 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; import BoardNewIssue from '~/boards/components/board_new_issue.vue'; -import '~/boards/models/list'; import { mockList, mockGroupProjects } from '../mock_data'; const localVue = createLocalVue(); @@ -31,7 +30,7 @@ describe('Issue boards new issue form', () => { const store = new Vuex.Store({ state: { selectedProject: mockGroupProjects[0] }, actions: { addListNewIssue: addListNewIssuesSpy }, - getters: {}, + getters: { isGroupBoard: () => false, isProjectBoard: () => true }, }); wrapper = shallowMount(BoardNewIssue, { diff --git a/spec/frontend/boards/components/filtered_search_spec.js b/spec/frontend/boards/components/filtered_search_spec.js new file mode 100644 index 00000000000..7f238aa671f --- /dev/null +++ b/spec/frontend/boards/components/filtered_search_spec.js @@ -0,0 +1,65 @@ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import Vuex from 'vuex'; +import FilteredSearch from '~/boards/components/filtered_search.vue'; +import { createStore } from '~/boards/stores'; +import * as commonUtils from '~/lib/utils/common_utils'; +import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('FilteredSearch', () => { + let wrapper; + let store; + + const createComponent = () => { + wrapper = shallowMount(FilteredSearch, { + localVue, + propsData: { search: '' }, + store, + attachTo: document.body, + }); + }; + + beforeEach(() => { + // this needed for actions call for performSearch + window.gon = { features: {} }; + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('default', () => { + beforeEach(() => { + store = createStore(); + + jest.spyOn(store, 'dispatch'); + + createComponent(); + }); + + it('finds FilteredSearch', () => { + expect(wrapper.find(FilteredSearchBarRoot).exists()).toBe(true); + }); + + describe('when onFilter is emitted', () => { + it('calls performSearch', () => { + wrapper.find(FilteredSearchBarRoot).vm.$emit('onFilter', [{ value: { data: '' } }]); + + expect(store.dispatch).toHaveBeenCalledWith('performSearch'); + }); + + it('calls historyPushState', () => { + commonUtils.historyPushState = jest.fn(); + wrapper + .find(FilteredSearchBarRoot) + .vm.$emit('onFilter', [{ value: { data: 'searchQuery' } }]); + + expect(commonUtils.historyPushState).toHaveBeenCalledWith( + 'http://test.host/?search=searchQuery', + ); + }); + }); + }); +}); diff --git a/spec/frontend/boards/components/issue_count_spec.js b/spec/frontend/boards/components/item_count_spec.js index f1870e9cc9e..45980c36f1c 100644 --- a/spec/frontend/boards/components/issue_count_spec.js +++ b/spec/frontend/boards/components/item_count_spec.js @@ -1,10 +1,10 @@ import { shallowMount } from '@vue/test-utils'; -import IssueCount from '~/boards/components/issue_count.vue'; +import IssueCount from '~/boards/components/item_count.vue'; describe('IssueCount', () => { let vm; let maxIssueCount; - let issuesSize; + let itemsSize; const createComponent = (props) => { vm = shallowMount(IssueCount, { propsData: props }); @@ -12,20 +12,20 @@ describe('IssueCount', () => { afterEach(() => { maxIssueCount = 0; - issuesSize = 0; + itemsSize = 0; if (vm) vm.destroy(); }); describe('when maxIssueCount is zero', () => { beforeEach(() => { - issuesSize = 3; + itemsSize = 3; - createComponent({ maxIssueCount: 0, issuesSize }); + createComponent({ maxIssueCount: 0, itemsSize }); }); it('contains issueSize in the template', () => { - expect(vm.find('.js-issue-size').text()).toEqual(String(issuesSize)); + expect(vm.find('[data-testid="board-items-count"]').text()).toEqual(String(itemsSize)); }); it('does not contains maxIssueCount in the template', () => { @@ -36,9 +36,9 @@ describe('IssueCount', () => { describe('when maxIssueCount is greater than zero', () => { beforeEach(() => { maxIssueCount = 2; - issuesSize = 1; + itemsSize = 1; - createComponent({ maxIssueCount, issuesSize }); + createComponent({ maxIssueCount, itemsSize }); }); afterEach(() => { @@ -46,7 +46,7 @@ describe('IssueCount', () => { }); it('contains issueSize in the template', () => { - expect(vm.find('.js-issue-size').text()).toEqual(String(issuesSize)); + expect(vm.find('[data-testid="board-items-count"]').text()).toEqual(String(itemsSize)); }); it('contains maxIssueCount in the template', () => { @@ -60,10 +60,10 @@ describe('IssueCount', () => { describe('when issueSize is greater than maxIssueCount', () => { beforeEach(() => { - issuesSize = 3; + itemsSize = 3; maxIssueCount = 2; - createComponent({ maxIssueCount, issuesSize }); + createComponent({ maxIssueCount, itemsSize }); }); afterEach(() => { @@ -71,7 +71,7 @@ describe('IssueCount', () => { }); it('contains issueSize in the template', () => { - expect(vm.find('.js-issue-size').text()).toEqual(String(issuesSize)); + expect(vm.find('[data-testid="board-items-count"]').text()).toEqual(String(itemsSize)); }); it('contains maxIssueCount in the template', () => { @@ -79,7 +79,7 @@ describe('IssueCount', () => { }); it('has text-danger class', () => { - expect(vm.find('.text-danger').text()).toEqual(String(issuesSize)); + expect(vm.find('.text-danger').text()).toEqual(String(itemsSize)); }); }); }); diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js index 7838b5a0b2f..8fd178a0856 100644 --- a/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js +++ b/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js @@ -24,7 +24,7 @@ describe('~/boards/components/sidebar/board_sidebar_due_date.vue', () => { const createWrapper = ({ dueDate = null } = {}) => { store = createStore(); - store.state.issues = { [TEST_ISSUE.id]: { ...TEST_ISSUE, dueDate } }; + store.state.boardItems = { [TEST_ISSUE.id]: { ...TEST_ISSUE, dueDate } }; store.state.activeId = TEST_ISSUE.id; wrapper = shallowMount(BoardSidebarDueDate, { @@ -61,7 +61,7 @@ describe('~/boards/components/sidebar/board_sidebar_due_date.vue', () => { createWrapper(); jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => { - store.state.issues[TEST_ISSUE.id].dueDate = TEST_DUE_DATE; + store.state.boardItems[TEST_ISSUE.id].dueDate = TEST_DUE_DATE; }); findDatePicker().vm.$emit('input', TEST_PARSED_DATE); await wrapper.vm.$nextTick(); @@ -86,7 +86,7 @@ describe('~/boards/components/sidebar/board_sidebar_due_date.vue', () => { createWrapper(); jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => { - store.state.issues[TEST_ISSUE.id].dueDate = null; + store.state.boardItems[TEST_ISSUE.id].dueDate = null; }); findDatePicker().vm.$emit('clear'); await wrapper.vm.$nextTick(); @@ -104,7 +104,7 @@ describe('~/boards/components/sidebar/board_sidebar_due_date.vue', () => { createWrapper({ dueDate: TEST_DUE_DATE }); jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => { - store.state.issues[TEST_ISSUE.id].dueDate = null; + store.state.boardItems[TEST_ISSUE.id].dueDate = null; }); findResetButton().vm.$emit('click'); await wrapper.vm.$nextTick(); diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_issue_title_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_issue_title_spec.js index bc7df1c76c6..723d0345f76 100644 --- a/spec/frontend/boards/components/sidebar/board_sidebar_issue_title_spec.js +++ b/spec/frontend/boards/components/sidebar/board_sidebar_issue_title_spec.js @@ -34,7 +34,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => { const createWrapper = (issue = TEST_ISSUE_A) => { store = createStore(); - store.state.issues = { [issue.id]: { ...issue } }; + store.state.boardItems = { [issue.id]: { ...issue } }; store.dispatch('setActiveId', { id: issue.id }); wrapper = shallowMount(BoardSidebarIssueTitle, { @@ -74,7 +74,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => { createWrapper(); jest.spyOn(wrapper.vm, 'setActiveIssueTitle').mockImplementation(() => { - store.state.issues[TEST_ISSUE_A.id].title = TEST_TITLE; + store.state.boardItems[TEST_ISSUE_A.id].title = TEST_TITLE; }); findFormInput().vm.$emit('input', TEST_TITLE); findForm().vm.$emit('submit', { preventDefault: () => {} }); @@ -147,7 +147,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => { createWrapper(TEST_ISSUE_B); jest.spyOn(wrapper.vm, 'setActiveIssueTitle').mockImplementation(() => { - store.state.issues[TEST_ISSUE_B.id].title = TEST_TITLE; + store.state.boardItems[TEST_ISSUE_B.id].title = TEST_TITLE; }); findFormInput().vm.$emit('input', TEST_TITLE); findCancelButton().vm.$emit('click'); diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js index 12b873ba7d8..98ac211238c 100644 --- a/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js +++ b/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js @@ -25,7 +25,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => { const createWrapper = ({ labels = [] } = {}) => { store = createStore(); - store.state.issues = { [TEST_ISSUE.id]: { ...TEST_ISSUE, labels } }; + store.state.boardItems = { [TEST_ISSUE.id]: { ...TEST_ISSUE, labels } }; store.state.activeId = TEST_ISSUE.id; wrapper = shallowMount(BoardSidebarLabelsSelect, { @@ -66,7 +66,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => { jest.spyOn(wrapper.vm, 'setActiveIssueLabels').mockImplementation(() => TEST_LABELS); findLabelsSelect().vm.$emit('updateSelectedLabels', TEST_LABELS_PAYLOAD); - store.state.issues[TEST_ISSUE.id].labels = TEST_LABELS; + store.state.boardItems[TEST_ISSUE.id].labels = TEST_LABELS; await wrapper.vm.$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 index 8820ec7ae63..8706424a296 100644 --- a/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js +++ b/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js @@ -22,7 +22,7 @@ describe('~/boards/components/sidebar/board_sidebar_milestone_select.vue', () => const createWrapper = ({ milestone = null, loading = false } = {}) => { store = createStore(); - store.state.issues = { [TEST_ISSUE.id]: { ...TEST_ISSUE, milestone } }; + store.state.boardItems = { [TEST_ISSUE.id]: { ...TEST_ISSUE, milestone } }; store.state.activeId = TEST_ISSUE.id; wrapper = shallowMount(BoardSidebarMilestoneSelect, { @@ -113,7 +113,7 @@ describe('~/boards/components/sidebar/board_sidebar_milestone_select.vue', () => createWrapper(); jest.spyOn(wrapper.vm, 'setActiveIssueMilestone').mockImplementation(() => { - store.state.issues[TEST_ISSUE.id].milestone = TEST_MILESTONE; + store.state.boardItems[TEST_ISSUE.id].milestone = TEST_MILESTONE; }); findDropdownItem().vm.$emit('click'); await wrapper.vm.$nextTick(); @@ -137,7 +137,7 @@ describe('~/boards/components/sidebar/board_sidebar_milestone_select.vue', () => createWrapper({ milestone: TEST_MILESTONE }); jest.spyOn(wrapper.vm, 'setActiveIssueMilestone').mockImplementation(() => { - store.state.issues[TEST_ISSUE.id].milestone = null; + store.state.boardItems[TEST_ISSUE.id].milestone = null; }); findUnsetMilestoneItem().vm.$emit('click'); await wrapper.vm.$nextTick(); diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js index 3e6b0be0267..cfd7f32b2cc 100644 --- a/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js +++ b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js @@ -22,7 +22,7 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () = const createComponent = (activeIssue = { ...mockActiveIssue }) => { store = createStore(); - store.state.issues = { [activeIssue.id]: activeIssue }; + store.state.boardItems = { [activeIssue.id]: activeIssue }; store.state.activeId = activeIssue.id; wrapper = mount(BoardSidebarSubscription, { @@ -45,6 +45,12 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () = expect(findNotificationHeader().text()).toBe('Notifications'); }); + it('renders toggle with label', () => { + createComponent(); + + expect(findToggle().props('label')).toBe(BoardSidebarSubscription.i18n.header.title); + }); + it('renders toggle as "off" when currently not subscribed', () => { createComponent(); diff --git a/spec/frontend/boards/components/sidebar/remove_issue_spec.js b/spec/frontend/boards/components/sidebar/remove_issue_spec.js deleted file mode 100644 index 1f740c10106..00000000000 --- a/spec/frontend/boards/components/sidebar/remove_issue_spec.js +++ /dev/null @@ -1,28 +0,0 @@ -import { GlButton } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; - -import RemoveIssue from '~/boards/components/sidebar/remove_issue.vue'; - -describe('boards sidebar remove issue', () => { - let wrapper; - - const findButton = () => wrapper.find(GlButton); - - const createComponent = (propsData) => { - wrapper = shallowMount(RemoveIssue, { - propsData: { - issue: {}, - list: {}, - ...propsData, - }, - }); - }; - - beforeEach(() => { - createComponent(); - }); - - it('renders remove button', () => { - expect(findButton().exists()).toBe(true); - }); -}); diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js index e106b9235d6..500240d00fc 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -351,6 +351,7 @@ export const issues = { [mockIssue4.id]: mockIssue4, }; +// The response from group project REST API export const mockRawGroupProjects = [ { id: 0, @@ -366,17 +367,34 @@ export const mockRawGroupProjects = [ }, ]; -export const mockGroupProjects = [ - { - id: 0, - name: 'Example Project', - nameWithNamespace: 'Awesome Group / Example Project', - fullPath: 'awesome-group/example-project', - }, - { - id: 1, - name: 'Foobar Project', - nameWithNamespace: 'Awesome Group / Foobar Project', - fullPath: 'awesome-group/foobar-project', - }, +// The response from GraphQL endpoint +export const mockGroupProject1 = { + id: 0, + name: 'Example Project', + nameWithNamespace: 'Awesome Group / Example Project', + fullPath: 'awesome-group/example-project', + archived: false, +}; + +export const mockGroupProject2 = { + id: 1, + name: 'Foobar Project', + nameWithNamespace: 'Awesome Group / Foobar Project', + fullPath: 'awesome-group/foobar-project', + archived: false, +}; + +export const mockArchivedGroupProject = { + id: 2, + name: 'Archived Project', + nameWithNamespace: 'Awesome Group / Archived Project', + fullPath: 'awesome-group/archived-project', + archived: true, +}; + +export const mockGroupProjects = [mockGroupProject1, mockGroupProject2]; + +export const mockActiveGroupProjects = [ + { ...mockGroupProject1, archived: false }, + { ...mockGroupProject2, archived: false }, ]; diff --git a/spec/frontend/boards/project_select_deprecated_spec.js b/spec/frontend/boards/project_select_deprecated_spec.js index 9042c4bf9ba..37f519ef5b9 100644 --- a/spec/frontend/boards/project_select_deprecated_spec.js +++ b/spec/frontend/boards/project_select_deprecated_spec.js @@ -27,6 +27,7 @@ const mockDefaultFetchOptions = { with_shared: false, include_subgroups: true, order_by: 'similarity', + archived: false, }; const itemsPerPage = 20; diff --git a/spec/frontend/boards/project_select_spec.js b/spec/frontend/boards/project_select_spec.js index aa71952c42b..de823094630 100644 --- a/spec/frontend/boards/project_select_spec.js +++ b/spec/frontend/boards/project_select_spec.js @@ -1,30 +1,17 @@ import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui'; -import { createLocalVue, mount } from '@vue/test-utils'; +import { mount } from '@vue/test-utils'; +import Vue from 'vue'; import Vuex from 'vuex'; import ProjectSelect from '~/boards/components/project_select.vue'; import defaultState from '~/boards/stores/state'; -import { mockList, mockGroupProjects } from './mock_data'; +import { mockList, mockActiveGroupProjects } from './mock_data'; -const localVue = createLocalVue(); -localVue.use(Vuex); - -const actions = { - fetchGroupProjects: jest.fn(), - setSelectedProject: jest.fn(), -}; - -const createStore = (state = defaultState) => { - return new Vuex.Store({ - state, - actions, - }); -}; - -const mockProjectsList1 = mockGroupProjects.slice(0, 1); +const mockProjectsList1 = mockActiveGroupProjects.slice(0, 1); describe('ProjectSelect component', () => { let wrapper; + let store; const findLabel = () => wrapper.find("[data-testid='header-label']"); const findGlDropdown = () => wrapper.find(GlDropdown); @@ -36,20 +23,37 @@ describe('ProjectSelect component', () => { const findInMenuLoadingIcon = () => wrapper.find("[data-testid='dropdown-text-loading-icon']"); const findEmptySearchMessage = () => wrapper.find("[data-testid='empty-result-message']"); - const createWrapper = (state = {}) => { - const store = createStore({ - groupProjects: [], - groupProjectsFlags: { - isLoading: false, - pageInfo: { - hasNextPage: false, + const createStore = ({ state, activeGroupProjects }) => { + Vue.use(Vuex); + + store = new Vuex.Store({ + state: { + defaultState, + groupProjectsFlags: { + isLoading: false, + pageInfo: { + hasNextPage: false, + }, }, + ...state, + }, + actions: { + fetchGroupProjects: jest.fn(), + setSelectedProject: jest.fn(), }, - ...state, + getters: { + activeGroupProjects: () => activeGroupProjects, + }, + }); + }; + + const createWrapper = ({ state = {}, activeGroupProjects = [] } = {}) => { + createStore({ + state, + activeGroupProjects, }); wrapper = mount(ProjectSelect, { - localVue, propsData: { list: mockList, }, @@ -93,7 +97,7 @@ describe('ProjectSelect component', () => { describe('when dropdown menu is open', () => { describe('by default', () => { beforeEach(() => { - createWrapper({ groupProjects: mockGroupProjects }); + createWrapper({ activeGroupProjects: mockActiveGroupProjects }); }); it('shows GlSearchBoxByType with default attributes', () => { @@ -128,7 +132,7 @@ describe('ProjectSelect component', () => { describe('when a project is selected', () => { beforeEach(() => { - createWrapper({ groupProjects: mockProjectsList1 }); + createWrapper({ activeGroupProjects: mockProjectsList1 }); findFirstGlDropdownItem().find('button').trigger('click'); }); @@ -142,7 +146,7 @@ describe('ProjectSelect component', () => { describe('when projects are loading', () => { beforeEach(() => { - createWrapper({ groupProjectsFlags: { isLoading: true } }); + createWrapper({ state: { groupProjectsFlags: { isLoading: true } } }); }); it('displays and hides gl-loading-icon while and after fetching data', () => { diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index 32d0e7ae886..69d2c8977fb 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -5,7 +5,7 @@ import { formatBoardLists, formatIssueInput, } from '~/boards/boards_util'; -import { inactiveId } from '~/boards/constants'; +import { inactiveId, ISSUABLE } from '~/boards/constants'; import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql'; import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql'; import issueMoveListMutation from '~/boards/graphql/issue_move_list.mutation.graphql'; @@ -112,6 +112,15 @@ describe('setActiveId', () => { }); describe('fetchLists', () => { + it('should dispatch fetchIssueLists action', () => { + testAction({ + action: actions.fetchLists, + expectedActions: [{ type: 'fetchIssueLists' }], + }); + }); +}); + +describe('fetchIssueLists', () => { const state = { fullPath: 'gitlab-org', boardId: '1', @@ -138,7 +147,7 @@ describe('fetchLists', () => { jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse); testAction( - actions.fetchLists, + actions.fetchIssueLists, {}, state, [ @@ -152,6 +161,23 @@ describe('fetchLists', () => { ); }); + it('should commit mutations RECEIVE_BOARD_LISTS_FAILURE on failure', (done) => { + jest.spyOn(gqlClient, 'query').mockResolvedValue(Promise.reject()); + + testAction( + actions.fetchIssueLists, + {}, + state, + [ + { + type: types.RECEIVE_BOARD_LISTS_FAILURE, + }, + ], + [], + done, + ); + }); + it('dispatch createList action when backlog list does not exist and is not hidden', (done) => { queryResponse = { data: { @@ -168,7 +194,7 @@ describe('fetchLists', () => { jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse); testAction( - actions.fetchLists, + actions.fetchIssueLists, {}, state, [ @@ -184,6 +210,16 @@ describe('fetchLists', () => { }); describe('createList', () => { + it('should dispatch createIssueList action', () => { + testAction({ + action: actions.createList, + payload: { backlog: true }, + expectedActions: [{ type: 'createIssueList', payload: { backlog: true } }], + }); + }); +}); + +describe('createIssueList', () => { let commit; let dispatch; let getters; @@ -223,7 +259,7 @@ describe('createList', () => { }), ); - await actions.createList({ getters, state, commit, dispatch }, { backlog: true }); + await actions.createIssueList({ getters, state, commit, dispatch }, { backlog: true }); expect(dispatch).toHaveBeenCalledWith('addList', backlogList); }); @@ -245,7 +281,7 @@ describe('createList', () => { }, }); - await actions.createList({ getters, state, commit, dispatch }, { labelId: '4' }); + await actions.createIssueList({ getters, state, commit, dispatch }, { labelId: '4' }); expect(dispatch).toHaveBeenCalledWith('addList', list); expect(dispatch).toHaveBeenCalledWith('highlightList', list.id); @@ -257,15 +293,15 @@ describe('createList', () => { data: { boardListCreate: { list: {}, - errors: [{ foo: 'bar' }], + errors: ['foo'], }, }, }), ); - await actions.createList({ getters, state, commit, dispatch }, { backlog: true }); + await actions.createIssueList({ getters, state, commit, dispatch }, { backlog: true }); - expect(commit).toHaveBeenCalledWith(types.CREATE_LIST_FAILURE); + expect(commit).toHaveBeenCalledWith(types.CREATE_LIST_FAILURE, 'foo'); }); it('highlights list and does not re-query if it already exists', async () => { @@ -280,7 +316,7 @@ describe('createList', () => { getListByLabelId: jest.fn().mockReturnValue(existingList), }; - await actions.createList({ getters, state, commit, dispatch }, { backlog: true }); + await actions.createIssueList({ getters, state, commit, dispatch }, { backlog: true }); expect(dispatch).toHaveBeenCalledWith('highlightList', existingList.id); expect(dispatch).toHaveBeenCalledTimes(1); @@ -301,11 +337,15 @@ describe('fetchLabels', () => { }; jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse); - await testAction({ - action: actions.fetchLabels, - state: { boardType: 'group' }, - expectedMutations: [{ type: types.RECEIVE_LABELS_SUCCESS, payload: labels }], - }); + const commit = jest.fn(); + const getters = { + shouldUseGraphQL: () => true, + }; + const state = { boardType: 'group' }; + + await actions.fetchLabels({ getters, state, commit }); + + expect(commit).toHaveBeenCalledWith(types.RECEIVE_LABELS_SUCCESS, labels); }); }); @@ -412,6 +452,22 @@ describe('updateList', () => { }); }); +describe('toggleListCollapsed', () => { + it('should commit TOGGLE_LIST_COLLAPSED mutation', async () => { + const payload = { listId: 'gid://gitlab/List/1', collapsed: true }; + await testAction({ + action: actions.toggleListCollapsed, + payload, + expectedMutations: [ + { + type: types.TOGGLE_LIST_COLLAPSED, + payload, + }, + ], + }); + }); +}); + describe('removeList', () => { let state; const list = mockLists[0]; @@ -490,7 +546,7 @@ describe('removeList', () => { }); }); -describe('fetchIssuesForList', () => { +describe('fetchItemsForList', () => { const listId = mockLists[0].id; const state = { @@ -533,21 +589,21 @@ describe('fetchIssuesForList', () => { [listId]: pageInfo, }; - it('should commit mutations REQUEST_ISSUES_FOR_LIST and RECEIVE_ISSUES_FOR_LIST_SUCCESS on success', (done) => { + it('should commit mutations REQUEST_ITEMS_FOR_LIST and RECEIVE_ITEMS_FOR_LIST_SUCCESS on success', (done) => { jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse); testAction( - actions.fetchIssuesForList, + actions.fetchItemsForList, { listId }, state, [ { - type: types.REQUEST_ISSUES_FOR_LIST, + type: types.REQUEST_ITEMS_FOR_LIST, payload: { listId, fetchNext: false }, }, { - type: types.RECEIVE_ISSUES_FOR_LIST_SUCCESS, - payload: { listIssues: formattedIssues, listPageInfo, listId }, + type: types.RECEIVE_ITEMS_FOR_LIST_SUCCESS, + payload: { listItems: formattedIssues, listPageInfo, listId }, }, ], [], @@ -555,19 +611,19 @@ describe('fetchIssuesForList', () => { ); }); - it('should commit mutations REQUEST_ISSUES_FOR_LIST and RECEIVE_ISSUES_FOR_LIST_FAILURE on failure', (done) => { + it('should commit mutations REQUEST_ITEMS_FOR_LIST and RECEIVE_ITEMS_FOR_LIST_FAILURE on failure', (done) => { jest.spyOn(gqlClient, 'query').mockResolvedValue(Promise.reject()); testAction( - actions.fetchIssuesForList, + actions.fetchItemsForList, { listId }, state, [ { - type: types.REQUEST_ISSUES_FOR_LIST, + type: types.REQUEST_ITEMS_FOR_LIST, payload: { listId, fetchNext: false }, }, - { type: types.RECEIVE_ISSUES_FOR_LIST_FAILURE, payload: listId }, + { type: types.RECEIVE_ITEMS_FOR_LIST_FAILURE, payload: listId }, ], [], done, @@ -581,6 +637,15 @@ describe('resetIssues', () => { }); }); +describe('moveItem', () => { + it('should dispatch moveIssue action', () => { + testAction({ + action: actions.moveItem, + expectedActions: [{ type: 'moveIssue' }], + }); + }); +}); + describe('moveIssue', () => { const listIssues = { 'gid://gitlab/List/1': [436, 437], @@ -598,8 +663,8 @@ describe('moveIssue', () => { boardType: 'group', disabled: false, boardLists: mockLists, - issuesByListId: listIssues, - issues, + boardItemsByListId: listIssues, + boardItems: issues, }; it('should commit MOVE_ISSUE mutation and MOVE_ISSUE_SUCCESS mutation when successful', (done) => { @@ -615,9 +680,9 @@ describe('moveIssue', () => { testAction( actions.moveIssue, { - issueId: '436', - issueIid: mockIssue.iid, - issuePath: mockIssue.referencePath, + itemId: '436', + itemIid: mockIssue.iid, + itemPath: mockIssue.referencePath, fromListId: 'gid://gitlab/List/1', toListId: 'gid://gitlab/List/2', }, @@ -666,9 +731,9 @@ describe('moveIssue', () => { actions.moveIssue( { state, commit: () => {} }, { - issueId: mockIssue.id, - issueIid: mockIssue.iid, - issuePath: mockIssue.referencePath, + itemId: mockIssue.id, + itemIid: mockIssue.iid, + itemPath: mockIssue.referencePath, fromListId: 'gid://gitlab/List/1', toListId: 'gid://gitlab/List/2', }, @@ -690,9 +755,9 @@ describe('moveIssue', () => { testAction( actions.moveIssue, { - issueId: '436', - issueIid: mockIssue.iid, - issuePath: mockIssue.referencePath, + itemId: '436', + itemIid: mockIssue.iid, + itemPath: mockIssue.referencePath, fromListId: 'gid://gitlab/List/1', toListId: 'gid://gitlab/List/2', }, @@ -879,7 +944,7 @@ describe('addListIssue', () => { }); describe('setActiveIssueLabels', () => { - const state = { issues: { [mockIssue.id]: mockIssue } }; + const state = { boardItems: { [mockIssue.id]: mockIssue } }; const getters = { activeIssue: mockIssue }; const testLabelIds = labels.map((label) => label.id); const input = { @@ -924,7 +989,7 @@ describe('setActiveIssueLabels', () => { }); describe('setActiveIssueDueDate', () => { - const state = { issues: { [mockIssue.id]: mockIssue } }; + const state = { boardItems: { [mockIssue.id]: mockIssue } }; const getters = { activeIssue: mockIssue }; const testDueDate = '2020-02-20'; const input = { @@ -975,7 +1040,7 @@ describe('setActiveIssueDueDate', () => { }); describe('setActiveIssueSubscribed', () => { - const state = { issues: { [mockActiveIssue.id]: mockActiveIssue } }; + const state = { boardItems: { [mockActiveIssue.id]: mockActiveIssue } }; const getters = { activeIssue: mockActiveIssue }; const subscribedState = true; const input = { @@ -1026,7 +1091,7 @@ describe('setActiveIssueSubscribed', () => { }); describe('setActiveIssueMilestone', () => { - const state = { issues: { [mockIssue.id]: mockIssue } }; + const state = { boardItems: { [mockIssue.id]: mockIssue } }; const getters = { activeIssue: mockIssue }; const testMilestone = { ...mockMilestone, @@ -1080,7 +1145,7 @@ describe('setActiveIssueMilestone', () => { }); describe('setActiveIssueTitle', () => { - const state = { issues: { [mockIssue.id]: mockIssue } }; + const state = { boardItems: { [mockIssue.id]: mockIssue } }; const getters = { activeIssue: mockIssue }; const testTitle = 'Test Title'; const input = { @@ -1220,6 +1285,7 @@ describe('setSelectedProject', () => { describe('toggleBoardItemMultiSelection', () => { const boardItem = mockIssue; + const boardItem2 = mockIssue2; it('should commit mutation ADD_BOARD_ITEM_TO_SELECTION if item is not on selection state', () => { testAction( @@ -1250,6 +1316,66 @@ describe('toggleBoardItemMultiSelection', () => { [], ); }); + + it('should additionally commit mutation ADD_BOARD_ITEM_TO_SELECTION for active issue and dispatch unsetActiveId', () => { + testAction( + actions.toggleBoardItemMultiSelection, + boardItem2, + { activeId: mockActiveIssue.id, activeIssue: mockActiveIssue, selectedBoardItems: [] }, + [ + { + type: types.ADD_BOARD_ITEM_TO_SELECTION, + payload: mockActiveIssue, + }, + { + type: types.ADD_BOARD_ITEM_TO_SELECTION, + payload: boardItem2, + }, + ], + [{ type: 'unsetActiveId' }], + ); + }); +}); + +describe('resetBoardItemMultiSelection', () => { + it('should commit mutation RESET_BOARD_ITEM_SELECTION', () => { + testAction({ + action: actions.resetBoardItemMultiSelection, + state: { selectedBoardItems: [mockIssue] }, + expectedMutations: [ + { + type: types.RESET_BOARD_ITEM_SELECTION, + }, + ], + }); + }); +}); + +describe('toggleBoardItem', () => { + it('should dispatch resetBoardItemMultiSelection and unsetActiveId when boardItem is the active item', () => { + testAction({ + action: actions.toggleBoardItem, + payload: { boardItem: mockIssue }, + state: { + activeId: mockIssue.id, + }, + expectedActions: [{ type: 'resetBoardItemMultiSelection' }, { type: 'unsetActiveId' }], + }); + }); + + it('should dispatch resetBoardItemMultiSelection and setActiveId when boardItem is not the active item', () => { + testAction({ + action: actions.toggleBoardItem, + payload: { boardItem: mockIssue }, + state: { + activeId: inactiveId, + }, + expectedActions: [ + { type: 'resetBoardItemMultiSelection' }, + { type: 'setActiveId', payload: { id: mockIssue.id, sidebarType: ISSUABLE } }, + ], + }); + }); }); describe('fetchBacklog', () => { diff --git a/spec/frontend/boards/stores/getters_spec.js b/spec/frontend/boards/stores/getters_spec.js index d5a19bf613f..32d73d861bc 100644 --- a/spec/frontend/boards/stores/getters_spec.js +++ b/spec/frontend/boards/stores/getters_spec.js @@ -7,9 +7,47 @@ import { mockIssuesByListId, issues, mockLists, + mockGroupProject1, + mockArchivedGroupProject, } from '../mock_data'; describe('Boards - Getters', () => { + describe('isGroupBoard', () => { + it('returns true when boardType on state is group', () => { + const state = { + boardType: 'group', + }; + + expect(getters.isGroupBoard(state)).toBe(true); + }); + + it('returns false when boardType on state is not group', () => { + const state = { + boardType: 'project', + }; + + expect(getters.isGroupBoard(state)).toBe(false); + }); + }); + + describe('isProjectBoard', () => { + it('returns true when boardType on state is project', () => { + const state = { + boardType: 'project', + }; + + expect(getters.isProjectBoard(state)).toBe(true); + }); + + it('returns false when boardType on state is not project', () => { + const state = { + boardType: 'group', + }; + + expect(getters.isProjectBoard(state)).toBe(false); + }); + }); + describe('isSidebarOpen', () => { it('returns true when activeId is not equal to 0', () => { const state = { @@ -38,15 +76,15 @@ describe('Boards - Getters', () => { }); }); - describe('getIssueById', () => { - const state = { issues: { 1: 'issue' } }; + describe('getBoardItemById', () => { + const state = { boardItems: { 1: 'issue' } }; it.each` id | expected ${'1'} | ${'issue'} ${''} | ${{}} `('returns $expected when $id is passed to state', ({ id, expected }) => { - expect(getters.getIssueById(state)(id)).toEqual(expected); + expect(getters.getBoardItemById(state)(id)).toEqual(expected); }); }); @@ -56,7 +94,7 @@ describe('Boards - Getters', () => { ${'1'} | ${'issue'} ${''} | ${{}} `('returns $expected when $id is passed to state', ({ id, expected }) => { - const state = { issues: { 1: 'issue' }, activeId: id }; + const state = { boardItems: { 1: 'issue' }, activeId: id }; expect(getters.activeIssue(state)).toEqual(expected); }); @@ -94,17 +132,18 @@ describe('Boards - Getters', () => { }); }); - describe('getIssuesByList', () => { + describe('getBoardItemsByList', () => { const boardsState = { - issuesByListId: mockIssuesByListId, - issues, + boardItemsByListId: mockIssuesByListId, + boardItems: issues, }; it('returns issues for a given listId', () => { - const getIssueById = (issueId) => [mockIssue, mockIssue2].find(({ id }) => id === issueId); + const getBoardItemById = (issueId) => + [mockIssue, mockIssue2].find(({ id }) => id === issueId); - expect(getters.getIssuesByList(boardsState, { getIssueById })('gid://gitlab/List/2')).toEqual( - mockIssues, - ); + expect( + getters.getBoardItemsByList(boardsState, { getBoardItemById })('gid://gitlab/List/2'), + ).toEqual(mockIssues); }); }); @@ -128,4 +167,14 @@ describe('Boards - Getters', () => { expect(getters.getListByTitle(boardsState)('To Do')).toEqual(mockLists[1]); }); }); + + describe('activeGroupProjects', () => { + const state = { + groupProjects: [mockGroupProject1, mockArchivedGroupProject], + }; + + it('returns only returns non-archived group projects', () => { + expect(getters.activeGroupProjects(state)).toEqual([mockGroupProject1]); + }); + }); }); diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js index 9423f2ed583..33897cc0250 100644 --- a/spec/frontend/boards/stores/mutations_spec.js +++ b/spec/frontend/boards/stores/mutations_spec.js @@ -1,3 +1,4 @@ +import { issuableTypes } from '~/boards/constants'; import * as types from '~/boards/stores/mutation_types'; import mutations from '~/boards/stores/mutations'; import defaultState from '~/boards/stores/state'; @@ -37,6 +38,7 @@ describe('Board Store Mutations', () => { const boardConfig = { milestoneTitle: 'Milestone 1', }; + const issuableType = issuableTypes.issue; mutations[types.SET_INITIAL_BOARD_DATA](state, { boardId, @@ -44,6 +46,7 @@ describe('Board Store Mutations', () => { boardType, disabled, boardConfig, + issuableType, }); expect(state.boardId).toEqual(boardId); @@ -51,6 +54,7 @@ describe('Board Store Mutations', () => { expect(state.boardType).toEqual(boardType); expect(state.disabled).toEqual(disabled); expect(state.boardConfig).toEqual(boardConfig); + expect(state.issuableType).toEqual(issuableType); }); }); @@ -106,11 +110,31 @@ describe('Board Store Mutations', () => { }); }); + describe('RECEIVE_LABELS_REQUEST', () => { + it('sets labelsLoading on state', () => { + mutations.RECEIVE_LABELS_REQUEST(state); + + expect(state.labelsLoading).toEqual(true); + }); + }); + describe('RECEIVE_LABELS_SUCCESS', () => { it('sets labels on state', () => { mutations.RECEIVE_LABELS_SUCCESS(state, labels); expect(state.labels).toEqual(labels); + expect(state.labelsLoading).toEqual(false); + }); + }); + + describe('RECEIVE_LABELS_FAILURE', () => { + it('sets error message', () => { + mutations.RECEIVE_LABELS_FAILURE(state); + + expect(state.error).toEqual( + 'An error occurred while fetching labels. Please reload the page.', + ); + expect(state.labelsLoading).toEqual(false); }); }); @@ -179,6 +203,24 @@ describe('Board Store Mutations', () => { }); }); + describe('TOGGLE_LIST_COLLAPSED', () => { + it('updates collapsed attribute of list in boardLists state', () => { + const listId = 'gid://gitlab/List/1'; + state = { + ...state, + boardLists: { + [listId]: mockLists[0], + }, + }; + + expect(state.boardLists[listId].collapsed).toEqual(false); + + mutations.TOGGLE_LIST_COLLAPSED(state, { listId, collapsed: true }); + + expect(state.boardLists[listId].collapsed).toEqual(true); + }); + }); + describe('REMOVE_LIST', () => { it('removes list from boardLists', () => { const [list, secondList] = mockLists; @@ -219,24 +261,24 @@ describe('Board Store Mutations', () => { }); describe('RESET_ISSUES', () => { - it('should remove issues from issuesByListId state', () => { - const issuesByListId = { + it('should remove issues from boardItemsByListId state', () => { + const boardItemsByListId = { 'gid://gitlab/List/1': [mockIssue.id], }; state = { ...state, - issuesByListId, + boardItemsByListId, }; mutations[types.RESET_ISSUES](state); - expect(state.issuesByListId).toEqual({ 'gid://gitlab/List/1': [] }); + expect(state.boardItemsByListId).toEqual({ 'gid://gitlab/List/1': [] }); }); }); - describe('RECEIVE_ISSUES_FOR_LIST_SUCCESS', () => { - it('updates issuesByListId and issues on state', () => { + describe('RECEIVE_ITEMS_FOR_LIST_SUCCESS', () => { + it('updates boardItemsByListId and issues on state', () => { const listIssues = { 'gid://gitlab/List/1': [mockIssue.id], }; @@ -246,10 +288,10 @@ describe('Board Store Mutations', () => { state = { ...state, - issuesByListId: { + boardItemsByListId: { 'gid://gitlab/List/1': [], }, - issues: {}, + boardItems: {}, boardLists: initialBoardListsState, }; @@ -260,18 +302,18 @@ describe('Board Store Mutations', () => { }, }; - mutations.RECEIVE_ISSUES_FOR_LIST_SUCCESS(state, { - listIssues: { listData: listIssues, issues }, + mutations.RECEIVE_ITEMS_FOR_LIST_SUCCESS(state, { + listItems: { listData: listIssues, boardItems: issues }, listPageInfo, listId: 'gid://gitlab/List/1', }); - expect(state.issuesByListId).toEqual(listIssues); - expect(state.issues).toEqual(issues); + expect(state.boardItemsByListId).toEqual(listIssues); + expect(state.boardItems).toEqual(issues); }); }); - describe('RECEIVE_ISSUES_FOR_LIST_FAILURE', () => { + describe('RECEIVE_ITEMS_FOR_LIST_FAILURE', () => { it('sets error message', () => { state = { ...state, @@ -281,7 +323,7 @@ describe('Board Store Mutations', () => { const listId = 'gid://gitlab/List/1'; - mutations.RECEIVE_ISSUES_FOR_LIST_FAILURE(state, listId); + mutations.RECEIVE_ITEMS_FOR_LIST_FAILURE(state, listId); expect(state.error).toEqual( 'An error occurred while fetching the board issues. Please reload the page.', @@ -303,7 +345,7 @@ describe('Board Store Mutations', () => { state = { ...state, error: undefined, - issues: { + boardItems: { ...issue, }, }; @@ -317,7 +359,7 @@ describe('Board Store Mutations', () => { value, }); - expect(state.issues[issueId]).toEqual({ ...issue[issueId], id: '2' }); + expect(state.boardItems[issueId]).toEqual({ ...issue[issueId], id: '2' }); }); }); @@ -343,7 +385,7 @@ describe('Board Store Mutations', () => { }); describe('MOVE_ISSUE', () => { - it('updates issuesByListId, moving issue between lists', () => { + it('updates boardItemsByListId, moving issue between lists', () => { const listIssues = { 'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id], 'gid://gitlab/List/2': [], @@ -356,9 +398,9 @@ describe('Board Store Mutations', () => { state = { ...state, - issuesByListId: listIssues, + boardItemsByListId: listIssues, boardLists: initialBoardListsState, - issues, + boardItems: issues, }; mutations.MOVE_ISSUE(state, { @@ -372,7 +414,7 @@ describe('Board Store Mutations', () => { 'gid://gitlab/List/2': [mockIssue2.id], }; - expect(state.issuesByListId).toEqual(updatedListIssues); + expect(state.boardItemsByListId).toEqual(updatedListIssues); }); }); @@ -384,19 +426,19 @@ describe('Board Store Mutations', () => { state = { ...state, - issues, + boardItems: issues, }; mutations.MOVE_ISSUE_SUCCESS(state, { issue: rawIssue, }); - expect(state.issues).toEqual({ 436: { ...mockIssue, id: 436 } }); + expect(state.boardItems).toEqual({ 436: { ...mockIssue, id: 436 } }); }); }); describe('MOVE_ISSUE_FAILURE', () => { - it('updates issuesByListId, reverting moving issue between lists, and sets error message', () => { + it('updates boardItemsByListId, reverting moving issue between lists, and sets error message', () => { const listIssues = { 'gid://gitlab/List/1': [mockIssue.id], 'gid://gitlab/List/2': [mockIssue2.id], @@ -404,7 +446,7 @@ describe('Board Store Mutations', () => { state = { ...state, - issuesByListId: listIssues, + boardItemsByListId: listIssues, boardLists: initialBoardListsState, }; @@ -420,7 +462,7 @@ describe('Board Store Mutations', () => { 'gid://gitlab/List/2': [], }; - expect(state.issuesByListId).toEqual(updatedListIssues); + expect(state.boardItemsByListId).toEqual(updatedListIssues); expect(state.error).toEqual('An error occurred while moving the issue. Please try again.'); }); }); @@ -446,7 +488,7 @@ describe('Board Store Mutations', () => { }); describe('ADD_ISSUE_TO_LIST', () => { - it('adds issue to issues state and issue id in list in issuesByListId', () => { + it('adds issue to issues state and issue id in list in boardItemsByListId', () => { const listIssues = { 'gid://gitlab/List/1': [mockIssue.id], }; @@ -456,8 +498,8 @@ describe('Board Store Mutations', () => { state = { ...state, - issuesByListId: listIssues, - issues, + boardItemsByListId: listIssues, + boardItems: issues, boardLists: initialBoardListsState, }; @@ -465,14 +507,14 @@ describe('Board Store Mutations', () => { 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.boardItemsByListId['gid://gitlab/List/1']).toContain(mockIssue2.id); + expect(state.boardItems[mockIssue2.id]).toEqual(mockIssue2); expect(state.boardLists['gid://gitlab/List/1'].issuesCount).toBe(2); }); }); describe('ADD_ISSUE_TO_LIST_FAILURE', () => { - it('removes issue id from list in issuesByListId and sets error message', () => { + it('removes issue id from list in boardItemsByListId and sets error message', () => { const listIssues = { 'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id], }; @@ -483,20 +525,20 @@ describe('Board Store Mutations', () => { state = { ...state, - issuesByListId: listIssues, - issues, + boardItemsByListId: listIssues, + boardItems: issues, boardLists: initialBoardListsState, }; mutations.ADD_ISSUE_TO_LIST_FAILURE(state, { list: mockLists[0], issueId: mockIssue2.id }); - expect(state.issuesByListId['gid://gitlab/List/1']).not.toContain(mockIssue2.id); + expect(state.boardItemsByListId['gid://gitlab/List/1']).not.toContain(mockIssue2.id); expect(state.error).toBe('An error occurred while creating the issue. Please try again.'); }); }); describe('REMOVE_ISSUE_FROM_LIST', () => { - it('removes issue id from list in issuesByListId and deletes issue from state', () => { + it('removes issue id from list in boardItemsByListId and deletes issue from state', () => { const listIssues = { 'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id], }; @@ -507,15 +549,15 @@ describe('Board Store Mutations', () => { state = { ...state, - issuesByListId: listIssues, - issues, + boardItemsByListId: listIssues, + boardItems: issues, boardLists: initialBoardListsState, }; mutations.ADD_ISSUE_TO_LIST_FAILURE(state, { list: mockLists[0], issueId: mockIssue2.id }); - expect(state.issuesByListId['gid://gitlab/List/1']).not.toContain(mockIssue2.id); - expect(state.issues).not.toContain(mockIssue2); + expect(state.boardItemsByListId['gid://gitlab/List/1']).not.toContain(mockIssue2.id); + expect(state.boardItems).not.toContain(mockIssue2); }); }); @@ -607,14 +649,21 @@ describe('Board Store Mutations', () => { describe('REMOVE_BOARD_ITEM_FROM_SELECTION', () => { it('Should remove boardItem to selectedBoardItems state', () => { - state = { - ...state, - selectedBoardItems: [mockIssue], - }; + state.selectedBoardItems = [mockIssue]; mutations[types.REMOVE_BOARD_ITEM_FROM_SELECTION](state, mockIssue); expect(state.selectedBoardItems).toEqual([]); }); }); + + describe('RESET_BOARD_ITEM_SELECTION', () => { + it('Should reset selectedBoardItems state', () => { + state.selectedBoardItems = [mockIssue]; + + mutations[types.RESET_BOARD_ITEM_SELECTION](state, mockIssue); + + expect(state.selectedBoardItems).toEqual([]); + }); + }); }); |