2067 lines
53 KiB
JavaScript
2067 lines
53 KiB
JavaScript
import * as Sentry from '@sentry/browser';
|
|
import { cloneDeep } from 'lodash';
|
|
import Vue from 'vue';
|
|
import Vuex from 'vuex';
|
|
import {
|
|
inactiveId,
|
|
ISSUABLE,
|
|
ListType,
|
|
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';
|
|
import {
|
|
formatListIssues,
|
|
formatBoardLists,
|
|
formatIssueInput,
|
|
formatIssue,
|
|
getMoveData,
|
|
updateListPosition,
|
|
} from 'ee_else_ce/boards/boards_util';
|
|
import { gqlClient } from '~/boards/graphql';
|
|
import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql';
|
|
import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql';
|
|
import actions 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 projectBoardMilestones from '~/boards/graphql/project_board_milestones.query.graphql';
|
|
import groupBoardMilestones from '~/boards/graphql/group_board_milestones.query.graphql';
|
|
import {
|
|
mockBoard,
|
|
mockBoardConfig,
|
|
mockLists,
|
|
mockListsById,
|
|
mockIssue,
|
|
mockIssue2,
|
|
rawIssue,
|
|
mockIssues,
|
|
labels,
|
|
mockActiveIssue,
|
|
mockGroupProjects,
|
|
mockMoveIssueParams,
|
|
mockMoveState,
|
|
mockMoveData,
|
|
mockList,
|
|
mockMilestones,
|
|
} from '../mock_data';
|
|
|
|
jest.mock('~/flash');
|
|
|
|
// We need this helper to make sure projectPath is including
|
|
// subgroups when the movIssue action is called.
|
|
const getProjectPath = (path) => path.split('#')[0];
|
|
|
|
Vue.use(Vuex);
|
|
|
|
beforeEach(() => {
|
|
window.gon = { features: {} };
|
|
});
|
|
|
|
describe('fetchBoard', () => {
|
|
const payload = {
|
|
fullPath: 'gitlab-org',
|
|
fullBoardId: 'gid://gitlab/Board/1',
|
|
boardType: 'project',
|
|
};
|
|
|
|
const queryResponse = {
|
|
data: {
|
|
workspace: {
|
|
board: mockBoard,
|
|
},
|
|
},
|
|
};
|
|
|
|
it('should commit mutation RECEIVE_BOARD_SUCCESS and dispatch setBoardConfig on success', async () => {
|
|
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
|
|
|
|
await testAction({
|
|
action: actions.fetchBoard,
|
|
payload,
|
|
expectedMutations: [
|
|
{
|
|
type: types.RECEIVE_BOARD_SUCCESS,
|
|
payload: mockBoard,
|
|
},
|
|
],
|
|
expectedActions: [{ type: 'setBoardConfig', payload: mockBoard }],
|
|
});
|
|
});
|
|
|
|
it('should commit mutation RECEIVE_BOARD_FAILURE on failure', async () => {
|
|
jest.spyOn(gqlClient, 'query').mockResolvedValue(Promise.reject());
|
|
|
|
await testAction({
|
|
action: actions.fetchBoard,
|
|
payload,
|
|
expectedMutations: [
|
|
{
|
|
type: types.RECEIVE_BOARD_FAILURE,
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('setInitialBoardData', () => {
|
|
it('sets data object', () => {
|
|
const mockData = {
|
|
foo: 'bar',
|
|
bar: 'baz',
|
|
};
|
|
|
|
return testAction({
|
|
action: actions.setInitialBoardData,
|
|
payload: mockData,
|
|
expectedMutations: [{ type: types.SET_INITIAL_BOARD_DATA, payload: mockData }],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('setBoardConfig', () => {
|
|
it('sets board config object from board object', () => {
|
|
return testAction({
|
|
action: actions.setBoardConfig,
|
|
payload: mockBoard,
|
|
expectedMutations: [{ type: types.SET_BOARD_CONFIG, payload: mockBoardConfig }],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('setFilters', () => {
|
|
it.each([
|
|
[
|
|
'with correct filters as payload',
|
|
{
|
|
filters: { labelName: 'label', foobar: 'not-a-filter', search: 'quick brown fox' },
|
|
filterVariables: { labelName: 'label', search: 'quick brown fox', not: {} },
|
|
},
|
|
],
|
|
[
|
|
"and use 'assigneeWildcardId' as filter variable for 'assigneeId' param",
|
|
{
|
|
filters: { assigneeId: 'None' },
|
|
filterVariables: { assigneeWildcardId: 'NONE', not: {} },
|
|
},
|
|
],
|
|
])('should commit mutation SET_FILTERS %s', (_, { filters, filterVariables }) => {
|
|
const state = {
|
|
filters: {},
|
|
issuableType: issuableTypes.issue,
|
|
};
|
|
|
|
testAction(
|
|
actions.setFilters,
|
|
filters,
|
|
state,
|
|
[{ type: types.SET_FILTERS, payload: filterVariables }],
|
|
[],
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('performSearch', () => {
|
|
it('should dispatch setFilters, fetchLists and resetIssues action', () => {
|
|
return testAction(
|
|
actions.performSearch,
|
|
{},
|
|
{},
|
|
[],
|
|
[{ type: 'setFilters', payload: {} }, { type: 'fetchLists' }, { type: 'resetIssues' }],
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('setActiveId', () => {
|
|
it('should commit mutation SET_ACTIVE_ID', () => {
|
|
const state = {
|
|
activeId: inactiveId,
|
|
};
|
|
|
|
return testAction(
|
|
actions.setActiveId,
|
|
{ id: 1, sidebarType: 'something' },
|
|
state,
|
|
[{ type: types.SET_ACTIVE_ID, payload: { id: 1, sidebarType: 'something' } }],
|
|
[],
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('fetchLists', () => {
|
|
let state = {
|
|
fullPath: 'gitlab-org',
|
|
fullBoardId: 'gid://gitlab/Board/1',
|
|
filterParams: {},
|
|
boardType: 'group',
|
|
issuableType: 'issue',
|
|
};
|
|
|
|
let queryResponse = {
|
|
data: {
|
|
group: {
|
|
board: {
|
|
hideBacklogList: true,
|
|
lists: {
|
|
nodes: [mockLists[1]],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const formattedLists = formatBoardLists(queryResponse.data.group.board.lists);
|
|
|
|
it('should commit mutations RECEIVE_BOARD_LISTS_SUCCESS on success', () => {
|
|
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
|
|
|
|
return testAction(
|
|
actions.fetchLists,
|
|
{},
|
|
state,
|
|
[
|
|
{
|
|
type: types.RECEIVE_BOARD_LISTS_SUCCESS,
|
|
payload: formattedLists,
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
});
|
|
|
|
it('should commit mutations RECEIVE_BOARD_LISTS_FAILURE on failure', () => {
|
|
jest.spyOn(gqlClient, 'query').mockResolvedValue(Promise.reject());
|
|
|
|
return testAction(
|
|
actions.fetchLists,
|
|
{},
|
|
state,
|
|
[
|
|
{
|
|
type: types.RECEIVE_BOARD_LISTS_FAILURE,
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
});
|
|
|
|
it('dispatch createList action when backlog list does not exist and is not hidden', () => {
|
|
queryResponse = {
|
|
data: {
|
|
group: {
|
|
board: {
|
|
hideBacklogList: false,
|
|
lists: {
|
|
nodes: [mockLists[1]],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
|
|
|
|
return testAction(
|
|
actions.fetchLists,
|
|
{},
|
|
state,
|
|
[
|
|
{
|
|
type: types.RECEIVE_BOARD_LISTS_SUCCESS,
|
|
payload: formattedLists,
|
|
},
|
|
],
|
|
[{ type: 'createList', payload: { backlog: true } }],
|
|
);
|
|
});
|
|
|
|
it.each`
|
|
issuableType | boardType | fullBoardId | isGroup | isProject
|
|
${issuableTypes.issue} | ${BoardType.group} | ${'gid://gitlab/Board/1'} | ${true} | ${false}
|
|
${issuableTypes.issue} | ${BoardType.project} | ${'gid://gitlab/Board/1'} | ${false} | ${true}
|
|
`(
|
|
'calls $issuableType query with correct variables',
|
|
async ({ issuableType, boardType, fullBoardId, isGroup, isProject }) => {
|
|
const commit = jest.fn();
|
|
const dispatch = jest.fn();
|
|
|
|
state = {
|
|
fullPath: 'gitlab-org',
|
|
fullBoardId,
|
|
filterParams: {},
|
|
boardType,
|
|
issuableType,
|
|
};
|
|
|
|
const variables = {
|
|
query: listsQuery[issuableType].query,
|
|
variables: {
|
|
fullPath: 'gitlab-org',
|
|
boardId: fullBoardId,
|
|
filters: {},
|
|
isGroup,
|
|
isProject,
|
|
},
|
|
};
|
|
|
|
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
|
|
|
|
await actions.fetchLists({ commit, state, dispatch });
|
|
|
|
expect(gqlClient.query).toHaveBeenCalledWith(variables);
|
|
},
|
|
);
|
|
});
|
|
|
|
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.each([
|
|
[
|
|
'project',
|
|
{
|
|
query: projectBoardMilestones,
|
|
variables: { fullPath: 'gitlab-org/gitlab' },
|
|
},
|
|
],
|
|
[
|
|
'group',
|
|
{
|
|
query: groupBoardMilestones,
|
|
variables: { fullPath: 'gitlab-org/gitlab' },
|
|
},
|
|
],
|
|
])(
|
|
'when boardType is %s it calls fetchMilestones with the correct query and variables',
|
|
(boardType, variables) => {
|
|
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
|
|
|
|
const store = createStore();
|
|
|
|
store.state.boardType = boardType;
|
|
|
|
actions.fetchMilestones(store);
|
|
|
|
expect(gqlClient.query).toHaveBeenCalledWith(variables);
|
|
},
|
|
);
|
|
|
|
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({
|
|
action: actions.createList,
|
|
payload: { backlog: true },
|
|
expectedActions: [{ type: 'createIssueList', payload: { backlog: true } }],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('createIssueList', () => {
|
|
let commit;
|
|
let dispatch;
|
|
let getters;
|
|
let state;
|
|
|
|
beforeEach(() => {
|
|
state = {
|
|
fullPath: 'gitlab-org',
|
|
fullBoardId: 'gid://gitlab/Board/1',
|
|
boardType: 'group',
|
|
disabled: false,
|
|
boardLists: [{ type: 'closed' }],
|
|
};
|
|
commit = jest.fn();
|
|
dispatch = jest.fn();
|
|
getters = {
|
|
getListByLabelId: jest.fn(),
|
|
};
|
|
});
|
|
|
|
it('should dispatch addList action when creating backlog list', async () => {
|
|
const backlogList = {
|
|
id: 'gid://gitlab/List/1',
|
|
listType: 'backlog',
|
|
title: 'Open',
|
|
position: 0,
|
|
};
|
|
|
|
jest.spyOn(gqlClient, 'mutate').mockReturnValue(
|
|
Promise.resolve({
|
|
data: {
|
|
boardListCreate: {
|
|
list: backlogList,
|
|
errors: [],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
await actions.createIssueList({ getters, state, commit, dispatch }, { backlog: true });
|
|
|
|
expect(dispatch).toHaveBeenCalledWith('addList', backlogList);
|
|
});
|
|
|
|
it('dispatches highlightList after addList has succeeded', async () => {
|
|
const list = {
|
|
id: 'gid://gitlab/List/1',
|
|
listType: 'label',
|
|
title: 'Open',
|
|
labelId: '4',
|
|
};
|
|
|
|
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
|
|
data: {
|
|
boardListCreate: {
|
|
list,
|
|
errors: [],
|
|
},
|
|
},
|
|
});
|
|
|
|
await actions.createIssueList({ getters, state, commit, dispatch }, { labelId: '4' });
|
|
|
|
expect(dispatch).toHaveBeenCalledWith('addList', list);
|
|
expect(dispatch).toHaveBeenCalledWith('highlightList', list.id);
|
|
});
|
|
|
|
it('should commit CREATE_LIST_FAILURE mutation when API returns an error', async () => {
|
|
jest.spyOn(gqlClient, 'mutate').mockReturnValue(
|
|
Promise.resolve({
|
|
data: {
|
|
boardListCreate: {
|
|
list: {},
|
|
errors: ['foo'],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
await actions.createIssueList({ getters, state, commit, dispatch }, { backlog: true });
|
|
|
|
expect(commit).toHaveBeenCalledWith(types.CREATE_LIST_FAILURE, 'foo');
|
|
});
|
|
|
|
it('highlights list and does not re-query if it already exists', async () => {
|
|
const existingList = {
|
|
id: 'gid://gitlab/List/1',
|
|
listType: 'label',
|
|
title: 'Some label',
|
|
position: 1,
|
|
};
|
|
|
|
getters = {
|
|
getListByLabelId: jest.fn().mockReturnValue(existingList),
|
|
};
|
|
|
|
await actions.createIssueList({ getters, state, commit, dispatch }, { backlog: true });
|
|
|
|
expect(dispatch).toHaveBeenCalledWith('highlightList', existingList.id);
|
|
expect(dispatch).toHaveBeenCalledTimes(1);
|
|
expect(commit).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('addList', () => {
|
|
const getters = {
|
|
getListByTitle: jest.fn().mockReturnValue(mockList),
|
|
};
|
|
|
|
it('should commit RECEIVE_ADD_LIST_SUCCESS mutation and dispatch fetchItemsForList action', () => {
|
|
testAction({
|
|
action: actions.addList,
|
|
payload: mockLists[1],
|
|
state: { ...getters },
|
|
expectedMutations: [
|
|
{ type: types.RECEIVE_ADD_LIST_SUCCESS, payload: updateListPosition(mockLists[1]) },
|
|
],
|
|
expectedActions: [{ type: 'fetchItemsForList', payload: { listId: mockList.id } }],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('fetchLabels', () => {
|
|
it('should commit mutation RECEIVE_LABELS_SUCCESS on success', async () => {
|
|
const queryResponse = {
|
|
data: {
|
|
group: {
|
|
labels: {
|
|
nodes: labels,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
|
|
|
|
const commit = jest.fn();
|
|
const state = { boardType: 'group' };
|
|
|
|
await actions.fetchLabels({ state, commit });
|
|
|
|
expect(commit).toHaveBeenCalledWith(types.RECEIVE_LABELS_SUCCESS, labels);
|
|
});
|
|
});
|
|
|
|
describe('moveList', () => {
|
|
const backlogListId = 'gid://1';
|
|
const closedListId = 'gid://5';
|
|
|
|
const boardLists1 = {
|
|
'gid://3': { listType: '', position: 0 },
|
|
'gid://4': { listType: '', position: 1 },
|
|
'gid://5': { listType: '', position: 2 },
|
|
};
|
|
|
|
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: [],
|
|
},
|
|
},
|
|
state: {},
|
|
expectedMutations: [],
|
|
expectedActions: [],
|
|
});
|
|
});
|
|
|
|
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 } })),
|
|
},
|
|
};
|
|
};
|
|
|
|
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),
|
|
},
|
|
},
|
|
],
|
|
});
|
|
});
|
|
},
|
|
);
|
|
|
|
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: [],
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('updateList', () => {
|
|
const listId = 'gid://gitlab/List/1';
|
|
const createState = (boardItemsByListId = {}) => ({
|
|
fullPath: 'gitlab-org',
|
|
fullBoardId: 'gid://gitlab/Board/1',
|
|
boardType: 'group',
|
|
disabled: false,
|
|
boardLists: [{ type: 'closed' }],
|
|
issuableType: issuableTypes.issue,
|
|
boardItemsByListId,
|
|
});
|
|
|
|
describe('when state doesnt have list items', () => {
|
|
it('calls fetchItemsByList', async () => {
|
|
const dispatch = jest.fn();
|
|
|
|
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
|
|
data: {
|
|
updateBoardList: {
|
|
errors: [],
|
|
list: {
|
|
id: listId,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
await actions.updateList({ commit: () => {}, state: createState(), dispatch }, { listId });
|
|
|
|
expect(dispatch.mock.calls).toEqual([['fetchItemsForList', { listId }]]);
|
|
});
|
|
});
|
|
|
|
describe('when state has list items', () => {
|
|
it('doesnt call fetchItemsByList', async () => {
|
|
const commit = jest.fn();
|
|
const dispatch = jest.fn();
|
|
|
|
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
|
|
data: {
|
|
updateBoardList: {
|
|
errors: [],
|
|
list: {
|
|
id: listId,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
await actions.updateList(
|
|
{ commit, state: createState({ [listId]: [] }), dispatch },
|
|
{ listId },
|
|
);
|
|
|
|
expect(dispatch.mock.calls).toEqual([]);
|
|
});
|
|
});
|
|
|
|
it('should dispatch handleUpdateListFailure when API returns an error', () => {
|
|
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
|
|
data: {
|
|
updateBoardList: {
|
|
list: {},
|
|
errors: [{ foo: 'bar' }],
|
|
},
|
|
},
|
|
});
|
|
|
|
return testAction(
|
|
actions.updateList,
|
|
{ listId: 'gid://gitlab/List/1', position: 1 },
|
|
createState(),
|
|
[],
|
|
[{ 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 };
|
|
await testAction({
|
|
action: actions.toggleListCollapsed,
|
|
payload,
|
|
expectedMutations: [
|
|
{
|
|
type: types.TOGGLE_LIST_COLLAPSED,
|
|
payload,
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('removeList', () => {
|
|
let state;
|
|
let getters;
|
|
const list = mockLists[1];
|
|
const listId = list.id;
|
|
const mutationVariables = {
|
|
mutation: destroyBoardListMutation,
|
|
variables: {
|
|
listId,
|
|
},
|
|
};
|
|
|
|
beforeEach(() => {
|
|
state = {
|
|
boardLists: mockListsById,
|
|
issuableType: issuableTypes.issue,
|
|
};
|
|
getters = {
|
|
getListByTitle: jest.fn().mockReturnValue(mockList),
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
state = null;
|
|
});
|
|
|
|
it('optimistically deletes the list', () => {
|
|
const commit = jest.fn();
|
|
|
|
actions.removeList({ commit, state, getters, dispatch: () => {} }, listId);
|
|
|
|
expect(commit.mock.calls).toEqual([[types.REMOVE_LIST, listId]]);
|
|
});
|
|
|
|
it('keeps the updated list if remove succeeds', async () => {
|
|
const commit = jest.fn();
|
|
const dispatch = jest.fn();
|
|
|
|
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
|
|
data: {
|
|
destroyBoardList: {
|
|
errors: [],
|
|
},
|
|
},
|
|
});
|
|
|
|
await actions.removeList({ commit, state, getters, dispatch }, listId);
|
|
|
|
expect(gqlClient.mutate).toHaveBeenCalledWith(mutationVariables);
|
|
expect(commit.mock.calls).toEqual([[types.REMOVE_LIST, listId]]);
|
|
expect(dispatch.mock.calls).toEqual([['fetchItemsForList', { listId: mockList.id }]]);
|
|
});
|
|
|
|
it('restores the list if update fails', async () => {
|
|
const commit = jest.fn();
|
|
jest.spyOn(gqlClient, 'mutate').mockResolvedValue(Promise.reject());
|
|
|
|
await actions.removeList({ commit, state, getters, dispatch: () => {} }, listId);
|
|
|
|
expect(gqlClient.mutate).toHaveBeenCalledWith(mutationVariables);
|
|
expect(commit.mock.calls).toEqual([
|
|
[types.REMOVE_LIST, listId],
|
|
[types.REMOVE_LIST_FAILURE, mockListsById],
|
|
]);
|
|
});
|
|
|
|
it('restores the list if update response has errors', async () => {
|
|
const commit = jest.fn();
|
|
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
|
|
data: {
|
|
destroyBoardList: {
|
|
errors: ['update failed, ID invalid'],
|
|
},
|
|
},
|
|
});
|
|
|
|
await actions.removeList({ commit, state, getters, dispatch: () => {} }, listId);
|
|
|
|
expect(gqlClient.mutate).toHaveBeenCalledWith(mutationVariables);
|
|
expect(commit.mock.calls).toEqual([
|
|
[types.REMOVE_LIST, listId],
|
|
[types.REMOVE_LIST_FAILURE, mockListsById],
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('fetchItemsForList', () => {
|
|
const listId = mockLists[0].id;
|
|
|
|
const state = {
|
|
fullPath: 'gitlab-org',
|
|
fullBoardId: 'gid://gitlab/Board/1',
|
|
filterParams: {},
|
|
boardType: 'group',
|
|
};
|
|
|
|
const mockIssuesNodes = mockIssues.map((issue) => ({ node: issue }));
|
|
|
|
const pageInfo = {
|
|
endCursor: '',
|
|
hasNextPage: false,
|
|
};
|
|
|
|
const queryResponse = {
|
|
data: {
|
|
group: {
|
|
board: {
|
|
lists: {
|
|
nodes: [
|
|
{
|
|
id: listId,
|
|
issues: {
|
|
edges: mockIssuesNodes,
|
|
pageInfo,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const formattedIssues = formatListIssues(queryResponse.data.group.board.lists);
|
|
|
|
const listPageInfo = {
|
|
[listId]: pageInfo,
|
|
};
|
|
|
|
describe('when list id is undefined', () => {
|
|
it('does not call the query', async () => {
|
|
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
|
|
|
|
await actions.fetchItemsForList(
|
|
{ state, getters: () => {}, commit: () => {} },
|
|
{ listId: undefined },
|
|
);
|
|
|
|
expect(gqlClient.query).toHaveBeenCalledTimes(0);
|
|
});
|
|
});
|
|
|
|
it('should commit mutations REQUEST_ITEMS_FOR_LIST and RECEIVE_ITEMS_FOR_LIST_SUCCESS on success', () => {
|
|
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
|
|
|
|
return testAction(
|
|
actions.fetchItemsForList,
|
|
{ listId },
|
|
state,
|
|
[
|
|
{
|
|
type: types.RESET_ITEMS_FOR_LIST,
|
|
payload: listId,
|
|
},
|
|
{
|
|
type: types.REQUEST_ITEMS_FOR_LIST,
|
|
payload: { listId, fetchNext: false },
|
|
},
|
|
{
|
|
type: types.RECEIVE_ITEMS_FOR_LIST_SUCCESS,
|
|
payload: { listItems: formattedIssues, listPageInfo, listId },
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
});
|
|
|
|
it('should commit mutations REQUEST_ITEMS_FOR_LIST and RECEIVE_ITEMS_FOR_LIST_FAILURE on failure', () => {
|
|
jest.spyOn(gqlClient, 'query').mockResolvedValue(Promise.reject());
|
|
|
|
return testAction(
|
|
actions.fetchItemsForList,
|
|
{ listId },
|
|
state,
|
|
[
|
|
{
|
|
type: types.RESET_ITEMS_FOR_LIST,
|
|
payload: listId,
|
|
},
|
|
{
|
|
type: types.REQUEST_ITEMS_FOR_LIST,
|
|
payload: { listId, fetchNext: false },
|
|
},
|
|
{ type: types.RECEIVE_ITEMS_FOR_LIST_FAILURE, payload: listId },
|
|
],
|
|
[],
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('resetIssues', () => {
|
|
it('commits RESET_ISSUES mutation', () => {
|
|
return testAction(actions.resetIssues, {}, {}, [{ type: types.RESET_ISSUES }], []);
|
|
});
|
|
});
|
|
|
|
describe('moveItem', () => {
|
|
it('should dispatch moveIssue action with payload', () => {
|
|
const payload = { mock: 'payload' };
|
|
|
|
testAction({
|
|
action: actions.moveItem,
|
|
payload,
|
|
expectedActions: [{ type: 'moveIssue', payload }],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('moveIssue', () => {
|
|
it('should dispatch a correct set of actions', () => {
|
|
testAction({
|
|
action: actions.moveIssue,
|
|
payload: mockMoveIssueParams,
|
|
state: mockMoveState,
|
|
expectedActions: [
|
|
{ type: 'moveIssueCard', payload: mockMoveData },
|
|
{ type: 'updateMovedIssue', payload: mockMoveData },
|
|
{ type: 'updateIssueOrder', payload: { moveData: mockMoveData } },
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('moveIssueCard and undoMoveIssueCard', () => {
|
|
describe('card should move without cloning', () => {
|
|
let state;
|
|
let params;
|
|
let moveMutations;
|
|
let undoMutations;
|
|
|
|
describe('when re-ordering card', () => {
|
|
beforeEach(
|
|
({
|
|
itemId = 123,
|
|
fromListId = 'gid://gitlab/List/1',
|
|
toListId = 'gid://gitlab/List/1',
|
|
originalIssue = { foo: 'bar' },
|
|
originalIndex = 0,
|
|
moveBeforeId = undefined,
|
|
moveAfterId = undefined,
|
|
} = {}) => {
|
|
state = {
|
|
boardLists: {
|
|
[toListId]: { listType: ListType.backlog },
|
|
[fromListId]: { listType: ListType.backlog },
|
|
},
|
|
boardItems: { [itemId]: originalIssue },
|
|
boardItemsByListId: { [fromListId]: [123] },
|
|
};
|
|
params = { itemId, fromListId, toListId, moveBeforeId, moveAfterId };
|
|
moveMutations = [
|
|
{ type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } },
|
|
{
|
|
type: types.ADD_BOARD_ITEM_TO_LIST,
|
|
payload: { itemId, listId: toListId, moveBeforeId, moveAfterId },
|
|
},
|
|
];
|
|
undoMutations = [
|
|
{ type: types.UPDATE_BOARD_ITEM, payload: originalIssue },
|
|
{ type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } },
|
|
{
|
|
type: types.ADD_BOARD_ITEM_TO_LIST,
|
|
payload: { itemId, listId: fromListId, atIndex: originalIndex },
|
|
},
|
|
];
|
|
},
|
|
);
|
|
|
|
it('moveIssueCard commits a correct set of actions', () => {
|
|
testAction({
|
|
action: actions.moveIssueCard,
|
|
state,
|
|
payload: getMoveData(state, params),
|
|
expectedMutations: moveMutations,
|
|
});
|
|
});
|
|
|
|
it('undoMoveIssueCard commits a correct set of actions', () => {
|
|
testAction({
|
|
action: actions.undoMoveIssueCard,
|
|
state,
|
|
payload: getMoveData(state, params),
|
|
expectedMutations: undoMutations,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe.each([
|
|
[
|
|
'issue moves out of backlog',
|
|
{
|
|
fromListType: ListType.backlog,
|
|
toListType: ListType.label,
|
|
},
|
|
],
|
|
[
|
|
'issue card moves to closed',
|
|
{
|
|
fromListType: ListType.label,
|
|
toListType: ListType.closed,
|
|
},
|
|
],
|
|
[
|
|
'issue card moves to non-closed, non-backlog list of the same type',
|
|
{
|
|
fromListType: ListType.label,
|
|
toListType: ListType.label,
|
|
},
|
|
],
|
|
])('when %s', (_, { toListType, fromListType }) => {
|
|
beforeEach(
|
|
({
|
|
itemId = 123,
|
|
fromListId = 'gid://gitlab/List/1',
|
|
toListId = 'gid://gitlab/List/2',
|
|
originalIssue = { foo: 'bar' },
|
|
originalIndex = 0,
|
|
moveBeforeId = undefined,
|
|
moveAfterId = undefined,
|
|
} = {}) => {
|
|
state = {
|
|
boardLists: {
|
|
[fromListId]: { listType: fromListType },
|
|
[toListId]: { listType: toListType },
|
|
},
|
|
boardItems: { [itemId]: originalIssue },
|
|
boardItemsByListId: { [fromListId]: [123], [toListId]: [] },
|
|
};
|
|
params = { itemId, fromListId, toListId, moveBeforeId, moveAfterId };
|
|
moveMutations = [
|
|
{ type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } },
|
|
{
|
|
type: types.ADD_BOARD_ITEM_TO_LIST,
|
|
payload: { itemId, listId: toListId, moveBeforeId, moveAfterId },
|
|
},
|
|
];
|
|
undoMutations = [
|
|
{ type: types.UPDATE_BOARD_ITEM, payload: originalIssue },
|
|
{ type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: toListId } },
|
|
{
|
|
type: types.ADD_BOARD_ITEM_TO_LIST,
|
|
payload: { itemId, listId: fromListId, atIndex: originalIndex },
|
|
},
|
|
];
|
|
},
|
|
);
|
|
|
|
it('moveIssueCard commits a correct set of actions', () => {
|
|
testAction({
|
|
action: actions.moveIssueCard,
|
|
state,
|
|
payload: getMoveData(state, params),
|
|
expectedMutations: moveMutations,
|
|
});
|
|
});
|
|
|
|
it('undoMoveIssueCard commits a correct set of actions', () => {
|
|
testAction({
|
|
action: actions.undoMoveIssueCard,
|
|
state,
|
|
payload: getMoveData(state, params),
|
|
expectedMutations: undoMutations,
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('card should clone on move', () => {
|
|
let state;
|
|
let params;
|
|
let moveMutations;
|
|
let undoMutations;
|
|
|
|
describe.each([
|
|
[
|
|
'issue card moves to non-closed, non-backlog list of a different type',
|
|
{
|
|
fromListType: ListType.label,
|
|
toListType: ListType.assignee,
|
|
},
|
|
],
|
|
])('when %s', (_, { toListType, fromListType }) => {
|
|
beforeEach(
|
|
({
|
|
itemId = 123,
|
|
fromListId = 'gid://gitlab/List/1',
|
|
toListId = 'gid://gitlab/List/2',
|
|
originalIssue = { foo: 'bar' },
|
|
originalIndex = 0,
|
|
moveBeforeId = undefined,
|
|
moveAfterId = undefined,
|
|
} = {}) => {
|
|
state = {
|
|
boardLists: {
|
|
[fromListId]: { listType: fromListType },
|
|
[toListId]: { listType: toListType },
|
|
},
|
|
boardItems: { [itemId]: originalIssue },
|
|
boardItemsByListId: { [fromListId]: [123], [toListId]: [] },
|
|
};
|
|
params = { itemId, fromListId, toListId, moveBeforeId, moveAfterId };
|
|
moveMutations = [
|
|
{ type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } },
|
|
{
|
|
type: types.ADD_BOARD_ITEM_TO_LIST,
|
|
payload: { itemId, listId: toListId, moveBeforeId, moveAfterId },
|
|
},
|
|
{
|
|
type: types.ADD_BOARD_ITEM_TO_LIST,
|
|
payload: { itemId, listId: fromListId, atIndex: originalIndex },
|
|
},
|
|
];
|
|
undoMutations = [
|
|
{ type: types.UPDATE_BOARD_ITEM, payload: originalIssue },
|
|
{ type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } },
|
|
{ type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: toListId } },
|
|
{
|
|
type: types.ADD_BOARD_ITEM_TO_LIST,
|
|
payload: { itemId, listId: fromListId, atIndex: originalIndex },
|
|
},
|
|
];
|
|
},
|
|
);
|
|
|
|
it('moveIssueCard commits a correct set of actions', () => {
|
|
testAction({
|
|
action: actions.moveIssueCard,
|
|
state,
|
|
payload: getMoveData(state, params),
|
|
expectedMutations: moveMutations,
|
|
});
|
|
});
|
|
|
|
it('undoMoveIssueCard commits a correct set of actions', () => {
|
|
testAction({
|
|
action: actions.undoMoveIssueCard,
|
|
state,
|
|
payload: getMoveData(state, params),
|
|
expectedMutations: undoMutations,
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('updateMovedIssueCard', () => {
|
|
const label1 = {
|
|
id: 'label1',
|
|
};
|
|
|
|
it.each([
|
|
[
|
|
'issue without a label is moved to a label list',
|
|
{
|
|
state: {
|
|
boardLists: {
|
|
from: {},
|
|
to: {
|
|
listType: ListType.label,
|
|
label: label1,
|
|
},
|
|
},
|
|
boardItems: {
|
|
1: {
|
|
labels: [],
|
|
},
|
|
},
|
|
},
|
|
moveData: {
|
|
itemId: 1,
|
|
fromListId: 'from',
|
|
toListId: 'to',
|
|
},
|
|
updatedIssue: { labels: [label1] },
|
|
},
|
|
],
|
|
])(
|
|
'should commit UPDATE_BOARD_ITEM with a correctly updated issue data when %s',
|
|
(_, { state, moveData, updatedIssue }) => {
|
|
testAction({
|
|
action: actions.updateMovedIssue,
|
|
payload: moveData,
|
|
state,
|
|
expectedMutations: [{ type: types.UPDATE_BOARD_ITEM, payload: updatedIssue }],
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
describe('updateIssueOrder', () => {
|
|
const issues = {
|
|
[mockIssue.id]: mockIssue,
|
|
[mockIssue2.id]: mockIssue2,
|
|
};
|
|
|
|
const state = {
|
|
boardItems: issues,
|
|
fullBoardId: 'gid://gitlab/Board/1',
|
|
};
|
|
|
|
const moveData = {
|
|
itemId: mockIssue.id,
|
|
fromListId: 'gid://gitlab/List/1',
|
|
toListId: 'gid://gitlab/List/2',
|
|
};
|
|
|
|
it('calls mutate with the correct variables', () => {
|
|
const mutationVariables = {
|
|
mutation: issueMoveListMutation,
|
|
variables: {
|
|
projectPath: getProjectPath(mockIssue.referencePath),
|
|
boardId: state.fullBoardId,
|
|
iid: mockIssue.iid,
|
|
fromListId: 1,
|
|
toListId: 2,
|
|
moveBeforeId: undefined,
|
|
moveAfterId: undefined,
|
|
},
|
|
update: expect.anything(),
|
|
};
|
|
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
|
|
data: {
|
|
issueMoveList: {
|
|
issue: rawIssue,
|
|
errors: [],
|
|
},
|
|
},
|
|
});
|
|
|
|
actions.updateIssueOrder({ state, commit: () => {}, dispatch: () => {} }, { moveData });
|
|
|
|
expect(gqlClient.mutate).toHaveBeenCalledWith(mutationVariables);
|
|
});
|
|
|
|
it('should commit MUTATE_ISSUE_SUCCESS mutation when successful', () => {
|
|
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
|
|
data: {
|
|
issueMoveList: {
|
|
issue: rawIssue,
|
|
errors: [],
|
|
},
|
|
},
|
|
});
|
|
|
|
testAction(
|
|
actions.updateIssueOrder,
|
|
{ moveData },
|
|
state,
|
|
[
|
|
{
|
|
type: types.MUTATE_ISSUE_SUCCESS,
|
|
payload: { issue: rawIssue },
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
});
|
|
|
|
it('should commit SET_ERROR and dispatch undoMoveIssueCard', () => {
|
|
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
|
|
data: {
|
|
issueMoveList: {
|
|
issue: {},
|
|
errors: [{ foo: 'bar' }],
|
|
},
|
|
},
|
|
});
|
|
|
|
testAction(
|
|
actions.updateIssueOrder,
|
|
{ moveData },
|
|
state,
|
|
[
|
|
{
|
|
type: types.SET_ERROR,
|
|
payload: 'An error occurred while moving the issue. Please try again.',
|
|
},
|
|
],
|
|
[{ type: 'undoMoveIssueCard', payload: moveData }],
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('setAssignees', () => {
|
|
const node = { username: 'name' };
|
|
|
|
describe('when succeeds', () => {
|
|
it('calls the correct mutation with the correct values', () => {
|
|
return testAction(
|
|
actions.setAssignees,
|
|
{ assignees: [node], iid: '1' },
|
|
{ commit: () => {} },
|
|
[
|
|
{
|
|
type: 'UPDATE_BOARD_ITEM_BY_ID',
|
|
payload: { prop: 'assignees', itemId: undefined, value: [node] },
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('addListItem', () => {
|
|
it('should commit ADD_BOARD_ITEM_TO_LIST and UPDATE_BOARD_ITEM mutations', () => {
|
|
const payload = {
|
|
list: mockLists[0],
|
|
item: mockIssue,
|
|
position: 0,
|
|
inProgress: true,
|
|
};
|
|
|
|
testAction(
|
|
actions.addListItem,
|
|
payload,
|
|
{},
|
|
[
|
|
{
|
|
type: types.ADD_BOARD_ITEM_TO_LIST,
|
|
payload: {
|
|
listId: mockLists[0].id,
|
|
itemId: mockIssue.id,
|
|
atIndex: 0,
|
|
inProgress: true,
|
|
},
|
|
},
|
|
{ type: types.UPDATE_BOARD_ITEM, payload: mockIssue },
|
|
],
|
|
[],
|
|
);
|
|
});
|
|
|
|
it('should commit ADD_BOARD_ITEM_TO_LIST and UPDATE_BOARD_ITEM mutations, dispatch setActiveId action when inProgress is false', () => {
|
|
const payload = {
|
|
list: mockLists[0],
|
|
item: mockIssue,
|
|
position: 0,
|
|
};
|
|
|
|
testAction(
|
|
actions.addListItem,
|
|
payload,
|
|
{},
|
|
[
|
|
{
|
|
type: types.ADD_BOARD_ITEM_TO_LIST,
|
|
payload: {
|
|
listId: mockLists[0].id,
|
|
itemId: mockIssue.id,
|
|
atIndex: 0,
|
|
inProgress: false,
|
|
},
|
|
},
|
|
{ type: types.UPDATE_BOARD_ITEM, payload: mockIssue },
|
|
],
|
|
[{ type: 'setActiveId', payload: { id: mockIssue.id, sidebarType: ISSUABLE } }],
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('removeListItem', () => {
|
|
it('should commit REMOVE_BOARD_ITEM_FROM_LIST and REMOVE_BOARD_ITEM mutations', () => {
|
|
const payload = {
|
|
listId: mockLists[0].id,
|
|
itemId: mockIssue.id,
|
|
};
|
|
|
|
testAction(actions.removeListItem, payload, {}, [
|
|
{ type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload },
|
|
{ type: types.REMOVE_BOARD_ITEM, payload: mockIssue.id },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('addListNewIssue', () => {
|
|
const state = {
|
|
boardType: 'group',
|
|
fullPath: 'gitlab-org/gitlab',
|
|
boardConfig: {
|
|
labelIds: [],
|
|
assigneeId: null,
|
|
milestoneId: -1,
|
|
},
|
|
};
|
|
|
|
const stateWithBoardConfig = {
|
|
boardConfig: {
|
|
labels: [
|
|
{
|
|
id: 5,
|
|
title: 'Test',
|
|
color: '#ff0000',
|
|
description: 'testing;',
|
|
textColor: 'white',
|
|
},
|
|
],
|
|
assigneeId: 2,
|
|
milestoneId: 3,
|
|
},
|
|
};
|
|
|
|
const fakeList = { id: 'gid://gitlab/List/123' };
|
|
|
|
it('should add board scope to the issue being created', async () => {
|
|
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
|
|
data: {
|
|
createIssue: {
|
|
issue: mockIssue,
|
|
errors: [],
|
|
},
|
|
},
|
|
});
|
|
|
|
await actions.addListNewIssue(
|
|
{ dispatch: jest.fn(), commit: jest.fn(), state: stateWithBoardConfig },
|
|
{ issueInput: mockIssue, list: fakeList },
|
|
);
|
|
|
|
expect(gqlClient.mutate).toHaveBeenCalledWith({
|
|
mutation: issueCreateMutation,
|
|
variables: {
|
|
input: formatIssueInput(mockIssue, stateWithBoardConfig.boardConfig),
|
|
},
|
|
update: expect.anything(),
|
|
});
|
|
});
|
|
|
|
it('should add board scope by merging attributes to the issue being created', async () => {
|
|
const issue = {
|
|
...mockIssue,
|
|
assigneeIds: ['gid://gitlab/User/1'],
|
|
labelIds: ['gid://gitlab/GroupLabel/4'],
|
|
};
|
|
|
|
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
|
|
data: {
|
|
createIssue: {
|
|
issue,
|
|
errors: [],
|
|
},
|
|
},
|
|
});
|
|
|
|
const payload = formatIssueInput(issue, stateWithBoardConfig.boardConfig);
|
|
|
|
await actions.addListNewIssue(
|
|
{ dispatch: jest.fn(), commit: jest.fn(), state: stateWithBoardConfig },
|
|
{ issueInput: issue, list: fakeList },
|
|
);
|
|
|
|
expect(gqlClient.mutate).toHaveBeenCalledWith({
|
|
mutation: issueCreateMutation,
|
|
variables: {
|
|
input: formatIssueInput(issue, stateWithBoardConfig.boardConfig),
|
|
},
|
|
update: expect.anything(),
|
|
});
|
|
expect(payload.labelIds).toEqual(['gid://gitlab/GroupLabel/4', 'gid://gitlab/GroupLabel/5']);
|
|
expect(payload.assigneeIds).toEqual(['gid://gitlab/User/1', 'gid://gitlab/User/2']);
|
|
});
|
|
|
|
describe('when issue creation mutation request succeeds', () => {
|
|
it('dispatches a correct set of mutations', () => {
|
|
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
|
|
data: {
|
|
createIssue: {
|
|
issue: mockIssue,
|
|
errors: [],
|
|
},
|
|
},
|
|
});
|
|
|
|
testAction({
|
|
action: actions.addListNewIssue,
|
|
payload: {
|
|
issueInput: mockIssue,
|
|
list: fakeList,
|
|
placeholderId: 'tmp',
|
|
},
|
|
state,
|
|
expectedActions: [
|
|
{
|
|
type: 'addListItem',
|
|
payload: {
|
|
list: fakeList,
|
|
item: formatIssue({ ...mockIssue, id: 'tmp', isLoading: true }),
|
|
position: 0,
|
|
inProgress: true,
|
|
},
|
|
},
|
|
{ type: 'removeListItem', payload: { listId: fakeList.id, itemId: 'tmp' } },
|
|
{
|
|
type: 'addListItem',
|
|
payload: {
|
|
list: fakeList,
|
|
item: formatIssue(mockIssue),
|
|
position: 0,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('when issue creation mutation request fails', () => {
|
|
it('dispatches a correct set of mutations', () => {
|
|
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
|
|
data: {
|
|
createIssue: {
|
|
issue: mockIssue,
|
|
errors: [{ foo: 'bar' }],
|
|
},
|
|
},
|
|
});
|
|
|
|
testAction({
|
|
action: actions.addListNewIssue,
|
|
payload: {
|
|
issueInput: mockIssue,
|
|
list: fakeList,
|
|
placeholderId: 'tmp',
|
|
},
|
|
state,
|
|
expectedActions: [
|
|
{
|
|
type: 'addListItem',
|
|
payload: {
|
|
list: fakeList,
|
|
item: formatIssue({ ...mockIssue, id: 'tmp', isLoading: true }),
|
|
position: 0,
|
|
inProgress: true,
|
|
},
|
|
},
|
|
{ type: 'removeListItem', payload: { listId: fakeList.id, itemId: 'tmp' } },
|
|
],
|
|
expectedMutations: [
|
|
{
|
|
type: types.SET_ERROR,
|
|
payload: 'An error occurred while creating the issue. Please try again.',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('setActiveIssueLabels', () => {
|
|
const state = { boardItems: { [mockIssue.id]: mockIssue } };
|
|
const getters = { activeBoardItem: { ...mockIssue, labels } };
|
|
const testLabelIds = labels.map((label) => label.id);
|
|
const input = {
|
|
labelIds: testLabelIds,
|
|
removeLabelIds: [],
|
|
projectPath: 'h/b',
|
|
labels,
|
|
};
|
|
|
|
it('should assign labels', () => {
|
|
const payload = {
|
|
itemId: getters.activeBoardItem.id,
|
|
prop: 'labels',
|
|
value: labels,
|
|
};
|
|
|
|
testAction(
|
|
actions.setActiveIssueLabels,
|
|
input,
|
|
{ ...state, ...getters },
|
|
[
|
|
{
|
|
type: types.UPDATE_BOARD_ITEM_BY_ID,
|
|
payload,
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
});
|
|
|
|
it('should remove label', () => {
|
|
const payload = {
|
|
itemId: getters.activeBoardItem.id,
|
|
prop: 'labels',
|
|
value: [labels[1]],
|
|
};
|
|
|
|
testAction(
|
|
actions.setActiveIssueLabels,
|
|
{ ...input, removeLabelIds: [getIdFromGraphQLId(labels[0].id)] },
|
|
{ ...state, ...getters },
|
|
[
|
|
{
|
|
type: types.UPDATE_BOARD_ITEM_BY_ID,
|
|
payload,
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('setActiveItemSubscribed', () => {
|
|
const state = {
|
|
boardItems: {
|
|
[mockActiveIssue.id]: mockActiveIssue,
|
|
},
|
|
fullPath: 'gitlab-org',
|
|
issuableType: issuableTypes.issue,
|
|
};
|
|
const getters = { activeBoardItem: mockActiveIssue, isEpicBoard: false };
|
|
const subscribedState = true;
|
|
const input = {
|
|
subscribedState,
|
|
projectPath: 'gitlab-org/gitlab-test',
|
|
};
|
|
|
|
it('should commit subscribed status', () => {
|
|
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
|
|
data: {
|
|
updateIssuableSubscription: {
|
|
issue: {
|
|
subscribed: subscribedState,
|
|
},
|
|
errors: [],
|
|
},
|
|
},
|
|
});
|
|
|
|
const payload = {
|
|
itemId: getters.activeBoardItem.id,
|
|
prop: 'subscribed',
|
|
value: subscribedState,
|
|
};
|
|
|
|
return testAction(
|
|
actions.setActiveItemSubscribed,
|
|
input,
|
|
{ ...state, ...getters },
|
|
[
|
|
{
|
|
type: types.UPDATE_BOARD_ITEM_BY_ID,
|
|
payload,
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
});
|
|
|
|
it('throws error if fails', async () => {
|
|
jest
|
|
.spyOn(gqlClient, 'mutate')
|
|
.mockResolvedValue({ data: { updateIssuableSubscription: { errors: ['failed mutation'] } } });
|
|
|
|
await expect(actions.setActiveItemSubscribed({ getters }, input)).rejects.toThrow(Error);
|
|
});
|
|
});
|
|
|
|
describe('setActiveItemTitle', () => {
|
|
const state = {
|
|
boardItems: { [mockIssue.id]: mockIssue },
|
|
issuableType: issuableTypes.issue,
|
|
fullPath: 'path/f',
|
|
};
|
|
const getters = { activeBoardItem: mockIssue, isEpicBoard: false };
|
|
const testTitle = 'Test Title';
|
|
const input = {
|
|
title: testTitle,
|
|
projectPath: 'h/b',
|
|
};
|
|
|
|
it('should commit title after setting the issue', () => {
|
|
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
|
|
data: {
|
|
updateIssuableTitle: {
|
|
issue: {
|
|
title: testTitle,
|
|
},
|
|
errors: [],
|
|
},
|
|
},
|
|
});
|
|
|
|
const payload = {
|
|
itemId: getters.activeBoardItem.id,
|
|
prop: 'title',
|
|
value: testTitle,
|
|
};
|
|
|
|
return testAction(
|
|
actions.setActiveItemTitle,
|
|
input,
|
|
{ ...state, ...getters },
|
|
[
|
|
{
|
|
type: types.UPDATE_BOARD_ITEM_BY_ID,
|
|
payload,
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
});
|
|
|
|
it('throws error if fails', async () => {
|
|
jest
|
|
.spyOn(gqlClient, 'mutate')
|
|
.mockResolvedValue({ data: { updateIssue: { errors: ['failed mutation'] } } });
|
|
|
|
await expect(actions.setActiveItemTitle({ getters }, input)).rejects.toThrow(Error);
|
|
});
|
|
});
|
|
|
|
describe('setActiveItemConfidential', () => {
|
|
const state = { boardItems: { [mockIssue.id]: mockIssue } };
|
|
const getters = { activeBoardItem: mockIssue };
|
|
|
|
it('set confidential value on board item', () => {
|
|
const payload = {
|
|
itemId: getters.activeBoardItem.id,
|
|
prop: 'confidential',
|
|
value: true,
|
|
};
|
|
|
|
return testAction(
|
|
actions.setActiveItemConfidential,
|
|
true,
|
|
{ ...state, ...getters },
|
|
[
|
|
{
|
|
type: types.UPDATE_BOARD_ITEM_BY_ID,
|
|
payload,
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('fetchGroupProjects', () => {
|
|
const state = {
|
|
fullPath: 'gitlab-org',
|
|
};
|
|
|
|
const pageInfo = {
|
|
endCursor: '',
|
|
hasNextPage: false,
|
|
};
|
|
|
|
const queryResponse = {
|
|
data: {
|
|
group: {
|
|
projects: {
|
|
nodes: mockGroupProjects,
|
|
pageInfo: {
|
|
endCursor: '',
|
|
hasNextPage: false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
it('should commit mutations REQUEST_GROUP_PROJECTS and RECEIVE_GROUP_PROJECTS_SUCCESS on success', () => {
|
|
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
|
|
|
|
return testAction(
|
|
actions.fetchGroupProjects,
|
|
{},
|
|
state,
|
|
[
|
|
{
|
|
type: types.REQUEST_GROUP_PROJECTS,
|
|
payload: false,
|
|
},
|
|
{
|
|
type: types.RECEIVE_GROUP_PROJECTS_SUCCESS,
|
|
payload: { projects: mockGroupProjects, pageInfo, fetchNext: false },
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
});
|
|
|
|
it('should commit mutations REQUEST_GROUP_PROJECTS and RECEIVE_GROUP_PROJECTS_FAILURE on failure', () => {
|
|
jest.spyOn(gqlClient, 'query').mockRejectedValue();
|
|
|
|
return testAction(
|
|
actions.fetchGroupProjects,
|
|
{},
|
|
state,
|
|
[
|
|
{
|
|
type: types.REQUEST_GROUP_PROJECTS,
|
|
payload: false,
|
|
},
|
|
{
|
|
type: types.RECEIVE_GROUP_PROJECTS_FAILURE,
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('setSelectedProject', () => {
|
|
it('should commit mutation SET_SELECTED_PROJECT', () => {
|
|
const project = mockGroupProjects[0];
|
|
|
|
return testAction(
|
|
actions.setSelectedProject,
|
|
project,
|
|
{},
|
|
[
|
|
{
|
|
type: types.SET_SELECTED_PROJECT,
|
|
payload: project,
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('toggleBoardItemMultiSelection', () => {
|
|
const boardItem = mockIssue;
|
|
const boardItem2 = mockIssue2;
|
|
|
|
it('should commit mutation ADD_BOARD_ITEM_TO_SELECTION if item is not on selection state', () => {
|
|
testAction(
|
|
actions.toggleBoardItemMultiSelection,
|
|
boardItem,
|
|
{ selectedBoardItems: [] },
|
|
[
|
|
{
|
|
type: types.ADD_BOARD_ITEM_TO_SELECTION,
|
|
payload: boardItem,
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
});
|
|
|
|
it('should commit mutation REMOVE_BOARD_ITEM_FROM_SELECTION if item is on selection state', () => {
|
|
testAction(
|
|
actions.toggleBoardItemMultiSelection,
|
|
boardItem,
|
|
{ selectedBoardItems: [mockIssue] },
|
|
[
|
|
{
|
|
type: types.REMOVE_BOARD_ITEM_FROM_SELECTION,
|
|
payload: boardItem,
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
});
|
|
|
|
it('should additionally commit mutation ADD_BOARD_ITEM_TO_SELECTION for active issue and dispatch unsetActiveId', () => {
|
|
testAction(
|
|
actions.toggleBoardItemMultiSelection,
|
|
boardItem2,
|
|
{ activeId: mockActiveIssue.id, activeBoardItem: mockActiveIssue, selectedBoardItems: [] },
|
|
[
|
|
{
|
|
type: types.ADD_BOARD_ITEM_TO_SELECTION,
|
|
payload: mockActiveIssue,
|
|
},
|
|
{
|
|
type: types.ADD_BOARD_ITEM_TO_SELECTION,
|
|
payload: boardItem2,
|
|
},
|
|
],
|
|
[{ type: 'unsetActiveId' }],
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('resetBoardItemMultiSelection', () => {
|
|
it('should commit mutation RESET_BOARD_ITEM_SELECTION', () => {
|
|
testAction({
|
|
action: actions.resetBoardItemMultiSelection,
|
|
state: { selectedBoardItems: [mockIssue] },
|
|
expectedMutations: [
|
|
{
|
|
type: types.RESET_BOARD_ITEM_SELECTION,
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('toggleBoardItem', () => {
|
|
it('should dispatch resetBoardItemMultiSelection and unsetActiveId when boardItem is the active item', () => {
|
|
testAction({
|
|
action: actions.toggleBoardItem,
|
|
payload: { boardItem: mockIssue },
|
|
state: {
|
|
activeId: mockIssue.id,
|
|
},
|
|
expectedActions: [{ type: 'resetBoardItemMultiSelection' }, { type: 'unsetActiveId' }],
|
|
});
|
|
});
|
|
|
|
it('should dispatch resetBoardItemMultiSelection and setActiveId when boardItem is not the active item', () => {
|
|
testAction({
|
|
action: actions.toggleBoardItem,
|
|
payload: { boardItem: mockIssue },
|
|
state: {
|
|
activeId: inactiveId,
|
|
},
|
|
expectedActions: [
|
|
{ type: 'resetBoardItemMultiSelection' },
|
|
{ type: 'setActiveId', payload: { id: mockIssue.id, sidebarType: ISSUABLE } },
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('setError', () => {
|
|
it('should commit mutation SET_ERROR', () => {
|
|
testAction({
|
|
action: actions.setError,
|
|
payload: { message: 'mayday' },
|
|
expectedMutations: [
|
|
{
|
|
payload: 'mayday',
|
|
type: types.SET_ERROR,
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should capture error using Sentry when captureError is true', () => {
|
|
jest.spyOn(Sentry, 'captureException');
|
|
|
|
const mockError = new Error();
|
|
actions.setError(
|
|
{ commit: () => {} },
|
|
{
|
|
message: 'mayday',
|
|
error: mockError,
|
|
captureError: true,
|
|
},
|
|
);
|
|
|
|
expect(Sentry.captureException).toHaveBeenNthCalledWith(1, mockError);
|
|
});
|
|
});
|
|
|
|
describe('unsetError', () => {
|
|
it('should commit mutation SET_ERROR with undefined as payload', () => {
|
|
testAction({
|
|
action: actions.unsetError,
|
|
expectedMutations: [
|
|
{
|
|
payload: undefined,
|
|
type: types.SET_ERROR,
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|