From d4a648835a41d0d05208c44d1bd7af1750f5ef15 Mon Sep 17 00:00:00 2001 From: Tomas Bulva <tbulva@gitlab.com> Date: Wed, 12 Mar 2025 19:56:17 +0100 Subject: [PATCH] Fixed not updating properly other scopes when multimatch search term changes --- .../javascripts/search/store/actions.js | 95 +++++----- .../javascripts/search/store/constants.js | 1 + .../javascripts/search/store/mutations.js | 2 +- app/assets/javascripts/search/store/utils.js | 21 ++- .../search/topbar/components/app.vue | 8 +- .../search/user_searches_for_code_spec.rb | 8 +- spec/frontend/search/mock_data.js | 2 +- spec/frontend/search/store/actions_spec.js | 178 ++++++++++++++---- spec/frontend/search/store/utils_spec.js | 44 +++++ .../search/topbar/components/app_spec.js | 44 +++++ spec/support/helpers/search_helpers.rb | 5 +- 11 files changed, 319 insertions(+), 89 deletions(-) diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js index afc08045afaae..c7a7e33889881 100644 --- a/app/assets/javascripts/search/store/actions.js +++ b/app/assets/javascripts/search/store/actions.js @@ -1,4 +1,5 @@ import { omitBy } from 'lodash'; +import { nextTick } from 'vue'; import Api from '~/api'; import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; @@ -23,6 +24,7 @@ import { prepareSearchAggregations, setDataToLS, skipBlobESCount, + buildDocumentTitle, } from './utils'; export const fetchGroups = ({ commit }, search) => { @@ -103,7 +105,45 @@ export const setFrequentProject = ({ state, commit }, item) => { commit(types.LOAD_FREQUENT_ITEMS, { key: PROJECTS_LOCAL_STORAGE_KEY, data: frequentItems }); }; -export const setQuery = ({ state, commit, getters }, { key, value }) => { +export const fetchSidebarCount = ({ commit, state }) => { + const items = Object.values(state.navigation) + .filter( + (navigationItem) => + !navigationItem.active && + navigationItem.count_link && + skipBlobESCount(state, navigationItem.scope), + ) + .map((navItem) => { + const navigationItem = { ...navItem }; + + const modifications = { + search: state.query?.search || '*', + }; + + if (navigationItem.scope === SCOPE_BLOB && loadDataFromLS(LS_REGEX_HANDLE)) { + modifications[REGEX_PARAM] = true; + } + + navigationItem.count_link = setUrlParams( + modifications, + getNormalizedURL(navigationItem.count_link), + ); + return navigationItem; + }); + + const promises = items.map((navigationItem) => + axios + .get(navigationItem.count_link) + .then(({ data: { count } }) => { + commit(types.RECEIVE_NAVIGATION_COUNT, { key: navigationItem.scope, count }); + }) + .catch((e) => logError(e)), + ); + + return Promise.all(promises); +}; + +export const setQuery = async ({ state, commit, getters }, { key, value }) => { commit(types.SET_QUERY, { key, value }); if (SIDEBAR_PARAMS.includes(key)) { @@ -117,10 +157,14 @@ export const setQuery = ({ state, commit, getters }, { key, value }) => { if ( state.searchType === SEARCH_TYPE_ZOEKT && getters.currentScope === SCOPE_BLOB && - gon.features.zoektMultimatchFrontend + gon.features?.zoektMultimatchFrontend ) { const newUrl = setUrlParams({ ...state.query }, window.location.href, false, true); - updateHistory({ state: state.query, url: newUrl, replace: true }); + document.title = buildDocumentTitle(state.query.search); + updateHistory({ state: state.query, title: state.query.search, url: newUrl, replace: false }); + + await nextTick(); + fetchSidebarCount({ state, commit }); } }; @@ -148,53 +192,16 @@ export const resetQuery = ({ state }) => { ); }; -export const closeLabel = ({ state, commit }, { title }) => { - const labels = state?.query?.[LABEL_FILTER_PARAM].filter((labelName) => labelName !== title); - setQuery({ state, commit }, { key: LABEL_FILTER_PARAM, value: labels }); +export const closeLabel = ({ state, commit, getters }, { title }) => { + const labels = + state?.query?.[LABEL_FILTER_PARAM]?.filter((labelName) => labelName !== title) || []; + setQuery({ state, commit, getters }, { key: LABEL_FILTER_PARAM, value: labels }); }; export const setLabelFilterSearch = ({ commit }, { value }) => { commit(types.SET_LABEL_SEARCH_STRING, value); }; -export const fetchSidebarCount = ({ commit, state }) => { - const items = Object.values(state.navigation) - .filter( - (navigationItem) => - !navigationItem.active && - navigationItem.count_link && - skipBlobESCount(state, navigationItem.scope), - ) - .map((navItem) => { - const navigationItem = { ...navItem }; - const modifications = { - search: state.query?.search || '*', - }; - - if (navigationItem.scope === SCOPE_BLOB && loadDataFromLS(LS_REGEX_HANDLE)) { - modifications[REGEX_PARAM] = true; - } - - navigationItem.count_link = setUrlParams( - modifications, - getNormalizedURL(navigationItem.count_link), - ); - - return navigationItem; - }); - - const promises = items.map((navigationItem) => - axios - .get(navigationItem.count_link) - .then(({ data: { count } }) => { - commit(types.RECEIVE_NAVIGATION_COUNT, { key: navigationItem.scope, count }); - }) - .catch((e) => logError(e)), - ); - - return Promise.all(promises); -}; - export const fetchAllAggregation = ({ commit, state }) => { commit(types.REQUEST_AGGREGATIONS); return axios diff --git a/app/assets/javascripts/search/store/constants.js b/app/assets/javascripts/search/store/constants.js index dbb2ce7f11205..fa9270ec5ff3a 100644 --- a/app/assets/javascripts/search/store/constants.js +++ b/app/assets/javascripts/search/store/constants.js @@ -84,3 +84,4 @@ export const SEARCH_LEVEL_PROJECT = 'project'; export const SEARCH_LEVEL_GROUP = 'group'; export const LS_REGEX_HANDLE = `${REGEX_PARAM}_advanced_search`; +export const SEARCH_WINDOW_TITLE = `${s__('GlobalSearch|Search')} · GitLab`; diff --git a/app/assets/javascripts/search/store/mutations.js b/app/assets/javascripts/search/store/mutations.js index 7627b2e0e08a7..b67f7d2843b77 100644 --- a/app/assets/javascripts/search/store/mutations.js +++ b/app/assets/javascripts/search/store/mutations.js @@ -33,7 +33,7 @@ export default { state.frequentItems[key] = data; }, [types.RECEIVE_NAVIGATION_COUNT](state, { key, count }) { - const item = { ...state.navigation[key], count, count_link: null }; + const item = { ...state.navigation[key], count }; state.navigation = { ...state.navigation, [key]: item }; }, [types.REQUEST_AGGREGATIONS](state) { diff --git a/app/assets/javascripts/search/store/utils.js b/app/assets/javascripts/search/store/utils.js index 4ef2d91c057df..aa203c0ba0485 100644 --- a/app/assets/javascripts/search/store/utils.js +++ b/app/assets/javascripts/search/store/utils.js @@ -14,6 +14,7 @@ import { NUMBER_FORMATING_OPTIONS, REGEX_PARAM, LS_REGEX_HANDLE, + SEARCH_WINDOW_TITLE, } from './constants'; function extractKeys(object, keyList) { @@ -114,7 +115,6 @@ export const mergeById = (inflatedData, storedData) => { export const isSidebarDirty = (currentQuery, urlQuery) => { return SIDEBAR_PARAMS.some((param) => { - // userAddParam ensures we don't get a false dirty from null !== undefined const userAddedParam = !urlQuery[param] && currentQuery[param]; const userChangedExistingParam = urlQuery[param] && urlQuery[param] !== currentQuery[param]; @@ -219,3 +219,22 @@ export const skipBlobESCount = (state, itemScope) => state.zoektAvailable && itemScope === SCOPE_BLOB ); + +export const buildDocumentTitle = (title) => { + const prevTitle = document.title; + + if (prevTitle.includes(SEARCH_WINDOW_TITLE)) { + if (prevTitle.startsWith(SEARCH_WINDOW_TITLE)) { + return `${title} · ${SEARCH_WINDOW_TITLE}`; + } + + if (prevTitle.trim().startsWith(` · ${SEARCH_WINDOW_TITLE}`.trim())) { + return `${title} · ${SEARCH_WINDOW_TITLE}`; + } + + const pattern = new RegExp(`^.*?(?= · ${SEARCH_WINDOW_TITLE})`); + return prevTitle.replace(pattern, title); + } + // If pattern not found, return the original + return title; +}; diff --git a/app/assets/javascripts/search/topbar/components/app.vue b/app/assets/javascripts/search/topbar/components/app.vue index 8e04bda01ae78..1811a608d7c2c 100644 --- a/app/assets/javascripts/search/topbar/components/app.vue +++ b/app/assets/javascripts/search/topbar/components/app.vue @@ -1,8 +1,9 @@ <script> import { GlButton } from '@gitlab/ui'; -import { isEmpty } from 'lodash'; +import { isEmpty, debounce } from 'lodash'; // eslint-disable-next-line no-restricted-imports import { mapState, mapActions, mapGetters } from 'vuex'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { InternalEvents } from '~/tracking'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { s__ } from '~/locale'; @@ -51,6 +52,10 @@ export default { return this.query ? this.query.search : ''; }, set(value) { + if (this.isMultiMatch) { + this.debouncedSetQuery({ key: 'search', value }); + return; + } this.setQuery({ key: 'search', value }); }, }, @@ -86,6 +91,7 @@ export default { created() { this.preloadStoredFrequentItems(); this.regexEnabled = loadDataFromLS(LS_REGEX_HANDLE); + this.debouncedSetQuery = debounce(this.setQuery, DEFAULT_DEBOUNCE_AND_THROTTLE_MS); }, methods: { ...mapActions(['applyQuery', 'setQuery', 'preloadStoredFrequentItems']), diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb index 48161e675dd84..ae861817234fe 100644 --- a/spec/features/search/user_searches_for_code_spec.rb +++ b/spec/features/search/user_searches_for_code_spec.rb @@ -7,7 +7,13 @@ include ListboxHelpers let_it_be(:user) { create(:user) } - let_it_be_with_reload(:project) { create(:project, :repository, namespace: user.namespace) } + let_it_be_with_reload(:project) do + # This helps with some of the test flakiness. + project = create(:project, :repository, namespace: user.namespace) + project.repository.root_ref + project.repository.ls_files('master') + project + end context 'when signed in' do before do diff --git a/spec/frontend/search/mock_data.js b/spec/frontend/search/mock_data.js index 4ec1e01e55326..558959b38e24c 100644 --- a/spec/frontend/search/mock_data.js +++ b/spec/frontend/search/mock_data.js @@ -220,7 +220,7 @@ export const MOCK_DATA_FOR_NAVIGATION_ACTION_MUTATION = { label: 'Projects', scope: 'projects', link: '/search?scope=projects&search=et', - count_link: null, + count_link: '/search/count?scope=projects&search=et', }, }; diff --git a/spec/frontend/search/store/actions_spec.js b/spec/frontend/search/store/actions_spec.js index c92600e50aafd..ba98fc85298b4 100644 --- a/spec/frontend/search/store/actions_spec.js +++ b/spec/frontend/search/store/actions_spec.js @@ -1,6 +1,9 @@ import MockAdapter from 'axios-mock-adapter'; import { mapValues } from 'lodash'; +// rspec spec/frontend/fixtures/search_navigation.rb to generate this file +import noActiveItems from 'test_fixtures/search_navigation/no_active_items.json'; import testAction from 'helpers/vuex_action_helper'; +import { setUrlParams, updateHistory } from '~/lib/utils/url_utility'; import Api from '~/api'; import { createAlert } from '~/alert'; import * as logger from '~/lib/logger'; @@ -45,6 +48,17 @@ jest.mock('~/lib/logger', () => ({ logError: jest.fn(), })); +jest.mock('~/lib/utils/url_utility', () => { + const urlUtility = jest.requireActual('~/lib/utils/url_utility'); + + return { + __esModule: true, + ...urlUtility, + setUrlParams: jest.fn(() => 'mocked-new-url'), + updateHistory: jest.fn(), + }; +}); + describe('Global Search Store Actions', () => { let mock; let state; @@ -159,41 +173,112 @@ describe('Global Search Store Actions', () => { }); }); - describe.each` - payload | isDirty | isDirtyMutation - ${{ key: SIDEBAR_PARAMS[0], value: 'test' }} | ${false} | ${[{ type: types.SET_SIDEBAR_DIRTY, payload: false }]} - ${{ key: SIDEBAR_PARAMS[0], value: 'test' }} | ${true} | ${[{ type: types.SET_SIDEBAR_DIRTY, payload: true }]} - ${{ key: SIDEBAR_PARAMS[1], value: 'test' }} | ${false} | ${[{ type: types.SET_SIDEBAR_DIRTY, payload: false }]} - ${{ key: SIDEBAR_PARAMS[1], value: 'test' }} | ${true} | ${[{ type: types.SET_SIDEBAR_DIRTY, payload: true }]} - ${{ key: 'non-sidebar', value: 'test' }} | ${false} | ${[]} - ${{ key: 'non-sidebar', value: 'test' }} | ${true} | ${[]} - `('setQuery', ({ payload, isDirty, isDirtyMutation }) => { - describe(`when filter param is ${payload.key} and utils.isSidebarDirty returns ${isDirty}`, () => { - const expectedMutations = [{ type: types.SET_QUERY, payload }].concat(isDirtyMutation); + describe('setQuery', () => { + describe('when search type is zoekt and scope is blob with zoektMultimatchFrontend feature enabled', () => { + const payload = { key: 'some-key', value: 'some-value' }; + let originalGon; + let commit; + let fetchSidebarCountSpy; beforeEach(() => { - storeUtils.isSidebarDirty = jest.fn().mockReturnValue(isDirty); + originalGon = window.gon; + commit = jest.fn(); + + fetchSidebarCountSpy = jest + .spyOn(actions, 'fetchSidebarCount') + .mockImplementation(() => Promise.resolve()); + + window.gon = { features: { zoektMultimatchFrontend: true } }; + storeUtils.isSidebarDirty = jest.fn().mockReturnValue(false); + + state = createState({ + query: { ...MOCK_QUERY, search: 'test-search' }, + navigation: { ...MOCK_NAVIGATION }, + searchType: 'zoekt', + }); }); - it(`should dispatch the correct mutations`, () => { - return testAction({ action: actions.setQuery, payload, state, expectedMutations }); + afterEach(() => { + window.gon = originalGon; + fetchSidebarCountSpy.mockRestore(); }); - }); - }); - describe.each` - payload - ${{ key: REGEX_PARAM, value: true }} - ${{ key: REGEX_PARAM, value: { random: 'test' } }} - `('setQuery', ({ payload }) => { - describe(`when query param is ${payload.key}`, () => { - beforeEach(() => { - storeUtils.setDataToLS = jest.fn(); - actions.setQuery({ state, commit: jest.fn() }, payload); + it('should update URL, document title, and history', async () => { + const getters = { currentScope: 'blobs' }; + + await actions.setQuery({ state, commit, getters }, payload); + + expect(setUrlParams).toHaveBeenCalledWith( + { ...state.query }, + window.location.href, + false, + true, + ); + + expect(document.title).toBe(state.query.search); + + expect(updateHistory).toHaveBeenCalledWith({ + state: state.query, + title: state.query.search, + url: 'mocked-new-url', + replace: false, + }); + }); + + it('does not update URL or fetch sidebar counts when conditions are not met', async () => { + let getters = { currentScope: 'blobs' }; + state.searchType = 'not-zoekt'; + + await actions.setQuery({ state, commit, getters }, payload); + + expect(setUrlParams).not.toHaveBeenCalled(); + expect(updateHistory).not.toHaveBeenCalled(); + expect(fetchSidebarCountSpy).not.toHaveBeenCalled(); + + setUrlParams.mockClear(); + updateHistory.mockClear(); + fetchSidebarCountSpy.mockClear(); + + state.searchType = 'zoekt'; + getters = { currentScope: 'not-blobs' }; + + await actions.setQuery({ state, commit, getters }, payload); + + expect(setUrlParams).not.toHaveBeenCalled(); + expect(updateHistory).not.toHaveBeenCalled(); + expect(fetchSidebarCountSpy).not.toHaveBeenCalled(); + + setUrlParams.mockClear(); + updateHistory.mockClear(); + fetchSidebarCountSpy.mockClear(); + + getters = { currentScope: 'blobs' }; + window.gon.features.zoektMultimatchFrontend = false; + + await actions.setQuery({ state, commit, getters }, payload); + + expect(setUrlParams).not.toHaveBeenCalled(); + expect(updateHistory).not.toHaveBeenCalled(); + expect(fetchSidebarCountSpy).not.toHaveBeenCalled(); }); + }); + + describe.each` + payload + ${{ key: REGEX_PARAM, value: true }} + ${{ key: REGEX_PARAM, value: { random: 'test' } }} + `('setQuery with REGEX_PARAM', ({ payload }) => { + describe(`when query param is ${payload.key}`, () => { + beforeEach(() => { + storeUtils.setDataToLS = jest.fn(); + window.gon = { features: { zoektMultimatchFrontend: false } }; + const getters = { currentScope: 'not-blobs' }; + actions.setQuery({ state, commit: jest.fn(), getters }, payload); + }); - it(`setsItem in local storage`, () => { - expect(storeUtils.setDataToLS).toHaveBeenCalledWith(LS_REGEX_HANDLE, expect.anything()); + it(`setsItem in local storage`, () => { + expect(storeUtils.setDataToLS).toHaveBeenCalledWith(LS_REGEX_HANDLE, expect.anything()); + }); }); }); }); @@ -201,7 +286,12 @@ describe('Global Search Store Actions', () => { describe('applyQuery', () => { beforeEach(() => { setWindowLocation('https://test/'); - jest.spyOn(urlUtils, 'visitUrl').mockReturnValue({}); + jest.spyOn(urlUtils, 'visitUrl').mockImplementation(() => {}); + jest + .spyOn(urlUtils, 'setUrlParams') + .mockReturnValue( + 'https://test/?scope=issues&state=all&group_id=1&language%5B%5D=C&language%5B%5D=JavaScript&label_name%5B%5D=Aftersync&label_name%5B%5D=Brist&search=*', + ); }); it('calls visitUrl and setParams with the state.query', async () => { @@ -355,17 +445,29 @@ describe('Global Search Store Actions', () => { }); }); - describe('fetchSidebarCount uses wild card seach', () => { + describe('fetchSidebarCount uses wild card search', () => { beforeEach(() => { - state.navigation = MOCK_NAVIGATION; - state.urlQuery.search = ''; + state.navigation = noActiveItems; + state.query = { search: '' }; + state.urlQuery = { search: '' }; + + jest.spyOn(urlUtils, 'setUrlParams').mockImplementation((params) => { + return `http://test.host/search/count?search=${params.search || '*'}`; + }); + + storeUtils.skipBlobESCount = jest.fn().mockReturnValue(true); + + mock.onGet().reply(HTTP_STATUS_OK, MOCK_ENDPOINT_RESPONSE); }); it('should use wild card', async () => { - await testAction({ action: actions.fetchSidebarCount, state, expectedMutations: [] }); - expect(mock.history.get[0].url).toBe('http://test.host/search/count?scope=projects&search=*'); - expect(mock.history.get[3].url).toBe( - 'http://test.host/search/count?scope=merge_requests&search=*', + const commit = jest.fn(); + + await actions.fetchSidebarCount({ commit, state }); + + expect(urlUtils.setUrlParams).toHaveBeenCalledWith( + expect.objectContaining({ search: '*' }), + expect.anything(), ); }); }); @@ -409,16 +511,16 @@ describe('Global Search Store Actions', () => { { payload: { key: 'label_name', - value: ['Aftersync', 'Brist'], + value: ['Aftersync'], }, type: 'SET_QUERY', }, { - payload: true, + payload: false, type: 'SET_SIDEBAR_DIRTY', }, ]; - return testAction(actions.closeLabel, { key: '60' }, state, expectedResult, []); + return testAction(actions.closeLabel, { title: 'Brist' }, state, expectedResult, []); }); }); diff --git a/spec/frontend/search/store/utils_spec.js b/spec/frontend/search/store/utils_spec.js index 014f963002e1c..ffb0c80ed06e0 100644 --- a/spec/frontend/search/store/utils_spec.js +++ b/spec/frontend/search/store/utils_spec.js @@ -1,3 +1,4 @@ +// rspec spec/frontend/fixtures/search_navigation.rb to generate these files import subItemActive from 'test_fixtures/search_navigation/sub_item_active.json'; import noActiveItems from 'test_fixtures/search_navigation/no_active_items.json'; import partialNavigationActive from 'test_fixtures/search_navigation/partial_navigation_active.json'; @@ -17,6 +18,7 @@ import { injectRegexSearch, scopeCrawler, skipBlobESCount, + buildDocumentTitle, } from '~/search/store/utils'; import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; import { TEST_HOST } from 'helpers/test_constants'; @@ -447,4 +449,46 @@ describe('Global Search Store Utils', () => { expect(skipBlobESCount(state, SCOPE_BLOB)).toBe(false); }); }); + + describe('buildDocumentTitle', () => { + const SEARCH_WINDOW_TITLE = `Search`; // Make sure this matches your actual constant + let originalTitle; + + beforeEach(() => { + originalTitle = document.title; + }); + + afterEach(() => { + document.title = originalTitle; + }); + + it('returns original title when document title does not include search title', () => { + document.title = 'GitLab'; + expect(buildDocumentTitle('test')).toBe('test'); + }); + + it('prepends new title when document title starts with search title', () => { + document.title = `${SEARCH_WINDOW_TITLE} · GitLab`; + const result = buildDocumentTitle('test'); + expect(result).toBe(`test · ${SEARCH_WINDOW_TITLE} · GitLab`); + }); + + it('prepends new title when document title starts with dot and search title', () => { + document.title = ` · ${SEARCH_WINDOW_TITLE} · GitLab`; + const result = buildDocumentTitle('test'); + expect(result).toBe(`test · ${SEARCH_WINDOW_TITLE} · GitLab`); + }); + + it('replaces title before search title with new title', () => { + document.title = `Issues · ${SEARCH_WINDOW_TITLE} · GitLab`; + const result = buildDocumentTitle('test'); + expect(result).toBe(`test · ${SEARCH_WINDOW_TITLE} · GitLab`); + }); + + it('handles complex titles correctly', () => { + document.title = `Something · With · Dots · ${SEARCH_WINDOW_TITLE} · GitLab`; + const result = buildDocumentTitle('test'); + expect(result).toBe(`test · ${SEARCH_WINDOW_TITLE} · GitLab`); + }); + }); }); diff --git a/spec/frontend/search/topbar/components/app_spec.js b/spec/frontend/search/topbar/components/app_spec.js index ca4023086db30..da2d3e12ffc66 100644 --- a/spec/frontend/search/topbar/components/app_spec.js +++ b/spec/frontend/search/topbar/components/app_spec.js @@ -229,4 +229,48 @@ describe('GlobalSearchTopbar', () => { }); }); }); + + describe('search computed property setter', () => { + describe.each` + FF | scope | searchType | debounced + ${{ zoektMultimatchFrontend: true }} | ${'blobs'} | ${'zoekt'} | ${true} + ${{ zoektMultimatchFrontend: false }} | ${'blobs'} | ${'zoekt'} | ${false} + ${{ zoektMultimatchFrontend: true }} | ${'issues'} | ${'zoekt'} | ${false} + ${{ zoektMultimatchFrontend: true }} | ${'blobs'} | ${'advanced'} | ${false} + `( + 'when isMultiMatch is $debounced (FF: $FF, scope: $scope, searchType: $searchType)', + ({ FF, scope, searchType, debounced }) => { + beforeEach(() => { + getterSpies.currentScope = jest.fn(() => scope); + actionSpies.setQuery.mockClear(); + + createComponent({ + featureFlag: FF, + initialState: { searchType }, + }); + + wrapper.vm.debouncedSetQuery = jest.fn(); + }); + + it(`${debounced ? 'calls debouncedSetQuery' : 'calls setQuery directly'}`, () => { + findGlSearchBox().vm.$emit('input', 'new search value'); + + if (debounced) { + expect(actionSpies.setQuery).not.toHaveBeenCalled(); + } else { + expect(actionSpies.setQuery).toHaveBeenCalled(); + + const lastCallArgs = actionSpies.setQuery.mock.calls[0]; + const payload = lastCallArgs[lastCallArgs.length - 1]; + expect(payload).toEqual( + expect.objectContaining({ + key: 'search', + value: 'new search value', + }), + ); + } + }); + }, + ); + }); }); diff --git a/spec/support/helpers/search_helpers.rb b/spec/support/helpers/search_helpers.rb index b915196462825..d4bf82f536a67 100644 --- a/spec/support/helpers/search_helpers.rb +++ b/spec/support/helpers/search_helpers.rb @@ -9,15 +9,16 @@ def fill_in_search(text) end def submit_search(query) - # Forms directly on the search page if page.has_css?('.search-page-form') search_form = '.search-page-form' - # Open search modal from super sidebar + else find_by_testid('super-sidebar-search-button').click search_form = '#super-sidebar-search-modal' end + wait_for_all_requests + page.within(search_form) do field = find_field('search') field.click -- GitLab