Files
siftlode/frontend/src/lib/plexImage.contract.test.ts
T
peter d44d8d6dda fix(gate): knip sees the watch entry, the build must emit watch.html, and two hand copies are held
- The raw-source contract tests read files from disk (rawSources.testkit)
  instead of import.meta.glob, which knip counted as importing every src file —
  its unused-files lane could never name an orphan. knip now names
  src/watchMain.tsx as an entry (registered in docs/gates.md).
- vite build fails when index.html or watch.html is not emitted.
- The watch-entry import walk follows dynamic import() too.
- audit_metrics.py's plan-code patterns are held equal to the guard's.
- CrashBoundary is domain-free: it moves into components/ui.
2026-10-06 03:19:33 +02:00

79 lines
3.4 KiB
TypeScript

import { readFileSync } from "node:fs";
import { expect, it } from "vitest";
import { cardImage } from "./plexImage";
import { readSources } from "./rawSources.testkit";
// A card's srcset names each Plex cut by its width, and the browser picks one by that width. The
// widths are the image proxy's (backend `app/plex/imgcache.py` SIZES), with no wire format between
// the two: a backend-only change would have the browser pick a cut by a width it does not have.
// Read from source, as plexFinishMargin.contract does.
const source = readFileSync(
new URL("../../../backend/app/plex/imgcache.py", import.meta.url),
"utf8",
);
const backendWidth = (variant: string) => {
const m = new RegExp(`"${variant}": \\((\\d+), \\d+\\)`).exec(source);
// If this fails the variant was renamed or moved: re-point this test, the invariant stayed.
expect(m, `${variant} not in imgcache.SIZES`).not.toBeNull();
return Number(m![1]);
};
const srcsetWidths = (url: string) =>
Object.fromEntries(
cardImage(url)
.srcSet!.split(", ")
.map((c) => {
const [u, w] = c.split(" ");
return [
new URL(u!, "http://x").searchParams.get("variant") ?? "thumb",
Number(w!.slice(0, -1)),
];
}),
);
it("names each poster cut by the width the proxy cuts it at", () => {
expect(srcsetWidths("/api/plex/image/1")).toEqual({
thumb_sm: backendWidth("thumb_sm"),
thumb: backendWidth("thumb"),
});
});
it("names each backdrop cut by the width the proxy cuts it at", () => {
expect(srcsetWidths("/api/plex/image/1?variant=art")).toEqual({
art_sm: backendWidth("art_sm"),
art: backendWidth("art"),
});
});
// Every small Plex picture asks for its card-size cut through cardImage (the
// playlist rows, the search box and the collection picker still fetched the 400px poster for a 32px
// picture). A plain `src={…}` is allowed only where the full cut is the point or the URL is not the
// proxy's; the SET is pinned, so a new one fails here. Every <img>'s src counts, wherever it sits
// among the attributes and whatever its variable is called: the first cut of this test read only a
// leading `src={…thumb…}` and missed two of the three sites it was written for.
const plexFiles = readSources(__dirname, [
"../components/{Plex,plex}*.tsx",
"../components/{Home,PosterTile,UpNextCard}.tsx",
"!../components/**/*.test.tsx",
]);
const ALLOWED = new Set([
"plexCards.tsx pic.src", // cardImage's own src (srcSet/sizes beside it): the card cut
"plexCards.tsx c.thumb", // cast photos: the person-image route, not the image proxy
"PlexBrowse.tsx p.photo", // the person filter chip's headshot: the person-image route
"plexViews.tsx d.photo", // the person page's headshot: the same
"PlexInfo.tsx c.thumb", // the same cast photos on the detail page
"PlexInfo.tsx detail.thumb", // the detail page's own poster: the full cut is the point
"plexDetailUi.tsx thumb", // the show/season header poster: the same
"Home.tsx card.thumb", // the hero (full width) and the shelf's own tiles (YouTube channel art)
]);
it("leaves no small Plex picture on the full cut", () => {
expect(Object.keys(plexFiles).length).toBeGreaterThan(5);
const bare = new Set<string>();
for (const [path, src] of Object.entries(plexFiles))
for (const m of (src as string).matchAll(/<img\b[^>]*?\bsrc=\{([^}]*)\}/g))
bare.add(`${path.split("/").pop()} ${m[1]!.trim()}`);
expect([...bare].filter((b) => !ALLOWED.has(b))).toEqual([]);
});