diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-11-23 00:10:35 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-11-23 00:10:35 +0300 |
commit | ebaefcebccee0575e8dddde1fe17dabaae62459b (patch) | |
tree | 286bf98e899eb48a0b2a4bbd6f0c506f6409b5a2 /spec/frontend/header_search/components | |
parent | 39cc8695fc20e17f4989fa99aa9fafc00f9e2953 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend/header_search/components')
4 files changed, 197 insertions, 51 deletions
diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js index 2ea2693a978..7ecbef8af4b 100644 --- a/spec/frontend/header_search/components/app_spec.js +++ b/spec/frontend/header_search/components/app_spec.js @@ -6,9 +6,15 @@ 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 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, +} from '../mock_data'; Vue.use(Vuex); @@ -24,7 +30,7 @@ describe('HeaderSearchApp', () => { fetchAutocompleteOptions: jest.fn(), }; - const createComponent = (initialState) => { + const createComponent = (initialState, mockGetters) => { const store = new Vuex.Store({ state: { ...initialState, @@ -32,6 +38,8 @@ describe('HeaderSearchApp', () => { actions: actionSpies, getters: { searchQuery: () => MOCK_SEARCH_QUERY, + searchOptions: () => MOCK_DEFAULT_SEARCH_OPTIONS, + ...mockGetters, }, }); @@ -50,6 +58,7 @@ describe('HeaderSearchApp', () => { const findHeaderSearchScopedItems = () => wrapper.findComponent(HeaderSearchScopedItems); const findHeaderSearchAutocompleteItems = () => wrapper.findComponent(HeaderSearchAutocompleteItems); + const findDropdownKeyboardNavigation = () => wrapper.findComponent(DropdownKeyboardNavigation); describe('template', () => { it('always renders Header Search Input', () => { @@ -66,8 +75,8 @@ 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; + createComponent(); wrapper.setData({ showDropdown }); }); @@ -78,31 +87,42 @@ 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); - }); - - it(`should${showScoped ? '' : ' not'} render the Scoped Dropdown Items`, () => { - expect(findHeaderSearchScopedItems().exists()).toBe(showScoped); + 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${showAutocomplete ? '' : ' not'} render the Autocomplete Dropdown Items`, () => { - expect(findHeaderSearchAutocompleteItems().exists()).toBe(showAutocomplete); - }); - }); - }); + }, + ); }); describe('events', () => { @@ -132,21 +152,6 @@ describe('HeaderSearchApp', () => { }); }); - describe('when dropdown is opened', () => { - beforeEach(() => { - wrapper.setData({ showDropdown: true }); - }); - - it('onKey-Escape closes dropdown', async () => { - expect(findHeaderSearchDropdown().exists()).toBe(true); - findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ESC_KEY })); - - await wrapper.vm.$nextTick(); - - expect(findHeaderSearchDropdown().exists()).toBe(false); - }); - }); - describe('onInput', () => { beforeEach(() => { findHeaderSearchInput().vm.$emit('input', MOCK_SEARCH); @@ -160,8 +165,49 @@ describe('HeaderSearchApp', () => { expect(actionSpies.fetchAutocompleteOptions).toHaveBeenCalled(); }); }); + }); + + describe('Dropdown Keyboard Navigation', () => { + beforeEach(() => { + findHeaderSearchInput().vm.$emit('click'); + }); - it('submits a search onKey-Enter', async () => { + it('closes dropdown when @tab is emitted', async () => { + expect(findHeaderSearchDropdown().exists()).toBe(true); + findDropdownKeyboardNavigation().vm.$emit('tab'); + + 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]); + }); + }); + }); + + 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 +215,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..ecfaa8e9443 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,42 @@ describe('HeaderSearchAutocompleteItems', () => { }); }); }); + + describe.each` + currentFocusedOption | isFocused + ${null} | ${false} + ${{ html_id: 'not-a-match' }} | ${false} + ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS[0]} | ${true} + `('isOptionFocused', ({ currentFocusedOption, isFocused }) => { + 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); + }); + }); + }); + }); + + 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..afad9b5b138 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,22 @@ describe('HeaderSearchDefaultItems', () => { }); }); }); + + describe.each` + currentFocusedOption | isFocused + ${null} | ${false} + ${{ html_id: 'not-a-match' }} | ${false} + ${MOCK_DEFAULT_SEARCH_OPTIONS[0]} | ${true} + `('isOptionFocused', ({ currentFocusedOption, isFocused }) => { + 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); + }); + }); + }); }); }); 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..2fdd03fdae3 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,6 +35,7 @@ describe('HeaderSearchScopedItems', () => { }); const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findFirstDropdownItem = () => findDropdownItems().at(0); const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => trimText(w.text())); const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href')); @@ -57,5 +61,22 @@ describe('HeaderSearchScopedItems', () => { expect(findDropdownItemLinks()).toStrictEqual(expectedLinks); }); }); + + describe.each` + currentFocusedOption | isFocused + ${null} | ${false} + ${{ html_id: 'not-a-match' }} | ${false} + ${MOCK_SCOPED_SEARCH_OPTIONS[0]} | ${true} + `('isOptionFocused', ({ currentFocusedOption, isFocused }) => { + 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); + }); + }); + }); }); }); |