summaryrefslogtreecommitdiff
path: root/spec/frontend/boards
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-03-31 18:07:42 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-03-31 18:07:42 +0000
commit580622bdb3c762a8e89facd8a3946881ee480442 (patch)
tree3ac9d759da23f78f95f50684bd238a9f76839538 /spec/frontend/boards
parentb211a4ea14d5e9ed9b0c248a4e8c5c1d85b542cb (diff)
downloadgitlab-ce-580622bdb3c762a8e89facd8a3946881ee480442.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend/boards')
-rw-r--r--spec/frontend/boards/board_card_spec.js213
-rw-r--r--spec/frontend/boards/board_list_spec.js274
-rw-r--r--spec/frontend/boards/list_spec.js232
3 files changed, 719 insertions, 0 deletions
diff --git a/spec/frontend/boards/board_card_spec.js b/spec/frontend/boards/board_card_spec.js
new file mode 100644
index 00000000000..2524af21826
--- /dev/null
+++ b/spec/frontend/boards/board_card_spec.js
@@ -0,0 +1,213 @@
+/* global List */
+/* global ListAssignee */
+/* global ListLabel */
+
+import { shallowMount } from '@vue/test-utils';
+
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import eventHub from '~/boards/eventhub';
+import '~/boards/models/label';
+import '~/boards/models/assignee';
+import '~/boards/models/list';
+import store from '~/boards/stores';
+import boardsStore from '~/boards/stores/boards_store';
+import boardCard from '~/boards/components/board_card.vue';
+import issueCardInner from '~/boards/components/issue_card_inner.vue';
+import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import { listObj, boardsMockInterceptor, setMockEndpoints } from './mock_data';
+
+describe('Board card', () => {
+ let wrapper;
+ let mock;
+ let list;
+
+ const findIssueCardInner = () => wrapper.find(issueCardInner);
+ 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 => {
+ wrapper = shallowMount(boardCard, {
+ stubs: {
+ issueCardInner,
+ },
+ store,
+ propsData: {
+ list,
+ issue: list.issues[0],
+ issueLinkBase: '/',
+ disabled: false,
+ index: 0,
+ rootPath: '/',
+ ...propsData,
+ },
+ });
+ };
+
+ const setupData = () => {
+ list = new List(listObj);
+ boardsStore.create();
+ boardsStore.detail.issue = {};
+ const label1 = new ListLabel({
+ id: 3,
+ title: 'testing 123',
+ color: '#000cff',
+ text_color: 'white',
+ description: 'test',
+ });
+ return waitForPromises().then(() => {
+ list.issues[0].labels.push(label1);
+ });
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onAny().reply(boardsMockInterceptor);
+ setMockEndpoints();
+ return setupData();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ list = null;
+ mock.restore();
+ });
+
+ it('when details issue is empty does not show the element', () => {
+ mountComponent();
+ expect(wrapper.classes()).not.toContain('is-active');
+ });
+
+ it('when detailIssue is equal to card issue shows the element', () => {
+ [boardsStore.detail.issue] = list.issues;
+ mountComponent();
+
+ expect(wrapper.classes()).toContain('is-active');
+ });
+
+ it('when multiSelect does not contain issue removes multi select class', () => {
+ mountComponent();
+ expect(wrapper.classes()).not.toContain('multi-select');
+ });
+
+ it('when multiSelect contain issue add multi select class', () => {
+ boardsStore.multiSelect.list = [list.issues[0]];
+ mountComponent();
+
+ expect(wrapper.classes()).toContain('multi-select');
+ });
+
+ it('adds user-can-drag class if not disabled', () => {
+ mountComponent();
+ expect(wrapper.classes()).toContain('user-can-drag');
+ });
+
+ it('does not add user-can-drag class disabled', () => {
+ mountComponent({ disabled: true });
+
+ expect(wrapper.classes()).not.toContain('user-can-drag');
+ });
+
+ it('does not add disabled class', () => {
+ mountComponent();
+ expect(wrapper.classes()).not.toContain('is-disabled');
+ });
+
+ it('adds disabled class is disabled is true', () => {
+ mountComponent({ disabled: true });
+
+ expect(wrapper.classes()).toContain('is-disabled');
+ });
+
+ describe('mouse events', () => {
+ it('sets showDetail to true on mousedown', () => {
+ mountComponent();
+ wrapper.trigger('mousedown');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.showDetail).toBe(true);
+ });
+ });
+
+ it('sets showDetail to false on mousemove', () => {
+ mountComponent();
+ wrapper.trigger('mousedown');
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ expect(wrapper.vm.showDetail).toBe(true);
+ wrapper.trigger('mousemove');
+ return wrapper.vm.$nextTick();
+ })
+ .then(() => {
+ expect(wrapper.vm.showDetail).toBe(false);
+ });
+ });
+
+ it('does not set detail issue if showDetail is false', () => {
+ mountComponent();
+ expect(boardsStore.detail.issue).toEqual({});
+ });
+
+ it('does not set detail issue if link is clicked', () => {
+ mountComponent();
+ findIssueCardInner()
+ .find('a')
+ .trigger('mouseup');
+
+ expect(boardsStore.detail.issue).toEqual({});
+ });
+
+ it('does not set detail issue if img is clicked', () => {
+ mountComponent({
+ issue: {
+ ...list.issues[0],
+ assignees: [
+ new ListAssignee({
+ id: 1,
+ name: 'testing 123',
+ username: 'test',
+ avatar: 'test_image',
+ }),
+ ],
+ },
+ });
+
+ findUserAvatarLink().trigger('mouseup');
+
+ expect(boardsStore.detail.issue).toEqual({});
+ });
+
+ it('does not set detail issue if showDetail is false after mouseup', () => {
+ mountComponent();
+ wrapper.trigger('mouseup');
+
+ expect(boardsStore.detail.issue).toEqual({});
+ });
+
+ it('sets detail issue to card issue on mouse up', () => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+
+ mountComponent();
+
+ wrapper.trigger('mousedown');
+ wrapper.trigger('mouseup');
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', wrapper.vm.issue, undefined);
+ expect(boardsStore.detail.list).toEqual(wrapper.vm.list);
+ });
+
+ it('resets detail issue to empty if already set', () => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ [boardsStore.detail.issue] = list.issues;
+ mountComponent();
+
+ wrapper.trigger('mousedown');
+ wrapper.trigger('mouseup');
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue', undefined);
+ });
+ });
+});
diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js
new file mode 100644
index 00000000000..882310030f8
--- /dev/null
+++ b/spec/frontend/boards/board_list_spec.js
@@ -0,0 +1,274 @@
+/* global List */
+/* global ListIssue */
+
+import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_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({
+ 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,
+ loading: false,
+ issueLinkBase: '/issues',
+ rootPath: '/',
+ ...componentProps,
+ },
+ }).$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 = Object.assign({}, 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.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(`hide-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/list_spec.js b/spec/frontend/boards/list_spec.js
new file mode 100644
index 00000000000..c0dd5afe498
--- /dev/null
+++ b/spec/frontend/boards/list_spec.js
@@ -0,0 +1,232 @@
+/* global List */
+/* global ListAssignee */
+/* global ListIssue */
+/* global ListLabel */
+
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import '~/boards/models/label';
+import '~/boards/models/assignee';
+import '~/boards/models/issue';
+import '~/boards/models/list';
+import { ListType } from '~/boards/constants';
+import boardsStore from '~/boards/stores/boards_store';
+import waitForPromises from 'helpers/wait_for_promises';
+import { listObj, listObjDuplicate, boardsMockInterceptor } from './mock_data';
+
+describe('List model', () => {
+ let list;
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onAny().reply(boardsMockInterceptor);
+ boardsStore.create();
+ boardsStore.setEndpoints({
+ listsEndpoint: '/test/-/boards/1/lists',
+ });
+
+ list = new List(listObj);
+ return waitForPromises();
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('list type', () => {
+ const notExpandableList = ['blank'];
+
+ const table = Object.keys(ListType).map(k => {
+ const value = ListType[k];
+ return [value, !notExpandableList.includes(value)];
+ });
+ it.each(table)(`when list_type is %s boards isExpandable is %p`, (type, result) => {
+ expect(new List({ id: 1, list_type: type }).isExpandable).toBe(result);
+ });
+ });
+
+ it('gets issues when created', () => {
+ expect(list.issues.length).toBe(1);
+ });
+
+ it('saves list and returns ID', () => {
+ list = new List({
+ title: 'test',
+ label: {
+ id: 1,
+ title: 'test',
+ color: 'red',
+ text_color: 'white',
+ },
+ });
+ return list.save().then(() => {
+ expect(list.id).toBe(listObj.id);
+ expect(list.type).toBe('label');
+ expect(list.position).toBe(0);
+ expect(list.label.color).toBe('red');
+ expect(list.label.textColor).toBe('white');
+ });
+ });
+
+ it('destroys the list', () => {
+ boardsStore.addList(listObj);
+ list = boardsStore.findList('id', listObj.id);
+
+ expect(boardsStore.state.lists.length).toBe(1);
+ list.destroy();
+
+ return waitForPromises().then(() => {
+ expect(boardsStore.state.lists.length).toBe(0);
+ });
+ });
+
+ it('gets issue from list', () => {
+ const issue = list.findIssue(1);
+
+ expect(issue).toBeDefined();
+ });
+
+ it('removes issue', () => {
+ const issue = list.findIssue(1);
+
+ expect(list.issues.length).toBe(1);
+ list.removeIssue(issue);
+
+ expect(list.issues.length).toBe(0);
+ });
+
+ it('sends service request to update issue label', () => {
+ const listDup = new List(listObjDuplicate);
+ const issue = new ListIssue({
+ title: 'Testing',
+ id: 1,
+ iid: 1,
+ confidential: false,
+ labels: [list.label, listDup.label],
+ assignees: [],
+ });
+
+ list.issues.push(issue);
+ listDup.issues.push(issue);
+
+ jest.spyOn(boardsStore, 'moveIssue');
+
+ listDup.updateIssueLabel(issue, list);
+
+ expect(boardsStore.moveIssue).toHaveBeenCalledWith(
+ issue.id,
+ list.id,
+ listDup.id,
+ undefined,
+ undefined,
+ );
+ });
+
+ describe('page number', () => {
+ beforeEach(() => {
+ jest.spyOn(list, 'getIssues').mockImplementation(() => {});
+ list.issues = [];
+ });
+
+ it('increase page number if current issue count is more than the page size', () => {
+ for (let i = 0; i < 30; i += 1) {
+ list.issues.push(
+ new ListIssue({
+ title: 'Testing',
+ id: i,
+ iid: i,
+ confidential: false,
+ labels: [list.label],
+ assignees: [],
+ }),
+ );
+ }
+ list.issuesSize = 50;
+
+ expect(list.issues.length).toBe(30);
+
+ list.nextPage();
+
+ expect(list.page).toBe(2);
+ expect(list.getIssues).toHaveBeenCalled();
+ });
+
+ it('does not increase page number if issue count is less than the page size', () => {
+ list.issues.push(
+ new ListIssue({
+ title: 'Testing',
+ id: 1,
+ confidential: false,
+ labels: [list.label],
+ assignees: [],
+ }),
+ );
+ list.issuesSize = 2;
+
+ list.nextPage();
+
+ expect(list.page).toBe(1);
+ expect(list.getIssues).toHaveBeenCalled();
+ });
+ });
+
+ describe('newIssue', () => {
+ beforeEach(() => {
+ jest.spyOn(boardsStore, 'newIssue').mockReturnValue(
+ Promise.resolve({
+ data: {
+ id: 42,
+ subscribed: false,
+ assignable_labels_endpoint: '/issue/42/labels',
+ toggle_subscription_endpoint: '/issue/42/subscriptions',
+ issue_sidebar_endpoint: '/issue/42/sidebar_info',
+ },
+ }),
+ );
+ list.issues = [];
+ });
+
+ it('adds new issue to top of list', done => {
+ const user = new ListAssignee({
+ id: 1,
+ name: 'testing 123',
+ username: 'test',
+ avatar: 'test_image',
+ });
+
+ list.issues.push(
+ new ListIssue({
+ title: 'Testing',
+ id: 1,
+ confidential: false,
+ labels: [new ListLabel(list.label)],
+ assignees: [],
+ }),
+ );
+ const dummyIssue = new ListIssue({
+ title: 'new issue',
+ id: 2,
+ confidential: false,
+ labels: [new ListLabel(list.label)],
+ assignees: [user],
+ subscribed: false,
+ });
+
+ list
+ .newIssue(dummyIssue)
+ .then(() => {
+ expect(list.issues.length).toBe(2);
+ expect(list.issues[0]).toBe(dummyIssue);
+ expect(list.issues[0].subscribed).toBe(false);
+ expect(list.issues[0].assignableLabelsEndpoint).toBe('/issue/42/labels');
+ expect(list.issues[0].toggleSubscriptionEndpoint).toBe('/issue/42/subscriptions');
+ expect(list.issues[0].sidebarInfoEndpoint).toBe('/issue/42/sidebar_info');
+ expect(list.issues[0].labels).toBe(dummyIssue.labels);
+ expect(list.issues[0].assignees).toBe(dummyIssue.assignees);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});