diff --git a/web/src/components/GlobalSearch.test.tsx b/web/src/components/GlobalSearch.test.tsx index 9758a0a2..ca67ae5d 100644 --- a/web/src/components/GlobalSearch.test.tsx +++ b/web/src/components/GlobalSearch.test.tsx @@ -94,7 +94,11 @@ describe("GlobalSearch", () => { mocks.useQuery.mockReset(); mocks.useCanRequest.mockReset(); mocks.useRequestSearch.mockReset(); - mocks.useCanRequest.mockReturnValue({ discoveryEnabled: false, submitDisabledReason: null }); + mocks.useCanRequest.mockReturnValue({ + discoveryEnabled: false, + isResolving: false, + submitDisabledReason: null, + }); mocks.useRequestSearch.mockReturnValue({ data: undefined, isLoading: false, @@ -145,7 +149,11 @@ describe("GlobalSearch + RequestToAddSection wiring", () => { mocks.useQuery.mockReset(); mocks.useCanRequest.mockReset(); mocks.useRequestSearch.mockReset(); - mocks.useCanRequest.mockReturnValue({ discoveryEnabled: false, submitDisabledReason: null }); + mocks.useCanRequest.mockReturnValue({ + discoveryEnabled: false, + isResolving: false, + submitDisabledReason: null, + }); mocks.useRequestSearch.mockReturnValue({ data: undefined, isLoading: false, @@ -159,7 +167,11 @@ describe("GlobalSearch + RequestToAddSection wiring", () => { }); it("renders the section with libraryHadHits=true when library returned results", () => { - mocks.useCanRequest.mockReturnValue({ discoveryEnabled: true, submitDisabledReason: null }); + mocks.useCanRequest.mockReturnValue({ + discoveryEnabled: true, + isResolving: false, + submitDisabledReason: null, + }); mocks.useRequestSearch.mockReturnValue({ data: { page: 1, @@ -186,7 +198,11 @@ describe("GlobalSearch + RequestToAddSection wiring", () => { }); it("renders the section with libraryHadHits=false when library returned 0 results", () => { - mocks.useCanRequest.mockReturnValue({ discoveryEnabled: true, submitDisabledReason: null }); + mocks.useCanRequest.mockReturnValue({ + discoveryEnabled: true, + isResolving: false, + submitDisabledReason: null, + }); mocks.useQuery.mockReturnValue({ data: { total: 0, has_more: false, items: [] }, isFetching: false, @@ -216,7 +232,11 @@ describe("GlobalSearch + RequestToAddSection wiring", () => { }); it("does not call useRequestSearch with enabled=true when discoveryEnabled is false", () => { - mocks.useCanRequest.mockReturnValue({ discoveryEnabled: false, submitDisabledReason: null }); + mocks.useCanRequest.mockReturnValue({ + discoveryEnabled: false, + isResolving: false, + submitDisabledReason: null, + }); renderSearchMarkup({ defaultOpen: true, initialQuery: "Dune" }); const call = mocks.useRequestSearch.mock.calls[mocks.useRequestSearch.mock.calls.length - 1]; @@ -224,14 +244,22 @@ describe("GlobalSearch + RequestToAddSection wiring", () => { }); it("does not mount RequestToAddSection when discovery is disabled", () => { - mocks.useCanRequest.mockReturnValue({ discoveryEnabled: false, submitDisabledReason: null }); + mocks.useCanRequest.mockReturnValue({ + discoveryEnabled: false, + isResolving: false, + submitDisabledReason: null, + }); const markup = renderSearchMarkup({ defaultOpen: true, initialQuery: "Dune" }); expect(markup).not.toContain('data-testid="request-section"'); }); it("suppresses 'No matches' when library is empty and TMDB is still loading", () => { - mocks.useCanRequest.mockReturnValue({ discoveryEnabled: true, submitDisabledReason: null }); + mocks.useCanRequest.mockReturnValue({ + discoveryEnabled: true, + isResolving: false, + submitDisabledReason: null, + }); mocks.useQuery.mockReturnValue({ data: { total: 0, has_more: false, items: [] }, isFetching: false, @@ -248,7 +276,11 @@ describe("GlobalSearch + RequestToAddSection wiring", () => { }); it("suppresses 'No matches' when library is empty and TMDB has missing results", () => { - mocks.useCanRequest.mockReturnValue({ discoveryEnabled: true, submitDisabledReason: null }); + mocks.useCanRequest.mockReturnValue({ + discoveryEnabled: true, + isResolving: false, + submitDisabledReason: null, + }); mocks.useQuery.mockReturnValue({ data: { total: 0, has_more: false, items: [] }, isFetching: false, @@ -278,7 +310,11 @@ describe("GlobalSearch + RequestToAddSection wiring", () => { }); it("still shows 'No matches' when both library and TMDB are empty", () => { - mocks.useCanRequest.mockReturnValue({ discoveryEnabled: true, submitDisabledReason: null }); + mocks.useCanRequest.mockReturnValue({ + discoveryEnabled: true, + isResolving: false, + submitDisabledReason: null, + }); mocks.useQuery.mockReturnValue({ data: { total: 0, has_more: false, items: [] }, isFetching: false, diff --git a/web/src/components/GlobalSearch.tsx b/web/src/components/GlobalSearch.tsx index 9d910a55..5eaf3f80 100644 --- a/web/src/components/GlobalSearch.tsx +++ b/web/src/components/GlobalSearch.tsx @@ -111,9 +111,15 @@ export function GlobalSearch({ }); const tmdbMissingCount = tmdbQuery.data?.results?.filter((result) => result.availability !== "available").length ?? 0; + // Cap at DIALOG_LIMIT (4) — RequestToAddSection slices results to that many rows. + const tmdbVisibleCount = Math.min(tmdbMissingCount, 4); const tmdbStillLoading = canRequest.discoveryEnabled && tmdbDebouncedQuery.length > 1 && tmdbQuery.isLoading; const tmdbWillRender = canRequest.discoveryEnabled && tmdbMissingCount > 0; + // Hide empty state while the TMDB debounce trails the library debounce; otherwise + // the user sees "No matches" flash between t=200ms and t=400ms after typing. + const tmdbDebounceCatchingUp = + canRequest.discoveryEnabled && tmdbDebouncedQuery !== debouncedQuery; const searchState = useMemo( () => createCatalogSearchState("query", { q: debouncedQuery || undefined }), @@ -195,7 +201,9 @@ export function GlobalSearch({ items.length === 0 && !previewQuery.isError && !tmdbStillLoading && - !tmdbWillRender; + !tmdbWillRender && + !canRequest.isResolving && + !tmdbDebounceCatchingUp; const showError = previewQuery.isError; return ( @@ -252,34 +260,33 @@ export function GlobalSearch({ {showResultsPanel && (
-
- {showLoading && ( -
- Searching... -
- )} - {showError && ( -
- Could not load results. Press Enter to open the search page. -
- )} - {showEmpty && ( -
- No matches -
- )} - {items.map((item, i) => ( - - ))} +
+
+ {showLoading && ( +
+ Searching... +
+ )} + {showError && ( +
+ Could not load results. Press Enter to open the search page. +
+ )} + {showEmpty && ( +
+ No matches +
+ )} + {items.map((item, i) => ( + + ))} +
{tmdbDebouncedQuery.length > 1 && canRequest.discoveryEnabled && (
- {items.length} results found + {tmdbVisibleCount > 0 + ? `${items.length} library results, ${tmdbVisibleCount} request suggestions` + : `${items.length} results found`}
{total > PREVIEW_LIMIT ? ( diff --git a/web/src/components/RequestPosterCard.test.tsx b/web/src/components/RequestPosterCard.test.tsx index 215a5ccf..904c9bcd 100644 --- a/web/src/components/RequestPosterCard.test.tsx +++ b/web/src/components/RequestPosterCard.test.tsx @@ -24,7 +24,9 @@ describe("RequestPosterCard (discover variant)", () => { /> , ); - expect(markup).toContain("Request"); + // Must render an actual