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/components')
-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
9 files changed, 584 insertions, 66 deletions
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();
+ });
+ });
+});