// @vitest-environment jsdom // // The channel manager's "All" page (~400 rows) blocked one frame for 0.7–0.9 s on prod to // mount, and every key in its search re-rendered every row. A long page now mounts its first rows at // once and the rest in small background steps (progressiveRows), and a step leaves the rows it holds // alone (a row is its own component, whose cells the React Compiler caches). // // These tests drive React without act(): act flushes the background steps along with the first render, // and the first render on its own is the property under test. import { describe, it, expect, afterEach, beforeAll, afterAll, vi } from "vitest"; import { flushSync } from "react-dom"; import { createRoot, type Root } from "react-dom/client"; import { waitFor } from "@testing-library/react"; import DataTable, { type Column } from "./DataTable"; import { FIRST_ROWS } from "./progressiveRows"; vi.mock("react-i18next", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useTranslation: () => ({ i18n: { language: "en" }, t: (key: string) => key }), }; }); const g = globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }; let wasAct: boolean | undefined; beforeAll(() => { wasAct = g.IS_REACT_ACT_ENVIRONMENT; g.IS_REACT_ACT_ENVIRONMENT = false; }); afterAll(() => { g.IS_REACT_ACT_ENVIRONMENT = wasAct; }); type Row = { id: string; name: string }; const many = (n: number, prefix = "r"): Row[] => Array.from({ length: n }, (_, i) => ({ id: `${prefix}${i}`, name: `${prefix} ${i}` })); let root: Root | null = null; let host: HTMLElement | null = null; afterEach(() => { root?.unmount(); host?.remove(); root = null; host = null; }); const bodyRows = () => host!.querySelectorAll("tbody tr").length; // Mounts synchronously, so what the DOM holds right after is the first render alone. function mount(rows: Row[], columns: Column[], narrowedBy = "") { host = document.createElement("div"); document.body.appendChild(host); root = createRoot(host); const draw = (r: Row[], n: string) => flushSync(() => root!.render( x.id} pageSize={0} narrowedBy={n} />, ), ); draw(rows, narrowedBy); return draw; } const plain: Column[] = [{ key: "name", header: "Name", render: (r) => r.name }]; describe("DataTable — a long page mounts in steps", () => { it("paints its first rows at once and the rest after", async () => { mount(many(200), plain); expect(bodyRows()).toBe(FIRST_ROWS); await waitFor(() => expect(bodyRows()).toBe(200)); }); it("does not re-render the rows it already holds as it grows", async () => { const renders = new Map(); const counted: Column[] = [ { key: "name", header: "Name", render: (r) => { renders.set(r.id, (renders.get(r.id) ?? 0) + 1); return r.name; }, }, ]; mount(many(200), counted); await waitFor(() => expect(bodyRows()).toBe(200)); // The first row was drawn by the first render, not again by each step after it. expect(renders.get("r0")).toBe(1); }); it("starts over from the first rows when the caller narrows them, not on new data", async () => { const draw = mount(many(200), plain, ""); await waitFor(() => expect(bodyRows()).toBe(200)); // New data for the same view (a refetch): the page keeps what it shows. draw(many(200), ""); expect(bodyRows()).toBe(200); // A search the caller applied: the urgent render is small again. draw(many(150, "s"), "s"); expect(bodyRows()).toBe(FIRST_ROWS); await waitFor(() => expect(bodyRows()).toBe(150)); }); // Channels turns the page to the row a priority pick moved and focuses it in the commit // that follows. That row was at index 40 of a 50-row page: not mounted yet, focus fell to . it("mounts a revealed row in the render that turns to its page", () => { host = document.createElement("div"); document.body.appendChild(host); root = createRoot(host); const rows = many(100); const draw = (reveal: { key: string; seq: number } | null) => flushSync(() => root!.render( x.id} pageSize={50} reveal={reveal} />, ), ); draw(null); draw({ key: "r90", seq: 1 }); expect(host.querySelector("tbody")?.textContent).toContain("r 90"); }); // The reveal floor once held for the page's lifetime. Channels never clears its `follow`, so // after one priority pick every later sort or search mounted every row up to that channel at once. it("lets a revealed row go once the reader moves to another view", () => { host = document.createElement("div"); document.body.appendChild(host); root = createRoot(host); const rows = many(200); const draw = (reveal: { key: string; seq: number } | null, narrowedBy: string) => flushSync(() => root!.render( x.id} pageSize={0} reveal={reveal} narrowedBy={narrowedBy} />, ), ); draw(null, ""); const follow = { key: "r150", seq: 1 }; draw(follow, ""); expect(host.querySelector("tbody")?.textContent).toContain("r 150"); draw(follow, "s"); // a search: a new view, the same (never cleared) reveal expect(bodyRows()).toBe(FIRST_ROWS); }); });