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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/boards')
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js87
-rw-r--r--spec/frontend/boards/board_new_issue_deprecated_spec.js16
-rw-r--r--spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap30
-rw-r--r--spec/frontend/boards/components/board_add_new_column_form_spec.js32
-rw-r--r--spec/frontend/boards/components/board_add_new_column_spec.js10
-rw-r--r--spec/frontend/boards/components/board_blocked_icon_spec.js226
-rw-r--r--spec/frontend/boards/components/board_content_sidebar_spec.js140
-rw-r--r--spec/frontend/boards/components/board_content_spec.js13
-rw-r--r--spec/frontend/boards/components/board_form_spec.js2
-rw-r--r--spec/frontend/boards/components/board_new_issue_spec.js6
-rw-r--r--spec/frontend/boards/components/board_settings_sidebar_spec.js43
-rw-r--r--spec/frontend/boards/components/filtered_search_spec.js65
-rw-r--r--spec/frontend/boards/components/issue_time_estimate_spec.js6
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js14
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js28
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js58
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js (renamed from spec/frontend/boards/components/sidebar/board_sidebar_issue_title_spec.js)36
-rw-r--r--spec/frontend/boards/mock_data.js130
-rw-r--r--spec/frontend/boards/modal_store_spec.js134
-rw-r--r--spec/frontend/boards/stores/actions_spec.js708
-rw-r--r--spec/frontend/boards/stores/getters_spec.js41
-rw-r--r--spec/frontend/boards/stores/mutations_spec.js239
22 files changed, 1449 insertions, 615 deletions
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index 4487fc15de6..36043b09636 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -1,11 +1,14 @@
import { GlLabel } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { range } from 'lodash';
+import Vuex from 'vuex';
+import BoardBlockedIcon from '~/boards/components/board_blocked_icon.vue';
import BoardCardInner from '~/boards/components/board_card_inner.vue';
+import { issuableTypes } from '~/boards/constants';
import eventHub from '~/boards/eventhub';
import defaultStore from '~/boards/stores';
import { updateHistory } from '~/lib/utils/url_utility';
-import { mockLabelList } from './mock_data';
+import { mockLabelList, mockIssue } from './mock_data';
jest.mock('~/lib/utils/url_utility');
jest.mock('~/boards/eventhub');
@@ -29,8 +32,28 @@ describe('Board card component', () => {
let wrapper;
let issue;
let list;
+ let store;
+
+ const findBoardBlockedIcon = () => wrapper.find(BoardBlockedIcon);
+
+ const createStore = () => {
+ store = new Vuex.Store({
+ ...defaultStore,
+ state: {
+ ...defaultStore.state,
+ issuableType: issuableTypes.issue,
+ },
+ getters: {
+ isGroupBoard: () => true,
+ isEpicBoard: () => false,
+ isProjectBoard: () => false,
+ },
+ });
+ };
+
+ const createWrapper = (props = {}) => {
+ createStore();
- const createWrapper = (props = {}, store = defaultStore) => {
wrapper = mount(BoardCardInner, {
store,
propsData: {
@@ -41,6 +64,13 @@ describe('Board card component', () => {
stubs: {
GlLabel: true,
},
+ mocks: {
+ $apollo: {
+ queries: {
+ blockingIssuables: { loading: false },
+ },
+ },
+ },
provide: {
rootPath: '/',
scopedLabelsAvailable: false,
@@ -51,14 +81,9 @@ describe('Board card component', () => {
beforeEach(() => {
list = mockLabelList;
issue = {
- title: 'Testing',
- id: 1,
- iid: 1,
- confidential: false,
+ ...mockIssue,
labels: [list.label],
assignees: [],
- referencePath: '#1',
- webUrl: '/test/1',
weight: 1,
};
@@ -68,6 +93,7 @@ describe('Board card component', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
+ store = null;
jest.clearAllMocks();
});
@@ -87,18 +113,38 @@ describe('Board card component', () => {
expect(wrapper.find('.confidential-icon').exists()).toBe(false);
});
- it('does not render blocked icon', () => {
- expect(wrapper.find('.issue-blocked-icon').exists()).toBe(false);
- });
-
it('renders issue ID with #', () => {
- expect(wrapper.find('.board-card-number').text()).toContain(`#${issue.id}`);
+ expect(wrapper.find('.board-card-number').text()).toContain(`#${issue.iid}`);
});
it('does not render assignee', () => {
expect(wrapper.find('.board-card-assignee .avatar').exists()).toBe(false);
});
+ describe('blocked', () => {
+ it('renders blocked icon if issue is blocked', async () => {
+ createWrapper({
+ item: {
+ ...issue,
+ blocked: true,
+ },
+ });
+
+ expect(findBoardBlockedIcon().exists()).toBe(true);
+ });
+
+ it('does not show blocked icon if issue is not blocked', () => {
+ createWrapper({
+ item: {
+ ...issue,
+ blocked: false,
+ },
+ });
+
+ expect(findBoardBlockedIcon().exists()).toBe(false);
+ });
+ });
+
describe('confidential issue', () => {
beforeEach(() => {
wrapper.setProps({
@@ -303,21 +349,6 @@ describe('Board card component', () => {
});
});
- describe('blocked', () => {
- beforeEach(() => {
- wrapper.setProps({
- item: {
- ...wrapper.props('item'),
- blocked: true,
- },
- });
- });
-
- it('renders blocked icon if issue is blocked', () => {
- expect(wrapper.find('.issue-blocked-icon').exists()).toBe(true);
- });
- });
-
describe('filterByLabel method', () => {
beforeEach(() => {
delete window.location;
diff --git a/spec/frontend/boards/board_new_issue_deprecated_spec.js b/spec/frontend/boards/board_new_issue_deprecated_spec.js
index 3903ad201b2..3beaf870bf5 100644
--- a/spec/frontend/boards/board_new_issue_deprecated_spec.js
+++ b/spec/frontend/boards/board_new_issue_deprecated_spec.js
@@ -111,7 +111,7 @@ describe('Issue boards new issue form', () => {
describe('submit success', () => {
it('creates new issue', () => {
- wrapper.setData({ title: 'submit issue' });
+ wrapper.setData({ title: 'create issue' });
return Vue.nextTick()
.then(submitIssue)
@@ -122,7 +122,7 @@ describe('Issue boards new issue form', () => {
it('enables button after submit', () => {
jest.spyOn(wrapper.vm, 'submit').mockImplementation();
- wrapper.setData({ title: 'submit issue' });
+ wrapper.setData({ title: 'create issue' });
return Vue.nextTick()
.then(submitIssue)
@@ -132,7 +132,7 @@ describe('Issue boards new issue form', () => {
});
it('clears title after submit', () => {
- wrapper.setData({ title: 'submit issue' });
+ wrapper.setData({ title: 'create issue' });
return Vue.nextTick()
.then(submitIssue)
@@ -143,17 +143,17 @@ describe('Issue boards new issue form', () => {
it('sets detail issue after submit', () => {
expect(boardsStore.detail.issue.title).toBe(undefined);
- wrapper.setData({ title: 'submit issue' });
+ wrapper.setData({ title: 'create issue' });
return Vue.nextTick()
.then(submitIssue)
.then(() => {
- expect(boardsStore.detail.issue.title).toBe('submit issue');
+ expect(boardsStore.detail.issue.title).toBe('create issue');
});
});
it('sets detail list after submit', () => {
- wrapper.setData({ title: 'submit issue' });
+ wrapper.setData({ title: 'create issue' });
return Vue.nextTick()
.then(submitIssue)
@@ -164,7 +164,7 @@ describe('Issue boards new issue form', () => {
it('sets detail weight after submit', () => {
boardsStore.weightFeatureAvailable = true;
- wrapper.setData({ title: 'submit issue' });
+ wrapper.setData({ title: 'create issue' });
return Vue.nextTick()
.then(submitIssue)
@@ -175,7 +175,7 @@ describe('Issue boards new issue form', () => {
it('does not set detail weight after submit', () => {
boardsStore.weightFeatureAvailable = false;
- wrapper.setData({ title: 'submit issue' });
+ wrapper.setData({ title: 'create issue' });
return Vue.nextTick()
.then(submitIssue)
diff --git a/spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap b/spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap
new file mode 100644
index 00000000000..c000f300e4d
--- /dev/null
+++ b/spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap
@@ -0,0 +1,30 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`BoardBlockedIcon on mouseenter on blocked icon with more than three blocking issues matches the snapshot 1`] = `
+"<div class=\\"gl-display-inline\\"><svg data-testid=\\"issue-blocked-icon\\" aria-hidden=\\"true\\" class=\\"issue-blocked-icon gl-mr-2 gl-cursor-pointer gl-icon s16\\" id=\\"blocked-icon-uniqueId\\">
+ <use href=\\"#issue-block\\"></use>
+ </svg>
+ <div class=\\"gl-popover\\">
+ <ul class=\\"gl-list-style-none gl-p-0\\">
+ <li><a href=\\"http://gdk.test:3000/gitlab-org/my-project-1/-/issues/6\\" class=\\"gl-link gl-text-blue-500! gl-font-sm\\">my-project-1#6</a>
+ <p data-testid=\\"issuable-title\\" class=\\"gl-mb-3 gl-display-block!\\">
+ blocking issue title 1
+ </p>
+ </li>
+ <li><a href=\\"http://gdk.test:3000/gitlab-org/my-project-1/-/issues/5\\" class=\\"gl-link gl-text-blue-500! gl-font-sm\\">my-project-1#5</a>
+ <p data-testid=\\"issuable-title\\" class=\\"gl-mb-3 gl-display-block!\\">
+ blocking issue title 2 + blocking issue title 2 + blocking issue title 2 + bloc…
+ </p>
+ </li>
+ <li><a href=\\"http://gdk.test:3000/gitlab-org/my-project-1/-/issues/4\\" class=\\"gl-link gl-text-blue-500! gl-font-sm\\">my-project-1#4</a>
+ <p data-testid=\\"issuable-title\\" class=\\"gl-mb-3 gl-display-block!\\">
+ blocking issue title 3
+ </p>
+ </li>
+ </ul>
+ <div class=\\"gl-mt-4\\">
+ <p data-testid=\\"hidden-blocking-count\\" class=\\"gl-mb-3\\">+ 1 more issue</p> <a data-testid=\\"view-all-issues\\" href=\\"http://gdk.test:3000/gitlab-org/my-project-1/-/issues/0#related-issues\\" class=\\"gl-link gl-text-blue-500! gl-font-sm\\">View all blocking issues</a>
+ </div><span data-testid=\\"popover-title\\">Blocked by 4 issues</span>
+ </div>
+</div>"
+`;
diff --git a/spec/frontend/boards/components/board_add_new_column_form_spec.js b/spec/frontend/boards/components/board_add_new_column_form_spec.js
index 3702f55f17b..3b26ca57d6f 100644
--- a/spec/frontend/boards/components/board_add_new_column_form_spec.js
+++ b/spec/frontend/boards/components/board_add_new_column_form_spec.js
@@ -1,6 +1,6 @@
-import { GlFormGroup, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui';
+import { GlDropdown, GlFormGroup, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue';
@@ -25,7 +25,7 @@ describe('Board card layout', () => {
const mountComponent = ({
loading = false,
- formDescription = '',
+ noneSelected = '',
searchLabel = '',
searchPlaceholder = '',
selectedId,
@@ -34,12 +34,9 @@ describe('Board card layout', () => {
} = {}) => {
wrapper = extendedWrapper(
shallowMount(BoardAddNewColumnForm, {
- stubs: {
- GlFormGroup: true,
- },
propsData: {
loading,
- formDescription,
+ noneSelected,
searchLabel,
searchPlaceholder,
selectedId,
@@ -51,13 +48,15 @@ describe('Board card layout', () => {
...actions,
},
}),
+ stubs: {
+ GlDropdown,
+ },
}),
);
};
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
const formTitle = () => wrapper.findByTestId('board-add-column-form-title').text();
@@ -65,10 +64,13 @@ describe('Board card layout', () => {
const findSearchLabel = () => wrapper.find(GlFormGroup);
const cancelButton = () => wrapper.findByTestId('cancelAddNewColumn');
const submitButton = () => wrapper.findByTestId('addNewColumnButton');
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
it('shows form title & search input', () => {
mountComponent();
+ findDropdown().vm.$emit('show');
+
expect(formTitle()).toEqual(BoardAddNewColumnForm.i18n.newList);
expect(findSearchInput().exists()).toBe(true);
});
@@ -86,16 +88,6 @@ describe('Board card layout', () => {
expect(setAddColumnFormVisibility).toHaveBeenCalledWith(expect.anything(), false);
});
- it('sets placeholder and description from props', () => {
- const props = {
- formDescription: 'Some description of a list',
- };
-
- mountComponent(props);
-
- expect(wrapper.html()).toHaveText(props.formDescription);
- });
-
describe('items', () => {
const mountWithItems = (loading) =>
mountComponent({
@@ -151,13 +143,11 @@ describe('Board card layout', () => {
expect(submitButton().props('disabled')).toBe(true);
});
- it('emits add-list event on click', async () => {
+ it('emits add-list event on click', () => {
mountComponent({
selectedId: mockLabelList.label.id,
});
- await nextTick();
-
submitButton().vm.$emit('click');
expect(wrapper.emitted('add-list')).toEqual([[]]);
diff --git a/spec/frontend/boards/components/board_add_new_column_spec.js b/spec/frontend/boards/components/board_add_new_column_spec.js
index 60584eaf6cf..61f210f566b 100644
--- a/spec/frontend/boards/components/board_add_new_column_spec.js
+++ b/spec/frontend/boards/components/board_add_new_column_spec.js
@@ -1,3 +1,4 @@
+import { GlFormRadioGroup } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
@@ -12,6 +13,10 @@ Vue.use(Vuex);
describe('Board card layout', () => {
let wrapper;
+ const selectLabel = (id) => {
+ wrapper.findComponent(GlFormRadioGroup).vm.$emit('change', id);
+ };
+
const createStore = ({ actions = {}, getters = {}, state = {} } = {}) => {
return new Vuex.Store({
state: {
@@ -57,6 +62,11 @@ describe('Board card layout', () => {
},
}),
);
+
+ // trigger change event
+ if (selectedId) {
+ selectLabel(selectedId);
+ }
};
afterEach(() => {
diff --git a/spec/frontend/boards/components/board_blocked_icon_spec.js b/spec/frontend/boards/components/board_blocked_icon_spec.js
new file mode 100644
index 00000000000..7b04942f056
--- /dev/null
+++ b/spec/frontend/boards/components/board_blocked_icon_spec.js
@@ -0,0 +1,226 @@
+import { GlIcon, GlLink, GlPopover, GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import BoardBlockedIcon from '~/boards/components/board_blocked_icon.vue';
+import { blockingIssuablesQueries, issuableTypes } from '~/boards/constants';
+import { truncate } from '~/lib/utils/text_utility';
+import {
+ mockIssue,
+ mockBlockingIssue1,
+ mockBlockingIssue2,
+ mockBlockingIssuablesResponse1,
+ mockBlockingIssuablesResponse2,
+ mockBlockingIssuablesResponse3,
+ mockBlockedIssue1,
+ mockBlockedIssue2,
+} from '../mock_data';
+
+describe('BoardBlockedIcon', () => {
+ let wrapper;
+ let mockApollo;
+
+ const findGlIcon = () => wrapper.find(GlIcon);
+ const findGlPopover = () => wrapper.find(GlPopover);
+ const findGlLink = () => wrapper.find(GlLink);
+ const findPopoverTitle = () => wrapper.findByTestId('popover-title');
+ const findIssuableTitle = () => wrapper.findByTestId('issuable-title');
+ const findHiddenBlockingCount = () => wrapper.findByTestId('hidden-blocking-count');
+ const findViewAllIssuableLink = () => wrapper.findByTestId('view-all-issues');
+
+ const waitForApollo = async () => {
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
+ };
+
+ const mouseenter = async () => {
+ findGlIcon().vm.$emit('mouseenter');
+
+ await wrapper.vm.$nextTick();
+ await waitForApollo();
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const createWrapperWithApollo = ({
+ item = mockBlockedIssue1,
+ blockingIssuablesSpy = jest.fn().mockResolvedValue(mockBlockingIssuablesResponse1),
+ } = {}) => {
+ mockApollo = createMockApollo([
+ [blockingIssuablesQueries[issuableTypes.issue].query, blockingIssuablesSpy],
+ ]);
+
+ Vue.use(VueApollo);
+ wrapper = extendedWrapper(
+ mount(BoardBlockedIcon, {
+ apolloProvider: mockApollo,
+ propsData: {
+ item: {
+ ...mockIssue,
+ ...item,
+ },
+ uniqueId: 'uniqueId',
+ issuableType: issuableTypes.issue,
+ },
+ attachTo: document.body,
+ }),
+ );
+ };
+
+ const createWrapper = ({ item = {}, queries = {}, data = {}, loading = false } = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(BoardBlockedIcon, {
+ propsData: {
+ item: {
+ ...mockIssue,
+ ...item,
+ },
+ uniqueId: 'uniqueid',
+ issuableType: issuableTypes.issue,
+ },
+ data() {
+ return {
+ ...data,
+ };
+ },
+ mocks: {
+ $apollo: {
+ queries: {
+ blockingIssuables: { loading },
+ ...queries,
+ },
+ },
+ },
+ stubs: {
+ GlPopover,
+ },
+ attachTo: document.body,
+ }),
+ );
+ };
+
+ it('should render blocked icon', () => {
+ createWrapper();
+
+ expect(findGlIcon().exists()).toBe(true);
+ });
+
+ it('should display a loading spinner while loading', () => {
+ createWrapper({ loading: true });
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('should not query for blocking issuables by default', async () => {
+ createWrapperWithApollo();
+
+ expect(findGlPopover().text()).not.toContain(mockBlockingIssue1.title);
+ });
+
+ describe('on mouseenter on blocked icon', () => {
+ it('should query for blocking issuables and render the result', async () => {
+ createWrapperWithApollo();
+
+ expect(findGlPopover().text()).not.toContain(mockBlockingIssue1.title);
+
+ await mouseenter();
+
+ expect(findGlPopover().exists()).toBe(true);
+ expect(findIssuableTitle().text()).toContain(mockBlockingIssue1.title);
+ expect(wrapper.vm.skip).toBe(true);
+ });
+
+ it('should emit "blocking-issuables-error" event on query error', async () => {
+ const mockError = new Error('mayday');
+ createWrapperWithApollo({ blockingIssuablesSpy: jest.fn().mockRejectedValue(mockError) });
+
+ await mouseenter();
+
+ const [
+ [
+ {
+ message,
+ error: { networkError },
+ },
+ ],
+ ] = wrapper.emitted('blocking-issuables-error');
+ expect(message).toBe('Failed to fetch blocking issues');
+ expect(networkError).toBe(mockError);
+ });
+
+ describe('with a single blocking issue', () => {
+ beforeEach(async () => {
+ createWrapperWithApollo();
+
+ await mouseenter();
+ });
+
+ it('should render a title of the issuable', async () => {
+ expect(findIssuableTitle().text()).toBe(mockBlockingIssue1.title);
+ });
+
+ it('should render issuable reference and link to the issuable', async () => {
+ const formattedRef = mockBlockingIssue1.reference.split('/')[1];
+
+ expect(findGlLink().text()).toBe(formattedRef);
+ expect(findGlLink().attributes('href')).toBe(mockBlockingIssue1.webUrl);
+ });
+
+ it('should render popover title with correct blocking issuable count', async () => {
+ expect(findPopoverTitle().text()).toBe('Blocked by 1 issue');
+ });
+ });
+
+ describe('when issue has a long title', () => {
+ it('should render a truncated title', async () => {
+ createWrapperWithApollo({
+ blockingIssuablesSpy: jest.fn().mockResolvedValue(mockBlockingIssuablesResponse2),
+ });
+
+ await mouseenter();
+
+ const truncatedTitle = truncate(
+ mockBlockingIssue2.title,
+ wrapper.vm.$options.textTruncateWidth,
+ );
+ expect(findIssuableTitle().text()).toBe(truncatedTitle);
+ });
+ });
+
+ describe('with more than three blocking issues', () => {
+ beforeEach(async () => {
+ createWrapperWithApollo({
+ item: mockBlockedIssue2,
+ blockingIssuablesSpy: jest.fn().mockResolvedValue(mockBlockingIssuablesResponse3),
+ });
+
+ await mouseenter();
+ });
+
+ it('matches the snapshot', () => {
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+
+ it('should render popover title with correct blocking issuable count', async () => {
+ expect(findPopoverTitle().text()).toBe('Blocked by 4 issues');
+ });
+
+ it('should render the number of hidden blocking issuables', () => {
+ expect(findHiddenBlockingCount().text()).toBe('+ 1 more issue');
+ });
+
+ it('should link to the blocked issue page at the related issue anchor', async () => {
+ expect(findViewAllIssuableLink().text()).toBe('View all blocking issues');
+ expect(findViewAllIssuableLink().attributes('href')).toBe(
+ `${mockBlockedIssue2.webUrl}#related-issues`,
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js
new file mode 100644
index 00000000000..7f949739891
--- /dev/null
+++ b/spec/frontend/boards/components/board_content_sidebar_spec.js
@@ -0,0 +1,140 @@
+import { GlDrawer } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { stubComponent } from 'helpers/stub_component';
+import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
+import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
+import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
+import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
+import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue';
+import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
+import { ISSUABLE } from '~/boards/constants';
+import { mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data';
+
+describe('BoardContentSidebar', () => {
+ let wrapper;
+ let store;
+
+ const createStore = ({ mockGetters = {}, mockActions = {} } = {}) => {
+ store = new Vuex.Store({
+ state: {
+ sidebarType: ISSUABLE,
+ issues: { [mockIssue.id]: { ...mockIssue, epic: null } },
+ activeId: mockIssue.id,
+ issuableType: 'issue',
+ },
+ getters: {
+ activeBoardItem: () => {
+ return { ...mockIssue, epic: null };
+ },
+ groupPathForActiveIssue: () => mockIssueGroupPath,
+ projectPathForActiveIssue: () => mockIssueProjectPath,
+ isSidebarOpen: () => true,
+ ...mockGetters,
+ },
+ actions: mockActions,
+ });
+ };
+
+ const createComponent = () => {
+ /*
+ Dynamically imported components (in our case ee imports)
+ aren't stubbed automatically in VTU v1:
+ https://github.com/vuejs/vue-test-utils/issues/1279.
+
+ This requires us to additionally mock apollo or vuex stores.
+ */
+ wrapper = shallowMount(BoardContentSidebar, {
+ provide: {
+ canUpdate: true,
+ rootPath: '/',
+ groupId: 1,
+ },
+ store,
+ stubs: {
+ GlDrawer: stubComponent(GlDrawer, {
+ template: '<div><slot name="header"></slot><slot></slot></div>',
+ }),
+ },
+ mocks: {
+ $apollo: {
+ queries: {
+ participants: {
+ loading: false,
+ },
+ currentIteration: {
+ loading: false,
+ },
+ iterations: {
+ loading: false,
+ },
+ },
+ },
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createStore();
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('confirms we render GlDrawer', () => {
+ expect(wrapper.find(GlDrawer).exists()).toBe(true);
+ });
+
+ it('does not render GlDrawer when isSidebarOpen is false', () => {
+ createStore({ mockGetters: { isSidebarOpen: () => false } });
+ createComponent();
+
+ expect(wrapper.find(GlDrawer).exists()).toBe(false);
+ });
+
+ it('applies an open attribute', () => {
+ expect(wrapper.find(GlDrawer).props('open')).toBe(true);
+ });
+
+ it('renders BoardSidebarLabelsSelect', () => {
+ expect(wrapper.find(BoardSidebarLabelsSelect).exists()).toBe(true);
+ });
+
+ it('renders BoardSidebarTitle', () => {
+ expect(wrapper.find(BoardSidebarTitle).exists()).toBe(true);
+ });
+
+ it('renders BoardSidebarDueDate', () => {
+ expect(wrapper.find(BoardSidebarDueDate).exists()).toBe(true);
+ });
+
+ it('renders BoardSidebarSubscription', () => {
+ expect(wrapper.find(BoardSidebarSubscription).exists()).toBe(true);
+ });
+
+ it('renders BoardSidebarMilestoneSelect', () => {
+ expect(wrapper.find(BoardSidebarMilestoneSelect).exists()).toBe(true);
+ });
+
+ describe('when we emit close', () => {
+ let toggleBoardItem;
+
+ beforeEach(() => {
+ toggleBoardItem = jest.fn();
+ createStore({ mockActions: { toggleBoardItem } });
+ createComponent();
+ });
+
+ it('calls toggleBoardItem with correct parameters', async () => {
+ wrapper.find(GlDrawer).vm.$emit('close');
+
+ expect(toggleBoardItem).toHaveBeenCalledTimes(1);
+ expect(toggleBoardItem).toHaveBeenCalledWith(expect.any(Object), {
+ boardItem: { ...mockIssue, epic: null },
+ sidebarType: ISSUABLE,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
index 159b67ccc67..8c1a7bd3947 100644
--- a/spec/frontend/boards/components/board_content_spec.js
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -33,7 +33,12 @@ describe('BoardContent', () => {
});
};
- const createComponent = ({ state, props = {}, graphqlBoardListsEnabled = false } = {}) => {
+ const createComponent = ({
+ state,
+ props = {},
+ graphqlBoardListsEnabled = false,
+ canAdminList = true,
+ } = {}) => {
const store = createStore({
...defaultState,
...state,
@@ -42,11 +47,11 @@ describe('BoardContent', () => {
localVue,
propsData: {
lists: mockListsWithModel,
- canAdminList: true,
disabled: false,
...props,
},
provide: {
+ canAdminList,
glFeatures: { graphqlBoardLists: graphqlBoardListsEnabled },
},
store,
@@ -82,7 +87,7 @@ describe('BoardContent', () => {
describe('can admin list', () => {
beforeEach(() => {
- createComponent({ graphqlBoardListsEnabled: true, props: { canAdminList: true } });
+ createComponent({ graphqlBoardListsEnabled: true, canAdminList: true });
});
it('renders draggable component', () => {
@@ -92,7 +97,7 @@ describe('BoardContent', () => {
describe('can not admin list', () => {
beforeEach(() => {
- createComponent({ graphqlBoardListsEnabled: true, props: { canAdminList: false } });
+ createComponent({ graphqlBoardListsEnabled: true, canAdminList: false });
});
it('does not render draggable component', () => {
diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js
index 32499bd5480..24fcdd528d5 100644
--- a/spec/frontend/boards/components/board_form_spec.js
+++ b/spec/frontend/boards/components/board_form_spec.js
@@ -226,7 +226,7 @@ describe('BoardForm', () => {
it('passes correct primary action text and variant', () => {
expect(findModalActionPrimary().text).toBe('Save changes');
- expect(findModalActionPrimary().attributes[0].variant).toBe('info');
+ expect(findModalActionPrimary().attributes[0].variant).toBe('confirm');
});
it('does not render delete confirmation message', () => {
diff --git a/spec/frontend/boards/components/board_new_issue_spec.js b/spec/frontend/boards/components/board_new_issue_spec.js
index 737a18294bc..e6405bbcff3 100644
--- a/spec/frontend/boards/components/board_new_issue_spec.js
+++ b/spec/frontend/boards/components/board_new_issue_spec.js
@@ -86,7 +86,7 @@ describe('Issue boards new issue form', () => {
describe('submit success', () => {
it('creates new issue', async () => {
- wrapper.setData({ title: 'submit issue' });
+ wrapper.setData({ title: 'create issue' });
await vm.$nextTick();
await submitIssue();
@@ -95,7 +95,7 @@ describe('Issue boards new issue form', () => {
it('enables button after submit', async () => {
jest.spyOn(wrapper.vm, 'submit').mockImplementation();
- wrapper.setData({ title: 'submit issue' });
+ wrapper.setData({ title: 'create issue' });
await vm.$nextTick();
await submitIssue();
@@ -103,7 +103,7 @@ describe('Issue boards new issue form', () => {
});
it('clears title after submit', async () => {
- wrapper.setData({ title: 'submit issue' });
+ wrapper.setData({ title: 'create issue' });
await vm.$nextTick();
await submitIssue();
diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js
index 52b4d71f7b9..464331b6e30 100644
--- a/spec/frontend/boards/components/board_settings_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js
@@ -4,6 +4,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import Vuex from 'vuex';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
import { inactiveId, LIST } from '~/boards/constants';
import { createStore } from '~/boards/stores';
@@ -22,11 +23,18 @@ describe('BoardSettingsSidebar', () => {
const labelColor = '#FFFF';
const listId = 1;
- const createComponent = () => {
- wrapper = shallowMount(BoardSettingsSidebar, {
- store,
- localVue,
- });
+ const findRemoveButton = () => wrapper.findByTestId('remove-list');
+
+ const createComponent = ({ canAdminList = false } = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(BoardSettingsSidebar, {
+ store,
+ localVue,
+ provide: {
+ canAdminList,
+ },
+ }),
+ );
};
const findLabel = () => wrapper.find(GlLabel);
const findDrawer = () => wrapper.find(GlDrawer);
@@ -164,4 +172,29 @@ describe('BoardSettingsSidebar', () => {
expect(findDrawer().exists()).toBe(false);
});
});
+
+ it('does not render "Remove list" when user cannot admin the boards list', () => {
+ createComponent();
+
+ expect(findRemoveButton().exists()).toBe(false);
+ });
+
+ describe('when user can admin the boards list', () => {
+ beforeEach(() => {
+ store.state.activeId = listId;
+ store.state.sidebarType = LIST;
+
+ boardsStore.addList({
+ id: listId,
+ label: { title: labelTitle, color: labelColor },
+ list_type: 'label',
+ });
+
+ createComponent({ canAdminList: true });
+ });
+
+ it('renders "Remove list" button', () => {
+ expect(findRemoveButton().exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/boards/components/filtered_search_spec.js b/spec/frontend/boards/components/filtered_search_spec.js
deleted file mode 100644
index 7f238aa671f..00000000000
--- a/spec/frontend/boards/components/filtered_search_spec.js
+++ /dev/null
@@ -1,65 +0,0 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import Vuex from 'vuex';
-import FilteredSearch from '~/boards/components/filtered_search.vue';
-import { createStore } from '~/boards/stores';
-import * as commonUtils from '~/lib/utils/common_utils';
-import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-describe('FilteredSearch', () => {
- let wrapper;
- let store;
-
- const createComponent = () => {
- wrapper = shallowMount(FilteredSearch, {
- localVue,
- propsData: { search: '' },
- store,
- attachTo: document.body,
- });
- };
-
- beforeEach(() => {
- // this needed for actions call for performSearch
- window.gon = { features: {} };
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('default', () => {
- beforeEach(() => {
- store = createStore();
-
- jest.spyOn(store, 'dispatch');
-
- createComponent();
- });
-
- it('finds FilteredSearch', () => {
- expect(wrapper.find(FilteredSearchBarRoot).exists()).toBe(true);
- });
-
- describe('when onFilter is emitted', () => {
- it('calls performSearch', () => {
- wrapper.find(FilteredSearchBarRoot).vm.$emit('onFilter', [{ value: { data: '' } }]);
-
- expect(store.dispatch).toHaveBeenCalledWith('performSearch');
- });
-
- it('calls historyPushState', () => {
- commonUtils.historyPushState = jest.fn();
- wrapper
- .find(FilteredSearchBarRoot)
- .vm.$emit('onFilter', [{ value: { data: 'searchQuery' } }]);
-
- expect(commonUtils.historyPushState).toHaveBeenCalledWith(
- 'http://test.host/?search=searchQuery',
- );
- });
- });
- });
-});
diff --git a/spec/frontend/boards/components/issue_time_estimate_spec.js b/spec/frontend/boards/components/issue_time_estimate_spec.js
index 2e253d24125..635964b6b4a 100644
--- a/spec/frontend/boards/components/issue_time_estimate_spec.js
+++ b/spec/frontend/boards/components/issue_time_estimate_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { config as vueConfig } from 'vue';
+import Vue from 'vue';
import IssueTimeEstimate from '~/boards/components/issue_time_estimate.vue';
describe('Issue Time Estimate component', () => {
@@ -34,10 +34,10 @@ describe('Issue Time Estimate component', () => {
try {
// This will raise props validating warning by Vue, silencing it
- vueConfig.silent = true;
+ Vue.config.silent = true;
await wrapper.setProps({ estimate: 'Foo <script>alert("XSS")</script>' });
} finally {
- vueConfig.silent = false;
+ Vue.config.silent = false;
}
expect(alertSpy).not.toHaveBeenCalled();
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js
index 98ac211238c..153d0640b23 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js
@@ -64,7 +64,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => {
beforeEach(async () => {
createWrapper();
- jest.spyOn(wrapper.vm, 'setActiveIssueLabels').mockImplementation(() => TEST_LABELS);
+ jest.spyOn(wrapper.vm, 'setActiveBoardItemLabels').mockImplementation(() => TEST_LABELS);
findLabelsSelect().vm.$emit('updateSelectedLabels', TEST_LABELS_PAYLOAD);
store.state.boardItems[TEST_ISSUE.id].labels = TEST_LABELS;
await wrapper.vm.$nextTick();
@@ -76,7 +76,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => {
});
it('commits change to the server', () => {
- expect(wrapper.vm.setActiveIssueLabels).toHaveBeenCalledWith({
+ expect(wrapper.vm.setActiveBoardItemLabels).toHaveBeenCalledWith({
addLabelIds: TEST_LABELS.map((label) => label.id),
projectPath: 'gitlab-org/test-subgroup/gitlab-test',
removeLabelIds: [],
@@ -94,13 +94,13 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => {
beforeEach(async () => {
createWrapper({ labels: TEST_LABELS });
- jest.spyOn(wrapper.vm, 'setActiveIssueLabels').mockImplementation(() => expectedLabels);
+ jest.spyOn(wrapper.vm, 'setActiveBoardItemLabels').mockImplementation(() => expectedLabels);
findLabelsSelect().vm.$emit('updateSelectedLabels', testLabelsPayload);
await wrapper.vm.$nextTick();
});
it('commits change to the server', () => {
- expect(wrapper.vm.setActiveIssueLabels).toHaveBeenCalledWith({
+ expect(wrapper.vm.setActiveBoardItemLabels).toHaveBeenCalledWith({
addLabelIds: [5, 7],
removeLabelIds: [6],
projectPath: 'gitlab-org/test-subgroup/gitlab-test',
@@ -114,13 +114,13 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => {
beforeEach(async () => {
createWrapper({ labels: [testLabel] });
- jest.spyOn(wrapper.vm, 'setActiveIssueLabels').mockImplementation(() => {});
+ jest.spyOn(wrapper.vm, 'setActiveBoardItemLabels').mockImplementation(() => {});
});
it('commits change to the server', () => {
wrapper.find(GlLabel).vm.$emit('close', testLabel);
- expect(wrapper.vm.setActiveIssueLabels).toHaveBeenCalledWith({
+ expect(wrapper.vm.setActiveBoardItemLabels).toHaveBeenCalledWith({
removeLabelIds: [getIdFromGraphQLId(testLabel.id)],
projectPath: 'gitlab-org/test-subgroup/gitlab-test',
});
@@ -131,7 +131,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => {
beforeEach(async () => {
createWrapper({ labels: TEST_LABELS });
- jest.spyOn(wrapper.vm, 'setActiveIssueLabels').mockImplementation(() => {
+ jest.spyOn(wrapper.vm, 'setActiveBoardItemLabels').mockImplementation(() => {
throw new Error(['failed mutation']);
});
findLabelsSelect().vm.$emit('updateSelectedLabels', [{ id: '?' }]);
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js
index cfd7f32b2cc..7976e73ff2f 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js
@@ -1,5 +1,6 @@
import { GlToggle, GlLoadingIcon } from '@gitlab/ui';
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue';
import { createStore } from '~/boards/stores';
@@ -9,8 +10,7 @@ import { mockActiveIssue } from '../../mock_data';
jest.mock('~/flash.js');
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () => {
let wrapper;
@@ -20,14 +20,16 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () =
const findToggle = () => wrapper.find(GlToggle);
const findGlLoadingIcon = () => wrapper.find(GlLoadingIcon);
- const createComponent = (activeIssue = { ...mockActiveIssue }) => {
+ const createComponent = (activeBoardItem = { ...mockActiveIssue }) => {
store = createStore();
- store.state.boardItems = { [activeIssue.id]: activeIssue };
- store.state.activeId = activeIssue.id;
+ store.state.boardItems = { [activeBoardItem.id]: activeBoardItem };
+ store.state.activeId = activeBoardItem.id;
wrapper = mount(BoardSidebarSubscription, {
- localVue,
store,
+ provide: {
+ emailsDisabled: false,
+ },
});
};
@@ -90,9 +92,9 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () =
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,
+ jest.spyOn(wrapper.vm, 'setActiveItemSubscribed').mockImplementation(async () => {
+ store.commit(types.UPDATE_BOARD_ITEM_BY_ID, {
+ itemId: mockActiveIssue.id,
prop: 'subscribed',
value: subscribedState,
});
@@ -110,7 +112,7 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () =
await wrapper.vm.$nextTick();
expect(findGlLoadingIcon().exists()).toBe(true);
- expect(wrapper.vm.setActiveIssueSubscribed).toHaveBeenCalledWith({
+ expect(wrapper.vm.setActiveItemSubscribed).toHaveBeenCalledWith({
subscribed: true,
projectPath: 'gitlab-org/test-subgroup/gitlab-test',
});
@@ -134,7 +136,7 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () =
await wrapper.vm.$nextTick();
- expect(wrapper.vm.setActiveIssueSubscribed).toHaveBeenCalledWith({
+ expect(wrapper.vm.setActiveItemSubscribed).toHaveBeenCalledWith({
subscribed: false,
projectPath: 'gitlab-org/test-subgroup/gitlab-test',
});
@@ -148,7 +150,7 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () =
it('flashes an error message when setting the subscribed state fails', async () => {
createComponent();
- jest.spyOn(wrapper.vm, 'setActiveIssueSubscribed').mockImplementation(async () => {
+ jest.spyOn(wrapper.vm, 'setActiveItemSubscribed').mockImplementation(async () => {
throw new Error();
});
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js
new file mode 100644
index 00000000000..03924bfa8d3
--- /dev/null
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js
@@ -0,0 +1,58 @@
+/*
+ To avoid duplicating tests in time_tracker.spec,
+ this spec only contains a simple test to check rendering.
+
+ A detailed feature spec is used to test time tracking feature
+ in swimlanes sidebar.
+*/
+
+import { shallowMount } from '@vue/test-utils';
+import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue';
+import { createStore } from '~/boards/stores';
+import IssuableTimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue';
+
+describe('BoardSidebarTimeTracker', () => {
+ let wrapper;
+ let store;
+
+ const createComponent = (options) => {
+ wrapper = shallowMount(BoardSidebarTimeTracker, {
+ store,
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ store = createStore();
+ store.state.boardItems = {
+ 1: {
+ timeEstimate: 3600,
+ totalTimeSpent: 1800,
+ humanTimeEstimate: '1h',
+ humanTotalTimeSpent: '30min',
+ },
+ };
+ store.state.activeId = '1';
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it.each([[true], [false]])(
+ 'renders IssuableTimeTracker with correct spent and estimated time (timeTrackingLimitToHours=%s)',
+ (timeTrackingLimitToHours) => {
+ createComponent({ provide: { timeTrackingLimitToHours } });
+
+ expect(wrapper.find(IssuableTimeTracker).props()).toEqual({
+ timeEstimate: 3600,
+ timeSpent: 1800,
+ humanTimeEstimate: '1h',
+ humanTimeSpent: '30min',
+ limitToHours: timeTrackingLimitToHours,
+ showCollapsed: false,
+ });
+ },
+ );
+});
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_issue_title_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
index 723d0345f76..c8ccd4c88a5 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_issue_title_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
@@ -1,11 +1,11 @@
import { GlAlert, GlFormInput, GlForm } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
-import BoardSidebarIssueTitle from '~/boards/components/sidebar/board_sidebar_issue_title.vue';
+import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { createStore } from '~/boards/stores';
import createFlash from '~/flash';
-const TEST_TITLE = 'New issue title';
+const TEST_TITLE = 'New item title';
const TEST_ISSUE_A = {
id: 'gid://gitlab/Issue/1',
iid: 8,
@@ -21,7 +21,7 @@ const TEST_ISSUE_B = {
jest.mock('~/flash');
-describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
+describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
let wrapper;
let store;
@@ -32,12 +32,12 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
wrapper = null;
});
- const createWrapper = (issue = TEST_ISSUE_A) => {
+ const createWrapper = (item = TEST_ISSUE_A) => {
store = createStore();
- store.state.boardItems = { [issue.id]: { ...issue } };
- store.dispatch('setActiveId', { id: issue.id });
+ store.state.boardItems = { [item.id]: { ...item } };
+ store.dispatch('setActiveId', { id: item.id });
- wrapper = shallowMount(BoardSidebarIssueTitle, {
+ wrapper = shallowMount(BoardSidebarTitle, {
store,
provide: {
canUpdate: true,
@@ -53,7 +53,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
const findFormInput = () => wrapper.find(GlFormInput);
const findEditableItem = () => wrapper.find(BoardEditableItem);
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
- const findTitle = () => wrapper.find('[data-testid="issue-title"]');
+ const findTitle = () => wrapper.find('[data-testid="item-title"]');
const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
it('renders title and reference', () => {
@@ -73,7 +73,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
beforeEach(async () => {
createWrapper();
- jest.spyOn(wrapper.vm, 'setActiveIssueTitle').mockImplementation(() => {
+ jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {
store.state.boardItems[TEST_ISSUE_A.id].title = TEST_TITLE;
});
findFormInput().vm.$emit('input', TEST_TITLE);
@@ -87,7 +87,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
});
it('commits change to the server', () => {
- expect(wrapper.vm.setActiveIssueTitle).toHaveBeenCalledWith({
+ expect(wrapper.vm.setActiveItemTitle).toHaveBeenCalledWith({
title: TEST_TITLE,
projectPath: 'h/b',
});
@@ -98,14 +98,14 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
beforeEach(async () => {
createWrapper();
- jest.spyOn(wrapper.vm, 'setActiveIssueTitle').mockImplementation(() => {});
+ jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {});
findFormInput().vm.$emit('input', '');
findForm().vm.$emit('submit', { preventDefault: () => {} });
await wrapper.vm.$nextTick();
});
it('commits change to the server', () => {
- expect(wrapper.vm.setActiveIssueTitle).not.toHaveBeenCalled();
+ expect(wrapper.vm.setActiveItemTitle).not.toHaveBeenCalled();
});
});
@@ -122,7 +122,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
it('does not collapses sidebar and shows alert', () => {
expect(findCollapsed().isVisible()).toBe(false);
expect(findAlert().exists()).toBe(true);
- expect(localStorage.getItem(`${TEST_ISSUE_A.id}/issue-title-pending-changes`)).toBe(
+ expect(localStorage.getItem(`${TEST_ISSUE_A.id}/item-title-pending-changes`)).toBe(
TEST_TITLE,
);
});
@@ -130,7 +130,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
describe('when accessing the form with pending changes', () => {
beforeAll(() => {
- localStorage.setItem(`${TEST_ISSUE_A.id}/issue-title-pending-changes`, TEST_TITLE);
+ localStorage.setItem(`${TEST_ISSUE_A.id}/item-title-pending-changes`, TEST_TITLE);
createWrapper();
});
@@ -146,7 +146,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
beforeEach(async () => {
createWrapper(TEST_ISSUE_B);
- jest.spyOn(wrapper.vm, 'setActiveIssueTitle').mockImplementation(() => {
+ jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {
store.state.boardItems[TEST_ISSUE_B.id].title = TEST_TITLE;
});
findFormInput().vm.$emit('input', TEST_TITLE);
@@ -155,7 +155,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
});
it('collapses sidebar and render former title', () => {
- expect(wrapper.vm.setActiveIssueTitle).not.toHaveBeenCalled();
+ expect(wrapper.vm.setActiveItemTitle).not.toHaveBeenCalled();
expect(findCollapsed().isVisible()).toBe(true);
expect(findTitle().text()).toBe(TEST_ISSUE_B.title);
});
@@ -165,7 +165,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
beforeEach(async () => {
createWrapper(TEST_ISSUE_B);
- jest.spyOn(wrapper.vm, 'setActiveIssueTitle').mockImplementation(() => {
+ jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {
throw new Error(['failed mutation']);
});
findFormInput().vm.$emit('input', 'Invalid title');
@@ -173,7 +173,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
await wrapper.vm.$nextTick();
});
- it('collapses sidebar and renders former issue title', () => {
+ it('collapses sidebar and renders former item title', () => {
expect(findCollapsed().isVisible()).toBe(true);
expect(findTitle().text()).toContain(TEST_ISSUE_B.title);
expect(createFlash).toHaveBeenCalled();
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index 500240d00fc..1c5b7cf8248 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -3,6 +3,7 @@
import { keyBy } from 'lodash';
import Vue from 'vue';
import '~/boards/models/list';
+import { ListType } from '~/boards/constants';
import boardsStore from '~/boards/stores/boards_store';
export const boardObj = {
@@ -125,7 +126,7 @@ export const labels = [
export const rawIssue = {
title: 'Issue 1',
id: 'gid://gitlab/Issue/436',
- iid: 27,
+ iid: '27',
dueDate: null,
timeEstimate: 0,
weight: null,
@@ -152,7 +153,7 @@ export const rawIssue = {
export const mockIssue = {
id: 'gid://gitlab/Issue/436',
- iid: 27,
+ iid: '27',
title: 'Issue 1',
dueDate: null,
timeEstimate: 0,
@@ -398,3 +399,128 @@ export const mockActiveGroupProjects = [
{ ...mockGroupProject1, archived: false },
{ ...mockGroupProject2, archived: false },
];
+
+export const mockIssueGroupPath = 'gitlab-org';
+export const mockIssueProjectPath = `${mockIssueGroupPath}/gitlab-test`;
+
+export const mockBlockingIssue1 = {
+ id: 'gid://gitlab/Issue/525',
+ iid: '6',
+ title: 'blocking issue title 1',
+ reference: 'gitlab-org/my-project-1#6',
+ webUrl: 'http://gdk.test:3000/gitlab-org/my-project-1/-/issues/6',
+ __typename: 'Issue',
+};
+
+export const mockBlockingIssue2 = {
+ id: 'gid://gitlab/Issue/524',
+ iid: '5',
+ title:
+ 'blocking issue title 2 + blocking issue title 2 + blocking issue title 2 + blocking issue title 2',
+ reference: 'gitlab-org/my-project-1#5',
+ webUrl: 'http://gdk.test:3000/gitlab-org/my-project-1/-/issues/5',
+ __typename: 'Issue',
+};
+
+export const mockBlockingIssue3 = {
+ id: 'gid://gitlab/Issue/523',
+ iid: '4',
+ title: 'blocking issue title 3',
+ reference: 'gitlab-org/my-project-1#4',
+ webUrl: 'http://gdk.test:3000/gitlab-org/my-project-1/-/issues/4',
+ __typename: 'Issue',
+};
+
+export const mockBlockingIssue4 = {
+ id: 'gid://gitlab/Issue/522',
+ iid: '3',
+ title: 'blocking issue title 4',
+ reference: 'gitlab-org/my-project-1#3',
+ webUrl: 'http://gdk.test:3000/gitlab-org/my-project-1/-/issues/3',
+ __typename: 'Issue',
+};
+
+export const mockBlockingIssuablesResponse1 = {
+ data: {
+ issuable: {
+ __typename: 'Issue',
+ id: 'gid://gitlab/Issue/527',
+ blockingIssuables: {
+ __typename: 'IssueConnection',
+ nodes: [mockBlockingIssue1],
+ },
+ },
+ },
+};
+
+export const mockBlockingIssuablesResponse2 = {
+ data: {
+ issuable: {
+ __typename: 'Issue',
+ id: 'gid://gitlab/Issue/527',
+ blockingIssuables: {
+ __typename: 'IssueConnection',
+ nodes: [mockBlockingIssue2],
+ },
+ },
+ },
+};
+
+export const mockBlockingIssuablesResponse3 = {
+ data: {
+ issuable: {
+ __typename: 'Issue',
+ id: 'gid://gitlab/Issue/527',
+ blockingIssuables: {
+ __typename: 'IssueConnection',
+ nodes: [mockBlockingIssue1, mockBlockingIssue2, mockBlockingIssue3, mockBlockingIssue4],
+ },
+ },
+ },
+};
+
+export const mockBlockedIssue1 = {
+ id: '527',
+ blockedByCount: 1,
+};
+
+export const mockBlockedIssue2 = {
+ id: '527',
+ blockedByCount: 4,
+ webUrl: 'http://gdk.test:3000/gitlab-org/my-project-1/-/issues/0',
+};
+
+export const mockMoveIssueParams = {
+ itemId: 1,
+ fromListId: 'gid://gitlab/List/1',
+ toListId: 'gid://gitlab/List/2',
+ moveBeforeId: undefined,
+ moveAfterId: undefined,
+};
+
+export const mockMoveState = {
+ boardLists: {
+ 'gid://gitlab/List/1': {
+ listType: ListType.backlog,
+ },
+ 'gid://gitlab/List/2': {
+ listType: ListType.closed,
+ },
+ },
+ boardItems: {
+ [mockMoveIssueParams.itemId]: { foo: 'bar' },
+ },
+ boardItemsByListId: {
+ [mockMoveIssueParams.fromListId]: [mockMoveIssueParams.itemId],
+ [mockMoveIssueParams.toListId]: [],
+ },
+};
+
+export const mockMoveData = {
+ reordering: false,
+ shouldClone: false,
+ itemNotInToList: true,
+ originalIndex: 0,
+ originalIssue: { foo: 'bar' },
+ ...mockMoveIssueParams,
+};
diff --git a/spec/frontend/boards/modal_store_spec.js b/spec/frontend/boards/modal_store_spec.js
deleted file mode 100644
index 5b5ae4b6556..00000000000
--- a/spec/frontend/boards/modal_store_spec.js
+++ /dev/null
@@ -1,134 +0,0 @@
-/* global ListIssue */
-
-import '~/boards/models/label';
-import '~/boards/models/assignee';
-import '~/boards/models/issue';
-import '~/boards/models/list';
-import Store from '~/boards/stores/modal_store';
-
-describe('Modal store', () => {
- let issue;
- let issue2;
-
- beforeEach(() => {
- // Set up default state
- Store.store.issues = [];
- Store.store.selectedIssues = [];
-
- issue = new ListIssue({
- title: 'Testing',
- id: 1,
- iid: 1,
- confidential: false,
- labels: [],
- assignees: [],
- });
- issue2 = new ListIssue({
- title: 'Testing',
- id: 2,
- iid: 2,
- confidential: false,
- labels: [],
- assignees: [],
- });
- Store.store.issues.push(issue);
- Store.store.issues.push(issue2);
- });
-
- it('returns selected count', () => {
- expect(Store.selectedCount()).toBe(0);
- });
-
- it('toggles the issue as selected', () => {
- Store.toggleIssue(issue);
-
- expect(issue.selected).toBe(true);
- expect(Store.selectedCount()).toBe(1);
- });
-
- it('toggles the issue as un-selected', () => {
- Store.toggleIssue(issue);
- Store.toggleIssue(issue);
-
- expect(issue.selected).toBe(false);
- expect(Store.selectedCount()).toBe(0);
- });
-
- it('toggles all issues as selected', () => {
- Store.toggleAll();
-
- expect(issue.selected).toBe(true);
- expect(issue2.selected).toBe(true);
- expect(Store.selectedCount()).toBe(2);
- });
-
- it('toggles all issues as un-selected', () => {
- Store.toggleAll();
- Store.toggleAll();
-
- expect(issue.selected).toBe(false);
- expect(issue2.selected).toBe(false);
- expect(Store.selectedCount()).toBe(0);
- });
-
- it('toggles all if a single issue is selected', () => {
- Store.toggleIssue(issue);
- Store.toggleAll();
-
- expect(issue.selected).toBe(true);
- expect(issue2.selected).toBe(true);
- expect(Store.selectedCount()).toBe(2);
- });
-
- it('adds issue to selected array', () => {
- issue.selected = true;
- Store.addSelectedIssue(issue);
-
- expect(Store.selectedCount()).toBe(1);
- });
-
- it('removes issue from selected array', () => {
- Store.addSelectedIssue(issue);
- Store.removeSelectedIssue(issue);
-
- expect(Store.selectedCount()).toBe(0);
- });
-
- it('returns selected issue index if present', () => {
- Store.toggleIssue(issue);
-
- expect(Store.selectedIssueIndex(issue)).toBe(0);
- });
-
- it('returns -1 if issue is not selected', () => {
- expect(Store.selectedIssueIndex(issue)).toBe(-1);
- });
-
- it('finds the selected issue', () => {
- Store.toggleIssue(issue);
-
- expect(Store.findSelectedIssue(issue)).toBe(issue);
- });
-
- it('does not find a selected issue', () => {
- expect(Store.findSelectedIssue(issue)).toBe(undefined);
- });
-
- it('does not remove from selected issue if tab is not all', () => {
- Store.store.activeTab = 'selected';
-
- Store.toggleIssue(issue);
- Store.toggleIssue(issue);
-
- expect(Store.store.selectedIssues.length).toBe(1);
- expect(Store.selectedCount()).toBe(0);
- });
-
- it('gets selected issue array with only selected issues', () => {
- Store.toggleIssue(issue);
- Store.toggleIssue(issue2);
- Store.toggleIssue(issue2);
-
- expect(Store.getSelectedIssues().length).toBe(1);
- });
-});
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index 69d2c8977fb..460e77a3f03 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -1,16 +1,21 @@
+import * as Sentry from '@sentry/browser';
+import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
import testAction from 'helpers/vuex_action_helper';
import {
fullBoardId,
formatListIssues,
formatBoardLists,
formatIssueInput,
+ formatIssue,
+ getMoveData,
} from '~/boards/boards_util';
-import { inactiveId, ISSUABLE } from '~/boards/constants';
+import { inactiveId, ISSUABLE, ListType } from '~/boards/constants';
import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql';
import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql';
-import issueMoveListMutation from '~/boards/graphql/issue_move_list.mutation.graphql';
import actions, { gqlClient } from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+
import {
mockLists,
mockListsById,
@@ -22,6 +27,9 @@ import {
labels,
mockActiveIssue,
mockGroupProjects,
+ mockMoveIssueParams,
+ mockMoveState,
+ mockMoveData,
} from '../mock_data';
jest.mock('~/flash');
@@ -638,73 +646,314 @@ describe('resetIssues', () => {
});
describe('moveItem', () => {
- it('should dispatch moveIssue action', () => {
+ it('should dispatch moveIssue action with payload', () => {
+ const payload = { mock: 'payload' };
+
testAction({
action: actions.moveItem,
- expectedActions: [{ type: 'moveIssue' }],
+ payload,
+ expectedActions: [{ type: 'moveIssue', payload }],
});
});
});
describe('moveIssue', () => {
- const listIssues = {
- 'gid://gitlab/List/1': [436, 437],
- 'gid://gitlab/List/2': [],
- };
-
- const issues = {
- 436: mockIssue,
- 437: mockIssue2,
- };
-
- const state = {
- fullPath: 'gitlab-org',
- boardId: '1',
- boardType: 'group',
- disabled: false,
- boardLists: mockLists,
- boardItemsByListId: listIssues,
- boardItems: issues,
- };
+ 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 } },
+ ],
+ });
+ });
+});
- it('should commit MOVE_ISSUE mutation and MOVE_ISSUE_SUCCESS mutation when successful', (done) => {
- jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
- data: {
- issueMoveList: {
- issue: rawIssue,
- errors: [],
+describe('moveIssueCard and undoMoveIssueCard', () => {
+ describe('card should move without clonning', () => {
+ 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,
+ });
+ });
});
- testAction(
- actions.moveIssue,
- {
- itemId: '436',
- itemIid: mockIssue.iid,
- itemPath: mockIssue.referencePath,
- fromListId: 'gid://gitlab/List/1',
- toListId: 'gid://gitlab/List/2',
- },
- state,
+ describe.each([
[
+ 'issue moves out of backlog',
{
- type: types.MOVE_ISSUE,
- payload: {
- originalIssue: mockIssue,
- fromListId: 'gid://gitlab/List/1',
- toListId: 'gid://gitlab/List/2',
- },
+ fromListType: ListType.backlog,
+ toListType: ListType.label,
},
+ ],
+ [
+ 'issue card moves to closed',
{
- type: types.MOVE_ISSUE_SUCCESS,
- payload: { issue: rawIssue },
+ fromListType: ListType.label,
+ toListType: ListType.closed,
},
],
- [],
- done,
- );
+ [
+ '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 = {
+ 436: mockIssue,
+ 437: mockIssue2,
+ };
+
+ const state = {
+ boardItems: issues,
+ boardId: 'gid://gitlab/Board/1',
+ };
+
+ const moveData = {
+ itemId: 436,
+ fromListId: 'gid://gitlab/List/1',
+ toListId: 'gid://gitlab/List/2',
+ };
it('calls mutate with the correct variables', () => {
const mutationVariables = {
@@ -728,61 +977,56 @@ describe('moveIssue', () => {
},
});
- actions.moveIssue(
- { state, commit: () => {} },
- {
- itemId: mockIssue.id,
- itemIid: mockIssue.iid,
- itemPath: mockIssue.referencePath,
- fromListId: 'gid://gitlab/List/1',
- toListId: 'gid://gitlab/List/2',
- },
- );
+ actions.updateIssueOrder({ state, commit: () => {}, dispatch: () => {} }, { moveData });
expect(gqlClient.mutate).toHaveBeenCalledWith(mutationVariables);
});
- it('should commit MOVE_ISSUE mutation and MOVE_ISSUE_FAILURE mutation when unsuccessful', (done) => {
+ it('should commit MUTATE_ISSUE_SUCCESS mutation when successful', () => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
issueMoveList: {
- issue: {},
- errors: [{ foo: 'bar' }],
+ issue: rawIssue,
+ errors: [],
},
},
});
testAction(
- actions.moveIssue,
- {
- itemId: '436',
- itemIid: mockIssue.iid,
- itemPath: mockIssue.referencePath,
- fromListId: 'gid://gitlab/List/1',
- toListId: 'gid://gitlab/List/2',
- },
+ actions.updateIssueOrder,
+ { moveData },
state,
[
{
- type: types.MOVE_ISSUE,
- payload: {
- originalIssue: mockIssue,
- fromListId: 'gid://gitlab/List/1',
- toListId: 'gid://gitlab/List/2',
- },
+ 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.MOVE_ISSUE_FAILURE,
- payload: {
- originalIssue: mockIssue,
- fromListId: 'gid://gitlab/List/1',
- toListId: 'gid://gitlab/List/2',
- originalIndex: 0,
- },
+ type: types.SET_ERROR,
+ payload: 'An error occurred while moving the issue. Please try again.',
},
],
- [],
- done,
+ [{ type: 'undoMoveIssueCard', payload: moveData }],
);
});
});
@@ -798,11 +1042,11 @@ describe('setAssignees', () => {
testAction(
actions.setAssignees,
[node],
- { activeIssue: { iid, referencePath: refPath }, commit: () => {} },
+ { activeBoardItem: { iid, referencePath: refPath }, commit: () => {} },
[
{
- type: 'UPDATE_ISSUE_BY_ID',
- payload: { prop: 'assignees', issueId: undefined, value: [node] },
+ type: 'UPDATE_BOARD_ITEM_BY_ID',
+ payload: { prop: 'assignees', itemId: undefined, value: [node] },
},
],
[],
@@ -812,7 +1056,43 @@ describe('setAssignees', () => {
});
});
-describe('createNewIssue', () => {
+describe('addListItem', () => {
+ it('should commit ADD_BOARD_ITEM_TO_LIST and UPDATE_BOARD_ITEM mutations', () => {
+ 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,
+ },
+ },
+ { type: types.UPDATE_BOARD_ITEM, payload: mockIssue },
+ ]);
+ });
+});
+
+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',
@@ -839,19 +1119,7 @@ describe('createNewIssue', () => {
},
};
- 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);
- });
+ const fakeList = { id: 'gid://gitlab/List/123' };
it('should add board scope to the issue being created', async () => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
@@ -863,7 +1131,11 @@ describe('createNewIssue', () => {
},
});
- await actions.createNewIssue({ state: stateWithBoardConfig }, mockIssue);
+ await actions.addListNewIssue(
+ { dispatch: jest.fn(), commit: jest.fn(), state: stateWithBoardConfig },
+ { issueInput: mockIssue, list: fakeList },
+ );
+
expect(gqlClient.mutate).toHaveBeenCalledWith({
mutation: issueCreateMutation,
variables: {
@@ -890,7 +1162,11 @@ describe('createNewIssue', () => {
const payload = formatIssueInput(issue, stateWithBoardConfig.boardConfig);
- await actions.createNewIssue({ state: stateWithBoardConfig }, issue);
+ await actions.addListNewIssue(
+ { dispatch: jest.fn(), commit: jest.fn(), state: stateWithBoardConfig },
+ { issueInput: issue, list: fakeList },
+ );
+
expect(gqlClient.mutate).toHaveBeenCalledWith({
mutation: issueCreateMutation,
variables: {
@@ -901,51 +1177,92 @@ describe('createNewIssue', () => {
expect(payload.assigneeIds).toEqual(['gid://gitlab/User/1', 'gid://gitlab/User/2']);
});
- it('should commit CREATE_ISSUE_FAILURE mutation when API returns an error', (done) => {
- jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
- data: {
- createIssue: {
- issue: mockIssue,
- errors: [{ foo: 'bar' }],
+ 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' }),
+ position: 0,
+ },
+ },
+ { type: 'removeListItem', payload: { listId: fakeList.id, itemId: 'tmp' } },
+ {
+ type: 'addListItem',
+ payload: {
+ list: fakeList,
+ item: formatIssue({ ...mockIssue, id: getIdFromGraphQLId(mockIssue.id) }),
+ position: 0,
+ },
+ },
+ ],
+ });
});
-
- const payload = mockIssue;
-
- testAction(
- actions.createNewIssue,
- payload,
- state,
- [{ type: types.CREATE_ISSUE_FAILURE }],
- [],
- done,
- );
});
-});
-
-describe('addListIssue', () => {
- it('should commit ADD_ISSUE_TO_LIST mutation', (done) => {
- const payload = {
- list: mockLists[0],
- issue: mockIssue,
- position: 0,
- };
- testAction(
- actions.addListIssue,
- payload,
- {},
- [{ type: types.ADD_ISSUE_TO_LIST, payload }],
- [],
- done,
- );
+ 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' }),
+ position: 0,
+ },
+ },
+ { 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 = { activeIssue: mockIssue };
+ const getters = { activeBoardItem: mockIssue };
const testLabelIds = labels.map((label) => label.id);
const input = {
addLabelIds: testLabelIds,
@@ -959,7 +1276,7 @@ describe('setActiveIssueLabels', () => {
.mockResolvedValue({ data: { updateIssue: { issue: { labels: { nodes: labels } } } } });
const payload = {
- issueId: getters.activeIssue.id,
+ itemId: getters.activeBoardItem.id,
prop: 'labels',
value: labels,
};
@@ -970,7 +1287,7 @@ describe('setActiveIssueLabels', () => {
{ ...state, ...getters },
[
{
- type: types.UPDATE_ISSUE_BY_ID,
+ type: types.UPDATE_BOARD_ITEM_BY_ID,
payload,
},
],
@@ -990,7 +1307,7 @@ describe('setActiveIssueLabels', () => {
describe('setActiveIssueDueDate', () => {
const state = { boardItems: { [mockIssue.id]: mockIssue } };
- const getters = { activeIssue: mockIssue };
+ const getters = { activeBoardItem: mockIssue };
const testDueDate = '2020-02-20';
const input = {
dueDate: testDueDate,
@@ -1010,7 +1327,7 @@ describe('setActiveIssueDueDate', () => {
});
const payload = {
- issueId: getters.activeIssue.id,
+ itemId: getters.activeBoardItem.id,
prop: 'dueDate',
value: testDueDate,
};
@@ -1021,7 +1338,7 @@ describe('setActiveIssueDueDate', () => {
{ ...state, ...getters },
[
{
- type: types.UPDATE_ISSUE_BY_ID,
+ type: types.UPDATE_BOARD_ITEM_BY_ID,
payload,
},
],
@@ -1039,9 +1356,15 @@ describe('setActiveIssueDueDate', () => {
});
});
-describe('setActiveIssueSubscribed', () => {
- const state = { boardItems: { [mockActiveIssue.id]: mockActiveIssue } };
- const getters = { activeIssue: mockActiveIssue };
+describe('setActiveItemSubscribed', () => {
+ const state = {
+ boardItems: {
+ [mockActiveIssue.id]: mockActiveIssue,
+ },
+ fullPath: 'gitlab-org',
+ issuableType: 'issue',
+ };
+ const getters = { activeBoardItem: mockActiveIssue, isEpicBoard: false };
const subscribedState = true;
const input = {
subscribedState,
@@ -1051,7 +1374,7 @@ describe('setActiveIssueSubscribed', () => {
it('should commit subscribed status', (done) => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
- issueSetSubscription: {
+ updateIssuableSubscription: {
issue: {
subscribed: subscribedState,
},
@@ -1061,18 +1384,18 @@ describe('setActiveIssueSubscribed', () => {
});
const payload = {
- issueId: getters.activeIssue.id,
+ itemId: getters.activeBoardItem.id,
prop: 'subscribed',
value: subscribedState,
};
testAction(
- actions.setActiveIssueSubscribed,
+ actions.setActiveItemSubscribed,
input,
{ ...state, ...getters },
[
{
- type: types.UPDATE_ISSUE_BY_ID,
+ type: types.UPDATE_BOARD_ITEM_BY_ID,
payload,
},
],
@@ -1084,15 +1407,15 @@ describe('setActiveIssueSubscribed', () => {
it('throws error if fails', async () => {
jest
.spyOn(gqlClient, 'mutate')
- .mockResolvedValue({ data: { issueSetSubscription: { errors: ['failed mutation'] } } });
+ .mockResolvedValue({ data: { updateIssuableSubscription: { errors: ['failed mutation'] } } });
- await expect(actions.setActiveIssueSubscribed({ getters }, input)).rejects.toThrow(Error);
+ await expect(actions.setActiveItemSubscribed({ getters }, input)).rejects.toThrow(Error);
});
});
describe('setActiveIssueMilestone', () => {
const state = { boardItems: { [mockIssue.id]: mockIssue } };
- const getters = { activeIssue: mockIssue };
+ const getters = { activeBoardItem: mockIssue };
const testMilestone = {
...mockMilestone,
id: 'gid://gitlab/Milestone/1',
@@ -1115,7 +1438,7 @@ describe('setActiveIssueMilestone', () => {
});
const payload = {
- issueId: getters.activeIssue.id,
+ itemId: getters.activeBoardItem.id,
prop: 'milestone',
value: testMilestone,
};
@@ -1126,7 +1449,7 @@ describe('setActiveIssueMilestone', () => {
{ ...state, ...getters },
[
{
- type: types.UPDATE_ISSUE_BY_ID,
+ type: types.UPDATE_BOARD_ITEM_BY_ID,
payload,
},
],
@@ -1144,9 +1467,13 @@ describe('setActiveIssueMilestone', () => {
});
});
-describe('setActiveIssueTitle', () => {
- const state = { boardItems: { [mockIssue.id]: mockIssue } };
- const getters = { activeIssue: mockIssue };
+describe('setActiveItemTitle', () => {
+ const state = {
+ boardItems: { [mockIssue.id]: mockIssue },
+ issuableType: 'issue',
+ fullPath: 'path/f',
+ };
+ const getters = { activeBoardItem: mockIssue, isEpicBoard: false };
const testTitle = 'Test Title';
const input = {
title: testTitle,
@@ -1156,7 +1483,7 @@ describe('setActiveIssueTitle', () => {
it('should commit title after setting the issue', (done) => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
- updateIssue: {
+ updateIssuableTitle: {
issue: {
title: testTitle,
},
@@ -1166,18 +1493,18 @@ describe('setActiveIssueTitle', () => {
});
const payload = {
- issueId: getters.activeIssue.id,
+ itemId: getters.activeBoardItem.id,
prop: 'title',
value: testTitle,
};
testAction(
- actions.setActiveIssueTitle,
+ actions.setActiveItemTitle,
input,
{ ...state, ...getters },
[
{
- type: types.UPDATE_ISSUE_BY_ID,
+ type: types.UPDATE_BOARD_ITEM_BY_ID,
payload,
},
],
@@ -1191,7 +1518,7 @@ describe('setActiveIssueTitle', () => {
.spyOn(gqlClient, 'mutate')
.mockResolvedValue({ data: { updateIssue: { errors: ['failed mutation'] } } });
- await expect(actions.setActiveIssueTitle({ getters }, input)).rejects.toThrow(Error);
+ await expect(actions.setActiveItemTitle({ getters }, input)).rejects.toThrow(Error);
});
});
@@ -1321,7 +1648,7 @@ describe('toggleBoardItemMultiSelection', () => {
testAction(
actions.toggleBoardItemMultiSelection,
boardItem2,
- { activeId: mockActiveIssue.id, activeIssue: mockActiveIssue, selectedBoardItems: [] },
+ { activeId: mockActiveIssue.id, activeBoardItem: mockActiveIssue, selectedBoardItems: [] },
[
{
type: types.ADD_BOARD_ITEM_TO_SELECTION,
@@ -1378,6 +1705,51 @@ describe('toggleBoardItem', () => {
});
});
+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,
+ },
+ ],
+ });
+ });
+});
+
describe('fetchBacklog', () => {
expectNotImplemented(actions.fetchBacklog);
});
diff --git a/spec/frontend/boards/stores/getters_spec.js b/spec/frontend/boards/stores/getters_spec.js
index 32d73d861bc..6114ba0af5f 100644
--- a/spec/frontend/boards/stores/getters_spec.js
+++ b/spec/frontend/boards/stores/getters_spec.js
@@ -88,7 +88,7 @@ describe('Boards - Getters', () => {
});
});
- describe('activeIssue', () => {
+ describe('activeBoardItem', () => {
it.each`
id | expected
${'1'} | ${'issue'}
@@ -96,7 +96,7 @@ describe('Boards - Getters', () => {
`('returns $expected when $id is passed to state', ({ id, expected }) => {
const state = { boardItems: { 1: 'issue' }, activeId: id };
- expect(getters.activeIssue(state)).toEqual(expected);
+ expect(getters.activeBoardItem(state)).toEqual(expected);
});
});
@@ -105,14 +105,14 @@ describe('Boards - Getters', () => {
const mockActiveIssue = {
referencePath: 'gitlab-org/gitlab-test#1',
};
- expect(getters.groupPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual(
+ expect(getters.groupPathForActiveIssue({}, { activeBoardItem: mockActiveIssue })).toEqual(
'gitlab-org',
);
});
it('returns empty string as group path when active issue is an empty object', () => {
const mockActiveIssue = {};
- expect(getters.groupPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual('');
+ expect(getters.groupPathForActiveIssue({}, { activeBoardItem: mockActiveIssue })).toEqual('');
});
});
@@ -121,14 +121,16 @@ describe('Boards - Getters', () => {
const mockActiveIssue = {
referencePath: 'gitlab-org/gitlab-test#1',
};
- expect(getters.projectPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual(
+ expect(getters.projectPathForActiveIssue({}, { activeBoardItem: mockActiveIssue })).toEqual(
'gitlab-org/gitlab-test',
);
});
it('returns empty string as project path when active issue is an empty object', () => {
const mockActiveIssue = {};
- expect(getters.projectPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual('');
+ expect(getters.projectPathForActiveIssue({}, { activeBoardItem: mockActiveIssue })).toEqual(
+ '',
+ );
});
});
@@ -177,4 +179,31 @@ describe('Boards - Getters', () => {
expect(getters.activeGroupProjects(state)).toEqual([mockGroupProject1]);
});
});
+
+ describe('isIssueBoard', () => {
+ it.each`
+ issuableType | expected
+ ${'issue'} | ${true}
+ ${'epic'} | ${false}
+ `(
+ 'returns $expected when issuableType on state is $issuableType',
+ ({ issuableType, expected }) => {
+ const state = {
+ issuableType,
+ };
+
+ expect(getters.isIssueBoard(state)).toBe(expected);
+ },
+ );
+ });
+
+ describe('isEpicBoard', () => {
+ afterEach(() => {
+ window.gon = { features: {} };
+ });
+
+ it('returns false', () => {
+ expect(getters.isEpicBoard()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js
index 33897cc0250..af6d439e294 100644
--- a/spec/frontend/boards/stores/mutations_spec.js
+++ b/spec/frontend/boards/stores/mutations_spec.js
@@ -1,3 +1,4 @@
+import { cloneDeep } from 'lodash';
import { issuableTypes } from '~/boards/constants';
import * as types from '~/boards/stores/mutation_types';
import mutations from '~/boards/stores/mutations';
@@ -9,6 +10,7 @@ import {
mockIssue2,
mockGroupProjects,
labels,
+ mockList,
} from '../mock_data';
const expectNotImplemented = (action) => {
@@ -25,6 +27,14 @@ describe('Board Store Mutations', () => {
'gid://gitlab/List/2': mockLists[1],
};
+ const setBoardsListsState = () => {
+ state = cloneDeep({
+ ...state,
+ boardItemsByListId: { 'gid://gitlab/List/1': [mockIssue.id] },
+ boardLists: { 'gid://gitlab/List/1': mockList },
+ });
+ };
+
beforeEach(() => {
state = defaultState();
});
@@ -335,7 +345,7 @@ describe('Board Store Mutations', () => {
expectNotImplemented(mutations.REQUEST_ADD_ISSUE);
});
- describe('UPDATE_ISSUE_BY_ID', () => {
+ describe('UPDATE_BOARD_ITEM_BY_ID', () => {
const issueId = '1';
const prop = 'id';
const value = '2';
@@ -353,8 +363,8 @@ describe('Board Store Mutations', () => {
describe('when the issue is in state', () => {
it('updates the property of the correct issue', () => {
- mutations.UPDATE_ISSUE_BY_ID(state, {
- issueId,
+ mutations.UPDATE_BOARD_ITEM_BY_ID(state, {
+ itemId: issueId,
prop,
value,
});
@@ -366,8 +376,8 @@ describe('Board Store Mutations', () => {
describe('when the issue is not in state', () => {
it('throws an error', () => {
expect(() => {
- mutations.UPDATE_ISSUE_BY_ID(state, {
- issueId: '3',
+ mutations.UPDATE_BOARD_ITEM_BY_ID(state, {
+ itemId: '3',
prop,
value,
});
@@ -384,41 +394,7 @@ describe('Board Store Mutations', () => {
expectNotImplemented(mutations.RECEIVE_ADD_ISSUE_ERROR);
});
- describe('MOVE_ISSUE', () => {
- it('updates boardItemsByListId, moving issue between lists', () => {
- const listIssues = {
- 'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id],
- 'gid://gitlab/List/2': [],
- };
-
- const issues = {
- 1: mockIssue,
- 2: mockIssue2,
- };
-
- state = {
- ...state,
- boardItemsByListId: listIssues,
- boardLists: initialBoardListsState,
- boardItems: issues,
- };
-
- mutations.MOVE_ISSUE(state, {
- originalIssue: mockIssue2,
- fromListId: 'gid://gitlab/List/1',
- toListId: 'gid://gitlab/List/2',
- });
-
- const updatedListIssues = {
- 'gid://gitlab/List/1': [mockIssue.id],
- 'gid://gitlab/List/2': [mockIssue2.id],
- };
-
- expect(state.boardItemsByListId).toEqual(updatedListIssues);
- });
- });
-
- describe('MOVE_ISSUE_SUCCESS', () => {
+ describe('MUTATE_ISSUE_SUCCESS', () => {
it('updates issue in issues state', () => {
const issues = {
436: { id: rawIssue.id },
@@ -429,7 +405,7 @@ describe('Board Store Mutations', () => {
boardItems: issues,
};
- mutations.MOVE_ISSUE_SUCCESS(state, {
+ mutations.MUTATE_ISSUE_SUCCESS(state, {
issue: rawIssue,
});
@@ -437,33 +413,24 @@ describe('Board Store Mutations', () => {
});
});
- describe('MOVE_ISSUE_FAILURE', () => {
- it('updates boardItemsByListId, reverting moving issue between lists, and sets error message', () => {
- const listIssues = {
- 'gid://gitlab/List/1': [mockIssue.id],
- 'gid://gitlab/List/2': [mockIssue2.id],
- };
+ describe('UPDATE_BOARD_ITEM', () => {
+ it('updates the given issue in state.boardItems', () => {
+ const updatedIssue = { id: 'some_gid', foo: 'bar' };
+ state = { boardItems: { some_gid: { id: 'some_gid' } } };
- state = {
- ...state,
- boardItemsByListId: listIssues,
- boardLists: initialBoardListsState,
- };
+ mutations.UPDATE_BOARD_ITEM(state, updatedIssue);
- mutations.MOVE_ISSUE_FAILURE(state, {
- originalIssue: mockIssue2,
- fromListId: 'gid://gitlab/List/1',
- toListId: 'gid://gitlab/List/2',
- originalIndex: 1,
- });
+ expect(state.boardItems.some_gid).toEqual(updatedIssue);
+ });
+ });
- const updatedListIssues = {
- 'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id],
- 'gid://gitlab/List/2': [],
- };
+ describe('REMOVE_BOARD_ITEM', () => {
+ it('removes the given issue from state.boardItems', () => {
+ state = { boardItems: { some_gid: {}, some_gid2: {} } };
+
+ mutations.REMOVE_BOARD_ITEM(state, 'some_gid');
- expect(state.boardItemsByListId).toEqual(updatedListIssues);
- expect(state.error).toEqual('An error occurred while moving the issue. Please try again.');
+ expect(state.boardItems).toEqual({ some_gid2: {} });
});
});
@@ -479,85 +446,89 @@ 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);
+ describe('ADD_BOARD_ITEM_TO_LIST', () => {
+ beforeEach(() => {
+ setBoardsListsState();
+ });
+
+ it.each([
+ [
+ 'at position 0 by default',
+ {
+ payload: {
+ itemId: mockIssue2.id,
+ listId: mockList.id,
+ },
+ listState: [mockIssue2.id, mockIssue.id],
+ },
+ ],
+ [
+ 'at a given position',
+ {
+ payload: {
+ itemId: mockIssue2.id,
+ listId: mockList.id,
+ atIndex: 1,
+ },
+ listState: [mockIssue.id, mockIssue2.id],
+ },
+ ],
+ [
+ "below the issue with id of 'moveBeforeId'",
+ {
+ payload: {
+ itemId: mockIssue2.id,
+ listId: mockList.id,
+ moveBeforeId: mockIssue.id,
+ },
+ listState: [mockIssue.id, mockIssue2.id],
+ },
+ ],
+ [
+ "above the issue with id of 'moveAfterId'",
+ {
+ payload: {
+ itemId: mockIssue2.id,
+ listId: mockList.id,
+ moveAfterId: mockIssue.id,
+ },
+ listState: [mockIssue2.id, mockIssue.id],
+ },
+ ],
+ ])(`inserts an item into a list %s`, (_, { payload, listState }) => {
+ mutations.ADD_BOARD_ITEM_TO_LIST(state, payload);
- expect(state.error).toBe('An error occurred while creating the issue. Please try again.');
+ expect(state.boardItemsByListId[payload.listId]).toEqual(listState);
});
- });
-
- describe('ADD_ISSUE_TO_LIST', () => {
- it('adds issue to issues state and issue id in list in boardItemsByListId', () => {
- const listIssues = {
- 'gid://gitlab/List/1': [mockIssue.id],
- };
- const issues = {
- 1: mockIssue,
- };
-
- state = {
- ...state,
- boardItemsByListId: listIssues,
- boardItems: issues,
- boardLists: initialBoardListsState,
- };
+ it("updates the list's items count", () => {
expect(state.boardLists['gid://gitlab/List/1'].issuesCount).toBe(1);
- mutations.ADD_ISSUE_TO_LIST(state, { list: mockLists[0], issue: mockIssue2 });
+ mutations.ADD_BOARD_ITEM_TO_LIST(state, {
+ itemId: mockIssue2.id,
+ listId: mockList.id,
+ });
- expect(state.boardItemsByListId['gid://gitlab/List/1']).toContain(mockIssue2.id);
- expect(state.boardItems[mockIssue2.id]).toEqual(mockIssue2);
expect(state.boardLists['gid://gitlab/List/1'].issuesCount).toBe(2);
});
});
- describe('ADD_ISSUE_TO_LIST_FAILURE', () => {
- it('removes issue id from list in boardItemsByListId and sets error message', () => {
- const listIssues = {
- 'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id],
- };
- const issues = {
- 1: mockIssue,
- 2: mockIssue2,
- };
-
- state = {
- ...state,
- boardItemsByListId: listIssues,
- boardItems: issues,
- boardLists: initialBoardListsState,
- };
-
- mutations.ADD_ISSUE_TO_LIST_FAILURE(state, { list: mockLists[0], issueId: mockIssue2.id });
-
- expect(state.boardItemsByListId['gid://gitlab/List/1']).not.toContain(mockIssue2.id);
- expect(state.error).toBe('An error occurred while creating the issue. Please try again.');
+ describe('REMOVE_BOARD_ITEM_FROM_LIST', () => {
+ beforeEach(() => {
+ setBoardsListsState();
});
- });
- describe('REMOVE_ISSUE_FROM_LIST', () => {
- it('removes issue id from list in boardItemsByListId and deletes issue from state', () => {
- const listIssues = {
- 'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id],
- };
- const issues = {
- 1: mockIssue,
- 2: mockIssue2,
- };
-
- state = {
- ...state,
- boardItemsByListId: listIssues,
- boardItems: issues,
- boardLists: initialBoardListsState,
- };
+ it("removes an item from a list and updates the list's items count", () => {
+ expect(state.boardLists['gid://gitlab/List/1'].issuesCount).toBe(1);
+ expect(state.boardItemsByListId['gid://gitlab/List/1']).toContain(mockIssue.id);
- mutations.ADD_ISSUE_TO_LIST_FAILURE(state, { list: mockLists[0], issueId: mockIssue2.id });
+ mutations.REMOVE_BOARD_ITEM_FROM_LIST(state, {
+ itemId: mockIssue.id,
+ listId: mockList.id,
+ });
- expect(state.boardItemsByListId['gid://gitlab/List/1']).not.toContain(mockIssue2.id);
- expect(state.boardItems).not.toContain(mockIssue2);
+ expect(state.boardItemsByListId['gid://gitlab/List/1']).not.toContain(mockIssue.id);
+ expect(state.boardLists['gid://gitlab/List/1'].issuesCount).toBe(0);
});
});
@@ -666,4 +637,14 @@ describe('Board Store Mutations', () => {
expect(state.selectedBoardItems).toEqual([]);
});
});
+
+ describe('SET_ERROR', () => {
+ it('Should set error state', () => {
+ state.error = undefined;
+
+ mutations[types.SET_ERROR](state, 'mayday');
+
+ expect(state.error).toBe('mayday');
+ });
+ });
});