Files
siftlode/frontend/src/lib/controlRules.contract.test.ts
T
peter 383d394e32 fix(ui): a picture that fails to load shows the card's own frame, then retries
A bare <img> sent a Referer that Google's image CDNs refuse and had no retry and no fallback, so a
Home shelf card showed the browser's broken-image mark. ui/RemoteImage gathers what Avatar, the
channel banner and the download rows had each hand-rolled: no Referer, up to three retries past the
browser's negative cache, the caller's fallback (by default the card's empty frame) instead of the
broken mark, the failure remembered by URL. Every picture now goes through it; a lint rule refuses
a bare <img>.
2026-10-14 20:51:08 +02:00

82 lines
3.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { eslint } from "./lintProbe.testkit";
// Two lint rules about controls: no native <select> (the canonical "FE control" row in CLAUDE.md;
// six had drifted in before the rule existed), and no hover gated on `enabled:` alone. A selector
// that matches nothing looks exactly like a clean codebase, so this pins that each fires.
async function refused(source: string): Promise<number> {
const [result] = await eslint.lintText(source, { filePath: "src/components/__probe.tsx" });
return (result?.messages ?? []).filter((m) => m.message.startsWith("No native <select>")).length;
}
describe("a native select", () => {
it("is refused; the house controls are not", async () => {
expect(await refused("export const A = () => <select />;\n")).toBe(1);
expect(await refused("export const B = () => <SegmentedToggle />;\n")).toBe(0);
});
});
// A bare <img> left the broken-image mark on a card whose picture failed (ui/RemoteImage's header).
describe("a bare img", () => {
it("is refused; ui/RemoteImage is not", async () => {
const lint = async (source: string) =>
(
await eslint.lintText(source, { filePath: "src/components/__probe.tsx" })
)[0]?.messages.filter((m) => m.message.startsWith("No bare <img>")).length;
expect(await lint('export const A = () => <img src="x" alt="" />;\n')).toBe(1);
expect(await lint('export const B = () => <RemoteImage src="x" alt="" />;\n')).toBe(0);
});
});
// `enabled:hover:` is blind to a focused FocusSafeButton (aria-disabled, not the native attribute; `act:` is not),
// which then lit up on hover as if a click would work.
describe("a hover gated on `enabled:` alone", () => {
it("is refused; the aria-aware gate is not", async () => {
const lint = async (cls: string) =>
(
await eslint.lintText(
// eslint-disable-next-line no-restricted-syntax -- probe SOURCE text handed to ESLint, not a class
`export const c = "p-1 ${cls}:text-fg";
`,
{
filePath: "src/components/__probe.tsx",
},
)
)[0]?.messages.filter((m) => m.message.startsWith("`enabled:hover:`")).length;
expect(await lint("enabled:hover")).toBe(1);
expect(await lint("act:hover")).toBe(0);
});
});
// A variant glued onto an interpolated prefix is never seen whole by Tailwind's scanner, so it ships
// with no CSS (every button and chip hover colour died that way). A plain interpolated value is fine.
describe("a class split by an interpolation", () => {
const split = async (src: string) =>
(await eslint.lintText(src, { filePath: "src/components/__probe.tsx" }))[0]?.messages.filter(
(m) => m.message.startsWith("a class split by an interpolation"),
).length;
const act = "enabled:"; // assembled, as the offender was
it("is refused after an expression", async () => {
expect(await split("const A = 'x';\nexport const b = `${A}" + "hover:text-fg`;\n")).toBe(1);
expect(await split("const A = 'x';\nexport const c = `p-1 ${A}" + act + "hover:x`;\n")).toBe(1);
});
it("is refused when the interpolation is the variant itself", async () => {
expect(
await split("const N = '@max-[40rem]/r';\nexport const f = `w-28 ${N}" + ":w-24`;\n"),
).toBe(1);
});
it("leaves a clock alone", async () => {
expect(await split("const m = 1;\nexport const g = `${m}:00 ${m}:${m}`;\n")).toBe(0);
});
it("leaves a whole class, a leading variant and a plain value alone", async () => {
expect(await split("const n = 1;\nexport const d = `hover:text-fg w-${n} ${n}px`;\n")).toBe(0);
expect(
await split(
"const u = 'h';\nexport const e = `${u}://x:${n()}`;\nfunction n() { return 1; }\n",
),
).toBe(0);
});
});