// @vitest-environment jsdom // // The count pill while a new search's count loads. keepPreviousData holds the last filter's count, and a // broad word's count takes seconds ("a": ~3 s on the prod-sized library): the pill showed the old // number as if it were the new search's. It now dims and is aria-busy until the new count lands. import { describe, it, expect, vi, beforeAll, afterEach } from "vitest"; import { render, screen, waitFor, cleanup } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; vi.mock("react-i18next", async (importOriginal) => ({ ...(await importOriginal()), useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }), })); vi.mock("./wizardContext", () => ({ useWizard: () => ({ openWizard: vi.fn() }) })); vi.mock("./VirtualFeed", () => ({ default: () => null })); vi.mock("./SelectedViewChip", () => ({ default: () => null })); vi.mock("./ActiveFilterChips", () => ({ default: () => null })); import Feed from "./Feed"; import { api, DEFAULT_TAG_MODES, type FeedFilters } from "../lib/api"; import { qk } from "../lib/queryKeys"; import type { ReactElement } from "react"; import { PageToolbarSlot } from "./ui/pageToolbarSlot"; beforeAll(() => { class Stub { observe() {} unobserve() {} disconnect() {} takeRecords() { return []; } } globalThis.ResizeObserver ??= Stub as unknown as typeof ResizeObserver; globalThis.IntersectionObserver ??= Stub as unknown as typeof IntersectionObserver; }); afterEach(() => { cleanup(); vi.restoreAllMocks(); }); const FILTERS: FeedFilters = { tags: [], tagModes: DEFAULT_TAG_MODES, channelIds: [], q: "", sort: "newest", scope: "my", includeNormal: true, includeShorts: false, includeLive: false, show: "unwatched", }; // The pill lives in the toolbar, which portals into the shell's band: give it one. const slot = document.body.appendChild(document.createElement("div")); const feed = (q: string, qc: QueryClient): ReactElement => ( {}} view="cards" setView={() => {}} canRead ytSearch="" onYtSearch={() => {}} onExitYtSearch={() => {}} /> ); describe("Feed — the count while a new search's count loads", () => { it("dims the previous count and marks it busy until the new one lands", async () => { vi.spyOn(api, "feed").mockResolvedValue({ items: [], next_cursor: null } as never); let settle: (v: unknown) => void = () => {}; vi.spyOn(api, "feedSummary").mockImplementation(((f: FeedFilters) => f.q === "kitchen" ? Promise.resolve({ count: 1937, facets: {} }) : new Promise((r) => (settle = r))) as never); const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); qc.setQueryData(qk.me(), { id: 1, can_write: true, prefs: {} }); const pill = () => screen.getByTestId("item-count"); const view = render(feed("kitchen", qc)); await waitFor(() => expect(pill().getAttribute("title")).toBe("1,937")); expect(pill().getAttribute("aria-busy")).toBeNull(); view.rerender(feed("a", qc)); await waitFor(() => expect(pill().getAttribute("aria-busy")).toBe("true")); expect(pill().getAttribute("title")).toBe("1,937"); // still the old figure, now marked as such settle({ count: 178991, facets: {} }); await waitFor(() => expect(pill().getAttribute("title")).toBe("178,991")); expect(pill().getAttribute("aria-busy")).toBeNull(); }); it("says the count failed instead of showing the empty box of a first load", async () => { vi.spyOn(api, "feed").mockResolvedValue({ items: [], next_cursor: null } as never); vi.spyOn(api, "feedSummary").mockImplementation(((f: FeedFilters) => f.q === "kitchen" ? Promise.resolve({ count: 1937, facets: {} }) : Promise.reject(new Error("boom"))) as never); const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); qc.setQueryData(qk.me(), { id: 1, can_write: true, prefs: {} }); const pill = () => screen.getByTestId("item-count"); const view = render(feed("kitchen", qc)); await waitFor(() => expect(pill().getAttribute("title")).toBe("1,937")); view.rerender(feed("a", qc)); await waitFor(() => expect(pill().getAttribute("title")).toBe("common.countFailed")); expect(pill().getAttribute("aria-busy")).toBeNull(); }); });