Commit 8795067b authored by Zack Cuddy's avatar Zack Cuddy

Global Search - Track top nav searches

This change adds logic to track the
group and projects used when searching
via the top nav bar.

This is done via the 'nav_source'
query parameter.

Changelog: changed
parent 88a88c89
...@@ -46,38 +46,44 @@ export const fetchProjects = ({ commit, state }, search) => { ...@@ -46,38 +46,44 @@ export const fetchProjects = ({ commit, state }, search) => {
} }
}; };
export const loadFrequentGroups = async ({ commit }) => { export const preloadStoredFrequentItems = ({ commit }) => {
const data = loadDataFromLS(GROUPS_LOCAL_STORAGE_KEY); const storedGroups = loadDataFromLS(GROUPS_LOCAL_STORAGE_KEY);
commit(types.LOAD_FREQUENT_ITEMS, { key: GROUPS_LOCAL_STORAGE_KEY, data }); commit(types.LOAD_FREQUENT_ITEMS, { key: GROUPS_LOCAL_STORAGE_KEY, data: storedGroups });
const promises = data.map((d) => Api.group(d.id)); const storedProjects = loadDataFromLS(PROJECTS_LOCAL_STORAGE_KEY);
commit(types.LOAD_FREQUENT_ITEMS, { key: PROJECTS_LOCAL_STORAGE_KEY, data: storedProjects });
};
export const loadFrequentGroups = async ({ commit, state }) => {
const storedData = state.frequentItems[GROUPS_LOCAL_STORAGE_KEY];
const promises = storedData.map((d) => Api.group(d.id));
try { try {
const inflatedData = mergeById(await Promise.all(promises), data); const inflatedData = mergeById(await Promise.all(promises), storedData);
commit(types.LOAD_FREQUENT_ITEMS, { key: GROUPS_LOCAL_STORAGE_KEY, data: inflatedData }); commit(types.LOAD_FREQUENT_ITEMS, { key: GROUPS_LOCAL_STORAGE_KEY, data: inflatedData });
} catch { } catch {
createFlash({ message: __('There was a problem fetching recent groups.') }); createFlash({ message: __('There was a problem fetching recent groups.') });
} }
}; };
export const loadFrequentProjects = async ({ commit }) => { export const loadFrequentProjects = async ({ commit, state }) => {
const data = loadDataFromLS(PROJECTS_LOCAL_STORAGE_KEY); const storedData = state.frequentItems[PROJECTS_LOCAL_STORAGE_KEY];
commit(types.LOAD_FREQUENT_ITEMS, { key: PROJECTS_LOCAL_STORAGE_KEY, data }); const promises = storedData.map((d) => Api.project(d.id).then((res) => res.data));
const promises = data.map((d) => Api.project(d.id).then((res) => res.data));
try { try {
const inflatedData = mergeById(await Promise.all(promises), data); const inflatedData = mergeById(await Promise.all(promises), storedData);
commit(types.LOAD_FREQUENT_ITEMS, { key: PROJECTS_LOCAL_STORAGE_KEY, data: inflatedData }); commit(types.LOAD_FREQUENT_ITEMS, { key: PROJECTS_LOCAL_STORAGE_KEY, data: inflatedData });
} catch { } catch {
createFlash({ message: __('There was a problem fetching recent projects.') }); createFlash({ message: __('There was a problem fetching recent projects.') });
} }
}; };
export const setFrequentGroup = ({ state }, item) => { export const setFrequentGroup = ({ state, commit }, item) => {
setFrequentItemToLS(GROUPS_LOCAL_STORAGE_KEY, state.frequentItems, item); const frequentItems = setFrequentItemToLS(GROUPS_LOCAL_STORAGE_KEY, state.frequentItems, item);
commit(types.LOAD_FREQUENT_ITEMS, { key: GROUPS_LOCAL_STORAGE_KEY, data: frequentItems });
}; };
export const setFrequentProject = ({ state }, item) => { export const setFrequentProject = ({ state, commit }, item) => {
setFrequentItemToLS(PROJECTS_LOCAL_STORAGE_KEY, state.frequentItems, item); const frequentItems = setFrequentItemToLS(PROJECTS_LOCAL_STORAGE_KEY, state.frequentItems, item);
commit(types.LOAD_FREQUENT_ITEMS, { key: PROJECTS_LOCAL_STORAGE_KEY, data: frequentItems });
}; };
export const setQuery = ({ commit }, { key, value }) => { export const setQuery = ({ commit }, { key, value }) => {
......
...@@ -21,7 +21,7 @@ export const loadDataFromLS = (key) => { ...@@ -21,7 +21,7 @@ export const loadDataFromLS = (key) => {
export const setFrequentItemToLS = (key, data, itemData) => { export const setFrequentItemToLS = (key, data, itemData) => {
if (!AccessorUtilities.isLocalStorageAccessSafe()) { if (!AccessorUtilities.isLocalStorageAccessSafe()) {
return; return [];
} }
const keyList = [ const keyList = [
...@@ -66,9 +66,11 @@ export const setFrequentItemToLS = (key, data, itemData) => { ...@@ -66,9 +66,11 @@ export const setFrequentItemToLS = (key, data, itemData) => {
// Note we do not need to commit a mutation here as immediately after this we refresh the page to // Note we do not need to commit a mutation here as immediately after this we refresh the page to
// update the search results. // update the search results.
localStorage.setItem(key, JSON.stringify(frequentItems)); localStorage.setItem(key, JSON.stringify(frequentItems));
return frequentItems;
} catch { } catch {
// The LS got in a bad state, let's wipe it // The LS got in a bad state, let's wipe it
localStorage.removeItem(key); localStorage.removeItem(key);
return [];
} }
}; };
......
...@@ -39,8 +39,11 @@ export default { ...@@ -39,8 +39,11 @@ export default {
return !this.query.snippets || this.query.snippets === 'false'; return !this.query.snippets || this.query.snippets === 'false';
}, },
}, },
created() {
this.preloadStoredFrequentItems();
},
methods: { methods: {
...mapActions(['applyQuery', 'setQuery']), ...mapActions(['applyQuery', 'setQuery', 'preloadStoredFrequentItems']),
}, },
}; };
</script> </script>
......
...@@ -18,12 +18,18 @@ export default { ...@@ -18,12 +18,18 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(['groups', 'fetchingGroups']), ...mapState(['query', 'groups', 'fetchingGroups']),
...mapGetters(['frequentGroups']), ...mapGetters(['frequentGroups']),
selectedGroup() { selectedGroup() {
return isEmpty(this.initialData) ? ANY_OPTION : this.initialData; return isEmpty(this.initialData) ? ANY_OPTION : this.initialData;
}, },
}, },
created() {
// This tracks groups searched via the top nav search bar
if (this.query.nav_source === 'navbar' && this.initialData?.id) {
this.setFrequentGroup(this.initialData);
}
},
methods: { methods: {
...mapActions(['fetchGroups', 'setFrequentGroup', 'loadFrequentGroups']), ...mapActions(['fetchGroups', 'setFrequentGroup', 'loadFrequentGroups']),
handleGroupChange(group) { handleGroupChange(group) {
...@@ -33,7 +39,11 @@ export default { ...@@ -33,7 +39,11 @@ export default {
} }
visitUrl( visitUrl(
setUrlParams({ [GROUP_DATA.queryParam]: group.id, [PROJECT_DATA.queryParam]: null }), setUrlParams({
[GROUP_DATA.queryParam]: group.id,
[PROJECT_DATA.queryParam]: null,
nav_source: null,
}),
); );
}, },
}, },
......
...@@ -17,12 +17,18 @@ export default { ...@@ -17,12 +17,18 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(['projects', 'fetchingProjects']), ...mapState(['query', 'projects', 'fetchingProjects']),
...mapGetters(['frequentProjects']), ...mapGetters(['frequentProjects']),
selectedProject() { selectedProject() {
return this.initialData ? this.initialData : ANY_OPTION; return this.initialData ? this.initialData : ANY_OPTION;
}, },
}, },
created() {
// This tracks projects searched via the top nav search bar
if (this.query.nav_source === 'navbar' && this.initialData?.id) {
this.setFrequentProject(this.initialData);
}
},
methods: { methods: {
...mapActions(['fetchProjects', 'setFrequentProject', 'loadFrequentProjects']), ...mapActions(['fetchProjects', 'setFrequentProject', 'loadFrequentProjects']),
handleProjectChange(project) { handleProjectChange(project) {
...@@ -35,6 +41,7 @@ export default { ...@@ -35,6 +41,7 @@ export default {
const queryParams = { const queryParams = {
...(project.namespace?.id && { [GROUP_DATA.queryParam]: project.namespace.id }), ...(project.namespace?.id && { [GROUP_DATA.queryParam]: project.namespace.id }),
[PROJECT_DATA.queryParam]: project.id, [PROJECT_DATA.queryParam]: project.id,
nav_source: null,
}; };
visitUrl(setUrlParams(queryParams)); visitUrl(setUrlParams(queryParams));
......
...@@ -284,8 +284,8 @@ export class SearchAutocomplete { ...@@ -284,8 +284,8 @@ export class SearchAutocomplete {
if (projectId) { if (projectId) {
const projectOptions = gl.projectOptions[getProjectSlug()]; const projectOptions = gl.projectOptions[getProjectSlug()];
const url = groupId const url = groupId
? `${gon.relative_url_root}/search?search=${term}&project_id=${projectId}&group_id=${groupId}` ? `${gon.relative_url_root}/search?search=${term}&project_id=${projectId}&group_id=${groupId}&nav_source=navbar`
: `${gon.relative_url_root}/search?search=${term}&project_id=${projectId}`; : `${gon.relative_url_root}/search?search=${term}&project_id=${projectId}&nav_source=navbar`;
options.push({ options.push({
icon, icon,
...@@ -313,7 +313,7 @@ export class SearchAutocomplete { ...@@ -313,7 +313,7 @@ export class SearchAutocomplete {
}, },
false, false,
), ),
url: `${gon.relative_url_root}/search?search=${term}&group_id=${groupId}`, url: `${gon.relative_url_root}/search?search=${term}&group_id=${groupId}&nav_source=navbar`,
}); });
} }
...@@ -321,7 +321,7 @@ export class SearchAutocomplete { ...@@ -321,7 +321,7 @@ export class SearchAutocomplete {
icon, icon,
text: term, text: term,
template: s__('SearchAutocomplete|in all GitLab'), template: s__('SearchAutocomplete|in all GitLab'),
url: `${gon.relative_url_root}/search?search=${term}`, url: `${gon.relative_url_root}/search?search=${term}&nav_source=navbar`,
}); });
return options; return options;
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
= hidden_field_tag :group_id, params[:group_id] = hidden_field_tag :group_id, params[:group_id]
- if params[:project_id].present? - if params[:project_id].present?
= hidden_field_tag :project_id, params[:project_id] = hidden_field_tag :project_id, params[:project_id]
- group_attributes = @group&.attributes&.slice('id', 'name')&.merge(full_name: @group&.full_name)
- project_attributes = @project&.attributes&.slice('id', 'namespace_id', 'name')&.merge(name_with_namespace: @project&.name_with_namespace) - project_attributes = @project&.attributes&.slice('id', 'namespace_id', 'name')&.merge(name_with_namespace: @project&.name_with_namespace)
- if @search_results - if @search_results
...@@ -16,7 +17,7 @@ ...@@ -16,7 +17,7 @@
= render_if_exists 'search/form_elasticsearch', attrs: { class: 'mb-2 mb-sm-0 align-self-center' } = render_if_exists 'search/form_elasticsearch', attrs: { class: 'mb-2 mb-sm-0 align-self-center' }
.gl-mt-3 .gl-mt-3
#js-search-topbar{ data: { "group-initial-data": @group.to_json, "project-initial-data": project_attributes.to_json } } #js-search-topbar{ data: { "group-initial-data": group_attributes.to_json, "project-initial-data": project_attributes.to_json } }
- if @search_term - if @search_term
= render 'search/category' = render 'search/category'
= render 'search/results' = render 'search/results'
...@@ -254,6 +254,7 @@ RSpec.describe 'User uses header search field', :js do ...@@ -254,6 +254,7 @@ RSpec.describe 'User uses header search field', :js do
href = search_path(search: term) href = search_path(search: term)
href.concat("&project_id=#{project_id}") if project_id href.concat("&project_id=#{project_id}") if project_id
href.concat("&group_id=#{group_id}") if group_id href.concat("&group_id=#{group_id}") if group_id
href.concat("&nav_source=navbar")
".dropdown a[href='#{href}']" ".dropdown a[href='#{href}']"
end end
......
...@@ -86,18 +86,21 @@ export const STALE_STORED_DATA = [ ...@@ -86,18 +86,21 @@ export const STALE_STORED_DATA = [
export const MOCK_FRESH_DATA_RES = { name: 'fresh' }; export const MOCK_FRESH_DATA_RES = { name: 'fresh' };
export const PROMISE_ALL_EXPECTED_MUTATIONS = { export const PRELOAD_EXPECTED_MUTATIONS = [
initGroups: { {
type: types.LOAD_FREQUENT_ITEMS, type: types.LOAD_FREQUENT_ITEMS,
payload: { key: GROUPS_LOCAL_STORAGE_KEY, data: FRESH_STORED_DATA }, payload: { key: GROUPS_LOCAL_STORAGE_KEY, data: FRESH_STORED_DATA },
}, },
resGroups: { {
type: types.LOAD_FREQUENT_ITEMS, type: types.LOAD_FREQUENT_ITEMS,
payload: { key: GROUPS_LOCAL_STORAGE_KEY, data: [MOCK_FRESH_DATA_RES, MOCK_FRESH_DATA_RES] }, payload: { key: PROJECTS_LOCAL_STORAGE_KEY, data: FRESH_STORED_DATA },
}, },
initProjects: { ];
export const PROMISE_ALL_EXPECTED_MUTATIONS = {
resGroups: {
type: types.LOAD_FREQUENT_ITEMS, type: types.LOAD_FREQUENT_ITEMS,
payload: { key: PROJECTS_LOCAL_STORAGE_KEY, data: FRESH_STORED_DATA }, payload: { key: GROUPS_LOCAL_STORAGE_KEY, data: [MOCK_FRESH_DATA_RES, MOCK_FRESH_DATA_RES] },
}, },
resProjects: { resProjects: {
type: types.LOAD_FREQUENT_ITEMS, type: types.LOAD_FREQUENT_ITEMS,
......
...@@ -17,6 +17,7 @@ import { ...@@ -17,6 +17,7 @@ import {
MOCK_GROUP, MOCK_GROUP,
FRESH_STORED_DATA, FRESH_STORED_DATA,
MOCK_FRESH_DATA_RES, MOCK_FRESH_DATA_RES,
PRELOAD_EXPECTED_MUTATIONS,
PROMISE_ALL_EXPECTED_MUTATIONS, PROMISE_ALL_EXPECTED_MUTATIONS,
} from '../mock_data'; } from '../mock_data';
...@@ -68,31 +69,31 @@ describe('Global Search Store Actions', () => { ...@@ -68,31 +69,31 @@ describe('Global Search Store Actions', () => {
}); });
describe.each` describe.each`
action | axiosMock | type | expectedMutations | flashCallCount | lsKey action | axiosMock | type | expectedMutations | flashCallCount
${actions.loadFrequentGroups} | ${{ method: 'onGet', code: 200 }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.initGroups, PROMISE_ALL_EXPECTED_MUTATIONS.resGroups]} | ${0} | ${GROUPS_LOCAL_STORAGE_KEY} ${actions.loadFrequentGroups} | ${{ method: 'onGet', code: 200 }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.resGroups]} | ${0}
${actions.loadFrequentGroups} | ${{ method: 'onGet', code: 500 }} | ${'error'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.initGroups]} | ${1} | ${GROUPS_LOCAL_STORAGE_KEY} ${actions.loadFrequentGroups} | ${{ method: 'onGet', code: 500 }} | ${'error'} | ${[]} | ${1}
${actions.loadFrequentProjects} | ${{ method: 'onGet', code: 200 }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.initProjects, PROMISE_ALL_EXPECTED_MUTATIONS.resProjects]} | ${0} | ${PROJECTS_LOCAL_STORAGE_KEY} ${actions.loadFrequentProjects} | ${{ method: 'onGet', code: 200 }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.resProjects]} | ${0}
${actions.loadFrequentProjects} | ${{ method: 'onGet', code: 500 }} | ${'error'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.initProjects]} | ${1} | ${PROJECTS_LOCAL_STORAGE_KEY} ${actions.loadFrequentProjects} | ${{ method: 'onGet', code: 500 }} | ${'error'} | ${[]} | ${1}
`( `('Promise.all calls', ({ action, axiosMock, type, expectedMutations, flashCallCount }) => {
'Promise.all calls',
({ action, axiosMock, type, expectedMutations, flashCallCount, lsKey }) => {
describe(action.name, () => { describe(action.name, () => {
describe(`on ${type}`, () => { describe(`on ${type}`, () => {
beforeEach(() => { beforeEach(() => {
storeUtils.loadDataFromLS = jest.fn().mockReturnValue(FRESH_STORED_DATA); state.frequentItems = {
[GROUPS_LOCAL_STORAGE_KEY]: FRESH_STORED_DATA,
[PROJECTS_LOCAL_STORAGE_KEY]: FRESH_STORED_DATA,
};
mock[axiosMock.method]().reply(axiosMock.code, MOCK_FRESH_DATA_RES); mock[axiosMock.method]().reply(axiosMock.code, MOCK_FRESH_DATA_RES);
}); });
it(`should dispatch the correct mutations`, () => { it(`should dispatch the correct mutations`, () => {
return testAction({ action, state, expectedMutations }).then(() => { return testAction({ action, state, expectedMutations }).then(() => {
expect(storeUtils.loadDataFromLS).toHaveBeenCalledWith(lsKey);
flashCallback(flashCallCount); flashCallback(flashCallCount);
}); });
}); });
}); });
}); });
}, });
);
describe('getGroupsData', () => { describe('getGroupsData', () => {
const mockCommit = () => {}; const mockCommit = () => {};
...@@ -182,14 +183,38 @@ describe('Global Search Store Actions', () => { ...@@ -182,14 +183,38 @@ describe('Global Search Store Actions', () => {
}); });
}); });
describe('preloadStoredFrequentItems', () => {
beforeEach(() => {
storeUtils.loadDataFromLS = jest.fn().mockReturnValue(FRESH_STORED_DATA);
});
it('calls preloadStoredFrequentItems for both groups and projects and commits LOAD_FREQUENT_ITEMS', async () => {
await testAction({
action: actions.preloadStoredFrequentItems,
state,
expectedMutations: PRELOAD_EXPECTED_MUTATIONS,
});
expect(storeUtils.loadDataFromLS).toHaveBeenCalledTimes(2);
expect(storeUtils.loadDataFromLS).toHaveBeenCalledWith(GROUPS_LOCAL_STORAGE_KEY);
expect(storeUtils.loadDataFromLS).toHaveBeenCalledWith(PROJECTS_LOCAL_STORAGE_KEY);
});
});
describe('setFrequentGroup', () => { describe('setFrequentGroup', () => {
beforeEach(() => { beforeEach(() => {
storeUtils.setFrequentItemToLS = jest.fn(); storeUtils.setFrequentItemToLS = jest.fn().mockReturnValue(FRESH_STORED_DATA);
}); });
it(`calls setFrequentItemToLS with ${GROUPS_LOCAL_STORAGE_KEY} and item data`, async () => { it(`calls setFrequentItemToLS with ${GROUPS_LOCAL_STORAGE_KEY} and item data then commits LOAD_FREQUENT_ITEMS`, async () => {
await testAction({ await testAction({
action: actions.setFrequentGroup, action: actions.setFrequentGroup,
expectedMutations: [
{
type: types.LOAD_FREQUENT_ITEMS,
payload: { key: GROUPS_LOCAL_STORAGE_KEY, data: FRESH_STORED_DATA },
},
],
payload: MOCK_GROUP, payload: MOCK_GROUP,
state, state,
}); });
...@@ -204,12 +229,18 @@ describe('Global Search Store Actions', () => { ...@@ -204,12 +229,18 @@ describe('Global Search Store Actions', () => {
describe('setFrequentProject', () => { describe('setFrequentProject', () => {
beforeEach(() => { beforeEach(() => {
storeUtils.setFrequentItemToLS = jest.fn(); storeUtils.setFrequentItemToLS = jest.fn().mockReturnValue(FRESH_STORED_DATA);
}); });
it(`calls setFrequentItemToLS with ${PROJECTS_LOCAL_STORAGE_KEY} and item data`, async () => { it(`calls setFrequentItemToLS with ${PROJECTS_LOCAL_STORAGE_KEY} and item data`, async () => {
await testAction({ await testAction({
action: actions.setFrequentProject, action: actions.setFrequentProject,
expectedMutations: [
{
type: types.LOAD_FREQUENT_ITEMS,
payload: { key: PROJECTS_LOCAL_STORAGE_KEY, data: FRESH_STORED_DATA },
},
],
payload: MOCK_PROJECT, payload: MOCK_PROJECT,
state, state,
}); });
......
...@@ -51,19 +51,25 @@ describe('Global Search Store Utils', () => { ...@@ -51,19 +51,25 @@ describe('Global Search Store Utils', () => {
describe('setFrequentItemToLS', () => { describe('setFrequentItemToLS', () => {
const frequentItems = {}; const frequentItems = {};
let res;
describe('with existing data', () => { describe('with existing data', () => {
describe(`when frequency is less than ${MAX_FREQUENCY}`, () => { describe(`when frequency is less than ${MAX_FREQUENCY}`, () => {
beforeEach(() => { beforeEach(() => {
frequentItems[MOCK_LS_KEY] = [{ ...MOCK_GROUPS[0], frequency: 1, lastUsed: PREV_TIME }]; frequentItems[MOCK_LS_KEY] = [{ ...MOCK_GROUPS[0], frequency: 1, lastUsed: PREV_TIME }];
setFrequentItemToLS(MOCK_LS_KEY, frequentItems, MOCK_GROUPS[0]); res = setFrequentItemToLS(MOCK_LS_KEY, frequentItems, MOCK_GROUPS[0]);
}); });
it('adds 1 to the frequency, tracks lastUsed, and calls localStorage.setItem', () => { it('adds 1 to the frequency, tracks lastUsed, calls localStorage.setItem and returns the array', () => {
const updatedFrequentItems = [
{ ...MOCK_GROUPS[0], frequency: 2, lastUsed: CURRENT_TIME },
];
expect(localStorage.setItem).toHaveBeenCalledWith( expect(localStorage.setItem).toHaveBeenCalledWith(
MOCK_LS_KEY, MOCK_LS_KEY,
JSON.stringify([{ ...MOCK_GROUPS[0], frequency: 2, lastUsed: CURRENT_TIME }]), JSON.stringify(updatedFrequentItems),
); );
expect(res).toEqual(updatedFrequentItems);
}); });
}); });
...@@ -72,16 +78,19 @@ describe('Global Search Store Utils', () => { ...@@ -72,16 +78,19 @@ describe('Global Search Store Utils', () => {
frequentItems[MOCK_LS_KEY] = [ frequentItems[MOCK_LS_KEY] = [
{ ...MOCK_GROUPS[0], frequency: MAX_FREQUENCY, lastUsed: PREV_TIME }, { ...MOCK_GROUPS[0], frequency: MAX_FREQUENCY, lastUsed: PREV_TIME },
]; ];
setFrequentItemToLS(MOCK_LS_KEY, frequentItems, MOCK_GROUPS[0]); res = setFrequentItemToLS(MOCK_LS_KEY, frequentItems, MOCK_GROUPS[0]);
}); });
it(`does not further increase frequency past ${MAX_FREQUENCY}, tracks lastUsed, and calls localStorage.setItem`, () => { it(`does not further increase frequency past ${MAX_FREQUENCY}, tracks lastUsed, calls localStorage.setItem, and returns the array`, () => {
const updatedFrequentItems = [
{ ...MOCK_GROUPS[0], frequency: MAX_FREQUENCY, lastUsed: CURRENT_TIME },
];
expect(localStorage.setItem).toHaveBeenCalledWith( expect(localStorage.setItem).toHaveBeenCalledWith(
MOCK_LS_KEY, MOCK_LS_KEY,
JSON.stringify([ JSON.stringify(updatedFrequentItems),
{ ...MOCK_GROUPS[0], frequency: MAX_FREQUENCY, lastUsed: CURRENT_TIME },
]),
); );
expect(res).toEqual(updatedFrequentItems);
}); });
}); });
}); });
...@@ -89,14 +98,17 @@ describe('Global Search Store Utils', () => { ...@@ -89,14 +98,17 @@ describe('Global Search Store Utils', () => {
describe('with no existing data', () => { describe('with no existing data', () => {
beforeEach(() => { beforeEach(() => {
frequentItems[MOCK_LS_KEY] = []; frequentItems[MOCK_LS_KEY] = [];
setFrequentItemToLS(MOCK_LS_KEY, frequentItems, MOCK_GROUPS[0]); res = setFrequentItemToLS(MOCK_LS_KEY, frequentItems, MOCK_GROUPS[0]);
}); });
it('adds a new entry with frequency 1, tracks lastUsed, and calls localStorage.setItem', () => { it('adds a new entry with frequency 1, tracks lastUsed, calls localStorage.setItem, and returns the array', () => {
const updatedFrequentItems = [{ ...MOCK_GROUPS[0], frequency: 1, lastUsed: CURRENT_TIME }];
expect(localStorage.setItem).toHaveBeenCalledWith( expect(localStorage.setItem).toHaveBeenCalledWith(
MOCK_LS_KEY, MOCK_LS_KEY,
JSON.stringify([{ ...MOCK_GROUPS[0], frequency: 1, lastUsed: CURRENT_TIME }]), JSON.stringify(updatedFrequentItems),
); );
expect(res).toEqual(updatedFrequentItems);
}); });
}); });
...@@ -107,18 +119,21 @@ describe('Global Search Store Utils', () => { ...@@ -107,18 +119,21 @@ describe('Global Search Store Utils', () => {
{ id: 2, frequency: 1, lastUsed: PREV_TIME }, { id: 2, frequency: 1, lastUsed: PREV_TIME },
{ id: 3, frequency: 1, lastUsed: PREV_TIME }, { id: 3, frequency: 1, lastUsed: PREV_TIME },
]; ];
setFrequentItemToLS(MOCK_LS_KEY, frequentItems, { id: 3 }); res = setFrequentItemToLS(MOCK_LS_KEY, frequentItems, { id: 3 });
}); });
it('sorts the array by most frequent and lastUsed', () => { it('sorts the array by most frequent and lastUsed and returns the array', () => {
expect(localStorage.setItem).toHaveBeenCalledWith( const updatedFrequentItems = [
MOCK_LS_KEY,
JSON.stringify([
{ id: 3, frequency: 2, lastUsed: CURRENT_TIME }, { id: 3, frequency: 2, lastUsed: CURRENT_TIME },
{ id: 1, frequency: 2, lastUsed: PREV_TIME }, { id: 1, frequency: 2, lastUsed: PREV_TIME },
{ id: 2, frequency: 1, lastUsed: PREV_TIME }, { id: 2, frequency: 1, lastUsed: PREV_TIME },
]), ];
expect(localStorage.setItem).toHaveBeenCalledWith(
MOCK_LS_KEY,
JSON.stringify(updatedFrequentItems),
); );
expect(res).toEqual(updatedFrequentItems);
}); });
}); });
...@@ -131,31 +146,35 @@ describe('Global Search Store Utils', () => { ...@@ -131,31 +146,35 @@ describe('Global Search Store Utils', () => {
{ id: 4, frequency: 2, lastUsed: PREV_TIME }, { id: 4, frequency: 2, lastUsed: PREV_TIME },
{ id: 5, frequency: 1, lastUsed: PREV_TIME }, { id: 5, frequency: 1, lastUsed: PREV_TIME },
]; ];
setFrequentItemToLS(MOCK_LS_KEY, frequentItems, { id: 6 }); res = setFrequentItemToLS(MOCK_LS_KEY, frequentItems, { id: 6 });
}); });
it('removes the last item in the array', () => { it('removes the last item in the array and returns the array', () => {
expect(localStorage.setItem).toHaveBeenCalledWith( const updatedFrequentItems = [
MOCK_LS_KEY,
JSON.stringify([
{ id: 1, frequency: 5, lastUsed: PREV_TIME }, { id: 1, frequency: 5, lastUsed: PREV_TIME },
{ id: 2, frequency: 4, lastUsed: PREV_TIME }, { id: 2, frequency: 4, lastUsed: PREV_TIME },
{ id: 3, frequency: 3, lastUsed: PREV_TIME }, { id: 3, frequency: 3, lastUsed: PREV_TIME },
{ id: 4, frequency: 2, lastUsed: PREV_TIME }, { id: 4, frequency: 2, lastUsed: PREV_TIME },
{ id: 6, frequency: 1, lastUsed: CURRENT_TIME }, { id: 6, frequency: 1, lastUsed: CURRENT_TIME },
]), ];
expect(localStorage.setItem).toHaveBeenCalledWith(
MOCK_LS_KEY,
JSON.stringify(updatedFrequentItems),
); );
expect(res).toEqual(updatedFrequentItems);
}); });
}); });
describe('with null data loaded in', () => { describe('with null data loaded in', () => {
beforeEach(() => { beforeEach(() => {
frequentItems[MOCK_LS_KEY] = null; frequentItems[MOCK_LS_KEY] = null;
setFrequentItemToLS(MOCK_LS_KEY, frequentItems, MOCK_GROUPS[0]); res = setFrequentItemToLS(MOCK_LS_KEY, frequentItems, MOCK_GROUPS[0]);
}); });
it('wipes local storage', () => { it('wipes local storage and returns empty array', () => {
expect(localStorage.removeItem).toHaveBeenCalledWith(MOCK_LS_KEY); expect(localStorage.removeItem).toHaveBeenCalledWith(MOCK_LS_KEY);
expect(res).toEqual([]);
}); });
}); });
...@@ -163,14 +182,17 @@ describe('Global Search Store Utils', () => { ...@@ -163,14 +182,17 @@ describe('Global Search Store Utils', () => {
beforeEach(() => { beforeEach(() => {
const MOCK_ADDITIONAL_DATA_GROUP = { ...MOCK_GROUPS[0], extraData: 'test' }; const MOCK_ADDITIONAL_DATA_GROUP = { ...MOCK_GROUPS[0], extraData: 'test' };
frequentItems[MOCK_LS_KEY] = []; frequentItems[MOCK_LS_KEY] = [];
setFrequentItemToLS(MOCK_LS_KEY, frequentItems, MOCK_ADDITIONAL_DATA_GROUP); res = setFrequentItemToLS(MOCK_LS_KEY, frequentItems, MOCK_ADDITIONAL_DATA_GROUP);
}); });
it('parses out extra data for LS', () => { it('parses out extra data for LS and returns the array', () => {
const updatedFrequentItems = [{ ...MOCK_GROUPS[0], frequency: 1, lastUsed: CURRENT_TIME }];
expect(localStorage.setItem).toHaveBeenCalledWith( expect(localStorage.setItem).toHaveBeenCalledWith(
MOCK_LS_KEY, MOCK_LS_KEY,
JSON.stringify([{ ...MOCK_GROUPS[0], frequency: 1, lastUsed: CURRENT_TIME }]), JSON.stringify(updatedFrequentItems),
); );
expect(res).toEqual(updatedFrequentItems);
}); });
}); });
}); });
......
import { GlForm, GlSearchBoxByType, GlButton } from '@gitlab/ui'; import { GlForm, GlSearchBoxByType, GlButton } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { MOCK_QUERY } from 'jest/search/mock_data'; import { MOCK_QUERY } from 'jest/search/mock_data';
import GlobalSearchTopbar from '~/search/topbar/components/app.vue'; import GlobalSearchTopbar from '~/search/topbar/components/app.vue';
import GroupFilter from '~/search/topbar/components/group_filter.vue'; import GroupFilter from '~/search/topbar/components/group_filter.vue';
import ProjectFilter from '~/search/topbar/components/project_filter.vue'; import ProjectFilter from '~/search/topbar/components/project_filter.vue';
const localVue = createLocalVue(); Vue.use(Vuex);
localVue.use(Vuex);
describe('GlobalSearchTopbar', () => { describe('GlobalSearchTopbar', () => {
let wrapper; let wrapper;
...@@ -15,6 +15,7 @@ describe('GlobalSearchTopbar', () => { ...@@ -15,6 +15,7 @@ describe('GlobalSearchTopbar', () => {
const actionSpies = { const actionSpies = {
applyQuery: jest.fn(), applyQuery: jest.fn(),
setQuery: jest.fn(), setQuery: jest.fn(),
preloadStoredFrequentItems: jest.fn(),
}; };
const createComponent = (initialState) => { const createComponent = (initialState) => {
...@@ -27,14 +28,12 @@ describe('GlobalSearchTopbar', () => { ...@@ -27,14 +28,12 @@ describe('GlobalSearchTopbar', () => {
}); });
wrapper = shallowMount(GlobalSearchTopbar, { wrapper = shallowMount(GlobalSearchTopbar, {
localVue,
store, store,
}); });
}; };
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
const findTopbarForm = () => wrapper.find(GlForm); const findTopbarForm = () => wrapper.find(GlForm);
...@@ -110,4 +109,14 @@ describe('GlobalSearchTopbar', () => { ...@@ -110,4 +109,14 @@ describe('GlobalSearchTopbar', () => {
expect(actionSpies.applyQuery).toHaveBeenCalled(); expect(actionSpies.applyQuery).toHaveBeenCalled();
}); });
}); });
describe('onCreate', () => {
beforeEach(() => {
createComponent();
});
it('calls preloadStoredFrequentItems', () => {
expect(actionSpies.preloadStoredFrequentItems).toHaveBeenCalled();
});
});
}); });
...@@ -51,7 +51,6 @@ describe('GroupFilter', () => { ...@@ -51,7 +51,6 @@ describe('GroupFilter', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
const findSearchableDropdown = () => wrapper.find(SearchableDropdown); const findSearchableDropdown = () => wrapper.find(SearchableDropdown);
...@@ -89,10 +88,11 @@ describe('GroupFilter', () => { ...@@ -89,10 +88,11 @@ describe('GroupFilter', () => {
findSearchableDropdown().vm.$emit('change', ANY_OPTION); findSearchableDropdown().vm.$emit('change', ANY_OPTION);
}); });
it('calls setUrlParams with group null, project id null, and then calls visitUrl', () => { it('calls setUrlParams with group null, project id null, nav_source null, and then calls visitUrl', () => {
expect(setUrlParams).toHaveBeenCalledWith({ expect(setUrlParams).toHaveBeenCalledWith({
[GROUP_DATA.queryParam]: null, [GROUP_DATA.queryParam]: null,
[PROJECT_DATA.queryParam]: null, [PROJECT_DATA.queryParam]: null,
nav_source: null,
}); });
expect(visitUrl).toHaveBeenCalled(); expect(visitUrl).toHaveBeenCalled();
...@@ -108,10 +108,11 @@ describe('GroupFilter', () => { ...@@ -108,10 +108,11 @@ describe('GroupFilter', () => {
findSearchableDropdown().vm.$emit('change', MOCK_GROUP); findSearchableDropdown().vm.$emit('change', MOCK_GROUP);
}); });
it('calls setUrlParams with group id, project id null, and then calls visitUrl', () => { it('calls setUrlParams with group id, project id null, nav_source null, and then calls visitUrl', () => {
expect(setUrlParams).toHaveBeenCalledWith({ expect(setUrlParams).toHaveBeenCalledWith({
[GROUP_DATA.queryParam]: MOCK_GROUP.id, [GROUP_DATA.queryParam]: MOCK_GROUP.id,
[PROJECT_DATA.queryParam]: null, [PROJECT_DATA.queryParam]: null,
nav_source: null,
}); });
expect(visitUrl).toHaveBeenCalled(); expect(visitUrl).toHaveBeenCalled();
...@@ -156,4 +157,31 @@ describe('GroupFilter', () => { ...@@ -156,4 +157,31 @@ describe('GroupFilter', () => {
}); });
}); });
}); });
describe.each`
navSource | initialData | callMethod
${null} | ${null} | ${false}
${null} | ${MOCK_GROUP} | ${false}
${'navbar'} | ${null} | ${false}
${'navbar'} | ${MOCK_GROUP} | ${true}
`('onCreate', ({ navSource, initialData, callMethod }) => {
describe(`when nav_source is ${navSource} and ${
initialData ? 'has' : 'does not have'
} an initial group`, () => {
beforeEach(() => {
createComponent({ query: { ...MOCK_QUERY, nav_source: navSource } }, { initialData });
});
it(`${callMethod ? 'does' : 'does not'} call setFrequentGroup`, () => {
if (callMethod) {
expect(actionSpies.setFrequentGroup).toHaveBeenCalledWith(
expect.any(Object),
initialData,
);
} else {
expect(actionSpies.setFrequentGroup).not.toHaveBeenCalled();
}
});
});
});
}); });
...@@ -51,7 +51,6 @@ describe('ProjectFilter', () => { ...@@ -51,7 +51,6 @@ describe('ProjectFilter', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
const findSearchableDropdown = () => wrapper.find(SearchableDropdown); const findSearchableDropdown = () => wrapper.find(SearchableDropdown);
...@@ -89,9 +88,10 @@ describe('ProjectFilter', () => { ...@@ -89,9 +88,10 @@ describe('ProjectFilter', () => {
findSearchableDropdown().vm.$emit('change', ANY_OPTION); findSearchableDropdown().vm.$emit('change', ANY_OPTION);
}); });
it('calls setUrlParams with null, no group id, then calls visitUrl', () => { it('calls setUrlParams with null, no group id, nav_source null, then calls visitUrl', () => {
expect(setUrlParams).toHaveBeenCalledWith({ expect(setUrlParams).toHaveBeenCalledWith({
[PROJECT_DATA.queryParam]: null, [PROJECT_DATA.queryParam]: null,
nav_source: null,
}); });
expect(visitUrl).toHaveBeenCalled(); expect(visitUrl).toHaveBeenCalled();
}); });
...@@ -106,10 +106,11 @@ describe('ProjectFilter', () => { ...@@ -106,10 +106,11 @@ describe('ProjectFilter', () => {
findSearchableDropdown().vm.$emit('change', MOCK_PROJECT); findSearchableDropdown().vm.$emit('change', MOCK_PROJECT);
}); });
it('calls setUrlParams with project id, group id, then calls visitUrl', () => { it('calls setUrlParams with project id, group id, nav_source null, then calls visitUrl', () => {
expect(setUrlParams).toHaveBeenCalledWith({ expect(setUrlParams).toHaveBeenCalledWith({
[GROUP_DATA.queryParam]: MOCK_PROJECT.namespace.id, [GROUP_DATA.queryParam]: MOCK_PROJECT.namespace.id,
[PROJECT_DATA.queryParam]: MOCK_PROJECT.id, [PROJECT_DATA.queryParam]: MOCK_PROJECT.id,
nav_source: null,
}); });
expect(visitUrl).toHaveBeenCalled(); expect(visitUrl).toHaveBeenCalled();
}); });
...@@ -157,4 +158,31 @@ describe('ProjectFilter', () => { ...@@ -157,4 +158,31 @@ describe('ProjectFilter', () => {
}); });
}); });
}); });
describe.each`
navSource | initialData | callMethod
${null} | ${null} | ${false}
${null} | ${MOCK_PROJECT} | ${false}
${'navbar'} | ${null} | ${false}
${'navbar'} | ${MOCK_PROJECT} | ${true}
`('onCreate', ({ navSource, initialData, callMethod }) => {
describe(`when nav_source is ${navSource} and ${
initialData ? 'has' : 'does not have'
} an initial project`, () => {
beforeEach(() => {
createComponent({ query: { ...MOCK_QUERY, nav_source: navSource } }, { initialData });
});
it(`${callMethod ? 'does' : 'does not'} call setFrequentProject`, () => {
if (callMethod) {
expect(actionSpies.setFrequentProject).toHaveBeenCalledWith(
expect.any(Object),
initialData,
);
} else {
expect(actionSpies.setFrequentProject).not.toHaveBeenCalled();
}
});
});
});
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment