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.
187 lines
7.6 KiB
TypeScript
187 lines
7.6 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// C2 (DESIGN "Touch"): on touch, and below 768, the side panel is a DRAWER — the tab opens it over the
|
|
// content (ui/Modal's start-edge drawer: Esc, Back, the scrim), it never peeks on a tap's emulated
|
|
// hover, and pinning is a button in its header offered only ≥ 1024. A mouse on a wide window keeps
|
|
// hover-to-peek and click-to-pin. jsdom has no matchMedia: the stub answers the two queries SidePanel
|
|
// asks from three switches, and can flip them live (a tablet turning).
|
|
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
import { act, render, screen, fireEvent, cleanup } from "@testing-library/react";
|
|
|
|
vi.mock("react-i18next", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("react-i18next")>();
|
|
return {
|
|
...actual,
|
|
useTranslation: () => ({
|
|
t: (k: string, o?: Record<string, unknown>) => (o?.title ? `${k}:${String(o.title)}` : k),
|
|
}),
|
|
};
|
|
});
|
|
|
|
import SidePanel from "./SidePanel";
|
|
|
|
type Env = { coarse: boolean; narrow: boolean; wide: boolean };
|
|
let env: Env = { coarse: false, narrow: false, wide: true };
|
|
const listeners = new Set<() => void>();
|
|
function stubMedia(next: Env) {
|
|
env = next;
|
|
window.matchMedia = ((query: string) => ({
|
|
get matches() {
|
|
// A comma list is an OR: each part answers from its own switch.
|
|
return query
|
|
.split(",")
|
|
.some((q) =>
|
|
q.includes("pointer: coarse")
|
|
? env.coarse
|
|
: q.includes("max-width: 767.98px")
|
|
? env.narrow
|
|
: q.includes("min-width: 1024px")
|
|
? env.wide
|
|
: false,
|
|
);
|
|
},
|
|
media: query,
|
|
addEventListener: (_: string, l: () => void) => listeners.add(l),
|
|
removeEventListener: (_: string, l: () => void) => listeners.delete(l),
|
|
})) as unknown as typeof window.matchMedia;
|
|
}
|
|
function turn(next: Env) {
|
|
act(() => {
|
|
env = next;
|
|
for (const l of [...listeners]) l();
|
|
});
|
|
}
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
listeners.clear();
|
|
// @ts-expect-error — jsdom ships none; put it back the way it was
|
|
delete window.matchMedia;
|
|
});
|
|
|
|
function mount(collapsed = true) {
|
|
const onToggleCollapse = vi.fn();
|
|
render(
|
|
<SidePanel
|
|
title="Filters"
|
|
icon={<span />}
|
|
collapsed={collapsed}
|
|
onToggleCollapse={onToggleCollapse}
|
|
>
|
|
<p>panel body</p>
|
|
</SidePanel>,
|
|
);
|
|
return { onToggleCollapse };
|
|
}
|
|
|
|
describe("SidePanel · a mouse on a wide window", () => {
|
|
it("pins from the tab, as before", () => {
|
|
stubMedia({ coarse: false, narrow: false, wide: true });
|
|
const { onToggleCollapse } = mount();
|
|
fireEvent.click(screen.getByRole("button", { name: "sidebar.pinNamed:Filters" }));
|
|
expect(onToggleCollapse).toHaveBeenCalledOnce();
|
|
expect(screen.queryByRole("dialog")).toBeNull();
|
|
});
|
|
});
|
|
|
|
// The pinned (or peeking) card covers the tab, which only goes transparent — yet it
|
|
// stayed a Tab stop, an invisible control the focus landed on, named just "Unpin panel".
|
|
describe("SidePanel · the tab under an open card", () => {
|
|
it("is out of the Tab order while the card is open, and names its panel", () => {
|
|
stubMedia({ coarse: false, narrow: false, wide: true });
|
|
globalThis.ResizeObserver ??= class {
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
} as unknown as typeof ResizeObserver;
|
|
mount(false); // pinned
|
|
// The card's own pin is the one control by that name; the tab under it is out of the tree too.
|
|
const tab = document.querySelector<HTMLElement>(
|
|
'[aria-hidden="true"][aria-label="sidebar.unpinNamed:Filters"]',
|
|
)!;
|
|
expect(tab.tabIndex).toBe(-1);
|
|
expect(screen.getAllByRole("button", { name: "sidebar.unpinNamed:Filters" })).toHaveLength(1);
|
|
cleanup();
|
|
mount(true);
|
|
expect(screen.getByRole("button", { name: "sidebar.pinNamed:Filters" }).tabIndex).toBe(0);
|
|
});
|
|
|
|
it("hands a key's focus on the tab to the card it peeks open, not to the hidden tab", () => {
|
|
stubMedia({ coarse: false, narrow: false, wide: true });
|
|
globalThis.ResizeObserver ??= class {
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
} as unknown as typeof ResizeObserver;
|
|
mount(true);
|
|
const tab = screen.getByRole("button", { name: "sidebar.pinNamed:Filters" });
|
|
// A page the browser is not painting runs no animation frame: the step must not wait for one.
|
|
const raf = vi.spyOn(window, "requestAnimationFrame").mockImplementation(() => 0);
|
|
act(() => tab.focus()); // jsdom calls every focus :focus-visible — a key's focus, here
|
|
// in the commit that opens the card, not a frame later
|
|
expect(document.activeElement?.closest("aside")).not.toBeNull();
|
|
expect(document.activeElement?.getAttribute("aria-label")).toBe("sidebar.pinNamed:Filters");
|
|
expect(document.activeElement).not.toBe(tab);
|
|
raf.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe("SidePanel · on touch", () => {
|
|
it("opens a drawer from the tab instead of pinning, and Esc closes it", () => {
|
|
stubMedia({ coarse: true, narrow: false, wide: false });
|
|
const { onToggleCollapse } = mount();
|
|
expect(screen.queryByText("panel body")).toBeNull();
|
|
fireEvent.click(screen.getByRole("button", { name: "sidebar.openPanel:Filters" }));
|
|
const dialog = screen.getByRole("dialog");
|
|
expect(dialog.textContent).toContain("panel body");
|
|
expect(onToggleCollapse).not.toHaveBeenCalled();
|
|
// Below 1024 a pinned panel would leave the content no room: no pin in the drawer.
|
|
expect(screen.queryByRole("button", { name: "sidebar.pinNamed:Filters" })).toBeNull();
|
|
fireEvent.keyDown(window, { key: "Escape" });
|
|
expect(screen.queryByRole("dialog")).toBeNull();
|
|
});
|
|
|
|
it("never peeks on the emulated hover a tap fires", () => {
|
|
stubMedia({ coarse: true, narrow: false, wide: false });
|
|
mount();
|
|
fireEvent.mouseEnter(screen.getByRole("button", { name: "sidebar.openPanel:Filters" }));
|
|
expect(screen.queryByText("panel body")).toBeNull();
|
|
});
|
|
|
|
it("offers the pin in the drawer's header at 1024 and up, and pinning closes the drawer", () => {
|
|
stubMedia({ coarse: true, narrow: false, wide: true });
|
|
const { onToggleCollapse } = mount();
|
|
fireEvent.click(screen.getByRole("button", { name: "sidebar.openPanel:Filters" }));
|
|
fireEvent.click(screen.getByRole("button", { name: "sidebar.pinNamed:Filters" }));
|
|
expect(onToggleCollapse).toHaveBeenCalledOnce();
|
|
expect(screen.queryByRole("dialog")).toBeNull();
|
|
});
|
|
|
|
it("leaves a pin saved on a desktop alone below 1024: a tab, not a pinned card", () => {
|
|
stubMedia({ coarse: true, narrow: false, wide: false });
|
|
mount(false);
|
|
expect(screen.queryByText("panel body")).toBeNull();
|
|
expect(screen.getByRole("button", { name: "sidebar.openPanel:Filters" })).toBeTruthy();
|
|
});
|
|
|
|
it("closes for good when the tablet turns to a width where the panel is a card again", () => {
|
|
stubMedia({ coarse: false, narrow: true, wide: false });
|
|
mount();
|
|
fireEvent.click(screen.getByRole("button", { name: "sidebar.openPanel:Filters" }));
|
|
expect(screen.getByRole("dialog")).toBeTruthy();
|
|
turn({ coarse: false, narrow: false, wide: true });
|
|
expect(screen.queryByRole("dialog")).toBeNull();
|
|
turn({ coarse: false, narrow: true, wide: false });
|
|
expect(screen.queryByRole("dialog")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("SidePanel · a narrow window with a mouse", () => {
|
|
it("is a drawer too (split screen, a small window)", () => {
|
|
stubMedia({ coarse: false, narrow: true, wide: false });
|
|
mount();
|
|
fireEvent.click(screen.getByRole("button", { name: "sidebar.openPanel:Filters" }));
|
|
expect(screen.getByRole("dialog").textContent).toContain("panel body");
|
|
});
|
|
});
|