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.
111 lines
4.9 KiB
TypeScript
111 lines
4.9 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// The card layout's pager clamps an out-of-range page DURING RENDER, which is the right
|
|
// shape — React discards that pass, so nothing is committed on a page that no longer exists — but
|
|
// it is also the shape that turns a non-convergent condition into "Too many re-renders" rather than
|
|
// a visual glitch. Neither the hook nor either of its two callers (ChannelDiscovery, Channels) had
|
|
// a test, so nothing held either half. This file holds both: the clamp happens, and it terminates.
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { renderHook, act } from "@testing-library/react";
|
|
|
|
vi.mock("react-i18next", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("react-i18next")>();
|
|
return { ...actual, useTranslation: () => ({ t: (k: string) => k }) };
|
|
});
|
|
|
|
import { useCardPager } from "./useCardPager";
|
|
|
|
const rows = (n: number) => Array.from({ length: n }, (_, i) => i);
|
|
const KEY = "siftlode.test.cardSize";
|
|
|
|
beforeEach(() => {
|
|
localStorage.clear();
|
|
sessionStorage.setItem("siftlode.activeAccount", "1");
|
|
});
|
|
|
|
describe("useCardPager — an out-of-range page", () => {
|
|
it("clamps to the last page that still exists when the list shrinks", () => {
|
|
const { result, rerender } = renderHook(({ n }) => useCardPager(rows(n), KEY, true), {
|
|
initialProps: { n: 30 },
|
|
});
|
|
act(() => result.current.pagerProps.onSizeChange(10));
|
|
act(() => result.current.pagerProps.onPageChange(2));
|
|
expect(result.current.pagerProps.page).toBe(2);
|
|
expect(result.current.paged).toEqual([20, 21, 22, 23, 24, 25, 26, 27, 28, 29]);
|
|
|
|
rerender({ n: 5 }); // an unsubscribe, a refetch — page 2 no longer exists
|
|
|
|
expect(result.current.pagerProps.page).toBe(0);
|
|
expect(result.current.paged).toEqual([0, 1, 2, 3, 4]);
|
|
});
|
|
|
|
it("does not spring back to the stale page when the list grows again", () => {
|
|
const { result, rerender } = renderHook(({ n }) => useCardPager(rows(n), KEY, true), {
|
|
initialProps: { n: 30 },
|
|
});
|
|
act(() => result.current.pagerProps.onSizeChange(10));
|
|
act(() => result.current.pagerProps.onPageChange(2));
|
|
|
|
rerender({ n: 5 });
|
|
rerender({ n: 30 });
|
|
|
|
// The clamp was COMMITTED, not merely displayed — `safePage` alone would have let 2 come back.
|
|
expect(result.current.pagerProps.page).toBe(0);
|
|
});
|
|
|
|
it("terminates on an empty list — the write falsifies its own condition", () => {
|
|
const { result, rerender } = renderHook(({ n }) => useCardPager(rows(n), KEY, true), {
|
|
initialProps: { n: 30 },
|
|
});
|
|
act(() => result.current.pagerProps.onSizeChange(10));
|
|
act(() => result.current.pagerProps.onPageChange(2));
|
|
|
|
// totalPages floors at 1, so the clamp target is 0 and the condition is false next pass. If it
|
|
// were not, this line would throw "Too many re-renders" instead of returning.
|
|
rerender({ n: 0 });
|
|
|
|
expect(result.current.pagerProps.page).toBe(0);
|
|
expect(result.current.paged).toEqual([]);
|
|
});
|
|
|
|
it("holds still in table mode, where DataTable does its own paging", () => {
|
|
const { result, rerender } = renderHook(({ n, on }) => useCardPager(rows(n), KEY, on), {
|
|
initialProps: { n: 30, on: false },
|
|
});
|
|
act(() => result.current.pagerProps.onSizeChange(10));
|
|
act(() => result.current.pagerProps.onPageChange(2));
|
|
|
|
rerender({ n: 5, on: false }); // the same shrink — but nothing is committed here
|
|
rerender({ n: 30, on: false });
|
|
|
|
// `safePage` still clamps for DISPLAY while the list is short; what must not happen is the
|
|
// clamp being written, which would lose the page for real. Growing back proves it was not.
|
|
expect(result.current.pagerProps.page).toBe(2);
|
|
expect(result.current.paged).toEqual(rows(30)); // and the rows are never sliced in table mode
|
|
});
|
|
});
|
|
|
|
describe("useCardPager — following a moved row", () => {
|
|
it("turns to the page that holds the row on each new seq, and not on mount", () => {
|
|
const { result, rerender } = renderHook(
|
|
({ list, seq }) =>
|
|
useCardPager(list, KEY, true, seq ? { seq, match: (r: number) => r === 3 } : null),
|
|
{ initialProps: { list: rows(30), seq: 0 } },
|
|
);
|
|
act(() => result.current.pagerProps.onSizeChange(10));
|
|
act(() => result.current.pagerProps.onPageChange(2));
|
|
// The row 3 sank to place 25: seq 1 follows it to the third page…
|
|
const sunk = [...rows(30).filter((r) => r !== 3)];
|
|
sunk.splice(25, 0, 3);
|
|
rerender({ list: sunk, seq: 1 });
|
|
expect(result.current.pagerProps.page).toBe(2);
|
|
// …and bubbled up to place 4: seq 2 follows it back to the first.
|
|
rerender({ list: rows(30), seq: 2 });
|
|
expect(result.current.pagerProps.page).toBe(0);
|
|
// A re-render with the same seq leaves the reader's own paging alone.
|
|
act(() => result.current.pagerProps.onPageChange(1));
|
|
rerender({ list: rows(30), seq: 2 });
|
|
expect(result.current.pagerProps.page).toBe(1);
|
|
});
|
|
});
|