diff options
Diffstat (limited to 'spec/frontend/boards/components')
8 files changed, 961 insertions, 3 deletions
diff --git a/spec/frontend/boards/components/board_assignee_dropdown_spec.js b/spec/frontend/boards/components/board_assignee_dropdown_spec.js new file mode 100644 index 00000000000..e185a6d5419 --- /dev/null +++ b/spec/frontend/boards/components/board_assignee_dropdown_spec.js @@ -0,0 +1,308 @@ +import { mount, createLocalVue } from '@vue/test-utils'; +import { GlDropdownItem, GlAvatarLink, GlAvatarLabeled, GlSearchBoxByType } from '@gitlab/ui'; +import createMockApollo from 'jest/helpers/mock_apollo_helper'; +import VueApollo from 'vue-apollo'; +import BoardAssigneeDropdown from '~/boards/components/board_assignee_dropdown.vue'; +import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue'; +import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue'; +import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue'; +import store from '~/boards/stores'; +import getIssueParticipants from '~/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql'; +import searchUsers from '~/boards/queries/users_search.query.graphql'; +import { participants } from '../mock_data'; + +const localVue = createLocalVue(); + +localVue.use(VueApollo); + +describe('BoardCardAssigneeDropdown', () => { + let wrapper; + let fakeApollo; + let getIssueParticipantsSpy; + let getSearchUsersSpy; + + const iid = '111'; + const activeIssueName = 'test'; + const anotherIssueName = 'hello'; + + const createComponent = (search = '') => { + wrapper = mount(BoardAssigneeDropdown, { + data() { + return { + search, + selected: store.getters.activeIssue.assignees, + participants, + }; + }, + store, + provide: { + canUpdate: true, + rootPath: '', + }, + }); + }; + + const createComponentWithApollo = (search = '') => { + fakeApollo = createMockApollo([ + [getIssueParticipants, getIssueParticipantsSpy], + [searchUsers, getSearchUsersSpy], + ]); + + wrapper = mount(BoardAssigneeDropdown, { + localVue, + apolloProvider: fakeApollo, + data() { + return { + search, + selected: store.getters.activeIssue.assignees, + participants, + }; + }, + store, + provide: { + canUpdate: true, + rootPath: '', + }, + }); + }; + + const unassign = async () => { + wrapper.find('[data-testid="unassign"]').trigger('click'); + + await wrapper.vm.$nextTick(); + }; + + const openDropdown = async () => { + wrapper.find('[data-testid="edit-button"]').trigger('click'); + + await wrapper.vm.$nextTick(); + }; + + const findByText = text => { + return wrapper.findAll(GlDropdownItem).wrappers.find(node => node.text().indexOf(text) === 0); + }; + + beforeEach(() => { + store.state.activeId = '1'; + store.state.issues = { + '1': { + iid, + assignees: [{ username: activeIssueName, name: activeIssueName, id: activeIssueName }], + }, + }; + + jest.spyOn(store, 'dispatch').mockResolvedValue(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('when mounted', () => { + beforeEach(() => { + createComponent(); + }); + + it.each` + text + ${anotherIssueName} + ${activeIssueName} + `('finds item with $text', ({ text }) => { + const item = findByText(text); + + expect(item.exists()).toBe(true); + }); + + it('renders gl-avatar-link in gl-dropdown-item', () => { + const item = findByText('hello'); + + expect(item.find(GlAvatarLink).exists()).toBe(true); + }); + + it('renders gl-avatar-labeled in gl-avatar-link', () => { + const item = findByText('hello'); + + expect( + item + .find(GlAvatarLink) + .find(GlAvatarLabeled) + .exists(), + ).toBe(true); + }); + }); + + describe('when selected users are present', () => { + it('renders a divider', () => { + createComponent(); + + expect(wrapper.find('[data-testid="selected-user-divider"]').exists()).toBe(true); + }); + }); + + describe('when collapsed', () => { + it('renders IssuableAssignees', () => { + createComponent(); + + expect(wrapper.find(IssuableAssignees).isVisible()).toBe(true); + expect(wrapper.find(MultiSelectDropdown).isVisible()).toBe(false); + }); + }); + + describe('when dropdown is open', () => { + beforeEach(async () => { + createComponent(); + + await openDropdown(); + }); + + it('shows assignees dropdown', async () => { + expect(wrapper.find(IssuableAssignees).isVisible()).toBe(false); + expect(wrapper.find(MultiSelectDropdown).isVisible()).toBe(true); + }); + + it('shows the issue returned as the activeIssue', async () => { + expect(findByText(activeIssueName).props('isChecked')).toBe(true); + }); + + describe('when "Unassign" is clicked', () => { + it('unassigns assignees', async () => { + await unassign(); + + expect(findByText('Unassign').props('isChecked')).toBe(true); + }); + }); + + describe('when an unselected item is clicked', () => { + beforeEach(async () => { + await unassign(); + }); + + it('assigns assignee in the dropdown', async () => { + wrapper.find('[data-testid="item_test"]').trigger('click'); + + await wrapper.vm.$nextTick(); + + expect(findByText(activeIssueName).props('isChecked')).toBe(true); + }); + + it('calls setAssignees with username list', async () => { + wrapper.find('[data-testid="item_test"]').trigger('click'); + + await wrapper.vm.$nextTick(); + + document.body.click(); + + await wrapper.vm.$nextTick(); + + expect(store.dispatch).toHaveBeenCalledWith('setAssignees', [activeIssueName]); + }); + }); + + describe('when the user off clicks', () => { + beforeEach(async () => { + await unassign(); + + document.body.click(); + + await wrapper.vm.$nextTick(); + }); + + it('calls setAssignees with username list', async () => { + expect(store.dispatch).toHaveBeenCalledWith('setAssignees', []); + }); + + it('closes the dropdown', async () => { + expect(wrapper.find(IssuableAssignees).isVisible()).toBe(true); + }); + }); + }); + + it('renders divider after unassign', () => { + createComponent(); + + expect(wrapper.find('[data-testid="unassign-divider"]').exists()).toBe(true); + }); + + it.each` + assignees | expected + ${[{ id: 5, username: '', name: '' }]} | ${'Assignee'} + ${[{ id: 6, username: '', name: '' }, { id: 7, username: '', name: '' }]} | ${'2 Assignees'} + `( + 'when assignees have a length of $assignees.length, it renders $expected', + ({ assignees, expected }) => { + store.state.issues['1'].assignees = assignees; + + createComponent(); + + expect(wrapper.find(BoardEditableItem).props('title')).toBe(expected); + }, + ); + + describe('Apollo', () => { + beforeEach(() => { + getIssueParticipantsSpy = jest.fn().mockResolvedValue({ + data: { + issue: { + participants: { + nodes: [ + { + username: 'participant', + name: 'participant', + webUrl: '', + avatarUrl: '', + id: '', + }, + ], + }, + }, + }, + }); + getSearchUsersSpy = jest.fn().mockResolvedValue({ + data: { + users: { + nodes: [{ username: 'root', name: 'root', webUrl: '', avatarUrl: '', id: '' }], + }, + }, + }); + }); + + describe('when search is empty', () => { + beforeEach(() => { + createComponentWithApollo(); + }); + + it('calls getIssueParticipants', async () => { + jest.runOnlyPendingTimers(); + await wrapper.vm.$nextTick(); + + expect(getIssueParticipantsSpy).toHaveBeenCalledWith({ id: 'gid://gitlab/Issue/111' }); + }); + }); + + describe('when search is not empty', () => { + beforeEach(() => { + createComponentWithApollo('search term'); + }); + + it('calls searchUsers', async () => { + jest.runOnlyPendingTimers(); + await wrapper.vm.$nextTick(); + + expect(getSearchUsersSpy).toHaveBeenCalledWith({ search: 'search term' }); + }); + }); + }); + + it('finds GlSearchBoxByType', async () => { + createComponent(); + + await openDropdown(); + + expect(wrapper.find(GlSearchBoxByType).exists()).toBe(true); + }); +}); diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js index a3ddcdf01b7..5e23c781eae 100644 --- a/spec/frontend/boards/components/board_card_spec.js +++ b/spec/frontend/boards/components/board_card_spec.js @@ -175,7 +175,7 @@ describe('BoardCard', () => { wrapper.trigger('mousedown'); wrapper.trigger('mouseup'); - expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', wrapper.vm.issue, undefined); + expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', wrapper.vm.issue, false); expect(boardsStore.detail.list).toEqual(wrapper.vm.list); }); @@ -188,7 +188,7 @@ describe('BoardCard', () => { wrapper.trigger('mousedown'); wrapper.trigger('mouseup'); - expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue', undefined); + expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue', false); }); }); diff --git a/spec/frontend/boards/components/board_column_new_spec.js b/spec/frontend/boards/components/board_column_new_spec.js new file mode 100644 index 00000000000..4aafc3a867a --- /dev/null +++ b/spec/frontend/boards/components/board_column_new_spec.js @@ -0,0 +1,72 @@ +import { shallowMount } from '@vue/test-utils'; + +import { listObj } from 'jest/boards/mock_data'; +import BoardColumn from '~/boards/components/board_column_new.vue'; +import List from '~/boards/models/list'; +import { ListType } from '~/boards/constants'; +import { createStore } from '~/boards/stores'; + +describe('Board Column Component', () => { + let wrapper; + let store; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + const createComponent = ({ listType = ListType.backlog, collapsed = false } = {}) => { + const boardId = '1'; + + const listMock = { + ...listObj, + list_type: listType, + collapsed, + }; + + if (listType === ListType.assignee) { + delete listMock.label; + listMock.user = {}; + } + + const list = new List({ ...listMock, doNotFetchIssues: true }); + + store = createStore(); + + wrapper = shallowMount(BoardColumn, { + store, + propsData: { + disabled: false, + list, + }, + provide: { + boardId, + }, + }); + }; + + const isExpandable = () => wrapper.classes('is-expandable'); + const isCollapsed = () => wrapper.classes('is-collapsed'); + + describe('Given different list types', () => { + it('is expandable when List Type is `backlog`', () => { + createComponent({ listType: ListType.backlog }); + + expect(isExpandable()).toBe(true); + }); + }); + + describe('expanded / collapsed column', () => { + it('has class is-collapsed when list is collapsed', () => { + createComponent({ collapsed: false }); + + expect(wrapper.vm.list.isExpanded).toBe(true); + }); + + it('does not have class is-collapsed when list is expanded', () => { + createComponent({ collapsed: true }); + + expect(isCollapsed()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/boards/components/board_column_spec.js b/spec/frontend/boards/components/board_column_spec.js index 2a4dbbb989e..ba11225676b 100644 --- a/spec/frontend/boards/components/board_column_spec.js +++ b/spec/frontend/boards/components/board_column_spec.js @@ -78,7 +78,7 @@ describe('Board Column Component', () => { }); }); - describe('expanded / collaped column', () => { + describe('expanded / collapsed column', () => { it('has class is-collapsed when list is collapsed', () => { createComponent({ collapsed: false }); diff --git a/spec/frontend/boards/components/board_list_header_new_spec.js b/spec/frontend/boards/components/board_list_header_new_spec.js new file mode 100644 index 00000000000..80786d82620 --- /dev/null +++ b/spec/frontend/boards/components/board_list_header_new_spec.js @@ -0,0 +1,169 @@ +import Vuex from 'vuex'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; + +import { listObj } from 'jest/boards/mock_data'; +import BoardListHeader from '~/boards/components/board_list_header_new.vue'; +import List from '~/boards/models/list'; +import { ListType } from '~/boards/constants'; + +const localVue = createLocalVue(); + +localVue.use(Vuex); + +describe('Board List Header Component', () => { + let wrapper; + let store; + + const updateListSpy = jest.fn(); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + + localStorage.clear(); + }); + + const createComponent = ({ + listType = ListType.backlog, + collapsed = false, + withLocalStorage = true, + currentUserId = null, + } = {}) => { + const boardId = '1'; + + const listMock = { + ...listObj, + list_type: listType, + collapsed, + }; + + if (listType === ListType.assignee) { + delete listMock.label; + listMock.user = {}; + } + + const list = new List({ ...listMock, doNotFetchIssues: true }); + + if (withLocalStorage) { + localStorage.setItem( + `boards.${boardId}.${list.type}.${list.id}.expanded`, + (!collapsed).toString(), + ); + } + + store = new Vuex.Store({ + state: {}, + actions: { updateList: updateListSpy }, + getters: {}, + }); + + wrapper = shallowMount(BoardListHeader, { + store, + localVue, + propsData: { + disabled: false, + list, + }, + provide: { + boardId, + weightFeatureAvailable: false, + currentUserId, + }, + }); + }; + + const isExpanded = () => wrapper.vm.list.isExpanded; + const isCollapsed = () => !isExpanded(); + + const findAddIssueButton = () => wrapper.find({ ref: 'newIssueBtn' }); + const findCaret = () => wrapper.find('.board-title-caret'); + + describe('Add issue button', () => { + const hasNoAddButton = [ListType.promotion, ListType.blank, ListType.closed]; + const hasAddButton = [ListType.backlog, ListType.label, ListType.milestone, ListType.assignee]; + + it.each(hasNoAddButton)('does not render when List Type is `%s`', listType => { + createComponent({ listType }); + + expect(findAddIssueButton().exists()).toBe(false); + }); + + it.each(hasAddButton)('does render when List Type is `%s`', listType => { + createComponent({ listType }); + + expect(findAddIssueButton().exists()).toBe(true); + }); + + it('has a test for each list type', () => { + createComponent(); + + Object.values(ListType).forEach(value => { + expect([...hasAddButton, ...hasNoAddButton]).toContain(value); + }); + }); + + it('does render when logged out', () => { + createComponent(); + + expect(findAddIssueButton().exists()).toBe(true); + }); + }); + + describe('expanding / collapsing the column', () => { + it('does not collapse when clicking the header', async () => { + createComponent(); + + expect(isCollapsed()).toBe(false); + + wrapper.find('[data-testid="board-list-header"]').trigger('click'); + + await wrapper.vm.$nextTick(); + + expect(isCollapsed()).toBe(false); + }); + + it('collapses expanded Column when clicking the collapse icon', async () => { + createComponent(); + + expect(isExpanded()).toBe(true); + + findCaret().vm.$emit('click'); + + await wrapper.vm.$nextTick(); + + expect(isCollapsed()).toBe(true); + }); + + it('expands collapsed Column when clicking the expand icon', async () => { + createComponent({ collapsed: true }); + + expect(isCollapsed()).toBe(true); + + findCaret().vm.$emit('click'); + + await wrapper.vm.$nextTick(); + + expect(isCollapsed()).toBe(false); + }); + + it("when logged in it calls list update and doesn't set localStorage", async () => { + createComponent({ withLocalStorage: false, currentUserId: 1 }); + + findCaret().vm.$emit('click'); + await wrapper.vm.$nextTick(); + + expect(updateListSpy).toHaveBeenCalledTimes(1); + expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(null); + }); + + it("when logged out it doesn't call list update and sets localStorage", async () => { + createComponent(); + + findCaret().vm.$emit('click'); + await wrapper.vm.$nextTick(); + + expect(updateListSpy).not.toHaveBeenCalled(); + expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(String(isExpanded())); + }); + }); +}); diff --git a/spec/frontend/boards/components/board_new_issue_new_spec.js b/spec/frontend/boards/components/board_new_issue_new_spec.js new file mode 100644 index 00000000000..af4bad65121 --- /dev/null +++ b/spec/frontend/boards/components/board_new_issue_new_spec.js @@ -0,0 +1,115 @@ +import Vuex from 'vuex'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import BoardNewIssue from '~/boards/components/board_new_issue_new.vue'; + +import '~/boards/models/list'; +import { mockListsWithModel } from '../mock_data'; + +const localVue = createLocalVue(); + +localVue.use(Vuex); + +describe('Issue boards new issue form', () => { + let wrapper; + let vm; + + const addListNewIssuesSpy = jest.fn(); + + const findSubmitButton = () => wrapper.find({ ref: 'submitButton' }); + const findCancelButton = () => wrapper.find({ ref: 'cancelButton' }); + const findSubmitForm = () => wrapper.find({ ref: 'submitForm' }); + + const submitIssue = () => { + const dummySubmitEvent = { + preventDefault() {}, + }; + + return findSubmitForm().trigger('submit', dummySubmitEvent); + }; + + beforeEach(() => { + const store = new Vuex.Store({ + state: {}, + actions: { addListNewIssue: addListNewIssuesSpy }, + getters: {}, + }); + + wrapper = shallowMount(BoardNewIssue, { + propsData: { + disabled: false, + list: mockListsWithModel[0], + }, + store, + localVue, + provide: { + groupId: null, + weightFeatureAvailable: false, + boardWeight: null, + }, + }); + + vm = wrapper.vm; + + return vm.$nextTick(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('calls submit if submit button is clicked', async () => { + jest.spyOn(wrapper.vm, 'submit').mockImplementation(); + wrapper.setData({ title: 'Testing Title' }); + + await vm.$nextTick(); + await submitIssue(); + expect(wrapper.vm.submit).toHaveBeenCalled(); + }); + + it('disables submit button if title is empty', () => { + expect(findSubmitButton().props().disabled).toBe(true); + }); + + it('enables submit button if title is not empty', async () => { + wrapper.setData({ title: 'Testing Title' }); + + await vm.$nextTick(); + expect(wrapper.find({ ref: 'input' }).element.value).toBe('Testing Title'); + expect(findSubmitButton().props().disabled).toBe(false); + }); + + it('clears title after clicking cancel', async () => { + findCancelButton().trigger('click'); + + await vm.$nextTick(); + expect(vm.title).toBe(''); + }); + + describe('submit success', () => { + it('creates new issue', async () => { + wrapper.setData({ title: 'submit issue' }); + + await vm.$nextTick(); + await submitIssue(); + expect(addListNewIssuesSpy).toHaveBeenCalled(); + }); + + it('enables button after submit', async () => { + jest.spyOn(wrapper.vm, 'submit').mockImplementation(); + wrapper.setData({ title: 'submit issue' }); + + await vm.$nextTick(); + await submitIssue(); + expect(findSubmitButton().props().disabled).toBe(false); + }); + + it('clears title after submit', async () => { + wrapper.setData({ title: 'submit issue' }); + + await vm.$nextTick(); + await submitIssue(); + await vm.$nextTick(); + expect(vm.title).toBe(''); + }); + }); +}); diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js new file mode 100644 index 00000000000..b034c8cb11d --- /dev/null +++ b/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js @@ -0,0 +1,137 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlDatepicker } from '@gitlab/ui'; +import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue'; +import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue'; +import { createStore } from '~/boards/stores'; +import createFlash from '~/flash'; + +const TEST_DUE_DATE = '2020-02-20'; +const TEST_FORMATTED_DUE_DATE = 'Feb 20, 2020'; +const TEST_PARSED_DATE = new Date(2020, 1, 20); +const TEST_ISSUE = { id: 'gid://gitlab/Issue/1', iid: 9, dueDate: null, referencePath: 'h/b#2' }; + +jest.mock('~/flash'); + +describe('~/boards/components/sidebar/board_sidebar_due_date.vue', () => { + let wrapper; + let store; + + afterEach(() => { + wrapper.destroy(); + store = null; + wrapper = null; + }); + + const createWrapper = ({ dueDate = null } = {}) => { + store = createStore(); + store.state.issues = { [TEST_ISSUE.id]: { ...TEST_ISSUE, dueDate } }; + store.state.activeId = TEST_ISSUE.id; + + wrapper = shallowMount(BoardSidebarDueDate, { + store, + provide: { + canUpdate: true, + }, + stubs: { + 'board-editable-item': BoardEditableItem, + }, + }); + }; + + const findDatePicker = () => wrapper.find(GlDatepicker); + const findResetButton = () => wrapper.find('[data-testid="reset-button"]'); + const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]'); + + it('renders "None" when no due date is set', () => { + createWrapper(); + + expect(findCollapsed().text()).toBe('None'); + expect(findResetButton().exists()).toBe(false); + }); + + it('renders formatted due date with reset button when set', () => { + createWrapper({ dueDate: TEST_DUE_DATE }); + + expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE); + expect(findResetButton().exists()).toBe(true); + }); + + describe('when due date is submitted', () => { + beforeEach(async () => { + createWrapper(); + + jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => { + store.state.issues[TEST_ISSUE.id].dueDate = TEST_DUE_DATE; + }); + findDatePicker().vm.$emit('input', TEST_PARSED_DATE); + await wrapper.vm.$nextTick(); + }); + + it('collapses sidebar and renders formatted due date with reset button', () => { + expect(findCollapsed().isVisible()).toBe(true); + expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE); + expect(findResetButton().exists()).toBe(true); + }); + + it('commits change to the server', () => { + expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalledWith({ + dueDate: TEST_DUE_DATE, + projectPath: 'h/b', + }); + }); + }); + + describe('when due date is cleared', () => { + beforeEach(async () => { + createWrapper(); + + jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => { + store.state.issues[TEST_ISSUE.id].dueDate = null; + }); + findDatePicker().vm.$emit('clear'); + await wrapper.vm.$nextTick(); + }); + + it('collapses sidebar and renders "None"', () => { + expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalled(); + expect(findCollapsed().isVisible()).toBe(true); + expect(findCollapsed().text()).toBe('None'); + }); + }); + + describe('when due date is resetted', () => { + beforeEach(async () => { + createWrapper({ dueDate: TEST_DUE_DATE }); + + jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => { + store.state.issues[TEST_ISSUE.id].dueDate = null; + }); + findResetButton().vm.$emit('click'); + await wrapper.vm.$nextTick(); + }); + + it('collapses sidebar and renders "None"', () => { + expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalled(); + expect(findCollapsed().isVisible()).toBe(true); + expect(findCollapsed().text()).toBe('None'); + }); + }); + + describe('when the mutation fails', () => { + beforeEach(async () => { + createWrapper({ dueDate: TEST_DUE_DATE }); + + jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => { + throw new Error(['failed mutation']); + }); + findDatePicker().vm.$emit('input', 'Invalid date'); + await wrapper.vm.$nextTick(); + }); + + it('collapses sidebar and renders former issue due date', () => { + expect(findCollapsed().isVisible()).toBe(true); + expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE); + expect(createFlash).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js new file mode 100644 index 00000000000..ee54c662167 --- /dev/null +++ b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js @@ -0,0 +1,157 @@ +import Vuex from 'vuex'; +import { mount, createLocalVue } from '@vue/test-utils'; +import { GlToggle, GlLoadingIcon } from '@gitlab/ui'; +import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue'; +import * as types from '~/boards/stores/mutation_types'; +import { createStore } from '~/boards/stores'; +import { mockActiveIssue } from '../../mock_data'; +import createFlash from '~/flash'; + +jest.mock('~/flash.js'); + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () => { + let wrapper; + let store; + + const findNotificationHeader = () => wrapper.find("[data-testid='notification-header-text']"); + const findToggle = () => wrapper.find(GlToggle); + const findGlLoadingIcon = () => wrapper.find(GlLoadingIcon); + + const createComponent = (activeIssue = { ...mockActiveIssue }) => { + store = createStore(); + store.state.issues = { [activeIssue.id]: activeIssue }; + store.state.activeId = activeIssue.id; + + wrapper = mount(BoardSidebarSubscription, { + localVue, + store, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + store = null; + jest.clearAllMocks(); + }); + + describe('Board sidebar subscription component template', () => { + it('displays "notifications" heading', () => { + createComponent(); + + expect(findNotificationHeader().text()).toBe('Notifications'); + }); + + it('renders toggle as "off" when currently not subscribed', () => { + createComponent(); + + expect(findToggle().exists()).toBe(true); + expect(findToggle().props('value')).toBe(false); + }); + + it('renders toggle as "on" when currently subscribed', () => { + createComponent({ + ...mockActiveIssue, + subscribed: true, + }); + + expect(findToggle().exists()).toBe(true); + expect(findToggle().props('value')).toBe(true); + }); + + describe('when notification emails have been disabled', () => { + beforeEach(() => { + createComponent({ + ...mockActiveIssue, + emailsDisabled: true, + }); + }); + + it('displays a message that notification have been disabled', () => { + expect(findNotificationHeader().text()).toBe( + 'Notifications have been disabled by the project or group owner', + ); + }); + + it('does not render the toggle button', () => { + expect(findToggle().exists()).toBe(false); + }); + }); + }); + + describe('Board sidebar subscription component `behavior`', () => { + const mockSetActiveIssueSubscribed = subscribedState => { + jest.spyOn(wrapper.vm, 'setActiveIssueSubscribed').mockImplementation(async () => { + store.commit(types.UPDATE_ISSUE_BY_ID, { + issueId: mockActiveIssue.id, + prop: 'subscribed', + value: subscribedState, + }); + }); + }; + + it('subscribing to notification', async () => { + createComponent(); + mockSetActiveIssueSubscribed(true); + + expect(findGlLoadingIcon().exists()).toBe(false); + + findToggle().trigger('click'); + + await wrapper.vm.$nextTick(); + + expect(findGlLoadingIcon().exists()).toBe(true); + expect(wrapper.vm.setActiveIssueSubscribed).toHaveBeenCalledWith({ + subscribed: true, + projectPath: 'gitlab-org/test-subgroup/gitlab-test', + }); + + await wrapper.vm.$nextTick(); + + expect(findGlLoadingIcon().exists()).toBe(false); + expect(findToggle().props('value')).toBe(true); + }); + + it('unsubscribing from notification', async () => { + createComponent({ + ...mockActiveIssue, + subscribed: true, + }); + mockSetActiveIssueSubscribed(false); + + expect(findGlLoadingIcon().exists()).toBe(false); + + findToggle().trigger('click'); + + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.setActiveIssueSubscribed).toHaveBeenCalledWith({ + subscribed: false, + projectPath: 'gitlab-org/test-subgroup/gitlab-test', + }); + expect(findGlLoadingIcon().exists()).toBe(true); + + await wrapper.vm.$nextTick(); + + expect(findGlLoadingIcon().exists()).toBe(false); + expect(findToggle().props('value')).toBe(false); + }); + + it('flashes an error message when setting the subscribed state fails', async () => { + createComponent(); + jest.spyOn(wrapper.vm, 'setActiveIssueSubscribed').mockImplementation(async () => { + throw new Error(); + }); + + findToggle().trigger('click'); + + await wrapper.vm.$nextTick(); + expect(createFlash).toHaveBeenNthCalledWith(1, { + message: wrapper.vm.$options.i18n.updateSubscribedErrorMessage, + }); + }); + }); +}); |