summaryrefslogtreecommitdiff
path: root/spec/frontend/header_search/components
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/header_search/components')
-rw-r--r--spec/frontend/header_search/components/app_spec.js272
-rw-r--r--spec/frontend/header_search/components/header_search_scoped_items_spec.js47
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);
- });
- });
- });
});
});