diff --git a/app/src/components/Search.js b/app/src/components/Search.js index 96abca9a..fde58c6f 100644 --- a/app/src/components/Search.js +++ b/app/src/components/Search.js @@ -131,12 +131,14 @@ export const useSearch = (defaultLimit = 48, totalPages = 0, emptySearch = false } export function useSuggestions(searchStr, tagNames, filter, anyTag) { + // The general groups are null until the suggestion request returns: an empty list means the + // server found none, and the dropdown may say "No results" only for that. const defaultSuggestions = { fileGroups: [], - channels: [], - domains: [], - authors: [], - subjects: [], + channels: null, + domains: null, + authors: null, + subjects: null, zimsEstimates: [], } const [suggestions, setSuggestions] = React.useState(defaultSuggestions); @@ -401,8 +403,8 @@ export function useSearchSuggestions(defaultSearchStr, defaultTagNames, anyTag) zims: zimSum, otherSum: otherSum, mapPlaces: newSuggestions.mapPlaces || 0, - channels: newSuggestions.channels.length, - domains: newSuggestions.domains.length, + channels: newSuggestions.channels?.length, + domains: newSuggestions.domains?.length, authors: newSuggestions.authors?.length, subjects: newSuggestions.subjects?.length, tags: matchingTags?.length, diff --git a/app/src/components/SearchSuggestions.test.js b/app/src/components/SearchSuggestions.test.js new file mode 100644 index 00000000..798f459d --- /dev/null +++ b/app/src/components/SearchSuggestions.test.js @@ -0,0 +1,105 @@ +import React from 'react'; +import {act, render, screen} from '../test-utils'; +import {useSearchSuggestions} from './Search'; +import {TagsContext} from '../Tags'; +import {tagsContextFixture} from '../test-fixtures'; +import { + searchEstimateFiles, searchEstimateMap, searchEstimateOthers, searchEstimateZims, searchSuggestions, +} from '../api'; + +// The global search dropdown's groups are built from useSuggestions. A "Channels: No results" row +// may only appear once the suggestion request has come back empty, never before the user has +// typed, and never while the request is pending. + +jest.mock('../api', () => ({ + ...jest.requireActual('../api'), + searchSuggestions: jest.fn(), + searchEstimateFiles: jest.fn(), + searchEstimateMap: jest.fn(), + searchEstimateOthers: jest.fn(), + searchEstimateZims: jest.fn(), +})); + +const DEBOUNCE = 500; +const pending = () => new Promise(() => { +}); + +let hook; + +function Probe() { + hook = useSearchSuggestions('', [], false); + return
{JSON.stringify(hook.suggestionsResults)}
; +} + +const results = () => JSON.parse(screen.getByTestId('results').textContent); + +const renderProbe = () => render(, { + contexts: [[TagsContext, tagsContextFixture({fuzzyMatchTagsByName: () => []})]], +}); + +beforeEach(() => { + jest.useFakeTimers(); + searchEstimateFiles.mockReturnValue(pending()); + searchEstimateMap.mockReturnValue(pending()); + searchEstimateOthers.mockReturnValue(pending()); + searchEstimateZims.mockReturnValue(pending()); +}); + +afterEach(() => { + jest.useRealTimers(); + jest.clearAllMocks(); +}); + +test('no Channels group before anything has been searched', () => { + searchSuggestions.mockReturnValue(pending()); + renderProbe(); + expect(results().channels).toBeUndefined(); +}); + +test('no Channels group while the suggestion request is pending', () => { + searchSuggestions.mockReturnValue(pending()); + renderProbe(); + act(() => { + hook.setSearchStr('foo'); + }); + expect(results().channels).toBeUndefined(); + + act(() => { + jest.advanceTimersByTime(DEBOUNCE); + }); + expect(searchSuggestions).toHaveBeenCalledWith('foo'); + expect(results().channels).toBeUndefined(); + expect(hook.loading).toBe(true); +}); + +test('a Channels "No results" row once the server said there are none', async () => { + let resolve; + searchSuggestions.mockReturnValue(new Promise(res => resolve = res)); + renderProbe(); + act(() => { + hook.setSearchStr('foo'); + }); + act(() => { + jest.advanceTimersByTime(DEBOUNCE); + }); + await act(async () => { + resolve({channels: [], domains: [], authors: [], subjects: []}); + }); + expect(results().channels.results).toEqual([{title: 'No results'}]); +}); + +test('Channels rows once the server found some', async () => { + let resolve; + searchSuggestions.mockReturnValue(new Promise(res => resolve = res)); + renderProbe(); + act(() => { + hook.setSearchStr('foo'); + }); + act(() => { + jest.advanceTimersByTime(DEBOUNCE); + }); + await act(async () => { + resolve({channels: [{id: 3, name: 'Food'}], domains: [], authors: [], subjects: []}); + }); + expect(results().channels.results.map(i => i.title)).toEqual(['Food']); +});