The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").
Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.
Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).
One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
156 lines
5.5 KiB
TypeScript
156 lines
5.5 KiB
TypeScript
// @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<typeof import("react-i18next")>();
|
||
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<Row>[], narrowedBy = "") {
|
||
host = document.createElement("div");
|
||
document.body.appendChild(host);
|
||
root = createRoot(host);
|
||
const draw = (r: Row[], n: string) =>
|
||
flushSync(() =>
|
||
root!.render(
|
||
<DataTable rows={r} columns={columns} rowKey={(x) => x.id} pageSize={0} narrowedBy={n} />,
|
||
),
|
||
);
|
||
draw(rows, narrowedBy);
|
||
return draw;
|
||
}
|
||
|
||
const plain: Column<Row>[] = [{ 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<string, number>();
|
||
const counted: Column<Row>[] = [
|
||
{
|
||
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 <body>.
|
||
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(
|
||
<DataTable
|
||
rows={rows}
|
||
columns={plain}
|
||
rowKey={(x) => 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(
|
||
<DataTable
|
||
rows={rows}
|
||
columns={plain}
|
||
rowKey={(x) => 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);
|
||
});
|
||
});
|