diff options
Diffstat (limited to 'spec/frontend/boards/components')
17 files changed, 738 insertions, 384 deletions
diff --git a/spec/frontend/boards/components/board_add_new_column_form_spec.js b/spec/frontend/boards/components/board_add_new_column_form_spec.js new file mode 100644 index 00000000000..3702f55f17b --- /dev/null +++ b/spec/frontend/boards/components/board_add_new_column_form_spec.js @@ -0,0 +1,166 @@ +import { GlFormGroup, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; +import Vuex from 'vuex'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue'; +import defaultState from '~/boards/stores/state'; +import { mockLabelList } from '../mock_data'; + +Vue.use(Vuex); + +describe('Board card layout', () => { + let wrapper; + + const createStore = ({ actions = {}, getters = {}, state = {} } = {}) => { + return new Vuex.Store({ + state: { + ...defaultState, + ...state, + }, + actions, + getters, + }); + }; + + const mountComponent = ({ + loading = false, + formDescription = '', + searchLabel = '', + searchPlaceholder = '', + selectedId, + actions, + slots, + } = {}) => { + wrapper = extendedWrapper( + shallowMount(BoardAddNewColumnForm, { + stubs: { + GlFormGroup: true, + }, + propsData: { + loading, + formDescription, + searchLabel, + searchPlaceholder, + selectedId, + }, + slots, + store: createStore({ + actions: { + setAddColumnFormVisibility: jest.fn(), + ...actions, + }, + }), + }), + ); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + const formTitle = () => wrapper.findByTestId('board-add-column-form-title').text(); + const findSearchInput = () => wrapper.find(GlSearchBoxByType); + const findSearchLabel = () => wrapper.find(GlFormGroup); + const cancelButton = () => wrapper.findByTestId('cancelAddNewColumn'); + const submitButton = () => wrapper.findByTestId('addNewColumnButton'); + + it('shows form title & search input', () => { + mountComponent(); + + expect(formTitle()).toEqual(BoardAddNewColumnForm.i18n.newList); + expect(findSearchInput().exists()).toBe(true); + }); + + it('clicking cancel hides the form', () => { + const setAddColumnFormVisibility = jest.fn(); + mountComponent({ + actions: { + setAddColumnFormVisibility, + }, + }); + + cancelButton().vm.$emit('click'); + + expect(setAddColumnFormVisibility).toHaveBeenCalledWith(expect.anything(), false); + }); + + it('sets placeholder and description from props', () => { + const props = { + formDescription: 'Some description of a list', + }; + + mountComponent(props); + + expect(wrapper.html()).toHaveText(props.formDescription); + }); + + describe('items', () => { + const mountWithItems = (loading) => + mountComponent({ + loading, + slots: { + items: '<div class="item-slot">Some kind of list</div>', + }, + }); + + it('hides items slot and shows skeleton while loading', () => { + mountWithItems(true); + + expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true); + expect(wrapper.find('.item-slot').exists()).toBe(false); + }); + + it('shows items slot and hides skeleton while not loading', () => { + mountWithItems(false); + + expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false); + expect(wrapper.find('.item-slot').exists()).toBe(true); + }); + }); + + describe('search box', () => { + it('sets label and placeholder text from props', () => { + const props = { + searchLabel: 'Some items', + searchPlaceholder: 'Search for an item', + }; + + mountComponent(props); + + expect(findSearchLabel().attributes('label')).toEqual(props.searchLabel); + expect(findSearchInput().attributes('placeholder')).toEqual(props.searchPlaceholder); + }); + + it('emits filter event on input', () => { + mountComponent(); + + const searchText = 'some text'; + + findSearchInput().vm.$emit('input', searchText); + + expect(wrapper.emitted('filter-items')).toEqual([[searchText]]); + }); + }); + + describe('Add list button', () => { + it('is disabled if no item is selected', () => { + mountComponent(); + + expect(submitButton().props('disabled')).toBe(true); + }); + + it('emits add-list event on click', async () => { + mountComponent({ + selectedId: mockLabelList.label.id, + }); + + await nextTick(); + + submitButton().vm.$emit('click'); + + expect(wrapper.emitted('add-list')).toEqual([[]]); + }); + }); +}); diff --git a/spec/frontend/boards/components/board_add_new_column_spec.js b/spec/frontend/boards/components/board_add_new_column_spec.js new file mode 100644 index 00000000000..60584eaf6cf --- /dev/null +++ b/spec/frontend/boards/components/board_add_new_column_spec.js @@ -0,0 +1,115 @@ +import { shallowMount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; +import Vuex from 'vuex'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import BoardAddNewColumn from '~/boards/components/board_add_new_column.vue'; +import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue'; +import defaultState from '~/boards/stores/state'; +import { mockLabelList } from '../mock_data'; + +Vue.use(Vuex); + +describe('Board card layout', () => { + let wrapper; + + const createStore = ({ actions = {}, getters = {}, state = {} } = {}) => { + return new Vuex.Store({ + state: { + ...defaultState, + ...state, + }, + actions, + getters, + }); + }; + + const mountComponent = ({ + selectedId, + labels = [], + getListByLabelId = jest.fn(), + actions = {}, + } = {}) => { + wrapper = extendedWrapper( + shallowMount(BoardAddNewColumn, { + data() { + return { + selectedId, + }; + }, + store: createStore({ + actions: { + fetchLabels: jest.fn(), + setAddColumnFormVisibility: jest.fn(), + ...actions, + }, + getters: { + shouldUseGraphQL: () => true, + getListByLabelId: () => getListByLabelId, + }, + state: { + labels, + labelsLoading: false, + isEpicBoard: false, + }, + }), + provide: { + scopedLabelsAvailable: true, + }, + }), + ); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('Add list button', () => { + it('calls addList', async () => { + const getListByLabelId = jest.fn().mockReturnValue(null); + const highlightList = jest.fn(); + const createList = jest.fn(); + + mountComponent({ + labels: [mockLabelList.label], + selectedId: mockLabelList.label.id, + getListByLabelId, + actions: { + createList, + highlightList, + }, + }); + + wrapper.findComponent(BoardAddNewColumnForm).vm.$emit('add-list'); + + await nextTick(); + + expect(highlightList).not.toHaveBeenCalled(); + expect(createList).toHaveBeenCalledWith(expect.anything(), { + labelId: mockLabelList.label.id, + }); + }); + + it('highlights existing list if trying to re-add', async () => { + const getListByLabelId = jest.fn().mockReturnValue(mockLabelList); + const highlightList = jest.fn(); + const createList = jest.fn(); + + mountComponent({ + labels: [mockLabelList.label], + selectedId: mockLabelList.label.id, + getListByLabelId, + actions: { + createList, + highlightList, + }, + }); + + wrapper.findComponent(BoardAddNewColumnForm).vm.$emit('add-list'); + + await nextTick(); + + expect(highlightList).toHaveBeenCalledWith(expect.anything(), mockLabelList.id); + expect(createList).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/boards/components/board_card_deprecated_spec.js b/spec/frontend/boards/components/board_card_deprecated_spec.js new file mode 100644 index 00000000000..266cbc7106d --- /dev/null +++ b/spec/frontend/boards/components/board_card_deprecated_spec.js @@ -0,0 +1,219 @@ +/* global List */ +/* global ListAssignee */ +/* global ListLabel */ + +import { mount } from '@vue/test-utils'; + +import MockAdapter from 'axios-mock-adapter'; +import waitForPromises from 'helpers/wait_for_promises'; +import BoardCardDeprecated from '~/boards/components/board_card_deprecated.vue'; +import issueCardInner from '~/boards/components/issue_card_inner_deprecated.vue'; +import eventHub from '~/boards/eventhub'; +import store from '~/boards/stores'; +import boardsStore from '~/boards/stores/boards_store'; +import axios from '~/lib/utils/axios_utils'; + +import sidebarEventHub from '~/sidebar/event_hub'; +import '~/boards/models/label'; +import '~/boards/models/assignee'; +import '~/boards/models/list'; +import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; +import { listObj, boardsMockInterceptor, setMockEndpoints } from '../mock_data'; + +describe('BoardCard', () => { + 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 = mount(BoardCardDeprecated, { + stubs: { + issueCardInner, + }, + store, + propsData: { + list, + issue: list.issues[0], + disabled: false, + index: 0, + ...propsData, + }, + provide: { + groupId: null, + rootPath: '/', + scopedLabelsAvailable: false, + }, + }); + }; + + const setupData = async () => { + 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', + }); + await waitForPromises(); + + 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.find('[data-testid="board_card"').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('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, false); + expect(boardsStore.detail.list).toEqual(wrapper.vm.list); + }); + + it('resets detail issue to empty if already set', () => { + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + const [issue] = list.issues; + boardsStore.detail.issue = issue; + mountComponent(); + + wrapper.trigger('mousedown'); + wrapper.trigger('mouseup'); + + expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue', false); + }); + }); + + describe('sidebarHub events', () => { + it('closes all sidebars before showing an issue if no issues are opened', () => { + jest.spyOn(sidebarEventHub, '$emit').mockImplementation(() => {}); + boardsStore.detail.issue = {}; + mountComponent(); + + // sets conditional so that event is emitted. + wrapper.trigger('mousedown'); + + wrapper.trigger('mouseup'); + + expect(sidebarEventHub.$emit).toHaveBeenCalledWith('sidebar.closeAll'); + }); + + it('it does not closes all sidebars before showing an issue if an issue is opened', () => { + jest.spyOn(sidebarEventHub, '$emit').mockImplementation(() => {}); + const [issue] = list.issues; + boardsStore.detail.issue = issue; + mountComponent(); + + wrapper.trigger('mousedown'); + + expect(sidebarEventHub.$emit).not.toHaveBeenCalledWith('sidebar.closeAll'); + }); + }); +}); diff --git a/spec/frontend/boards/components/board_card_layout_deprecated_spec.js b/spec/frontend/boards/components/board_card_layout_deprecated_spec.js index 426c5289ba6..9853c9f434f 100644 --- a/spec/frontend/boards/components/board_card_layout_deprecated_spec.js +++ b/spec/frontend/boards/components/board_card_layout_deprecated_spec.js @@ -11,7 +11,7 @@ import '~/boards/models/label'; import '~/boards/models/assignee'; import '~/boards/models/list'; import BoardCardLayout from '~/boards/components/board_card_layout_deprecated.vue'; -import issueCardInner from '~/boards/components/issue_card_inner.vue'; +import issueCardInner from '~/boards/components/issue_card_inner_deprecated.vue'; import { ISSUABLE } from '~/boards/constants'; import boardsVuexStore from '~/boards/stores'; import boardsStore from '~/boards/stores/boards_store'; diff --git a/spec/frontend/boards/components/board_card_layout_spec.js b/spec/frontend/boards/components/board_card_layout_spec.js deleted file mode 100644 index 3fa8714807c..00000000000 --- a/spec/frontend/boards/components/board_card_layout_spec.js +++ /dev/null @@ -1,116 +0,0 @@ -import { createLocalVue, shallowMount } from '@vue/test-utils'; -import Vuex from 'vuex'; - -import BoardCardLayout from '~/boards/components/board_card_layout.vue'; -import IssueCardInner from '~/boards/components/issue_card_inner.vue'; -import { ISSUABLE } from '~/boards/constants'; -import defaultState from '~/boards/stores/state'; -import { mockLabelList, mockIssue } from '../mock_data'; - -describe('Board card layout', () => { - let wrapper; - let store; - - const localVue = createLocalVue(); - localVue.use(Vuex); - - const createStore = ({ getters = {}, actions = {} } = {}) => { - store = new Vuex.Store({ - state: defaultState, - actions, - getters, - }); - }; - - // this particular mount component needs to be used after the root beforeEach because it depends on list being initialized - const mountComponent = ({ propsData = {}, provide = {} } = {}) => { - wrapper = shallowMount(BoardCardLayout, { - localVue, - stubs: { - IssueCardInner, - }, - store, - propsData: { - list: mockLabelList, - issue: mockIssue, - disabled: false, - index: 0, - ...propsData, - }, - provide: { - groupId: null, - rootPath: '/', - scopedLabelsAvailable: false, - ...provide, - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - describe('mouse events', () => { - it('sets showDetail to true on mousedown', async () => { - createStore(); - mountComponent(); - - wrapper.trigger('mousedown'); - await wrapper.vm.$nextTick(); - - expect(wrapper.vm.showDetail).toBe(true); - }); - - it('sets showDetail to false on mousemove', async () => { - createStore(); - mountComponent(); - wrapper.trigger('mousedown'); - await wrapper.vm.$nextTick(); - expect(wrapper.vm.showDetail).toBe(true); - wrapper.trigger('mousemove'); - await wrapper.vm.$nextTick(); - expect(wrapper.vm.showDetail).toBe(false); - }); - - it("calls 'setActiveId'", async () => { - const setActiveId = jest.fn(); - createStore({ - actions: { - setActiveId, - }, - }); - mountComponent(); - - wrapper.trigger('mouseup'); - await wrapper.vm.$nextTick(); - - expect(setActiveId).toHaveBeenCalledTimes(1); - expect(setActiveId).toHaveBeenCalledWith(expect.any(Object), { - id: mockIssue.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: mockIssue.id, - sidebarType: ISSUABLE, - }); - }); - }); -}); diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js index 5f26ae1bb3b..022f8c05e1e 100644 --- a/spec/frontend/boards/components/board_card_spec.js +++ b/spec/frontend/boards/components/board_card_spec.js @@ -1,43 +1,50 @@ -/* global List */ -/* global ListAssignee */ -/* global ListLabel */ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import Vuex from 'vuex'; -import { mount } from '@vue/test-utils'; - -import MockAdapter from 'axios-mock-adapter'; -import waitForPromises from 'helpers/wait_for_promises'; import BoardCard from '~/boards/components/board_card.vue'; -import issueCardInner from '~/boards/components/issue_card_inner.vue'; -import eventHub from '~/boards/eventhub'; -import store from '~/boards/stores'; -import boardsStore from '~/boards/stores/boards_store'; -import axios from '~/lib/utils/axios_utils'; - -import sidebarEventHub from '~/sidebar/event_hub'; -import '~/boards/models/label'; -import '~/boards/models/assignee'; -import '~/boards/models/list'; -import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; -import { listObj, boardsMockInterceptor, setMockEndpoints } from '../mock_data'; - -describe('BoardCard', () => { - let wrapper; - let mock; - let list; +import BoardCardInner from '~/boards/components/board_card_inner.vue'; +import { inactiveId } from '~/boards/constants'; +import { mockLabelList, mockIssue } from '../mock_data'; - const findIssueCardInner = () => wrapper.find(issueCardInner); - const findUserAvatarLink = () => wrapper.find(userAvatarLink); +describe('Board card', () => { + let wrapper; + let store; + let mockActions; + + const localVue = createLocalVue(); + localVue.use(Vuex); + + const createStore = ({ initialState = {}, isSwimlanesOn = false } = {}) => { + mockActions = { + toggleBoardItem: jest.fn(), + toggleBoardItemMultiSelection: jest.fn(), + }; + + store = new Vuex.Store({ + state: { + activeId: inactiveId, + selectedBoardItems: [], + ...initialState, + }, + actions: mockActions, + getters: { + isSwimlanesOn: () => isSwimlanesOn, + isEpicBoard: () => false, + }, + }); + }; // this particular mount component needs to be used after the root beforeEach because it depends on list being initialized - const mountComponent = (propsData) => { - wrapper = mount(BoardCard, { + const mountComponent = ({ propsData = {}, provide = {} } = {}) => { + wrapper = shallowMount(BoardCard, { + localVue, stubs: { - issueCardInner, + BoardCardInner, }, store, propsData: { - list, - issue: list.issues[0], + list: mockLabelList, + item: mockIssue, disabled: false, index: 0, ...propsData, @@ -46,174 +53,94 @@ describe('BoardCard', () => { groupId: null, rootPath: '/', scopedLabelsAvailable: false, + ...provide, }, }); }; - const setupData = async () => { - 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', - }); - await waitForPromises(); - - list.issues[0].labels.push(label1); + const selectCard = async () => { + wrapper.trigger('mouseup'); + await wrapper.vm.$nextTick(); }; - beforeEach(() => { - mock = new MockAdapter(axios); - mock.onAny().reply(boardsMockInterceptor); - setMockEndpoints(); - return setupData(); - }); + const multiSelectCard = async () => { + wrapper.trigger('mouseup', { ctrlKey: true }); + await wrapper.vm.$nextTick(); + }; afterEach(() => { wrapper.destroy(); wrapper = null; - list = null; - mock.restore(); - }); - - it('when details issue is empty does not show the element', () => { - mountComponent(); - expect(wrapper.find('[data-testid="board_card"').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'); + store = null; }); - it('adds disabled class is disabled is true', () => { - mountComponent({ disabled: true }); - - expect(wrapper.classes()).toContain('is-disabled'); - }); - - describe('mouse events', () => { - it('does not set detail issue if showDetail is false', () => { + describe.each` + isSwimlanesOn + ${true} | ${false} + `('when isSwimlanesOn is $isSwimlanesOn', ({ isSwimlanesOn }) => { + it('should not highlight the card by default', async () => { + createStore({ isSwimlanesOn }); 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({}); + expect(wrapper.classes()).not.toContain('is-active'); + expect(wrapper.classes()).not.toContain('multi-select'); }); - 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', - }), - ], + it('should highlight the card with a correct style when selected', async () => { + createStore({ + initialState: { + activeId: mockIssue.id, }, + isSwimlanesOn, }); - - 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, false); - expect(boardsStore.detail.list).toEqual(wrapper.vm.list); + expect(wrapper.classes()).toContain('is-active'); + expect(wrapper.classes()).not.toContain('multi-select'); }); - it('resets detail issue to empty if already set', () => { - jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - const [issue] = list.issues; - boardsStore.detail.issue = issue; + it('should highlight the card with a correct style when multi-selected', async () => { + createStore({ + initialState: { + activeId: inactiveId, + selectedBoardItems: [mockIssue], + }, + isSwimlanesOn, + }); mountComponent(); - wrapper.trigger('mousedown'); - wrapper.trigger('mouseup'); - - expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue', false); + expect(wrapper.classes()).toContain('multi-select'); + expect(wrapper.classes()).not.toContain('is-active'); }); - }); - - describe('sidebarHub events', () => { - it('closes all sidebars before showing an issue if no issues are opened', () => { - jest.spyOn(sidebarEventHub, '$emit').mockImplementation(() => {}); - boardsStore.detail.issue = {}; - mountComponent(); - - // sets conditional so that event is emitted. - wrapper.trigger('mousedown'); - wrapper.trigger('mouseup'); + describe('when mouseup event is called on the card', () => { + beforeEach(() => { + createStore({ isSwimlanesOn }); + mountComponent(); + }); - expect(sidebarEventHub.$emit).toHaveBeenCalledWith('sidebar.closeAll'); - }); + describe('when not using multi-select', () => { + it('should call vuex action "toggleBoardItem" with correct parameters', async () => { + await selectCard(); - it('it does not closes all sidebars before showing an issue if an issue is opened', () => { - jest.spyOn(sidebarEventHub, '$emit').mockImplementation(() => {}); - const [issue] = list.issues; - boardsStore.detail.issue = issue; - mountComponent(); + expect(mockActions.toggleBoardItem).toHaveBeenCalledTimes(1); + expect(mockActions.toggleBoardItem).toHaveBeenCalledWith(expect.any(Object), { + boardItem: mockIssue, + }); + }); + }); - wrapper.trigger('mousedown'); + describe('when using multi-select', () => { + it('should call vuex action "multiSelectBoardItem" with correct parameters', async () => { + await multiSelectCard(); - expect(sidebarEventHub.$emit).not.toHaveBeenCalledWith('sidebar.closeAll'); + expect(mockActions.toggleBoardItemMultiSelection).toHaveBeenCalledTimes(1); + expect(mockActions.toggleBoardItemMultiSelection).toHaveBeenCalledWith( + expect.any(Object), + mockIssue, + ); + }); + }); }); }); }); diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js index 858efea99ad..32499bd5480 100644 --- a/spec/frontend/boards/components/board_form_spec.js +++ b/spec/frontend/boards/components/board_form_spec.js @@ -8,6 +8,7 @@ import { formType } from '~/boards/constants'; import createBoardMutation from '~/boards/graphql/board_create.mutation.graphql'; import destroyBoardMutation from '~/boards/graphql/board_destroy.mutation.graphql'; import updateBoardMutation from '~/boards/graphql/board_update.mutation.graphql'; +import { createStore } from '~/boards/stores'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { visitUrl } from '~/lib/utils/url_utility'; @@ -48,6 +49,13 @@ describe('BoardForm', () => { const findDeleteConfirmation = () => wrapper.find('[data-testid="delete-confirmation-message"]'); const findInput = () => wrapper.find('#board-new-name'); + const store = createStore({ + getters: { + isGroupBoard: () => true, + isProjectBoard: () => false, + }, + }); + const createComponent = (props, data) => { wrapper = shallowMount(BoardForm, { propsData: { ...defaultProps, ...props }, @@ -64,6 +72,7 @@ describe('BoardForm', () => { mutate, }, }, + store, attachTo: document.body, }); }; diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js index f30e3792435..d2dfb4148b3 100644 --- a/spec/frontend/boards/components/board_list_header_spec.js +++ b/spec/frontend/boards/components/board_list_header_spec.js @@ -1,5 +1,6 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { mockLabelList } from 'jest/boards/mock_data'; import BoardListHeader from '~/boards/components/board_list_header.vue'; @@ -14,6 +15,7 @@ describe('Board List Header Component', () => { let store; const updateListSpy = jest.fn(); + const toggleListCollapsedSpy = jest.fn(); afterEach(() => { wrapper.destroy(); @@ -43,38 +45,39 @@ describe('Board List Header Component', () => { if (withLocalStorage) { localStorage.setItem( - `boards.${boardId}.${listMock.listType}.${listMock.id}.expanded`, - (!collapsed).toString(), + `boards.${boardId}.${listMock.listType}.${listMock.id}.collapsed`, + collapsed.toString(), ); } store = new Vuex.Store({ state: {}, - actions: { updateList: updateListSpy }, - getters: {}, + actions: { updateList: updateListSpy, toggleListCollapsed: toggleListCollapsedSpy }, + getters: { isEpicBoard: () => false }, }); - wrapper = shallowMount(BoardListHeader, { - store, - localVue, - propsData: { - disabled: false, - list: listMock, - }, - provide: { - boardId, - weightFeatureAvailable: false, - currentUserId, - }, - }); + wrapper = extendedWrapper( + shallowMount(BoardListHeader, { + store, + localVue, + propsData: { + disabled: false, + list: listMock, + }, + provide: { + boardId, + weightFeatureAvailable: false, + currentUserId, + }, + }), + ); }; 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'); + const findCaret = () => wrapper.findByTestId('board-title-caret'); describe('Add issue button', () => { const hasNoAddButton = [ListType.closed]; @@ -114,40 +117,29 @@ describe('Board List Header Component', () => { }); describe('expanding / collapsing the column', () => { - it('does not collapse when clicking the header', async () => { + it('should display collapse icon when column is expanded', async () => { createComponent(); - expect(isCollapsed()).toBe(false); - - wrapper.find('[data-testid="board-list-header"]').trigger('click'); + const icon = findCaret(); - await wrapper.vm.$nextTick(); - - expect(isCollapsed()).toBe(false); + expect(icon.props('icon')).toBe('chevron-right'); }); - it('collapses expanded Column when clicking the collapse icon', async () => { - createComponent(); - - expect(isCollapsed()).toBe(false); - - findCaret().vm.$emit('click'); + it('should display expand icon when column is collapsed', async () => { + createComponent({ collapsed: true }); - await wrapper.vm.$nextTick(); + const icon = findCaret(); - expect(isCollapsed()).toBe(true); + expect(icon.props('icon')).toBe('chevron-down'); }); - it('expands collapsed Column when clicking the expand icon', async () => { - createComponent({ collapsed: true }); - - expect(isCollapsed()).toBe(true); + it('should dispatch toggleListCollapse when clicking the collapse icon', async () => { + createComponent(); findCaret().vm.$emit('click'); await wrapper.vm.$nextTick(); - - expect(isCollapsed()).toBe(false); + expect(toggleListCollapsedSpy).toHaveBeenCalledTimes(1); }); it("when logged in it calls list update and doesn't set localStorage", async () => { @@ -157,7 +149,7 @@ describe('Board List Header Component', () => { await wrapper.vm.$nextTick(); expect(updateListSpy).toHaveBeenCalledTimes(1); - expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(null); + expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.collapsed`)).toBe(null); }); it("when logged out it doesn't call list update and sets localStorage", async () => { @@ -167,7 +159,7 @@ describe('Board List Header Component', () => { await wrapper.vm.$nextTick(); expect(updateListSpy).not.toHaveBeenCalled(); - expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(String(isExpanded())); + expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.collapsed`)).toBe(String(isCollapsed())); }); }); diff --git a/spec/frontend/boards/components/board_new_issue_spec.js b/spec/frontend/boards/components/board_new_issue_spec.js index ce8c95527e9..737a18294bc 100644 --- a/spec/frontend/boards/components/board_new_issue_spec.js +++ b/spec/frontend/boards/components/board_new_issue_spec.js @@ -2,7 +2,6 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; import BoardNewIssue from '~/boards/components/board_new_issue.vue'; -import '~/boards/models/list'; import { mockList, mockGroupProjects } from '../mock_data'; const localVue = createLocalVue(); @@ -31,7 +30,7 @@ describe('Issue boards new issue form', () => { const store = new Vuex.Store({ state: { selectedProject: mockGroupProjects[0] }, actions: { addListNewIssue: addListNewIssuesSpy }, - getters: {}, + getters: { isGroupBoard: () => false, isProjectBoard: () => true }, }); wrapper = shallowMount(BoardNewIssue, { diff --git a/spec/frontend/boards/components/filtered_search_spec.js b/spec/frontend/boards/components/filtered_search_spec.js new file mode 100644 index 00000000000..7f238aa671f --- /dev/null +++ b/spec/frontend/boards/components/filtered_search_spec.js @@ -0,0 +1,65 @@ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import Vuex from 'vuex'; +import FilteredSearch from '~/boards/components/filtered_search.vue'; +import { createStore } from '~/boards/stores'; +import * as commonUtils from '~/lib/utils/common_utils'; +import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('FilteredSearch', () => { + let wrapper; + let store; + + const createComponent = () => { + wrapper = shallowMount(FilteredSearch, { + localVue, + propsData: { search: '' }, + store, + attachTo: document.body, + }); + }; + + beforeEach(() => { + // this needed for actions call for performSearch + window.gon = { features: {} }; + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('default', () => { + beforeEach(() => { + store = createStore(); + + jest.spyOn(store, 'dispatch'); + + createComponent(); + }); + + it('finds FilteredSearch', () => { + expect(wrapper.find(FilteredSearchBarRoot).exists()).toBe(true); + }); + + describe('when onFilter is emitted', () => { + it('calls performSearch', () => { + wrapper.find(FilteredSearchBarRoot).vm.$emit('onFilter', [{ value: { data: '' } }]); + + expect(store.dispatch).toHaveBeenCalledWith('performSearch'); + }); + + it('calls historyPushState', () => { + commonUtils.historyPushState = jest.fn(); + wrapper + .find(FilteredSearchBarRoot) + .vm.$emit('onFilter', [{ value: { data: 'searchQuery' } }]); + + expect(commonUtils.historyPushState).toHaveBeenCalledWith( + 'http://test.host/?search=searchQuery', + ); + }); + }); + }); +}); diff --git a/spec/frontend/boards/components/issue_count_spec.js b/spec/frontend/boards/components/item_count_spec.js index f1870e9cc9e..45980c36f1c 100644 --- a/spec/frontend/boards/components/issue_count_spec.js +++ b/spec/frontend/boards/components/item_count_spec.js @@ -1,10 +1,10 @@ import { shallowMount } from '@vue/test-utils'; -import IssueCount from '~/boards/components/issue_count.vue'; +import IssueCount from '~/boards/components/item_count.vue'; describe('IssueCount', () => { let vm; let maxIssueCount; - let issuesSize; + let itemsSize; const createComponent = (props) => { vm = shallowMount(IssueCount, { propsData: props }); @@ -12,20 +12,20 @@ describe('IssueCount', () => { afterEach(() => { maxIssueCount = 0; - issuesSize = 0; + itemsSize = 0; if (vm) vm.destroy(); }); describe('when maxIssueCount is zero', () => { beforeEach(() => { - issuesSize = 3; + itemsSize = 3; - createComponent({ maxIssueCount: 0, issuesSize }); + createComponent({ maxIssueCount: 0, itemsSize }); }); it('contains issueSize in the template', () => { - expect(vm.find('.js-issue-size').text()).toEqual(String(issuesSize)); + expect(vm.find('[data-testid="board-items-count"]').text()).toEqual(String(itemsSize)); }); it('does not contains maxIssueCount in the template', () => { @@ -36,9 +36,9 @@ describe('IssueCount', () => { describe('when maxIssueCount is greater than zero', () => { beforeEach(() => { maxIssueCount = 2; - issuesSize = 1; + itemsSize = 1; - createComponent({ maxIssueCount, issuesSize }); + createComponent({ maxIssueCount, itemsSize }); }); afterEach(() => { @@ -46,7 +46,7 @@ describe('IssueCount', () => { }); it('contains issueSize in the template', () => { - expect(vm.find('.js-issue-size').text()).toEqual(String(issuesSize)); + expect(vm.find('[data-testid="board-items-count"]').text()).toEqual(String(itemsSize)); }); it('contains maxIssueCount in the template', () => { @@ -60,10 +60,10 @@ describe('IssueCount', () => { describe('when issueSize is greater than maxIssueCount', () => { beforeEach(() => { - issuesSize = 3; + itemsSize = 3; maxIssueCount = 2; - createComponent({ maxIssueCount, issuesSize }); + createComponent({ maxIssueCount, itemsSize }); }); afterEach(() => { @@ -71,7 +71,7 @@ describe('IssueCount', () => { }); it('contains issueSize in the template', () => { - expect(vm.find('.js-issue-size').text()).toEqual(String(issuesSize)); + expect(vm.find('[data-testid="board-items-count"]').text()).toEqual(String(itemsSize)); }); it('contains maxIssueCount in the template', () => { @@ -79,7 +79,7 @@ describe('IssueCount', () => { }); it('has text-danger class', () => { - expect(vm.find('.text-danger').text()).toEqual(String(issuesSize)); + expect(vm.find('.text-danger').text()).toEqual(String(itemsSize)); }); }); }); 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 index 7838b5a0b2f..8fd178a0856 100644 --- a/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js +++ b/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js @@ -24,7 +24,7 @@ describe('~/boards/components/sidebar/board_sidebar_due_date.vue', () => { const createWrapper = ({ dueDate = null } = {}) => { store = createStore(); - store.state.issues = { [TEST_ISSUE.id]: { ...TEST_ISSUE, dueDate } }; + store.state.boardItems = { [TEST_ISSUE.id]: { ...TEST_ISSUE, dueDate } }; store.state.activeId = TEST_ISSUE.id; wrapper = shallowMount(BoardSidebarDueDate, { @@ -61,7 +61,7 @@ describe('~/boards/components/sidebar/board_sidebar_due_date.vue', () => { createWrapper(); jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => { - store.state.issues[TEST_ISSUE.id].dueDate = TEST_DUE_DATE; + store.state.boardItems[TEST_ISSUE.id].dueDate = TEST_DUE_DATE; }); findDatePicker().vm.$emit('input', TEST_PARSED_DATE); await wrapper.vm.$nextTick(); @@ -86,7 +86,7 @@ describe('~/boards/components/sidebar/board_sidebar_due_date.vue', () => { createWrapper(); jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => { - store.state.issues[TEST_ISSUE.id].dueDate = null; + store.state.boardItems[TEST_ISSUE.id].dueDate = null; }); findDatePicker().vm.$emit('clear'); await wrapper.vm.$nextTick(); @@ -104,7 +104,7 @@ describe('~/boards/components/sidebar/board_sidebar_due_date.vue', () => { createWrapper({ dueDate: TEST_DUE_DATE }); jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => { - store.state.issues[TEST_ISSUE.id].dueDate = null; + store.state.boardItems[TEST_ISSUE.id].dueDate = null; }); findResetButton().vm.$emit('click'); await wrapper.vm.$nextTick(); 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 index bc7df1c76c6..723d0345f76 100644 --- a/spec/frontend/boards/components/sidebar/board_sidebar_issue_title_spec.js +++ b/spec/frontend/boards/components/sidebar/board_sidebar_issue_title_spec.js @@ -34,7 +34,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => { const createWrapper = (issue = TEST_ISSUE_A) => { store = createStore(); - store.state.issues = { [issue.id]: { ...issue } }; + store.state.boardItems = { [issue.id]: { ...issue } }; store.dispatch('setActiveId', { id: issue.id }); wrapper = shallowMount(BoardSidebarIssueTitle, { @@ -74,7 +74,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => { createWrapper(); jest.spyOn(wrapper.vm, 'setActiveIssueTitle').mockImplementation(() => { - store.state.issues[TEST_ISSUE_A.id].title = TEST_TITLE; + store.state.boardItems[TEST_ISSUE_A.id].title = TEST_TITLE; }); findFormInput().vm.$emit('input', TEST_TITLE); findForm().vm.$emit('submit', { preventDefault: () => {} }); @@ -147,7 +147,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => { createWrapper(TEST_ISSUE_B); jest.spyOn(wrapper.vm, 'setActiveIssueTitle').mockImplementation(() => { - store.state.issues[TEST_ISSUE_B.id].title = TEST_TITLE; + store.state.boardItems[TEST_ISSUE_B.id].title = TEST_TITLE; }); findFormInput().vm.$emit('input', TEST_TITLE); findCancelButton().vm.$emit('click'); 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 12b873ba7d8..98ac211238c 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 @@ -25,7 +25,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => { const createWrapper = ({ labels = [] } = {}) => { store = createStore(); - store.state.issues = { [TEST_ISSUE.id]: { ...TEST_ISSUE, labels } }; + store.state.boardItems = { [TEST_ISSUE.id]: { ...TEST_ISSUE, labels } }; store.state.activeId = TEST_ISSUE.id; wrapper = shallowMount(BoardSidebarLabelsSelect, { @@ -66,7 +66,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => { jest.spyOn(wrapper.vm, 'setActiveIssueLabels').mockImplementation(() => TEST_LABELS); findLabelsSelect().vm.$emit('updateSelectedLabels', TEST_LABELS_PAYLOAD); - store.state.issues[TEST_ISSUE.id].labels = TEST_LABELS; + store.state.boardItems[TEST_ISSUE.id].labels = TEST_LABELS; await wrapper.vm.$nextTick(); }); diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js index 8820ec7ae63..8706424a296 100644 --- a/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js +++ b/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js @@ -22,7 +22,7 @@ describe('~/boards/components/sidebar/board_sidebar_milestone_select.vue', () => const createWrapper = ({ milestone = null, loading = false } = {}) => { store = createStore(); - store.state.issues = { [TEST_ISSUE.id]: { ...TEST_ISSUE, milestone } }; + store.state.boardItems = { [TEST_ISSUE.id]: { ...TEST_ISSUE, milestone } }; store.state.activeId = TEST_ISSUE.id; wrapper = shallowMount(BoardSidebarMilestoneSelect, { @@ -113,7 +113,7 @@ describe('~/boards/components/sidebar/board_sidebar_milestone_select.vue', () => createWrapper(); jest.spyOn(wrapper.vm, 'setActiveIssueMilestone').mockImplementation(() => { - store.state.issues[TEST_ISSUE.id].milestone = TEST_MILESTONE; + store.state.boardItems[TEST_ISSUE.id].milestone = TEST_MILESTONE; }); findDropdownItem().vm.$emit('click'); await wrapper.vm.$nextTick(); @@ -137,7 +137,7 @@ describe('~/boards/components/sidebar/board_sidebar_milestone_select.vue', () => createWrapper({ milestone: TEST_MILESTONE }); jest.spyOn(wrapper.vm, 'setActiveIssueMilestone').mockImplementation(() => { - store.state.issues[TEST_ISSUE.id].milestone = null; + store.state.boardItems[TEST_ISSUE.id].milestone = null; }); findUnsetMilestoneItem().vm.$emit('click'); await wrapper.vm.$nextTick(); 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 3e6b0be0267..cfd7f32b2cc 100644 --- a/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js +++ b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js @@ -22,7 +22,7 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () = const createComponent = (activeIssue = { ...mockActiveIssue }) => { store = createStore(); - store.state.issues = { [activeIssue.id]: activeIssue }; + store.state.boardItems = { [activeIssue.id]: activeIssue }; store.state.activeId = activeIssue.id; wrapper = mount(BoardSidebarSubscription, { @@ -45,6 +45,12 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () = expect(findNotificationHeader().text()).toBe('Notifications'); }); + it('renders toggle with label', () => { + createComponent(); + + expect(findToggle().props('label')).toBe(BoardSidebarSubscription.i18n.header.title); + }); + it('renders toggle as "off" when currently not subscribed', () => { createComponent(); diff --git a/spec/frontend/boards/components/sidebar/remove_issue_spec.js b/spec/frontend/boards/components/sidebar/remove_issue_spec.js deleted file mode 100644 index 1f740c10106..00000000000 --- a/spec/frontend/boards/components/sidebar/remove_issue_spec.js +++ /dev/null @@ -1,28 +0,0 @@ -import { GlButton } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; - -import RemoveIssue from '~/boards/components/sidebar/remove_issue.vue'; - -describe('boards sidebar remove issue', () => { - let wrapper; - - const findButton = () => wrapper.find(GlButton); - - const createComponent = (propsData) => { - wrapper = shallowMount(RemoveIssue, { - propsData: { - issue: {}, - list: {}, - ...propsData, - }, - }); - }; - - beforeEach(() => { - createComponent(); - }); - - it('renders remove button', () => { - expect(findButton().exists()).toBe(true); - }); -}); |