summaryrefslogtreecommitdiff
path: root/spec/frontend/boards
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/boards')
-rw-r--r--spec/frontend/boards/board_list_deprecated_spec.js275
-rw-r--r--spec/frontend/boards/board_list_helper.js2
-rw-r--r--spec/frontend/boards/board_list_new_spec.js268
-rw-r--r--spec/frontend/boards/board_list_spec.js346
-rw-r--r--spec/frontend/boards/board_new_issue_deprecated_spec.js (renamed from spec/frontend/boards/board_new_issue_spec.js)2
-rw-r--r--spec/frontend/boards/boards_store_spec.js32
-rw-r--r--spec/frontend/boards/components/board_assignee_dropdown_spec.js15
-rw-r--r--spec/frontend/boards/components/board_card_layout_spec.js70
-rw-r--r--spec/frontend/boards/components/board_card_spec.js7
-rw-r--r--spec/frontend/boards/components/board_column_deprecated_spec.js (renamed from spec/frontend/boards/components/board_column_new_spec.js)49
-rw-r--r--spec/frontend/boards/components/board_column_spec.js49
-rw-r--r--spec/frontend/boards/components/board_configuration_options_spec.js49
-rw-r--r--spec/frontend/boards/components/board_content_spec.js18
-rw-r--r--spec/frontend/boards/components/board_form_spec.js216
-rw-r--r--spec/frontend/boards/components/board_list_header_deprecated_spec.js (renamed from spec/frontend/boards/components/board_list_header_new_spec.js)140
-rw-r--r--spec/frontend/boards/components/board_list_header_spec.js136
-rw-r--r--spec/frontend/boards/components/board_new_issue_spec.js (renamed from spec/frontend/boards/components/board_new_issue_new_spec.js)6
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js8
-rw-r--r--spec/frontend/boards/components/issue_count_spec.js2
-rw-r--r--spec/frontend/boards/components/issue_due_date_spec.js2
-rw-r--r--spec/frontend/boards/components/issue_time_estimate_deprecated_spec.js64
-rw-r--r--spec/frontend/boards/components/issue_time_estimate_spec.js54
-rw-r--r--spec/frontend/boards/components/sidebar/board_editable_item_spec.js25
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_issue_title_spec.js182
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js18
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js2
-rw-r--r--spec/frontend/boards/components/sidebar/remove_issue_spec.js2
-rw-r--r--spec/frontend/boards/issue_card_deprecated_spec.js (renamed from spec/frontend/boards/issue_card_spec.js)38
-rw-r--r--spec/frontend/boards/issue_card_inner_spec.js372
-rw-r--r--spec/frontend/boards/list_spec.js4
-rw-r--r--spec/frontend/boards/mock_data.js36
-rw-r--r--spec/frontend/boards/project_select_deprecated_spec.js261
-rw-r--r--spec/frontend/boards/project_select_spec.js154
-rw-r--r--spec/frontend/boards/stores/actions_spec.js307
-rw-r--r--spec/frontend/boards/stores/getters_spec.js6
-rw-r--r--spec/frontend/boards/stores/mutations_spec.js100
36 files changed, 2395 insertions, 922 deletions
diff --git a/spec/frontend/boards/board_list_deprecated_spec.js b/spec/frontend/boards/board_list_deprecated_spec.js
new file mode 100644
index 00000000000..393d7f954b1
--- /dev/null
+++ b/spec/frontend/boards/board_list_deprecated_spec.js
@@ -0,0 +1,275 @@
+/* global List */
+/* global ListIssue */
+
+import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
+import eventHub from '~/boards/eventhub';
+import BoardList from '~/boards/components/board_list_deprecated.vue';
+import '~/boards/models/issue';
+import '~/boards/models/list';
+import { listObj, boardsMockInterceptor } from './mock_data';
+import store from '~/boards/stores';
+import boardsStore from '~/boards/stores/boards_store';
+
+const createComponent = ({ done, listIssueProps = {}, componentProps = {}, listProps = {} }) => {
+ const el = document.createElement('div');
+
+ document.body.appendChild(el);
+ const mock = new MockAdapter(axios);
+ mock.onAny().reply(boardsMockInterceptor);
+ boardsStore.create();
+
+ const BoardListComp = Vue.extend(BoardList);
+ const list = new List({ ...listObj, ...listProps });
+ const issue = new ListIssue({
+ title: 'Testing',
+ id: 1,
+ iid: 1,
+ confidential: false,
+ labels: [],
+ assignees: [],
+ ...listIssueProps,
+ });
+ if (!Object.prototype.hasOwnProperty.call(listProps, 'issuesSize')) {
+ list.issuesSize = 1;
+ }
+ list.issues.push(issue);
+
+ const component = new BoardListComp({
+ el,
+ store,
+ propsData: {
+ disabled: false,
+ list,
+ issues: list.issues,
+ ...componentProps,
+ },
+ provide: {
+ groupId: null,
+ rootPath: '/',
+ },
+ }).$mount();
+
+ Vue.nextTick(() => {
+ done();
+ });
+
+ return { component, mock };
+};
+
+describe('Board list component', () => {
+ let mock;
+ let component;
+ let getIssues;
+ function generateIssues(compWrapper) {
+ for (let i = 1; i < 20; i += 1) {
+ const issue = { ...compWrapper.list.issues[0] };
+ issue.id += i;
+ compWrapper.list.issues.push(issue);
+ }
+ }
+
+ describe('When Expanded', () => {
+ beforeEach((done) => {
+ getIssues = jest.spyOn(List.prototype, 'getIssues').mockReturnValue(new Promise(() => {}));
+ ({ mock, component } = createComponent({ done }));
+ });
+
+ afterEach(() => {
+ mock.restore();
+ component.$destroy();
+ });
+
+ it('loads first page of issues', () => {
+ return waitForPromises().then(() => {
+ expect(getIssues).toHaveBeenCalled();
+ });
+ });
+
+ it('renders component', () => {
+ expect(component.$el.classList.contains('board-list-component')).toBe(true);
+ });
+
+ it('renders loading icon', () => {
+ component.list.loading = true;
+
+ return Vue.nextTick().then(() => {
+ expect(component.$el.querySelector('.board-list-loading')).not.toBeNull();
+ });
+ });
+
+ it('renders issues', () => {
+ expect(component.$el.querySelectorAll('.board-card').length).toBe(1);
+ });
+
+ it('sets data attribute with issue id', () => {
+ expect(component.$el.querySelector('.board-card').getAttribute('data-issue-id')).toBe('1');
+ });
+
+ it('shows new issue form', () => {
+ component.toggleForm();
+
+ return Vue.nextTick().then(() => {
+ expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull();
+
+ expect(component.$el.querySelector('.is-smaller')).not.toBeNull();
+ });
+ });
+
+ it('shows new issue form after eventhub event', () => {
+ eventHub.$emit(`toggle-issue-form-${component.list.id}`);
+
+ return Vue.nextTick().then(() => {
+ expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull();
+
+ expect(component.$el.querySelector('.is-smaller')).not.toBeNull();
+ });
+ });
+
+ it('does not show new issue form for closed list', () => {
+ component.list.type = 'closed';
+ component.toggleForm();
+
+ return Vue.nextTick().then(() => {
+ expect(component.$el.querySelector('.board-new-issue-form')).toBeNull();
+ });
+ });
+
+ it('shows count list item', () => {
+ component.showCount = true;
+
+ return Vue.nextTick().then(() => {
+ expect(component.$el.querySelector('.board-list-count')).not.toBeNull();
+
+ expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe(
+ 'Showing all issues',
+ );
+ });
+ });
+
+ it('sets data attribute with invalid id', () => {
+ component.showCount = true;
+
+ return Vue.nextTick().then(() => {
+ expect(component.$el.querySelector('.board-list-count').getAttribute('data-issue-id')).toBe(
+ '-1',
+ );
+ });
+ });
+
+ it('shows how many more issues to load', () => {
+ component.showCount = true;
+ component.list.issuesSize = 20;
+
+ return Vue.nextTick().then(() => {
+ expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe(
+ 'Showing 1 of 20 issues',
+ );
+ });
+ });
+
+ it('loads more issues after scrolling', () => {
+ jest.spyOn(component.list, 'nextPage').mockImplementation(() => {});
+ generateIssues(component);
+ component.$refs.list.dispatchEvent(new Event('scroll'));
+
+ return waitForPromises().then(() => {
+ expect(component.list.nextPage).toHaveBeenCalled();
+ });
+ });
+
+ it('does not load issues if already loading', () => {
+ component.list.nextPage = jest
+ .spyOn(component.list, 'nextPage')
+ .mockReturnValue(new Promise(() => {}));
+
+ component.onScroll();
+ component.onScroll();
+
+ return waitForPromises().then(() => {
+ expect(component.list.nextPage).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ it('shows loading more spinner', () => {
+ component.showCount = true;
+ component.list.loadingMore = true;
+
+ return Vue.nextTick().then(() => {
+ expect(component.$el.querySelector('.board-list-count .gl-spinner')).not.toBeNull();
+ });
+ });
+ });
+
+ describe('When Collapsed', () => {
+ beforeEach((done) => {
+ getIssues = jest.spyOn(List.prototype, 'getIssues').mockReturnValue(new Promise(() => {}));
+ ({ mock, component } = createComponent({
+ done,
+ listProps: { type: 'closed', collapsed: true, issuesSize: 50 },
+ }));
+ generateIssues(component);
+ component.scrollHeight = jest.spyOn(component, 'scrollHeight').mockReturnValue(0);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ component.$destroy();
+ });
+
+ it('does not load all issues', () => {
+ return waitForPromises().then(() => {
+ // Initial getIssues from list constructor
+ expect(getIssues).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+
+ describe('max issue count warning', () => {
+ beforeEach((done) => {
+ ({ mock, component } = createComponent({
+ done,
+ listProps: { type: 'closed', collapsed: true, issuesSize: 50 },
+ }));
+ });
+
+ afterEach(() => {
+ mock.restore();
+ component.$destroy();
+ });
+
+ describe('when issue count exceeds max issue count', () => {
+ it('sets background to bg-danger-100', () => {
+ component.list.issuesSize = 4;
+ component.list.maxIssueCount = 3;
+
+ return Vue.nextTick().then(() => {
+ expect(component.$el.querySelector('.bg-danger-100')).not.toBeNull();
+ });
+ });
+ });
+
+ describe('when list issue count does NOT exceed list max issue count', () => {
+ it('does not sets background to bg-danger-100', () => {
+ component.list.issuesSize = 2;
+ component.list.maxIssueCount = 3;
+
+ return Vue.nextTick().then(() => {
+ expect(component.$el.querySelector('.bg-danger-100')).toBeNull();
+ });
+ });
+ });
+
+ describe('when list max issue count is 0', () => {
+ it('does not sets background to bg-danger-100', () => {
+ component.list.maxIssueCount = 0;
+
+ return Vue.nextTick().then(() => {
+ expect(component.$el.querySelector('.bg-danger-100')).toBeNull();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js
index 80d7a72151d..f82b1f7ed5c 100644
--- a/spec/frontend/boards/board_list_helper.js
+++ b/spec/frontend/boards/board_list_helper.js
@@ -5,7 +5,7 @@ import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import Sortable from 'sortablejs';
import axios from '~/lib/utils/axios_utils';
-import BoardList from '~/boards/components/board_list.vue';
+import BoardList from '~/boards/components/board_list_deprecated.vue';
import '~/boards/models/issue';
import '~/boards/models/list';
diff --git a/spec/frontend/boards/board_list_new_spec.js b/spec/frontend/boards/board_list_new_spec.js
deleted file mode 100644
index 96b03ed927e..00000000000
--- a/spec/frontend/boards/board_list_new_spec.js
+++ /dev/null
@@ -1,268 +0,0 @@
-import Vuex from 'vuex';
-import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
-import { createLocalVue, mount } from '@vue/test-utils';
-import eventHub from '~/boards/eventhub';
-import BoardList from '~/boards/components/board_list_new.vue';
-import BoardCard from '~/boards/components/board_card.vue';
-import '~/boards/models/list';
-import { mockList, mockIssuesByListId, issues, mockIssues } from './mock_data';
-import defaultState from '~/boards/stores/state';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-const actions = {
- fetchIssuesForList: jest.fn(),
-};
-
-const createStore = (state = defaultState) => {
- return new Vuex.Store({
- state,
- actions,
- });
-};
-
-const createComponent = ({
- listIssueProps = {},
- componentProps = {},
- listProps = {},
- state = {},
-} = {}) => {
- const store = createStore({
- issuesByListId: mockIssuesByListId,
- issues,
- pageInfoByListId: {
- 'gid://gitlab/List/1': { hasNextPage: true },
- 'gid://gitlab/List/2': {},
- },
- listsFlags: {
- 'gid://gitlab/List/1': {},
- 'gid://gitlab/List/2': {},
- },
- ...state,
- });
-
- const list = {
- ...mockList,
- ...listProps,
- };
- const issue = {
- title: 'Testing',
- id: 1,
- iid: 1,
- confidential: false,
- labels: [],
- assignees: [],
- ...listIssueProps,
- };
- if (!Object.prototype.hasOwnProperty.call(listProps, 'issuesCount')) {
- list.issuesCount = 1;
- }
-
- const component = mount(BoardList, {
- localVue,
- propsData: {
- disabled: false,
- list,
- issues: [issue],
- canAdminList: true,
- ...componentProps,
- },
- store,
- provide: {
- groupId: null,
- rootPath: '/',
- weightFeatureAvailable: false,
- boardWeight: null,
- },
- });
-
- return component;
-};
-
-describe('Board list component', () => {
- let wrapper;
- const findByTestId = testId => wrapper.find(`[data-testid="${testId}"]`);
- useFakeRequestAnimationFrame();
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('When Expanded', () => {
- beforeEach(() => {
- wrapper = createComponent();
- });
-
- it('renders component', () => {
- expect(wrapper.find('.board-list-component').exists()).toBe(true);
- });
-
- it('renders loading icon', () => {
- wrapper = createComponent({
- state: { listsFlags: { 'gid://gitlab/List/1': { isLoading: true } } },
- });
-
- expect(findByTestId('board_list_loading').exists()).toBe(true);
- });
-
- it('renders issues', () => {
- expect(wrapper.findAll(BoardCard).length).toBe(1);
- });
-
- it('sets data attribute with issue id', () => {
- expect(wrapper.find('.board-card').attributes('data-issue-id')).toBe('1');
- });
-
- it('shows new issue form', async () => {
- wrapper.vm.toggleForm();
-
- await wrapper.vm.$nextTick();
- expect(wrapper.find('.board-new-issue-form').exists()).toBe(true);
- });
-
- it('shows new issue form after eventhub event', async () => {
- eventHub.$emit(`toggle-issue-form-${wrapper.vm.list.id}`);
-
- await wrapper.vm.$nextTick();
- expect(wrapper.find('.board-new-issue-form').exists()).toBe(true);
- });
-
- it('does not show new issue form for closed list', () => {
- wrapper.setProps({ list: { type: 'closed' } });
- wrapper.vm.toggleForm();
-
- expect(wrapper.find('.board-new-issue-form').exists()).toBe(false);
- });
-
- it('shows count list item', async () => {
- wrapper.vm.showCount = true;
-
- await wrapper.vm.$nextTick();
- expect(wrapper.find('.board-list-count').exists()).toBe(true);
-
- expect(wrapper.find('.board-list-count').text()).toBe('Showing all issues');
- });
-
- it('sets data attribute with invalid id', async () => {
- wrapper.vm.showCount = true;
-
- await wrapper.vm.$nextTick();
- expect(wrapper.find('.board-list-count').attributes('data-issue-id')).toBe('-1');
- });
-
- it('shows how many more issues to load', async () => {
- wrapper.vm.showCount = true;
- wrapper.setProps({ list: { issuesCount: 20 } });
-
- await wrapper.vm.$nextTick();
- expect(wrapper.find('.board-list-count').text()).toBe('Showing 1 of 20 issues');
- });
- });
-
- describe('load more issues', () => {
- beforeEach(() => {
- wrapper = createComponent({
- listProps: { issuesCount: 25 },
- });
- });
-
- it('loads more issues after scrolling', () => {
- wrapper.vm.listRef.dispatchEvent(new Event('scroll'));
-
- expect(actions.fetchIssuesForList).toHaveBeenCalled();
- });
-
- it('does not load issues if already loading', () => {
- wrapper = createComponent({
- state: { listsFlags: { 'gid://gitlab/List/1': { isLoadingMore: true } } },
- });
- wrapper.vm.listRef.dispatchEvent(new Event('scroll'));
-
- expect(actions.fetchIssuesForList).not.toHaveBeenCalled();
- });
-
- it('shows loading more spinner', async () => {
- wrapper = createComponent({
- state: { listsFlags: { 'gid://gitlab/List/1': { isLoadingMore: true } } },
- });
- wrapper.vm.showCount = true;
-
- await wrapper.vm.$nextTick();
- expect(wrapper.find('.board-list-count .gl-spinner').exists()).toBe(true);
- });
- });
-
- describe('max issue count warning', () => {
- beforeEach(() => {
- wrapper = createComponent({
- listProps: { issuesCount: 50 },
- });
- });
-
- describe('when issue count exceeds max issue count', () => {
- it('sets background to bg-danger-100', async () => {
- wrapper.setProps({ list: { issuesCount: 4, maxIssueCount: 3 } });
-
- await wrapper.vm.$nextTick();
- expect(wrapper.find('.bg-danger-100').exists()).toBe(true);
- });
- });
-
- describe('when list issue count does NOT exceed list max issue count', () => {
- it('does not sets background to bg-danger-100', () => {
- wrapper.setProps({ list: { issuesCount: 2, maxIssueCount: 3 } });
-
- expect(wrapper.find('.bg-danger-100').exists()).toBe(false);
- });
- });
-
- describe('when list max issue count is 0', () => {
- it('does not sets background to bg-danger-100', () => {
- wrapper.setProps({ list: { maxIssueCount: 0 } });
-
- expect(wrapper.find('.bg-danger-100').exists()).toBe(false);
- });
- });
- });
-
- describe('drag & drop issue', () => {
- beforeEach(() => {
- wrapper = createComponent();
- });
-
- describe('handleDragOnStart', () => {
- it('adds a class `is-dragging` to document body', () => {
- expect(document.body.classList.contains('is-dragging')).toBe(false);
-
- findByTestId('tree-root-wrapper').vm.$emit('start');
-
- expect(document.body.classList.contains('is-dragging')).toBe(true);
- });
- });
-
- describe('handleDragOnEnd', () => {
- it('removes class `is-dragging` from document body', () => {
- jest.spyOn(wrapper.vm, 'moveIssue').mockImplementation(() => {});
- document.body.classList.add('is-dragging');
-
- findByTestId('tree-root-wrapper').vm.$emit('end', {
- oldIndex: 1,
- newIndex: 0,
- item: {
- dataset: {
- issueId: mockIssues[0].id,
- issueIid: mockIssues[0].iid,
- issuePath: mockIssues[0].referencePath,
- },
- },
- to: { children: [], dataset: { listId: 'gid://gitlab/List/1' } },
- from: { dataset: { listId: 'gid://gitlab/List/2' } },
- });
-
- expect(document.body.classList.contains('is-dragging')).toBe(false);
- });
- });
- });
-});
diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js
index 0fe3c88f518..1b62f25044e 100644
--- a/spec/frontend/boards/board_list_spec.js
+++ b/spec/frontend/boards/board_list_spec.js
@@ -1,29 +1,51 @@
-/* global List */
-/* global ListIssue */
-
-import Vue from 'vue';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
+import Vuex from 'vuex';
+import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
+import { createLocalVue, mount } from '@vue/test-utils';
import eventHub from '~/boards/eventhub';
-import waitForPromises from '../helpers/wait_for_promises';
import BoardList from '~/boards/components/board_list.vue';
-import '~/boards/models/issue';
-import '~/boards/models/list';
-import { listObj, boardsMockInterceptor } from './mock_data';
-import store from '~/boards/stores';
-import boardsStore from '~/boards/stores/boards_store';
-
-const createComponent = ({ done, listIssueProps = {}, componentProps = {}, listProps = {} }) => {
- const el = document.createElement('div');
-
- document.body.appendChild(el);
- const mock = new MockAdapter(axios);
- mock.onAny().reply(boardsMockInterceptor);
- boardsStore.create();
-
- const BoardListComp = Vue.extend(BoardList);
- const list = new List({ ...listObj, ...listProps });
- const issue = new ListIssue({
+import BoardCard from '~/boards/components/board_card.vue';
+import { mockList, mockIssuesByListId, issues, mockIssues } from './mock_data';
+import defaultState from '~/boards/stores/state';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+const actions = {
+ fetchIssuesForList: jest.fn(),
+};
+
+const createStore = (state = defaultState) => {
+ return new Vuex.Store({
+ state,
+ actions,
+ });
+};
+
+const createComponent = ({
+ listIssueProps = {},
+ componentProps = {},
+ listProps = {},
+ state = {},
+} = {}) => {
+ const store = createStore({
+ issuesByListId: mockIssuesByListId,
+ issues,
+ pageInfoByListId: {
+ 'gid://gitlab/List/1': { hasNextPage: true },
+ 'gid://gitlab/List/2': {},
+ },
+ listsFlags: {
+ 'gid://gitlab/List/1': {},
+ 'gid://gitlab/List/2': {},
+ },
+ ...state,
+ });
+
+ const list = {
+ ...mockList,
+ ...listProps,
+ };
+ const issue = {
title: 'Testing',
id: 1,
iid: 1,
@@ -31,244 +53,214 @@ const createComponent = ({ done, listIssueProps = {}, componentProps = {}, listP
labels: [],
assignees: [],
...listIssueProps,
- });
- if (!Object.prototype.hasOwnProperty.call(listProps, 'issuesSize')) {
- list.issuesSize = 1;
+ };
+ if (!Object.prototype.hasOwnProperty.call(listProps, 'issuesCount')) {
+ list.issuesCount = 1;
}
- list.issues.push(issue);
- const component = new BoardListComp({
- el,
- store,
+ const component = mount(BoardList, {
+ localVue,
propsData: {
disabled: false,
list,
- issues: list.issues,
+ issues: [issue],
+ canAdminList: true,
...componentProps,
},
+ store,
provide: {
groupId: null,
rootPath: '/',
+ weightFeatureAvailable: false,
+ boardWeight: null,
},
- }).$mount();
-
- Vue.nextTick(() => {
- done();
});
- return { component, mock };
+ return component;
};
describe('Board list component', () => {
- let mock;
- let component;
- let getIssues;
- function generateIssues(compWrapper) {
- for (let i = 1; i < 20; i += 1) {
- const issue = { ...compWrapper.list.issues[0] };
- issue.id += i;
- compWrapper.list.issues.push(issue);
- }
- }
+ let wrapper;
+ const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
+ useFakeRequestAnimationFrame();
- describe('When Expanded', () => {
- beforeEach(done => {
- getIssues = jest.spyOn(List.prototype, 'getIssues').mockReturnValue(new Promise(() => {}));
- ({ mock, component } = createComponent({ done }));
- });
-
- afterEach(() => {
- mock.restore();
- component.$destroy();
- });
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
- it('loads first page of issues', () => {
- return waitForPromises().then(() => {
- expect(getIssues).toHaveBeenCalled();
- });
+ describe('When Expanded', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
});
it('renders component', () => {
- expect(component.$el.classList.contains('board-list-component')).toBe(true);
+ expect(wrapper.find('.board-list-component').exists()).toBe(true);
});
it('renders loading icon', () => {
- component.list.loading = true;
-
- return Vue.nextTick().then(() => {
- expect(component.$el.querySelector('.board-list-loading')).not.toBeNull();
+ wrapper = createComponent({
+ state: { listsFlags: { 'gid://gitlab/List/1': { isLoading: true } } },
});
+
+ expect(findByTestId('board_list_loading').exists()).toBe(true);
});
it('renders issues', () => {
- expect(component.$el.querySelectorAll('.board-card').length).toBe(1);
+ expect(wrapper.findAll(BoardCard).length).toBe(1);
});
it('sets data attribute with issue id', () => {
- expect(component.$el.querySelector('.board-card').getAttribute('data-issue-id')).toBe('1');
+ expect(wrapper.find('.board-card').attributes('data-issue-id')).toBe('1');
});
- it('shows new issue form', () => {
- component.toggleForm();
-
- return Vue.nextTick().then(() => {
- expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull();
+ it('shows new issue form', async () => {
+ wrapper.vm.toggleForm();
- expect(component.$el.querySelector('.is-smaller')).not.toBeNull();
- });
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find('.board-new-issue-form').exists()).toBe(true);
});
- it('shows new issue form after eventhub event', () => {
- eventHub.$emit(`toggle-issue-form-${component.list.id}`);
-
- return Vue.nextTick().then(() => {
- expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull();
+ it('shows new issue form after eventhub event', async () => {
+ eventHub.$emit(`toggle-issue-form-${wrapper.vm.list.id}`);
- expect(component.$el.querySelector('.is-smaller')).not.toBeNull();
- });
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find('.board-new-issue-form').exists()).toBe(true);
});
it('does not show new issue form for closed list', () => {
- component.list.type = 'closed';
- component.toggleForm();
+ wrapper.setProps({ list: { type: 'closed' } });
+ wrapper.vm.toggleForm();
- return Vue.nextTick().then(() => {
- expect(component.$el.querySelector('.board-new-issue-form')).toBeNull();
- });
+ expect(wrapper.find('.board-new-issue-form').exists()).toBe(false);
});
- it('shows count list item', () => {
- component.showCount = true;
+ it('shows count list item', async () => {
+ wrapper.vm.showCount = true;
- return Vue.nextTick().then(() => {
- expect(component.$el.querySelector('.board-list-count')).not.toBeNull();
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find('.board-list-count').exists()).toBe(true);
- expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe(
- 'Showing all issues',
- );
- });
+ expect(wrapper.find('.board-list-count').text()).toBe('Showing all issues');
});
- it('sets data attribute with invalid id', () => {
- component.showCount = true;
+ it('sets data attribute with invalid id', async () => {
+ wrapper.vm.showCount = true;
- return Vue.nextTick().then(() => {
- expect(component.$el.querySelector('.board-list-count').getAttribute('data-issue-id')).toBe(
- '-1',
- );
- });
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find('.board-list-count').attributes('data-issue-id')).toBe('-1');
});
- it('shows how many more issues to load', () => {
- component.showCount = true;
- component.list.issuesSize = 20;
+ it('shows how many more issues to load', async () => {
+ wrapper.vm.showCount = true;
+ wrapper.setProps({ list: { issuesCount: 20 } });
- return Vue.nextTick().then(() => {
- expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe(
- 'Showing 1 of 20 issues',
- );
- });
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find('.board-list-count').text()).toBe('Showing 1 of 20 issues');
});
+ });
- it('loads more issues after scrolling', () => {
- jest.spyOn(component.list, 'nextPage').mockImplementation(() => {});
- generateIssues(component);
- component.$refs.list.dispatchEvent(new Event('scroll'));
-
- return waitForPromises().then(() => {
- expect(component.list.nextPage).toHaveBeenCalled();
+ describe('load more issues', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ listProps: { issuesCount: 25 },
});
});
- it('does not load issues if already loading', () => {
- component.list.nextPage = jest
- .spyOn(component.list, 'nextPage')
- .mockReturnValue(new Promise(() => {}));
-
- component.onScroll();
- component.onScroll();
+ it('loads more issues after scrolling', () => {
+ wrapper.vm.listRef.dispatchEvent(new Event('scroll'));
- return waitForPromises().then(() => {
- expect(component.list.nextPage).toHaveBeenCalledTimes(1);
- });
+ expect(actions.fetchIssuesForList).toHaveBeenCalled();
});
- it('shows loading more spinner', () => {
- component.showCount = true;
- component.list.loadingMore = true;
-
- return Vue.nextTick().then(() => {
- expect(component.$el.querySelector('.board-list-count .gl-spinner')).not.toBeNull();
+ it('does not load issues if already loading', () => {
+ wrapper = createComponent({
+ state: { listsFlags: { 'gid://gitlab/List/1': { isLoadingMore: true } } },
});
- });
- });
+ wrapper.vm.listRef.dispatchEvent(new Event('scroll'));
- describe('When Collapsed', () => {
- beforeEach(done => {
- getIssues = jest.spyOn(List.prototype, 'getIssues').mockReturnValue(new Promise(() => {}));
- ({ mock, component } = createComponent({
- done,
- listProps: { type: 'closed', collapsed: true, issuesSize: 50 },
- }));
- generateIssues(component);
- component.scrollHeight = jest.spyOn(component, 'scrollHeight').mockReturnValue(0);
+ expect(actions.fetchIssuesForList).not.toHaveBeenCalled();
});
- afterEach(() => {
- mock.restore();
- component.$destroy();
- });
-
- it('does not load all issues', () => {
- return waitForPromises().then(() => {
- // Initial getIssues from list constructor
- expect(getIssues).toHaveBeenCalledTimes(1);
+ it('shows loading more spinner', async () => {
+ wrapper = createComponent({
+ state: { listsFlags: { 'gid://gitlab/List/1': { isLoadingMore: true } } },
});
+ wrapper.vm.showCount = true;
+
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find('.board-list-count .gl-spinner').exists()).toBe(true);
});
});
describe('max issue count warning', () => {
- beforeEach(done => {
- ({ mock, component } = createComponent({
- done,
- listProps: { type: 'closed', collapsed: true, issuesSize: 50 },
- }));
- });
-
- afterEach(() => {
- mock.restore();
- component.$destroy();
+ beforeEach(() => {
+ wrapper = createComponent({
+ listProps: { issuesCount: 50 },
+ });
});
describe('when issue count exceeds max issue count', () => {
- it('sets background to bg-danger-100', () => {
- component.list.issuesSize = 4;
- component.list.maxIssueCount = 3;
+ it('sets background to bg-danger-100', async () => {
+ wrapper.setProps({ list: { issuesCount: 4, maxIssueCount: 3 } });
- return Vue.nextTick().then(() => {
- expect(component.$el.querySelector('.bg-danger-100')).not.toBeNull();
- });
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find('.bg-danger-100').exists()).toBe(true);
});
});
describe('when list issue count does NOT exceed list max issue count', () => {
it('does not sets background to bg-danger-100', () => {
- component.list.issuesSize = 2;
- component.list.maxIssueCount = 3;
+ wrapper.setProps({ list: { issuesCount: 2, maxIssueCount: 3 } });
- return Vue.nextTick().then(() => {
- expect(component.$el.querySelector('.bg-danger-100')).toBeNull();
- });
+ expect(wrapper.find('.bg-danger-100').exists()).toBe(false);
});
});
describe('when list max issue count is 0', () => {
it('does not sets background to bg-danger-100', () => {
- component.list.maxIssueCount = 0;
+ wrapper.setProps({ list: { maxIssueCount: 0 } });
+
+ expect(wrapper.find('.bg-danger-100').exists()).toBe(false);
+ });
+ });
+ });
- return Vue.nextTick().then(() => {
- expect(component.$el.querySelector('.bg-danger-100')).toBeNull();
+ describe('drag & drop issue', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ describe('handleDragOnStart', () => {
+ it('adds a class `is-dragging` to document body', () => {
+ expect(document.body.classList.contains('is-dragging')).toBe(false);
+
+ findByTestId('tree-root-wrapper').vm.$emit('start');
+
+ expect(document.body.classList.contains('is-dragging')).toBe(true);
+ });
+ });
+
+ describe('handleDragOnEnd', () => {
+ it('removes class `is-dragging` from document body', () => {
+ jest.spyOn(wrapper.vm, 'moveIssue').mockImplementation(() => {});
+ document.body.classList.add('is-dragging');
+
+ findByTestId('tree-root-wrapper').vm.$emit('end', {
+ oldIndex: 1,
+ newIndex: 0,
+ item: {
+ dataset: {
+ issueId: mockIssues[0].id,
+ issueIid: mockIssues[0].iid,
+ issuePath: mockIssues[0].referencePath,
+ },
+ },
+ to: { children: [], dataset: { listId: 'gid://gitlab/List/1' } },
+ from: { dataset: { listId: 'gid://gitlab/List/2' } },
});
+
+ expect(document.body.classList.contains('is-dragging')).toBe(false);
});
});
});
diff --git a/spec/frontend/boards/board_new_issue_spec.js b/spec/frontend/boards/board_new_issue_deprecated_spec.js
index 3eebfeca965..8236b468189 100644
--- a/spec/frontend/boards/board_new_issue_spec.js
+++ b/spec/frontend/boards/board_new_issue_deprecated_spec.js
@@ -4,7 +4,7 @@ import Vue from 'vue';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import boardNewIssue from '~/boards/components/board_new_issue.vue';
+import boardNewIssue from '~/boards/components/board_new_issue_deprecated.vue';
import boardsStore from '~/boards/stores/boards_store';
import '~/boards/models/list';
diff --git a/spec/frontend/boards/boards_store_spec.js b/spec/frontend/boards/boards_store_spec.js
index c89f6d22ef2..f1d249ff069 100644
--- a/spec/frontend/boards/boards_store_spec.js
+++ b/spec/frontend/boards/boards_store_spec.js
@@ -77,7 +77,7 @@ describe('boardsStore', () => {
beforeEach(() => {
requestSpy = jest.fn();
- axiosMock.onPost(endpoints.listsEndpoint).replyOnce(config => requestSpy(config));
+ axiosMock.onPost(endpoints.listsEndpoint).replyOnce((config) => requestSpy(config));
});
it('makes a request to create a list', () => {
@@ -114,7 +114,7 @@ describe('boardsStore', () => {
beforeEach(() => {
requestSpy = jest.fn();
- axiosMock.onPut(`${endpoints.listsEndpoint}/${id}`).replyOnce(config => requestSpy(config));
+ axiosMock.onPut(`${endpoints.listsEndpoint}/${id}`).replyOnce((config) => requestSpy(config));
});
it('makes a request to update a list position', () => {
@@ -148,7 +148,7 @@ describe('boardsStore', () => {
requestSpy = jest.fn();
axiosMock
.onDelete(`${endpoints.listsEndpoint}/${id}`)
- .replyOnce(config => requestSpy(config));
+ .replyOnce((config) => requestSpy(config));
});
it('makes a request to delete a list', () => {
@@ -269,7 +269,7 @@ describe('boardsStore', () => {
requestSpy = jest.fn();
axiosMock
.onPut(`${urlRoot}/-/boards/${boardId}/issues/${id}`)
- .replyOnce(config => requestSpy(config));
+ .replyOnce((config) => requestSpy(config));
});
it('makes a request to move an issue between lists', () => {
@@ -308,7 +308,7 @@ describe('boardsStore', () => {
beforeEach(() => {
requestSpy = jest.fn();
- axiosMock.onPost(url).replyOnce(config => requestSpy(config));
+ axiosMock.onPost(url).replyOnce((config) => requestSpy(config));
});
it('makes a request to create a new issue', () => {
@@ -378,7 +378,7 @@ describe('boardsStore', () => {
beforeEach(() => {
requestSpy = jest.fn();
- axiosMock.onPost(endpoints.bulkUpdatePath).replyOnce(config => requestSpy(config));
+ axiosMock.onPost(endpoints.bulkUpdatePath).replyOnce((config) => requestSpy(config));
});
it('makes a request to create a list', () => {
@@ -456,24 +456,6 @@ describe('boardsStore', () => {
});
});
- describe('deleteBoard', () => {
- const id = 'capsized';
- const url = `${endpoints.boardsEndpoint}/${id}.json`;
-
- it('makes a request to delete a boards', () => {
- axiosMock.onDelete(url).replyOnce(200, dummyResponse);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.deleteBoard({ id })).resolves.toEqual(expectedResponse);
- });
-
- it('fails for error response', () => {
- axiosMock.onDelete(url).replyOnce(500);
-
- return expect(boardsStore.deleteBoard({ id })).rejects.toThrow();
- });
- });
-
describe('when created', () => {
beforeEach(() => {
setupDefaultResponses();
@@ -603,7 +585,7 @@ describe('boardsStore', () => {
expect(boardsStore.state.lists.length).toBe(1);
- boardsStore.removeList(listObj.id, 'label');
+ boardsStore.removeList(listObj.id);
expect(boardsStore.state.lists.length).toBe(0);
});
diff --git a/spec/frontend/boards/components/board_assignee_dropdown_spec.js b/spec/frontend/boards/components/board_assignee_dropdown_spec.js
index bbdcc707f09..e52c14f9783 100644
--- a/spec/frontend/boards/components/board_assignee_dropdown_spec.js
+++ b/spec/frontend/boards/components/board_assignee_dropdown_spec.js
@@ -6,7 +6,7 @@ import {
GlSearchBoxByType,
GlLoadingIcon,
} from '@gitlab/ui';
-import createMockApollo from 'jest/helpers/mock_apollo_helper';
+import createMockApollo from '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';
@@ -93,8 +93,8 @@ describe('BoardCardAssigneeDropdown', () => {
await wrapper.vm.$nextTick();
};
- const findByText = text => {
- return wrapper.findAll(GlDropdownItem).wrappers.find(node => node.text().indexOf(text) === 0);
+ const findByText = (text) => {
+ return wrapper.findAll(GlDropdownItem).wrappers.find((node) => node.text().indexOf(text) === 0);
};
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
@@ -102,7 +102,7 @@ describe('BoardCardAssigneeDropdown', () => {
beforeEach(() => {
store.state.activeId = '1';
store.state.issues = {
- '1': {
+ 1: {
iid,
assignees: [{ username: activeIssueName, name: activeIssueName, id: activeIssueName }],
},
@@ -145,12 +145,7 @@ describe('BoardCardAssigneeDropdown', () => {
it('renders gl-avatar-labeled in gl-avatar-link', () => {
const item = findByText('hello');
- expect(
- item
- .find(GlAvatarLink)
- .find(GlAvatarLabeled)
- .exists(),
- ).toBe(true);
+ expect(item.find(GlAvatarLink).find(GlAvatarLabeled).exists()).toBe(true);
});
});
diff --git a/spec/frontend/boards/components/board_card_layout_spec.js b/spec/frontend/boards/components/board_card_layout_spec.js
index 80f649a1a96..d8633871e8d 100644
--- a/spec/frontend/boards/components/board_card_layout_spec.js
+++ b/spec/frontend/boards/components/board_card_layout_spec.js
@@ -1,7 +1,8 @@
/* global List */
/* global ListLabel */
-import { shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
@@ -10,20 +11,35 @@ import axios from '~/lib/utils/axios_utils';
import '~/boards/models/label';
import '~/boards/models/assignee';
import '~/boards/models/list';
-import store from '~/boards/stores';
+import boardsVuexStore from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
import BoardCardLayout from '~/boards/components/board_card_layout.vue';
import issueCardInner from '~/boards/components/issue_card_inner.vue';
import { listObj, boardsMockInterceptor, setMockEndpoints } from '../mock_data';
+import { ISSUABLE } from '~/boards/constants';
+
describe('Board card layout', () => {
let wrapper;
let mock;
let list;
+ let store;
+
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ const createStore = ({ getters = {}, actions = {} } = {}) => {
+ store = new Vuex.Store({
+ ...boardsVuexStore,
+ actions,
+ getters,
+ });
+ };
// this particular mount component needs to be used after the root beforeEach because it depends on list being initialized
- const mountComponent = propsData => {
+ const mountComponent = ({ propsData = {}, provide = {} } = {}) => {
wrapper = shallowMount(BoardCardLayout, {
+ localVue,
stubs: {
issueCardInner,
},
@@ -38,6 +54,8 @@ describe('Board card layout', () => {
provide: {
groupId: null,
rootPath: '/',
+ scopedLabelsAvailable: false,
+ ...provide,
},
});
};
@@ -74,6 +92,7 @@ describe('Board card layout', () => {
describe('mouse events', () => {
it('sets showDetail to true on mousedown', async () => {
+ createStore();
mountComponent();
wrapper.trigger('mousedown');
@@ -83,6 +102,7 @@ describe('Board card layout', () => {
});
it('sets showDetail to false on mousemove', async () => {
+ createStore();
mountComponent();
wrapper.trigger('mousedown');
await wrapper.vm.$nextTick();
@@ -91,5 +111,49 @@ describe('Board card layout', () => {
await wrapper.vm.$nextTick();
expect(wrapper.vm.showDetail).toBe(false);
});
+
+ it("calls 'setActiveId' when 'graphqlBoardLists' feature flag is turned on", async () => {
+ const setActiveId = jest.fn();
+ createStore({
+ actions: {
+ setActiveId,
+ },
+ });
+ mountComponent({
+ provide: {
+ glFeatures: { graphqlBoardLists: true },
+ },
+ });
+
+ wrapper.trigger('mouseup');
+ await wrapper.vm.$nextTick();
+
+ expect(setActiveId).toHaveBeenCalledTimes(1);
+ expect(setActiveId).toHaveBeenCalledWith(expect.any(Object), {
+ id: list.issues[0].id,
+ sidebarType: ISSUABLE,
+ });
+ });
+
+ it("calls 'setActiveId' when epic swimlanes is active", async () => {
+ const setActiveId = jest.fn();
+ const isSwimlanesOn = () => true;
+ createStore({
+ getters: { isSwimlanesOn },
+ actions: {
+ setActiveId,
+ },
+ });
+ mountComponent();
+
+ wrapper.trigger('mouseup');
+ await wrapper.vm.$nextTick();
+
+ expect(setActiveId).toHaveBeenCalledTimes(1);
+ expect(setActiveId).toHaveBeenCalledWith(expect.any(Object), {
+ id: list.issues[0].id,
+ sidebarType: ISSUABLE,
+ });
+ });
});
});
diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js
index 5e23c781eae..1084009caad 100644
--- a/spec/frontend/boards/components/board_card_spec.js
+++ b/spec/frontend/boards/components/board_card_spec.js
@@ -29,7 +29,7 @@ describe('BoardCard', () => {
const findUserAvatarLink = () => wrapper.find(userAvatarLink);
// this particular mount component needs to be used after the root beforeEach because it depends on list being initialized
- const mountComponent = propsData => {
+ const mountComponent = (propsData) => {
wrapper = mount(BoardCard, {
stubs: {
issueCardInner,
@@ -45,6 +45,7 @@ describe('BoardCard', () => {
provide: {
groupId: null,
rootPath: '/',
+ scopedLabelsAvailable: false,
},
});
};
@@ -133,9 +134,7 @@ describe('BoardCard', () => {
it('does not set detail issue if link is clicked', () => {
mountComponent();
- findIssueCardInner()
- .find('a')
- .trigger('mouseup');
+ findIssueCardInner().find('a').trigger('mouseup');
expect(boardsStore.detail.issue).toEqual({});
});
diff --git a/spec/frontend/boards/components/board_column_new_spec.js b/spec/frontend/boards/components/board_column_deprecated_spec.js
index 81c0e60f931..a703caca4eb 100644
--- a/spec/frontend/boards/components/board_column_new_spec.js
+++ b/spec/frontend/boards/components/board_column_deprecated_spec.js
@@ -1,40 +1,65 @@
+import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'helpers/test_constants';
import { listObj } from 'jest/boards/mock_data';
-import BoardColumn from '~/boards/components/board_column_new.vue';
+import Board from '~/boards/components/board_column_deprecated.vue';
+import List from '~/boards/models/list';
import { ListType } from '~/boards/constants';
-import { createStore } from '~/boards/stores';
+import axios from '~/lib/utils/axios_utils';
describe('Board Column Component', () => {
let wrapper;
- let store;
+ let axiosMock;
+
+ beforeEach(() => {
+ window.gon = {};
+ axiosMock = new AxiosMockAdapter(axios);
+ axiosMock.onGet(`${TEST_HOST}/lists/1/issues`).reply(200, { issues: [] });
+ });
afterEach(() => {
+ axiosMock.restore();
+
wrapper.destroy();
- wrapper = null;
+
+ localStorage.clear();
});
- const createComponent = ({ listType = ListType.backlog, collapsed = false } = {}) => {
+ const createComponent = ({
+ listType = ListType.backlog,
+ collapsed = false,
+ withLocalStorage = true,
+ } = {}) => {
const boardId = '1';
const listMock = {
...listObj,
- listType,
+ list_type: listType,
collapsed,
};
if (listType === ListType.assignee) {
delete listMock.label;
- listMock.assignee = {};
+ listMock.user = {};
}
- store = createStore();
+ // Making List reactive
+ const list = Vue.observable(new List(listMock));
- wrapper = shallowMount(BoardColumn, {
- store,
+ if (withLocalStorage) {
+ localStorage.setItem(
+ `boards.${boardId}.${list.type}.${list.id}.expanded`,
+ (!collapsed).toString(),
+ );
+ }
+
+ wrapper = shallowMount(Board, {
propsData: {
+ boardId,
disabled: false,
- list: listMock,
+ list,
},
provide: {
boardId,
@@ -57,7 +82,7 @@ describe('Board Column Component', () => {
it('has class is-collapsed when list is collapsed', () => {
createComponent({ collapsed: false });
- expect(isCollapsed()).toBe(false);
+ expect(wrapper.vm.list.isExpanded).toBe(true);
});
it('does not have class is-collapsed when list is expanded', () => {
diff --git a/spec/frontend/boards/components/board_column_spec.js b/spec/frontend/boards/components/board_column_spec.js
index ba11225676b..1dcdad2b492 100644
--- a/spec/frontend/boards/components/board_column_spec.js
+++ b/spec/frontend/boards/components/board_column_spec.js
@@ -1,65 +1,40 @@
-import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
-import AxiosMockAdapter from 'axios-mock-adapter';
-import { TEST_HOST } from 'helpers/test_constants';
import { listObj } from 'jest/boards/mock_data';
-import Board from '~/boards/components/board_column.vue';
-import List from '~/boards/models/list';
+import BoardColumn from '~/boards/components/board_column.vue';
import { ListType } from '~/boards/constants';
-import axios from '~/lib/utils/axios_utils';
+import { createStore } from '~/boards/stores';
describe('Board Column Component', () => {
let wrapper;
- let axiosMock;
-
- beforeEach(() => {
- window.gon = {};
- axiosMock = new AxiosMockAdapter(axios);
- axiosMock.onGet(`${TEST_HOST}/lists/1/issues`).reply(200, { issues: [] });
- });
+ let store;
afterEach(() => {
- axiosMock.restore();
-
wrapper.destroy();
-
- localStorage.clear();
+ wrapper = null;
});
- const createComponent = ({
- listType = ListType.backlog,
- collapsed = false,
- withLocalStorage = true,
- } = {}) => {
+ const createComponent = ({ listType = ListType.backlog, collapsed = false } = {}) => {
const boardId = '1';
const listMock = {
...listObj,
- list_type: listType,
+ listType,
collapsed,
};
if (listType === ListType.assignee) {
delete listMock.label;
- listMock.user = {};
+ listMock.assignee = {};
}
- // Making List reactive
- const list = Vue.observable(new List(listMock));
+ store = createStore();
- if (withLocalStorage) {
- localStorage.setItem(
- `boards.${boardId}.${list.type}.${list.id}.expanded`,
- (!collapsed).toString(),
- );
- }
-
- wrapper = shallowMount(Board, {
+ wrapper = shallowMount(BoardColumn, {
+ store,
propsData: {
- boardId,
disabled: false,
- list,
+ list: listMock,
},
provide: {
boardId,
@@ -82,7 +57,7 @@ describe('Board Column Component', () => {
it('has class is-collapsed when list is collapsed', () => {
createComponent({ collapsed: false });
- expect(wrapper.vm.list.isExpanded).toBe(true);
+ expect(isCollapsed()).toBe(false);
});
it('does not have class is-collapsed when list is expanded', () => {
diff --git a/spec/frontend/boards/components/board_configuration_options_spec.js b/spec/frontend/boards/components/board_configuration_options_spec.js
index e9a1cb6a4e8..d9614c254e2 100644
--- a/spec/frontend/boards/components/board_configuration_options_spec.js
+++ b/spec/frontend/boards/components/board_configuration_options_spec.js
@@ -3,38 +3,30 @@ import BoardConfigurationOptions from '~/boards/components/board_configuration_o
describe('BoardConfigurationOptions', () => {
let wrapper;
- const board = { hide_backlog_list: false, hide_closed_list: false };
const defaultProps = {
- currentBoard: board,
- board,
- isNewForm: false,
+ hideBacklogList: false,
+ hideClosedList: false,
};
- const createComponent = () => {
+ const createComponent = (props = {}) => {
wrapper = shallowMount(BoardConfigurationOptions, {
- propsData: { ...defaultProps },
+ propsData: { ...defaultProps, ...props },
});
};
- beforeEach(() => {
- createComponent();
- });
-
afterEach(() => {
wrapper.destroy();
});
- const backlogListCheckbox = el => el.find('[data-testid="backlog-list-checkbox"]');
- const closedListCheckbox = el => el.find('[data-testid="closed-list-checkbox"]');
+ const backlogListCheckbox = () => wrapper.find('[data-testid="backlog-list-checkbox"]');
+ const closedListCheckbox = () => wrapper.find('[data-testid="closed-list-checkbox"]');
const checkboxAssert = (backlogCheckbox, closedCheckbox) => {
- expect(backlogListCheckbox(wrapper).attributes('checked')).toEqual(
+ expect(backlogListCheckbox().attributes('checked')).toEqual(
backlogCheckbox ? undefined : 'true',
);
- expect(closedListCheckbox(wrapper).attributes('checked')).toEqual(
- closedCheckbox ? undefined : 'true',
- );
+ expect(closedListCheckbox().attributes('checked')).toEqual(closedCheckbox ? undefined : 'true');
};
it.each`
@@ -45,15 +37,28 @@ describe('BoardConfigurationOptions', () => {
${false} | ${false}
`(
'renders two checkbox when one is $backlogCheckboxValue and other is $closedCheckboxValue',
- async ({ backlogCheckboxValue, closedCheckboxValue }) => {
- await wrapper.setData({
+ ({ backlogCheckboxValue, closedCheckboxValue }) => {
+ createComponent({
hideBacklogList: backlogCheckboxValue,
hideClosedList: closedCheckboxValue,
});
-
- return wrapper.vm.$nextTick().then(() => {
- checkboxAssert(backlogCheckboxValue, closedCheckboxValue);
- });
+ checkboxAssert(backlogCheckboxValue, closedCheckboxValue);
},
);
+
+ it('emits a correct value on backlog checkbox change', () => {
+ createComponent();
+
+ backlogListCheckbox().vm.$emit('change');
+
+ expect(wrapper.emitted('update:hideBacklogList')).toEqual([[true]]);
+ });
+
+ it('emits a correct value on closed checkbox change', () => {
+ createComponent();
+
+ closedListCheckbox().vm.$emit('change');
+
+ expect(wrapper.emitted('update:hideClosedList')).toEqual([[true]]);
+ });
});
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
index 291013c561e..98be02d7dbf 100644
--- a/spec/frontend/boards/components/board_content_spec.js
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -4,7 +4,7 @@ import { GlAlert } from '@gitlab/ui';
import Draggable from 'vuedraggable';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
import getters from 'ee_else_ce/boards/stores/getters';
-import BoardColumn from '~/boards/components/board_column.vue';
+import BoardColumnDeprecated from '~/boards/components/board_column_deprecated.vue';
import { mockLists, mockListsWithModel } from '../mock_data';
import BoardContent from '~/boards/components/board_content.vue';
@@ -17,6 +17,7 @@ const actions = {
describe('BoardContent', () => {
let wrapper;
+ window.gon = {};
const defaultState = {
isShowingEpicsSwimlanes: false,
@@ -56,10 +57,12 @@ describe('BoardContent', () => {
wrapper.destroy();
});
- it('renders a BoardColumn component per list', () => {
+ it('renders a BoardColumnDeprecated component per list', () => {
createComponent();
- expect(wrapper.findAll(BoardColumn)).toHaveLength(mockLists.length);
+ expect(wrapper.findAllComponents(BoardColumnDeprecated)).toHaveLength(
+ mockListsWithModel.length,
+ );
});
it('does not display EpicsSwimlanes component', () => {
@@ -70,6 +73,13 @@ describe('BoardContent', () => {
});
describe('graphqlBoardLists feature flag enabled', () => {
+ beforeEach(() => {
+ createComponent({ graphqlBoardListsEnabled: true });
+ gon.features = {
+ graphqlBoardLists: true,
+ };
+ });
+
describe('can admin list', () => {
beforeEach(() => {
createComponent({ graphqlBoardListsEnabled: true, props: { canAdminList: true } });
@@ -85,7 +95,7 @@ describe('BoardContent', () => {
createComponent({ graphqlBoardListsEnabled: true, props: { canAdminList: false } });
});
- it('renders draggable component', () => {
+ it('does not render draggable component', () => {
expect(wrapper.find(Draggable).exists()).toBe(false);
});
});
diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js
index 3b15cbb6b7e..c34987a55de 100644
--- a/spec/frontend/boards/components/board_form_spec.js
+++ b/spec/frontend/boards/components/board_form_spec.js
@@ -1,20 +1,22 @@
import { shallowMount } from '@vue/test-utils';
-import AxiosMockAdapter from 'axios-mock-adapter';
-import { TEST_HOST } from 'jest/helpers/test_constants';
+import { TEST_HOST } from 'helpers/test_constants';
import { GlModal } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
-import axios from '~/lib/utils/axios_utils';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import boardsStore from '~/boards/stores/boards_store';
import BoardForm from '~/boards/components/board_form.vue';
-import BoardConfigurationOptions from '~/boards/components/board_configuration_options.vue';
-import createBoardMutation from '~/boards/graphql/board.mutation.graphql';
+import updateBoardMutation from '~/boards/graphql/board_update.mutation.graphql';
+import createBoardMutation from '~/boards/graphql/board_create.mutation.graphql';
+import destroyBoardMutation from '~/boards/graphql/board_destroy.mutation.graphql';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn().mockName('visitUrlMock'),
+ stripFinalUrlSegment: jest.requireActual('~/lib/utils/url_utility').stripFinalUrlSegment,
}));
+jest.mock('~/flash');
const currentBoard = {
id: 1,
@@ -28,18 +30,6 @@ const currentBoard = {
hide_closed_list: false,
};
-const boardDefaults = {
- id: false,
- name: '',
- labels: [],
- milestone_id: undefined,
- assignee: {},
- assignee_id: undefined,
- weight: null,
- hide_backlog_list: false,
- hide_closed_list: false,
-};
-
const defaultProps = {
canAdminBoard: false,
labelsPath: `${TEST_HOST}/labels/path`,
@@ -47,22 +37,15 @@ const defaultProps = {
currentBoard,
};
-const endpoints = {
- boardsEndpoint: 'test-endpoint',
-};
-
-const mutate = jest.fn().mockResolvedValue({});
-
describe('BoardForm', () => {
let wrapper;
- let axiosMock;
+ let mutate;
const findModal = () => wrapper.find(GlModal);
const findModalActionPrimary = () => findModal().props('actionPrimary');
const findForm = () => wrapper.find('[data-testid="board-form"]');
const findFormWrapper = () => wrapper.find('[data-testid="board-form-wrapper"]');
const findDeleteConfirmation = () => wrapper.find('[data-testid="delete-confirmation-message"]');
- const findConfigurationOptions = () => wrapper.find(BoardConfigurationOptions);
const findInput = () => wrapper.find('#board-new-name');
const createComponent = (props, data) => {
@@ -74,26 +57,26 @@ describe('BoardForm', () => {
};
},
provide: {
- endpoints,
+ rootPath: 'root',
},
mocks: {
$apollo: {
mutate,
},
},
- attachToDocument: true,
+ attachTo: document.body,
});
};
beforeEach(() => {
- axiosMock = new AxiosMockAdapter(axios);
+ delete window.location;
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
- axiosMock.restore();
boardsStore.state.currentPage = null;
+ mutate = null;
});
describe('when user can not admin the board', () => {
@@ -145,7 +128,7 @@ describe('BoardForm', () => {
});
it('clears the form', () => {
- expect(findConfigurationOptions().props('board')).toEqual(boardDefaults);
+ expect(findInput().element.value).toBe('');
});
it('shows a correct title about creating a board', () => {
@@ -164,16 +147,21 @@ describe('BoardForm', () => {
it('renders form wrapper', () => {
expect(findFormWrapper().exists()).toBe(true);
});
-
- it('passes a true isNewForm prop to BoardConfigurationOptions component', () => {
- expect(findConfigurationOptions().props('isNewForm')).toBe(true);
- });
});
describe('when submitting a create event', () => {
+ const fillForm = () => {
+ findInput().value = 'Test name';
+ findInput().trigger('input');
+ findInput().trigger('keyup.enter', { metaKey: true });
+ };
+
beforeEach(() => {
- const url = `${endpoints.boardsEndpoint}.json`;
- axiosMock.onPost(url).reply(200, { id: '2', board_path: 'new path' });
+ mutate = jest.fn().mockResolvedValue({
+ data: {
+ createBoard: { board: { id: 'gid://gitlab/Board/123', webPath: 'test-path' } },
+ },
+ });
});
it('does not call API if board name is empty', async () => {
@@ -185,28 +173,37 @@ describe('BoardForm', () => {
expect(mutate).not.toHaveBeenCalled();
});
- it('calls REST and GraphQL API and redirects to correct page', async () => {
+ it('calls a correct GraphQL mutation and redirects to correct page from existing board', async () => {
createComponent({ canAdminBoard: true });
-
- findInput().value = 'Test name';
- findInput().trigger('input');
- findInput().trigger('keyup.enter', { metaKey: true });
+ fillForm();
await waitForPromises();
- expect(axiosMock.history.post[0].data).toBe(
- JSON.stringify({ board: { ...boardDefaults, name: 'test', label_ids: [''] } }),
- );
-
expect(mutate).toHaveBeenCalledWith({
mutation: createBoardMutation,
variables: {
- id: 'gid://gitlab/Board/2',
+ input: expect.objectContaining({
+ name: 'test',
+ }),
},
});
await waitForPromises();
- expect(visitUrl).toHaveBeenCalledWith('new path');
+ expect(visitUrl).toHaveBeenCalledWith('test-path');
+ });
+
+ it('shows an error flash if GraphQL mutation fails', async () => {
+ mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
+ createComponent({ canAdminBoard: true });
+ fillForm();
+
+ await waitForPromises();
+
+ expect(mutate).toHaveBeenCalled();
+
+ await waitForPromises();
+ expect(visitUrl).not.toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalled();
});
});
});
@@ -222,7 +219,7 @@ describe('BoardForm', () => {
});
it('clears the form', () => {
- expect(findConfigurationOptions().props('board')).toEqual(currentBoard);
+ expect(findInput().element.value).toEqual(currentBoard.name);
});
it('shows a correct title about creating a board', () => {
@@ -241,36 +238,121 @@ describe('BoardForm', () => {
it('renders form wrapper', () => {
expect(findFormWrapper().exists()).toBe(true);
});
+ });
- it('passes a false isNewForm prop to BoardConfigurationOptions component', () => {
- expect(findConfigurationOptions().props('isNewForm')).toBe(false);
+ it('calls GraphQL mutation with correct parameters when issues are not grouped', async () => {
+ mutate = jest.fn().mockResolvedValue({
+ data: {
+ updateBoard: { board: { id: 'gid://gitlab/Board/321', webPath: 'test-path' } },
+ },
});
- });
+ window.location = new URL('https://test/boards/1');
+ createComponent({ canAdminBoard: true });
- describe('when submitting an update event', () => {
- beforeEach(() => {
- const url = endpoints.boardsEndpoint;
- axiosMock.onPut(url).reply(200, { board_path: 'new path' });
+ findInput().trigger('keyup.enter', { metaKey: true });
+
+ await waitForPromises();
+
+ expect(mutate).toHaveBeenCalledWith({
+ mutation: updateBoardMutation,
+ variables: {
+ input: expect.objectContaining({
+ id: `gid://gitlab/Board/${currentBoard.id}`,
+ }),
+ },
});
- it('calls REST and GraphQL API with correct parameters', async () => {
- createComponent({ canAdminBoard: true });
+ await waitForPromises();
+ expect(visitUrl).toHaveBeenCalledWith('test-path');
+ });
- findInput().trigger('keyup.enter', { metaKey: true });
+ it('calls GraphQL mutation with correct parameters when issues are grouped by epic', async () => {
+ mutate = jest.fn().mockResolvedValue({
+ data: {
+ updateBoard: { board: { id: 'gid://gitlab/Board/321', webPath: 'test-path' } },
+ },
+ });
+ window.location = new URL('https://test/boards/1?group_by=epic');
+ createComponent({ canAdminBoard: true });
- await waitForPromises();
+ findInput().trigger('keyup.enter', { metaKey: true });
- expect(axiosMock.history.put[0].data).toBe(
- JSON.stringify({ board: { ...currentBoard, label_ids: [''] } }),
- );
+ await waitForPromises();
- expect(mutate).toHaveBeenCalledWith({
- mutation: createBoardMutation,
- variables: {
+ expect(mutate).toHaveBeenCalledWith({
+ mutation: updateBoardMutation,
+ variables: {
+ input: expect.objectContaining({
id: `gid://gitlab/Board/${currentBoard.id}`,
- },
- });
+ }),
+ },
});
+
+ await waitForPromises();
+ expect(visitUrl).toHaveBeenCalledWith('test-path?group_by=epic');
+ });
+
+ it('shows an error flash if GraphQL mutation fails', async () => {
+ mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
+ createComponent({ canAdminBoard: true });
+ findInput().trigger('keyup.enter', { metaKey: true });
+
+ await waitForPromises();
+
+ expect(mutate).toHaveBeenCalled();
+
+ await waitForPromises();
+ expect(visitUrl).not.toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalled();
+ });
+ });
+
+ describe('when deleting a board', () => {
+ beforeEach(() => {
+ boardsStore.state.currentPage = 'delete';
+ });
+
+ it('passes correct primary action text and variant', () => {
+ createComponent({ canAdminBoard: true });
+ expect(findModalActionPrimary().text).toBe('Delete');
+ expect(findModalActionPrimary().attributes[0].variant).toBe('danger');
+ });
+
+ it('renders delete confirmation message', () => {
+ createComponent({ canAdminBoard: true });
+ expect(findDeleteConfirmation().exists()).toBe(true);
+ });
+
+ it('calls a correct GraphQL mutation and redirects to correct page after deleting board', async () => {
+ mutate = jest.fn().mockResolvedValue({});
+ createComponent({ canAdminBoard: true });
+ findModal().vm.$emit('primary');
+
+ await waitForPromises();
+
+ expect(mutate).toHaveBeenCalledWith({
+ mutation: destroyBoardMutation,
+ variables: {
+ id: 'gid://gitlab/Board/1',
+ },
+ });
+
+ await waitForPromises();
+ expect(visitUrl).toHaveBeenCalledWith('root');
+ });
+
+ it('shows an error flash if GraphQL mutation fails', async () => {
+ mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
+ createComponent({ canAdminBoard: true });
+ findModal().vm.$emit('primary');
+
+ await waitForPromises();
+
+ expect(mutate).toHaveBeenCalled();
+
+ await waitForPromises();
+ expect(visitUrl).not.toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/boards/components/board_list_header_new_spec.js b/spec/frontend/boards/components/board_list_header_deprecated_spec.js
index 7428dfae83f..6207724e6a9 100644
--- a/spec/frontend/boards/components/board_list_header_new_spec.js
+++ b/spec/frontend/boards/components/board_list_header_deprecated_spec.js
@@ -1,23 +1,28 @@
-import Vuex from 'vuex';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-
-import { mockLabelList } from 'jest/boards/mock_data';
-import BoardListHeader from '~/boards/components/board_list_header_new.vue';
+import Vue from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import AxiosMockAdapter from 'axios-mock-adapter';
+
+import { TEST_HOST } from 'helpers/test_constants';
+import { listObj } from 'jest/boards/mock_data';
+import BoardListHeader from '~/boards/components/board_list_header_deprecated.vue';
+import List from '~/boards/models/list';
import { ListType } from '~/boards/constants';
-
-const localVue = createLocalVue();
-
-localVue.use(Vuex);
+import axios from '~/lib/utils/axios_utils';
describe('Board List Header Component', () => {
let wrapper;
- let store;
+ let axiosMock;
- const updateListSpy = jest.fn();
+ beforeEach(() => {
+ window.gon = {};
+ axiosMock = new AxiosMockAdapter(axios);
+ axiosMock.onGet(`${TEST_HOST}/lists/1/issues`).reply(200, { issues: [] });
+ });
afterEach(() => {
+ axiosMock.restore();
+
wrapper.destroy();
- wrapper = null;
localStorage.clear();
});
@@ -26,76 +31,65 @@ describe('Board List Header Component', () => {
listType = ListType.backlog,
collapsed = false,
withLocalStorage = true,
- currentUserId = null,
} = {}) => {
const boardId = '1';
const listMock = {
- ...mockLabelList,
- listType,
+ ...listObj,
+ list_type: listType,
collapsed,
};
if (listType === ListType.assignee) {
delete listMock.label;
- listMock.assignee = {};
+ listMock.user = {};
}
+ // Making List reactive
+ const list = Vue.observable(new List(listMock));
+
if (withLocalStorage) {
localStorage.setItem(
- `boards.${boardId}.${listMock.listType}.${listMock.id}.expanded`,
+ `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: listMock,
+ list,
},
provide: {
boardId,
- weightFeatureAvailable: false,
- currentUserId,
},
});
};
- const isCollapsed = () => wrapper.vm.list.collapsed;
- const isExpanded = () => !isCollapsed;
+ const isCollapsed = () => !wrapper.props().list.isExpanded;
+ const isExpanded = () => wrapper.vm.list.isExpanded;
const findAddIssueButton = () => wrapper.find({ ref: 'newIssueBtn' });
- const findTitle = () => wrapper.find('.board-title');
const findCaret = () => wrapper.find('.board-title-caret');
describe('Add issue button', () => {
const hasNoAddButton = [ListType.closed];
const hasAddButton = [ListType.backlog, ListType.label, ListType.milestone, ListType.assignee];
- it.each(hasNoAddButton)('does not render when List Type is `%s`', listType => {
+ 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 => {
+ 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 => {
+ Object.values(ListType).forEach((value) => {
expect([...hasAddButton, ...hasNoAddButton]).toContain(value);
});
});
@@ -108,80 +102,64 @@ describe('Board List Header Component', () => {
});
describe('expanding / collapsing the column', () => {
- it('does not collapse when clicking the header', async () => {
+ it('does not collapse when clicking the header', () => {
createComponent();
expect(isCollapsed()).toBe(false);
-
wrapper.find('[data-testid="board-list-header"]').trigger('click');
- await wrapper.vm.$nextTick();
-
- expect(isCollapsed()).toBe(false);
+ return wrapper.vm.$nextTick().then(() => {
+ expect(isCollapsed()).toBe(false);
+ });
});
- it('collapses expanded Column when clicking the collapse icon', async () => {
+ it('collapses expanded Column when clicking the collapse icon', () => {
createComponent();
- expect(isCollapsed()).toBe(false);
-
+ expect(isExpanded()).toBe(true);
findCaret().vm.$emit('click');
- await wrapper.vm.$nextTick();
-
- expect(isCollapsed()).toBe(true);
+ return wrapper.vm.$nextTick().then(() => {
+ expect(isCollapsed()).toBe(true);
+ });
});
- it('expands collapsed Column when clicking the expand icon', async () => {
+ it('expands collapsed Column when clicking the expand icon', () => {
createComponent({ collapsed: true });
expect(isCollapsed()).toBe(true);
-
findCaret().vm.$emit('click');
- await wrapper.vm.$nextTick();
-
- expect(isCollapsed()).toBe(false);
+ return wrapper.vm.$nextTick().then(() => {
+ 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 in it calls list update and doesn't set localStorage", () => {
+ jest.spyOn(List.prototype, 'update');
+ window.gon.current_user_id = 1;
- it("when logged out it doesn't call list update and sets localStorage", async () => {
- createComponent();
+ createComponent({ withLocalStorage: false });
findCaret().vm.$emit('click');
- await wrapper.vm.$nextTick();
- expect(updateListSpy).not.toHaveBeenCalled();
- expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(String(isExpanded()));
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.list.update).toHaveBeenCalledTimes(1);
+ expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(null);
+ });
});
- });
- describe('user can drag', () => {
- const cannotDragList = [ListType.backlog, ListType.closed];
- const canDragList = [ListType.label, ListType.milestone, ListType.assignee];
+ it("when logged out it doesn't call list update and sets localStorage", () => {
+ jest.spyOn(List.prototype, 'update');
- it.each(cannotDragList)(
- 'does not have user-can-drag-class so user cannot drag list',
- listType => {
- createComponent({ listType });
-
- expect(findTitle().classes()).not.toContain('user-can-drag');
- },
- );
+ createComponent();
- it.each(canDragList)('has user-can-drag-class so user can drag list', listType => {
- createComponent({ listType });
+ findCaret().vm.$emit('click');
- expect(findTitle().classes()).toContain('user-can-drag');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.list.update).not.toHaveBeenCalled();
+ expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(String(isExpanded()));
+ });
});
});
});
diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js
index 656a503bb86..357d05ced02 100644
--- a/spec/frontend/boards/components/board_list_header_spec.js
+++ b/spec/frontend/boards/components/board_list_header_spec.js
@@ -1,28 +1,23 @@
-import Vue from 'vue';
-import { shallowMount } from '@vue/test-utils';
-import AxiosMockAdapter from 'axios-mock-adapter';
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { TEST_HOST } from 'helpers/test_constants';
-import { listObj } from 'jest/boards/mock_data';
+import { mockLabelList } from 'jest/boards/mock_data';
import BoardListHeader from '~/boards/components/board_list_header.vue';
-import List from '~/boards/models/list';
import { ListType } from '~/boards/constants';
-import axios from '~/lib/utils/axios_utils';
+
+const localVue = createLocalVue();
+
+localVue.use(Vuex);
describe('Board List Header Component', () => {
let wrapper;
- let axiosMock;
+ let store;
- beforeEach(() => {
- window.gon = {};
- axiosMock = new AxiosMockAdapter(axios);
- axiosMock.onGet(`${TEST_HOST}/lists/1/issues`).reply(200, { issues: [] });
- });
+ const updateListSpy = jest.fn();
afterEach(() => {
- axiosMock.restore();
-
wrapper.destroy();
+ wrapper = null;
localStorage.clear();
});
@@ -31,65 +26,76 @@ describe('Board List Header Component', () => {
listType = ListType.backlog,
collapsed = false,
withLocalStorage = true,
+ currentUserId = null,
} = {}) => {
const boardId = '1';
const listMock = {
- ...listObj,
- list_type: listType,
+ ...mockLabelList,
+ listType,
collapsed,
};
if (listType === ListType.assignee) {
delete listMock.label;
- listMock.user = {};
+ listMock.assignee = {};
}
- // Making List reactive
- const list = Vue.observable(new List(listMock));
-
if (withLocalStorage) {
localStorage.setItem(
- `boards.${boardId}.${list.type}.${list.id}.expanded`,
+ `boards.${boardId}.${listMock.listType}.${listMock.id}.expanded`,
(!collapsed).toString(),
);
}
+ store = new Vuex.Store({
+ state: {},
+ actions: { updateList: updateListSpy },
+ getters: {},
+ });
+
wrapper = shallowMount(BoardListHeader, {
+ store,
+ localVue,
propsData: {
disabled: false,
- list,
+ list: listMock,
},
provide: {
boardId,
+ weightFeatureAvailable: false,
+ currentUserId,
},
});
};
- const isCollapsed = () => !wrapper.props().list.isExpanded;
- const isExpanded = () => wrapper.vm.list.isExpanded;
+ const isCollapsed = () => wrapper.vm.list.collapsed;
+ const isExpanded = () => !isCollapsed;
const findAddIssueButton = () => wrapper.find({ ref: 'newIssueBtn' });
+ const findTitle = () => wrapper.find('.board-title');
const findCaret = () => wrapper.find('.board-title-caret');
describe('Add issue button', () => {
const hasNoAddButton = [ListType.closed];
const hasAddButton = [ListType.backlog, ListType.label, ListType.milestone, ListType.assignee];
- it.each(hasNoAddButton)('does not render when List Type is `%s`', listType => {
+ 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 => {
+ 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', () => {
- Object.values(ListType).forEach(value => {
+ createComponent();
+
+ Object.values(ListType).forEach((value) => {
expect([...hasAddButton, ...hasNoAddButton]).toContain(value);
});
});
@@ -102,64 +108,80 @@ describe('Board List Header Component', () => {
});
describe('expanding / collapsing the column', () => {
- it('does not collapse when clicking the header', () => {
+ it('does not collapse when clicking the header', async () => {
createComponent();
expect(isCollapsed()).toBe(false);
+
wrapper.find('[data-testid="board-list-header"]').trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(isCollapsed()).toBe(false);
- });
+ await wrapper.vm.$nextTick();
+
+ expect(isCollapsed()).toBe(false);
});
- it('collapses expanded Column when clicking the collapse icon', () => {
+ it('collapses expanded Column when clicking the collapse icon', async () => {
createComponent();
- expect(isExpanded()).toBe(true);
+ expect(isCollapsed()).toBe(false);
+
findCaret().vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(isCollapsed()).toBe(true);
- });
+ await wrapper.vm.$nextTick();
+
+ expect(isCollapsed()).toBe(true);
});
- it('expands collapsed Column when clicking the expand icon', () => {
+ it('expands collapsed Column when clicking the expand icon', async () => {
createComponent({ collapsed: true });
expect(isCollapsed()).toBe(true);
+
findCaret().vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(isCollapsed()).toBe(false);
- });
- });
+ await wrapper.vm.$nextTick();
- it("when logged in it calls list update and doesn't set localStorage", () => {
- jest.spyOn(List.prototype, 'update');
- window.gon.current_user_id = 1;
+ expect(isCollapsed()).toBe(false);
+ });
- createComponent({ withLocalStorage: 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();
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.list.update).toHaveBeenCalledTimes(1);
- expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(null);
- });
+ 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", () => {
- jest.spyOn(List.prototype, 'update');
-
+ it("when logged out it doesn't call list update and sets localStorage", async () => {
createComponent();
findCaret().vm.$emit('click');
+ await wrapper.vm.$nextTick();
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.list.update).not.toHaveBeenCalled();
- expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(String(isExpanded()));
- });
+ expect(updateListSpy).not.toHaveBeenCalled();
+ expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(String(isExpanded()));
+ });
+ });
+
+ describe('user can drag', () => {
+ const cannotDragList = [ListType.backlog, ListType.closed];
+ const canDragList = [ListType.label, ListType.milestone, ListType.assignee];
+
+ it.each(cannotDragList)(
+ 'does not have user-can-drag-class so user cannot drag list',
+ (listType) => {
+ createComponent({ listType });
+
+ expect(findTitle().classes()).not.toContain('user-can-drag');
+ },
+ );
+
+ it.each(canDragList)('has user-can-drag-class so user can drag list', (listType) => {
+ createComponent({ listType });
+
+ expect(findTitle().classes()).toContain('user-can-drag');
});
});
});
diff --git a/spec/frontend/boards/components/board_new_issue_new_spec.js b/spec/frontend/boards/components/board_new_issue_spec.js
index ee1c4f31cf0..5a01221a5be 100644
--- a/spec/frontend/boards/components/board_new_issue_new_spec.js
+++ b/spec/frontend/boards/components/board_new_issue_spec.js
@@ -1,9 +1,9 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import BoardNewIssue from '~/boards/components/board_new_issue_new.vue';
+import BoardNewIssue from '~/boards/components/board_new_issue.vue';
import '~/boards/models/list';
-import { mockList } from '../mock_data';
+import { mockList, mockGroupProjects } from '../mock_data';
const localVue = createLocalVue();
@@ -29,7 +29,7 @@ describe('Issue boards new issue form', () => {
beforeEach(() => {
const store = new Vuex.Store({
- state: {},
+ state: { selectedProject: mockGroupProjects[0] },
actions: { addListNewIssue: addListNewIssuesSpy },
getters: {},
});
diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js
index db3c8c22950..81575bf486a 100644
--- a/spec/frontend/boards/components/boards_selector_spec.js
+++ b/spec/frontend/boards/components/boards_selector_spec.js
@@ -26,7 +26,7 @@ describe('BoardsSelector', () => {
const boards = boardGenerator(20);
const recentBoards = boardGenerator(5);
- const fillSearchBox = filterTerm => {
+ const fillSearchBox = (filterTerm) => {
const searchBox = wrapper.find({ ref: 'searchBox' });
const searchBoxInput = searchBox.find('input');
searchBoxInput.setValue(filterTerm);
@@ -59,7 +59,7 @@ describe('BoardsSelector', () => {
data: {
group: {
boards: {
- edges: boards.map(board => ({ node: board })),
+ edges: boards.map((board) => ({ node: board })),
},
},
},
@@ -94,7 +94,7 @@ describe('BoardsSelector', () => {
weights: [],
},
mocks: { $apollo },
- attachToDocument: true,
+ attachTo: document.body,
});
wrapper.vm.$apollo.addSmartQuery = jest.fn((_, options) => {
@@ -152,7 +152,7 @@ describe('BoardsSelector', () => {
it('shows only matching boards when filtering', () => {
const filterTerm = 'board1';
- const expectedCount = boards.filter(board => board.name.includes(filterTerm)).length;
+ const expectedCount = boards.filter((board) => board.name.includes(filterTerm)).length;
fillSearchBox(filterTerm);
diff --git a/spec/frontend/boards/components/issue_count_spec.js b/spec/frontend/boards/components/issue_count_spec.js
index d1ff0bdbf88..f1870e9cc9e 100644
--- a/spec/frontend/boards/components/issue_count_spec.js
+++ b/spec/frontend/boards/components/issue_count_spec.js
@@ -6,7 +6,7 @@ describe('IssueCount', () => {
let maxIssueCount;
let issuesSize;
- const createComponent = props => {
+ const createComponent = (props) => {
vm = shallowMount(IssueCount, { propsData: props });
};
diff --git a/spec/frontend/boards/components/issue_due_date_spec.js b/spec/frontend/boards/components/issue_due_date_spec.js
index 880859287e1..73340c1b96b 100644
--- a/spec/frontend/boards/components/issue_due_date_spec.js
+++ b/spec/frontend/boards/components/issue_due_date_spec.js
@@ -10,7 +10,7 @@ const createComponent = (dueDate = new Date(), closed = false) =>
},
});
-const findTime = wrapper => wrapper.find('time');
+const findTime = (wrapper) => wrapper.find('time');
describe('Issue Due Date component', () => {
let wrapper;
diff --git a/spec/frontend/boards/components/issue_time_estimate_deprecated_spec.js b/spec/frontend/boards/components/issue_time_estimate_deprecated_spec.js
new file mode 100644
index 00000000000..fafebaf3a4e
--- /dev/null
+++ b/spec/frontend/boards/components/issue_time_estimate_deprecated_spec.js
@@ -0,0 +1,64 @@
+import { shallowMount } from '@vue/test-utils';
+import IssueTimeEstimate from '~/boards/components/issue_time_estimate_deprecated.vue';
+import boardsStore from '~/boards/stores/boards_store';
+
+describe('Issue Time Estimate component', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ boardsStore.create();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when limitToHours is false', () => {
+ beforeEach(() => {
+ boardsStore.timeTracking.limitToHours = false;
+ wrapper = shallowMount(IssueTimeEstimate, {
+ propsData: {
+ estimate: 374460,
+ },
+ });
+ });
+
+ it('renders the correct time estimate', () => {
+ expect(wrapper.find('time').text().trim()).toEqual('2w 3d 1m');
+ });
+
+ it('renders expanded time estimate in tooltip', () => {
+ expect(wrapper.find('.js-issue-time-estimate').text()).toContain('2 weeks 3 days 1 minute');
+ });
+
+ it('prevents tooltip xss', (done) => {
+ const alertSpy = jest.spyOn(window, 'alert');
+ wrapper.setProps({ estimate: 'Foo <script>alert("XSS")</script>' });
+ wrapper.vm.$nextTick(() => {
+ expect(alertSpy).not.toHaveBeenCalled();
+ expect(wrapper.find('time').text().trim()).toEqual('0m');
+ expect(wrapper.find('.js-issue-time-estimate').text()).toContain('0m');
+ done();
+ });
+ });
+ });
+
+ describe('when limitToHours is true', () => {
+ beforeEach(() => {
+ boardsStore.timeTracking.limitToHours = true;
+ wrapper = shallowMount(IssueTimeEstimate, {
+ propsData: {
+ estimate: 374460,
+ },
+ });
+ });
+
+ it('renders the correct time estimate', () => {
+ expect(wrapper.find('time').text().trim()).toEqual('104h 1m');
+ });
+
+ it('renders expanded time estimate in tooltip', () => {
+ expect(wrapper.find('.js-issue-time-estimate').text()).toContain('104 hours 1 minute');
+ });
+ });
+});
diff --git a/spec/frontend/boards/components/issue_time_estimate_spec.js b/spec/frontend/boards/components/issue_time_estimate_spec.js
index 162a6df828b..9ac8fae3fcc 100644
--- a/spec/frontend/boards/components/issue_time_estimate_spec.js
+++ b/spec/frontend/boards/components/issue_time_estimate_spec.js
@@ -1,75 +1,65 @@
+import { config as vueConfig } from 'vue';
import { shallowMount } from '@vue/test-utils';
import IssueTimeEstimate from '~/boards/components/issue_time_estimate.vue';
-import boardsStore from '~/boards/stores/boards_store';
describe('Issue Time Estimate component', () => {
let wrapper;
- beforeEach(() => {
- boardsStore.create();
- });
-
afterEach(() => {
wrapper.destroy();
});
describe('when limitToHours is false', () => {
beforeEach(() => {
- boardsStore.timeTracking.limitToHours = false;
wrapper = shallowMount(IssueTimeEstimate, {
propsData: {
estimate: 374460,
},
+ provide: {
+ timeTrackingLimitToHours: false,
+ },
});
});
it('renders the correct time estimate', () => {
- expect(
- wrapper
- .find('time')
- .text()
- .trim(),
- ).toEqual('2w 3d 1m');
+ expect(wrapper.find('time').text().trim()).toEqual('2w 3d 1m');
});
it('renders expanded time estimate in tooltip', () => {
expect(wrapper.find('.js-issue-time-estimate').text()).toContain('2 weeks 3 days 1 minute');
});
- it('prevents tooltip xss', done => {
+ it('prevents tooltip xss', async () => {
const alertSpy = jest.spyOn(window, 'alert');
- wrapper.setProps({ estimate: 'Foo <script>alert("XSS")</script>' });
- wrapper.vm.$nextTick(() => {
- expect(alertSpy).not.toHaveBeenCalled();
- expect(
- wrapper
- .find('time')
- .text()
- .trim(),
- ).toEqual('0m');
- expect(wrapper.find('.js-issue-time-estimate').text()).toContain('0m');
- done();
- });
+
+ try {
+ // This will raise props validating warning by Vue, silencing it
+ vueConfig.silent = true;
+ await wrapper.setProps({ estimate: 'Foo <script>alert("XSS")</script>' });
+ } finally {
+ vueConfig.silent = false;
+ }
+
+ expect(alertSpy).not.toHaveBeenCalled();
+ expect(wrapper.find('time').text().trim()).toEqual('0m');
+ expect(wrapper.find('.js-issue-time-estimate').text()).toContain('0m');
});
});
describe('when limitToHours is true', () => {
beforeEach(() => {
- boardsStore.timeTracking.limitToHours = true;
wrapper = shallowMount(IssueTimeEstimate, {
propsData: {
estimate: 374460,
},
+ provide: {
+ timeTrackingLimitToHours: true,
+ },
});
});
it('renders the correct time estimate', () => {
- expect(
- wrapper
- .find('time')
- .text()
- .trim(),
- ).toEqual('104h 1m');
+ expect(wrapper.find('time').text().trim()).toEqual('104h 1m');
});
it('renders expanded time estimate in tooltip', () => {
diff --git a/spec/frontend/boards/components/sidebar/board_editable_item_spec.js b/spec/frontend/boards/components/sidebar/board_editable_item_spec.js
index d7df2ff1563..de414bb929e 100644
--- a/spec/frontend/boards/components/sidebar/board_editable_item_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_editable_item_spec.js
@@ -33,6 +33,14 @@ describe('boards sidebar remove issue', () => {
expect(findTitle().text()).toBe(title);
});
+ it('renders provided title slot', () => {
+ const title = 'Sidebar item title on slot';
+ const slots = { title: `<strong>${title}</strong>` };
+ createComponent({ slots });
+
+ expect(wrapper.text()).toContain(title);
+ });
+
it('hides edit button, loader and expanded content by default', () => {
createComponent();
@@ -74,9 +82,19 @@ describe('boards sidebar remove issue', () => {
return wrapper.vm.$nextTick().then(() => {
expect(findCollapsed().isVisible()).toBe(false);
expect(findExpanded().isVisible()).toBe(true);
- expect(findExpanded().text()).toBe('Select item');
});
});
+
+ it('hides the header while editing if `toggleHeader` is true', async () => {
+ createComponent({ canUpdate: true, props: { toggleHeader: true } });
+ findEditButton().vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+
+ expect(findEditButton().isVisible()).toBe(false);
+ expect(findTitle().isVisible()).toBe(false);
+ expect(findExpanded().isVisible()).toBe(true);
+ });
});
describe('collapsing an item by offclicking', () => {
@@ -96,12 +114,13 @@ describe('boards sidebar remove issue', () => {
expect(findExpanded().isVisible()).toBe(false);
});
- it('emits close event', async () => {
+ it('emits events', async () => {
document.body.click();
await wrapper.vm.$nextTick();
- expect(wrapper.emitted().close.length).toBe(1);
+ expect(wrapper.emitted().close).toHaveLength(1);
+ expect(wrapper.emitted()['off-click']).toHaveLength(1);
});
});
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_issue_title_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_issue_title_spec.js
new file mode 100644
index 00000000000..86895c648a4
--- /dev/null
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_issue_title_spec.js
@@ -0,0 +1,182 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlAlert, GlFormInput, GlForm } from '@gitlab/ui';
+import BoardSidebarIssueTitle from '~/boards/components/sidebar/board_sidebar_issue_title.vue';
+import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
+import createFlash from '~/flash';
+import { createStore } from '~/boards/stores';
+
+const TEST_TITLE = 'New issue title';
+const TEST_ISSUE_A = {
+ id: 'gid://gitlab/Issue/1',
+ iid: 8,
+ title: 'Issue 1',
+ referencePath: 'h/b#1',
+};
+const TEST_ISSUE_B = {
+ id: 'gid://gitlab/Issue/2',
+ iid: 9,
+ title: 'Issue 2',
+ referencePath: 'h/b#2',
+};
+
+jest.mock('~/flash');
+
+describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
+ let wrapper;
+ let store;
+
+ afterEach(() => {
+ localStorage.clear();
+ wrapper.destroy();
+ store = null;
+ wrapper = null;
+ });
+
+ const createWrapper = (issue = TEST_ISSUE_A) => {
+ store = createStore();
+ store.state.issues = { [issue.id]: { ...issue } };
+ store.dispatch('setActiveId', { id: issue.id });
+
+ wrapper = shallowMount(BoardSidebarIssueTitle, {
+ store,
+ provide: {
+ canUpdate: true,
+ },
+ stubs: {
+ 'board-editable-item': BoardEditableItem,
+ },
+ });
+ };
+
+ const findForm = () => wrapper.find(GlForm);
+ const findAlert = () => wrapper.find(GlAlert);
+ 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 findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
+
+ it('renders title and reference', () => {
+ createWrapper();
+
+ expect(findTitle().text()).toContain(TEST_ISSUE_A.title);
+ expect(findCollapsed().text()).toContain(TEST_ISSUE_A.referencePath);
+ });
+
+ it('does not render alert', () => {
+ createWrapper();
+
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ describe('when new title is submitted', () => {
+ beforeEach(async () => {
+ createWrapper();
+
+ jest.spyOn(wrapper.vm, 'setActiveIssueTitle').mockImplementation(() => {
+ store.state.issues[TEST_ISSUE_A.id].title = TEST_TITLE;
+ });
+ findFormInput().vm.$emit('input', TEST_TITLE);
+ findForm().vm.$emit('submit', { preventDefault: () => {} });
+ await wrapper.vm.$nextTick();
+ });
+
+ it('collapses sidebar and renders new title', () => {
+ expect(findCollapsed().isVisible()).toBe(true);
+ expect(findTitle().text()).toContain(TEST_TITLE);
+ });
+
+ it('commits change to the server', () => {
+ expect(wrapper.vm.setActiveIssueTitle).toHaveBeenCalledWith({
+ title: TEST_TITLE,
+ projectPath: 'h/b',
+ });
+ });
+ });
+
+ describe('when submitting and invalid title', () => {
+ beforeEach(async () => {
+ createWrapper();
+
+ jest.spyOn(wrapper.vm, 'setActiveIssueTitle').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();
+ });
+ });
+
+ describe('when abandoning the form without saving', () => {
+ beforeEach(async () => {
+ createWrapper();
+
+ wrapper.vm.$refs.sidebarItem.expand();
+ findFormInput().vm.$emit('input', TEST_TITLE);
+ findEditableItem().vm.$emit('off-click');
+ await wrapper.vm.$nextTick();
+ });
+
+ 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(
+ TEST_TITLE,
+ );
+ });
+ });
+
+ describe('when accessing the form with pending changes', () => {
+ beforeAll(() => {
+ localStorage.setItem(`${TEST_ISSUE_A.id}/issue-title-pending-changes`, TEST_TITLE);
+
+ createWrapper();
+ });
+
+ it('sets title, expands item and shows alert', async () => {
+ expect(wrapper.vm.title).toBe(TEST_TITLE);
+ expect(findCollapsed().isVisible()).toBe(false);
+ expect(findAlert().exists()).toBe(true);
+ });
+ });
+
+ describe('when cancel button is clicked', () => {
+ beforeEach(async () => {
+ createWrapper(TEST_ISSUE_B);
+
+ jest.spyOn(wrapper.vm, 'setActiveIssueTitle').mockImplementation(() => {
+ store.state.issues[TEST_ISSUE_B.id].title = TEST_TITLE;
+ });
+ findFormInput().vm.$emit('input', TEST_TITLE);
+ findCancelButton().vm.$emit('click');
+ await wrapper.vm.$nextTick();
+ });
+
+ it('collapses sidebar and render former title', () => {
+ expect(wrapper.vm.setActiveIssueTitle).not.toHaveBeenCalled();
+ expect(findCollapsed().isVisible()).toBe(true);
+ expect(findTitle().text()).toBe(TEST_ISSUE_B.title);
+ });
+ });
+
+ describe('when the mutation fails', () => {
+ beforeEach(async () => {
+ createWrapper(TEST_ISSUE_B);
+
+ jest.spyOn(wrapper.vm, 'setActiveIssueTitle').mockImplementation(() => {
+ throw new Error(['failed mutation']);
+ });
+ findFormInput().vm.$emit('input', 'Invalid title');
+ findForm().vm.$emit('submit', { preventDefault: () => {} });
+ await wrapper.vm.$nextTick();
+ });
+
+ it('collapses sidebar and renders former issue title', () => {
+ expect(findCollapsed().isVisible()).toBe(true);
+ expect(findTitle().text()).toContain(TEST_ISSUE_B.title);
+ expect(createFlash).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 da000d21f6a..2342caa9dfd 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
@@ -10,8 +10,8 @@ import createFlash from '~/flash';
jest.mock('~/flash');
-const TEST_LABELS_PAYLOAD = TEST_LABELS.map(label => ({ ...label, set: true }));
-const TEST_LABELS_TITLES = TEST_LABELS.map(label => label.title);
+const TEST_LABELS_PAYLOAD = TEST_LABELS.map((label) => ({ ...label, set: true }));
+const TEST_LABELS_TITLES = TEST_LABELS.map((label) => label.title);
describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => {
let wrapper;
@@ -37,14 +37,15 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => {
labelsFilterBasePath: TEST_HOST,
},
stubs: {
- 'board-editable-item': BoardEditableItem,
- 'labels-select': '<div></div>',
+ BoardEditableItem,
+ LabelsSelect: true,
},
});
};
const findLabelsSelect = () => wrapper.find({ ref: 'labelsSelect' });
- const findLabelsTitles = () => wrapper.findAll(GlLabel).wrappers.map(item => item.props('title'));
+ const findLabelsTitles = () =>
+ wrapper.findAll(GlLabel).wrappers.map((item) => item.props('title'));
const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
it('renders "None" when no labels are selected', () => {
@@ -76,7 +77,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => {
it('commits change to the server', () => {
expect(wrapper.vm.setActiveIssueLabels).toHaveBeenCalledWith({
- addLabelIds: TEST_LABELS.map(label => label.id),
+ addLabelIds: TEST_LABELS.map((label) => label.id),
projectPath: 'gitlab-org/test-subgroup/gitlab-test',
removeLabelIds: [],
});
@@ -84,7 +85,10 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => {
});
describe('when labels are updated over existing labels', () => {
- const testLabelsPayload = [{ id: 5, set: true }, { id: 7, set: true }];
+ const testLabelsPayload = [
+ { id: 5, set: true },
+ { id: 7, set: true },
+ ];
const expectedLabels = [{ id: 5 }, { id: 7 }];
beforeEach(async () => {
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 ee54c662167..b1df0f2d771 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js
@@ -83,7 +83,7 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () =
});
describe('Board sidebar subscription component `behavior`', () => {
- const mockSetActiveIssueSubscribed = subscribedState => {
+ const mockSetActiveIssueSubscribed = (subscribedState) => {
jest.spyOn(wrapper.vm, 'setActiveIssueSubscribed').mockImplementation(async () => {
store.commit(types.UPDATE_ISSUE_BY_ID, {
issueId: mockActiveIssue.id,
diff --git a/spec/frontend/boards/components/sidebar/remove_issue_spec.js b/spec/frontend/boards/components/sidebar/remove_issue_spec.js
index a33e4046724..1b7a78e6e58 100644
--- a/spec/frontend/boards/components/sidebar/remove_issue_spec.js
+++ b/spec/frontend/boards/components/sidebar/remove_issue_spec.js
@@ -8,7 +8,7 @@ describe('boards sidebar remove issue', () => {
const findButton = () => wrapper.find(GlButton);
- const createComponent = propsData => {
+ const createComponent = (propsData) => {
wrapper = shallowMount(RemoveIssue, {
propsData: {
issue: {},
diff --git a/spec/frontend/boards/issue_card_spec.js b/spec/frontend/boards/issue_card_deprecated_spec.js
index 7e22e9647f0..fd7b0edb97e 100644
--- a/spec/frontend/boards/issue_card_spec.js
+++ b/spec/frontend/boards/issue_card_deprecated_spec.js
@@ -6,7 +6,7 @@ import '~/boards/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
import { GlLabel } from '@gitlab/ui';
-import IssueCardInner from '~/boards/components/issue_card_inner.vue';
+import IssueCardInner from '~/boards/components/issue_card_inner_deprecated.vue';
import { listObj } from './mock_data';
import store from '~/boards/stores';
@@ -79,7 +79,7 @@ describe('Issue card component', () => {
expect(wrapper.find('.issue-blocked-icon').exists()).toBe(false);
});
- it('renders confidential icon', done => {
+ it('renders confidential icon', (done) => {
wrapper.setProps({
issue: {
...wrapper.props('issue'),
@@ -102,7 +102,7 @@ describe('Issue card component', () => {
});
describe('exists', () => {
- beforeEach(done => {
+ beforeEach((done) => {
wrapper.setProps({
issue: {
...wrapper.props('issue'),
@@ -132,7 +132,7 @@ describe('Issue card component', () => {
expect(wrapper.find('.board-card-assignee img').exists()).toBe(true);
});
- it('renders the avatar using avatar_url property', done => {
+ it('renders the avatar using avatar_url property', (done) => {
wrapper.props('issue').updateData({
...wrapper.props('issue'),
assignees: [
@@ -156,7 +156,7 @@ describe('Issue card component', () => {
});
describe('assignee default avatar', () => {
- beforeEach(done => {
+ beforeEach((done) => {
global.gon.default_avatar_url = 'default_avatar';
wrapper.setProps({
@@ -189,7 +189,7 @@ describe('Issue card component', () => {
});
describe('multiple assignees', () => {
- beforeEach(done => {
+ beforeEach((done) => {
wrapper.setProps({
issue: {
...wrapper.props('issue'),
@@ -224,7 +224,7 @@ describe('Issue card component', () => {
});
describe('more than three assignees', () => {
- beforeEach(done => {
+ beforeEach((done) => {
const { assignees } = wrapper.props('issue');
assignees.push(
new ListAssignee({
@@ -245,23 +245,18 @@ describe('Issue card component', () => {
});
it('renders more avatar counter', () => {
- expect(
- wrapper
- .find('.board-card-assignee .avatar-counter')
- .text()
- .trim(),
- ).toEqual('+2');
+ expect(wrapper.find('.board-card-assignee .avatar-counter').text().trim()).toEqual('+2');
});
it('renders two assignees', () => {
expect(wrapper.findAll('.board-card-assignee .avatar').length).toEqual(2);
});
- it('renders 99+ avatar counter', done => {
+ it('renders 99+ avatar counter', (done) => {
const assignees = [
...wrapper.props('issue').assignees,
...range(5, 103).map(
- i =>
+ (i) =>
new ListAssignee({
id: i,
name: 'name',
@@ -278,12 +273,7 @@ describe('Issue card component', () => {
});
wrapper.vm.$nextTick(() => {
- expect(
- wrapper
- .find('.board-card-assignee .avatar-counter')
- .text()
- .trim(),
- ).toEqual('99+');
+ expect(wrapper.find('.board-card-assignee .avatar-counter').text().trim()).toEqual('99+');
done();
});
});
@@ -291,7 +281,7 @@ describe('Issue card component', () => {
});
describe('labels', () => {
- beforeEach(done => {
+ beforeEach((done) => {
issue.addLabel(label1);
wrapper.setProps({ issue: { ...issue } });
@@ -306,7 +296,7 @@ describe('Issue card component', () => {
expect(label.props('backgroundColor')).toEqual(label1.color);
});
- it('does not render label if label does not have an ID', done => {
+ it('does not render label if label does not have an ID', (done) => {
issue.addLabel(
new ListLabel({
title: 'closed',
@@ -325,7 +315,7 @@ describe('Issue card component', () => {
});
describe('blocked', () => {
- beforeEach(done => {
+ beforeEach((done) => {
wrapper.setProps({
issue: {
...wrapper.props('issue'),
diff --git a/spec/frontend/boards/issue_card_inner_spec.js b/spec/frontend/boards/issue_card_inner_spec.js
new file mode 100644
index 00000000000..f9ad78494af
--- /dev/null
+++ b/spec/frontend/boards/issue_card_inner_spec.js
@@ -0,0 +1,372 @@
+import { mount } from '@vue/test-utils';
+import { range } from 'lodash';
+import { GlLabel } from '@gitlab/ui';
+import IssueCardInner from '~/boards/components/issue_card_inner.vue';
+import { mockLabelList } from './mock_data';
+import defaultStore from '~/boards/stores';
+import eventHub from '~/boards/eventhub';
+import { updateHistory } from '~/lib/utils/url_utility';
+
+jest.mock('~/lib/utils/url_utility');
+jest.mock('~/boards/eventhub');
+
+describe('Issue card component', () => {
+ const user = {
+ id: 1,
+ name: 'testing 123',
+ username: 'test',
+ avatarUrl: 'test_image',
+ };
+
+ const label1 = {
+ id: 3,
+ title: 'testing 123',
+ color: '#000CFF',
+ textColor: 'white',
+ description: 'test',
+ };
+
+ let wrapper;
+ let issue;
+ let list;
+
+ const createWrapper = (props = {}, store = defaultStore) => {
+ wrapper = mount(IssueCardInner, {
+ store,
+ propsData: {
+ list,
+ issue,
+ ...props,
+ },
+ stubs: {
+ GlLabel: true,
+ },
+ provide: {
+ groupId: null,
+ rootPath: '/',
+ scopedLabelsAvailable: false,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ list = mockLabelList;
+ issue = {
+ title: 'Testing',
+ id: 1,
+ iid: 1,
+ confidential: false,
+ labels: [list.label],
+ assignees: [],
+ referencePath: '#1',
+ webUrl: '/test/1',
+ weight: 1,
+ };
+
+ createWrapper({ issue, list });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ jest.clearAllMocks();
+ });
+
+ it('renders issue title', () => {
+ expect(wrapper.find('.board-card-title').text()).toContain(issue.title);
+ });
+
+ it('includes issue base in link', () => {
+ expect(wrapper.find('.board-card-title a').attributes('href')).toContain('/test');
+ });
+
+ it('includes issue title on link', () => {
+ expect(wrapper.find('.board-card-title a').attributes('title')).toBe(issue.title);
+ });
+
+ it('does not render confidential icon', () => {
+ 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}`);
+ });
+
+ it('does not render assignee', () => {
+ expect(wrapper.find('.board-card-assignee .avatar').exists()).toBe(false);
+ });
+
+ describe('confidential issue', () => {
+ beforeEach(() => {
+ wrapper.setProps({
+ issue: {
+ ...wrapper.props('issue'),
+ confidential: true,
+ },
+ });
+ });
+
+ it('renders confidential icon', () => {
+ expect(wrapper.find('.confidential-icon').exists()).toBe(true);
+ });
+ });
+
+ describe('with assignee', () => {
+ describe('with avatar', () => {
+ beforeEach(() => {
+ wrapper.setProps({
+ issue: {
+ ...wrapper.props('issue'),
+ assignees: [user],
+ updateData(newData) {
+ Object.assign(this, newData);
+ },
+ },
+ });
+ });
+
+ it('renders assignee', () => {
+ expect(wrapper.find('.board-card-assignee .avatar').exists()).toBe(true);
+ });
+
+ it('sets title', () => {
+ expect(wrapper.find('.js-assignee-tooltip').text()).toContain(`${user.name}`);
+ });
+
+ it('sets users path', () => {
+ expect(wrapper.find('.board-card-assignee a').attributes('href')).toBe('/test');
+ });
+
+ it('renders avatar', () => {
+ expect(wrapper.find('.board-card-assignee img').exists()).toBe(true);
+ });
+
+ it('renders the avatar using avatarUrl property', async () => {
+ wrapper.props('issue').updateData({
+ ...wrapper.props('issue'),
+ assignees: [
+ {
+ id: '1',
+ name: 'test',
+ state: 'active',
+ username: 'test_name',
+ avatarUrl: 'test_image_from_avatar_url',
+ },
+ ],
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.find('.board-card-assignee img').attributes('src')).toBe(
+ 'test_image_from_avatar_url?width=24',
+ );
+ });
+ });
+
+ describe('with default avatar', () => {
+ beforeEach(() => {
+ global.gon.default_avatar_url = 'default_avatar';
+
+ wrapper.setProps({
+ issue: {
+ ...wrapper.props('issue'),
+ assignees: [
+ {
+ id: 1,
+ name: 'testing 123',
+ username: 'test',
+ },
+ ],
+ },
+ });
+ });
+
+ afterEach(() => {
+ global.gon.default_avatar_url = null;
+ });
+
+ it('displays defaults avatar if users avatar is null', () => {
+ expect(wrapper.find('.board-card-assignee img').exists()).toBe(true);
+ expect(wrapper.find('.board-card-assignee img').attributes('src')).toBe(
+ 'default_avatar?width=24',
+ );
+ });
+ });
+ });
+
+ describe('multiple assignees', () => {
+ beforeEach(() => {
+ wrapper.setProps({
+ issue: {
+ ...wrapper.props('issue'),
+ assignees: [
+ {
+ id: 2,
+ name: 'user2',
+ username: 'user2',
+ avatarUrl: 'test_image',
+ },
+ {
+ id: 3,
+ name: 'user3',
+ username: 'user3',
+ avatarUrl: 'test_image',
+ },
+ {
+ id: 4,
+ name: 'user4',
+ username: 'user4',
+ avatarUrl: 'test_image',
+ },
+ ],
+ },
+ });
+ });
+
+ it('renders all three assignees', () => {
+ expect(wrapper.findAll('.board-card-assignee .avatar').length).toEqual(3);
+ });
+
+ describe('more than three assignees', () => {
+ beforeEach(() => {
+ const { assignees } = wrapper.props('issue');
+ assignees.push({
+ id: 5,
+ name: 'user5',
+ username: 'user5',
+ avatarUrl: 'test_image',
+ });
+
+ wrapper.setProps({
+ issue: {
+ ...wrapper.props('issue'),
+ assignees,
+ },
+ });
+ });
+
+ it('renders more avatar counter', () => {
+ expect(wrapper.find('.board-card-assignee .avatar-counter').text().trim()).toEqual('+2');
+ });
+
+ it('renders two assignees', () => {
+ expect(wrapper.findAll('.board-card-assignee .avatar').length).toEqual(2);
+ });
+
+ it('renders 99+ avatar counter', async () => {
+ const assignees = [
+ ...wrapper.props('issue').assignees,
+ ...range(5, 103).map((i) => ({
+ id: i,
+ name: 'name',
+ username: 'username',
+ avatarUrl: 'test_image',
+ })),
+ ];
+ wrapper.setProps({
+ issue: {
+ ...wrapper.props('issue'),
+ assignees,
+ },
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.find('.board-card-assignee .avatar-counter').text().trim()).toEqual('99+');
+ });
+ });
+ });
+
+ describe('labels', () => {
+ beforeEach(() => {
+ wrapper.setProps({ issue: { ...issue, labels: [list.label, label1] } });
+ });
+
+ it('does not render list label but renders all other labels', () => {
+ expect(wrapper.findAll(GlLabel).length).toBe(1);
+ const label = wrapper.find(GlLabel);
+ expect(label.props('title')).toEqual(label1.title);
+ expect(label.props('description')).toEqual(label1.description);
+ expect(label.props('backgroundColor')).toEqual(label1.color);
+ });
+
+ it('does not render label if label does not have an ID', async () => {
+ wrapper.setProps({ issue: { ...issue, labels: [label1, { title: 'closed' }] } });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.findAll(GlLabel).length).toBe(1);
+ expect(wrapper.text()).not.toContain('closed');
+ });
+ });
+
+ describe('blocked', () => {
+ beforeEach(() => {
+ wrapper.setProps({
+ issue: {
+ ...wrapper.props('issue'),
+ 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;
+
+ wrapper.setProps({
+ updateFilters: true,
+ });
+ });
+
+ describe('when selected label is not in the filter', () => {
+ beforeEach(() => {
+ jest.spyOn(wrapper.vm, 'performSearch').mockImplementation(() => {});
+ window.location = { search: '' };
+ wrapper.vm.filterByLabel(label1);
+ });
+
+ it('calls updateHistory', () => {
+ expect(updateHistory).toHaveBeenCalledTimes(1);
+ });
+
+ it('dispatches performSearch vuex action', () => {
+ expect(wrapper.vm.performSearch).toHaveBeenCalledTimes(1);
+ });
+
+ it('emits updateTokens event', () => {
+ expect(eventHub.$emit).toHaveBeenCalledTimes(1);
+ expect(eventHub.$emit).toHaveBeenCalledWith('updateTokens');
+ });
+ });
+
+ describe('when selected label is already in the filter', () => {
+ beforeEach(() => {
+ jest.spyOn(wrapper.vm, 'performSearch').mockImplementation(() => {});
+ window.location = { search: '?label_name[]=testing%20123' };
+ wrapper.vm.filterByLabel(label1);
+ });
+
+ it('does not call updateHistory', () => {
+ expect(updateHistory).not.toHaveBeenCalled();
+ });
+
+ it('does not dispatch performSearch vuex action', () => {
+ expect(wrapper.vm.performSearch).not.toHaveBeenCalled();
+ });
+
+ it('does not emit updateTokens event', () => {
+ expect(eventHub.$emit).not.toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/boards/list_spec.js b/spec/frontend/boards/list_spec.js
index b731bb6e474..db01f62c9a6 100644
--- a/spec/frontend/boards/list_spec.js
+++ b/spec/frontend/boards/list_spec.js
@@ -37,7 +37,7 @@ describe('List model', () => {
describe('list type', () => {
const notExpandableList = ['blank'];
- const table = Object.keys(ListType).map(k => {
+ const table = Object.keys(ListType).map((k) => {
const value = ListType[k];
return [value, !notExpandableList.includes(value)];
});
@@ -186,7 +186,7 @@ describe('List model', () => {
list.issues = [];
});
- it('adds new issue to top of list', done => {
+ it('adds new issue to top of list', (done) => {
const user = new ListAssignee({
id: 1,
name: 'testing 123',
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index ea6c52c6830..d5cfb9b7d07 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -263,7 +263,7 @@ export const BoardsMockData = {
},
};
-export const boardsMockInterceptor = config => {
+export const boardsMockInterceptor = (config) => {
const body = BoardsMockData[config.method.toUpperCase()][config.url];
return [200, body];
};
@@ -285,7 +285,7 @@ export const setMockEndpoints = (opts = {}) => {
export const mockList = {
id: 'gid://gitlab/List/1',
title: 'Backlog',
- position: null,
+ position: -Infinity,
listType: 'backlog',
collapsed: false,
label: null,
@@ -318,7 +318,7 @@ export const mockLists = [mockList, mockLabelList];
export const mockListsById = keyBy(mockLists, 'id');
-export const mockListsWithModel = mockLists.map(listMock =>
+export const mockListsWithModel = mockLists.map((listMock) =>
Vue.observable(new List({ ...listMock, doNotFetchIssues: true })),
);
@@ -350,3 +350,33 @@ export const issues = {
[mockIssue3.id]: mockIssue3,
[mockIssue4.id]: mockIssue4,
};
+
+export const mockRawGroupProjects = [
+ {
+ id: 0,
+ name: 'Example Project',
+ name_with_namespace: 'Awesome Group / Example Project',
+ path_with_namespace: 'awesome-group/example-project',
+ },
+ {
+ id: 1,
+ name: 'Foobar Project',
+ name_with_namespace: 'Awesome Group / Foobar Project',
+ path_with_namespace: 'awesome-group/foobar-project',
+ },
+];
+
+export const mockGroupProjects = [
+ {
+ id: 0,
+ name: 'Example Project',
+ nameWithNamespace: 'Awesome Group / Example Project',
+ fullPath: 'awesome-group/example-project',
+ },
+ {
+ id: 1,
+ name: 'Foobar Project',
+ nameWithNamespace: 'Awesome Group / Foobar Project',
+ fullPath: 'awesome-group/foobar-project',
+ },
+];
diff --git a/spec/frontend/boards/project_select_deprecated_spec.js b/spec/frontend/boards/project_select_deprecated_spec.js
new file mode 100644
index 00000000000..e4f8f96bd33
--- /dev/null
+++ b/spec/frontend/boards/project_select_deprecated_spec.js
@@ -0,0 +1,261 @@
+import { mount } from '@vue/test-utils';
+import axios from 'axios';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui';
+import httpStatus from '~/lib/utils/http_status';
+import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants';
+import { ListType } from '~/boards/constants';
+import eventHub from '~/boards/eventhub';
+import { deprecatedCreateFlash as flash } from '~/flash';
+
+import ProjectSelect from '~/boards/components/project_select_deprecated.vue';
+
+import { listObj, mockRawGroupProjects } from './mock_data';
+
+jest.mock('~/boards/eventhub');
+jest.mock('~/flash');
+
+const dummyGon = {
+ api_version: 'v4',
+ relative_url_root: '/gitlab',
+};
+
+const mockGroupId = 1;
+const mockProjectsList1 = mockRawGroupProjects.slice(0, 1);
+const mockProjectsList2 = mockRawGroupProjects.slice(1);
+const mockDefaultFetchOptions = {
+ with_issues_enabled: true,
+ with_shared: false,
+ include_subgroups: true,
+ order_by: 'similarity',
+};
+
+const itemsPerPage = 20;
+
+describe('ProjectSelect component', () => {
+ let wrapper;
+ let axiosMock;
+
+ const findLabel = () => wrapper.find("[data-testid='header-label']");
+ const findGlDropdown = () => wrapper.find(GlDropdown);
+ const findGlDropdownLoadingIcon = () =>
+ findGlDropdown().find('button:first-child').find(GlLoadingIcon);
+ const findGlSearchBoxByType = () => wrapper.find(GlSearchBoxByType);
+ const findGlDropdownItems = () => wrapper.findAll(GlDropdownItem);
+ const findFirstGlDropdownItem = () => findGlDropdownItems().at(0);
+ const findInMenuLoadingIcon = () => wrapper.find("[data-testid='dropdown-text-loading-icon']");
+ const findEmptySearchMessage = () => wrapper.find("[data-testid='empty-result-message']");
+
+ const mockGetRequest = (data = [], statusCode = httpStatus.OK) => {
+ axiosMock
+ .onGet(`/gitlab/api/v4/groups/${mockGroupId}/projects.json`)
+ .replyOnce(statusCode, data);
+ };
+
+ const searchForProject = async (keyword, waitForAll = true) => {
+ findGlSearchBoxByType().vm.$emit('input', keyword);
+
+ if (waitForAll) {
+ await axios.waitForAll();
+ }
+ };
+
+ const createWrapper = async ({ list = listObj } = {}, waitForAll = true) => {
+ wrapper = mount(ProjectSelect, {
+ propsData: {
+ list,
+ },
+ provide: {
+ groupId: 1,
+ },
+ });
+
+ if (waitForAll) {
+ await axios.waitForAll();
+ }
+ };
+
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ window.gon = dummyGon;
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ axiosMock.restore();
+ jest.clearAllMocks();
+ });
+
+ it('displays a header title', async () => {
+ createWrapper({});
+
+ expect(findLabel().text()).toBe('Projects');
+ });
+
+ it('renders a default dropdown text', async () => {
+ createWrapper({});
+
+ expect(findGlDropdown().exists()).toBe(true);
+ expect(findGlDropdown().text()).toContain('Select a project');
+ });
+
+ describe('when mounted', () => {
+ it('displays a loading icon while projects are being fetched', async () => {
+ mockGetRequest([]);
+
+ createWrapper({}, false);
+
+ expect(findGlDropdownLoadingIcon().exists()).toBe(true);
+
+ await axios.waitForAll();
+
+ expect(axiosMock.history.get[0].params).toMatchObject({ search: '' });
+ expect(axiosMock.history.get[0].url).toBe(
+ `/gitlab/api/v4/groups/${mockGroupId}/projects.json`,
+ );
+
+ expect(findGlDropdownLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('when dropdown menu is open', () => {
+ describe('by default', () => {
+ beforeEach(async () => {
+ mockGetRequest(mockProjectsList1);
+
+ await createWrapper();
+ });
+
+ it('shows GlSearchBoxByType with default attributes', () => {
+ expect(findGlSearchBoxByType().exists()).toBe(true);
+ expect(findGlSearchBoxByType().vm.$attrs).toMatchObject({
+ placeholder: 'Search projects',
+ debounce: '250',
+ });
+ });
+
+ it("displays the fetched project's name", () => {
+ expect(findFirstGlDropdownItem().exists()).toBe(true);
+ expect(findFirstGlDropdownItem().text()).toContain(mockProjectsList1[0].name);
+ });
+
+ it("doesn't render loading icon in the menu", () => {
+ expect(findInMenuLoadingIcon().isVisible()).toBe(false);
+ });
+
+ it('renders empty search result message', async () => {
+ await createWrapper();
+
+ expect(findEmptySearchMessage().exists()).toBe(true);
+ });
+ });
+
+ describe('when a project is selected', () => {
+ beforeEach(async () => {
+ mockGetRequest(mockProjectsList1);
+
+ await createWrapper();
+
+ await findFirstGlDropdownItem().find('button').trigger('click');
+ });
+
+ it('emits setSelectedProject with correct project metadata', () => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('setSelectedProject', {
+ id: mockProjectsList1[0].id,
+ path: mockProjectsList1[0].path_with_namespace,
+ name: mockProjectsList1[0].name,
+ namespacedName: mockProjectsList1[0].name_with_namespace,
+ });
+ });
+
+ it('renders the name of the selected project', () => {
+ expect(findGlDropdown().find('.gl-new-dropdown-button-text').text()).toBe(
+ mockProjectsList1[0].name,
+ );
+ });
+ });
+
+ describe('when user searches for a project', () => {
+ beforeEach(async () => {
+ mockGetRequest(mockProjectsList1);
+
+ await createWrapper();
+ });
+
+ it('calls API with correct parameters with default fetch options', async () => {
+ await searchForProject('foobar');
+
+ const expectedApiParams = {
+ search: 'foobar',
+ per_page: itemsPerPage,
+ ...mockDefaultFetchOptions,
+ };
+
+ expect(axiosMock.history.get[1].params).toMatchObject(expectedApiParams);
+ expect(axiosMock.history.get[1].url).toBe(
+ `/gitlab/api/v4/groups/${mockGroupId}/projects.json`,
+ );
+ });
+
+ describe("when list type is defined and isn't backlog", () => {
+ it('calls API with an additional fetch option (min_access_level)', async () => {
+ axiosMock.reset();
+
+ await createWrapper({ list: { ...listObj, type: ListType.label } });
+
+ await searchForProject('foobar');
+
+ const expectedApiParams = {
+ search: 'foobar',
+ per_page: itemsPerPage,
+ ...mockDefaultFetchOptions,
+ min_access_level: featureAccessLevel.EVERYONE,
+ };
+
+ expect(axiosMock.history.get[1].params).toMatchObject(expectedApiParams);
+ expect(axiosMock.history.get[1].url).toBe(
+ `/gitlab/api/v4/groups/${mockGroupId}/projects.json`,
+ );
+ });
+ });
+
+ it('displays and hides gl-loading-icon while and after fetching data', async () => {
+ await searchForProject('some keyword', false);
+
+ await wrapper.vm.$nextTick();
+
+ expect(findInMenuLoadingIcon().isVisible()).toBe(true);
+
+ await axios.waitForAll();
+
+ expect(findInMenuLoadingIcon().isVisible()).toBe(false);
+ });
+
+ it('flashes an error message when fetching fails', async () => {
+ mockGetRequest([], httpStatus.INTERNAL_SERVER_ERROR);
+
+ await searchForProject('foobar');
+
+ expect(flash).toHaveBeenCalledTimes(1);
+ expect(flash).toHaveBeenCalledWith('Something went wrong while fetching projects');
+ });
+
+ describe('with non-empty search result', () => {
+ beforeEach(async () => {
+ mockGetRequest(mockProjectsList2);
+
+ await searchForProject('foobar');
+ });
+
+ it('displays the retrieved list of projects', async () => {
+ expect(findFirstGlDropdownItem().text()).toContain(mockProjectsList2[0].name);
+ });
+
+ it('does not render empty search result message', async () => {
+ expect(findEmptySearchMessage().exists()).toBe(false);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/boards/project_select_spec.js b/spec/frontend/boards/project_select_spec.js
new file mode 100644
index 00000000000..14ddab3542b
--- /dev/null
+++ b/spec/frontend/boards/project_select_spec.js
@@ -0,0 +1,154 @@
+import Vuex from 'vuex';
+import { createLocalVue, mount } from '@vue/test-utils';
+import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui';
+import defaultState from '~/boards/stores/state';
+
+import ProjectSelect from '~/boards/components/project_select.vue';
+
+import { mockList, mockGroupProjects } from './mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+const actions = {
+ fetchGroupProjects: jest.fn(),
+ setSelectedProject: jest.fn(),
+};
+
+const createStore = (state = defaultState) => {
+ return new Vuex.Store({
+ state,
+ actions,
+ });
+};
+
+const mockProjectsList1 = mockGroupProjects.slice(0, 1);
+
+describe('ProjectSelect component', () => {
+ let wrapper;
+
+ const findLabel = () => wrapper.find("[data-testid='header-label']");
+ const findGlDropdown = () => wrapper.find(GlDropdown);
+ const findGlDropdownLoadingIcon = () =>
+ findGlDropdown().find('button:first-child').find(GlLoadingIcon);
+ const findGlSearchBoxByType = () => wrapper.find(GlSearchBoxByType);
+ const findGlDropdownItems = () => wrapper.findAll(GlDropdownItem);
+ const findFirstGlDropdownItem = () => findGlDropdownItems().at(0);
+ const findInMenuLoadingIcon = () => wrapper.find("[data-testid='dropdown-text-loading-icon']");
+ const findEmptySearchMessage = () => wrapper.find("[data-testid='empty-result-message']");
+
+ const createWrapper = (state = {}) => {
+ const store = createStore({
+ groupProjects: [],
+ groupProjectsFlags: {
+ isLoading: false,
+ pageInfo: {
+ hasNextPage: false,
+ },
+ },
+ ...state,
+ });
+
+ wrapper = mount(ProjectSelect, {
+ localVue,
+ propsData: {
+ list: mockList,
+ },
+ store,
+ provide: {
+ groupId: 1,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('displays a header title', () => {
+ createWrapper();
+
+ expect(findLabel().text()).toBe('Projects');
+ });
+
+ it('renders a default dropdown text', () => {
+ createWrapper();
+
+ expect(findGlDropdown().exists()).toBe(true);
+ expect(findGlDropdown().text()).toContain('Select a project');
+ });
+
+ describe('when mounted', () => {
+ it('displays a loading icon while projects are being fetched', async () => {
+ createWrapper();
+
+ expect(findGlDropdownLoadingIcon().exists()).toBe(true);
+
+ await wrapper.vm.$nextTick();
+
+ expect(findGlDropdownLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('when dropdown menu is open', () => {
+ describe('by default', () => {
+ beforeEach(() => {
+ createWrapper({ groupProjects: mockGroupProjects });
+ });
+
+ it('shows GlSearchBoxByType with default attributes', () => {
+ expect(findGlSearchBoxByType().exists()).toBe(true);
+ expect(findGlSearchBoxByType().vm.$attrs).toMatchObject({
+ placeholder: 'Search projects',
+ debounce: '250',
+ });
+ });
+
+ it("displays the fetched project's name", () => {
+ expect(findFirstGlDropdownItem().exists()).toBe(true);
+ expect(findFirstGlDropdownItem().text()).toContain(mockProjectsList1[0].name);
+ });
+
+ it("doesn't render loading icon in the menu", () => {
+ expect(findInMenuLoadingIcon().isVisible()).toBe(false);
+ });
+
+ it('does not render empty search result message', () => {
+ expect(findEmptySearchMessage().exists()).toBe(false);
+ });
+ });
+
+ describe('when no projects are being returned', () => {
+ it('renders empty search result message', () => {
+ createWrapper();
+
+ expect(findEmptySearchMessage().exists()).toBe(true);
+ });
+ });
+
+ describe('when a project is selected', () => {
+ beforeEach(() => {
+ createWrapper({ groupProjects: mockProjectsList1 });
+
+ findFirstGlDropdownItem().find('button').trigger('click');
+ });
+
+ it('renders the name of the selected project', () => {
+ expect(findGlDropdown().find('.gl-new-dropdown-button-text').text()).toBe(
+ mockProjectsList1[0].name,
+ );
+ });
+ });
+
+ describe('when projects are loading', () => {
+ beforeEach(() => {
+ createWrapper({ groupProjectsFlags: { isLoading: true } });
+ });
+
+ it('displays and hides gl-loading-icon while and after fetching data', () => {
+ expect(findInMenuLoadingIcon().isVisible()).toBe(true);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index 0cae6456887..e4209cd5e55 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -9,19 +9,26 @@ import {
mockMilestone,
labels,
mockActiveIssue,
+ mockGroupProjects,
} from '../mock_data';
import actions, { gqlClient } from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types';
import { inactiveId } from '~/boards/constants';
import issueMoveListMutation from '~/boards/graphql/issue_move_list.mutation.graphql';
import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql';
+import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql';
import updateAssignees from '~/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql';
-import { fullBoardId, formatListIssues, formatBoardLists } from '~/boards/boards_util';
+import {
+ fullBoardId,
+ formatListIssues,
+ formatBoardLists,
+ formatIssueInput,
+} from '~/boards/boards_util';
import createFlash from '~/flash';
jest.mock('~/flash');
-const expectNotImplemented = action => {
+const expectNotImplemented = (action) => {
it('is not implemented', () => {
expect(action).toThrow(new Error('Not implemented!'));
});
@@ -29,7 +36,7 @@ const expectNotImplemented = action => {
// We need this helper to make sure projectPath is including
// subgroups when the movIssue action is called.
-const getProjectPath = path => path.split('#')[0];
+const getProjectPath = (path) => path.split('#')[0];
beforeEach(() => {
window.gon = { features: {} };
@@ -53,7 +60,7 @@ describe('setInitialBoardData', () => {
});
describe('setFilters', () => {
- it('should commit mutation SET_FILTERS', done => {
+ it('should commit mutation SET_FILTERS', (done) => {
const state = {
filters: {},
};
@@ -72,11 +79,11 @@ describe('setFilters', () => {
});
describe('performSearch', () => {
- it('should dispatch setFilters action', done => {
+ it('should dispatch setFilters action', (done) => {
testAction(actions.performSearch, {}, {}, [], [{ type: 'setFilters', payload: {} }], done);
});
- it('should dispatch setFilters, fetchLists and resetIssues action when graphqlBoardLists FF is on', done => {
+ it('should dispatch setFilters, fetchLists and resetIssues action when graphqlBoardLists FF is on', (done) => {
window.gon = { features: { graphqlBoardLists: true } };
testAction(
actions.performSearch,
@@ -90,7 +97,7 @@ describe('performSearch', () => {
});
describe('setActiveId', () => {
- it('should commit mutation SET_ACTIVE_ID', done => {
+ it('should commit mutation SET_ACTIVE_ID', (done) => {
const state = {
activeId: inactiveId,
};
@@ -108,10 +115,8 @@ describe('setActiveId', () => {
describe('fetchLists', () => {
const state = {
- endpoints: {
- fullPath: 'gitlab-org',
- boardId: 1,
- },
+ fullPath: 'gitlab-org',
+ boardId: '1',
filterParams: {},
boardType: 'group',
};
@@ -131,7 +136,7 @@ describe('fetchLists', () => {
const formattedLists = formatBoardLists(queryResponse.data.group.board.lists);
- it('should commit mutations RECEIVE_BOARD_LISTS_SUCCESS on success', done => {
+ it('should commit mutations RECEIVE_BOARD_LISTS_SUCCESS on success', (done) => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
testAction(
@@ -149,7 +154,7 @@ describe('fetchLists', () => {
);
});
- it('dispatch createList action when backlog list does not exist and is not hidden', done => {
+ it('dispatch createList action when backlog list does not exist and is not hidden', (done) => {
queryResponse = {
data: {
group: {
@@ -181,7 +186,7 @@ describe('fetchLists', () => {
});
describe('createList', () => {
- it('should dispatch addList action when creating backlog list', done => {
+ it('should dispatch addList action when creating backlog list', (done) => {
const backlogList = {
id: 'gid://gitlab/List/1',
listType: 'backlog',
@@ -201,7 +206,8 @@ describe('createList', () => {
);
const state = {
- endpoints: { fullPath: 'gitlab-org', boardId: '1' },
+ fullPath: 'gitlab-org',
+ boardId: '1',
boardType: 'group',
disabled: false,
boardLists: [{ type: 'closed' }],
@@ -217,7 +223,7 @@ describe('createList', () => {
);
});
- it('should commit CREATE_LIST_FAILURE mutation when API returns an error', done => {
+ it('should commit CREATE_LIST_FAILURE mutation when API returns an error', (done) => {
jest.spyOn(gqlClient, 'mutate').mockReturnValue(
Promise.resolve({
data: {
@@ -230,7 +236,8 @@ describe('createList', () => {
);
const state = {
- endpoints: { fullPath: 'gitlab-org', boardId: '1' },
+ fullPath: 'gitlab-org',
+ boardId: '1',
boardType: 'group',
disabled: false,
boardLists: [{ type: 'closed' }],
@@ -248,14 +255,15 @@ describe('createList', () => {
});
describe('moveList', () => {
- it('should commit MOVE_LIST mutation and dispatch updateList action', done => {
+ it('should commit MOVE_LIST mutation and dispatch updateList action', (done) => {
const initialBoardListsState = {
'gid://gitlab/List/1': mockLists[0],
'gid://gitlab/List/2': mockLists[1],
};
const state = {
- endpoints: { fullPath: 'gitlab-org', boardId: '1' },
+ fullPath: 'gitlab-org',
+ boardId: '1',
boardType: 'group',
disabled: false,
boardLists: initialBoardListsState,
@@ -297,7 +305,8 @@ describe('moveList', () => {
};
const state = {
- endpoints: { fullPath: 'gitlab-org', boardId: '1' },
+ fullPath: 'gitlab-org',
+ boardId: '1',
boardType: 'group',
disabled: false,
boardLists: initialBoardListsState,
@@ -319,7 +328,7 @@ describe('moveList', () => {
});
describe('updateList', () => {
- it('should commit UPDATE_LIST_FAILURE mutation when API returns an error', done => {
+ it('should commit UPDATE_LIST_FAILURE mutation when API returns an error', (done) => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
updateBoardList: {
@@ -330,7 +339,8 @@ describe('updateList', () => {
});
const state = {
- endpoints: { fullPath: 'gitlab-org', boardId: '1' },
+ fullPath: 'gitlab-org',
+ boardId: '1',
boardType: 'group',
disabled: false,
boardLists: [{ type: 'closed' }],
@@ -429,15 +439,13 @@ describe('fetchIssuesForList', () => {
const listId = mockLists[0].id;
const state = {
- endpoints: {
- fullPath: 'gitlab-org',
- boardId: 1,
- },
+ fullPath: 'gitlab-org',
+ boardId: '1',
filterParams: {},
boardType: 'group',
};
- const mockIssuesNodes = mockIssues.map(issue => ({ node: issue }));
+ const mockIssuesNodes = mockIssues.map((issue) => ({ node: issue }));
const pageInfo = {
endCursor: '',
@@ -470,7 +478,7 @@ describe('fetchIssuesForList', () => {
[listId]: pageInfo,
};
- it('should commit mutations REQUEST_ISSUES_FOR_LIST and RECEIVE_ISSUES_FOR_LIST_SUCCESS on success', done => {
+ it('should commit mutations REQUEST_ISSUES_FOR_LIST and RECEIVE_ISSUES_FOR_LIST_SUCCESS on success', (done) => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
testAction(
@@ -492,7 +500,7 @@ describe('fetchIssuesForList', () => {
);
});
- it('should commit mutations REQUEST_ISSUES_FOR_LIST and RECEIVE_ISSUES_FOR_LIST_FAILURE on failure', done => {
+ it('should commit mutations REQUEST_ISSUES_FOR_LIST and RECEIVE_ISSUES_FOR_LIST_FAILURE on failure', (done) => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(Promise.reject());
testAction(
@@ -525,12 +533,13 @@ describe('moveIssue', () => {
};
const issues = {
- '436': mockIssue,
- '437': mockIssue2,
+ 436: mockIssue,
+ 437: mockIssue2,
};
const state = {
- endpoints: { fullPath: 'gitlab-org', boardId: '1' },
+ fullPath: 'gitlab-org',
+ boardId: '1',
boardType: 'group',
disabled: false,
boardLists: mockLists,
@@ -538,7 +547,7 @@ describe('moveIssue', () => {
issues,
};
- it('should commit MOVE_ISSUE mutation and MOVE_ISSUE_SUCCESS mutation when successful', done => {
+ it('should commit MOVE_ISSUE mutation and MOVE_ISSUE_SUCCESS mutation when successful', (done) => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
issueMoveList: {
@@ -582,7 +591,7 @@ describe('moveIssue', () => {
mutation: issueMoveListMutation,
variables: {
projectPath: getProjectPath(mockIssue.referencePath),
- boardId: fullBoardId(state.endpoints.boardId),
+ boardId: fullBoardId(state.boardId),
iid: mockIssue.iid,
fromListId: 1,
toListId: 2,
@@ -613,7 +622,7 @@ describe('moveIssue', () => {
expect(gqlClient.mutate).toHaveBeenCalledWith(mutationVariables);
});
- it('should commit MOVE_ISSUE mutation and MOVE_ISSUE_FAILURE mutation when unsuccessful', done => {
+ it('should commit MOVE_ISSUE mutation and MOVE_ISSUE_FAILURE mutation when unsuccessful', (done) => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
issueMoveList: {
@@ -684,7 +693,7 @@ describe('setAssignees', () => {
});
});
- it('calls the correct mutation with the correct values', done => {
+ it('calls the correct mutation with the correct values', (done) => {
testAction(
actions.setAssignees,
{},
@@ -724,8 +733,27 @@ describe('setAssignees', () => {
describe('createNewIssue', () => {
const state = {
boardType: 'group',
- endpoints: {
- fullPath: 'gitlab-org/gitlab',
+ fullPath: 'gitlab-org/gitlab',
+ boardConfig: {
+ labelIds: [],
+ assigneeId: null,
+ milestoneId: -1,
+ },
+ };
+
+ const stateWithBoardConfig = {
+ boardConfig: {
+ labels: [
+ {
+ id: 5,
+ title: 'Test',
+ color: '#ff0000',
+ description: 'testing;',
+ textColor: 'white',
+ },
+ ],
+ assigneeId: 2,
+ milestoneId: 3,
},
};
@@ -743,11 +771,59 @@ describe('createNewIssue', () => {
expect(result).toEqual(mockIssue);
});
- it('should commit CREATE_ISSUE_FAILURE mutation when API returns an error', done => {
+ it('should add board scope to the issue being created', async () => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
createIssue: {
- issue: {},
+ issue: mockIssue,
+ errors: [],
+ },
+ },
+ });
+
+ await actions.createNewIssue({ state: stateWithBoardConfig }, mockIssue);
+ expect(gqlClient.mutate).toHaveBeenCalledWith({
+ mutation: issueCreateMutation,
+ variables: {
+ input: formatIssueInput(mockIssue, stateWithBoardConfig.boardConfig),
+ },
+ });
+ });
+
+ it('should add board scope by merging attributes to the issue being created', async () => {
+ const issue = {
+ ...mockIssue,
+ assigneeIds: ['gid://gitlab/User/1'],
+ labelIds: ['gid://gitlab/GroupLabel/4'],
+ };
+
+ jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
+ data: {
+ createIssue: {
+ issue,
+ errors: [],
+ },
+ },
+ });
+
+ const payload = formatIssueInput(issue, stateWithBoardConfig.boardConfig);
+
+ await actions.createNewIssue({ state: stateWithBoardConfig }, issue);
+ expect(gqlClient.mutate).toHaveBeenCalledWith({
+ mutation: issueCreateMutation,
+ variables: {
+ input: formatIssueInput(issue, stateWithBoardConfig.boardConfig),
+ },
+ });
+ expect(payload.labelIds).toEqual(['gid://gitlab/GroupLabel/4', 'gid://gitlab/GroupLabel/5']);
+ 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' }],
},
},
@@ -767,7 +843,7 @@ describe('createNewIssue', () => {
});
describe('addListIssue', () => {
- it('should commit ADD_ISSUE_TO_LIST mutation', done => {
+ it('should commit ADD_ISSUE_TO_LIST mutation', (done) => {
const payload = {
list: mockLists[0],
issue: mockIssue,
@@ -788,14 +864,14 @@ describe('addListIssue', () => {
describe('setActiveIssueLabels', () => {
const state = { issues: { [mockIssue.id]: mockIssue } };
const getters = { activeIssue: mockIssue };
- const testLabelIds = labels.map(label => label.id);
+ const testLabelIds = labels.map((label) => label.id);
const input = {
addLabelIds: testLabelIds,
removeLabelIds: [],
projectPath: 'h/b',
};
- it('should assign labels on success', done => {
+ it('should assign labels on success', (done) => {
jest
.spyOn(gqlClient, 'mutate')
.mockResolvedValue({ data: { updateIssue: { issue: { labels: { nodes: labels } } } } });
@@ -839,7 +915,7 @@ describe('setActiveIssueDueDate', () => {
projectPath: 'h/b',
};
- it('should commit due date after setting the issue', done => {
+ it('should commit due date after setting the issue', (done) => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
updateIssue: {
@@ -890,7 +966,7 @@ describe('setActiveIssueSubscribed', () => {
projectPath: 'gitlab-org/gitlab-test',
};
- it('should commit subscribed status', done => {
+ it('should commit subscribed status', (done) => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
issueSetSubscription: {
@@ -944,7 +1020,7 @@ describe('setActiveIssueMilestone', () => {
projectPath: 'h/b',
};
- it('should commit milestone after setting the issue', done => {
+ it('should commit milestone after setting the issue', (done) => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
updateIssue: {
@@ -986,6 +1062,145 @@ describe('setActiveIssueMilestone', () => {
});
});
+describe('setActiveIssueTitle', () => {
+ const state = { issues: { [mockIssue.id]: mockIssue } };
+ const getters = { activeIssue: mockIssue };
+ const testTitle = 'Test Title';
+ const input = {
+ title: testTitle,
+ projectPath: 'h/b',
+ };
+
+ it('should commit title after setting the issue', (done) => {
+ jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
+ data: {
+ updateIssue: {
+ issue: {
+ title: testTitle,
+ },
+ errors: [],
+ },
+ },
+ });
+
+ const payload = {
+ issueId: getters.activeIssue.id,
+ prop: 'title',
+ value: testTitle,
+ };
+
+ testAction(
+ actions.setActiveIssueTitle,
+ 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.setActiveIssueTitle({ getters }, input)).rejects.toThrow(Error);
+ });
+});
+
+describe('fetchGroupProjects', () => {
+ const state = {
+ fullPath: 'gitlab-org',
+ };
+
+ const pageInfo = {
+ endCursor: '',
+ hasNextPage: false,
+ };
+
+ const queryResponse = {
+ data: {
+ group: {
+ projects: {
+ nodes: mockGroupProjects,
+ pageInfo: {
+ endCursor: '',
+ hasNextPage: false,
+ },
+ },
+ },
+ },
+ };
+
+ it('should commit mutations REQUEST_GROUP_PROJECTS and RECEIVE_GROUP_PROJECTS_SUCCESS on success', (done) => {
+ jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
+
+ testAction(
+ actions.fetchGroupProjects,
+ {},
+ state,
+ [
+ {
+ type: types.REQUEST_GROUP_PROJECTS,
+ payload: false,
+ },
+ {
+ type: types.RECEIVE_GROUP_PROJECTS_SUCCESS,
+ payload: { projects: mockGroupProjects, pageInfo, fetchNext: false },
+ },
+ ],
+ [],
+ done,
+ );
+ });
+
+ it('should commit mutations REQUEST_GROUP_PROJECTS and RECEIVE_GROUP_PROJECTS_FAILURE on failure', (done) => {
+ jest.spyOn(gqlClient, 'query').mockRejectedValue();
+
+ testAction(
+ actions.fetchGroupProjects,
+ {},
+ state,
+ [
+ {
+ type: types.REQUEST_GROUP_PROJECTS,
+ payload: false,
+ },
+ {
+ type: types.RECEIVE_GROUP_PROJECTS_FAILURE,
+ },
+ ],
+ [],
+ done,
+ );
+ });
+});
+
+describe('setSelectedProject', () => {
+ it('should commit mutation SET_SELECTED_PROJECT', (done) => {
+ const project = mockGroupProjects[0];
+
+ testAction(
+ actions.setSelectedProject,
+ project,
+ {},
+ [
+ {
+ type: types.SET_SELECTED_PROJECT,
+ payload: project,
+ },
+ ],
+ [],
+ done,
+ );
+ });
+});
+
describe('fetchBacklog', () => {
expectNotImplemented(actions.fetchBacklog);
});
diff --git a/spec/frontend/boards/stores/getters_spec.js b/spec/frontend/boards/stores/getters_spec.js
index 6ceb8867d1f..44b41b5667d 100644
--- a/spec/frontend/boards/stores/getters_spec.js
+++ b/spec/frontend/boards/stores/getters_spec.js
@@ -39,7 +39,7 @@ describe('Boards - Getters', () => {
});
describe('getIssueById', () => {
- const state = { issues: { '1': 'issue' } };
+ const state = { issues: { 1: 'issue' } };
it.each`
id | expected
@@ -56,7 +56,7 @@ describe('Boards - Getters', () => {
${'1'} | ${'issue'}
${''} | ${{}}
`('returns $expected when $id is passed to state', ({ id, expected }) => {
- const state = { issues: { '1': 'issue' }, activeId: id };
+ const state = { issues: { 1: 'issue' }, activeId: id };
expect(getters.activeIssue(state)).toEqual(expected);
});
@@ -84,7 +84,7 @@ describe('Boards - Getters', () => {
issues,
};
it('returns issues for a given listId', () => {
- const getIssueById = issueId => [mockIssue, mockIssue2].find(({ id }) => id === issueId);
+ const getIssueById = (issueId) => [mockIssue, mockIssue2].find(({ id }) => id === issueId);
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 d93119ede3d..c5fe0e22c3c 100644
--- a/spec/frontend/boards/stores/mutations_spec.js
+++ b/spec/frontend/boards/stores/mutations_spec.js
@@ -1,9 +1,9 @@
import mutations from '~/boards/stores/mutations';
import * as types from '~/boards/stores/mutation_types';
import defaultState from '~/boards/stores/state';
-import { mockLists, rawIssue, mockIssue, mockIssue2 } from '../mock_data';
+import { mockLists, rawIssue, mockIssue, mockIssue2, mockGroupProjects } from '../mock_data';
-const expectNotImplemented = action => {
+const expectNotImplemented = (action) => {
it('is not implemented', () => {
expect(action).toThrow(new Error('Not implemented!'));
});
@@ -23,14 +23,8 @@ describe('Board Store Mutations', () => {
describe('SET_INITIAL_BOARD_DATA', () => {
it('Should set initial Boards data to state', () => {
- const endpoints = {
- boardsEndpoint: '/boards/',
- recentBoardsEndpoint: '/boards/',
- listsEndpoint: '/boards/lists',
- bulkUpdatePath: '/boards/bulkUpdate',
- boardId: 1,
- fullPath: 'gitlab-org',
- };
+ const boardId = 1;
+ const fullPath = 'gitlab-org';
const boardType = 'group';
const disabled = false;
const boardConfig = {
@@ -38,13 +32,15 @@ describe('Board Store Mutations', () => {
};
mutations[types.SET_INITIAL_BOARD_DATA](state, {
- ...endpoints,
+ boardId,
+ fullPath,
boardType,
disabled,
boardConfig,
});
- expect(state.endpoints).toEqual(endpoints);
+ expect(state.boardId).toEqual(boardId);
+ expect(state.fullPath).toEqual(fullPath);
expect(state.boardType).toEqual(boardType);
expect(state.disabled).toEqual(disabled);
expect(state.boardConfig).toEqual(boardConfig);
@@ -240,7 +236,7 @@ describe('Board Store Mutations', () => {
'gid://gitlab/List/1': [mockIssue.id],
};
const issues = {
- '1': mockIssue,
+ 1: mockIssue,
};
state = {
@@ -349,8 +345,8 @@ describe('Board Store Mutations', () => {
};
const issues = {
- '1': mockIssue,
- '2': mockIssue2,
+ 1: mockIssue,
+ 2: mockIssue2,
};
state = {
@@ -378,7 +374,7 @@ describe('Board Store Mutations', () => {
describe('MOVE_ISSUE_SUCCESS', () => {
it('updates issue in issues state', () => {
const issues = {
- '436': { id: rawIssue.id },
+ 436: { id: rawIssue.id },
};
state = {
@@ -390,7 +386,7 @@ describe('Board Store Mutations', () => {
issue: rawIssue,
});
- expect(state.issues).toEqual({ '436': { ...mockIssue, id: 436 } });
+ expect(state.issues).toEqual({ 436: { ...mockIssue, id: 436 } });
});
});
@@ -450,7 +446,7 @@ describe('Board Store Mutations', () => {
'gid://gitlab/List/1': [mockIssue.id],
};
const issues = {
- '1': mockIssue,
+ 1: mockIssue,
};
state = {
@@ -476,8 +472,8 @@ describe('Board Store Mutations', () => {
'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id],
};
const issues = {
- '1': mockIssue,
- '2': mockIssue2,
+ 1: mockIssue,
+ 2: mockIssue2,
};
state = {
@@ -500,8 +496,8 @@ describe('Board Store Mutations', () => {
'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id],
};
const issues = {
- '1': mockIssue,
- '2': mockIssue2,
+ 1: mockIssue,
+ 2: mockIssue2,
};
state = {
@@ -533,4 +529,64 @@ describe('Board Store Mutations', () => {
describe('TOGGLE_EMPTY_STATE', () => {
expectNotImplemented(mutations.TOGGLE_EMPTY_STATE);
});
+
+ describe('REQUEST_GROUP_PROJECTS', () => {
+ it('Should set isLoading in groupProjectsFlags to true in state when fetchNext is false', () => {
+ mutations[types.REQUEST_GROUP_PROJECTS](state, false);
+
+ expect(state.groupProjectsFlags.isLoading).toBe(true);
+ });
+
+ it('Should set isLoading in groupProjectsFlags to true in state when fetchNext is true', () => {
+ mutations[types.REQUEST_GROUP_PROJECTS](state, true);
+
+ expect(state.groupProjectsFlags.isLoadingMore).toBe(true);
+ });
+ });
+
+ describe('RECEIVE_GROUP_PROJECTS_SUCCESS', () => {
+ it('Should set groupProjects and pageInfo to state and isLoading in groupProjectsFlags to false', () => {
+ mutations[types.RECEIVE_GROUP_PROJECTS_SUCCESS](state, {
+ projects: mockGroupProjects,
+ pageInfo: { hasNextPage: false },
+ });
+
+ expect(state.groupProjects).toEqual(mockGroupProjects);
+ expect(state.groupProjectsFlags.isLoading).toBe(false);
+ expect(state.groupProjectsFlags.pageInfo).toEqual({ hasNextPage: false });
+ });
+
+ it('Should merge projects in groupProjects in state when fetchNext is true', () => {
+ state = {
+ ...state,
+ groupProjects: [mockGroupProjects[0]],
+ };
+
+ mutations[types.RECEIVE_GROUP_PROJECTS_SUCCESS](state, {
+ projects: [mockGroupProjects[1]],
+ fetchNext: true,
+ });
+
+ expect(state.groupProjects).toEqual(mockGroupProjects);
+ });
+ });
+
+ describe('RECEIVE_GROUP_PROJECTS_FAILURE', () => {
+ it('Should set error in state and isLoading in groupProjectsFlags to false', () => {
+ mutations[types.RECEIVE_GROUP_PROJECTS_FAILURE](state);
+
+ expect(state.error).toEqual(
+ 'An error occurred while fetching group projects. Please try again.',
+ );
+ expect(state.groupProjectsFlags.isLoading).toBe(false);
+ });
+ });
+
+ describe('SET_SELECTED_PROJECT', () => {
+ it('Should set selectedProject to state', () => {
+ mutations[types.SET_SELECTED_PROJECT](state, mockGroupProjects[0]);
+
+ expect(state.selectedProject).toEqual(mockGroupProjects[0]);
+ });
+ });
});