// @vitest-environment jsdom // // "Get full history" on a channel page must not leave the video grid frozen on the cards the // channel already had. The backfill runs in a BackgroundTask, so the PATCH that starts it returns // before a single row is written — invalidating the feed on that RESPONSE (what the mutation does) // refetches the OLD catalogue. Reported from daily use: the stored count climbed into the // thousands while the list stayed on the ~19 cards from subscribe time, until a browser F5. // // The channel detail's 4s live poll is the only thing that sees the job progress, so it is what // has to drive the grid. These assert the real cache effect (the feed query ends up invalidated), // not that some helper was called. import { describe, it, expect } from "vitest"; import { renderHook } from "@testing-library/react"; import { QueryClient } from "@tanstack/react-query"; import { useBackfillFeedRefresh } from "./channelDetailPoll"; import { qk } from "./queryKeys"; import type { ChannelDetail, FeedFilters } from "./api"; const CH = "UCsXVk37bltHxD1rDPwtNM8Q"; // The channel page's Feed is channel-scoped; only the shape matters for the cache key. const filters = { q: "", channel: CH } as unknown as FeedFilters; /** A detail reading mid-backfill: full history requested, back-catalogue not in yet. */ const detail = (over: Partial = {}): ChannelDetail => ({ id: CH, deep_requested: true, backfill_done: false, stored_videos: 19, ...over, }) as ChannelDetail; /** The two queries behind the grid and the count pill beside it. */ const seed = (qc: QueryClient) => { qc.setQueryData(qk.feed(filters), { pages: [{ items: [], next_cursor: null }], pageParams: [null], }); qc.setQueryData(qk.feedSummary(filters), { count: 19 }); }; const stale = (qc: QueryClient) => ({ grid: qc.getQueryState(qk.feed(filters))?.isInvalidated === true, count: qc.getQueryState(qk.feedSummary(filters))?.isInvalidated === true, }); /** Mount on `first`, then hand the hook the next poll reading. */ const poll = (qc: QueryClient, first: ChannelDetail | undefined, next: ChannelDetail) => { const { rerender } = renderHook((d: ChannelDetail | undefined) => useBackfillFeedRefresh(d, qc), { initialProps: first, }); rerender(next); }; describe("useBackfillFeedRefresh", () => { it("refreshes the grid and its count when the running backfill stores more videos", () => { const qc = new QueryClient(); seed(qc); poll(qc, detail(), detail({ stored_videos: 79 })); expect(stale(qc)).toEqual({ grid: true, count: true }); }); it("refreshes when the backfill finishes, even if its last page added nothing new", () => { // The tail can be all duplicates: the count never moves, but the grid's `hasNextPage` is still // the one it cached from the pre-backfill catalogue. const qc = new QueryClient(); seed(qc); poll(qc, detail(), detail({ backfill_done: true })); expect(stale(qc)).toEqual({ grid: true, count: true }); }); it("leaves the grid alone on a poll tick that changed nothing", () => { const qc = new QueryClient(); seed(qc); poll(qc, detail(), detail()); expect(stale(qc)).toEqual({ grid: false, count: false }); }); it("stays out of the way when no backfill is running", () => { // A detail refetch outside the poll window comes from a mutation, and every one of those // invalidates the feed itself — firing here too would just double it. const qc = new QueryClient(); seed(qc); poll( qc, detail({ deep_requested: false }), detail({ deep_requested: false, stored_videos: 79 }), ); expect(stale(qc)).toEqual({ grid: false, count: false }); }); it("does not mistake a switch to another channel for backfill progress", () => { const qc = new QueryClient(); seed(qc); poll(qc, detail(), detail({ id: "UCba9j5PIA1zvHf7L5WVJKAg", stored_videos: 412 })); expect(stale(qc)).toEqual({ grid: false, count: false }); }); it("survives the first render, when there is no previous reading yet", () => { const qc = new QueryClient(); seed(qc); poll(qc, undefined, detail()); expect(stale(qc)).toEqual({ grid: false, count: false }); }); });