import { GlFilteredSearchToken, GlLoadingIcon, GlFilteredSearchSuggestion, GlDropdownSectionHeader, GlDropdownDivider, GlDropdownText, } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { mockRegularLabel, mockLabels, } from 'jest/sidebar/components/labels/labels_select_vue/mock_data'; import { OPTIONS_NONE_ANY, OPERATOR_IS, OPERATOR_NOT, OPERATOR_OR, } from '~/vue_shared/components/filtered_search_bar/constants'; import { getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed, } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils'; import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; import { mockLabelToken } from '../mock_data'; jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils', () => ({ getRecentlyUsedSuggestions: jest.fn(), setTokenValueToRecentlyUsed: jest.fn(), stripQuotes: jest.requireActual('~/lib/utils/text_utility').stripQuotes, })); const mockStorageKey = 'recent-tokens-label_name'; const defaultStubs = { Portal: true, GlFilteredSearchToken: { template: `
`, }, GlFilteredSearchSuggestionList: { template: '
', methods: { getValue: () => '=', }, }, }; const mockSuggestionListTestId = 'suggestion-list'; const defaultSlots = { 'view-token': `
${mockRegularLabel.title}
`, view: `
${mockRegularLabel.title}
`, }; const defaultScopedSlots = { 'suggestions-list': `
`, }; const mockConfig = { ...mockLabelToken, recentSuggestionsStorageKey: mockStorageKey }; const mockProps = { config: mockConfig, value: { data: '' }, active: false, suggestions: [], suggestionsLoading: false, defaultSuggestions: OPTIONS_NONE_ANY, getActiveTokenValue: (labels, data) => labels.find((label) => label.title === data), cursorPosition: 'start', }; function createComponent({ props = {}, data = {}, stubs = defaultStubs, slots = defaultSlots, scopedSlots = defaultScopedSlots, mountFn = mount, } = {}) { return mountFn(BaseToken, { propsData: { ...mockProps, ...props, }, provide: { portalName: 'fake target', alignSuggestions: jest.fn(), suggestionsListClass: () => 'custom-class', termsAsTokens: () => false, filteredSearchSuggestionListInstance: { register: jest.fn(), unregister: jest.fn(), }, }, data() { return data; }, stubs, slots, scopedSlots, }); } describe('BaseToken', () => { let wrapper; const findGlFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken); const findMockSuggestionList = () => wrapper.findByTestId(mockSuggestionListTestId); const getMockSuggestionListSuggestions = () => JSON.parse(findMockSuggestionList().attributes('data-suggestions')); describe('data', () => { it('calls `getRecentlyUsedSuggestions` to populate `recentSuggestions` when `recentSuggestionsStorageKey` is defined', () => { wrapper = createComponent(); expect(getRecentlyUsedSuggestions).toHaveBeenCalledWith(mockStorageKey); }); }); describe('computed', () => { describe('activeTokenValue', () => { it('calls `getActiveTokenValue` when it is provided', () => { const mockGetActiveTokenValue = jest.fn(); wrapper = createComponent({ props: { value: { data: `"${mockRegularLabel.title}"` }, suggestions: mockLabels, getActiveTokenValue: mockGetActiveTokenValue, }, }); expect(mockGetActiveTokenValue).toHaveBeenCalledTimes(1); expect(mockGetActiveTokenValue).toHaveBeenCalledWith( mockLabels, `"${mockRegularLabel.title}"`, ); }); }); }); describe('watch', () => { describe('active', () => { beforeEach(() => { wrapper = createComponent({ props: { value: { data: `"${mockRegularLabel.title}"` }, active: true, }, }); }); it('emits `fetch-suggestions` event on the component when value of this prop is changed to false and `suggestions` array is empty', async () => { await wrapper.setProps({ active: false }); expect(wrapper.emitted('fetch-suggestions')).toEqual([[`"${mockRegularLabel.title}"`]]); }); }); }); describe('suggestions', () => { describe('with suggestions disabled', () => { beforeEach(() => { wrapper = createComponent({ props: { config: { suggestionsDisabled: true, }, suggestions: [{ id: 'Foo' }], }, mountFn: shallowMountExtended, }); }); it('does not render suggestions', () => { expect(findMockSuggestionList().exists()).toBe(false); }); }); describe('with available suggestions', () => { let mockSuggestions; describe.each` hasSuggestions | searchKey | shouldRenderSuggestions ${true} | ${null} | ${true} ${true} | ${'foo'} | ${true} ${false} | ${null} | ${false} `( `when hasSuggestions is $hasSuggestions`, ({ hasSuggestions, searchKey, shouldRenderSuggestions }) => { beforeEach(async () => { mockSuggestions = hasSuggestions ? [{ id: 'Foo' }] : []; const props = { defaultSuggestions: [], suggestions: mockSuggestions }; getRecentlyUsedSuggestions.mockReturnValue([]); wrapper = createComponent({ props, mountFn: shallowMountExtended, stubs: {} }); findGlFilteredSearchToken().vm.$emit('input', { data: searchKey }); await nextTick(); }); it(`${shouldRenderSuggestions ? 'should' : 'should not'} render suggestions`, () => { expect(findMockSuggestionList().exists()).toBe(shouldRenderSuggestions); if (shouldRenderSuggestions) { expect(getMockSuggestionListSuggestions()).toEqual(mockSuggestions); } }); }, ); it('limits the length of the rendered list using config.maxSuggestions', () => { mockSuggestions = ['a', 'b', 'c', 'd'].map((id) => ({ id })); const maxSuggestions = 2; const config = { ...mockConfig, maxSuggestions }; const props = { defaultSuggestions: [], suggestions: mockSuggestions, config }; getRecentlyUsedSuggestions.mockReturnValue([]); wrapper = createComponent({ props, mountFn: shallowMountExtended, stubs: {} }); expect(findMockSuggestionList().exists()).toBe(true); expect(getMockSuggestionListSuggestions().length).toEqual(maxSuggestions); }); }); describe('with preloaded suggestions', () => { const mockPreloadedSuggestions = [{ id: 'Foo' }, { id: 'Bar' }]; describe.each` searchKey | shouldRenderPreloadedSuggestions ${null} | ${true} ${'foo'} | ${false} `('when searchKey is $searchKey', ({ shouldRenderPreloadedSuggestions, searchKey }) => { beforeEach(async () => { const props = { preloadedSuggestions: mockPreloadedSuggestions }; wrapper = createComponent({ props, mountFn: shallowMountExtended, stubs: {} }); findGlFilteredSearchToken().vm.$emit('input', { data: searchKey }); await nextTick(); }); it(`${ shouldRenderPreloadedSuggestions ? 'should' : 'should not' } render preloaded suggestions`, () => { expect(findMockSuggestionList().exists()).toBe(shouldRenderPreloadedSuggestions); if (shouldRenderPreloadedSuggestions) { expect(getMockSuggestionListSuggestions()).toEqual(mockPreloadedSuggestions); } }); }); }); describe('with recent suggestions', () => { let mockSuggestions; describe.each` searchKey | recentEnabled | shouldRenderRecentSuggestions ${null} | ${true} | ${true} ${'foo'} | ${true} | ${false} ${null} | ${false} | ${false} `( 'when searchKey is $searchKey and recentEnabled is $recentEnabled', ({ shouldRenderRecentSuggestions, recentEnabled, searchKey }) => { beforeEach(async () => { const props = { value: { data: '', operator: '=' }, defaultSuggestions: [] }; if (recentEnabled) { mockSuggestions = [{ id: 'Foo' }, { id: 'Bar' }]; getRecentlyUsedSuggestions.mockReturnValue(mockSuggestions); } props.config = { recentSuggestionsStorageKey: recentEnabled ? mockStorageKey : null }; wrapper = createComponent({ props, mountFn: shallowMountExtended, stubs: {} }); findGlFilteredSearchToken().vm.$emit('input', { data: searchKey }); await nextTick(); }); it(`${ shouldRenderRecentSuggestions ? 'should' : 'should not' } render recent suggestions`, () => { expect(findMockSuggestionList().exists()).toBe(shouldRenderRecentSuggestions); expect(wrapper.findComponent(GlDropdownSectionHeader).exists()).toBe( shouldRenderRecentSuggestions, ); expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe( shouldRenderRecentSuggestions, ); if (shouldRenderRecentSuggestions) { expect(getMockSuggestionListSuggestions()).toEqual(mockSuggestions); } }); }, ); }); describe('with default suggestions', () => { describe.each` operator | shouldRenderFilteredSearchSuggestion ${OPERATOR_IS} | ${true} ${OPERATOR_NOT} | ${false} ${OPERATOR_OR} | ${false} `('when operator is $operator', ({ shouldRenderFilteredSearchSuggestion, operator }) => { beforeEach(() => { const props = { defaultSuggestions: OPTIONS_NONE_ANY, value: { data: '', operator }, }; wrapper = createComponent({ props, mountFn: shallowMountExtended }); }); it(`${ shouldRenderFilteredSearchSuggestion ? 'should' : 'should not' } render GlFilteredSearchSuggestion`, () => { const filteredSearchSuggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion) .wrappers; if (shouldRenderFilteredSearchSuggestion) { expect(filteredSearchSuggestions.map((c) => c.props())).toMatchObject( OPTIONS_NONE_ANY.map((opt) => ({ value: opt.value })), ); } else { expect(filteredSearchSuggestions).toHaveLength(0); } }); }); }); describe('with no suggestions', () => { it.each` data | expected ${{ searchKey: 'search' }} | ${'No matches found'} ${{ hasFetched: true }} | ${'No suggestions found'} `('shows $expected text', ({ data, expected }) => { wrapper = createComponent({ props: { config: { recentSuggestionsStorageKey: null }, defaultSuggestions: [], preloadedSuggestions: [], suggestions: [], suggestionsLoading: false, }, data, mountFn: shallowMountExtended, }); expect(wrapper.findComponent(GlDropdownText).text()).toBe(expected); }); }); }); describe('methods', () => { describe('handleTokenValueSelected', () => { const mockTokenValue = mockLabels[0]; it('calls `setTokenValueToRecentlyUsed` when `recentSuggestionsStorageKey` is defined', () => { wrapper = createComponent({ props: { suggestions: mockLabels } }); wrapper.vm.handleTokenValueSelected(mockTokenValue.title); expect(setTokenValueToRecentlyUsed).toHaveBeenCalledWith(mockStorageKey, mockTokenValue); }); it('does not add token from preloadedSuggestions', () => { wrapper = createComponent({ props: { preloadedSuggestions: [mockTokenValue] } }); wrapper.vm.handleTokenValueSelected(mockTokenValue.title); expect(setTokenValueToRecentlyUsed).not.toHaveBeenCalled(); }); }); }); describe('template', () => { it('renders gl-filtered-search-token component', () => { wrapper = createComponent({ stubs: {} }); expect(findGlFilteredSearchToken().props('config')).toEqual(mockProps.config); }); it('renders `view-token` slot when present', () => { wrapper = createComponent(); expect(wrapper.find('.js-view-token').exists()).toBe(true); }); it('renders `view` slot when present', () => { wrapper = createComponent(); expect(wrapper.find('.js-view').exists()).toBe(true); }); it('renders loading spinner when loading', () => { wrapper = createComponent({ props: { active: true, config: mockLabelToken, suggestionsLoading: true, }, stubs: { Portal: true }, }); expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); describe('events', () => { describe('when activeToken has been selected', () => { beforeEach(() => { wrapper = createComponent({ props: { getActiveTokenValue: () => ({ title: '' }) }, stubs: { Portal: true }, }); }); it('does not emit `fetch-suggestions` event on component after a delay when component emits `input` event', async () => { findGlFilteredSearchToken().vm.$emit('input', { data: 'foo' }); await nextTick(); jest.runAllTimers(); expect(wrapper.emitted('fetch-suggestions')).toEqual([['']]); }); }); describe('when activeToken has not been selected', () => { beforeEach(() => { wrapper = createComponent({ stubs: { Portal: true }, }); }); it('emits `fetch-suggestions` event on component after a delay when component emits `input` event', async () => { findGlFilteredSearchToken().vm.$emit('input', { data: 'foo' }); await nextTick(); jest.runAllTimers(); expect(wrapper.emitted('fetch-suggestions')[2]).toEqual(['foo']); }); describe('when search is started with a quote', () => { it('emits `fetch-suggestions` with filtered value', () => { findGlFilteredSearchToken().vm.$emit('input', { data: '"foo' }); expect(wrapper.emitted('fetch-suggestions')[2]).toEqual(['foo']); }); }); describe('when search starts and ends with a quote', () => { it('emits `fetch-suggestions` with filtered value', () => { findGlFilteredSearchToken().vm.$emit('input', { data: '"foo"' }); expect(wrapper.emitted('fetch-suggestions')[2]).toEqual(['foo']); }); }); }); }); }); });