diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-31 18:07:42 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-31 18:07:42 +0000 |
commit | 580622bdb3c762a8e89facd8a3946881ee480442 (patch) | |
tree | 3ac9d759da23f78f95f50684bd238a9f76839538 /spec/frontend/boards | |
parent | b211a4ea14d5e9ed9b0c248a4e8c5c1d85b542cb (diff) | |
download | gitlab-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.js | 213 | ||||
-rw-r--r-- | spec/frontend/boards/board_list_spec.js | 274 | ||||
-rw-r--r-- | spec/frontend/boards/list_spec.js | 232 |
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); + }); + }); +}); |