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.
166 lines
7.7 KiB
TypeScript
166 lines
7.7 KiB
TypeScript
// @vitest-environment jsdom
|
||
//
|
||
// PageShell measures the floating header and publishes its real height as `--hdr-h` onto the
|
||
// content wrapper, so a header that flex-wraps to 2–3 rows on a narrow window never overlaps the
|
||
// content below it. jsdom does no layout (offsetHeight is 0) and has no ResizeObserver, so we stub
|
||
// both: a ResizeObserver that hands us its callback, and offset metrics on the measured header.
|
||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||
import { render, cleanup } from "@testing-library/react";
|
||
import PageShell from "./PageShell";
|
||
import { PageHeading } from "./Headings";
|
||
|
||
vi.mock("./PageScroller", () => ({
|
||
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||
}));
|
||
|
||
let roCb: (() => void) | null = null;
|
||
beforeEach(() => {
|
||
roCb = null;
|
||
vi.stubGlobal(
|
||
"ResizeObserver",
|
||
class {
|
||
constructor(cb: () => void) {
|
||
roCb = cb;
|
||
}
|
||
observe() {}
|
||
disconnect() {}
|
||
},
|
||
);
|
||
});
|
||
afterEach(() => {
|
||
cleanup();
|
||
vi.unstubAllGlobals();
|
||
});
|
||
|
||
describe("PageShell --hdr-h", () => {
|
||
it("publishes the measured header height onto the content wrapper", () => {
|
||
const { container } = render(
|
||
<PageShell header={<div data-testid="hdr">H</div>}>content</PageShell>,
|
||
);
|
||
const content = container.querySelector('[class*="pt-(--hdr-h)"]') as HTMLElement;
|
||
expect(content).toBeTruthy();
|
||
// Give the header a real (multi-row) box, then fire the ResizeObserver callback the effect made.
|
||
const hdr = container.querySelector('[data-testid="hdr"]') as HTMLElement;
|
||
Object.defineProperty(hdr, "offsetTop", { value: 12, configurable: true });
|
||
Object.defineProperty(hdr, "offsetHeight", { value: 96, configurable: true });
|
||
roCb?.();
|
||
expect(content.style.getPropertyValue("--hdr-h")).toBe("112px"); // 12 + 96 + 4
|
||
});
|
||
|
||
it("adds no `--hdr-h` pad when there is no header", () => {
|
||
const { container } = render(<PageShell>content</PageShell>);
|
||
expect(container.querySelector('[class*="pt-(--hdr-h)"]')).toBeNull();
|
||
});
|
||
|
||
it("re-measures when the header is removed then restored (feed→channel→back)", () => {
|
||
// PageShell is REUSED across feed↔channel navigation (same component position in App.tsx) — it
|
||
// never remounts, so the header DOM node is swapped under a stable PageShell. The measurement
|
||
// must re-run on that swap; a one-shot effect kept observing the FIRST node and, when it detached,
|
||
// rewrote --hdr-h to a stale 4px that stuck on Back. Regression for the channel-Back header overlap.
|
||
const hdr = () => <div data-testid="hdr">H</div>;
|
||
const { container, rerender } = render(<PageShell header={hdr()}>content</PageShell>);
|
||
const measure = (top: number, h: number) => {
|
||
const el = container.querySelector('[data-testid="hdr"]') as HTMLElement;
|
||
Object.defineProperty(el, "offsetTop", { value: top, configurable: true });
|
||
Object.defineProperty(el, "offsetHeight", { value: h, configurable: true });
|
||
roCb?.();
|
||
};
|
||
measure(12, 52);
|
||
const content = () => container.querySelector('[class*="pt-(--hdr-h)"]') as HTMLElement;
|
||
expect(content().style.getPropertyValue("--hdr-h")).toBe("68px"); // 12 + 52 + 4
|
||
|
||
rerender(<PageShell>content</PageShell>); // → channel page: header gone
|
||
rerender(<PageShell header={hdr()}>content</PageShell>); // → Back: header returns as a NEW node
|
||
measure(12, 52); // fires the NEW header's observer
|
||
expect(content().style.getPropertyValue("--hdr-h")).toBe("68px"); // re-measured, not the stale 4px
|
||
});
|
||
});
|
||
|
||
// The pinned-rail overlay decision: a rail pins as OVERLAY (--rail-pinned-w 2.25rem, the toolbar keeps a
|
||
// slim gutter and content stays put) once the window is wide enough that the centred content leaves room
|
||
// for the rail in its left margin (maxWidth + ~36rem); below that it pins in-flow (16.75rem). The rem→px
|
||
// conversion must use the LIVE root font-size, not a hardcoded 16, so it stays correct at every text size.
|
||
describe("PageShell --rail-pinned-w / --content-mw", () => {
|
||
const root = document.documentElement;
|
||
const setWidth = (w: number) =>
|
||
Object.defineProperty(window, "innerWidth", { value: w, configurable: true });
|
||
afterEach(() => {
|
||
root.style.removeProperty("font-size");
|
||
root.style.removeProperty("--content-mw");
|
||
root.style.removeProperty("--rail-pinned-w");
|
||
});
|
||
|
||
it("overlays the rail once the window clears maxWidth + 36rem (at 16px/rem)", () => {
|
||
setWidth(1600); // 64rem*16 + 36rem*16 = 1024 + 576 = 1600 → exactly roomy
|
||
render(<PageShell maxWidth="64rem">content</PageShell>);
|
||
expect(root.style.getPropertyValue("--content-mw")).toBe("64rem");
|
||
expect(root.style.getPropertyValue("--rail-pinned-w")).toBe("2.25rem");
|
||
});
|
||
|
||
it("keeps the rail in-flow just below the threshold", () => {
|
||
setWidth(1599);
|
||
render(<PageShell maxWidth="64rem">content</PageShell>);
|
||
expect(root.style.getPropertyValue("--rail-pinned-w")).toBe("16.75rem");
|
||
});
|
||
|
||
it("never overlays on a full-width page (no maxWidth)", () => {
|
||
setWidth(3000);
|
||
render(<PageShell>content</PageShell>);
|
||
expect(root.style.getPropertyValue("--content-mw")).toBe("none");
|
||
expect(root.style.getPropertyValue("--rail-pinned-w")).toBe("16.75rem");
|
||
});
|
||
|
||
it("recomputes on resize", () => {
|
||
setWidth(1599);
|
||
render(<PageShell maxWidth="64rem">content</PageShell>);
|
||
expect(root.style.getPropertyValue("--rail-pinned-w")).toBe("16.75rem");
|
||
setWidth(1600);
|
||
window.dispatchEvent(new Event("resize"));
|
||
expect(root.style.getPropertyValue("--rail-pinned-w")).toBe("2.25rem");
|
||
});
|
||
|
||
it("scales the rem threshold by the live root font-size, not a hardcoded 16", () => {
|
||
root.style.fontSize = "17px"; // as the --font-scale 1.06 default renders it
|
||
setWidth(1650); // roomy at 16px/rem (>1600) but NOT at 17px/rem (needs 100rem*17 = 1700)
|
||
render(<PageShell maxWidth="64rem">content</PageShell>);
|
||
expect(root.style.getPropertyValue("--rail-pinned-w")).toBe("16.75rem");
|
||
});
|
||
|
||
it("recomputes at runtime when --font-scale changes the root font, without looping", async () => {
|
||
setWidth(1650); // roomy at 16px/rem (1650 >= 1600) → overlay
|
||
render(<PageShell maxWidth="64rem">content</PageShell>);
|
||
expect(root.style.getPropertyValue("--rail-pinned-w")).toBe("2.25rem");
|
||
// The text-size slider changes the root font-size with NO resize event — the MutationObserver
|
||
// must recompute. At 17px/rem the threshold is 100rem*17 = 1700 > 1650, so the rail goes in-flow.
|
||
// (If the observer's self-write guard were broken this would loop forever and time out.)
|
||
root.style.fontSize = "17px";
|
||
await new Promise((r) => setTimeout(r, 0)); // flush the MutationObserver microtask
|
||
expect(root.style.getPropertyValue("--rail-pinned-w")).toBe("16.75rem");
|
||
});
|
||
});
|
||
|
||
// DESIGN.md B4: every page has one h1 — the page's name, visually hidden, unless a view shows its
|
||
// own title (a channel, a Plex show), which is then the page's h1 instead.
|
||
describe("PageShell page heading", () => {
|
||
it("names the page with a visually hidden h1", () => {
|
||
render(<PageShell title="Downloads">content</PageShell>);
|
||
const h1s = document.querySelectorAll("h1");
|
||
expect(h1s).toHaveLength(1);
|
||
expect(h1s[0]!.textContent).toBe("Downloads");
|
||
expect(h1s[0]!.className).toContain("sr-only");
|
||
});
|
||
|
||
it("steps aside while a view renders its own PageHeading, and returns when it goes", () => {
|
||
const { rerender } = render(
|
||
<PageShell title="Plex">
|
||
<PageHeading>Knight Rider</PageHeading>
|
||
</PageShell>,
|
||
);
|
||
expect([...document.querySelectorAll("h1")].map((h) => h.textContent)).toEqual([
|
||
"Knight Rider",
|
||
]);
|
||
rerender(<PageShell title="Plex">grid</PageShell>);
|
||
expect([...document.querySelectorAll("h1")].map((h) => h.textContent)).toEqual(["Plex"]);
|
||
});
|
||
});
|