diff --git a/app/assets/javascripts/header_search/components/app.vue b/app/assets/javascripts/header_search/components/app.vue index 67e3998bc9763355734fd56d9c65780e2fe8af64..c22f532d7acaf4814f40185eef8004de3f958381 100644 --- a/app/assets/javascripts/header_search/components/app.vue +++ b/app/assets/javascripts/header_search/components/app.vue @@ -1,7 +1,9 @@ <script> import { GlSearchBoxByType, GlOutsideDirective as Outside } from '@gitlab/ui'; import { mapState, mapActions, mapGetters } from 'vuex'; +import { debounce } from 'lodash'; import { visitUrl } from '~/lib/utils/url_utility'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { s__, sprintf } from '~/locale'; import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue'; import { @@ -106,7 +108,7 @@ export default { }, }, methods: { - ...mapActions(['setSearch', 'fetchAutocompleteOptions']), + ...mapActions(['setSearch', 'fetchAutocompleteOptions', 'clearAutocomplete']), openDropdown() { this.showDropdown = true; }, @@ -116,13 +118,13 @@ export default { submitSearch() { return visitUrl(this.currentFocusedOption?.url || this.searchQuery); }, - getAutocompleteOptions(searchTerm) { + getAutocompleteOptions: debounce(function debouncedSearch(searchTerm) { if (!searchTerm) { - return; + this.clearAutocomplete(); + } else { + this.fetchAutocompleteOptions(); } - - this.fetchAutocompleteOptions(); - }, + }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS), }, SEARCH_BOX_INDEX, SEARCH_INPUT_DESCRIPTION, @@ -141,7 +143,6 @@ export default { v-model="searchText" role="searchbox" class="gl-z-index-1" - :debounce="500" autocomplete="off" :placeholder="$options.i18n.searchPlaceholder" :aria-activedescendant="currentFocusedId" diff --git a/app/assets/javascripts/header_search/store/actions.js b/app/assets/javascripts/header_search/store/actions.js index 2c3b1bd4c0f13fe5b2e02d5b0dff1771a5a7f3d7..0ba956f3ed125a6fd715f7b6a6410a628dfa3147 100644 --- a/app/assets/javascripts/header_search/store/actions.js +++ b/app/assets/javascripts/header_search/store/actions.js @@ -14,6 +14,10 @@ export const fetchAutocompleteOptions = ({ commit, getters }) => { }); }; +export const clearAutocomplete = ({ commit }) => { + commit(types.CLEAR_AUTOCOMPLETE); +}; + export const setSearch = ({ commit }, value) => { commit(types.SET_SEARCH, value); }; diff --git a/app/assets/javascripts/header_search/store/mutation_types.js b/app/assets/javascripts/header_search/store/mutation_types.js index a2358621ce6c3a25f255c36cc6bf045eee0cce6c..6e65345757f7fec143eca3a47a12fd3f7d06c528 100644 --- a/app/assets/javascripts/header_search/store/mutation_types.js +++ b/app/assets/javascripts/header_search/store/mutation_types.js @@ -1,5 +1,6 @@ export const REQUEST_AUTOCOMPLETE = 'REQUEST_AUTOCOMPLETE'; export const RECEIVE_AUTOCOMPLETE_SUCCESS = 'RECEIVE_AUTOCOMPLETE_SUCCESS'; export const RECEIVE_AUTOCOMPLETE_ERROR = 'RECEIVE_AUTOCOMPLETE_ERROR'; +export const CLEAR_AUTOCOMPLETE = 'CLEAR_AUTOCOMPLETE'; export const SET_SEARCH = 'SET_SEARCH'; diff --git a/app/assets/javascripts/header_search/store/mutations.js b/app/assets/javascripts/header_search/store/mutations.js index 7fe13600ac9be8cccbbc270e6c48ae2c3a423eec..26b4a8854fe8bc713a75d102e6414b0d916f7729 100644 --- a/app/assets/javascripts/header_search/store/mutations.js +++ b/app/assets/javascripts/header_search/store/mutations.js @@ -15,6 +15,9 @@ export default { state.loading = false; state.autocompleteOptions = []; }, + [types.CLEAR_AUTOCOMPLETE](state) { + state.autocompleteOptions = []; + }, [types.SET_SEARCH](state, value) { state.search = value; }, diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js index 194846c410acce0fc5fcb05d218de20dfe094e0d..3200c6614f1994c17fc656c0003e2adfbf6a05dc 100644 --- a/spec/frontend/header_search/components/app_spec.js +++ b/spec/frontend/header_search/components/app_spec.js @@ -30,6 +30,7 @@ describe('HeaderSearchApp', () => { const actionSpies = { setSearch: jest.fn(), fetchAutocompleteOptions: jest.fn(), + clearAutocomplete: jest.fn(), }; const createComponent = (initialState, mockGetters) => { @@ -217,16 +218,40 @@ describe('HeaderSearchApp', () => { }); describe('onInput', () => { - beforeEach(() => { - findHeaderSearchInput().vm.$emit('input', MOCK_SEARCH); - }); + 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 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('calls fetchAutocompleteOptions', () => { - expect(actionSpies.fetchAutocompleteOptions).toHaveBeenCalled(); + describe('when search is emptied', () => { + beforeEach(() => { + findHeaderSearchInput().vm.$emit('input', ''); + }); + + it('calls setSearch with empty term', () => { + expect(actionSpies.setSearch).toHaveBeenCalledWith(expect.any(Object), ''); + }); + + it('does not call fetchAutocompleteOptions', () => { + expect(actionSpies.fetchAutocompleteOptions).not.toHaveBeenCalled(); + }); + + it('calls clearAutocomplete', () => { + expect(actionSpies.clearAutocomplete).toHaveBeenCalled(); + }); }); }); }); diff --git a/spec/frontend/header_search/store/actions_spec.js b/spec/frontend/header_search/store/actions_spec.js index bd2c4158157a626a402f2c5ab4acbadda3a67188..6599115f0172e50239f6d4c008e20c94570ecd7e 100644 --- a/spec/frontend/header_search/store/actions_spec.js +++ b/spec/frontend/header_search/store/actions_spec.js @@ -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/mutations_spec.js b/spec/frontend/header_search/store/mutations_spec.js index a60ef6e60e191f0cbf7c404c497a68282d2293c3..7bcf8e491182e649c3c73dc50c49a80171fcda7c 100644 --- a/spec/frontend/header_search/store/mutations_spec.js +++ b/spec/frontend/header_search/store/mutations_spec.js @@ -41,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);