Skip to content
代码片段 群组 项目
未验证 提交 d4a64883 编辑于 作者: Tomas Bulva's avatar Tomas Bulva 提交者: GitLab
浏览文件

Fixed not updating properly other scopes when multimatch search term changes

上级 b01909a9
No related branches found
No related tags found
2 合并请求!3031Merge per-main-jh to main-jh by luzhiyuan,!3030Merge per-main-jh to main-jh
import { omitBy } from 'lodash'; import { omitBy } from 'lodash';
import { nextTick } from 'vue';
import Api from '~/api'; import Api from '~/api';
import { createAlert } from '~/alert'; import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
...@@ -23,6 +24,7 @@ import { ...@@ -23,6 +24,7 @@ import {
prepareSearchAggregations, prepareSearchAggregations,
setDataToLS, setDataToLS,
skipBlobESCount, skipBlobESCount,
buildDocumentTitle,
} from './utils'; } from './utils';
export const fetchGroups = ({ commit }, search) => { export const fetchGroups = ({ commit }, search) => {
...@@ -103,7 +105,45 @@ export const setFrequentProject = ({ state, commit }, item) => { ...@@ -103,7 +105,45 @@ export const setFrequentProject = ({ state, commit }, item) => {
commit(types.LOAD_FREQUENT_ITEMS, { key: PROJECTS_LOCAL_STORAGE_KEY, data: frequentItems }); 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 }); commit(types.SET_QUERY, { key, value });
if (SIDEBAR_PARAMS.includes(key)) { if (SIDEBAR_PARAMS.includes(key)) {
...@@ -117,10 +157,14 @@ export const setQuery = ({ state, commit, getters }, { key, value }) => { ...@@ -117,10 +157,14 @@ export const setQuery = ({ state, commit, getters }, { key, value }) => {
if ( if (
state.searchType === SEARCH_TYPE_ZOEKT && state.searchType === SEARCH_TYPE_ZOEKT &&
getters.currentScope === SCOPE_BLOB && getters.currentScope === SCOPE_BLOB &&
gon.features.zoektMultimatchFrontend gon.features?.zoektMultimatchFrontend
) { ) {
const newUrl = setUrlParams({ ...state.query }, window.location.href, false, true); 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 }) => { ...@@ -148,53 +192,16 @@ export const resetQuery = ({ state }) => {
); );
}; };
export const closeLabel = ({ state, commit }, { title }) => { export const closeLabel = ({ state, commit, getters }, { title }) => {
const labels = state?.query?.[LABEL_FILTER_PARAM].filter((labelName) => labelName !== title); const labels =
setQuery({ state, commit }, { key: LABEL_FILTER_PARAM, value: 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 }) => { export const setLabelFilterSearch = ({ commit }, { value }) => {
commit(types.SET_LABEL_SEARCH_STRING, 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 }) => { export const fetchAllAggregation = ({ commit, state }) => {
commit(types.REQUEST_AGGREGATIONS); commit(types.REQUEST_AGGREGATIONS);
return axios return axios
......
...@@ -84,3 +84,4 @@ export const SEARCH_LEVEL_PROJECT = 'project'; ...@@ -84,3 +84,4 @@ export const SEARCH_LEVEL_PROJECT = 'project';
export const SEARCH_LEVEL_GROUP = 'group'; export const SEARCH_LEVEL_GROUP = 'group';
export const LS_REGEX_HANDLE = `${REGEX_PARAM}_advanced_search`; export const LS_REGEX_HANDLE = `${REGEX_PARAM}_advanced_search`;
export const SEARCH_WINDOW_TITLE = `${s__('GlobalSearch|Search')} · GitLab`;
...@@ -33,7 +33,7 @@ export default { ...@@ -33,7 +33,7 @@ export default {
state.frequentItems[key] = data; state.frequentItems[key] = data;
}, },
[types.RECEIVE_NAVIGATION_COUNT](state, { key, count }) { [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 }; state.navigation = { ...state.navigation, [key]: item };
}, },
[types.REQUEST_AGGREGATIONS](state) { [types.REQUEST_AGGREGATIONS](state) {
......
...@@ -14,6 +14,7 @@ import { ...@@ -14,6 +14,7 @@ import {
NUMBER_FORMATING_OPTIONS, NUMBER_FORMATING_OPTIONS,
REGEX_PARAM, REGEX_PARAM,
LS_REGEX_HANDLE, LS_REGEX_HANDLE,
SEARCH_WINDOW_TITLE,
} from './constants'; } from './constants';
function extractKeys(object, keyList) { function extractKeys(object, keyList) {
...@@ -114,7 +115,6 @@ export const mergeById = (inflatedData, storedData) => { ...@@ -114,7 +115,6 @@ export const mergeById = (inflatedData, storedData) => {
export const isSidebarDirty = (currentQuery, urlQuery) => { export const isSidebarDirty = (currentQuery, urlQuery) => {
return SIDEBAR_PARAMS.some((param) => { return SIDEBAR_PARAMS.some((param) => {
// userAddParam ensures we don't get a false dirty from null !== undefined
const userAddedParam = !urlQuery[param] && currentQuery[param]; const userAddedParam = !urlQuery[param] && currentQuery[param];
const userChangedExistingParam = urlQuery[param] && urlQuery[param] !== currentQuery[param]; const userChangedExistingParam = urlQuery[param] && urlQuery[param] !== currentQuery[param];
...@@ -219,3 +219,22 @@ export const skipBlobESCount = (state, itemScope) => ...@@ -219,3 +219,22 @@ export const skipBlobESCount = (state, itemScope) =>
state.zoektAvailable && state.zoektAvailable &&
itemScope === SCOPE_BLOB 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;
};
<script> <script>
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import { isEmpty } from 'lodash'; import { isEmpty, debounce } from 'lodash';
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { mapState, mapActions, mapGetters } from 'vuex'; import { mapState, mapActions, mapGetters } from 'vuex';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { InternalEvents } from '~/tracking'; import { InternalEvents } from '~/tracking';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
...@@ -51,6 +52,10 @@ export default { ...@@ -51,6 +52,10 @@ export default {
return this.query ? this.query.search : ''; return this.query ? this.query.search : '';
}, },
set(value) { set(value) {
if (this.isMultiMatch) {
this.debouncedSetQuery({ key: 'search', value });
return;
}
this.setQuery({ key: 'search', value }); this.setQuery({ key: 'search', value });
}, },
}, },
...@@ -86,6 +91,7 @@ export default { ...@@ -86,6 +91,7 @@ export default {
created() { created() {
this.preloadStoredFrequentItems(); this.preloadStoredFrequentItems();
this.regexEnabled = loadDataFromLS(LS_REGEX_HANDLE); this.regexEnabled = loadDataFromLS(LS_REGEX_HANDLE);
this.debouncedSetQuery = debounce(this.setQuery, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
}, },
methods: { methods: {
...mapActions(['applyQuery', 'setQuery', 'preloadStoredFrequentItems']), ...mapActions(['applyQuery', 'setQuery', 'preloadStoredFrequentItems']),
......
...@@ -7,7 +7,13 @@ ...@@ -7,7 +7,13 @@
include ListboxHelpers include ListboxHelpers
let_it_be(:user) { create(:user) } 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 context 'when signed in' do
before do before do
......
...@@ -220,7 +220,7 @@ export const MOCK_DATA_FOR_NAVIGATION_ACTION_MUTATION = { ...@@ -220,7 +220,7 @@ export const MOCK_DATA_FOR_NAVIGATION_ACTION_MUTATION = {
label: 'Projects', label: 'Projects',
scope: 'projects', scope: 'projects',
link: '/search?scope=projects&search=et', link: '/search?scope=projects&search=et',
count_link: null, count_link: '/search/count?scope=projects&search=et',
}, },
}; };
......
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { mapValues } from 'lodash'; 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 testAction from 'helpers/vuex_action_helper';
import { setUrlParams, updateHistory } from '~/lib/utils/url_utility';
import Api from '~/api'; import Api from '~/api';
import { createAlert } from '~/alert'; import { createAlert } from '~/alert';
import * as logger from '~/lib/logger'; import * as logger from '~/lib/logger';
...@@ -45,6 +48,17 @@ jest.mock('~/lib/logger', () => ({ ...@@ -45,6 +48,17 @@ jest.mock('~/lib/logger', () => ({
logError: jest.fn(), 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', () => { describe('Global Search Store Actions', () => {
let mock; let mock;
let state; let state;
...@@ -159,41 +173,112 @@ describe('Global Search Store Actions', () => { ...@@ -159,41 +173,112 @@ describe('Global Search Store Actions', () => {
}); });
}); });
describe.each` describe('setQuery', () => {
payload | isDirty | isDirtyMutation describe('when search type is zoekt and scope is blob with zoektMultimatchFrontend feature enabled', () => {
${{ key: SIDEBAR_PARAMS[0], value: 'test' }} | ${false} | ${[{ type: types.SET_SIDEBAR_DIRTY, payload: false }]} const payload = { key: 'some-key', value: 'some-value' };
${{ key: SIDEBAR_PARAMS[0], value: 'test' }} | ${true} | ${[{ type: types.SET_SIDEBAR_DIRTY, payload: true }]} let originalGon;
${{ key: SIDEBAR_PARAMS[1], value: 'test' }} | ${false} | ${[{ type: types.SET_SIDEBAR_DIRTY, payload: false }]} let commit;
${{ key: SIDEBAR_PARAMS[1], value: 'test' }} | ${true} | ${[{ type: types.SET_SIDEBAR_DIRTY, payload: true }]} let fetchSidebarCountSpy;
${{ 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);
beforeEach(() => { 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`, () => { afterEach(() => {
return testAction({ action: actions.setQuery, payload, state, expectedMutations }); window.gon = originalGon;
fetchSidebarCountSpy.mockRestore();
}); });
});
});
describe.each` it('should update URL, document title, and history', async () => {
payload const getters = { currentScope: 'blobs' };
${{ key: REGEX_PARAM, value: true }}
${{ key: REGEX_PARAM, value: { random: 'test' } }} await actions.setQuery({ state, commit, getters }, payload);
`('setQuery', ({ payload }) => {
describe(`when query param is ${payload.key}`, () => { expect(setUrlParams).toHaveBeenCalledWith(
beforeEach(() => { { ...state.query },
storeUtils.setDataToLS = jest.fn(); window.location.href,
actions.setQuery({ state, commit: jest.fn() }, payload); 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`, () => { it(`setsItem in local storage`, () => {
expect(storeUtils.setDataToLS).toHaveBeenCalledWith(LS_REGEX_HANDLE, expect.anything()); expect(storeUtils.setDataToLS).toHaveBeenCalledWith(LS_REGEX_HANDLE, expect.anything());
});
}); });
}); });
}); });
...@@ -201,7 +286,12 @@ describe('Global Search Store Actions', () => { ...@@ -201,7 +286,12 @@ describe('Global Search Store Actions', () => {
describe('applyQuery', () => { describe('applyQuery', () => {
beforeEach(() => { beforeEach(() => {
setWindowLocation('https://test/'); 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 () => { it('calls visitUrl and setParams with the state.query', async () => {
...@@ -355,17 +445,29 @@ describe('Global Search Store Actions', () => { ...@@ -355,17 +445,29 @@ describe('Global Search Store Actions', () => {
}); });
}); });
describe('fetchSidebarCount uses wild card seach', () => { describe('fetchSidebarCount uses wild card search', () => {
beforeEach(() => { beforeEach(() => {
state.navigation = MOCK_NAVIGATION; state.navigation = noActiveItems;
state.urlQuery.search = ''; 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 () => { it('should use wild card', async () => {
await testAction({ action: actions.fetchSidebarCount, state, expectedMutations: [] }); const commit = jest.fn();
expect(mock.history.get[0].url).toBe('http://test.host/search/count?scope=projects&search=*');
expect(mock.history.get[3].url).toBe( await actions.fetchSidebarCount({ commit, state });
'http://test.host/search/count?scope=merge_requests&search=*',
expect(urlUtils.setUrlParams).toHaveBeenCalledWith(
expect.objectContaining({ search: '*' }),
expect.anything(),
); );
}); });
}); });
...@@ -409,16 +511,16 @@ describe('Global Search Store Actions', () => { ...@@ -409,16 +511,16 @@ describe('Global Search Store Actions', () => {
{ {
payload: { payload: {
key: 'label_name', key: 'label_name',
value: ['Aftersync', 'Brist'], value: ['Aftersync'],
}, },
type: 'SET_QUERY', type: 'SET_QUERY',
}, },
{ {
payload: true, payload: false,
type: 'SET_SIDEBAR_DIRTY', type: 'SET_SIDEBAR_DIRTY',
}, },
]; ];
return testAction(actions.closeLabel, { key: '60' }, state, expectedResult, []); return testAction(actions.closeLabel, { title: 'Brist' }, state, expectedResult, []);
}); });
}); });
......
// rspec spec/frontend/fixtures/search_navigation.rb to generate these files
import subItemActive from 'test_fixtures/search_navigation/sub_item_active.json'; import subItemActive from 'test_fixtures/search_navigation/sub_item_active.json';
import noActiveItems from 'test_fixtures/search_navigation/no_active_items.json'; import noActiveItems from 'test_fixtures/search_navigation/no_active_items.json';
import partialNavigationActive from 'test_fixtures/search_navigation/partial_navigation_active.json'; import partialNavigationActive from 'test_fixtures/search_navigation/partial_navigation_active.json';
...@@ -17,6 +18,7 @@ import { ...@@ -17,6 +18,7 @@ import {
injectRegexSearch, injectRegexSearch,
scopeCrawler, scopeCrawler,
skipBlobESCount, skipBlobESCount,
buildDocumentTitle,
} from '~/search/store/utils'; } from '~/search/store/utils';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
...@@ -447,4 +449,46 @@ describe('Global Search Store Utils', () => { ...@@ -447,4 +449,46 @@ describe('Global Search Store Utils', () => {
expect(skipBlobESCount(state, SCOPE_BLOB)).toBe(false); 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`);
});
});
}); });
...@@ -229,4 +229,48 @@ describe('GlobalSearchTopbar', () => { ...@@ -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',
}),
);
}
});
},
);
});
}); });
...@@ -9,15 +9,16 @@ def fill_in_search(text) ...@@ -9,15 +9,16 @@ def fill_in_search(text)
end end
def submit_search(query) def submit_search(query)
# Forms directly on the search page
if page.has_css?('.search-page-form') if page.has_css?('.search-page-form')
search_form = '.search-page-form' search_form = '.search-page-form'
# Open search modal from super sidebar
else else
find_by_testid('super-sidebar-search-button').click find_by_testid('super-sidebar-search-button').click
search_form = '#super-sidebar-search-modal' search_form = '#super-sidebar-search-modal'
end end
wait_for_all_requests
page.within(search_form) do page.within(search_form) do
field = find_field('search') field = find_field('search')
field.click field.click
......
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册