mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-25 18:40:06 -04:00
app: search dropdown offers "Channels: No results" only after a search
useSuggestions started with channels: [] and reset to that on every keystroke, and the normalizer treats an empty list as "the server found none". So the global search dropdown showed a "Channels: No results" row on focus before anything was typed, and on every keystroke through the debounce and the request. Because that row counts as a result, the SearchBox's own "Searching…" state never appeared. The channel, domain, author and subject groups now start as null, meaning not fetched; an empty list only ever comes from the server. The count summary tolerates the null. Tests render the hook and check no Channels group exists before a search or while the request is pending, and that the "No results" row and real rows still appear after the response. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
9caa140fb6
commit
70d335a977
2 changed files with 113 additions and 6 deletions
|
|
@ -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,
|
||||
|
|
|
|||
105
app/src/components/SearchSuggestions.test.js
Normal file
105
app/src/components/SearchSuggestions.test.js
Normal file
|
|
@ -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 <pre data-testid='results'>{JSON.stringify(hook.suggestionsResults)}</pre>;
|
||||
}
|
||||
|
||||
const results = () => JSON.parse(screen.getByTestId('results').textContent);
|
||||
|
||||
const renderProbe = () => render(<Probe/>, {
|
||||
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']);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue