diff options
Diffstat (limited to 'spec/frontend/boards')
-rw-r--r-- | spec/frontend/boards/board_card_inner_spec.js | 24 | ||||
-rw-r--r-- | spec/frontend/boards/board_list_helper.js | 6 | ||||
-rw-r--r-- | spec/frontend/boards/board_list_spec.js | 116 | ||||
-rw-r--r-- | spec/frontend/boards/components/board_card_spec.js | 1 | ||||
-rw-r--r-- | spec/frontend/boards/components/board_filtered_search_spec.js | 6 | ||||
-rw-r--r-- | spec/frontend/boards/components/board_form_spec.js | 9 | ||||
-rw-r--r-- | spec/frontend/boards/components/board_new_issue_spec.js | 154 | ||||
-rw-r--r-- | spec/frontend/boards/components/board_new_item_spec.js | 103 | ||||
-rw-r--r-- | spec/frontend/boards/components/issue_board_filtered_search_spec.js | 31 | ||||
-rw-r--r-- | spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js | 2 | ||||
-rw-r--r-- | spec/frontend/boards/mock_data.js | 47 | ||||
-rw-r--r-- | spec/frontend/boards/stores/actions_spec.js | 274 | ||||
-rw-r--r-- | spec/frontend/boards/stores/mutations_spec.js | 40 |
13 files changed, 589 insertions, 224 deletions
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js index 87f9a68f5dd..7d3ecc773a6 100644 --- a/spec/frontend/boards/board_card_inner_spec.js +++ b/spec/frontend/boards/board_card_inner_spec.js @@ -1,6 +1,7 @@ import { GlLabel, GlLoadingIcon, GlTooltip } from '@gitlab/ui'; import { range } from 'lodash'; import Vuex from 'vuex'; +import setWindowLocation from 'helpers/set_window_location_helper'; 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'; @@ -8,7 +9,7 @@ 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 } from './mock_data'; +import { mockLabelList, mockIssue, mockIssueFullPath } from './mock_data'; jest.mock('~/lib/utils/url_utility'); jest.mock('~/boards/eventhub'); @@ -44,7 +45,7 @@ describe('Board card component', () => { const findEpicCountablesTotalWeight = () => wrapper.findByTestId('epic-countables-total-weight'); const findEpicProgressTooltip = () => wrapper.findByTestId('epic-progress-tooltip-content'); - const createStore = ({ isEpicBoard = false } = {}) => { + const createStore = ({ isEpicBoard = false, isProjectBoard = false } = {}) => { store = new Vuex.Store({ ...defaultStore, state: { @@ -54,7 +55,7 @@ describe('Board card component', () => { getters: { isGroupBoard: () => true, isEpicBoard: () => isEpicBoard, - isProjectBoard: () => false, + isProjectBoard: () => isProjectBoard, }, }); }; @@ -133,6 +134,17 @@ describe('Board card component', () => { expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); }); + it('does not render item reference path', () => { + createStore({ isProjectBoard: true }); + createWrapper(); + + expect(wrapper.find('.board-card-number').text()).not.toContain(mockIssueFullPath); + }); + + it('renders item reference path', () => { + expect(wrapper.find('.board-card-number').text()).toContain(mockIssueFullPath); + }); + describe('blocked', () => { it('renders blocked icon if issue is blocked', async () => { createWrapper({ @@ -363,8 +375,6 @@ describe('Board card component', () => { describe('filterByLabel method', () => { beforeEach(() => { - delete window.location; - wrapper.setProps({ updateFilters: true, }); @@ -373,7 +383,7 @@ describe('Board card component', () => { describe('when selected label is not in the filter', () => { beforeEach(() => { jest.spyOn(wrapper.vm, 'performSearch').mockImplementation(() => {}); - window.location = { search: '' }; + setWindowLocation('?'); wrapper.vm.filterByLabel(label1); }); @@ -394,7 +404,7 @@ describe('Board card component', () => { describe('when selected label is already in the filter', () => { beforeEach(() => { jest.spyOn(wrapper.vm, 'performSearch').mockImplementation(() => {}); - window.location = { search: '?label_name[]=testing%20123' }; + setWindowLocation('?label_name[]=testing%20123'); wrapper.vm.filterByLabel(label1); }); diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js index c440c110094..811f0043a01 100644 --- a/spec/frontend/boards/board_list_helper.js +++ b/spec/frontend/boards/board_list_helper.js @@ -4,8 +4,9 @@ import Vuex from 'vuex'; 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 BoardNewItem from '~/boards/components/board_new_item.vue'; import defaultState from '~/boards/stores/state'; -import { mockList, mockIssuesByListId, issues } from './mock_data'; +import { mockList, mockIssuesByListId, issues, mockGroupProjects } from './mock_data'; export default function createComponent({ listIssueProps = {}, @@ -17,6 +18,7 @@ export default function createComponent({ state = defaultState, stubs = { BoardNewIssue, + BoardNewItem, BoardCard, }, } = {}) { @@ -25,6 +27,7 @@ export default function createComponent({ const store = new Vuex.Store({ state: { + selectedProject: mockGroupProjects[0], boardItemsByListId: mockIssuesByListId, boardItems: issues, pageInfoByListId: { @@ -77,6 +80,7 @@ export default function createComponent({ provide: { groupId: null, rootPath: '/', + boardId: '1', weightFeatureAvailable: false, boardWeight: null, canAdminList: true, diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js index a3b1810ab80..6f623eab1af 100644 --- a/spec/frontend/boards/board_list_spec.js +++ b/spec/frontend/boards/board_list_spec.js @@ -1,3 +1,5 @@ +import Draggable from 'vuedraggable'; +import { DraggableItemTypes } from 'ee_else_ce/boards/constants'; import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame'; import createComponent from 'jest/boards/board_list_helper'; import BoardCard from '~/boards/components/board_card.vue'; @@ -10,6 +12,23 @@ describe('Board list component', () => { const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`); const findIssueCountLoadingIcon = () => wrapper.find('[data-testid="count-loading-icon"]'); + const findDraggable = () => wrapper.findComponent(Draggable); + + const startDrag = ( + params = { + item: { + dataset: { + draggableItemType: DraggableItemTypes.card, + }, + }, + }, + ) => { + findByTestId('tree-root-wrapper').vm.$emit('start', params); + }; + + const endDrag = (params) => { + findByTestId('tree-root-wrapper').vm.$emit('end', params); + }; useFakeRequestAnimationFrame(); @@ -155,40 +174,89 @@ describe('Board list component', () => { }); describe('drag & drop issue', () => { - beforeEach(() => { - wrapper = createComponent(); - }); + describe('when dragging is allowed', () => { + beforeEach(() => { + wrapper = createComponent({ + componentProps: { + disabled: false, + }, + }); + }); - describe('handleDragOnStart', () => { - it('adds a class `is-dragging` to document body', () => { - expect(document.body.classList.contains('is-dragging')).toBe(false); + it('Draggable is used', () => { + expect(findDraggable().exists()).toBe(true); + }); + + describe('handleDragOnStart', () => { + it('adds a class `is-dragging` to document body', () => { + expect(document.body.classList.contains('is-dragging')).toBe(false); - findByTestId('tree-root-wrapper').vm.$emit('start'); + startDrag(); - expect(document.body.classList.contains('is-dragging')).toBe(true); + expect(document.body.classList.contains('is-dragging')).toBe(true); + }); }); - }); - describe('handleDragOnEnd', () => { - it('removes class `is-dragging` from document body', () => { - jest.spyOn(wrapper.vm, 'moveItem').mockImplementation(() => {}); - document.body.classList.add('is-dragging'); + describe('handleDragOnEnd', () => { + beforeEach(() => { + jest.spyOn(wrapper.vm, 'moveItem').mockImplementation(() => {}); + + startDrag(); + }); + + it('removes class `is-dragging` from document body', () => { + document.body.classList.add('is-dragging'); + + endDrag({ + oldIndex: 1, + newIndex: 0, + item: { + dataset: { + draggableItemType: DraggableItemTypes.card, + itemId: mockIssues[0].id, + itemIid: mockIssues[0].iid, + itemPath: mockIssues[0].referencePath, + }, + }, + to: { children: [], dataset: { listId: 'gid://gitlab/List/1' } }, + from: { dataset: { listId: 'gid://gitlab/List/2' } }, + }); - findByTestId('tree-root-wrapper').vm.$emit('end', { - oldIndex: 1, - newIndex: 0, - item: { - dataset: { - itemId: mockIssues[0].id, - itemIid: mockIssues[0].iid, - itemPath: mockIssues[0].referencePath, + expect(document.body.classList.contains('is-dragging')).toBe(false); + }); + + it(`should not handle the event if the dragged item is not a "${DraggableItemTypes.card}"`, () => { + endDrag({ + oldIndex: 1, + newIndex: 0, + item: { + dataset: { + draggableItemType: DraggableItemTypes.list, + itemId: mockIssues[0].id, + itemIid: mockIssues[0].iid, + itemPath: mockIssues[0].referencePath, + }, }, + to: { children: [], dataset: { listId: 'gid://gitlab/List/1' } }, + from: { dataset: { listId: 'gid://gitlab/List/2' } }, + }); + + expect(document.body.classList.contains('is-dragging')).toBe(true); + }); + }); + }); + + describe('when dragging is not allowed', () => { + beforeEach(() => { + wrapper = createComponent({ + componentProps: { + disabled: true, }, - to: { children: [], dataset: { listId: 'gid://gitlab/List/1' } }, - from: { dataset: { listId: 'gid://gitlab/List/2' } }, }); + }); - expect(document.body.classList.contains('is-dragging')).toBe(false); + it('Draggable is not used', () => { + expect(findDraggable().exists()).toBe(false); }); }); }); diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js index 9a9ce7b8dc1..25ec568e48d 100644 --- a/spec/frontend/boards/components/board_card_spec.js +++ b/spec/frontend/boards/components/board_card_spec.js @@ -31,6 +31,7 @@ describe('Board card', () => { actions: mockActions, getters: { isEpicBoard: () => false, + isProjectBoard: () => false, }, }); }; diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js index 6ac5d16e5a3..50f86e92adb 100644 --- a/spec/frontend/boards/components/board_filtered_search_spec.js +++ b/spec/frontend/boards/components/board_filtered_search_spec.js @@ -115,6 +115,9 @@ describe('BoardFilteredSearch', () => { { type: 'author_username', value: { data: 'root', operator: '=' } }, { type: 'label_name', value: { data: 'label', operator: '=' } }, { type: 'label_name', value: { data: 'label2', operator: '=' } }, + { type: 'milestone_title', value: { data: 'New Milestone', operator: '=' } }, + { type: 'types', value: { data: 'INCIDENT', operator: '=' } }, + { type: 'weight', value: { data: '2', operator: '=' } }, ]; jest.spyOn(urlUtility, 'updateHistory'); findFilteredSearch().vm.$emit('onFilter', mockFilters); @@ -122,7 +125,8 @@ describe('BoardFilteredSearch', () => { expect(urlUtility.updateHistory).toHaveBeenCalledWith({ title: '', replace: true, - url: 'http://test.host/?author_username=root&label_name[]=label&label_name[]=label2', + url: + 'http://test.host/?author_username=root&label_name[]=label&label_name[]=label2&milestone_title=New+Milestone&types=INCIDENT&weight=2', }); }); }); diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js index 3966c3e6b87..52f1907654a 100644 --- a/spec/frontend/boards/components/board_form_spec.js +++ b/spec/frontend/boards/components/board_form_spec.js @@ -1,5 +1,6 @@ import { GlModal } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import setWindowLocation from 'helpers/set_window_location_helper'; import { TEST_HOST } from 'helpers/test_constants'; import waitForPromises from 'helpers/wait_for_promises'; @@ -75,10 +76,6 @@ describe('BoardForm', () => { }); }; - beforeEach(() => { - delete window.location; - }); - afterEach(() => { wrapper.destroy(); wrapper = null; @@ -244,7 +241,7 @@ describe('BoardForm', () => { updateBoard: { board: { id: 'gid://gitlab/Board/321', webPath: 'test-path' } }, }, }); - window.location = new URL('https://test/boards/1'); + setWindowLocation('https://test/boards/1'); createComponent({ canAdminBoard: true, currentPage: formType.edit }); findInput().trigger('keyup.enter', { metaKey: true }); @@ -270,7 +267,7 @@ describe('BoardForm', () => { updateBoard: { board: { id: 'gid://gitlab/Board/321', webPath: 'test-path' } }, }, }); - window.location = new URL('https://test/boards/1?group_by=epic'); + setWindowLocation('https://test/boards/1?group_by=epic'); createComponent({ canAdminBoard: true, currentPage: formType.edit }); findInput().trigger('keyup.enter', { metaKey: true }); diff --git a/spec/frontend/boards/components/board_new_issue_spec.js b/spec/frontend/boards/components/board_new_issue_spec.js index e6405bbcff3..57ccebf3676 100644 --- a/spec/frontend/boards/components/board_new_issue_spec.js +++ b/spec/frontend/boards/components/board_new_issue_spec.js @@ -1,6 +1,9 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; 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'; @@ -8,107 +11,104 @@ const localVue = createLocalVue(); localVue.use(Vuex); +const addListNewIssuesSpy = jest.fn().mockResolvedValue(); +const mockActions = { addListNewIssue: addListNewIssuesSpy }; + +const createComponent = ({ + state = { selectedProject: mockGroupProjects[0], fullPath: mockGroupProjects[0].fullPath }, + actions = mockActions, + getters = { isGroupBoard: () => true, isProjectBoard: () => false }, +} = {}) => + shallowMount(BoardNewIssue, { + localVue, + store: new Vuex.Store({ + state, + actions, + getters, + }), + propsData: { + list: mockList, + }, + provide: { + groupId: 1, + weightFeatureAvailable: false, + boardWeight: null, + }, + stubs: { + BoardNewItem, + }, + }); + describe('Issue boards new issue form', () => { let wrapper; - let vm; - - const addListNewIssuesSpy = jest.fn(); - - const findSubmitButton = () => wrapper.find({ ref: 'submitButton' }); - const findCancelButton = () => wrapper.find({ ref: 'cancelButton' }); - const findSubmitForm = () => wrapper.find({ ref: 'submitForm' }); - - const submitIssue = () => { - const dummySubmitEvent = { - preventDefault() {}, - }; - return findSubmitForm().trigger('submit', dummySubmitEvent); - }; - - beforeEach(() => { - const store = new Vuex.Store({ - state: { selectedProject: mockGroupProjects[0] }, - actions: { addListNewIssue: addListNewIssuesSpy }, - getters: { isGroupBoard: () => false, isProjectBoard: () => true }, - }); - - wrapper = shallowMount(BoardNewIssue, { - propsData: { - disabled: false, - list: mockList, - }, - store, - localVue, - provide: { - groupId: null, - weightFeatureAvailable: false, - boardWeight: null, - }, - }); + const findBoardNewItem = () => wrapper.findComponent(BoardNewItem); - vm = wrapper.vm; + beforeEach(async () => { + wrapper = createComponent(); - return vm.$nextTick(); + await wrapper.vm.$nextTick(); }); afterEach(() => { wrapper.destroy(); }); - it('calls submit if submit button is clicked', async () => { - jest.spyOn(wrapper.vm, 'submit').mockImplementation(); - wrapper.setData({ title: 'Testing Title' }); - - await vm.$nextTick(); - await submitIssue(); - expect(wrapper.vm.submit).toHaveBeenCalled(); - }); - - it('disables submit button if title is empty', () => { - expect(findSubmitButton().props().disabled).toBe(true); + it('renders board-new-item component', () => { + const boardNewItem = findBoardNewItem(); + expect(boardNewItem.exists()).toBe(true); + expect(boardNewItem.props()).toEqual({ + list: mockList, + formEventPrefix: 'toggle-issue-form-', + submitButtonTitle: 'Create issue', + disableSubmit: false, + }); }); - it('enables submit button if title is not empty', async () => { - wrapper.setData({ title: 'Testing Title' }); - - await vm.$nextTick(); - expect(wrapper.find({ ref: 'input' }).element.value).toBe('Testing Title'); - expect(findSubmitButton().props().disabled).toBe(false); + it('calls addListNewIssue action when `board-new-item` emits form-submit event', async () => { + findBoardNewItem().vm.$emit('form-submit', { title: 'Foo' }); + + await wrapper.vm.$nextTick(); + expect(addListNewIssuesSpy).toHaveBeenCalledWith(expect.any(Object), { + list: mockList, + issueInput: { + title: 'Foo', + labelIds: [], + assigneeIds: [], + milestoneId: undefined, + projectPath: mockGroupProjects[0].fullPath, + }, + }); }); - it('clears title after clicking cancel', async () => { - findCancelButton().trigger('click'); + 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 vm.$nextTick(); - expect(vm.title).toBe(''); + await wrapper.vm.$nextTick(); + expect(eventHub.$emit).toHaveBeenCalledWith(`toggle-issue-form-${mockList.id}`); }); - describe('submit success', () => { - it('creates new issue', async () => { - wrapper.setData({ title: 'create issue' }); + describe('when in group issue board', () => { + it('renders project-select component within board-new-item component', () => { + const projectSelect = findBoardNewItem().findComponent(ProjectSelect); - await vm.$nextTick(); - await submitIssue(); - expect(addListNewIssuesSpy).toHaveBeenCalled(); + expect(projectSelect.exists()).toBe(true); + expect(projectSelect.props('list')).toEqual(mockList); }); + }); - it('enables button after submit', async () => { - jest.spyOn(wrapper.vm, 'submit').mockImplementation(); - wrapper.setData({ title: 'create issue' }); - - await vm.$nextTick(); - await submitIssue(); - expect(findSubmitButton().props().disabled).toBe(false); + describe('when in project issue board', () => { + beforeEach(() => { + wrapper = createComponent({ + getters: { isGroupBoard: () => false, isProjectBoard: () => true }, + }); }); - it('clears title after submit', async () => { - wrapper.setData({ title: 'create issue' }); + it('does not render project-select component within board-new-item component', () => { + const projectSelect = findBoardNewItem().findComponent(ProjectSelect); - await vm.$nextTick(); - await submitIssue(); - await vm.$nextTick(); - expect(vm.title).toBe(''); + expect(projectSelect.exists()).toBe(false); }); }); }); diff --git a/spec/frontend/boards/components/board_new_item_spec.js b/spec/frontend/boards/components/board_new_item_spec.js new file mode 100644 index 00000000000..0151d9c1c14 --- /dev/null +++ b/spec/frontend/boards/components/board_new_item_spec.js @@ -0,0 +1,103 @@ +import { GlForm, GlFormInput, GlButton } from '@gitlab/ui'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; + +import BoardNewItem from '~/boards/components/board_new_item.vue'; +import eventHub from '~/boards/eventhub'; + +import { mockList } from '../mock_data'; + +const createComponent = ({ + list = mockList, + formEventPrefix = 'toggle-issue-form-', + disabledSubmit = false, + submitButtonTitle = 'Create item', +} = {}) => + mountExtended(BoardNewItem, { + propsData: { + list, + formEventPrefix, + disabledSubmit, + submitButtonTitle, + }, + slots: { + default: '<div id="default-slot"></div>', + }, + stubs: { + GlForm, + }, + }); + +describe('BoardNewItem', () => { + let wrapper; + + beforeEach(() => { + wrapper = createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('template', () => { + it('renders gl-form component', () => { + expect(wrapper.findComponent(GlForm).exists()).toBe(true); + }); + + it('renders field label', () => { + expect(wrapper.find('label').exists()).toBe(true); + expect(wrapper.find('label').text()).toBe('Title'); + }); + + it('renders gl-form-input field', () => { + expect(wrapper.findComponent(GlFormInput).exists()).toBe(true); + }); + + it('renders default slot contents', () => { + expect(wrapper.find('#default-slot').exists()).toBe(true); + }); + + it('renders submit and cancel buttons', () => { + const buttons = wrapper.findAllComponents(GlButton); + expect(buttons).toHaveLength(2); + expect(buttons.at(0).text()).toBe('Create item'); + expect(buttons.at(1).text()).toBe('Cancel'); + }); + + describe('events', () => { + const glForm = () => wrapper.findComponent(GlForm); + const titleInput = () => wrapper.find('input[name="issue_title"]'); + + it('emits `form-submit` event with title value when `submit` is triggered on gl-form', async () => { + titleInput().setValue('Foo'); + await glForm().trigger('submit'); + + expect(wrapper.emitted('form-submit')).toBeTruthy(); + 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'); + + expect(eventHub.$emit).toHaveBeenCalledWith(`scroll-board-list-${mockList.id}`); + }); + + it('emits `form-cancel` event and clears title value when `reset` is triggered on gl-form', async () => { + titleInput().setValue('Foo'); + + await wrapper.vm.$nextTick(); + expect(titleInput().element.value).toBe('Foo'); + + await glForm().trigger('reset'); + + expect(titleInput().element.value).toBe(''); + expect(wrapper.emitted('form-cancel')).toBeTruthy(); + }); + }); + }); +}); diff --git a/spec/frontend/boards/components/issue_board_filtered_search_spec.js b/spec/frontend/boards/components/issue_board_filtered_search_spec.js index 0e3cf59901e..b6de46f8db8 100644 --- a/spec/frontend/boards/components/issue_board_filtered_search_spec.js +++ b/spec/frontend/boards/components/issue_board_filtered_search_spec.js @@ -1,16 +1,16 @@ import { shallowMount } from '@vue/test-utils'; import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue'; import IssueBoardFilteredSpec from '~/boards/components/issue_board_filtered_search.vue'; -import { BoardType } from '~/boards/constants'; import issueBoardFilters from '~/boards/issue_board_filters'; import { mockTokens } from '../mock_data'; +jest.mock('~/boards/issue_board_filters'); + describe('IssueBoardFilter', () => { let wrapper; - const createComponent = ({ initialFilterParams = {} } = {}) => { + const createComponent = () => { wrapper = shallowMount(IssueBoardFilteredSpec, { - provide: { initialFilterParams }, props: { fullPath: '', boardType: '' }, }); }; @@ -20,7 +20,17 @@ describe('IssueBoardFilter', () => { }); describe('default', () => { + let fetchAuthorsSpy; + let fetchLabelsSpy; beforeEach(() => { + fetchAuthorsSpy = jest.fn(); + fetchLabelsSpy = jest.fn(); + + issueBoardFilters.mockReturnValue({ + fetchAuthors: fetchAuthorsSpy, + fetchLabels: fetchLabelsSpy, + }); + createComponent(); }); @@ -28,17 +38,10 @@ describe('IssueBoardFilter', () => { expect(wrapper.find(BoardFilteredSearch).exists()).toBe(true); }); - it.each([[BoardType.group], [BoardType.project]])( - 'when boardType is %s we pass the correct tokens to BoardFilteredSearch', - (boardType) => { - const { fetchAuthors, fetchLabels } = issueBoardFilters({}, '', boardType); + it('passes the correct tokens to BoardFilteredSearch', () => { + const tokens = mockTokens(fetchLabelsSpy, fetchAuthorsSpy, wrapper.vm.fetchMilestones); - const tokens = mockTokens(fetchLabels, fetchAuthors); - - expect(wrapper.find(BoardFilteredSearch).props('tokens').toString()).toBe( - tokens.toString(), - ); - }, - ); + expect(wrapper.find(BoardFilteredSearch).props('tokens')).toEqual(tokens); + }); }); }); 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 8992a5780f3..60474767f2d 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 @@ -97,6 +97,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => { addLabelIds: TEST_LABELS.map((label) => label.id), projectPath: TEST_ISSUE_FULLPATH, removeLabelIds: [], + iid: null, }); }); }); @@ -121,6 +122,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => { addLabelIds: [5, 7], removeLabelIds: [6], projectPath: TEST_ISSUE_FULLPATH, + iid: null, }); }); }); diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js index 6ac4db8cdaa..106f7b04c4b 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -1,5 +1,6 @@ /* global List */ +import { GlFilteredSearchToken } from '@gitlab/ui'; import { keyBy } from 'lodash'; import Vue from 'vue'; import '~/boards/models/list'; @@ -8,6 +9,8 @@ import boardsStore from '~/boards/stores/boards_store'; import { __ } from '~/locale'; import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; +import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; +import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue'; export const boardObj = { id: 1, @@ -101,6 +104,17 @@ export const mockMilestone = { due_date: '2019-12-31', }; +export const mockMilestones = [ + { + id: 'gid://gitlab/Milestone/1', + title: 'Milestone 1', + }, + { + id: 'gid://gitlab/Milestone/2', + title: 'Milestone 2', + }, +]; + export const assignees = [ { id: 'gid://gitlab/User/2', @@ -531,7 +545,7 @@ export const mockMoveData = { ...mockMoveIssueParams, }; -export const mockTokens = (fetchLabels, fetchAuthors) => [ +export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [ { icon: 'labels', title: __('Label'), @@ -557,6 +571,7 @@ export const mockTokens = (fetchLabels, fetchAuthors) => [ token: AuthorToken, unique: true, fetchAuthors, + preloadedAuthors: [], }, { icon: 'user', @@ -569,5 +584,35 @@ export const mockTokens = (fetchLabels, fetchAuthors) => [ token: AuthorToken, unique: true, fetchAuthors, + preloadedAuthors: [], + }, + { + icon: 'issues', + title: __('Type'), + type: 'types', + operators: [{ value: '=', description: 'is' }], + token: GlFilteredSearchToken, + unique: true, + options: [ + { icon: 'issue-type-issue', value: 'ISSUE', title: 'Issue' }, + { icon: 'issue-type-incident', value: 'INCIDENT', title: 'Incident' }, + ], + }, + { + icon: 'clock', + title: __('Milestone'), + symbol: '%', + type: 'milestone_title', + token: MilestoneToken, + unique: true, + defaultMilestones: [], + fetchMilestones, + }, + { + icon: 'weight', + title: __('Weight'), + type: 'weight', + token: WeightToken, + unique: true, }, ]; diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index 5e16e389ddc..1272a573d2f 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -1,4 +1,7 @@ import * as Sentry from '@sentry/browser'; +import { cloneDeep } from 'lodash'; +import Vue from 'vue'; +import Vuex from 'vuex'; import { inactiveId, ISSUABLE, @@ -6,6 +9,7 @@ import { issuableTypes, BoardType, listsQuery, + DraggableItemTypes, } from 'ee_else_ce/boards/constants'; import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql'; import testAction from 'helpers/vuex_action_helper'; @@ -21,6 +25,7 @@ import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutati import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql'; import actions, { gqlClient } from '~/boards/stores/actions'; import * as types from '~/boards/stores/mutation_types'; +import mutations from '~/boards/stores/mutations'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { @@ -37,6 +42,7 @@ import { mockMoveState, mockMoveData, mockList, + mockMilestones, } from '../mock_data'; jest.mock('~/flash'); @@ -45,6 +51,8 @@ jest.mock('~/flash'); // subgroups when the movIssue action is called. const getProjectPath = (path) => path.split('#')[0]; +Vue.use(Vuex); + beforeEach(() => { window.gon = { features: {} }; }); @@ -260,6 +268,87 @@ describe('fetchLists', () => { ); }); +describe('fetchMilestones', () => { + const queryResponse = { + data: { + project: { + milestones: { + nodes: mockMilestones, + }, + }, + }, + }; + + const queryErrors = { + data: { + project: { + errors: ['You cannot view these milestones'], + milestones: {}, + }, + }, + }; + + function createStore({ + state = { + boardType: 'project', + fullPath: 'gitlab-org/gitlab', + milestones: [], + milestonesLoading: false, + }, + } = {}) { + return new Vuex.Store({ + state, + mutations, + }); + } + + it('throws error if state.boardType is not group or project', () => { + const store = createStore({ + state: { + boardType: 'invalid', + }, + }); + + expect(() => actions.fetchMilestones(store)).toThrow(new Error('Unknown board type')); + }); + + it('sets milestonesLoading to true', async () => { + jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse); + + const store = createStore(); + + actions.fetchMilestones(store); + + expect(store.state.milestonesLoading).toBe(true); + }); + + describe('success', () => { + it('sets state.milestones from query result', async () => { + jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse); + + const store = createStore(); + + await actions.fetchMilestones(store); + + expect(store.state.milestonesLoading).toBe(false); + expect(store.state.milestones).toBe(mockMilestones); + }); + }); + + describe('failure', () => { + it('sets state.milestones from query result', async () => { + jest.spyOn(gqlClient, 'query').mockResolvedValue(queryErrors); + + const store = createStore(); + + await expect(actions.fetchMilestones(store)).rejects.toThrow(); + + expect(store.state.milestonesLoading).toBe(false); + expect(store.state.error).toBe('Failed to load milestones.'); + }); + }); +}); + describe('createList', () => { it('should dispatch createIssueList action', () => { testAction({ @@ -419,75 +508,114 @@ describe('fetchLabels', () => { }); describe('moveList', () => { - it('should commit MOVE_LIST mutation and dispatch updateList action', (done) => { - const initialBoardListsState = { - 'gid://gitlab/List/1': mockLists[0], - 'gid://gitlab/List/2': mockLists[1], - }; + const backlogListId = 'gid://1'; + const closedListId = 'gid://5'; - const state = { - fullPath: 'gitlab-org', - fullBoardId: 'gid://gitlab/Board/1', - boardType: 'group', - disabled: false, - boardLists: initialBoardListsState, - }; + const boardLists1 = { + 'gid://3': { listType: '', position: 0 }, + 'gid://4': { listType: '', position: 1 }, + 'gid://5': { listType: '', position: 2 }, + }; - testAction( - actions.moveList, - { - listId: 'gid://gitlab/List/1', - replacedListId: 'gid://gitlab/List/2', - newIndex: 1, - adjustmentValue: 1, - }, - state, - [ - { - type: types.MOVE_LIST, - payload: { movedList: mockLists[0], listAtNewIndex: mockLists[1] }, - }, - ], - [ - { - type: 'updateList', - payload: { - listId: 'gid://gitlab/List/1', - position: 0, - backupList: initialBoardListsState, - }, + const boardLists2 = { + [backlogListId]: { listType: ListType.backlog, position: -Infinity }, + [closedListId]: { listType: ListType.closed, position: Infinity }, + ...cloneDeep(boardLists1), + }; + + const movableListsOrder = ['gid://3', 'gid://4', 'gid://5']; + const allListsOrder = [backlogListId, ...movableListsOrder, closedListId]; + + it(`should not handle the event if the dragged item is not a "${DraggableItemTypes.list}"`, () => { + return testAction({ + action: actions.moveList, + payload: { + item: { dataset: { listId: '', draggableItemType: DraggableItemTypes.card } }, + to: { + children: [], }, - ], - done, - ); + }, + state: {}, + expectedMutations: [], + expectedActions: [], + }); }); - it('should not commit MOVE_LIST or dispatch updateList if listId and replacedListId are the same', () => { - const initialBoardListsState = { - 'gid://gitlab/List/1': mockLists[0], - 'gid://gitlab/List/2': mockLists[1], - }; + describe.each` + draggableFrom | draggableTo | boardLists | boardListsOrder | expectedMovableListsOrder + ${0} | ${2} | ${boardLists1} | ${movableListsOrder} | ${['gid://4', 'gid://5', 'gid://3']} + ${2} | ${0} | ${boardLists1} | ${movableListsOrder} | ${['gid://5', 'gid://3', 'gid://4']} + ${0} | ${1} | ${boardLists1} | ${movableListsOrder} | ${['gid://4', 'gid://3', 'gid://5']} + ${1} | ${2} | ${boardLists1} | ${movableListsOrder} | ${['gid://3', 'gid://5', 'gid://4']} + ${2} | ${1} | ${boardLists1} | ${movableListsOrder} | ${['gid://3', 'gid://5', 'gid://4']} + ${1} | ${3} | ${boardLists2} | ${allListsOrder} | ${['gid://4', 'gid://5', 'gid://3']} + ${3} | ${1} | ${boardLists2} | ${allListsOrder} | ${['gid://5', 'gid://3', 'gid://4']} + ${1} | ${2} | ${boardLists2} | ${allListsOrder} | ${['gid://4', 'gid://3', 'gid://5']} + ${2} | ${3} | ${boardLists2} | ${allListsOrder} | ${['gid://3', 'gid://5', 'gid://4']} + ${3} | ${2} | ${boardLists2} | ${allListsOrder} | ${['gid://3', 'gid://5', 'gid://4']} + `( + 'when moving a list from position $draggableFrom to $draggableTo with lists $boardListsOrder', + ({ draggableFrom, draggableTo, boardLists, boardListsOrder, expectedMovableListsOrder }) => { + const movedListId = boardListsOrder[draggableFrom]; + const displacedListId = boardListsOrder[draggableTo]; + const buildDraggablePayload = () => { + return { + item: { + dataset: { + listId: boardListsOrder[draggableFrom], + draggableItemType: DraggableItemTypes.list, + }, + }, + newIndex: draggableTo, + to: { + children: boardListsOrder.map((listId) => ({ dataset: { listId } })), + }, + }; + }; - const state = { - fullPath: 'gitlab-org', - fullBoardId: 'gid://gitlab/Board/1', - boardType: 'group', - disabled: false, - boardLists: initialBoardListsState, - }; + it('should commit MOVE_LIST mutations and dispatch updateList action with correct payloads', () => { + return testAction({ + action: actions.moveList, + payload: buildDraggablePayload(), + state: { boardLists }, + expectedMutations: [ + { + type: types.MOVE_LISTS, + payload: expectedMovableListsOrder.map((listId, i) => ({ listId, position: i })), + }, + ], + expectedActions: [ + { + type: 'updateList', + payload: { + listId: movedListId, + position: movableListsOrder.findIndex((i) => i === displacedListId), + }, + }, + ], + }); + }); + }, + ); - testAction( - actions.moveList, - { - listId: 'gid://gitlab/List/1', - replacedListId: 'gid://gitlab/List/1', - newIndex: 1, - adjustmentValue: 1, - }, - state, - [], - [], - ); + describe('when moving from and to the same position', () => { + it('should not commit MOVE_LIST and should not dispatch updateList', () => { + const listId = 'gid://1000'; + + return testAction({ + action: actions.moveList, + payload: { + item: { dataset: { listId, draggbaleItemType: DraggableItemTypes.list } }, + newIndex: 0, + to: { + children: [{ dataset: { listId } }], + }, + }, + state: { boardLists: { [listId]: { position: 0 } } }, + expectedMutations: [], + expectedActions: [], + }); + }); }); }); @@ -549,7 +677,7 @@ describe('updateList', () => { }); }); - it('should commit UPDATE_LIST_FAILURE mutation when API returns an error', (done) => { + it('should dispatch handleUpdateListFailure when API returns an error', () => { jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ data: { updateBoardList: { @@ -559,17 +687,31 @@ describe('updateList', () => { }, }); - testAction( + return testAction( actions.updateList, { listId: 'gid://gitlab/List/1', position: 1 }, createState(), - [{ type: types.UPDATE_LIST_FAILURE }], [], - done, + [{ type: 'handleUpdateListFailure' }], ); }); }); +describe('handleUpdateListFailure', () => { + it('should dispatch fetchLists action and commit SET_ERROR mutation', async () => { + await testAction({ + action: actions.handleUpdateListFailure, + expectedMutations: [ + { + type: types.SET_ERROR, + payload: 'An error occurred while updating the board list. Please try again.', + }, + ], + expectedActions: [{ type: 'fetchLists' }], + }); + }); +}); + describe('toggleListCollapsed', () => { it('should commit TOGGLE_LIST_COLLAPSED mutation', async () => { const payload = { listId: 'gid://gitlab/List/1', collapsed: true }; diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js index 37f0969a39a..a2ba1e9eb5e 100644 --- a/spec/frontend/boards/stores/mutations_spec.js +++ b/spec/frontend/boards/stores/mutations_spec.js @@ -165,40 +165,26 @@ describe('Board Store Mutations', () => { }); }); - describe('MOVE_LIST', () => { - it('updates boardLists state with reordered lists', () => { + describe('MOVE_LISTS', () => { + it('updates the positions of board lists', () => { state = { ...state, boardLists: initialBoardListsState, }; - mutations.MOVE_LIST(state, { - movedList: mockLists[0], - listAtNewIndex: mockLists[1], - }); - - expect(state.boardLists).toEqual({ - 'gid://gitlab/List/2': mockLists[1], - 'gid://gitlab/List/1': mockLists[0], - }); - }); - }); - - describe('UPDATE_LIST_FAILURE', () => { - it('updates boardLists state with previous order and sets error message', () => { - state = { - ...state, - boardLists: { - 'gid://gitlab/List/2': mockLists[1], - 'gid://gitlab/List/1': mockLists[0], + mutations.MOVE_LISTS(state, [ + { + listId: mockLists[0].id, + position: 1, }, - error: undefined, - }; - - mutations.UPDATE_LIST_FAILURE(state, initialBoardListsState); + { + listId: mockLists[1].id, + position: 0, + }, + ]); - expect(state.boardLists).toEqual(initialBoardListsState); - expect(state.error).toEqual('An error occurred while updating the list. Please try again.'); + expect(state.boardLists[mockLists[0].id].position).toBe(1); + expect(state.boardLists[mockLists[1].id].position).toBe(0); }); }); |