Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/boards')
-rw-r--r--spec/frontend/boards/board_list_new_spec.js106
-rw-r--r--spec/frontend/boards/boards_store_spec.js149
-rw-r--r--spec/frontend/boards/components/board_assignee_dropdown_spec.js91
-rw-r--r--spec/frontend/boards/components/board_column_new_spec.js11
-rw-r--r--spec/frontend/boards/components/board_content_spec.js60
-rw-r--r--spec/frontend/boards/components/board_form_spec.js274
-rw-r--r--spec/frontend/boards/components/board_list_header_new_spec.js44
-rw-r--r--spec/frontend/boards/components/board_list_header_spec.js2
-rw-r--r--spec/frontend/boards/components/board_new_issue_new_spec.js4
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js12
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js152
-rw-r--r--spec/frontend/boards/list_spec.js1
-rw-r--r--spec/frontend/boards/mock_data.js69
-rw-r--r--spec/frontend/boards/stores/actions_spec.js235
-rw-r--r--spec/frontend/boards/stores/getters_spec.js76
-rw-r--r--spec/frontend/boards/stores/mutations_spec.js62
16 files changed, 893 insertions, 455 deletions
diff --git a/spec/frontend/boards/board_list_new_spec.js b/spec/frontend/boards/board_list_new_spec.js
index 55516e3fd56..96b03ed927e 100644
--- a/spec/frontend/boards/board_list_new_spec.js
+++ b/spec/frontend/boards/board_list_new_spec.js
@@ -1,15 +1,11 @@
-/* global List */
-/* global ListIssue */
-
import Vuex from 'vuex';
import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
import { createLocalVue, mount } from '@vue/test-utils';
import eventHub from '~/boards/eventhub';
import BoardList from '~/boards/components/board_list_new.vue';
import BoardCard from '~/boards/components/board_card.vue';
-import '~/boards/models/issue';
import '~/boards/models/list';
-import { listObj, mockIssuesByListId, issues } from './mock_data';
+import { mockList, mockIssuesByListId, issues, mockIssues } from './mock_data';
import defaultState from '~/boards/stores/state';
const localVue = createLocalVue();
@@ -46,13 +42,11 @@ const createComponent = ({
...state,
});
- const list = new List({
- ...listObj,
- id: 'gid://gitlab/List/1',
+ const list = {
+ ...mockList,
...listProps,
- doNotFetchIssues: true,
- });
- const issue = new ListIssue({
+ };
+ const issue = {
title: 'Testing',
id: 1,
iid: 1,
@@ -60,9 +54,9 @@ const createComponent = ({
labels: [],
assignees: [],
...listIssueProps,
- });
- if (!Object.prototype.hasOwnProperty.call(listProps, 'issuesSize')) {
- list.issuesSize = 1;
+ };
+ if (!Object.prototype.hasOwnProperty.call(listProps, 'issuesCount')) {
+ list.issuesCount = 1;
}
const component = mount(BoardList, {
@@ -71,6 +65,7 @@ const createComponent = ({
disabled: false,
list,
issues: [issue],
+ canAdminList: true,
...componentProps,
},
store,
@@ -87,17 +82,19 @@ const createComponent = ({
describe('Board list component', () => {
let wrapper;
+ const findByTestId = testId => wrapper.find(`[data-testid="${testId}"]`);
useFakeRequestAnimationFrame();
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
describe('When Expanded', () => {
beforeEach(() => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders component', () => {
expect(wrapper.find('.board-list-component').exists()).toBe(true);
});
@@ -107,7 +104,7 @@ describe('Board list component', () => {
state: { listsFlags: { 'gid://gitlab/List/1': { isLoading: true } } },
});
- expect(wrapper.find('[data-testid="board_list_loading"').exists()).toBe(true);
+ expect(findByTestId('board_list_loading').exists()).toBe(true);
});
it('renders issues', () => {
@@ -157,7 +154,7 @@ describe('Board list component', () => {
it('shows how many more issues to load', async () => {
wrapper.vm.showCount = true;
- wrapper.setProps({ list: { issuesSize: 20 } });
+ wrapper.setProps({ list: { issuesCount: 20 } });
await wrapper.vm.$nextTick();
expect(wrapper.find('.board-list-count').text()).toBe('Showing 1 of 20 issues');
@@ -167,30 +164,30 @@ describe('Board list component', () => {
describe('load more issues', () => {
beforeEach(() => {
wrapper = createComponent({
- listProps: { issuesSize: 25 },
+ listProps: { issuesCount: 25 },
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('loads more issues after scrolling', () => {
- wrapper.vm.$refs.list.dispatchEvent(new Event('scroll'));
+ wrapper.vm.listRef.dispatchEvent(new Event('scroll'));
expect(actions.fetchIssuesForList).toHaveBeenCalled();
});
it('does not load issues if already loading', () => {
- wrapper.vm.$refs.list.dispatchEvent(new Event('scroll'));
- wrapper.vm.$refs.list.dispatchEvent(new Event('scroll'));
+ wrapper = createComponent({
+ state: { listsFlags: { 'gid://gitlab/List/1': { isLoadingMore: true } } },
+ });
+ wrapper.vm.listRef.dispatchEvent(new Event('scroll'));
- expect(actions.fetchIssuesForList).toHaveBeenCalledTimes(1);
+ expect(actions.fetchIssuesForList).not.toHaveBeenCalled();
});
it('shows loading more spinner', async () => {
+ wrapper = createComponent({
+ state: { listsFlags: { 'gid://gitlab/List/1': { isLoadingMore: true } } },
+ });
wrapper.vm.showCount = true;
- wrapper.vm.list.loadingMore = true;
await wrapper.vm.$nextTick();
expect(wrapper.find('.board-list-count .gl-spinner').exists()).toBe(true);
@@ -200,17 +197,13 @@ describe('Board list component', () => {
describe('max issue count warning', () => {
beforeEach(() => {
wrapper = createComponent({
- listProps: { issuesSize: 50 },
+ listProps: { issuesCount: 50 },
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when issue count exceeds max issue count', () => {
it('sets background to bg-danger-100', async () => {
- wrapper.setProps({ list: { issuesSize: 4, maxIssueCount: 3 } });
+ wrapper.setProps({ list: { issuesCount: 4, maxIssueCount: 3 } });
await wrapper.vm.$nextTick();
expect(wrapper.find('.bg-danger-100').exists()).toBe(true);
@@ -219,7 +212,7 @@ describe('Board list component', () => {
describe('when list issue count does NOT exceed list max issue count', () => {
it('does not sets background to bg-danger-100', () => {
- wrapper.setProps({ list: { issuesSize: 2, maxIssueCount: 3 } });
+ wrapper.setProps({ list: { issuesCount: 2, maxIssueCount: 3 } });
expect(wrapper.find('.bg-danger-100').exists()).toBe(false);
});
@@ -233,4 +226,43 @@ describe('Board list component', () => {
});
});
});
+
+ describe('drag & drop issue', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ 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');
+
+ expect(document.body.classList.contains('is-dragging')).toBe(true);
+ });
+ });
+
+ describe('handleDragOnEnd', () => {
+ it('removes class `is-dragging` from document body', () => {
+ jest.spyOn(wrapper.vm, 'moveIssue').mockImplementation(() => {});
+ document.body.classList.add('is-dragging');
+
+ findByTestId('tree-root-wrapper').vm.$emit('end', {
+ oldIndex: 1,
+ newIndex: 0,
+ item: {
+ dataset: {
+ issueId: mockIssues[0].id,
+ issueIid: mockIssues[0].iid,
+ issuePath: 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(false);
+ });
+ });
+ });
});
diff --git a/spec/frontend/boards/boards_store_spec.js b/spec/frontend/boards/boards_store_spec.js
index e7c1cf79fdc..c89f6d22ef2 100644
--- a/spec/frontend/boards/boards_store_spec.js
+++ b/spec/frontend/boards/boards_store_spec.js
@@ -1,7 +1,7 @@
import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
-import boardsStore, { gqlClient } from '~/boards/stores/boards_store';
+import boardsStore from '~/boards/stores/boards_store';
import eventHub from '~/boards/eventhub';
import { listObj, listObjDuplicate } from './mock_data';
@@ -66,23 +66,6 @@ describe('boardsStore', () => {
});
});
- describe('generateDefaultLists', () => {
- const listsEndpointGenerate = `${endpoints.listsEndpoint}/generate.json`;
-
- it('makes a request to generate default lists', () => {
- axiosMock.onPost(listsEndpointGenerate).replyOnce(200, dummyResponse);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.generateDefaultLists()).resolves.toEqual(expectedResponse);
- });
-
- it('fails for error response', () => {
- axiosMock.onPost(listsEndpointGenerate).replyOnce(500);
-
- return expect(boardsStore.generateDefaultLists()).rejects.toThrow();
- });
- });
-
describe('createList', () => {
const entityType = 'moorhen';
const entityId = 'quack';
@@ -473,118 +456,6 @@ describe('boardsStore', () => {
});
});
- describe('createBoard', () => {
- const labelIds = ['first label', 'second label'];
- const assigneeId = 'as sign ee';
- const milestoneId = 'vegetable soup';
- const board = {
- labels: labelIds.map(id => ({ id })),
- assignee: { id: assigneeId },
- milestone: { id: milestoneId },
- };
-
- describe('for existing board', () => {
- const id = 'skate-board';
- const url = `${endpoints.boardsEndpoint}/${id}.json`;
- const expectedRequest = expect.objectContaining({
- data: JSON.stringify({
- board: {
- ...board,
- id,
- label_ids: labelIds,
- assignee_id: assigneeId,
- milestone_id: milestoneId,
- },
- }),
- });
-
- let requestSpy;
-
- beforeEach(() => {
- requestSpy = jest.fn();
- axiosMock.onPut(url).replyOnce(config => requestSpy(config));
- jest.spyOn(gqlClient, 'mutate').mockReturnValue(Promise.resolve({}));
- });
-
- it('makes a request to update the board', () => {
- requestSpy.mockReturnValue([200, dummyResponse]);
- const expectedResponse = [
- expect.objectContaining({ data: dummyResponse }),
- expect.objectContaining({}),
- ];
-
- return expect(
- boardsStore.createBoard({
- ...board,
- id,
- }),
- )
- .resolves.toEqual(expectedResponse)
- .then(() => {
- expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
- });
- });
-
- it('fails for error response', () => {
- requestSpy.mockReturnValue([500]);
-
- return expect(
- boardsStore.createBoard({
- ...board,
- id,
- }),
- )
- .rejects.toThrow()
- .then(() => {
- expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
- });
- });
- });
-
- describe('for new board', () => {
- const url = `${endpoints.boardsEndpoint}.json`;
- const expectedRequest = expect.objectContaining({
- data: JSON.stringify({
- board: {
- ...board,
- label_ids: labelIds,
- assignee_id: assigneeId,
- milestone_id: milestoneId,
- },
- }),
- });
-
- let requestSpy;
-
- beforeEach(() => {
- requestSpy = jest.fn();
- axiosMock.onPost(url).replyOnce(config => requestSpy(config));
- jest.spyOn(gqlClient, 'mutate').mockReturnValue(Promise.resolve({}));
- });
-
- it('makes a request to create a new board', () => {
- requestSpy.mockReturnValue([200, dummyResponse]);
- const expectedResponse = dummyResponse;
-
- return expect(boardsStore.createBoard(board))
- .resolves.toEqual(expectedResponse)
- .then(() => {
- expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
- });
- });
-
- it('fails for error response', () => {
- requestSpy.mockReturnValue([500]);
-
- return expect(boardsStore.createBoard(board))
- .rejects.toThrow()
- .then(() => {
- expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
- });
- });
- });
- });
-
describe('deleteBoard', () => {
const id = 'capsized';
const url = `${endpoints.boardsEndpoint}/${id}.json`;
@@ -727,24 +598,6 @@ describe('boardsStore', () => {
});
});
- it('check for blank state adding', () => {
- expect(boardsStore.shouldAddBlankState()).toBe(true);
- });
-
- it('check for blank state not adding', () => {
- boardsStore.addList(listObj);
-
- expect(boardsStore.shouldAddBlankState()).toBe(false);
- });
-
- it('check for blank state adding when closed list exist', () => {
- boardsStore.addList({
- list_type: 'closed',
- });
-
- expect(boardsStore.shouldAddBlankState()).toBe(true);
- });
-
it('removes list from state', () => {
boardsStore.addList(listObj);
diff --git a/spec/frontend/boards/components/board_assignee_dropdown_spec.js b/spec/frontend/boards/components/board_assignee_dropdown_spec.js
index e185a6d5419..bbdcc707f09 100644
--- a/spec/frontend/boards/components/board_assignee_dropdown_spec.js
+++ b/spec/frontend/boards/components/board_assignee_dropdown_spec.js
@@ -1,5 +1,11 @@
import { mount, createLocalVue } from '@vue/test-utils';
-import { GlDropdownItem, GlAvatarLink, GlAvatarLabeled, GlSearchBoxByType } from '@gitlab/ui';
+import {
+ GlDropdownItem,
+ GlAvatarLink,
+ GlAvatarLabeled,
+ GlSearchBoxByType,
+ GlLoadingIcon,
+} from '@gitlab/ui';
import createMockApollo from 'jest/helpers/mock_apollo_helper';
import VueApollo from 'vue-apollo';
import BoardAssigneeDropdown from '~/boards/components/board_assignee_dropdown.vue';
@@ -8,7 +14,7 @@ import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dro
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import store from '~/boards/stores';
import getIssueParticipants from '~/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql';
-import searchUsers from '~/boards/queries/users_search.query.graphql';
+import searchUsers from '~/boards/graphql/users_search.query.graphql';
import { participants } from '../mock_data';
const localVue = createLocalVue();
@@ -20,17 +26,18 @@ describe('BoardCardAssigneeDropdown', () => {
let fakeApollo;
let getIssueParticipantsSpy;
let getSearchUsersSpy;
+ let dispatchSpy;
const iid = '111';
const activeIssueName = 'test';
const anotherIssueName = 'hello';
- const createComponent = (search = '') => {
+ const createComponent = (search = '', loading = false) => {
wrapper = mount(BoardAssigneeDropdown, {
data() {
return {
search,
- selected: store.getters.activeIssue.assignees,
+ selected: [],
participants,
};
},
@@ -39,6 +46,15 @@ describe('BoardCardAssigneeDropdown', () => {
canUpdate: true,
rootPath: '',
},
+ mocks: {
+ $apollo: {
+ queries: {
+ participants: {
+ loading,
+ },
+ },
+ },
+ },
});
};
@@ -47,14 +63,13 @@ describe('BoardCardAssigneeDropdown', () => {
[getIssueParticipants, getIssueParticipantsSpy],
[searchUsers, getSearchUsersSpy],
]);
-
wrapper = mount(BoardAssigneeDropdown, {
localVue,
apolloProvider: fakeApollo,
data() {
return {
search,
- selected: store.getters.activeIssue.assignees,
+ selected: [],
participants,
};
},
@@ -82,6 +97,8 @@ describe('BoardCardAssigneeDropdown', () => {
return wrapper.findAll(GlDropdownItem).wrappers.find(node => node.text().indexOf(text) === 0);
};
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+
beforeEach(() => {
store.state.activeId = '1';
store.state.issues = {
@@ -91,10 +108,11 @@ describe('BoardCardAssigneeDropdown', () => {
},
};
- jest.spyOn(store, 'dispatch').mockResolvedValue();
+ dispatchSpy = jest.spyOn(store, 'dispatch').mockResolvedValue();
});
afterEach(() => {
+ window.gon = {};
jest.restoreAllMocks();
});
@@ -243,6 +261,30 @@ describe('BoardCardAssigneeDropdown', () => {
},
);
+ describe('when participants is loading', () => {
+ beforeEach(() => {
+ createComponent('', true);
+ });
+
+ it('finds a loading icon in the dropdown', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+ });
+
+ describe('when participants is loading is false', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('does not find GlLoading icon in the dropdown', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it('finds at least 1 GlDropdownItem', () => {
+ expect(wrapper.findAll(GlDropdownItem).length).toBeGreaterThan(0);
+ });
+ });
+
describe('Apollo', () => {
beforeEach(() => {
getIssueParticipantsSpy = jest.fn().mockResolvedValue({
@@ -305,4 +347,39 @@ describe('BoardCardAssigneeDropdown', () => {
expect(wrapper.find(GlSearchBoxByType).exists()).toBe(true);
});
+
+ describe('when assign-self is emitted from IssuableAssignees', () => {
+ const currentUser = { username: 'self', name: '', id: '' };
+
+ beforeEach(() => {
+ window.gon = { current_username: currentUser.username };
+
+ dispatchSpy.mockResolvedValue([currentUser]);
+ createComponent();
+
+ wrapper.find(IssuableAssignees).vm.$emit('assign-self');
+ });
+
+ it('calls setAssignees with currentUser', () => {
+ expect(store.dispatch).toHaveBeenCalledWith('setAssignees', currentUser.username);
+ });
+
+ it('adds the user to the selected list', async () => {
+ expect(findByText(currentUser.username).exists()).toBe(true);
+ });
+ });
+
+ describe('when setting an assignee', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('passes loading state from Vuex to BoardEditableItem', async () => {
+ store.state.isSettingAssignees = true;
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.find(BoardEditableItem).props('loading')).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/boards/components/board_column_new_spec.js b/spec/frontend/boards/components/board_column_new_spec.js
index 4aafc3a867a..81c0e60f931 100644
--- a/spec/frontend/boards/components/board_column_new_spec.js
+++ b/spec/frontend/boards/components/board_column_new_spec.js
@@ -2,7 +2,6 @@ import { shallowMount } from '@vue/test-utils';
import { listObj } from 'jest/boards/mock_data';
import BoardColumn from '~/boards/components/board_column_new.vue';
-import List from '~/boards/models/list';
import { ListType } from '~/boards/constants';
import { createStore } from '~/boards/stores';
@@ -20,24 +19,22 @@ describe('Board Column Component', () => {
const listMock = {
...listObj,
- list_type: listType,
+ listType,
collapsed,
};
if (listType === ListType.assignee) {
delete listMock.label;
- listMock.user = {};
+ listMock.assignee = {};
}
- const list = new List({ ...listMock, doNotFetchIssues: true });
-
store = createStore();
wrapper = shallowMount(BoardColumn, {
store,
propsData: {
disabled: false,
- list,
+ list: listMock,
},
provide: {
boardId,
@@ -60,7 +57,7 @@ describe('Board Column Component', () => {
it('has class is-collapsed when list is collapsed', () => {
createComponent({ collapsed: false });
- expect(wrapper.vm.list.isExpanded).toBe(true);
+ expect(isCollapsed()).toBe(false);
});
it('does not have class is-collapsed when list is expanded', () => {
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
index 09e38001e2e..291013c561e 100644
--- a/spec/frontend/boards/components/board_content_spec.js
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -1,32 +1,38 @@
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlAlert } from '@gitlab/ui';
+import Draggable from 'vuedraggable';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
-import BoardColumn from 'ee_else_ce/boards/components/board_column.vue';
import getters from 'ee_else_ce/boards/stores/getters';
-import { mockListsWithModel } from '../mock_data';
+import BoardColumn from '~/boards/components/board_column.vue';
+import { mockLists, mockListsWithModel } from '../mock_data';
import BoardContent from '~/boards/components/board_content.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
+const actions = {
+ moveList: jest.fn(),
+};
+
describe('BoardContent', () => {
let wrapper;
const defaultState = {
isShowingEpicsSwimlanes: false,
- boardLists: mockListsWithModel,
+ boardLists: mockLists,
error: undefined,
};
const createStore = (state = defaultState) => {
return new Vuex.Store({
+ actions,
getters,
state,
});
};
- const createComponent = state => {
+ const createComponent = ({ state, props = {}, graphqlBoardListsEnabled = false } = {}) => {
const store = createStore({
...defaultState,
...state,
@@ -37,25 +43,61 @@ describe('BoardContent', () => {
lists: mockListsWithModel,
canAdminList: true,
disabled: false,
+ ...props,
+ },
+ provide: {
+ glFeatures: { graphqlBoardLists: graphqlBoardListsEnabled },
},
store,
});
};
- beforeEach(() => {
- createComponent();
- });
-
afterEach(() => {
wrapper.destroy();
});
it('renders a BoardColumn component per list', () => {
- expect(wrapper.findAll(BoardColumn)).toHaveLength(mockListsWithModel.length);
+ createComponent();
+
+ expect(wrapper.findAll(BoardColumn)).toHaveLength(mockLists.length);
});
it('does not display EpicsSwimlanes component', () => {
+ createComponent();
+
expect(wrapper.find(EpicsSwimlanes).exists()).toBe(false);
expect(wrapper.find(GlAlert).exists()).toBe(false);
});
+
+ describe('graphqlBoardLists feature flag enabled', () => {
+ describe('can admin list', () => {
+ beforeEach(() => {
+ createComponent({ graphqlBoardListsEnabled: true, props: { canAdminList: true } });
+ });
+
+ it('renders draggable component', () => {
+ expect(wrapper.find(Draggable).exists()).toBe(true);
+ });
+ });
+
+ describe('can not admin list', () => {
+ beforeEach(() => {
+ createComponent({ graphqlBoardListsEnabled: true, props: { canAdminList: false } });
+ });
+
+ it('renders draggable component', () => {
+ expect(wrapper.find(Draggable).exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('graphqlBoardLists feature flag disabled', () => {
+ beforeEach(() => {
+ createComponent({ graphqlBoardListsEnabled: false });
+ });
+
+ it('does not render draggable component', () => {
+ expect(wrapper.find(Draggable).exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js
index 65d8070192c..3b15cbb6b7e 100644
--- a/spec/frontend/boards/components/board_form_spec.js
+++ b/spec/frontend/boards/components/board_form_spec.js
@@ -1,47 +1,275 @@
-import { mount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'jest/helpers/test_constants';
+import { GlModal } from '@gitlab/ui';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import axios from '~/lib/utils/axios_utils';
+import { visitUrl } from '~/lib/utils/url_utility';
import boardsStore from '~/boards/stores/boards_store';
-import boardForm from '~/boards/components/board_form.vue';
-import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
+import BoardForm from '~/boards/components/board_form.vue';
+import BoardConfigurationOptions from '~/boards/components/board_configuration_options.vue';
+import createBoardMutation from '~/boards/graphql/board.mutation.graphql';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ visitUrl: jest.fn().mockName('visitUrlMock'),
+}));
+
+const currentBoard = {
+ id: 1,
+ name: 'test',
+ labels: [],
+ milestone_id: undefined,
+ assignee: {},
+ assignee_id: undefined,
+ weight: null,
+ hide_backlog_list: false,
+ hide_closed_list: false,
+};
+
+const boardDefaults = {
+ id: false,
+ name: '',
+ labels: [],
+ milestone_id: undefined,
+ assignee: {},
+ assignee_id: undefined,
+ weight: null,
+ hide_backlog_list: false,
+ hide_closed_list: false,
+};
+
+const defaultProps = {
+ canAdminBoard: false,
+ labelsPath: `${TEST_HOST}/labels/path`,
+ labelsWebUrl: `${TEST_HOST}/-/labels`,
+ currentBoard,
+};
-describe('board_form.vue', () => {
+const endpoints = {
+ boardsEndpoint: 'test-endpoint',
+};
+
+const mutate = jest.fn().mockResolvedValue({});
+
+describe('BoardForm', () => {
let wrapper;
+ let axiosMock;
- const propsData = {
- canAdminBoard: false,
- labelsPath: `${TEST_HOST}/labels/path`,
- labelsWebUrl: `${TEST_HOST}/-/labels`,
- };
+ const findModal = () => wrapper.find(GlModal);
+ const findModalActionPrimary = () => findModal().props('actionPrimary');
+ const findForm = () => wrapper.find('[data-testid="board-form"]');
+ const findFormWrapper = () => wrapper.find('[data-testid="board-form-wrapper"]');
+ const findDeleteConfirmation = () => wrapper.find('[data-testid="delete-confirmation-message"]');
+ const findConfigurationOptions = () => wrapper.find(BoardConfigurationOptions);
+ const findInput = () => wrapper.find('#board-new-name');
- const findModal = () => wrapper.find(DeprecatedModal);
+ const createComponent = (props, data) => {
+ wrapper = shallowMount(BoardForm, {
+ propsData: { ...defaultProps, ...props },
+ data() {
+ return {
+ ...data,
+ };
+ },
+ provide: {
+ endpoints,
+ },
+ mocks: {
+ $apollo: {
+ mutate,
+ },
+ },
+ attachToDocument: true,
+ });
+ };
beforeEach(() => {
- boardsStore.state.currentPage = 'edit';
- wrapper = mount(boardForm, { propsData });
+ axiosMock = new AxiosMockAdapter(axios);
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
+ axiosMock.restore();
+ boardsStore.state.currentPage = null;
});
- describe('methods', () => {
- describe('cancel', () => {
- it('resets currentPage', () => {
- wrapper.vm.cancel();
- expect(boardsStore.state.currentPage).toBe('');
+ describe('when user can not admin the board', () => {
+ beforeEach(() => {
+ boardsStore.state.currentPage = 'new';
+ createComponent();
+ });
+
+ it('hides modal footer when user is not a board admin', () => {
+ expect(findModal().attributes('hide-footer')).toBeDefined();
+ });
+
+ it('displays board scope title', () => {
+ expect(findModal().attributes('title')).toBe('Board scope');
+ });
+
+ it('does not display a form', () => {
+ expect(findForm().exists()).toBe(false);
+ });
+ });
+
+ describe('when user can admin the board', () => {
+ beforeEach(() => {
+ boardsStore.state.currentPage = 'new';
+ createComponent({ canAdminBoard: true });
+ });
+
+ it('shows modal footer when user is a board admin', () => {
+ expect(findModal().attributes('hide-footer')).toBeUndefined();
+ });
+
+ it('displays a form', () => {
+ expect(findForm().exists()).toBe(true);
+ });
+
+ it('focuses an input field', async () => {
+ expect(document.activeElement).toBe(wrapper.vm.$refs.name);
+ });
+ });
+
+ describe('when creating a new board', () => {
+ beforeEach(() => {
+ boardsStore.state.currentPage = 'new';
+ });
+
+ describe('on non-scoped-board', () => {
+ beforeEach(() => {
+ createComponent({ canAdminBoard: true });
+ });
+
+ it('clears the form', () => {
+ expect(findConfigurationOptions().props('board')).toEqual(boardDefaults);
+ });
+
+ it('shows a correct title about creating a board', () => {
+ expect(findModal().attributes('title')).toBe('Create new board');
+ });
+
+ it('passes correct primary action text and variant', () => {
+ expect(findModalActionPrimary().text).toBe('Create board');
+ expect(findModalActionPrimary().attributes[0].variant).toBe('success');
+ });
+
+ it('does not render delete confirmation message', () => {
+ expect(findDeleteConfirmation().exists()).toBe(false);
+ });
+
+ it('renders form wrapper', () => {
+ expect(findFormWrapper().exists()).toBe(true);
+ });
+
+ it('passes a true isNewForm prop to BoardConfigurationOptions component', () => {
+ expect(findConfigurationOptions().props('isNewForm')).toBe(true);
+ });
+ });
+
+ describe('when submitting a create event', () => {
+ beforeEach(() => {
+ const url = `${endpoints.boardsEndpoint}.json`;
+ axiosMock.onPost(url).reply(200, { id: '2', board_path: 'new path' });
+ });
+
+ it('does not call API if board name is empty', async () => {
+ createComponent({ canAdminBoard: true });
+ findInput().trigger('keyup.enter', { metaKey: true });
+
+ await waitForPromises();
+
+ expect(mutate).not.toHaveBeenCalled();
+ });
+
+ it('calls REST and GraphQL API and redirects to correct page', async () => {
+ createComponent({ canAdminBoard: true });
+
+ findInput().value = 'Test name';
+ findInput().trigger('input');
+ findInput().trigger('keyup.enter', { metaKey: true });
+
+ await waitForPromises();
+
+ expect(axiosMock.history.post[0].data).toBe(
+ JSON.stringify({ board: { ...boardDefaults, name: 'test', label_ids: [''] } }),
+ );
+
+ expect(mutate).toHaveBeenCalledWith({
+ mutation: createBoardMutation,
+ variables: {
+ id: 'gid://gitlab/Board/2',
+ },
+ });
+
+ await waitForPromises();
+ expect(visitUrl).toHaveBeenCalledWith('new path');
});
});
});
- describe('buttons', () => {
- it('cancel button triggers cancel()', () => {
- wrapper.setMethods({ cancel: jest.fn() });
- findModal().vm.$emit('cancel');
+ describe('when editing a board', () => {
+ beforeEach(() => {
+ boardsStore.state.currentPage = 'edit';
+ });
+
+ describe('on non-scoped-board', () => {
+ beforeEach(() => {
+ createComponent({ canAdminBoard: true });
+ });
+
+ it('clears the form', () => {
+ expect(findConfigurationOptions().props('board')).toEqual(currentBoard);
+ });
+
+ it('shows a correct title about creating a board', () => {
+ expect(findModal().attributes('title')).toBe('Edit board');
+ });
+
+ it('passes correct primary action text and variant', () => {
+ expect(findModalActionPrimary().text).toBe('Save changes');
+ expect(findModalActionPrimary().attributes[0].variant).toBe('info');
+ });
+
+ it('does not render delete confirmation message', () => {
+ expect(findDeleteConfirmation().exists()).toBe(false);
+ });
+
+ it('renders form wrapper', () => {
+ expect(findFormWrapper().exists()).toBe(true);
+ });
+
+ it('passes a false isNewForm prop to BoardConfigurationOptions component', () => {
+ expect(findConfigurationOptions().props('isNewForm')).toBe(false);
+ });
+ });
+
+ describe('when submitting an update event', () => {
+ beforeEach(() => {
+ const url = endpoints.boardsEndpoint;
+ axiosMock.onPut(url).reply(200, { board_path: 'new path' });
+ });
+
+ it('calls REST and GraphQL API with correct parameters', async () => {
+ createComponent({ canAdminBoard: true });
+
+ findInput().trigger('keyup.enter', { metaKey: true });
+
+ await waitForPromises();
+
+ expect(axiosMock.history.put[0].data).toBe(
+ JSON.stringify({ board: { ...currentBoard, label_ids: [''] } }),
+ );
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.cancel).toHaveBeenCalled();
+ expect(mutate).toHaveBeenCalledWith({
+ mutation: createBoardMutation,
+ variables: {
+ id: `gid://gitlab/Board/${currentBoard.id}`,
+ },
+ });
});
});
});
diff --git a/spec/frontend/boards/components/board_list_header_new_spec.js b/spec/frontend/boards/components/board_list_header_new_spec.js
index 80786d82620..7428dfae83f 100644
--- a/spec/frontend/boards/components/board_list_header_new_spec.js
+++ b/spec/frontend/boards/components/board_list_header_new_spec.js
@@ -1,9 +1,8 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { listObj } from 'jest/boards/mock_data';
+import { mockLabelList } from 'jest/boards/mock_data';
import BoardListHeader from '~/boards/components/board_list_header_new.vue';
-import List from '~/boards/models/list';
import { ListType } from '~/boards/constants';
const localVue = createLocalVue();
@@ -32,21 +31,19 @@ describe('Board List Header Component', () => {
const boardId = '1';
const listMock = {
- ...listObj,
- list_type: listType,
+ ...mockLabelList,
+ listType,
collapsed,
};
if (listType === ListType.assignee) {
delete listMock.label;
- listMock.user = {};
+ listMock.assignee = {};
}
- const list = new List({ ...listMock, doNotFetchIssues: true });
-
if (withLocalStorage) {
localStorage.setItem(
- `boards.${boardId}.${list.type}.${list.id}.expanded`,
+ `boards.${boardId}.${listMock.listType}.${listMock.id}.expanded`,
(!collapsed).toString(),
);
}
@@ -62,7 +59,7 @@ describe('Board List Header Component', () => {
localVue,
propsData: {
disabled: false,
- list,
+ list: listMock,
},
provide: {
boardId,
@@ -72,14 +69,15 @@ describe('Board List Header Component', () => {
});
};
- const isExpanded = () => wrapper.vm.list.isExpanded;
- const isCollapsed = () => !isExpanded();
+ const isCollapsed = () => wrapper.vm.list.collapsed;
+ const isExpanded = () => !isCollapsed;
const findAddIssueButton = () => wrapper.find({ ref: 'newIssueBtn' });
+ const findTitle = () => wrapper.find('.board-title');
const findCaret = () => wrapper.find('.board-title-caret');
describe('Add issue button', () => {
- const hasNoAddButton = [ListType.promotion, ListType.blank, ListType.closed];
+ const hasNoAddButton = [ListType.closed];
const hasAddButton = [ListType.backlog, ListType.label, ListType.milestone, ListType.assignee];
it.each(hasNoAddButton)('does not render when List Type is `%s`', listType => {
@@ -125,7 +123,7 @@ describe('Board List Header Component', () => {
it('collapses expanded Column when clicking the collapse icon', async () => {
createComponent();
- expect(isExpanded()).toBe(true);
+ expect(isCollapsed()).toBe(false);
findCaret().vm.$emit('click');
@@ -166,4 +164,24 @@ describe('Board List Header Component', () => {
expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(String(isExpanded()));
});
});
+
+ describe('user can drag', () => {
+ const cannotDragList = [ListType.backlog, ListType.closed];
+ const canDragList = [ListType.label, ListType.milestone, ListType.assignee];
+
+ it.each(cannotDragList)(
+ 'does not have user-can-drag-class so user cannot drag list',
+ listType => {
+ createComponent({ listType });
+
+ expect(findTitle().classes()).not.toContain('user-can-drag');
+ },
+ );
+
+ it.each(canDragList)('has user-can-drag-class so user can drag list', listType => {
+ createComponent({ listType });
+
+ expect(findTitle().classes()).toContain('user-can-drag');
+ });
+ });
});
diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js
index 2439c347bf0..656a503bb86 100644
--- a/spec/frontend/boards/components/board_list_header_spec.js
+++ b/spec/frontend/boards/components/board_list_header_spec.js
@@ -73,7 +73,7 @@ describe('Board List Header Component', () => {
const findCaret = () => wrapper.find('.board-title-caret');
describe('Add issue button', () => {
- const hasNoAddButton = [ListType.promotion, ListType.blank, ListType.closed];
+ const hasNoAddButton = [ListType.closed];
const hasAddButton = [ListType.backlog, ListType.label, ListType.milestone, ListType.assignee];
it.each(hasNoAddButton)('does not render when List Type is `%s`', listType => {
diff --git a/spec/frontend/boards/components/board_new_issue_new_spec.js b/spec/frontend/boards/components/board_new_issue_new_spec.js
index af4bad65121..ee1c4f31cf0 100644
--- a/spec/frontend/boards/components/board_new_issue_new_spec.js
+++ b/spec/frontend/boards/components/board_new_issue_new_spec.js
@@ -3,7 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import BoardNewIssue from '~/boards/components/board_new_issue_new.vue';
import '~/boards/models/list';
-import { mockListsWithModel } from '../mock_data';
+import { mockList } from '../mock_data';
const localVue = createLocalVue();
@@ -37,7 +37,7 @@ describe('Issue boards new issue form', () => {
wrapper = shallowMount(BoardNewIssue, {
propsData: {
disabled: false,
- list: mockListsWithModel[0],
+ list: mockList,
},
store,
localVue,
diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js
index 2b7605a3f7c..db3c8c22950 100644
--- a/spec/frontend/boards/components/boards_selector_spec.js
+++ b/spec/frontend/boards/components/boards_selector_spec.js
@@ -1,6 +1,6 @@
import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
-import { GlDeprecatedDropdown, GlLoadingIcon } from '@gitlab/ui';
+import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui';
import { TEST_HOST } from 'spec/test_constants';
import BoardsSelector from '~/boards/components/boards_selector.vue';
import boardsStore from '~/boards/stores/boards_store';
@@ -34,8 +34,9 @@ describe('BoardsSelector', () => {
};
const getDropdownItems = () => wrapper.findAll('.js-dropdown-item');
- const getDropdownHeaders = () => wrapper.findAll('.dropdown-bold-header');
+ const getDropdownHeaders = () => wrapper.findAll(GlDropdownSectionHeader);
const getLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findDropdown = () => wrapper.find(GlDropdown);
beforeEach(() => {
const $apollo = {
@@ -103,7 +104,7 @@ describe('BoardsSelector', () => {
});
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
- wrapper.find(GlDeprecatedDropdown).vm.$emit('show');
+ findDropdown().vm.$emit('show');
});
afterEach(() => {
@@ -125,7 +126,10 @@ describe('BoardsSelector', () => {
});
describe('loaded', () => {
- beforeEach(() => {
+ beforeEach(async () => {
+ await wrapper.setData({
+ loadingBoards: false,
+ });
return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick());
});
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js
new file mode 100644
index 00000000000..74d88d9f34c
--- /dev/null
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js
@@ -0,0 +1,152 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { mockMilestone as TEST_MILESTONE } from 'jest/boards/mock_data';
+import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
+import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
+import { createStore } from '~/boards/stores';
+import createFlash from '~/flash';
+
+const TEST_ISSUE = { id: 'gid://gitlab/Issue/1', iid: 9, referencePath: 'h/b#2' };
+
+jest.mock('~/flash');
+
+describe('~/boards/components/sidebar/board_sidebar_milestone_select.vue', () => {
+ let wrapper;
+ let store;
+
+ afterEach(() => {
+ wrapper.destroy();
+ store = null;
+ wrapper = null;
+ });
+
+ const createWrapper = ({ milestone = null } = {}) => {
+ store = createStore();
+ store.state.issues = { [TEST_ISSUE.id]: { ...TEST_ISSUE, milestone } };
+ store.state.activeId = TEST_ISSUE.id;
+
+ wrapper = shallowMount(BoardSidebarMilestoneSelect, {
+ store,
+ provide: {
+ canUpdate: true,
+ },
+ data: () => ({
+ milestones: [TEST_MILESTONE],
+ }),
+ stubs: {
+ 'board-editable-item': BoardEditableItem,
+ },
+ mocks: {
+ $apollo: {
+ loading: false,
+ },
+ },
+ });
+ };
+
+ const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
+ const findLoader = () => wrapper.find(GlLoadingIcon);
+ const findDropdownItem = () => wrapper.find('[data-testid="milestone-item"]');
+ const findUnsetMilestoneItem = () => wrapper.find('[data-testid="no-milestone-item"]');
+ const findNoMilestonesFoundItem = () => wrapper.find('[data-testid="no-milestones-found"]');
+
+ it('renders "None" when no milestone is selected', () => {
+ createWrapper();
+
+ expect(findCollapsed().text()).toBe('None');
+ });
+
+ it('renders milestone title when set', () => {
+ createWrapper({ milestone: TEST_MILESTONE });
+
+ expect(findCollapsed().text()).toContain(TEST_MILESTONE.title);
+ });
+
+ it('shows loader while Apollo is loading', async () => {
+ createWrapper({ milestone: TEST_MILESTONE });
+
+ expect(findLoader().exists()).toBe(false);
+
+ wrapper.vm.$apollo.loading = true;
+ await wrapper.vm.$nextTick();
+
+ expect(findLoader().exists()).toBe(true);
+ });
+
+ it('shows message when error or no milestones found', async () => {
+ createWrapper();
+
+ wrapper.setData({ milestones: [] });
+ await wrapper.vm.$nextTick();
+
+ expect(findNoMilestonesFoundItem().text()).toBe('No milestones found');
+ });
+
+ describe('when milestone is selected', () => {
+ beforeEach(async () => {
+ createWrapper();
+
+ jest.spyOn(wrapper.vm, 'setActiveIssueMilestone').mockImplementation(() => {
+ store.state.issues[TEST_ISSUE.id].milestone = TEST_MILESTONE;
+ });
+ findDropdownItem().vm.$emit('click');
+ await wrapper.vm.$nextTick();
+ });
+
+ it('collapses sidebar and renders selected milestone', () => {
+ expect(findCollapsed().isVisible()).toBe(true);
+ expect(findCollapsed().text()).toContain(TEST_MILESTONE.title);
+ });
+
+ it('commits change to the server', () => {
+ expect(wrapper.vm.setActiveIssueMilestone).toHaveBeenCalledWith({
+ milestoneId: TEST_MILESTONE.id,
+ projectPath: 'h/b',
+ });
+ });
+ });
+
+ describe('when milestone is set to "None"', () => {
+ beforeEach(async () => {
+ createWrapper({ milestone: TEST_MILESTONE });
+
+ jest.spyOn(wrapper.vm, 'setActiveIssueMilestone').mockImplementation(() => {
+ store.state.issues[TEST_ISSUE.id].milestone = null;
+ });
+ findUnsetMilestoneItem().vm.$emit('click');
+ await wrapper.vm.$nextTick();
+ });
+
+ it('collapses sidebar and renders "None"', () => {
+ expect(findCollapsed().isVisible()).toBe(true);
+ expect(findCollapsed().text()).toBe('None');
+ });
+
+ it('commits change to the server', () => {
+ expect(wrapper.vm.setActiveIssueMilestone).toHaveBeenCalledWith({
+ milestoneId: null,
+ projectPath: 'h/b',
+ });
+ });
+ });
+
+ describe('when the mutation fails', () => {
+ const testMilestone = { id: '1', title: 'Former milestone' };
+
+ beforeEach(async () => {
+ createWrapper({ milestone: testMilestone });
+
+ jest.spyOn(wrapper.vm, 'setActiveIssueMilestone').mockImplementation(() => {
+ throw new Error(['failed mutation']);
+ });
+ findDropdownItem().vm.$emit('click');
+ await wrapper.vm.$nextTick();
+ });
+
+ it('collapses sidebar and renders former milestone', () => {
+ expect(findCollapsed().isVisible()).toBe(true);
+ expect(findCollapsed().text()).toContain(testMilestone.title);
+ expect(createFlash).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/boards/list_spec.js b/spec/frontend/boards/list_spec.js
index 9c3a6e66ef4..b731bb6e474 100644
--- a/spec/frontend/boards/list_spec.js
+++ b/spec/frontend/boards/list_spec.js
@@ -184,7 +184,6 @@ describe('List model', () => {
}),
);
list.issues = [];
- global.gon.features = { boardsWithSwimlanes: false };
});
it('adds new issue to top of list', done => {
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index 58f67231d55..ea6c52c6830 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -1,10 +1,8 @@
-/* global ListIssue */
/* global List */
import Vue from 'vue';
import { keyBy } from 'lodash';
import '~/boards/models/list';
-import '~/boards/models/issue';
import boardsStore from '~/boards/stores/boards_store';
export const boardObj = {
@@ -99,7 +97,7 @@ export const mockMilestone = {
due_date: '2019-12-31',
};
-const assignees = [
+export const assignees = [
{
id: 'gid://gitlab/User/2',
username: 'angelina.herman',
@@ -184,8 +182,6 @@ export const mockActiveIssue = {
emailsDisabled: false,
};
-export const mockIssueWithModel = new ListIssue(mockIssue);
-
export const mockIssue2 = {
id: 'gid://gitlab/Issue/437',
iid: 28,
@@ -203,8 +199,6 @@ export const mockIssue2 = {
},
};
-export const mockIssue2WithModel = new ListIssue(mockIssue2);
-
export const mockIssue3 = {
id: 'gid://gitlab/Issue/438',
iid: 29,
@@ -288,38 +282,39 @@ export const setMockEndpoints = (opts = {}) => {
});
};
-export const mockLists = [
- {
- id: 'gid://gitlab/List/1',
- title: 'Backlog',
- position: null,
- listType: 'backlog',
- collapsed: false,
- label: null,
- assignee: null,
- milestone: null,
- loading: false,
- issuesSize: 1,
- },
- {
- id: 'gid://gitlab/List/2',
+export const mockList = {
+ id: 'gid://gitlab/List/1',
+ title: 'Backlog',
+ position: null,
+ listType: 'backlog',
+ collapsed: false,
+ label: null,
+ assignee: null,
+ milestone: null,
+ loading: false,
+ issuesCount: 1,
+};
+
+export const mockLabelList = {
+ id: 'gid://gitlab/List/2',
+ title: 'To Do',
+ position: 0,
+ listType: 'label',
+ collapsed: false,
+ label: {
+ id: 'gid://gitlab/GroupLabel/121',
title: 'To Do',
- position: 0,
- listType: 'label',
- collapsed: false,
- label: {
- id: 'gid://gitlab/GroupLabel/121',
- title: 'To Do',
- color: '#F0AD4E',
- textColor: '#FFFFFF',
- description: null,
- },
- assignee: null,
- milestone: null,
- loading: false,
- issuesSize: 0,
+ color: '#F0AD4E',
+ textColor: '#FFFFFF',
+ description: null,
},
-];
+ assignee: null,
+ milestone: null,
+ loading: false,
+ issuesCount: 0,
+};
+
+export const mockLists = [mockList, mockLabelList];
export const mockListsById = keyBy(mockLists, 'id');
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index 4d529580a7a..0cae6456887 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -1,23 +1,25 @@
import testAction from 'helpers/vuex_action_helper';
import {
- mockListsWithModel,
mockLists,
mockListsById,
mockIssue,
- mockIssueWithModel,
- mockIssue2WithModel,
+ mockIssue2,
rawIssue,
mockIssues,
+ mockMilestone,
labels,
mockActiveIssue,
} from '../mock_data';
import actions, { gqlClient } from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types';
import { inactiveId } from '~/boards/constants';
-import issueMoveListMutation from '~/boards/queries/issue_move_list.mutation.graphql';
-import destroyBoardListMutation from '~/boards/queries/board_list_destroy.mutation.graphql';
+import issueMoveListMutation from '~/boards/graphql/issue_move_list.mutation.graphql';
+import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql';
import updateAssignees from '~/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql';
import { fullBoardId, formatListIssues, formatBoardLists } from '~/boards/boards_util';
+import createFlash from '~/flash';
+
+jest.mock('~/flash');
const expectNotImplemented = action => {
it('is not implemented', () => {
@@ -29,6 +31,10 @@ const expectNotImplemented = action => {
// subgroups when the movIssue action is called.
const getProjectPath = path => path.split('#')[0];
+beforeEach(() => {
+ window.gon = { features: {} };
+});
+
describe('setInitialBoardData', () => {
it('sets data object', () => {
const mockData = {
@@ -65,6 +71,24 @@ describe('setFilters', () => {
});
});
+describe('performSearch', () => {
+ it('should dispatch setFilters action', done => {
+ testAction(actions.performSearch, {}, {}, [], [{ type: 'setFilters', payload: {} }], done);
+ });
+
+ it('should dispatch setFilters, fetchLists and resetIssues action when graphqlBoardLists FF is on', done => {
+ window.gon = { features: { graphqlBoardLists: true } };
+ testAction(
+ actions.performSearch,
+ {},
+ {},
+ [],
+ [{ type: 'setFilters', payload: {} }, { type: 'fetchLists' }, { type: 'resetIssues' }],
+ done,
+ );
+ });
+});
+
describe('setActiveId', () => {
it('should commit mutation SET_ACTIVE_ID', done => {
const state = {
@@ -120,7 +144,7 @@ describe('fetchLists', () => {
payload: formattedLists,
},
],
- [{ type: 'generateDefaultLists' }],
+ [],
done,
);
});
@@ -150,37 +174,12 @@ describe('fetchLists', () => {
payload: formattedLists,
},
],
- [{ type: 'createList', payload: { backlog: true } }, { type: 'generateDefaultLists' }],
+ [{ type: 'createList', payload: { backlog: true } }],
done,
);
});
});
-describe('generateDefaultLists', () => {
- let store;
- beforeEach(() => {
- const state = {
- endpoints: { fullPath: 'gitlab-org', boardId: '1' },
- boardType: 'group',
- disabled: false,
- boardLists: [{ type: 'backlog' }, { type: 'closed' }],
- };
-
- store = {
- commit: jest.fn(),
- dispatch: jest.fn(() => Promise.resolve()),
- state,
- };
- });
-
- it('should dispatch fetchLabels', () => {
- return actions.generateDefaultLists(store).then(() => {
- expect(store.dispatch.mock.calls[0]).toEqual(['fetchLabels', 'to do']);
- expect(store.dispatch.mock.calls[1]).toEqual(['fetchLabels', 'doing']);
- });
- });
-});
-
describe('createList', () => {
it('should dispatch addList action when creating backlog list', done => {
const backlogList = {
@@ -251,8 +250,8 @@ describe('createList', () => {
describe('moveList', () => {
it('should commit MOVE_LIST mutation and dispatch updateList action', done => {
const initialBoardListsState = {
- 'gid://gitlab/List/1': mockListsWithModel[0],
- 'gid://gitlab/List/2': mockListsWithModel[1],
+ 'gid://gitlab/List/1': mockLists[0],
+ 'gid://gitlab/List/2': mockLists[1],
};
const state = {
@@ -274,7 +273,7 @@ describe('moveList', () => {
[
{
type: types.MOVE_LIST,
- payload: { movedList: mockListsWithModel[0], listAtNewIndex: mockListsWithModel[1] },
+ payload: { movedList: mockLists[0], listAtNewIndex: mockLists[1] },
},
],
[
@@ -290,6 +289,33 @@ describe('moveList', () => {
done,
);
});
+
+ 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],
+ };
+
+ const state = {
+ endpoints: { fullPath: 'gitlab-org', boardId: '1' },
+ boardType: 'group',
+ disabled: false,
+ boardLists: initialBoardListsState,
+ };
+
+ testAction(
+ actions.moveList,
+ {
+ listId: 'gid://gitlab/List/1',
+ replacedListId: 'gid://gitlab/List/1',
+ newIndex: 1,
+ adjustmentValue: 1,
+ },
+ state,
+ [],
+ [],
+ );
+ });
});
describe('updateList', () => {
@@ -499,15 +525,15 @@ describe('moveIssue', () => {
};
const issues = {
- '436': mockIssueWithModel,
- '437': mockIssue2WithModel,
+ '436': mockIssue,
+ '437': mockIssue2,
};
const state = {
endpoints: { fullPath: 'gitlab-org', boardId: '1' },
boardType: 'group',
disabled: false,
- boardLists: mockListsWithModel,
+ boardLists: mockLists,
issuesByListId: listIssues,
issues,
};
@@ -536,7 +562,7 @@ describe('moveIssue', () => {
{
type: types.MOVE_ISSUE,
payload: {
- originalIssue: mockIssueWithModel,
+ originalIssue: mockIssue,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
},
@@ -611,7 +637,7 @@ describe('moveIssue', () => {
{
type: types.MOVE_ISSUE,
payload: {
- originalIssue: mockIssueWithModel,
+ originalIssue: mockIssue,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
},
@@ -619,7 +645,7 @@ describe('moveIssue', () => {
{
type: types.MOVE_ISSUE_FAILURE,
payload: {
- originalIssue: mockIssueWithModel,
+ originalIssue: mockIssue,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
originalIndex: 0,
@@ -639,38 +665,59 @@ describe('setAssignees', () => {
const refPath = `${projectPath}#3`;
const iid = '1';
- beforeEach(() => {
- jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
- data: { issueSetAssignees: { issue: { assignees: { nodes: [{ ...node }] } } } },
+ describe('when succeeds', () => {
+ beforeEach(() => {
+ jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
+ data: { issueSetAssignees: { issue: { assignees: { nodes: [{ ...node }] } } } },
+ });
});
- });
- it('calls mutate with the correct values', async () => {
- await actions.setAssignees(
- { commit: () => {}, getters: { activeIssue: { iid, referencePath: refPath } } },
- [name],
- );
+ it('calls mutate with the correct values', async () => {
+ await actions.setAssignees(
+ { commit: () => {}, getters: { activeIssue: { iid, referencePath: refPath } } },
+ [name],
+ );
+
+ expect(gqlClient.mutate).toHaveBeenCalledWith({
+ mutation: updateAssignees,
+ variables: { iid, assigneeUsernames: [name], projectPath },
+ });
+ });
- expect(gqlClient.mutate).toHaveBeenCalledWith({
- mutation: updateAssignees,
- variables: { iid, assigneeUsernames: [name], projectPath },
+ it('calls the correct mutation with the correct values', done => {
+ testAction(
+ actions.setAssignees,
+ {},
+ { activeIssue: { iid, referencePath: refPath }, commit: () => {} },
+ [
+ { type: types.SET_ASSIGNEE_LOADING, payload: true },
+ {
+ type: 'UPDATE_ISSUE_BY_ID',
+ payload: { prop: 'assignees', issueId: undefined, value: [node] },
+ },
+ { type: types.SET_ASSIGNEE_LOADING, payload: false },
+ ],
+ [],
+ done,
+ );
});
});
- it('calls the correct mutation with the correct values', done => {
- testAction(
- actions.setAssignees,
- {},
- { activeIssue: { iid, referencePath: refPath }, commit: () => {} },
- [
- {
- type: 'UPDATE_ISSUE_BY_ID',
- payload: { prop: 'assignees', issueId: undefined, value: [node] },
- },
- ],
- [],
- done,
- );
+ describe('when fails', () => {
+ beforeEach(() => {
+ jest.spyOn(gqlClient, 'mutate').mockRejectedValue();
+ });
+
+ it('calls createFlash', async () => {
+ await actions.setAssignees({
+ commit: () => {},
+ getters: { activeIssue: { iid, referencePath: refPath } },
+ });
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'An error occurred while updating assignees.',
+ });
+ });
});
});
@@ -885,6 +932,60 @@ describe('setActiveIssueSubscribed', () => {
});
});
+describe('setActiveIssueMilestone', () => {
+ const state = { issues: { [mockIssue.id]: mockIssue } };
+ const getters = { activeIssue: mockIssue };
+ const testMilestone = {
+ ...mockMilestone,
+ id: 'gid://gitlab/Milestone/1',
+ };
+ const input = {
+ milestoneId: testMilestone.id,
+ projectPath: 'h/b',
+ };
+
+ it('should commit milestone after setting the issue', done => {
+ jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
+ data: {
+ updateIssue: {
+ issue: {
+ milestone: testMilestone,
+ },
+ errors: [],
+ },
+ },
+ });
+
+ const payload = {
+ issueId: getters.activeIssue.id,
+ prop: 'milestone',
+ value: testMilestone,
+ };
+
+ testAction(
+ actions.setActiveIssueMilestone,
+ input,
+ { ...state, ...getters },
+ [
+ {
+ type: types.UPDATE_ISSUE_BY_ID,
+ payload,
+ },
+ ],
+ [],
+ done,
+ );
+ });
+
+ it('throws error if fails', async () => {
+ jest
+ .spyOn(gqlClient, 'mutate')
+ .mockResolvedValue({ data: { updateIssue: { errors: ['failed mutation'] } } });
+
+ await expect(actions.setActiveIssueMilestone({ getters }, input)).rejects.toThrow(Error);
+ });
+});
+
describe('fetchBacklog', () => {
expectNotImplemented(actions.fetchBacklog);
});
diff --git a/spec/frontend/boards/stores/getters_spec.js b/spec/frontend/boards/stores/getters_spec.js
index 64025726dd1..6ceb8867d1f 100644
--- a/spec/frontend/boards/stores/getters_spec.js
+++ b/spec/frontend/boards/stores/getters_spec.js
@@ -6,28 +6,10 @@ import {
mockIssues,
mockIssuesByListId,
issues,
- mockListsWithModel,
+ mockLists,
} from '../mock_data';
describe('Boards - Getters', () => {
- describe('labelToggleState', () => {
- it('should return "on" when isShowingLabels is true', () => {
- const state = {
- isShowingLabels: true,
- };
-
- expect(getters.labelToggleState(state)).toBe('on');
- });
-
- it('should return "off" when isShowingLabels is false', () => {
- const state = {
- isShowingLabels: false,
- };
-
- expect(getters.labelToggleState(state)).toBe('off');
- });
- });
-
describe('isSidebarOpen', () => {
it('returns true when activeId is not equal to 0', () => {
const state = {
@@ -51,52 +33,8 @@ describe('Boards - Getters', () => {
window.gon = { features: {} };
});
- describe('when boardsWithSwimlanes is true', () => {
- beforeEach(() => {
- window.gon = { features: { boardsWithSwimlanes: true } };
- });
-
- describe('when isShowingEpicsSwimlanes is true', () => {
- it('returns true', () => {
- const state = {
- isShowingEpicsSwimlanes: true,
- };
-
- expect(getters.isSwimlanesOn(state)).toBe(true);
- });
- });
-
- describe('when isShowingEpicsSwimlanes is false', () => {
- it('returns false', () => {
- const state = {
- isShowingEpicsSwimlanes: false,
- };
-
- expect(getters.isSwimlanesOn(state)).toBe(false);
- });
- });
- });
-
- describe('when boardsWithSwimlanes is false', () => {
- describe('when isShowingEpicsSwimlanes is true', () => {
- it('returns false', () => {
- const state = {
- isShowingEpicsSwimlanes: true,
- };
-
- expect(getters.isSwimlanesOn(state)).toBe(false);
- });
- });
-
- describe('when isShowingEpicsSwimlanes is false', () => {
- it('returns false', () => {
- const state = {
- isShowingEpicsSwimlanes: false,
- };
-
- expect(getters.isSwimlanesOn(state)).toBe(false);
- });
- });
+ it('returns false', () => {
+ expect(getters.isSwimlanesOn()).toBe(false);
});
});
@@ -156,22 +94,22 @@ describe('Boards - Getters', () => {
const boardsState = {
boardLists: {
- 'gid://gitlab/List/1': mockListsWithModel[0],
- 'gid://gitlab/List/2': mockListsWithModel[1],
+ 'gid://gitlab/List/1': mockLists[0],
+ 'gid://gitlab/List/2': mockLists[1],
},
};
describe('getListByLabelId', () => {
it('returns list for a given label id', () => {
expect(getters.getListByLabelId(boardsState)('gid://gitlab/GroupLabel/121')).toEqual(
- mockListsWithModel[1],
+ mockLists[1],
);
});
});
describe('getListByTitle', () => {
it('returns list for a given list title', () => {
- expect(getters.getListByTitle(boardsState)('To Do')).toEqual(mockListsWithModel[1]);
+ expect(getters.getListByTitle(boardsState)('To Do')).toEqual(mockLists[1]);
});
});
});
diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js
index e1e57a8fd43..d93119ede3d 100644
--- a/spec/frontend/boards/stores/mutations_spec.js
+++ b/spec/frontend/boards/stores/mutations_spec.js
@@ -1,15 +1,7 @@
import mutations from '~/boards/stores/mutations';
import * as types from '~/boards/stores/mutation_types';
import defaultState from '~/boards/stores/state';
-import {
- mockListsWithModel,
- mockLists,
- rawIssue,
- mockIssue,
- mockIssue2,
- mockIssueWithModel,
- mockIssue2WithModel,
-} from '../mock_data';
+import { mockLists, rawIssue, mockIssue, mockIssue2 } from '../mock_data';
const expectNotImplemented = action => {
it('is not implemented', () => {
@@ -21,8 +13,8 @@ describe('Board Store Mutations', () => {
let state;
const initialBoardListsState = {
- 'gid://gitlab/List/1': mockListsWithModel[0],
- 'gid://gitlab/List/2': mockListsWithModel[1],
+ 'gid://gitlab/List/1': mockLists[0],
+ 'gid://gitlab/List/2': mockLists[1],
};
beforeEach(() => {
@@ -41,19 +33,21 @@ describe('Board Store Mutations', () => {
};
const boardType = 'group';
const disabled = false;
- const showPromotion = false;
+ const boardConfig = {
+ milestoneTitle: 'Milestone 1',
+ };
mutations[types.SET_INITIAL_BOARD_DATA](state, {
...endpoints,
boardType,
disabled,
- showPromotion,
+ boardConfig,
});
expect(state.endpoints).toEqual(endpoints);
expect(state.boardType).toEqual(boardType);
expect(state.disabled).toEqual(disabled);
- expect(state.showPromotion).toEqual(showPromotion);
+ expect(state.boardConfig).toEqual(boardConfig);
});
});
@@ -135,10 +129,10 @@ describe('Board Store Mutations', () => {
describe('RECEIVE_ADD_LIST_SUCCESS', () => {
it('adds list to boardLists state', () => {
- mutations.RECEIVE_ADD_LIST_SUCCESS(state, mockListsWithModel[0]);
+ mutations.RECEIVE_ADD_LIST_SUCCESS(state, mockLists[0]);
expect(state.boardLists).toEqual({
- [mockListsWithModel[0].id]: mockListsWithModel[0],
+ [mockLists[0].id]: mockLists[0],
});
});
});
@@ -155,13 +149,13 @@ describe('Board Store Mutations', () => {
};
mutations.MOVE_LIST(state, {
- movedList: mockListsWithModel[0],
- listAtNewIndex: mockListsWithModel[1],
+ movedList: mockLists[0],
+ listAtNewIndex: mockLists[1],
});
expect(state.boardLists).toEqual({
- 'gid://gitlab/List/2': mockListsWithModel[1],
- 'gid://gitlab/List/1': mockListsWithModel[0],
+ 'gid://gitlab/List/2': mockLists[1],
+ 'gid://gitlab/List/1': mockLists[0],
});
});
});
@@ -171,8 +165,8 @@ describe('Board Store Mutations', () => {
state = {
...state,
boardLists: {
- 'gid://gitlab/List/2': mockListsWithModel[1],
- 'gid://gitlab/List/1': mockListsWithModel[0],
+ 'gid://gitlab/List/2': mockLists[1],
+ 'gid://gitlab/List/1': mockLists[0],
},
error: undefined,
};
@@ -186,7 +180,7 @@ describe('Board Store Mutations', () => {
describe('REMOVE_LIST', () => {
it('removes list from boardLists', () => {
- const [list, secondList] = mockListsWithModel;
+ const [list, secondList] = mockLists;
const expected = {
[secondList.id]: secondList,
};
@@ -355,8 +349,8 @@ describe('Board Store Mutations', () => {
};
const issues = {
- '1': mockIssueWithModel,
- '2': mockIssue2WithModel,
+ '1': mockIssue,
+ '2': mockIssue2,
};
state = {
@@ -367,7 +361,7 @@ describe('Board Store Mutations', () => {
};
mutations.MOVE_ISSUE(state, {
- originalIssue: mockIssue2WithModel,
+ originalIssue: mockIssue2,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
});
@@ -396,7 +390,7 @@ describe('Board Store Mutations', () => {
issue: rawIssue,
});
- expect(state.issues).toEqual({ '436': { ...mockIssueWithModel, id: 436 } });
+ expect(state.issues).toEqual({ '436': { ...mockIssue, id: 436 } });
});
});
@@ -466,13 +460,13 @@ describe('Board Store Mutations', () => {
boardLists: initialBoardListsState,
};
- expect(state.boardLists['gid://gitlab/List/1'].issuesSize).toBe(1);
+ expect(state.boardLists['gid://gitlab/List/1'].issuesCount).toBe(1);
- mutations.ADD_ISSUE_TO_LIST(state, { list: mockListsWithModel[0], issue: mockIssue2 });
+ 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.boardLists['gid://gitlab/List/1'].issuesSize).toBe(2);
+ expect(state.boardLists['gid://gitlab/List/1'].issuesCount).toBe(2);
});
});
@@ -524,6 +518,14 @@ describe('Board Store Mutations', () => {
});
});
+ describe('SET_ASSIGNEE_LOADING', () => {
+ it('sets isSettingAssignees to the value passed', () => {
+ mutations.SET_ASSIGNEE_LOADING(state, true);
+
+ expect(state.isSettingAssignees).toBe(true);
+ });
+ });
+
describe('SET_CURRENT_PAGE', () => {
expectNotImplemented(mutations.SET_CURRENT_PAGE);
});