summaryrefslogtreecommitdiff
path: root/spec/frontend/boards
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/boards')
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js24
-rw-r--r--spec/frontend/boards/board_list_helper.js6
-rw-r--r--spec/frontend/boards/board_list_spec.js116
-rw-r--r--spec/frontend/boards/components/board_card_spec.js1
-rw-r--r--spec/frontend/boards/components/board_filtered_search_spec.js6
-rw-r--r--spec/frontend/boards/components/board_form_spec.js9
-rw-r--r--spec/frontend/boards/components/board_new_issue_spec.js154
-rw-r--r--spec/frontend/boards/components/board_new_item_spec.js103
-rw-r--r--spec/frontend/boards/components/issue_board_filtered_search_spec.js31
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js2
-rw-r--r--spec/frontend/boards/mock_data.js47
-rw-r--r--spec/frontend/boards/stores/actions_spec.js274
-rw-r--r--spec/frontend/boards/stores/mutations_spec.js40
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);
});
});