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.
197 lines
6.8 KiB
TypeScript
197 lines
6.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
sortRows,
|
|
parseSortState,
|
|
cycleSort,
|
|
pickSort,
|
|
healSort,
|
|
hydrateSort,
|
|
type SortOption,
|
|
type SortState,
|
|
} from "./columnSort";
|
|
|
|
interface Row {
|
|
name: string;
|
|
views: number;
|
|
}
|
|
const rows: Row[] = [
|
|
{ name: "beta", views: 30 },
|
|
{ name: "alpha", views: 100 },
|
|
{ name: "gamma", views: 5 },
|
|
];
|
|
const columns = [
|
|
{ key: "name", sortValue: (r: Row) => r.name },
|
|
{ key: "views", sortValue: (r: Row) => r.views },
|
|
{ key: "nosort" }, // a column with no sortValue
|
|
];
|
|
|
|
describe("sortRows", () => {
|
|
it("returns the same array reference when there is no sort", () => {
|
|
expect(sortRows(rows, columns, null, "en")).toBe(rows);
|
|
});
|
|
|
|
it("returns the input unchanged for a column that has no sortValue", () => {
|
|
expect(sortRows(rows, columns, { key: "nosort", dir: "asc" }, "en")).toBe(rows);
|
|
});
|
|
|
|
it("sorts numbers by numeric difference, not string order", () => {
|
|
// string order would put "100" before "30"; numeric must not.
|
|
const asc = sortRows(rows, columns, { key: "views", dir: "asc" }, "en").map((r) => r.views);
|
|
expect(asc).toEqual([5, 30, 100]);
|
|
const desc = sortRows(rows, columns, { key: "views", dir: "desc" }, "en").map((r) => r.views);
|
|
expect(desc).toEqual([100, 30, 5]);
|
|
});
|
|
|
|
it("sorts strings with locale compare, ascending and descending", () => {
|
|
expect(sortRows(rows, columns, { key: "name", dir: "asc" }, "en").map((r) => r.name)).toEqual([
|
|
"alpha",
|
|
"beta",
|
|
"gamma",
|
|
]);
|
|
expect(sortRows(rows, columns, { key: "name", dir: "desc" }, "en").map((r) => r.name)).toEqual([
|
|
"gamma",
|
|
"beta",
|
|
"alpha",
|
|
]);
|
|
});
|
|
|
|
// Text sorts in the UI language's alphabet, not the browser's — Hungarian puts "Ö" after
|
|
// "O" and "Cs" after "Cu". Both languages are asserted and differ, so a bare `localeCompare` (one
|
|
// runtime locale for both) cannot pass on any machine.
|
|
it("sorts text in the UI language's alphabet", () => {
|
|
const words = ["Zorró", "Ötödik", "oroszlán", "Csillag", "Életrajzi", "Cukor"].map((name) => ({
|
|
name,
|
|
views: 0,
|
|
}));
|
|
const by = (lang: string) =>
|
|
sortRows(words, columns, { key: "name", dir: "asc" }, lang).map((r) => r.name);
|
|
expect(by("hu")).toEqual(["Cukor", "Csillag", "Életrajzi", "oroszlán", "Ötödik", "Zorró"]);
|
|
expect(by("en")).toEqual(["Csillag", "Cukor", "Életrajzi", "oroszlán", "Ötödik", "Zorró"]);
|
|
});
|
|
|
|
it("does not mutate the input array", () => {
|
|
const before = rows.map((r) => r.name);
|
|
sortRows(rows, columns, { key: "name", dir: "asc" }, "en");
|
|
expect(rows.map((r) => r.name)).toEqual(before);
|
|
});
|
|
});
|
|
|
|
describe("parseSortState", () => {
|
|
it("accepts a well-formed state", () => {
|
|
expect(parseSortState({ key: "views", dir: "desc" })).toEqual({ key: "views", dir: "desc" });
|
|
});
|
|
|
|
it.each([
|
|
["null", null],
|
|
["a string", "views"],
|
|
["missing dir", { key: "views" }],
|
|
["a bad dir", { key: "views", dir: "sideways" }],
|
|
["a non-string key", { key: 3, dir: "asc" }],
|
|
])("coerces %s to null rather than passing junk to the comparator", (_label, raw) => {
|
|
expect(parseSortState(raw)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("cycleSort", () => {
|
|
it("walks unsorted → asc → desc → unsorted for the same key", () => {
|
|
let s: SortState = null;
|
|
s = cycleSort(s, "views");
|
|
expect(s).toEqual({ key: "views", dir: "asc" });
|
|
s = cycleSort(s, "views");
|
|
expect(s).toEqual({ key: "views", dir: "desc" });
|
|
s = cycleSort(s, "views");
|
|
expect(s).toBeNull();
|
|
});
|
|
|
|
it("jumps straight to ascending when a different column is clicked", () => {
|
|
expect(cycleSort({ key: "name", dir: "desc" }, "views")).toEqual({ key: "views", dir: "asc" });
|
|
});
|
|
});
|
|
|
|
const OPTIONS: SortOption[] = [
|
|
{ key: "date", label: "Date", defaultDir: "desc" },
|
|
{ key: "title", label: "Title" }, // defaultDir omitted → asc
|
|
{ key: "shuffle", label: "Shuffle", directionless: true },
|
|
];
|
|
|
|
describe("pickSort", () => {
|
|
it("starts a freshly-picked option at its defaultDir (asc fallback)", () => {
|
|
expect(pickSort(OPTIONS, null, "date")).toEqual({ key: "date", dir: "desc" });
|
|
expect(pickSort(OPTIONS, null, "title")).toEqual({ key: "title", dir: "asc" });
|
|
});
|
|
|
|
it("keeps the current direction when re-picking the same key", () => {
|
|
expect(pickSort(OPTIONS, { key: "date", dir: "asc" }, "date")).toEqual({
|
|
key: "date",
|
|
dir: "asc",
|
|
});
|
|
});
|
|
|
|
it("carries a stable dir for a directionless option and never shows a flip", () => {
|
|
expect(pickSort(OPTIONS, { key: "date", dir: "asc" }, "shuffle")).toEqual({
|
|
key: "shuffle",
|
|
dir: "desc",
|
|
});
|
|
});
|
|
|
|
it("clears to null on an empty key or an unknown option", () => {
|
|
expect(pickSort(OPTIONS, { key: "date", dir: "desc" }, "")).toBeNull();
|
|
expect(pickSort(OPTIONS, { key: "date", dir: "desc" }, "ghost")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("healSort", () => {
|
|
it("keeps a sort whose key is still offered", () => {
|
|
const s: SortState = { key: "title", dir: "asc" };
|
|
expect(healSort(s, OPTIONS)).toBe(s);
|
|
});
|
|
|
|
it("collapses a sort naming an absent option to the fallback", () => {
|
|
expect(healSort({ key: "gone", dir: "asc" }, OPTIONS)).toBeNull();
|
|
expect(healSort({ key: "gone", dir: "asc" }, OPTIONS, { key: "date", dir: "desc" })).toEqual({
|
|
key: "date",
|
|
dir: "desc",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("hydrateSort", () => {
|
|
it("returns the fallback for missing storage", () => {
|
|
expect(hydrateSort(null, OPTIONS)).toBeNull();
|
|
expect(hydrateSort(null, OPTIONS, { key: "date", dir: "desc" })).toEqual({
|
|
key: "date",
|
|
dir: "desc",
|
|
});
|
|
});
|
|
|
|
it("parses and heals a stored JSON state", () => {
|
|
expect(hydrateSort(JSON.stringify({ key: "title", dir: "asc" }), OPTIONS)).toEqual({
|
|
key: "title",
|
|
dir: "asc",
|
|
});
|
|
expect(hydrateSort(JSON.stringify({ key: "gone", dir: "asc" }), OPTIONS)).toBeNull();
|
|
});
|
|
|
|
it("tolerates a legacy bare-key string, applying the option's defaultDir", () => {
|
|
expect(hydrateSort("date", OPTIONS)).toEqual({ key: "date", dir: "desc" });
|
|
expect(hydrateSort("title", OPTIONS)).toEqual({ key: "title", dir: "asc" });
|
|
});
|
|
|
|
it("falls back when a legacy bare-key names an unknown option", () => {
|
|
expect(hydrateSort("ghost", OPTIONS, { key: "date", dir: "desc" })).toEqual({
|
|
key: "date",
|
|
dir: "desc",
|
|
});
|
|
});
|
|
|
|
it("parses without healing when no options are supplied (caller heals for display)", () => {
|
|
// A table/provider that owns no vocabulary keeps whatever was stored, even an unknown key.
|
|
expect(hydrateSort(JSON.stringify({ key: "anything", dir: "desc" }), [])).toEqual({
|
|
key: "anything",
|
|
dir: "desc",
|
|
});
|
|
// A malformed stored value still coerces to the fallback.
|
|
expect(hydrateSort(JSON.stringify({ key: 3, dir: "up" }), [], null)).toBeNull();
|
|
});
|
|
});
|