import { GlIntersectionObserver, GlButton, GlLoadingIcon, GlSearchBoxByType, GlLink, } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; import DropdownContentsLabelsView from '~/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view.vue'; import LabelItem from '~/sidebar/components/labels/labels_select_vue/label_item.vue'; import * as actions from '~/sidebar/components/labels/labels_select_vue/store/actions'; import * as getters from '~/sidebar/components/labels/labels_select_vue/store/getters'; import mutations from '~/sidebar/components/labels/labels_select_vue/store/mutations'; import defaultState from '~/sidebar/components/labels/labels_select_vue/store/state'; import { mockConfig, mockLabels, mockRegularLabel } from './mock_data'; Vue.use(Vuex); describe('DropdownContentsLabelsView', () => { let wrapper; const createComponent = (initialState = mockConfig) => { const store = new Vuex.Store({ getters, mutations, state: { ...defaultState(), footerCreateLabelTitle: 'Create label', footerManageLabelTitle: 'Manage labels', }, actions: { ...actions, fetchLabels: jest.fn(), }, }); store.dispatch('setInitialState', initialState); store.dispatch('receiveLabelsSuccess', mockLabels); wrapper = shallowMount(DropdownContentsLabelsView, { store, }); }; beforeEach(() => { createComponent(); }); afterEach(() => { wrapper.destroy(); }); const findDropdownContent = () => wrapper.find('[data-testid="dropdown-content"]'); const findDropdownTitle = () => wrapper.find('[data-testid="dropdown-title"]'); const findDropdownFooter = () => wrapper.find('[data-testid="dropdown-footer"]'); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); describe('computed', () => { describe('visibleLabels', () => { it('returns matching labels filtered with `searchKey`', () => { // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax wrapper.setData({ searchKey: 'bug', }); expect(wrapper.vm.visibleLabels.length).toBe(1); expect(wrapper.vm.visibleLabels[0].title).toBe('Bug'); }); it('returns matching labels with fuzzy filtering', () => { // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax wrapper.setData({ searchKey: 'bg', }); expect(wrapper.vm.visibleLabels.length).toBe(2); expect(wrapper.vm.visibleLabels[0].title).toBe('Bug'); expect(wrapper.vm.visibleLabels[1].title).toBe('Boog'); }); it('returns all labels when `searchKey` is empty', () => { // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax wrapper.setData({ searchKey: '', }); expect(wrapper.vm.visibleLabels.length).toBe(mockLabels.length); }); }); describe('showNoMatchingResultsMessage', () => { it.each` searchKey | labels | labelsDescription | returnValue ${''} | ${[]} | ${'empty'} | ${false} ${'bug'} | ${[]} | ${'empty'} | ${true} ${''} | ${mockLabels} | ${'not empty'} | ${false} ${'bug'} | ${mockLabels} | ${'not empty'} | ${false} `( 'returns $returnValue when searchKey is "$searchKey" and visibleLabels is $labelsDescription', async ({ searchKey, labels, returnValue }) => { // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax wrapper.setData({ searchKey, }); wrapper.vm.$store.dispatch('receiveLabelsSuccess', labels); await nextTick(); expect(wrapper.vm.showNoMatchingResultsMessage).toBe(returnValue); }, ); }); }); describe('methods', () => { const fakePreventDefault = jest.fn(); describe('isLabelSelected', () => { it('returns true when provided `label` param is one of the selected labels', () => { expect(wrapper.vm.isLabelSelected(mockRegularLabel)).toBe(true); }); it('returns false when provided `label` param is not one of the selected labels', () => { expect(wrapper.vm.isLabelSelected(mockLabels[1])).toBe(false); }); }); describe('handleComponentAppear', () => { it('calls `focusInput` on searchInput field', async () => { wrapper.vm.$refs.searchInput.focusInput = jest.fn(); await wrapper.vm.handleComponentAppear(); expect(wrapper.vm.$refs.searchInput.focusInput).toHaveBeenCalled(); }); }); describe('handleComponentDisappear', () => { it('calls action `receiveLabelsSuccess` with empty array', () => { jest.spyOn(wrapper.vm, 'receiveLabelsSuccess'); wrapper.vm.handleComponentDisappear(); expect(wrapper.vm.receiveLabelsSuccess).toHaveBeenCalledWith([]); }); }); describe('handleCreateLabelClick', () => { it('calls actions `receiveLabelsSuccess` with empty array and `toggleDropdownContentsCreateView`', () => { jest.spyOn(wrapper.vm, 'receiveLabelsSuccess'); jest.spyOn(wrapper.vm, 'toggleDropdownContentsCreateView'); wrapper.vm.handleCreateLabelClick(); expect(wrapper.vm.receiveLabelsSuccess).toHaveBeenCalledWith([]); expect(wrapper.vm.toggleDropdownContentsCreateView).toHaveBeenCalled(); }); }); describe('handleKeyDown', () => { it('decreases `currentHighlightItem` value by 1 when Up arrow key is pressed', () => { // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax wrapper.setData({ currentHighlightItem: 1, }); wrapper.vm.handleKeyDown({ keyCode: UP_KEY_CODE, }); expect(wrapper.vm.currentHighlightItem).toBe(0); }); it('increases `currentHighlightItem` value by 1 when Down arrow key is pressed', () => { // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax wrapper.setData({ currentHighlightItem: 1, }); wrapper.vm.handleKeyDown({ keyCode: DOWN_KEY_CODE, }); expect(wrapper.vm.currentHighlightItem).toBe(2); }); it('resets the search text when the Enter key is pressed', () => { // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax wrapper.setData({ currentHighlightItem: 1, searchKey: 'bug', }); wrapper.vm.handleKeyDown({ keyCode: ENTER_KEY_CODE, preventDefault: fakePreventDefault, }); expect(wrapper.vm.searchKey).toBe(''); expect(fakePreventDefault).toHaveBeenCalled(); }); it('calls action `updateSelectedLabels` with currently highlighted label when Enter key is pressed', () => { jest.spyOn(wrapper.vm, 'updateSelectedLabels').mockImplementation(); // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax wrapper.setData({ currentHighlightItem: 2, }); wrapper.vm.handleKeyDown({ keyCode: ENTER_KEY_CODE, preventDefault: fakePreventDefault, }); expect(wrapper.vm.updateSelectedLabels).toHaveBeenCalledWith([mockLabels[2]]); }); it('calls action `toggleDropdownContents` when Esc key is pressed', () => { jest.spyOn(wrapper.vm, 'toggleDropdownContents').mockImplementation(); // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax wrapper.setData({ currentHighlightItem: 1, }); wrapper.vm.handleKeyDown({ keyCode: ESC_KEY_CODE, }); expect(wrapper.vm.toggleDropdownContents).toHaveBeenCalled(); }); it('calls action `scrollIntoViewIfNeeded` in next tick when any key is pressed', async () => { jest.spyOn(wrapper.vm, 'scrollIntoViewIfNeeded').mockImplementation(); // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax wrapper.setData({ currentHighlightItem: 1, }); wrapper.vm.handleKeyDown({ keyCode: DOWN_KEY_CODE, }); await nextTick(); expect(wrapper.vm.scrollIntoViewIfNeeded).toHaveBeenCalled(); }); }); describe('handleLabelClick', () => { beforeEach(() => { jest.spyOn(wrapper.vm, 'updateSelectedLabels').mockImplementation(); }); it('calls action `updateSelectedLabels` with provided `label` param', () => { wrapper.vm.handleLabelClick(mockRegularLabel); expect(wrapper.vm.updateSelectedLabels).toHaveBeenCalledWith([mockRegularLabel]); }); it('calls action `toggleDropdownContents` when `state.allowMultiselect` is false', () => { jest.spyOn(wrapper.vm, 'toggleDropdownContents'); wrapper.vm.$store.state.allowMultiselect = false; wrapper.vm.handleLabelClick(mockRegularLabel); expect(wrapper.vm.toggleDropdownContents).toHaveBeenCalled(); }); }); }); describe('template', () => { it('renders gl-intersection-observer as component root', () => { expect(wrapper.findComponent(GlIntersectionObserver).exists()).toBe(true); }); it('renders gl-loading-icon component when `labelsFetchInProgress` prop is true', async () => { wrapper.vm.$store.dispatch('requestLabels'); await nextTick(); const loadingIconEl = findLoadingIcon(); expect(loadingIconEl.exists()).toBe(true); expect(loadingIconEl.attributes('class')).toContain('labels-fetch-loading'); }); it('renders dropdown title element', () => { const titleEl = findDropdownTitle(); expect(titleEl.exists()).toBe(true); expect(titleEl.text()).toBe('Assign labels'); }); it('does not render dropdown title element when `state.variant` is "standalone"', () => { createComponent({ ...mockConfig, variant: 'standalone' }); expect(findDropdownTitle().exists()).toBe(false); }); it('renders dropdown title element when `state.variant` is "embedded"', () => { createComponent({ ...mockConfig, variant: 'embedded' }); expect(findDropdownTitle().exists()).toBe(true); }); it('renders dropdown close button element', () => { const closeButtonEl = findDropdownTitle().findComponent(GlButton); expect(closeButtonEl.exists()).toBe(true); expect(closeButtonEl.props('icon')).toBe('close'); }); it('renders label search input element', () => { const searchInputEl = wrapper.findComponent(GlSearchBoxByType); expect(searchInputEl.exists()).toBe(true); }); it('renders label elements for all labels', () => { expect(wrapper.findAllComponents(LabelItem)).toHaveLength(mockLabels.length); }); it('renders label element with `highlight` set to true when value of `currentHighlightItem` is more than -1', async () => { // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax wrapper.setData({ currentHighlightItem: 0, }); await nextTick(); const labelItemEl = findDropdownContent().findComponent(LabelItem); expect(labelItemEl.attributes('highlight')).toBe('true'); }); it('renders element containing "No matching results" when `searchKey` does not match with any label', async () => { // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax wrapper.setData({ searchKey: 'abc', }); await nextTick(); const noMatchEl = findDropdownContent().find('li'); expect(noMatchEl.isVisible()).toBe(true); expect(noMatchEl.text()).toContain('No matching results'); }); it('renders empty content while loading', async () => { wrapper.vm.$store.state.labelsFetchInProgress = true; await nextTick(); const dropdownContent = findDropdownContent(); const loadingIcon = findLoadingIcon(); expect(dropdownContent.exists()).toBe(true); expect(dropdownContent.isVisible()).toBe(true); expect(loadingIcon.exists()).toBe(true); expect(loadingIcon.isVisible()).toBe(true); }); it('renders footer list items', () => { const footerLinks = findDropdownFooter().findAllComponents(GlLink); const createLabelLink = footerLinks.at(0); const manageLabelsLink = footerLinks.at(1); expect(createLabelLink.exists()).toBe(true); expect(createLabelLink.text()).toBe('Create label'); expect(manageLabelsLink.exists()).toBe(true); expect(manageLabelsLink.text()).toBe('Manage labels'); }); it('does not render "Create label" footer link when `state.allowLabelCreate` is `false`', async () => { wrapper.vm.$store.state.allowLabelCreate = false; await nextTick(); const createLabelLink = findDropdownFooter().findAllComponents(GlLink).at(0); expect(createLabelLink.text()).not.toBe('Create label'); }); it('does not render footer list items when `state.variant` is "standalone"', () => { createComponent({ ...mockConfig, variant: 'standalone' }); expect(findDropdownFooter().exists()).toBe(false); }); it('does not render footer list items when `allowLabelCreate` is false and `labelsManagePath` is null', () => { createComponent({ ...mockConfig, allowLabelCreate: false, labelsManagePath: null, }); expect(findDropdownFooter().exists()).toBe(false); }); it('renders footer list items when `state.variant` is "embedded"', () => { expect(findDropdownFooter().exists()).toBe(true); }); }); });