Files
siftlode/frontend/src/lib/plexImage.contract.test.ts
T
peter 63a0fecc8b refactor(frontend): the flat components/ folder moves into shell/, modules/<page>/ and features/<name>/
A pure move (git mv, history kept): 333 files leave components/, which now holds only the platform
layer (ui/) and the legal pages. The shell is the frame (App, header, nav rail, palette, boot); each
page lives in modules/<page>/ with its rail, store and tests; what several pages share sits in
features/<name>/; the shared-link page is watch/. Domain-free helpers went straight to ui/
(ViewSwitcher, FilterRail, the confirm dialog, the language switcher, the copy rows).

Every relative specifier was rewritten (imports, dynamic imports, vi.mock, new URL), and the
contract tests that scan sources now scan the whole app source, not only components/ — each glob
was counted before and after so none went green over fewer files. The locale-format rule matched
`/src/components/` only, so the move would have taken ~300 files out from under its i18n-singleton
check; it now covers every folder that holds components, with a test per folder.
2026-10-22 23:12:45 +02:00

81 lines
3.5 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, [
"../modules/plex/{Plex,plex}*.tsx",
"../features/plexCards/{Plex,plex}*.tsx",
"../modules/home/Home.tsx",
"../modules/plex/{PosterTile,UpNextCard}.tsx",
"!../**/*.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([]);
});