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.
109 lines
4.0 KiB
TypeScript
109 lines
4.0 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// The Plex playlist header's meta line — the count and the playlist's total runtime, mirroring the
|
|
// YouTube playlist header — was never rendered by a test, so a regression in the sum shipped green.
|
|
// The runtime sums the WHOLE list, an unknown duration counts as nothing, and a list
|
|
// nothing sums for says no runtime at all.
|
|
import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
|
|
import { render, cleanup, screen, fireEvent, waitFor } from "@testing-library/react";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
|
|
vi.mock("react-i18next", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("react-i18next")>()),
|
|
useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }),
|
|
}));
|
|
|
|
import PlexPlaylistView from "./PlexPlaylistView";
|
|
import { api, type PlexCard } from "../lib/api";
|
|
|
|
beforeAll(() => {
|
|
class Stub {
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
takeRecords() {
|
|
return [];
|
|
}
|
|
}
|
|
globalThis.ResizeObserver ??= Stub as unknown as typeof ResizeObserver;
|
|
globalThis.IntersectionObserver ??= Stub as unknown as typeof IntersectionObserver;
|
|
});
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
// An unknown runtime is an ABSENT field: `5400 + null` is 5400 in JS, so null would prove nothing
|
|
// about the sum's guard; `undefined` turns an unguarded sum into NaN.
|
|
const movie = (id: string, duration_seconds?: number) =>
|
|
({
|
|
id,
|
|
type: "movie",
|
|
title: `Movie ${id}`,
|
|
year: 2020,
|
|
thumb: "",
|
|
duration_seconds,
|
|
}) as unknown as PlexCard;
|
|
|
|
async function meta(items: PlexCard[]) {
|
|
vi.spyOn(api, "plexPlaylist").mockResolvedValue({ id: 5, title: "Films", items } as never);
|
|
render(
|
|
<QueryClientProvider
|
|
client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}
|
|
>
|
|
<PlexPlaylistView playlistId={5} onBack={() => {}} onPlay={() => {}} />
|
|
</QueryClientProvider>,
|
|
);
|
|
await screen.findByText(`Movie ${items[0]!.id}`);
|
|
return screen.getByText(
|
|
(_, el) => /^plex\.browse\.items/.test(el?.textContent ?? "") && el?.childElementCount === 0,
|
|
).textContent;
|
|
}
|
|
|
|
/** Pick `label` from the menu `open` opens. The surface is visibility:hidden in jsdom (floating-ui
|
|
* never positions it), so the row is matched on its text. */
|
|
async function pickFromMenu(open: () => void, label: string) {
|
|
open();
|
|
const row = await waitFor(() => {
|
|
const found = screen
|
|
.getAllByRole("menuitem", { hidden: true })
|
|
.find((b) => b.textContent === label);
|
|
expect(found).toBeTruthy();
|
|
return found!;
|
|
});
|
|
fireEvent.click(row);
|
|
}
|
|
|
|
describe("PlexPlaylistView · the header's meta line", () => {
|
|
it("sums the known runtimes and counts an unknown one as nothing", async () => {
|
|
expect(await meta([movie("1", 5400), movie("2"), movie("3", 1800)])).toBe(
|
|
"plex.browse.items · 2 hr 0 min",
|
|
);
|
|
});
|
|
|
|
it("drops the runtime when nothing sums", async () => {
|
|
expect(await meta([movie("1")])).toBe("plex.browse.items");
|
|
});
|
|
});
|
|
|
|
// Enter on an unchanged (or emptied) title wrote the same name back to Plex, and an empty
|
|
// one armed the focus hand-back while leaving the field open. Now both close with no write, as the
|
|
// collection rename does.
|
|
describe("PlexPlaylistView · rename", () => {
|
|
it("closes without a write when the title is unchanged or empty", async () => {
|
|
const write = vi.spyOn(api, "plexRenamePlaylist").mockResolvedValue({} as never);
|
|
await meta([movie("1", 5400)]);
|
|
for (const value of [null, " "]) {
|
|
await pickFromMenu(
|
|
() => fireEvent.click(screen.getByRole("button", { name: "plex.playlist.menu" })),
|
|
"plex.playlist.rename",
|
|
);
|
|
const field = await screen.findByRole("textbox", { name: "plex.playlist.rename" });
|
|
if (value !== null) fireEvent.change(field, { target: { value } });
|
|
fireEvent.keyDown(field, { key: "Enter" });
|
|
await screen.findByRole("heading", { name: "Films" });
|
|
}
|
|
expect(write).not.toHaveBeenCalled();
|
|
});
|
|
});
|