// @vitest-environment jsdom // // E31 (the owner): a freshly opened channel page shows what is still unwatched, and a watch state the // viewer picks there sticks — a reload (a fresh mount, seeded from the stored preference) keeps it. import type { ReactNode } from "react"; import { describe, it, expect, vi, beforeAll, afterEach } from "vitest"; import { render, cleanup, act } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import type { FeedFilters } from "../lib/api"; vi.mock("react-i18next", async (importOriginal) => ({ ...(await importOriginal()), useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }), })); vi.mock("./navigationContext", () => ({ useNavigationActions: () => ({ closeChannel: vi.fn(), openChannel: vi.fn() }), })); vi.mock("./confirmContext", () => ({ useConfirm: () => async () => true })); vi.mock("./feedFiltersContext", () => ({ useFeedFilters: () => ({ includeNormal: true, includeShorts: false, includeLive: false }), })); const feed = vi.hoisted(() => ({ last: null as null | { filters: FeedFilters; setFilters: (f: FeedFilters) => void }, })); vi.mock("./Feed", () => ({ default: (p: { filters: FeedFilters; setFilters: (f: FeedFilters) => void }) => { feed.last = p; return null; }, })); vi.mock("./ChannelAbout", () => ({ default: () => null })); vi.mock("./ui/PageShell", async (importOriginal) => ({ ...(await importOriginal()), PageToolbar: ({ children }: { children?: ReactNode }) => <>{children}, })); import ChannelPage from "./ChannelPage"; import { api, type ChannelDetail, type Me } from "../lib/api"; import { qk } from "../lib/queryKeys"; 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(); feed.last = null; }); const CH = "UCqoAEDirJPjEUFcF2FklnBA"; function mount(preferences: Record = {}) { vi.spyOn(api, "channelDetail").mockResolvedValue({ id: CH, title: "StarTalk", subscribed: true, stored_videos: 10, } as unknown as ChannelDetail); vi.spyOn(api, "tags").mockResolvedValue([] as never); const save = vi.spyOn(api, "savePrefs").mockResolvedValue({} as never); const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); qc.setQueryData(qk.me(), { id: 1, preferences } as unknown as Me); render( {}} onShowInFeed={() => {}} /> , ); return save; } describe("ChannelPage — the watch state", () => { it("shows only the unwatched videos on a freshly opened page", () => { mount(); expect(feed.last?.filters.show).toBe("unwatched"); }); it("stores a picked watch state", () => { const save = mount(); act(() => feed.last!.setFilters({ ...feed.last!.filters, show: "all" })); expect(feed.last?.filters.show).toBe("all"); expect(save).toHaveBeenCalledWith({ channelShow: "all" }); }); it("keeps the stored watch state after a reload", () => { mount({ channelShow: "watched" }); expect(feed.last?.filters.show).toBe("watched"); }); it("falls back to unwatched for a stored value it does not know", () => { mount({ channelShow: "bogus" }); expect(feed.last?.filters.show).toBe("unwatched"); }); });