diff options
Diffstat (limited to 'spec/frontend/header_search/components')
-rw-r--r-- | spec/frontend/header_search/components/app_spec.js | 272 | ||||
-rw-r--r-- | spec/frontend/header_search/components/header_search_scoped_items_spec.js | 47 |
2 files changed, 238 insertions, 81 deletions
diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js index f0de5b083ae..d89218f5542 100644 --- a/spec/frontend/header_search/components/app_spec.js +++ b/spec/frontend/header_search/components/app_spec.js @@ -1,22 +1,32 @@ -import { GlSearchBoxByType } from '@gitlab/ui'; +import { GlSearchBoxByType, GlToken, GlIcon } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { s__, sprintf } from '~/locale'; import HeaderSearchApp from '~/header_search/components/app.vue'; import HeaderSearchAutocompleteItems from '~/header_search/components/header_search_autocomplete_items.vue'; import HeaderSearchDefaultItems from '~/header_search/components/header_search_default_items.vue'; import HeaderSearchScopedItems from '~/header_search/components/header_search_scoped_items.vue'; -import { SEARCH_INPUT_DESCRIPTION, SEARCH_RESULTS_DESCRIPTION } from '~/header_search/constants'; +import { + SEARCH_INPUT_DESCRIPTION, + SEARCH_RESULTS_DESCRIPTION, + SEARCH_BOX_INDEX, + ICON_PROJECT, + ICON_GROUP, + ICON_SUBGROUP, + SCOPE_TOKEN_MAX_LENGTH, +} from '~/header_search/constants'; import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue'; import { ENTER_KEY } from '~/lib/utils/keys'; import { visitUrl } from '~/lib/utils/url_utility'; +import { truncate } from '~/lib/utils/text_utility'; import { MOCK_SEARCH, MOCK_SEARCH_QUERY, MOCK_USERNAME, MOCK_DEFAULT_SEARCH_OPTIONS, MOCK_SCOPED_SEARCH_OPTIONS, - MOCK_SORTED_AUTOCOMPLETE_OPTIONS, + MOCK_SEARCH_CONTEXT_FULL, } from '../mock_data'; Vue.use(Vuex); @@ -52,11 +62,27 @@ describe('HeaderSearchApp', () => { }); }; + const formatScopeName = (scopeName) => { + if (!scopeName) { + return false; + } + const searchResultsScope = s__('GlobalSearch|in %{scope}'); + return truncate( + sprintf(searchResultsScope, { + scope: scopeName, + }), + SCOPE_TOKEN_MAX_LENGTH, + ); + }; + afterEach(() => { wrapper.destroy(); }); + const findHeaderSearchForm = () => wrapper.findByTestId('header-search-form'); const findHeaderSearchInput = () => wrapper.findComponent(GlSearchBoxByType); + const findScopeToken = () => wrapper.findComponent(GlToken); + const findHeaderSearchInputKBD = () => wrapper.find('.keyboard-shortcut-helper'); const findHeaderSearchDropdown = () => wrapper.findByTestId('header-search-dropdown-menu'); const findHeaderSearchDefaultItems = () => wrapper.findComponent(HeaderSearchDefaultItems); const findHeaderSearchScopedItems = () => wrapper.findComponent(HeaderSearchScopedItems); @@ -76,6 +102,14 @@ describe('HeaderSearchApp', () => { expect(findHeaderSearchInput().exists()).toBe(true); }); + it('Header Search Input KBD hint', () => { + expect(findHeaderSearchInputKBD().exists()).toBe(true); + expect(findHeaderSearchInputKBD().text()).toContain('/'); + expect(findHeaderSearchInputKBD().attributes('title')).toContain( + 'Use the shortcut key <kbd>/</kbd> to start a search', + ); + }); + it('Search Input Description', () => { expect(findSearchInputDescription().exists()).toBe(true); }); @@ -106,53 +140,38 @@ describe('HeaderSearchApp', () => { }); describe.each` - search | showDefault | showScoped | showAutocomplete | showDropdownNavigation - ${null} | ${true} | ${false} | ${false} | ${true} - ${''} | ${true} | ${false} | ${false} | ${true} - ${'1'} | ${false} | ${false} | ${false} | ${false} - ${')'} | ${false} | ${false} | ${false} | ${false} - ${'t'} | ${false} | ${false} | ${true} | ${true} - ${'te'} | ${false} | ${true} | ${true} | ${true} - ${'tes'} | ${false} | ${true} | ${true} | ${true} - ${MOCK_SEARCH} | ${false} | ${true} | ${true} | ${true} - `( - 'Header Search Dropdown Items', - ({ search, showDefault, showScoped, showAutocomplete, showDropdownNavigation }) => { - describe(`when search is ${search}`, () => { - beforeEach(() => { - window.gon.current_username = MOCK_USERNAME; - createComponent( - { search }, - { - autocompleteGroupedSearchOptions: () => - search.match(/^[A-Za-z]+$/g) ? MOCK_SORTED_AUTOCOMPLETE_OPTIONS : [], - }, - ); - findHeaderSearchInput().vm.$emit('click'); - }); + search | showDefault | showScoped | showAutocomplete + ${null} | ${true} | ${false} | ${false} + ${''} | ${true} | ${false} | ${false} + ${'t'} | ${false} | ${false} | ${true} + ${'te'} | ${false} | ${false} | ${true} + ${'tes'} | ${false} | ${true} | ${true} + ${MOCK_SEARCH} | ${false} | ${true} | ${true} + `('Header Search Dropdown Items', ({ search, showDefault, showScoped, showAutocomplete }) => { + describe(`when search is ${search}`, () => { + beforeEach(() => { + window.gon.current_username = MOCK_USERNAME; + createComponent({ search }, {}); + findHeaderSearchInput().vm.$emit('click'); + }); - it(`should${showDefault ? '' : ' not'} render the Default Dropdown Items`, () => { - expect(findHeaderSearchDefaultItems().exists()).toBe(showDefault); - }); + it(`should${showDefault ? '' : ' not'} render the Default Dropdown Items`, () => { + expect(findHeaderSearchDefaultItems().exists()).toBe(showDefault); + }); - it(`should${showScoped ? '' : ' not'} render the Scoped Dropdown Items`, () => { - expect(findHeaderSearchScopedItems().exists()).toBe(showScoped); - }); + it(`should${showScoped ? '' : ' not'} render the Scoped Dropdown Items`, () => { + expect(findHeaderSearchScopedItems().exists()).toBe(showScoped); + }); - it(`should${ - showAutocomplete ? '' : ' not' - } render the Autocomplete Dropdown Items`, () => { - expect(findHeaderSearchAutocompleteItems().exists()).toBe(showAutocomplete); - }); + it(`should${showAutocomplete ? '' : ' not'} render the Autocomplete Dropdown Items`, () => { + expect(findHeaderSearchAutocompleteItems().exists()).toBe(showAutocomplete); + }); - it(`should${ - showDropdownNavigation ? '' : ' not' - } render the Dropdown Navigation Component`, () => { - expect(findDropdownKeyboardNavigation().exists()).toBe(showDropdownNavigation); - }); + it(`should render the Dropdown Navigation Component`, () => { + expect(findDropdownKeyboardNavigation().exists()).toBe(true); }); - }, - ); + }); + }); describe.each` username | showDropdown | expectedDesc @@ -185,12 +204,18 @@ describe('HeaderSearchApp', () => { `( 'Search Results Description', ({ username, showDropdown, search, loading, searchOptions, expectedDesc }) => { - describe(`search is ${search}, loading is ${loading}, and showSearchDropdown is ${ - Boolean(username) && showDropdown - }`, () => { + describe(`search is "${search}", loading is ${loading}, and showSearchDropdown is ${showDropdown}`, () => { beforeEach(() => { window.gon.current_username = username; - createComponent({ search, loading }, { searchOptions: () => searchOptions }); + createComponent( + { + search, + loading, + }, + { + searchOptions: () => searchOptions, + }, + ); findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : ''); }); @@ -200,6 +225,121 @@ describe('HeaderSearchApp', () => { }); }, ); + + describe('input box', () => { + describe.each` + search | searchOptions | hasToken + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[0]]} | ${true} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[1]]} | ${true} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[2]]} | ${true} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[3]]} | ${true} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[4]]} | ${true} + ${'te'} | ${[MOCK_SCOPED_SEARCH_OPTIONS[5]]} | ${false} + ${'x'} | ${[]} | ${false} + `('token', ({ search, searchOptions, hasToken }) => { + beforeEach(() => { + window.gon.current_username = MOCK_USERNAME; + createComponent( + { search }, + { + searchOptions: () => searchOptions, + }, + ); + }); + + it(`${hasToken ? 'is' : 'is NOT'} rendered when data set has type "${ + searchOptions[0]?.html_id + }"`, () => { + expect(findScopeToken().exists()).toBe(hasToken); + }); + + it(`text ${hasToken ? 'is correctly' : 'is NOT'} rendered when text is "${ + searchOptions[0]?.scope || searchOptions[0]?.description + }"`, () => { + expect(findScopeToken().exists() && findScopeToken().text()).toBe( + formatScopeName(searchOptions[0]?.scope || searchOptions[0]?.description), + ); + }); + }); + }); + + describe('form wrapper', () => { + describe.each` + searchContext | search | searchOptions + ${MOCK_SEARCH_CONTEXT_FULL} | ${null} | ${[]} + ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${[]} + ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} + ${null} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} + ${null} | ${null} | ${MOCK_SCOPED_SEARCH_OPTIONS} + ${null} | ${null} | ${[]} + `('', ({ searchContext, search, searchOptions }) => { + beforeEach(() => { + window.gon.current_username = MOCK_USERNAME; + + createComponent({ search, searchContext }, { searchOptions: () => searchOptions }); + + findHeaderSearchInput().vm.$emit('click'); + }); + + const hasIcon = Boolean(searchContext?.group); + const isSearching = Boolean(search); + const isActive = Boolean(searchOptions.length > 0); + + it(`${hasIcon ? 'with' : 'without'} search context classes contain "${ + hasIcon ? 'has-icon' : 'has-no-icon' + }"`, () => { + const iconClassRegex = hasIcon ? 'has-icon' : 'has-no-icon'; + expect(findHeaderSearchForm().classes()).toContain(iconClassRegex); + }); + + it(`${isSearching ? 'with' : 'without'} search string classes contain "${ + isSearching ? 'is-searching' : 'is-not-searching' + }"`, () => { + const iconClassRegex = isSearching ? 'is-searching' : 'is-not-searching'; + expect(findHeaderSearchForm().classes()).toContain(iconClassRegex); + }); + + it(`${isActive ? 'with' : 'without'} search results classes contain "${ + isActive ? 'is-active' : 'is-not-active' + }"`, () => { + const iconClassRegex = isActive ? 'is-active' : 'is-not-active'; + expect(findHeaderSearchForm().classes()).toContain(iconClassRegex); + }); + }); + }); + + describe.each` + search | searchOptions | hasIcon | iconName + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[0]]} | ${true} | ${ICON_PROJECT} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[2]]} | ${true} | ${ICON_GROUP} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[3]]} | ${true} | ${ICON_SUBGROUP} + ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[4]]} | ${false} | ${false} + `('token', ({ search, searchOptions, hasIcon, iconName }) => { + beforeEach(() => { + window.gon.current_username = MOCK_USERNAME; + createComponent( + { search }, + { + searchOptions: () => searchOptions, + }, + ); + }); + + it(`icon for data set type "${searchOptions[0]?.html_id}" ${ + hasIcon ? 'is' : 'is NOT' + } rendered`, () => { + expect(findScopeToken().findComponent(GlIcon).exists()).toBe(hasIcon); + }); + + it(`render ${iconName ? `"${iconName}"` : 'NO'} icon for data set type "${ + searchOptions[0]?.html_id + }"`, () => { + expect( + findScopeToken().findComponent(GlIcon).exists() && + findScopeToken().findComponent(GlIcon).attributes('name'), + ).toBe(iconName); + }); + }); }); describe('events', () => { @@ -285,18 +425,20 @@ describe('HeaderSearchApp', () => { }); describe('computed', () => { - describe('currentFocusedOption', () => { - const MOCK_INDEX = 1; - + describe.each` + MOCK_INDEX | search + ${1} | ${null} + ${SEARCH_BOX_INDEX} | ${'test'} + ${2} | ${'test1'} + `('currentFocusedOption', ({ MOCK_INDEX, search }) => { beforeEach(() => { - createComponent(); + createComponent({ search }); window.gon.current_username = MOCK_USERNAME; findHeaderSearchInput().vm.$emit('click'); }); - it(`when currentFocusIndex changes to ${MOCK_INDEX} updates the data to searchOptions[${MOCK_INDEX}]`, async () => { + it(`when currentFocusIndex changes to ${MOCK_INDEX} updates the data to searchOptions[${MOCK_INDEX}]`, () => { findDropdownKeyboardNavigation().vm.$emit('change', MOCK_INDEX); - await nextTick(); expect(wrapper.vm.currentFocusedOption).toBe(MOCK_DEFAULT_SEARCH_OPTIONS[MOCK_INDEX]); }); }); @@ -308,15 +450,25 @@ describe('HeaderSearchApp', () => { createComponent(); }); - it('onKey-enter submits a search', async () => { + it('onKey-enter submits a search', () => { findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY })); - await nextTick(); - expect(visitUrl).toHaveBeenCalledWith(MOCK_SEARCH_QUERY); }); }); + describe('with less than min characters and no dropdown results', () => { + beforeEach(() => { + createComponent({ search: 'x' }); + }); + + it('onKey-enter will NOT submit a search', () => { + findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY })); + + expect(visitUrl).not.toHaveBeenCalledWith(MOCK_SEARCH_QUERY); + }); + }); + describe('with currentFocusedOption', () => { const MOCK_INDEX = 1; @@ -326,9 +478,9 @@ describe('HeaderSearchApp', () => { findHeaderSearchInput().vm.$emit('click'); }); - it('onKey-enter clicks the selected dropdown item rather than submitting a search', async () => { + it('onKey-enter clicks the selected dropdown item rather than submitting a search', () => { findDropdownKeyboardNavigation().vm.$emit('change', MOCK_INDEX); - await nextTick(); + findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY })); expect(visitUrl).toHaveBeenCalledWith(MOCK_DEFAULT_SEARCH_OPTIONS[MOCK_INDEX].url); }); diff --git a/spec/frontend/header_search/components/header_search_scoped_items_spec.js b/spec/frontend/header_search/components/header_search_scoped_items_spec.js index 8788fb23458..2db9f71d702 100644 --- a/spec/frontend/header_search/components/header_search_scoped_items_spec.js +++ b/spec/frontend/header_search/components/header_search_scoped_items_spec.js @@ -1,9 +1,11 @@ -import { GlDropdownItem, GlDropdownDivider } from '@gitlab/ui'; +import { GlDropdownItem, GlToken, GlIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import Vuex from 'vuex'; import { trimText } from 'helpers/text_helper'; import HeaderSearchScopedItems from '~/header_search/components/header_search_scoped_items.vue'; +import { truncate } from '~/lib/utils/text_utility'; +import { MSG_IN_ALL_GITLAB, SCOPE_TOKEN_MAX_LENGTH } from '~/header_search/constants'; import { MOCK_SEARCH, MOCK_SCOPED_SEARCH_OPTIONS, @@ -41,9 +43,12 @@ describe('HeaderSearchScopedItems', () => { }); const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); - const findGlDropdownDivider = () => wrapper.findComponent(GlDropdownDivider); const findFirstDropdownItem = () => findDropdownItems().at(0); const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => trimText(w.text())); + const findScopeTokens = () => wrapper.findAllComponents(GlToken); + const findScopeTokensText = () => findScopeTokens().wrappers.map((w) => trimText(w.text())); + const findScopeTokensIcons = () => + findScopeTokens().wrappers.map((w) => w.findAllComponents(GlIcon)); const findDropdownItemAriaLabels = () => findDropdownItems().wrappers.map((w) => trimText(w.attributes('aria-label'))); const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href')); @@ -59,15 +64,31 @@ describe('HeaderSearchScopedItems', () => { }); it('renders titles correctly', () => { + findDropdownItemTitles().forEach((title) => expect(title).toContain(MOCK_SEARCH)); + }); + + it('renders scope names correctly', () => { const expectedTitles = MOCK_SCOPED_SEARCH_OPTIONS.map((o) => - trimText(`"${MOCK_SEARCH}" ${o.description} ${o.scope || ''}`), + truncate(trimText(`in ${o.description || o.scope}`), SCOPE_TOKEN_MAX_LENGTH), ); - expect(findDropdownItemTitles()).toStrictEqual(expectedTitles); + + expect(findScopeTokensText()).toStrictEqual(expectedTitles); + }); + + it('renders scope icons correctly', () => { + findScopeTokensIcons().forEach((icon, i) => { + const w = icon.wrappers[0]; + expect(w?.attributes('name')).toBe(MOCK_SCOPED_SEARCH_OPTIONS[i].icon); + }); + }); + + it(`renders scope ${MSG_IN_ALL_GITLAB} correctly`, () => { + expect(findScopeTokens().at(-1).findComponent(GlIcon).exists()).toBe(false); }); it('renders aria-labels correctly', () => { const expectedLabels = MOCK_SCOPED_SEARCH_OPTIONS.map((o) => - trimText(`${MOCK_SEARCH} ${o.description} ${o.scope || ''}`), + trimText(`${MOCK_SEARCH} ${o.description || o.icon} ${o.scope || ''}`), ); expect(findDropdownItemAriaLabels()).toStrictEqual(expectedLabels); }); @@ -98,21 +119,5 @@ describe('HeaderSearchScopedItems', () => { }); }); }); - - describe.each` - autosuggestResults | showDivider - ${[]} | ${false} - ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${true} - `('scoped search items', ({ autosuggestResults, showDivider }) => { - describe(`when when we have ${autosuggestResults.length} auto-sugest results`, () => { - beforeEach(() => { - createComponent({}, { autocompleteGroupedSearchOptions: () => autosuggestResults }, {}); - }); - - it(`divider should${showDivider ? '' : ' not'} be shown`, () => { - expect(findGlDropdownDivider().exists()).toBe(showDivider); - }); - }); - }); }); }); |