diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-20 16:37:47 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-20 16:37:47 +0300 |
commit | aee0a117a889461ce8ced6fcf73207fe017f1d99 (patch) | |
tree | 891d9ef189227a8445d83f35c1b0fc99573f4380 /spec/frontend/header_search | |
parent | 8d46af3258650d305f53b819eabf7ab18d22f59e (diff) |
Add latest changes from gitlab-org/gitlab@14-6-stable-eev14.6.0-rc42
Diffstat (limited to 'spec/frontend/header_search')
8 files changed, 506 insertions, 95 deletions
diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js index 2ea2693a978..3200c6614f1 100644 --- a/spec/frontend/header_search/components/app_spec.js +++ b/spec/frontend/header_search/components/app_spec.js @@ -6,9 +6,17 @@ 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 { ENTER_KEY, ESC_KEY } from '~/lib/utils/keys'; +import { SEARCH_INPUT_DESCRIPTION, SEARCH_RESULTS_DESCRIPTION } 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 { MOCK_SEARCH, MOCK_SEARCH_QUERY, MOCK_USERNAME } from '../mock_data'; +import { + MOCK_SEARCH, + MOCK_SEARCH_QUERY, + MOCK_USERNAME, + MOCK_DEFAULT_SEARCH_OPTIONS, + MOCK_SCOPED_SEARCH_OPTIONS, +} from '../mock_data'; Vue.use(Vuex); @@ -22,9 +30,10 @@ describe('HeaderSearchApp', () => { const actionSpies = { setSearch: jest.fn(), fetchAutocompleteOptions: jest.fn(), + clearAutocomplete: jest.fn(), }; - const createComponent = (initialState) => { + const createComponent = (initialState, mockGetters) => { const store = new Vuex.Store({ state: { ...initialState, @@ -32,6 +41,8 @@ describe('HeaderSearchApp', () => { actions: actionSpies, getters: { searchQuery: () => MOCK_SEARCH_QUERY, + searchOptions: () => MOCK_DEFAULT_SEARCH_OPTIONS, + ...mockGetters, }, }); @@ -50,11 +61,27 @@ describe('HeaderSearchApp', () => { const findHeaderSearchScopedItems = () => wrapper.findComponent(HeaderSearchScopedItems); const findHeaderSearchAutocompleteItems = () => wrapper.findComponent(HeaderSearchAutocompleteItems); + const findDropdownKeyboardNavigation = () => wrapper.findComponent(DropdownKeyboardNavigation); + const findSearchInputDescription = () => wrapper.find(`#${SEARCH_INPUT_DESCRIPTION}`); + const findSearchResultsDescription = () => wrapper.findByTestId(SEARCH_RESULTS_DESCRIPTION); describe('template', () => { - it('always renders Header Search Input', () => { - createComponent(); - expect(findHeaderSearchInput().exists()).toBe(true); + describe('always renders', () => { + beforeEach(() => { + createComponent(); + }); + + it('Header Search Input', () => { + expect(findHeaderSearchInput().exists()).toBe(true); + }); + + it('Search Input Description', () => { + expect(findSearchInputDescription().exists()).toBe(true); + }); + + it('Search Results Description', () => { + expect(findSearchResultsDescription().exists()).toBe(true); + }); }); describe.each` @@ -66,9 +93,9 @@ describe('HeaderSearchApp', () => { `('Header Search Dropdown', ({ showDropdown, username, showSearchDropdown }) => { describe(`when showDropdown is ${showDropdown} and current_username is ${username}`, () => { beforeEach(() => { - createComponent(); window.gon.current_username = username; - wrapper.setData({ showDropdown }); + createComponent(); + findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : ''); }); it(`should${showSearchDropdown ? '' : ' not'} render`, () => { @@ -78,31 +105,89 @@ describe('HeaderSearchApp', () => { }); describe.each` - search | showDefault | showScoped | showAutocomplete - ${null} | ${true} | ${false} | ${false} - ${''} | ${true} | ${false} | ${false} - ${MOCK_SEARCH} | ${false} | ${true} | ${true} - `('Header Search Dropdown Items', ({ search, showDefault, showScoped, showAutocomplete }) => { - describe(`when search is ${search}`, () => { - beforeEach(() => { - createComponent({ search }); - window.gon.current_username = MOCK_USERNAME; - wrapper.setData({ showDropdown: true }); - }); - - it(`should${showDefault ? '' : ' not'} render the Default Dropdown Items`, () => { - expect(findHeaderSearchDefaultItems().exists()).toBe(showDefault); + search | showDefault | showScoped | showAutocomplete | showDropdownNavigation + ${null} | ${true} | ${false} | ${false} | ${true} + ${''} | ${true} | ${false} | ${false} | ${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 }); + findHeaderSearchInput().vm.$emit('click'); + }); + + 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${ + 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${showScoped ? '' : ' not'} render the Scoped Dropdown Items`, () => { - expect(findHeaderSearchScopedItems().exists()).toBe(showScoped); + describe.each` + username | showDropdown | expectedDesc + ${null} | ${false} | ${HeaderSearchApp.i18n.searchInputDescribeByNoDropdown} + ${null} | ${true} | ${HeaderSearchApp.i18n.searchInputDescribeByNoDropdown} + ${MOCK_USERNAME} | ${false} | ${HeaderSearchApp.i18n.searchInputDescribeByWithDropdown} + ${MOCK_USERNAME} | ${true} | ${HeaderSearchApp.i18n.searchInputDescribeByWithDropdown} + `('Search Input Description', ({ username, showDropdown, expectedDesc }) => { + describe(`current_username is ${username} and showDropdown is ${showDropdown}`, () => { + beforeEach(() => { + window.gon.current_username = username; + createComponent(); + findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : ''); }); - it(`should${showAutocomplete ? '' : ' not'} render the Autocomplete Dropdown Items`, () => { - expect(findHeaderSearchAutocompleteItems().exists()).toBe(showAutocomplete); + it(`sets description to ${expectedDesc}`, () => { + expect(findSearchInputDescription().text()).toBe(expectedDesc); }); }); }); + + describe.each` + username | showDropdown | search | loading | searchOptions | expectedDesc + ${null} | ${true} | ${''} | ${false} | ${[]} | ${''} + ${MOCK_USERNAME} | ${false} | ${''} | ${false} | ${[]} | ${''} + ${MOCK_USERNAME} | ${true} | ${''} | ${false} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${`${MOCK_DEFAULT_SEARCH_OPTIONS.length} default results provided. Use the up and down arrow keys to navigate search results list.`} + ${MOCK_USERNAME} | ${true} | ${''} | ${true} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${`${MOCK_DEFAULT_SEARCH_OPTIONS.length} default results provided. Use the up and down arrow keys to navigate search results list.`} + ${MOCK_USERNAME} | ${true} | ${MOCK_SEARCH} | ${false} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${`Results updated. ${MOCK_SCOPED_SEARCH_OPTIONS.length} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit.`} + ${MOCK_USERNAME} | ${true} | ${MOCK_SEARCH} | ${true} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${HeaderSearchApp.i18n.searchResultsLoading} + `( + 'Search Results Description', + ({ username, showDropdown, search, loading, searchOptions, expectedDesc }) => { + describe(`search is ${search}, loading is ${loading}, and showSearchDropdown is ${ + Boolean(username) && showDropdown + }`, () => { + beforeEach(() => { + window.gon.current_username = username; + createComponent({ search, loading }, { searchOptions: () => searchOptions }); + findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : ''); + }); + + it(`sets description to ${expectedDesc}`, () => { + expect(findSearchResultsDescription().text()).toBe(expectedDesc); + }); + }); + }, + ); }); describe('events', () => { @@ -132,36 +217,86 @@ describe('HeaderSearchApp', () => { }); }); - describe('when dropdown is opened', () => { - beforeEach(() => { - wrapper.setData({ showDropdown: true }); + describe('onInput', () => { + describe('when search has text', () => { + beforeEach(() => { + findHeaderSearchInput().vm.$emit('input', MOCK_SEARCH); + }); + + it('calls setSearch with search term', () => { + expect(actionSpies.setSearch).toHaveBeenCalledWith(expect.any(Object), MOCK_SEARCH); + }); + + it('calls fetchAutocompleteOptions', () => { + expect(actionSpies.fetchAutocompleteOptions).toHaveBeenCalled(); + }); + + it('does not call clearAutocomplete', () => { + expect(actionSpies.clearAutocomplete).not.toHaveBeenCalled(); + }); }); - it('onKey-Escape closes dropdown', async () => { - expect(findHeaderSearchDropdown().exists()).toBe(true); - findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ESC_KEY })); + describe('when search is emptied', () => { + beforeEach(() => { + findHeaderSearchInput().vm.$emit('input', ''); + }); - await wrapper.vm.$nextTick(); + it('calls setSearch with empty term', () => { + expect(actionSpies.setSearch).toHaveBeenCalledWith(expect.any(Object), ''); + }); - expect(findHeaderSearchDropdown().exists()).toBe(false); + it('does not call fetchAutocompleteOptions', () => { + expect(actionSpies.fetchAutocompleteOptions).not.toHaveBeenCalled(); + }); + + it('calls clearAutocomplete', () => { + expect(actionSpies.clearAutocomplete).toHaveBeenCalled(); + }); }); }); + }); - describe('onInput', () => { - beforeEach(() => { - findHeaderSearchInput().vm.$emit('input', MOCK_SEARCH); - }); + describe('Dropdown Keyboard Navigation', () => { + beforeEach(() => { + findHeaderSearchInput().vm.$emit('click'); + }); - it('calls setSearch with search term', () => { - expect(actionSpies.setSearch).toHaveBeenCalledWith(expect.any(Object), MOCK_SEARCH); - }); + it('closes dropdown when @tab is emitted', async () => { + expect(findHeaderSearchDropdown().exists()).toBe(true); + findDropdownKeyboardNavigation().vm.$emit('tab'); - it('calls fetchAutocompleteOptions', () => { - expect(actionSpies.fetchAutocompleteOptions).toHaveBeenCalled(); - }); + await wrapper.vm.$nextTick(); + + expect(findHeaderSearchDropdown().exists()).toBe(false); + }); + }); + }); + + describe('computed', () => { + describe('currentFocusedOption', () => { + const MOCK_INDEX = 1; + + beforeEach(() => { + createComponent(); + 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 () => { + findDropdownKeyboardNavigation().vm.$emit('change', MOCK_INDEX); + await wrapper.vm.$nextTick(); + expect(wrapper.vm.currentFocusedOption).toBe(MOCK_DEFAULT_SEARCH_OPTIONS[MOCK_INDEX]); }); + }); + }); - it('submits a search onKey-Enter', async () => { + describe('Submitting a search', () => { + describe('with no currentFocusedOption', () => { + beforeEach(() => { + createComponent(); + }); + + it('onKey-enter submits a search', async () => { findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY })); await wrapper.vm.$nextTick(); @@ -169,5 +304,22 @@ describe('HeaderSearchApp', () => { expect(visitUrl).toHaveBeenCalledWith(MOCK_SEARCH_QUERY); }); }); + + describe('with currentFocusedOption', () => { + const MOCK_INDEX = 1; + + beforeEach(() => { + createComponent(); + window.gon.current_username = MOCK_USERNAME; + findHeaderSearchInput().vm.$emit('click'); + }); + + it('onKey-enter clicks the selected dropdown item rather than submitting a search', async () => { + findDropdownKeyboardNavigation().vm.$emit('change', MOCK_INDEX); + await wrapper.vm.$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_autocomplete_items_spec.js b/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js index 6b84e63989d..bec0cbc8a5c 100644 --- a/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js +++ b/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js @@ -9,14 +9,14 @@ import { PROJECTS_CATEGORY, SMALL_AVATAR_PX, } from '~/header_search/constants'; -import { MOCK_GROUPED_AUTOCOMPLETE_OPTIONS, MOCK_AUTOCOMPLETE_OPTIONS } from '../mock_data'; +import { MOCK_GROUPED_AUTOCOMPLETE_OPTIONS, MOCK_SORTED_AUTOCOMPLETE_OPTIONS } from '../mock_data'; Vue.use(Vuex); describe('HeaderSearchAutocompleteItems', () => { let wrapper; - const createComponent = (initialState, mockGetters) => { + const createComponent = (initialState, mockGetters, props) => { const store = new Vuex.Store({ state: { loading: false, @@ -30,6 +30,9 @@ describe('HeaderSearchAutocompleteItems', () => { wrapper = shallowMount(HeaderSearchAutocompleteItems, { store, + propsData: { + ...props, + }, }); }; @@ -38,6 +41,7 @@ describe('HeaderSearchAutocompleteItems', () => { }); const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findFirstDropdownItem = () => findDropdownItems().at(0); const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => w.text()); const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href')); const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); @@ -69,16 +73,16 @@ describe('HeaderSearchAutocompleteItems', () => { describe('Dropdown items', () => { it('renders item for each option in autocomplete option', () => { - expect(findDropdownItems()).toHaveLength(MOCK_AUTOCOMPLETE_OPTIONS.length); + expect(findDropdownItems()).toHaveLength(MOCK_SORTED_AUTOCOMPLETE_OPTIONS.length); }); it('renders titles correctly', () => { - const expectedTitles = MOCK_AUTOCOMPLETE_OPTIONS.map((o) => o.label); + const expectedTitles = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.label); expect(findDropdownItemTitles()).toStrictEqual(expectedTitles); }); it('renders links correctly', () => { - const expectedLinks = MOCK_AUTOCOMPLETE_OPTIONS.map((o) => o.url); + const expectedLinks = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.url); expect(findDropdownItemLinks()).toStrictEqual(expectedLinks); }); }); @@ -104,5 +108,46 @@ describe('HeaderSearchAutocompleteItems', () => { }); }); }); + + describe.each` + currentFocusedOption | isFocused | ariaSelected + ${null} | ${false} | ${undefined} + ${{ html_id: 'not-a-match' }} | ${false} | ${undefined} + ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS[0]} | ${true} | ${'true'} + `('isOptionFocused', ({ currentFocusedOption, isFocused, ariaSelected }) => { + describe(`when currentFocusedOption.html_id is ${currentFocusedOption?.html_id}`, () => { + beforeEach(() => { + createComponent({}, {}, { currentFocusedOption }); + }); + + it(`should${isFocused ? '' : ' not'} have gl-bg-gray-50 applied`, () => { + expect(findFirstDropdownItem().classes('gl-bg-gray-50')).toBe(isFocused); + }); + + it(`sets "aria-selected to ${ariaSelected}`, () => { + expect(findFirstDropdownItem().attributes('aria-selected')).toBe(ariaSelected); + }); + }); + }); + }); + + describe('watchers', () => { + describe('currentFocusedOption', () => { + beforeEach(() => { + createComponent(); + }); + + it('when focused changes to existing element calls scroll into view on the newly focused element', async () => { + const focusedElement = findFirstDropdownItem().element; + const scrollSpy = jest.spyOn(focusedElement, 'scrollIntoView'); + + wrapper.setProps({ currentFocusedOption: MOCK_SORTED_AUTOCOMPLETE_OPTIONS[0] }); + + await wrapper.vm.$nextTick(); + + expect(scrollSpy).toHaveBeenCalledWith(false); + scrollSpy.mockRestore(); + }); + }); }); }); diff --git a/spec/frontend/header_search/components/header_search_default_items_spec.js b/spec/frontend/header_search/components/header_search_default_items_spec.js index ce083d0df72..abcacc487df 100644 --- a/spec/frontend/header_search/components/header_search_default_items_spec.js +++ b/spec/frontend/header_search/components/header_search_default_items_spec.js @@ -10,7 +10,7 @@ Vue.use(Vuex); describe('HeaderSearchDefaultItems', () => { let wrapper; - const createComponent = (initialState) => { + const createComponent = (initialState, props) => { const store = new Vuex.Store({ state: { searchContext: MOCK_SEARCH_CONTEXT, @@ -23,6 +23,9 @@ describe('HeaderSearchDefaultItems', () => { wrapper = shallowMount(HeaderSearchDefaultItems, { store, + propsData: { + ...props, + }, }); }; @@ -32,6 +35,7 @@ describe('HeaderSearchDefaultItems', () => { const findDropdownHeader = () => wrapper.findComponent(GlDropdownSectionHeader); const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findFirstDropdownItem = () => findDropdownItems().at(0); const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => w.text()); const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href')); @@ -77,5 +81,26 @@ describe('HeaderSearchDefaultItems', () => { }); }); }); + + describe.each` + currentFocusedOption | isFocused | ariaSelected + ${null} | ${false} | ${undefined} + ${{ html_id: 'not-a-match' }} | ${false} | ${undefined} + ${MOCK_DEFAULT_SEARCH_OPTIONS[0]} | ${true} | ${'true'} + `('isOptionFocused', ({ currentFocusedOption, isFocused, ariaSelected }) => { + describe(`when currentFocusedOption.html_id is ${currentFocusedOption?.html_id}`, () => { + beforeEach(() => { + createComponent({}, { currentFocusedOption }); + }); + + it(`should${isFocused ? '' : ' not'} have gl-bg-gray-50 applied`, () => { + expect(findFirstDropdownItem().classes('gl-bg-gray-50')).toBe(isFocused); + }); + + it(`sets "aria-selected to ${ariaSelected}`, () => { + expect(findFirstDropdownItem().attributes('aria-selected')).toBe(ariaSelected); + }); + }); + }); }); }); 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 f0e5e182ec4..a65b4d8b813 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 @@ -11,7 +11,7 @@ Vue.use(Vuex); describe('HeaderSearchScopedItems', () => { let wrapper; - const createComponent = (initialState) => { + const createComponent = (initialState, props) => { const store = new Vuex.Store({ state: { search: MOCK_SEARCH, @@ -24,6 +24,9 @@ describe('HeaderSearchScopedItems', () => { wrapper = shallowMount(HeaderSearchScopedItems, { store, + propsData: { + ...props, + }, }); }; @@ -32,7 +35,10 @@ describe('HeaderSearchScopedItems', () => { }); const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findFirstDropdownItem = () => findDropdownItems().at(0); const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => trimText(w.text())); + const findDropdownItemAriaLabels = () => + findDropdownItems().wrappers.map((w) => trimText(w.attributes('aria-label'))); const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href')); describe('template', () => { @@ -52,10 +58,38 @@ describe('HeaderSearchScopedItems', () => { expect(findDropdownItemTitles()).toStrictEqual(expectedTitles); }); + it('renders aria-labels correctly', () => { + const expectedLabels = MOCK_SCOPED_SEARCH_OPTIONS.map((o) => + trimText(`${MOCK_SEARCH} ${o.description} ${o.scope || ''}`), + ); + expect(findDropdownItemAriaLabels()).toStrictEqual(expectedLabels); + }); + it('renders links correctly', () => { const expectedLinks = MOCK_SCOPED_SEARCH_OPTIONS.map((o) => o.url); expect(findDropdownItemLinks()).toStrictEqual(expectedLinks); }); }); + + describe.each` + currentFocusedOption | isFocused | ariaSelected + ${null} | ${false} | ${undefined} + ${{ html_id: 'not-a-match' }} | ${false} | ${undefined} + ${MOCK_SCOPED_SEARCH_OPTIONS[0]} | ${true} | ${'true'} + `('isOptionFocused', ({ currentFocusedOption, isFocused, ariaSelected }) => { + describe(`when currentFocusedOption.html_id is ${currentFocusedOption?.html_id}`, () => { + beforeEach(() => { + createComponent({}, { currentFocusedOption }); + }); + + it(`should${isFocused ? '' : ' not'} have gl-bg-gray-50 applied`, () => { + expect(findFirstDropdownItem().classes('gl-bg-gray-50')).toBe(isFocused); + }); + + it(`sets "aria-selected to ${ariaSelected}`, () => { + expect(findFirstDropdownItem().attributes('aria-selected')).toBe(ariaSelected); + }); + }); + }); }); }); diff --git a/spec/frontend/header_search/mock_data.js b/spec/frontend/header_search/mock_data.js index 915b3a4a678..1d980679547 100644 --- a/spec/frontend/header_search/mock_data.js +++ b/spec/frontend/header_search/mock_data.js @@ -46,22 +46,27 @@ export const MOCK_SEARCH_CONTEXT = { export const MOCK_DEFAULT_SEARCH_OPTIONS = [ { + html_id: 'default-issues-assigned', title: MSG_ISSUES_ASSIGNED_TO_ME, url: `${MOCK_ISSUE_PATH}/?assignee_username=${MOCK_USERNAME}`, }, { + html_id: 'default-issues-created', title: MSG_ISSUES_IVE_CREATED, url: `${MOCK_ISSUE_PATH}/?author_username=${MOCK_USERNAME}`, }, { + html_id: 'default-mrs-assigned', title: MSG_MR_ASSIGNED_TO_ME, url: `${MOCK_MR_PATH}/?assignee_username=${MOCK_USERNAME}`, }, { + html_id: 'default-mrs-reviewer', title: MSG_MR_IM_REVIEWER, url: `${MOCK_MR_PATH}/?reviewer_username=${MOCK_USERNAME}`, }, { + html_id: 'default-mrs-created', title: MSG_MR_IVE_CREATED, url: `${MOCK_MR_PATH}/?author_username=${MOCK_USERNAME}`, }, @@ -69,22 +74,25 @@ export const MOCK_DEFAULT_SEARCH_OPTIONS = [ export const MOCK_SCOPED_SEARCH_OPTIONS = [ { + html_id: 'scoped-in-project', scope: MOCK_PROJECT.name, description: MSG_IN_PROJECT, url: MOCK_PROJECT.path, }, { + html_id: 'scoped-in-group', scope: MOCK_GROUP.name, description: MSG_IN_GROUP, url: MOCK_GROUP.path, }, { + html_id: 'scoped-in-all', description: MSG_IN_ALL_GITLAB, url: MOCK_ALL_PATH, }, ]; -export const MOCK_AUTOCOMPLETE_OPTIONS = [ +export const MOCK_AUTOCOMPLETE_OPTIONS_RES = [ { category: 'Projects', id: 1, @@ -92,19 +100,49 @@ export const MOCK_AUTOCOMPLETE_OPTIONS = [ url: 'project/1', }, { + category: 'Groups', + id: 1, + label: 'MockGroup1', + url: 'group/1', + }, + { category: 'Projects', id: 2, label: 'MockProject2', url: 'project/2', }, { + category: 'Help', + label: 'GitLab Help', + url: 'help/gitlab', + }, +]; + +export const MOCK_AUTOCOMPLETE_OPTIONS = [ + { + category: 'Projects', + html_id: 'autocomplete-Projects-0', + id: 1, + label: 'MockProject1', + url: 'project/1', + }, + { category: 'Groups', + html_id: 'autocomplete-Groups-1', id: 1, label: 'MockGroup1', url: 'group/1', }, { + category: 'Projects', + html_id: 'autocomplete-Projects-2', + id: 2, + label: 'MockProject2', + url: 'project/2', + }, + { category: 'Help', + html_id: 'autocomplete-Help-3', label: 'GitLab Help', url: 'help/gitlab', }, @@ -116,12 +154,16 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [ data: [ { category: 'Projects', + html_id: 'autocomplete-Projects-0', + id: 1, label: 'MockProject1', url: 'project/1', }, { category: 'Projects', + html_id: 'autocomplete-Projects-2', + id: 2, label: 'MockProject2', url: 'project/2', @@ -133,6 +175,8 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [ data: [ { category: 'Groups', + html_id: 'autocomplete-Groups-1', + id: 1, label: 'MockGroup1', url: 'group/1', @@ -144,9 +188,41 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [ data: [ { category: 'Help', + html_id: 'autocomplete-Help-3', + label: 'GitLab Help', url: 'help/gitlab', }, ], }, ]; + +export const MOCK_SORTED_AUTOCOMPLETE_OPTIONS = [ + { + category: 'Projects', + html_id: 'autocomplete-Projects-0', + id: 1, + label: 'MockProject1', + url: 'project/1', + }, + { + category: 'Projects', + html_id: 'autocomplete-Projects-2', + id: 2, + label: 'MockProject2', + url: 'project/2', + }, + { + category: 'Groups', + html_id: 'autocomplete-Groups-1', + id: 1, + label: 'MockGroup1', + url: 'group/1', + }, + { + category: 'Help', + html_id: 'autocomplete-Help-3', + label: 'GitLab Help', + url: 'help/gitlab', + }, +]; diff --git a/spec/frontend/header_search/store/actions_spec.js b/spec/frontend/header_search/store/actions_spec.js index ee2c72df77b..6599115f017 100644 --- a/spec/frontend/header_search/store/actions_spec.js +++ b/spec/frontend/header_search/store/actions_spec.js @@ -5,7 +5,7 @@ import * as actions from '~/header_search/store/actions'; import * as types from '~/header_search/store/mutation_types'; import createState from '~/header_search/store/state'; import axios from '~/lib/utils/axios_utils'; -import { MOCK_SEARCH, MOCK_AUTOCOMPLETE_OPTIONS } from '../mock_data'; +import { MOCK_SEARCH, MOCK_AUTOCOMPLETE_OPTIONS_RES } from '../mock_data'; jest.mock('~/flash'); @@ -29,9 +29,9 @@ describe('Header Search Store Actions', () => { }); describe.each` - axiosMock | type | expectedMutations | flashCallCount - ${{ method: 'onGet', code: 200, res: MOCK_AUTOCOMPLETE_OPTIONS }} | ${'success'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_SUCCESS, payload: MOCK_AUTOCOMPLETE_OPTIONS }]} | ${0} - ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_ERROR }]} | ${1} + axiosMock | type | expectedMutations | flashCallCount + ${{ method: 'onGet', code: 200, res: MOCK_AUTOCOMPLETE_OPTIONS_RES }} | ${'success'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_SUCCESS, payload: MOCK_AUTOCOMPLETE_OPTIONS_RES }]} | ${0} + ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_ERROR }]} | ${1} `('fetchAutocompleteOptions', ({ axiosMock, type, expectedMutations, flashCallCount }) => { describe(`on ${type}`, () => { beforeEach(() => { @@ -47,6 +47,16 @@ describe('Header Search Store Actions', () => { }); }); + describe('clearAutocomplete', () => { + it('calls the CLEAR_AUTOCOMPLETE mutation', () => { + return testAction({ + action: actions.clearAutocomplete, + state, + expectedMutations: [{ type: types.CLEAR_AUTOCOMPLETE }], + }); + }); + }); + describe('setSearch', () => { it('calls the SET_SEARCH mutation', () => { return testAction({ diff --git a/spec/frontend/header_search/store/getters_spec.js b/spec/frontend/header_search/store/getters_spec.js index d55db07188e..35d1bf350d7 100644 --- a/spec/frontend/header_search/store/getters_spec.js +++ b/spec/frontend/header_search/store/getters_spec.js @@ -15,6 +15,7 @@ import { MOCK_SEARCH, MOCK_AUTOCOMPLETE_OPTIONS, MOCK_GROUPED_AUTOCOMPLETE_OPTIONS, + MOCK_SORTED_AUTOCOMPLETE_OPTIONS, } from '../mock_data'; describe('Header Search Store Getters', () => { @@ -36,18 +37,20 @@ describe('Header Search Store Getters', () => { }); describe.each` - group | project | expectedPath - ${null} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=undefined&group_id=undefined&scope=issues`} - ${MOCK_GROUP} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=undefined&group_id=${MOCK_GROUP.id}&scope=issues`} - ${MOCK_GROUP} | ${MOCK_PROJECT} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues`} - `('searchQuery', ({ group, project, expectedPath }) => { - describe(`when group is ${group?.name} and project is ${project?.name}`, () => { + group | project | scope | expectedPath + ${null} | ${null} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`} + ${MOCK_GROUP} | ${null} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}`} + ${null} | ${MOCK_PROJECT} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues`} + `('searchQuery', ({ group, project, scope, expectedPath }) => { + describe(`when group is ${group?.name}, project is ${project?.name}, and scope is ${scope}`, () => { beforeEach(() => { createState({ searchContext: { group, project, - scope: 'issues', + scope, }, }); state.search = MOCK_SEARCH; @@ -61,8 +64,9 @@ describe('Header Search Store Getters', () => { describe.each` project | ref | expectedPath - ${null} | ${null} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}&project_id=undefined&project_ref=null`} - ${MOCK_PROJECT} | ${null} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}&project_id=${MOCK_PROJECT.id}&project_ref=null`} + ${null} | ${null} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}`} + ${MOCK_PROJECT} | ${null} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}&project_id=${MOCK_PROJECT.id}`} + ${null} | ${MOCK_PROJECT.id} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}&project_ref=${MOCK_PROJECT.id}`} ${MOCK_PROJECT} | ${MOCK_PROJECT.id} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}&project_id=${MOCK_PROJECT.id}&project_ref=${MOCK_PROJECT.id}`} `('autocompleteQuery', ({ project, ref, expectedPath }) => { describe(`when project is ${project?.name} and project ref is ${ref}`, () => { @@ -131,18 +135,20 @@ describe('Header Search Store Getters', () => { }); describe.each` - group | project | expectedPath - ${null} | ${null} | ${null} - ${MOCK_GROUP} | ${null} | ${null} - ${MOCK_GROUP} | ${MOCK_PROJECT} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues`} - `('projectUrl', ({ group, project, expectedPath }) => { - describe(`when group is ${group?.name} and project is ${project?.name}`, () => { + group | project | scope | expectedPath + ${null} | ${null} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`} + ${MOCK_GROUP} | ${null} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}`} + ${null} | ${MOCK_PROJECT} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues`} + `('projectUrl', ({ group, project, scope, expectedPath }) => { + describe(`when group is ${group?.name}, project is ${project?.name}, and scope is ${scope}`, () => { beforeEach(() => { createState({ searchContext: { group, project, - scope: 'issues', + scope, }, }); state.search = MOCK_SEARCH; @@ -155,18 +161,20 @@ describe('Header Search Store Getters', () => { }); describe.each` - group | project | expectedPath - ${null} | ${null} | ${null} - ${MOCK_GROUP} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}&scope=issues`} - ${MOCK_GROUP} | ${MOCK_PROJECT} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}&scope=issues`} - `('groupUrl', ({ group, project, expectedPath }) => { - describe(`when group is ${group?.name} and project is ${project?.name}`, () => { + group | project | scope | expectedPath + ${null} | ${null} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`} + ${MOCK_GROUP} | ${null} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}`} + ${null} | ${MOCK_PROJECT} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}&scope=issues`} + `('groupUrl', ({ group, project, scope, expectedPath }) => { + describe(`when group is ${group?.name}, project is ${project?.name}, and scope is ${scope}`, () => { beforeEach(() => { createState({ searchContext: { group, project, - scope: 'issues', + scope, }, }); state.search = MOCK_SEARCH; @@ -178,20 +186,29 @@ describe('Header Search Store Getters', () => { }); }); - describe('allUrl', () => { - const expectedPath = `${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&scope=issues`; - - beforeEach(() => { - createState({ - searchContext: { - scope: 'issues', - }, + describe.each` + group | project | scope | expectedPath + ${null} | ${null} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`} + ${MOCK_GROUP} | ${null} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`} + ${null} | ${MOCK_PROJECT} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`} + ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&scope=issues`} + `('allUrl', ({ group, project, scope, expectedPath }) => { + describe(`when group is ${group?.name}, project is ${project?.name}, and scope is ${scope}`, () => { + beforeEach(() => { + createState({ + searchContext: { + group, + project, + scope, + }, + }); + state.search = MOCK_SEARCH; }); - state.search = MOCK_SEARCH; - }); - it(`should return ${expectedPath}`, () => { - expect(getters.allUrl(state)).toBe(expectedPath); + it(`should return ${expectedPath}`, () => { + expect(getters.allUrl(state)).toBe(expectedPath); + }); }); }); @@ -248,4 +265,44 @@ describe('Header Search Store Getters', () => { ); }); }); + + describe.each` + search | defaultSearchOptions | scopedSearchOptions | autocompleteGroupedSearchOptions | expectedArray + ${null} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_DEFAULT_SEARCH_OPTIONS} + ${MOCK_SEARCH} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${[]} | ${MOCK_SCOPED_SEARCH_OPTIONS} + ${MOCK_SEARCH} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${[]} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS} + ${MOCK_SEARCH} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS.concat(MOCK_SORTED_AUTOCOMPLETE_OPTIONS)} + `( + 'searchOptions', + ({ + search, + defaultSearchOptions, + scopedSearchOptions, + autocompleteGroupedSearchOptions, + expectedArray, + }) => { + describe(`when search is ${search} and the defaultSearchOptions${ + defaultSearchOptions.length ? '' : ' do not' + } exist, scopedSearchOptions${ + scopedSearchOptions.length ? '' : ' do not' + } exist, and autocompleteGroupedSearchOptions${ + autocompleteGroupedSearchOptions.length ? '' : ' do not' + } exist`, () => { + const mockGetters = { + defaultSearchOptions, + scopedSearchOptions, + autocompleteGroupedSearchOptions, + }; + + beforeEach(() => { + createState(); + state.search = search; + }); + + it(`should return the correct combined array`, () => { + expect(getters.searchOptions(state, mockGetters)).toStrictEqual(expectedArray); + }); + }); + }, + ); }); diff --git a/spec/frontend/header_search/store/mutations_spec.js b/spec/frontend/header_search/store/mutations_spec.js index 7f9b7631a7e..7bcf8e49118 100644 --- a/spec/frontend/header_search/store/mutations_spec.js +++ b/spec/frontend/header_search/store/mutations_spec.js @@ -1,7 +1,11 @@ import * as types from '~/header_search/store/mutation_types'; import mutations from '~/header_search/store/mutations'; import createState from '~/header_search/store/state'; -import { MOCK_SEARCH, MOCK_AUTOCOMPLETE_OPTIONS } from '../mock_data'; +import { + MOCK_SEARCH, + MOCK_AUTOCOMPLETE_OPTIONS_RES, + MOCK_AUTOCOMPLETE_OPTIONS, +} from '../mock_data'; describe('Header Search Store Mutations', () => { let state; @@ -20,8 +24,8 @@ describe('Header Search Store Mutations', () => { }); describe('RECEIVE_AUTOCOMPLETE_SUCCESS', () => { - it('sets loading to false and sets autocompleteOptions array', () => { - mutations[types.RECEIVE_AUTOCOMPLETE_SUCCESS](state, MOCK_AUTOCOMPLETE_OPTIONS); + it('sets loading to false and then formats and sets the autocompleteOptions array', () => { + mutations[types.RECEIVE_AUTOCOMPLETE_SUCCESS](state, MOCK_AUTOCOMPLETE_OPTIONS_RES); expect(state.loading).toBe(false); expect(state.autocompleteOptions).toStrictEqual(MOCK_AUTOCOMPLETE_OPTIONS); @@ -37,6 +41,14 @@ describe('Header Search Store Mutations', () => { }); }); + describe('CLEAR_AUTOCOMPLETE', () => { + it('empties autocompleteOptions array', () => { + mutations[types.CLEAR_AUTOCOMPLETE](state); + + expect(state.autocompleteOptions).toStrictEqual([]); + }); + }); + describe('SET_SEARCH', () => { it('sets search to value', () => { mutations[types.SET_SEARCH](state, MOCK_SEARCH); |