summaryrefslogtreecommitdiff
path: root/spec/frontend/boards
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-11-19 08:27:35 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-11-19 08:27:35 +0000
commit7e9c479f7de77702622631cff2628a9c8dcbc627 (patch)
treec8f718a08e110ad7e1894510980d2155a6549197 /spec/frontend/boards
parente852b0ae16db4052c1c567d9efa4facc81146e88 (diff)
downloadgitlab-ce-7e9c479f7de77702622631cff2628a9c8dcbc627.tar.gz
Add latest changes from gitlab-org/gitlab@13-6-stable-eev13.6.0-rc42
Diffstat (limited to 'spec/frontend/boards')
-rw-r--r--spec/frontend/boards/board_list_new_spec.js2
-rw-r--r--spec/frontend/boards/components/board_assignee_dropdown_spec.js308
-rw-r--r--spec/frontend/boards/components/board_card_spec.js4
-rw-r--r--spec/frontend/boards/components/board_column_new_spec.js72
-rw-r--r--spec/frontend/boards/components/board_column_spec.js2
-rw-r--r--spec/frontend/boards/components/board_list_header_new_spec.js169
-rw-r--r--spec/frontend/boards/components/board_new_issue_new_spec.js115
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js137
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js157
-rw-r--r--spec/frontend/boards/mock_data.js30
-rw-r--r--spec/frontend/boards/stores/actions_spec.js323
-rw-r--r--spec/frontend/boards/stores/getters_spec.js30
-rw-r--r--spec/frontend/boards/stores/mutations_spec.js114
13 files changed, 1400 insertions, 63 deletions
diff --git a/spec/frontend/boards/board_list_new_spec.js b/spec/frontend/boards/board_list_new_spec.js
index 163611c2197..55516e3fd56 100644
--- a/spec/frontend/boards/board_list_new_spec.js
+++ b/spec/frontend/boards/board_list_new_spec.js
@@ -77,6 +77,8 @@ const createComponent = ({
provide: {
groupId: null,
rootPath: '/',
+ weightFeatureAvailable: false,
+ boardWeight: null,
},
});
diff --git a/spec/frontend/boards/components/board_assignee_dropdown_spec.js b/spec/frontend/boards/components/board_assignee_dropdown_spec.js
new file mode 100644
index 00000000000..e185a6d5419
--- /dev/null
+++ b/spec/frontend/boards/components/board_assignee_dropdown_spec.js
@@ -0,0 +1,308 @@
+import { mount, createLocalVue } from '@vue/test-utils';
+import { GlDropdownItem, GlAvatarLink, GlAvatarLabeled, GlSearchBoxByType } 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';
+import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
+import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue';
+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 { participants } from '../mock_data';
+
+const localVue = createLocalVue();
+
+localVue.use(VueApollo);
+
+describe('BoardCardAssigneeDropdown', () => {
+ let wrapper;
+ let fakeApollo;
+ let getIssueParticipantsSpy;
+ let getSearchUsersSpy;
+
+ const iid = '111';
+ const activeIssueName = 'test';
+ const anotherIssueName = 'hello';
+
+ const createComponent = (search = '') => {
+ wrapper = mount(BoardAssigneeDropdown, {
+ data() {
+ return {
+ search,
+ selected: store.getters.activeIssue.assignees,
+ participants,
+ };
+ },
+ store,
+ provide: {
+ canUpdate: true,
+ rootPath: '',
+ },
+ });
+ };
+
+ const createComponentWithApollo = (search = '') => {
+ fakeApollo = createMockApollo([
+ [getIssueParticipants, getIssueParticipantsSpy],
+ [searchUsers, getSearchUsersSpy],
+ ]);
+
+ wrapper = mount(BoardAssigneeDropdown, {
+ localVue,
+ apolloProvider: fakeApollo,
+ data() {
+ return {
+ search,
+ selected: store.getters.activeIssue.assignees,
+ participants,
+ };
+ },
+ store,
+ provide: {
+ canUpdate: true,
+ rootPath: '',
+ },
+ });
+ };
+
+ const unassign = async () => {
+ wrapper.find('[data-testid="unassign"]').trigger('click');
+
+ await wrapper.vm.$nextTick();
+ };
+
+ const openDropdown = async () => {
+ wrapper.find('[data-testid="edit-button"]').trigger('click');
+
+ await wrapper.vm.$nextTick();
+ };
+
+ const findByText = text => {
+ return wrapper.findAll(GlDropdownItem).wrappers.find(node => node.text().indexOf(text) === 0);
+ };
+
+ beforeEach(() => {
+ store.state.activeId = '1';
+ store.state.issues = {
+ '1': {
+ iid,
+ assignees: [{ username: activeIssueName, name: activeIssueName, id: activeIssueName }],
+ },
+ };
+
+ jest.spyOn(store, 'dispatch').mockResolvedValue();
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when mounted', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it.each`
+ text
+ ${anotherIssueName}
+ ${activeIssueName}
+ `('finds item with $text', ({ text }) => {
+ const item = findByText(text);
+
+ expect(item.exists()).toBe(true);
+ });
+
+ it('renders gl-avatar-link in gl-dropdown-item', () => {
+ const item = findByText('hello');
+
+ expect(item.find(GlAvatarLink).exists()).toBe(true);
+ });
+
+ it('renders gl-avatar-labeled in gl-avatar-link', () => {
+ const item = findByText('hello');
+
+ expect(
+ item
+ .find(GlAvatarLink)
+ .find(GlAvatarLabeled)
+ .exists(),
+ ).toBe(true);
+ });
+ });
+
+ describe('when selected users are present', () => {
+ it('renders a divider', () => {
+ createComponent();
+
+ expect(wrapper.find('[data-testid="selected-user-divider"]').exists()).toBe(true);
+ });
+ });
+
+ describe('when collapsed', () => {
+ it('renders IssuableAssignees', () => {
+ createComponent();
+
+ expect(wrapper.find(IssuableAssignees).isVisible()).toBe(true);
+ expect(wrapper.find(MultiSelectDropdown).isVisible()).toBe(false);
+ });
+ });
+
+ describe('when dropdown is open', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await openDropdown();
+ });
+
+ it('shows assignees dropdown', async () => {
+ expect(wrapper.find(IssuableAssignees).isVisible()).toBe(false);
+ expect(wrapper.find(MultiSelectDropdown).isVisible()).toBe(true);
+ });
+
+ it('shows the issue returned as the activeIssue', async () => {
+ expect(findByText(activeIssueName).props('isChecked')).toBe(true);
+ });
+
+ describe('when "Unassign" is clicked', () => {
+ it('unassigns assignees', async () => {
+ await unassign();
+
+ expect(findByText('Unassign').props('isChecked')).toBe(true);
+ });
+ });
+
+ describe('when an unselected item is clicked', () => {
+ beforeEach(async () => {
+ await unassign();
+ });
+
+ it('assigns assignee in the dropdown', async () => {
+ wrapper.find('[data-testid="item_test"]').trigger('click');
+
+ await wrapper.vm.$nextTick();
+
+ expect(findByText(activeIssueName).props('isChecked')).toBe(true);
+ });
+
+ it('calls setAssignees with username list', async () => {
+ wrapper.find('[data-testid="item_test"]').trigger('click');
+
+ await wrapper.vm.$nextTick();
+
+ document.body.click();
+
+ await wrapper.vm.$nextTick();
+
+ expect(store.dispatch).toHaveBeenCalledWith('setAssignees', [activeIssueName]);
+ });
+ });
+
+ describe('when the user off clicks', () => {
+ beforeEach(async () => {
+ await unassign();
+
+ document.body.click();
+
+ await wrapper.vm.$nextTick();
+ });
+
+ it('calls setAssignees with username list', async () => {
+ expect(store.dispatch).toHaveBeenCalledWith('setAssignees', []);
+ });
+
+ it('closes the dropdown', async () => {
+ expect(wrapper.find(IssuableAssignees).isVisible()).toBe(true);
+ });
+ });
+ });
+
+ it('renders divider after unassign', () => {
+ createComponent();
+
+ expect(wrapper.find('[data-testid="unassign-divider"]').exists()).toBe(true);
+ });
+
+ it.each`
+ assignees | expected
+ ${[{ id: 5, username: '', name: '' }]} | ${'Assignee'}
+ ${[{ id: 6, username: '', name: '' }, { id: 7, username: '', name: '' }]} | ${'2 Assignees'}
+ `(
+ 'when assignees have a length of $assignees.length, it renders $expected',
+ ({ assignees, expected }) => {
+ store.state.issues['1'].assignees = assignees;
+
+ createComponent();
+
+ expect(wrapper.find(BoardEditableItem).props('title')).toBe(expected);
+ },
+ );
+
+ describe('Apollo', () => {
+ beforeEach(() => {
+ getIssueParticipantsSpy = jest.fn().mockResolvedValue({
+ data: {
+ issue: {
+ participants: {
+ nodes: [
+ {
+ username: 'participant',
+ name: 'participant',
+ webUrl: '',
+ avatarUrl: '',
+ id: '',
+ },
+ ],
+ },
+ },
+ },
+ });
+ getSearchUsersSpy = jest.fn().mockResolvedValue({
+ data: {
+ users: {
+ nodes: [{ username: 'root', name: 'root', webUrl: '', avatarUrl: '', id: '' }],
+ },
+ },
+ });
+ });
+
+ describe('when search is empty', () => {
+ beforeEach(() => {
+ createComponentWithApollo();
+ });
+
+ it('calls getIssueParticipants', async () => {
+ jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+
+ expect(getIssueParticipantsSpy).toHaveBeenCalledWith({ id: 'gid://gitlab/Issue/111' });
+ });
+ });
+
+ describe('when search is not empty', () => {
+ beforeEach(() => {
+ createComponentWithApollo('search term');
+ });
+
+ it('calls searchUsers', async () => {
+ jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+
+ expect(getSearchUsersSpy).toHaveBeenCalledWith({ search: 'search term' });
+ });
+ });
+ });
+
+ it('finds GlSearchBoxByType', async () => {
+ createComponent();
+
+ await openDropdown();
+
+ expect(wrapper.find(GlSearchBoxByType).exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js
index a3ddcdf01b7..5e23c781eae 100644
--- a/spec/frontend/boards/components/board_card_spec.js
+++ b/spec/frontend/boards/components/board_card_spec.js
@@ -175,7 +175,7 @@ describe('BoardCard', () => {
wrapper.trigger('mousedown');
wrapper.trigger('mouseup');
- expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', wrapper.vm.issue, undefined);
+ expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', wrapper.vm.issue, false);
expect(boardsStore.detail.list).toEqual(wrapper.vm.list);
});
@@ -188,7 +188,7 @@ describe('BoardCard', () => {
wrapper.trigger('mousedown');
wrapper.trigger('mouseup');
- expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue', undefined);
+ expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue', false);
});
});
diff --git a/spec/frontend/boards/components/board_column_new_spec.js b/spec/frontend/boards/components/board_column_new_spec.js
new file mode 100644
index 00000000000..4aafc3a867a
--- /dev/null
+++ b/spec/frontend/boards/components/board_column_new_spec.js
@@ -0,0 +1,72 @@
+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';
+
+describe('Board Column Component', () => {
+ let wrapper;
+ let store;
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const createComponent = ({ listType = ListType.backlog, collapsed = false } = {}) => {
+ const boardId = '1';
+
+ const listMock = {
+ ...listObj,
+ list_type: listType,
+ collapsed,
+ };
+
+ if (listType === ListType.assignee) {
+ delete listMock.label;
+ listMock.user = {};
+ }
+
+ const list = new List({ ...listMock, doNotFetchIssues: true });
+
+ store = createStore();
+
+ wrapper = shallowMount(BoardColumn, {
+ store,
+ propsData: {
+ disabled: false,
+ list,
+ },
+ provide: {
+ boardId,
+ },
+ });
+ };
+
+ const isExpandable = () => wrapper.classes('is-expandable');
+ const isCollapsed = () => wrapper.classes('is-collapsed');
+
+ describe('Given different list types', () => {
+ it('is expandable when List Type is `backlog`', () => {
+ createComponent({ listType: ListType.backlog });
+
+ expect(isExpandable()).toBe(true);
+ });
+ });
+
+ describe('expanded / collapsed column', () => {
+ it('has class is-collapsed when list is collapsed', () => {
+ createComponent({ collapsed: false });
+
+ expect(wrapper.vm.list.isExpanded).toBe(true);
+ });
+
+ it('does not have class is-collapsed when list is expanded', () => {
+ createComponent({ collapsed: true });
+
+ expect(isCollapsed()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/boards/components/board_column_spec.js b/spec/frontend/boards/components/board_column_spec.js
index 2a4dbbb989e..ba11225676b 100644
--- a/spec/frontend/boards/components/board_column_spec.js
+++ b/spec/frontend/boards/components/board_column_spec.js
@@ -78,7 +78,7 @@ describe('Board Column Component', () => {
});
});
- describe('expanded / collaped column', () => {
+ describe('expanded / collapsed column', () => {
it('has class is-collapsed when list is collapsed', () => {
createComponent({ collapsed: false });
diff --git a/spec/frontend/boards/components/board_list_header_new_spec.js b/spec/frontend/boards/components/board_list_header_new_spec.js
new file mode 100644
index 00000000000..80786d82620
--- /dev/null
+++ b/spec/frontend/boards/components/board_list_header_new_spec.js
@@ -0,0 +1,169 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+
+import { listObj } 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();
+
+localVue.use(Vuex);
+
+describe('Board List Header Component', () => {
+ let wrapper;
+ let store;
+
+ const updateListSpy = jest.fn();
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+
+ localStorage.clear();
+ });
+
+ const createComponent = ({
+ listType = ListType.backlog,
+ collapsed = false,
+ withLocalStorage = true,
+ currentUserId = null,
+ } = {}) => {
+ const boardId = '1';
+
+ const listMock = {
+ ...listObj,
+ list_type: listType,
+ collapsed,
+ };
+
+ if (listType === ListType.assignee) {
+ delete listMock.label;
+ listMock.user = {};
+ }
+
+ const list = new List({ ...listMock, doNotFetchIssues: true });
+
+ if (withLocalStorage) {
+ localStorage.setItem(
+ `boards.${boardId}.${list.type}.${list.id}.expanded`,
+ (!collapsed).toString(),
+ );
+ }
+
+ store = new Vuex.Store({
+ state: {},
+ actions: { updateList: updateListSpy },
+ getters: {},
+ });
+
+ wrapper = shallowMount(BoardListHeader, {
+ store,
+ localVue,
+ propsData: {
+ disabled: false,
+ list,
+ },
+ provide: {
+ boardId,
+ weightFeatureAvailable: false,
+ currentUserId,
+ },
+ });
+ };
+
+ const isExpanded = () => wrapper.vm.list.isExpanded;
+ const isCollapsed = () => !isExpanded();
+
+ const findAddIssueButton = () => wrapper.find({ ref: 'newIssueBtn' });
+ const findCaret = () => wrapper.find('.board-title-caret');
+
+ describe('Add issue button', () => {
+ const hasNoAddButton = [ListType.promotion, ListType.blank, ListType.closed];
+ const hasAddButton = [ListType.backlog, ListType.label, ListType.milestone, ListType.assignee];
+
+ it.each(hasNoAddButton)('does not render when List Type is `%s`', listType => {
+ createComponent({ listType });
+
+ expect(findAddIssueButton().exists()).toBe(false);
+ });
+
+ it.each(hasAddButton)('does render when List Type is `%s`', listType => {
+ createComponent({ listType });
+
+ expect(findAddIssueButton().exists()).toBe(true);
+ });
+
+ it('has a test for each list type', () => {
+ createComponent();
+
+ Object.values(ListType).forEach(value => {
+ expect([...hasAddButton, ...hasNoAddButton]).toContain(value);
+ });
+ });
+
+ it('does render when logged out', () => {
+ createComponent();
+
+ expect(findAddIssueButton().exists()).toBe(true);
+ });
+ });
+
+ describe('expanding / collapsing the column', () => {
+ it('does not collapse when clicking the header', async () => {
+ createComponent();
+
+ expect(isCollapsed()).toBe(false);
+
+ wrapper.find('[data-testid="board-list-header"]').trigger('click');
+
+ await wrapper.vm.$nextTick();
+
+ expect(isCollapsed()).toBe(false);
+ });
+
+ it('collapses expanded Column when clicking the collapse icon', async () => {
+ createComponent();
+
+ expect(isExpanded()).toBe(true);
+
+ findCaret().vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+
+ expect(isCollapsed()).toBe(true);
+ });
+
+ it('expands collapsed Column when clicking the expand icon', async () => {
+ createComponent({ collapsed: true });
+
+ expect(isCollapsed()).toBe(true);
+
+ findCaret().vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+
+ expect(isCollapsed()).toBe(false);
+ });
+
+ it("when logged in it calls list update and doesn't set localStorage", async () => {
+ createComponent({ withLocalStorage: false, currentUserId: 1 });
+
+ findCaret().vm.$emit('click');
+ await wrapper.vm.$nextTick();
+
+ expect(updateListSpy).toHaveBeenCalledTimes(1);
+ expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(null);
+ });
+
+ it("when logged out it doesn't call list update and sets localStorage", async () => {
+ createComponent();
+
+ findCaret().vm.$emit('click');
+ await wrapper.vm.$nextTick();
+
+ expect(updateListSpy).not.toHaveBeenCalled();
+ expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(String(isExpanded()));
+ });
+ });
+});
diff --git a/spec/frontend/boards/components/board_new_issue_new_spec.js b/spec/frontend/boards/components/board_new_issue_new_spec.js
new file mode 100644
index 00000000000..af4bad65121
--- /dev/null
+++ b/spec/frontend/boards/components/board_new_issue_new_spec.js
@@ -0,0 +1,115 @@
+import Vuex from 'vuex';
+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';
+
+const localVue = createLocalVue();
+
+localVue.use(Vuex);
+
+describe('Issue boards new issue form', () => {
+ let wrapper;
+ let vm;
+
+ const addListNewIssuesSpy = jest.fn();
+
+ const findSubmitButton = () => wrapper.find({ ref: 'submitButton' });
+ const findCancelButton = () => wrapper.find({ ref: 'cancelButton' });
+ const findSubmitForm = () => wrapper.find({ ref: 'submitForm' });
+
+ const submitIssue = () => {
+ const dummySubmitEvent = {
+ preventDefault() {},
+ };
+
+ return findSubmitForm().trigger('submit', dummySubmitEvent);
+ };
+
+ beforeEach(() => {
+ const store = new Vuex.Store({
+ state: {},
+ actions: { addListNewIssue: addListNewIssuesSpy },
+ getters: {},
+ });
+
+ wrapper = shallowMount(BoardNewIssue, {
+ propsData: {
+ disabled: false,
+ list: mockListsWithModel[0],
+ },
+ store,
+ localVue,
+ provide: {
+ groupId: null,
+ weightFeatureAvailable: false,
+ boardWeight: null,
+ },
+ });
+
+ vm = wrapper.vm;
+
+ return vm.$nextTick();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('calls submit if submit button is clicked', async () => {
+ jest.spyOn(wrapper.vm, 'submit').mockImplementation();
+ wrapper.setData({ title: 'Testing Title' });
+
+ await vm.$nextTick();
+ await submitIssue();
+ expect(wrapper.vm.submit).toHaveBeenCalled();
+ });
+
+ it('disables submit button if title is empty', () => {
+ expect(findSubmitButton().props().disabled).toBe(true);
+ });
+
+ it('enables submit button if title is not empty', async () => {
+ wrapper.setData({ title: 'Testing Title' });
+
+ await vm.$nextTick();
+ expect(wrapper.find({ ref: 'input' }).element.value).toBe('Testing Title');
+ expect(findSubmitButton().props().disabled).toBe(false);
+ });
+
+ it('clears title after clicking cancel', async () => {
+ findCancelButton().trigger('click');
+
+ await vm.$nextTick();
+ expect(vm.title).toBe('');
+ });
+
+ describe('submit success', () => {
+ it('creates new issue', async () => {
+ wrapper.setData({ title: 'submit issue' });
+
+ await vm.$nextTick();
+ await submitIssue();
+ expect(addListNewIssuesSpy).toHaveBeenCalled();
+ });
+
+ it('enables button after submit', async () => {
+ jest.spyOn(wrapper.vm, 'submit').mockImplementation();
+ wrapper.setData({ title: 'submit issue' });
+
+ await vm.$nextTick();
+ await submitIssue();
+ expect(findSubmitButton().props().disabled).toBe(false);
+ });
+
+ it('clears title after submit', async () => {
+ wrapper.setData({ title: 'submit issue' });
+
+ await vm.$nextTick();
+ await submitIssue();
+ await vm.$nextTick();
+ expect(vm.title).toBe('');
+ });
+ });
+});
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js
new file mode 100644
index 00000000000..b034c8cb11d
--- /dev/null
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js
@@ -0,0 +1,137 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlDatepicker } from '@gitlab/ui';
+import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
+import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
+import { createStore } from '~/boards/stores';
+import createFlash from '~/flash';
+
+const TEST_DUE_DATE = '2020-02-20';
+const TEST_FORMATTED_DUE_DATE = 'Feb 20, 2020';
+const TEST_PARSED_DATE = new Date(2020, 1, 20);
+const TEST_ISSUE = { id: 'gid://gitlab/Issue/1', iid: 9, dueDate: null, referencePath: 'h/b#2' };
+
+jest.mock('~/flash');
+
+describe('~/boards/components/sidebar/board_sidebar_due_date.vue', () => {
+ let wrapper;
+ let store;
+
+ afterEach(() => {
+ wrapper.destroy();
+ store = null;
+ wrapper = null;
+ });
+
+ const createWrapper = ({ dueDate = null } = {}) => {
+ store = createStore();
+ store.state.issues = { [TEST_ISSUE.id]: { ...TEST_ISSUE, dueDate } };
+ store.state.activeId = TEST_ISSUE.id;
+
+ wrapper = shallowMount(BoardSidebarDueDate, {
+ store,
+ provide: {
+ canUpdate: true,
+ },
+ stubs: {
+ 'board-editable-item': BoardEditableItem,
+ },
+ });
+ };
+
+ const findDatePicker = () => wrapper.find(GlDatepicker);
+ const findResetButton = () => wrapper.find('[data-testid="reset-button"]');
+ const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
+
+ it('renders "None" when no due date is set', () => {
+ createWrapper();
+
+ expect(findCollapsed().text()).toBe('None');
+ expect(findResetButton().exists()).toBe(false);
+ });
+
+ it('renders formatted due date with reset button when set', () => {
+ createWrapper({ dueDate: TEST_DUE_DATE });
+
+ expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE);
+ expect(findResetButton().exists()).toBe(true);
+ });
+
+ describe('when due date is submitted', () => {
+ beforeEach(async () => {
+ createWrapper();
+
+ jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => {
+ store.state.issues[TEST_ISSUE.id].dueDate = TEST_DUE_DATE;
+ });
+ findDatePicker().vm.$emit('input', TEST_PARSED_DATE);
+ await wrapper.vm.$nextTick();
+ });
+
+ it('collapses sidebar and renders formatted due date with reset button', () => {
+ expect(findCollapsed().isVisible()).toBe(true);
+ expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE);
+ expect(findResetButton().exists()).toBe(true);
+ });
+
+ it('commits change to the server', () => {
+ expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalledWith({
+ dueDate: TEST_DUE_DATE,
+ projectPath: 'h/b',
+ });
+ });
+ });
+
+ describe('when due date is cleared', () => {
+ beforeEach(async () => {
+ createWrapper();
+
+ jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => {
+ store.state.issues[TEST_ISSUE.id].dueDate = null;
+ });
+ findDatePicker().vm.$emit('clear');
+ await wrapper.vm.$nextTick();
+ });
+
+ it('collapses sidebar and renders "None"', () => {
+ expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalled();
+ expect(findCollapsed().isVisible()).toBe(true);
+ expect(findCollapsed().text()).toBe('None');
+ });
+ });
+
+ describe('when due date is resetted', () => {
+ beforeEach(async () => {
+ createWrapper({ dueDate: TEST_DUE_DATE });
+
+ jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => {
+ store.state.issues[TEST_ISSUE.id].dueDate = null;
+ });
+ findResetButton().vm.$emit('click');
+ await wrapper.vm.$nextTick();
+ });
+
+ it('collapses sidebar and renders "None"', () => {
+ expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalled();
+ expect(findCollapsed().isVisible()).toBe(true);
+ expect(findCollapsed().text()).toBe('None');
+ });
+ });
+
+ describe('when the mutation fails', () => {
+ beforeEach(async () => {
+ createWrapper({ dueDate: TEST_DUE_DATE });
+
+ jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => {
+ throw new Error(['failed mutation']);
+ });
+ findDatePicker().vm.$emit('input', 'Invalid date');
+ await wrapper.vm.$nextTick();
+ });
+
+ it('collapses sidebar and renders former issue due date', () => {
+ expect(findCollapsed().isVisible()).toBe(true);
+ expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE);
+ expect(createFlash).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js
new file mode 100644
index 00000000000..ee54c662167
--- /dev/null
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js
@@ -0,0 +1,157 @@
+import Vuex from 'vuex';
+import { mount, createLocalVue } from '@vue/test-utils';
+import { GlToggle, GlLoadingIcon } from '@gitlab/ui';
+import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue';
+import * as types from '~/boards/stores/mutation_types';
+import { createStore } from '~/boards/stores';
+import { mockActiveIssue } from '../../mock_data';
+import createFlash from '~/flash';
+
+jest.mock('~/flash.js');
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () => {
+ let wrapper;
+ let store;
+
+ const findNotificationHeader = () => wrapper.find("[data-testid='notification-header-text']");
+ const findToggle = () => wrapper.find(GlToggle);
+ const findGlLoadingIcon = () => wrapper.find(GlLoadingIcon);
+
+ const createComponent = (activeIssue = { ...mockActiveIssue }) => {
+ store = createStore();
+ store.state.issues = { [activeIssue.id]: activeIssue };
+ store.state.activeId = activeIssue.id;
+
+ wrapper = mount(BoardSidebarSubscription, {
+ localVue,
+ store,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ store = null;
+ jest.clearAllMocks();
+ });
+
+ describe('Board sidebar subscription component template', () => {
+ it('displays "notifications" heading', () => {
+ createComponent();
+
+ expect(findNotificationHeader().text()).toBe('Notifications');
+ });
+
+ it('renders toggle as "off" when currently not subscribed', () => {
+ createComponent();
+
+ expect(findToggle().exists()).toBe(true);
+ expect(findToggle().props('value')).toBe(false);
+ });
+
+ it('renders toggle as "on" when currently subscribed', () => {
+ createComponent({
+ ...mockActiveIssue,
+ subscribed: true,
+ });
+
+ expect(findToggle().exists()).toBe(true);
+ expect(findToggle().props('value')).toBe(true);
+ });
+
+ describe('when notification emails have been disabled', () => {
+ beforeEach(() => {
+ createComponent({
+ ...mockActiveIssue,
+ emailsDisabled: true,
+ });
+ });
+
+ it('displays a message that notification have been disabled', () => {
+ expect(findNotificationHeader().text()).toBe(
+ 'Notifications have been disabled by the project or group owner',
+ );
+ });
+
+ it('does not render the toggle button', () => {
+ expect(findToggle().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('Board sidebar subscription component `behavior`', () => {
+ const mockSetActiveIssueSubscribed = subscribedState => {
+ jest.spyOn(wrapper.vm, 'setActiveIssueSubscribed').mockImplementation(async () => {
+ store.commit(types.UPDATE_ISSUE_BY_ID, {
+ issueId: mockActiveIssue.id,
+ prop: 'subscribed',
+ value: subscribedState,
+ });
+ });
+ };
+
+ it('subscribing to notification', async () => {
+ createComponent();
+ mockSetActiveIssueSubscribed(true);
+
+ expect(findGlLoadingIcon().exists()).toBe(false);
+
+ findToggle().trigger('click');
+
+ await wrapper.vm.$nextTick();
+
+ expect(findGlLoadingIcon().exists()).toBe(true);
+ expect(wrapper.vm.setActiveIssueSubscribed).toHaveBeenCalledWith({
+ subscribed: true,
+ projectPath: 'gitlab-org/test-subgroup/gitlab-test',
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(findGlLoadingIcon().exists()).toBe(false);
+ expect(findToggle().props('value')).toBe(true);
+ });
+
+ it('unsubscribing from notification', async () => {
+ createComponent({
+ ...mockActiveIssue,
+ subscribed: true,
+ });
+ mockSetActiveIssueSubscribed(false);
+
+ expect(findGlLoadingIcon().exists()).toBe(false);
+
+ findToggle().trigger('click');
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.setActiveIssueSubscribed).toHaveBeenCalledWith({
+ subscribed: false,
+ projectPath: 'gitlab-org/test-subgroup/gitlab-test',
+ });
+ expect(findGlLoadingIcon().exists()).toBe(true);
+
+ await wrapper.vm.$nextTick();
+
+ expect(findGlLoadingIcon().exists()).toBe(false);
+ expect(findToggle().props('value')).toBe(false);
+ });
+
+ it('flashes an error message when setting the subscribed state fails', async () => {
+ createComponent();
+ jest.spyOn(wrapper.vm, 'setActiveIssueSubscribed').mockImplementation(async () => {
+ throw new Error();
+ });
+
+ findToggle().trigger('click');
+
+ await wrapper.vm.$nextTick();
+ expect(createFlash).toHaveBeenNthCalledWith(1, {
+ message: wrapper.vm.$options.i18n.updateSubscribedErrorMessage,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index 50c0a85fc70..58f67231d55 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -2,6 +2,7 @@
/* 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';
@@ -175,6 +176,14 @@ export const mockIssue = {
},
};
+export const mockActiveIssue = {
+ ...mockIssue,
+ id: 436,
+ iid: '27',
+ subscribed: false,
+ emailsDisabled: false,
+};
+
export const mockIssueWithModel = new ListIssue(mockIssue);
export const mockIssue2 = {
@@ -290,6 +299,7 @@ export const mockLists = [
assignee: null,
milestone: null,
loading: false,
+ issuesSize: 1,
},
{
id: 'gid://gitlab/List/2',
@@ -307,9 +317,12 @@ export const mockLists = [
assignee: null,
milestone: null,
loading: false,
+ issuesSize: 0,
},
];
+export const mockListsById = keyBy(mockLists, 'id');
+
export const mockListsWithModel = mockLists.map(listMock =>
Vue.observable(new List({ ...listMock, doNotFetchIssues: true })),
);
@@ -319,6 +332,23 @@ export const mockIssuesByListId = {
'gid://gitlab/List/2': mockIssues.map(({ id }) => id),
};
+export const participants = [
+ {
+ id: '1',
+ username: 'test',
+ name: 'test',
+ avatar: '',
+ avatarUrl: '',
+ },
+ {
+ id: '2',
+ username: 'hello',
+ name: 'hello',
+ avatar: '',
+ avatarUrl: '',
+ },
+];
+
export const issues = {
[mockIssue.id]: mockIssue,
[mockIssue2.id]: mockIssue2,
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index 78e70161121..4d529580a7a 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -2,17 +2,21 @@ import testAction from 'helpers/vuex_action_helper';
import {
mockListsWithModel,
mockLists,
+ mockListsById,
mockIssue,
mockIssueWithModel,
mockIssue2WithModel,
rawIssue,
mockIssues,
labels,
+ mockActiveIssue,
} from '../mock_data';
import actions, { gqlClient } from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types';
-import { inactiveId, ListType } from '~/boards/constants';
+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 updateAssignees from '~/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql';
import { fullBoardId, formatListIssues, formatBoardLists } from '~/boards/boards_util';
const expectNotImplemented = action => {
@@ -116,7 +120,7 @@ describe('fetchLists', () => {
payload: formattedLists,
},
],
- [{ type: 'showWelcomeList' }],
+ [{ type: 'generateDefaultLists' }],
done,
);
});
@@ -146,14 +150,15 @@ describe('fetchLists', () => {
payload: formattedLists,
},
],
- [{ type: 'createList', payload: { backlog: true } }, { type: 'showWelcomeList' }],
+ [{ type: 'createList', payload: { backlog: true } }, { type: 'generateDefaultLists' }],
done,
);
});
});
-describe('showWelcomeList', () => {
- it('should dispatch addList action', done => {
+describe('generateDefaultLists', () => {
+ let store;
+ beforeEach(() => {
const state = {
endpoints: { fullPath: 'gitlab-org', boardId: '1' },
boardType: 'group',
@@ -161,26 +166,19 @@ describe('showWelcomeList', () => {
boardLists: [{ type: 'backlog' }, { type: 'closed' }],
};
- const blankList = {
- id: 'blank',
- listType: ListType.blank,
- title: 'Welcome to your issue board!',
- position: 0,
- };
-
- testAction(
- actions.showWelcomeList,
- {},
+ store = {
+ commit: jest.fn(),
+ dispatch: jest.fn(() => Promise.resolve()),
state,
- [],
- [{ type: 'addList', payload: blankList }],
- done,
- );
+ };
});
-});
-describe('generateDefaultLists', () => {
- expectNotImplemented(actions.generateDefaultLists);
+ 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', () => {
@@ -323,8 +321,82 @@ describe('updateList', () => {
});
});
-describe('deleteList', () => {
- expectNotImplemented(actions.deleteList);
+describe('removeList', () => {
+ let state;
+ const list = mockLists[0];
+ const listId = list.id;
+ const mutationVariables = {
+ mutation: destroyBoardListMutation,
+ variables: {
+ listId,
+ },
+ };
+
+ beforeEach(() => {
+ state = {
+ boardLists: mockListsById,
+ };
+ });
+
+ afterEach(() => {
+ state = null;
+ });
+
+ it('optimistically deletes the list', () => {
+ const commit = jest.fn();
+
+ actions.removeList({ commit, state }, listId);
+
+ expect(commit.mock.calls).toEqual([[types.REMOVE_LIST, listId]]);
+ });
+
+ it('keeps the updated list if remove succeeds', async () => {
+ const commit = jest.fn();
+ jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
+ data: {
+ destroyBoardList: {
+ errors: [],
+ },
+ },
+ });
+
+ await actions.removeList({ commit, state }, listId);
+
+ expect(gqlClient.mutate).toHaveBeenCalledWith(mutationVariables);
+ expect(commit.mock.calls).toEqual([[types.REMOVE_LIST, listId]]);
+ });
+
+ it('restores the list if update fails', async () => {
+ const commit = jest.fn();
+ jest.spyOn(gqlClient, 'mutate').mockResolvedValue(Promise.reject());
+
+ await actions.removeList({ commit, state }, 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 }, listId);
+
+ expect(gqlClient.mutate).toHaveBeenCalledWith(mutationVariables);
+ expect(commit.mock.calls).toEqual([
+ [types.REMOVE_LIST, listId],
+ [types.REMOVE_LIST_FAILURE, mockListsById],
+ ]);
+ });
});
describe('fetchIssuesForList', () => {
@@ -560,41 +632,106 @@ describe('moveIssue', () => {
});
});
-describe('createNewIssue', () => {
- expectNotImplemented(actions.createNewIssue);
+describe('setAssignees', () => {
+ const node = { username: 'name' };
+ const name = 'username';
+ const projectPath = 'h/h';
+ const refPath = `${projectPath}#3`;
+ const iid = '1';
+
+ 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],
+ );
+
+ 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: 'UPDATE_ISSUE_BY_ID',
+ payload: { prop: 'assignees', issueId: undefined, value: [node] },
+ },
+ ],
+ [],
+ done,
+ );
+ });
});
-describe('addListIssue', () => {
- it('should commit UPDATE_LIST_FAILURE mutation when API returns an error', done => {
- const payload = {
- list: mockLists[0],
- issue: mockIssue,
- position: 0,
- };
+describe('createNewIssue', () => {
+ const state = {
+ boardType: 'group',
+ endpoints: {
+ fullPath: 'gitlab-org/gitlab',
+ },
+ };
+
+ it('should return issue from API on success', async () => {
+ jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
+ data: {
+ createIssue: {
+ issue: mockIssue,
+ errors: [],
+ },
+ },
+ });
+
+ const result = await actions.createNewIssue({ state }, mockIssue);
+ expect(result).toEqual(mockIssue);
+ });
+
+ it('should commit CREATE_ISSUE_FAILURE mutation when API returns an error', done => {
+ jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
+ data: {
+ createIssue: {
+ issue: {},
+ errors: [{ foo: 'bar' }],
+ },
+ },
+ });
+
+ const payload = mockIssue;
testAction(
- actions.addListIssue,
+ actions.createNewIssue,
payload,
- {},
- [{ type: types.ADD_ISSUE_TO_LIST, payload }],
+ state,
+ [{ type: types.CREATE_ISSUE_FAILURE }],
[],
done,
);
});
});
-describe('addListIssueFailure', () => {
- it('should commit UPDATE_LIST_FAILURE mutation when API returns an error', done => {
+describe('addListIssue', () => {
+ it('should commit ADD_ISSUE_TO_LIST mutation', done => {
const payload = {
list: mockLists[0],
issue: mockIssue,
+ position: 0,
};
testAction(
- actions.addListIssueFailure,
+ actions.addListIssue,
payload,
{},
- [{ type: types.ADD_ISSUE_TO_LIST_FAILURE, payload }],
+ [{ type: types.ADD_ISSUE_TO_LIST, payload }],
[],
done,
);
@@ -603,7 +740,7 @@ describe('addListIssueFailure', () => {
describe('setActiveIssueLabels', () => {
const state = { issues: { [mockIssue.id]: mockIssue } };
- const getters = { getActiveIssue: mockIssue };
+ const getters = { activeIssue: mockIssue };
const testLabelIds = labels.map(label => label.id);
const input = {
addLabelIds: testLabelIds,
@@ -617,7 +754,7 @@ describe('setActiveIssueLabels', () => {
.mockResolvedValue({ data: { updateIssue: { issue: { labels: { nodes: labels } } } } });
const payload = {
- issueId: getters.getActiveIssue.id,
+ issueId: getters.activeIssue.id,
prop: 'labels',
value: labels,
};
@@ -646,6 +783,108 @@ describe('setActiveIssueLabels', () => {
});
});
+describe('setActiveIssueDueDate', () => {
+ const state = { issues: { [mockIssue.id]: mockIssue } };
+ const getters = { activeIssue: mockIssue };
+ const testDueDate = '2020-02-20';
+ const input = {
+ dueDate: testDueDate,
+ projectPath: 'h/b',
+ };
+
+ it('should commit due date after setting the issue', done => {
+ jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
+ data: {
+ updateIssue: {
+ issue: {
+ dueDate: testDueDate,
+ },
+ errors: [],
+ },
+ },
+ });
+
+ const payload = {
+ issueId: getters.activeIssue.id,
+ prop: 'dueDate',
+ value: testDueDate,
+ };
+
+ testAction(
+ actions.setActiveIssueDueDate,
+ 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.setActiveIssueDueDate({ getters }, input)).rejects.toThrow(Error);
+ });
+});
+
+describe('setActiveIssueSubscribed', () => {
+ const state = { issues: { [mockActiveIssue.id]: mockActiveIssue } };
+ const getters = { activeIssue: mockActiveIssue };
+ const subscribedState = true;
+ const input = {
+ subscribedState,
+ projectPath: 'gitlab-org/gitlab-test',
+ };
+
+ it('should commit subscribed status', done => {
+ jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
+ data: {
+ issueSetSubscription: {
+ issue: {
+ subscribed: subscribedState,
+ },
+ errors: [],
+ },
+ },
+ });
+
+ const payload = {
+ issueId: getters.activeIssue.id,
+ prop: 'subscribed',
+ value: subscribedState,
+ };
+
+ testAction(
+ actions.setActiveIssueSubscribed,
+ input,
+ { ...state, ...getters },
+ [
+ {
+ type: types.UPDATE_ISSUE_BY_ID,
+ payload,
+ },
+ ],
+ [],
+ done,
+ );
+ });
+
+ it('throws error if fails', async () => {
+ jest
+ .spyOn(gqlClient, 'mutate')
+ .mockResolvedValue({ data: { issueSetSubscription: { errors: ['failed mutation'] } } });
+
+ await expect(actions.setActiveIssueSubscribed({ 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 b987080abab..64025726dd1 100644
--- a/spec/frontend/boards/stores/getters_spec.js
+++ b/spec/frontend/boards/stores/getters_spec.js
@@ -10,13 +10,13 @@ import {
} from '../mock_data';
describe('Boards - Getters', () => {
- describe('getLabelToggleState', () => {
+ describe('labelToggleState', () => {
it('should return "on" when isShowingLabels is true', () => {
const state = {
isShowingLabels: true,
};
- expect(getters.getLabelToggleState(state)).toBe('on');
+ expect(getters.labelToggleState(state)).toBe('on');
});
it('should return "off" when isShowingLabels is false', () => {
@@ -24,7 +24,7 @@ describe('Boards - Getters', () => {
isShowingLabels: false,
};
- expect(getters.getLabelToggleState(state)).toBe('off');
+ expect(getters.labelToggleState(state)).toBe('off');
});
});
@@ -112,7 +112,7 @@ describe('Boards - Getters', () => {
});
});
- describe('getActiveIssue', () => {
+ describe('activeIssue', () => {
it.each`
id | expected
${'1'} | ${'issue'}
@@ -120,11 +120,27 @@ describe('Boards - Getters', () => {
`('returns $expected when $id is passed to state', ({ id, expected }) => {
const state = { issues: { '1': 'issue' }, activeId: id };
- expect(getters.getActiveIssue(state)).toEqual(expected);
+ expect(getters.activeIssue(state)).toEqual(expected);
});
});
- describe('getIssues', () => {
+ describe('projectPathByIssueId', () => {
+ it('returns project path for the active issue', () => {
+ const mockActiveIssue = {
+ referencePath: 'gitlab-org/gitlab-test#1',
+ };
+ expect(getters.projectPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual(
+ 'gitlab-org/gitlab-test',
+ );
+ });
+
+ it('returns empty string as project when active issue is an empty object', () => {
+ const mockActiveIssue = {};
+ expect(getters.projectPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual('');
+ });
+ });
+
+ describe('getIssuesByList', () => {
const boardsState = {
issuesByListId: mockIssuesByListId,
issues,
@@ -132,7 +148,7 @@ describe('Boards - Getters', () => {
it('returns issues for a given listId', () => {
const getIssueById = issueId => [mockIssue, mockIssue2].find(({ id }) => id === issueId);
- expect(getters.getIssues(boardsState, { getIssueById })('gid://gitlab/List/2')).toEqual(
+ expect(getters.getIssuesByList(boardsState, { getIssueById })('gid://gitlab/List/2')).toEqual(
mockIssues,
);
});
diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js
index 6e53f184bb3..e1e57a8fd43 100644
--- a/spec/frontend/boards/stores/mutations_spec.js
+++ b/spec/frontend/boards/stores/mutations_spec.js
@@ -82,7 +82,7 @@ describe('Board Store Mutations', () => {
mutations.SET_ACTIVE_ID(state, expected);
});
- it('updates aciveListId to be the value that is passed', () => {
+ it('updates activeListId to be the value that is passed', () => {
expect(state.activeId).toBe(expected.id);
});
@@ -101,6 +101,34 @@ describe('Board Store Mutations', () => {
});
});
+ describe('CREATE_LIST_FAILURE', () => {
+ it('sets error message', () => {
+ mutations.CREATE_LIST_FAILURE(state);
+
+ expect(state.error).toEqual('An error occurred while creating the list. Please try again.');
+ });
+ });
+
+ describe('RECEIVE_LABELS_FAILURE', () => {
+ it('sets error message', () => {
+ mutations.RECEIVE_LABELS_FAILURE(state);
+
+ expect(state.error).toEqual(
+ 'An error occurred while fetching labels. Please reload the page.',
+ );
+ });
+ });
+
+ describe('GENERATE_DEFAULT_LISTS_FAILURE', () => {
+ it('sets error message', () => {
+ mutations.GENERATE_DEFAULT_LISTS_FAILURE(state);
+
+ expect(state.error).toEqual(
+ 'An error occurred while generating lists. Please reload the page.',
+ );
+ });
+ });
+
describe('REQUEST_ADD_LIST', () => {
expectNotImplemented(mutations.REQUEST_ADD_LIST);
});
@@ -156,16 +184,43 @@ describe('Board Store Mutations', () => {
});
});
- describe('REQUEST_REMOVE_LIST', () => {
- expectNotImplemented(mutations.REQUEST_REMOVE_LIST);
- });
+ describe('REMOVE_LIST', () => {
+ it('removes list from boardLists', () => {
+ const [list, secondList] = mockListsWithModel;
+ const expected = {
+ [secondList.id]: secondList,
+ };
+ state = {
+ ...state,
+ boardLists: { ...initialBoardListsState },
+ };
- describe('RECEIVE_REMOVE_LIST_SUCCESS', () => {
- expectNotImplemented(mutations.RECEIVE_REMOVE_LIST_SUCCESS);
+ mutations[types.REMOVE_LIST](state, list.id);
+
+ expect(state.boardLists).toEqual(expected);
+ });
});
- describe('RECEIVE_REMOVE_LIST_ERROR', () => {
- expectNotImplemented(mutations.RECEIVE_REMOVE_LIST_ERROR);
+ describe('REMOVE_LIST_FAILURE', () => {
+ it('restores lists from backup', () => {
+ const backupLists = { ...initialBoardListsState };
+
+ mutations[types.REMOVE_LIST_FAILURE](state, backupLists);
+
+ expect(state.boardLists).toEqual(backupLists);
+ });
+
+ it('sets error state', () => {
+ const backupLists = { ...initialBoardListsState };
+ state = {
+ ...state,
+ error: undefined,
+ };
+
+ mutations[types.REMOVE_LIST_FAILURE](state, backupLists);
+
+ expect(state.error).toEqual('An error occurred while removing the list. Please try again.');
+ });
});
describe('RESET_ISSUES', () => {
@@ -387,6 +442,14 @@ describe('Board Store Mutations', () => {
expectNotImplemented(mutations.RECEIVE_UPDATE_ISSUE_ERROR);
});
+ describe('CREATE_ISSUE_FAILURE', () => {
+ it('sets error message on state', () => {
+ mutations.CREATE_ISSUE_FAILURE(state);
+
+ expect(state.error).toBe('An error occurred while creating the issue. Please try again.');
+ });
+ });
+
describe('ADD_ISSUE_TO_LIST', () => {
it('adds issue to issues state and issue id in list in issuesByListId', () => {
const listIssues = {
@@ -400,17 +463,45 @@ describe('Board Store Mutations', () => {
...state,
issuesByListId: listIssues,
issues,
+ boardLists: initialBoardListsState,
};
- mutations.ADD_ISSUE_TO_LIST(state, { list: mockLists[0], issue: mockIssue2 });
+ expect(state.boardLists['gid://gitlab/List/1'].issuesSize).toBe(1);
+
+ mutations.ADD_ISSUE_TO_LIST(state, { list: mockListsWithModel[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);
});
});
describe('ADD_ISSUE_TO_LIST_FAILURE', () => {
- it('removes issue id from list in issuesByListId', () => {
+ it('removes issue id from list in issuesByListId and sets error message', () => {
+ const listIssues = {
+ 'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id],
+ };
+ const issues = {
+ '1': mockIssue,
+ '2': mockIssue2,
+ };
+
+ state = {
+ ...state,
+ issuesByListId: listIssues,
+ issues,
+ boardLists: initialBoardListsState,
+ };
+
+ mutations.ADD_ISSUE_TO_LIST_FAILURE(state, { list: mockLists[0], issueId: mockIssue2.id });
+
+ expect(state.issuesByListId['gid://gitlab/List/1']).not.toContain(mockIssue2.id);
+ expect(state.error).toBe('An error occurred while creating the issue. Please try again.');
+ });
+ });
+
+ describe('REMOVE_ISSUE_FROM_LIST', () => {
+ it('removes issue id from list in issuesByListId and deletes issue from state', () => {
const listIssues = {
'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id],
};
@@ -426,9 +517,10 @@ describe('Board Store Mutations', () => {
boardLists: initialBoardListsState,
};
- mutations.ADD_ISSUE_TO_LIST_FAILURE(state, { list: mockLists[0], issue: mockIssue2 });
+ mutations.ADD_ISSUE_TO_LIST_FAILURE(state, { list: mockLists[0], issueId: mockIssue2.id });
expect(state.issuesByListId['gid://gitlab/List/1']).not.toContain(mockIssue2.id);
+ expect(state.issues).not.toContain(mockIssue2);
});
});