Files
siftlode/frontend/src/lib/buttonPrimitive.contract.test.ts
T
peter d1bbe4524c fix(gate): close the gaps in the plan-code guard, the button lists and the tooling
- planCodes: the quote and slash lookarounds let the commonest form through (a docstring or test
  title opening with a code, a slash pair); the two real exceptions (an episode titled "E1", ruff's
  E4/E7/E9) are now named and cut first, with cases that fail on the old pattern. audit_metrics
  counts by the same definition.
- button-primitive: both file lists live in the rule body, out of sight of the snapshot and the
  register; each is now pinned to its exact set of ui/ files (red when either is widened).
- check.sh --brief shows a short log whole and keeps the line before each hit, so ruff's rule and
  eslint's file path survive the excerpt.
- lint-changed runs prettier over every changed frontend file, as the gate does, not only code.
- CLAUDE.md and gates.md no longer claim the basedpyright baseline was found harmless: it was never
  triaged, and the reportOptional* entries are the likeliest real bugs.
2026-10-05 00:19:50 +02:00

88 lines
3.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { eslint, uiFiles } from "./lintProbe.testkit";
// `local/button-primitive` (eslint.config.js): one primitive per button family (docs/DESIGN.md).
// 268 hand-rolled buttons were swept onto ui/Button, IconButton, Pressable, Chip and
// DragHandle; this pins that the rule keeping a new one out actually fires, and where it stands down.
const RULE = "local/button-primitive";
async function hits(source: string, filePath = "src/components/__probe.tsx"): Promise<number> {
const [result] = await eslint.lintText(source, { filePath });
return (result?.messages ?? []).filter((m) => m.ruleId === RULE).length;
}
describe("a hand-rolled button", () => {
it("is refused in a feature component, the lab and lib, raw or focus-safe", async () => {
const raw = 'export const A = () => <button className="px-3">x</button>;\n';
expect(await hits(raw)).toBe(1);
expect(await hits(raw, "src/design-lab/__probe.tsx")).toBe(1);
expect(await hits(raw, "src/lib/__probe.tsx")).toBe(1);
// …and in a ui/ COMPOSITE: a pager or a side panel is built from the primitives too.
expect(await hits(raw, "src/components/ui/Pager.tsx")).toBe(1);
expect(
await hits(
'import { FocusSafeButton } from "./ui/FocusSafeButton";\nexport const B = () => <FocusSafeButton>x</FocusSafeButton>;\n',
),
).toBe(1);
});
it("is allowed only in a control primitive, and the primitives pass everywhere", async () => {
for (const file of ["IconButton.tsx", "SegmentedToggle.tsx", "AnchoredPopover.tsx"])
expect(
await hits("export const C = () => <button>x</button>;\n", `src/components/ui/${file}`),
).toBe(0);
// Named like a primitive but outside ui/: still refused.
expect(
await hits("export const E = () => <button>x</button>;\n", "src/components/Button.tsx"),
).toBe(1);
expect(
await hits(
'import { Button } from "./ui/Button";\nexport const D = () => <Button>x</Button>;\n',
),
).toBe(0);
});
// The primitive list lives in the rule body, where neither the gate snapshot nor docs/gates.md
// sees it, so this holds it to exactly these files of ui/.
it("is allowed in exactly the control primitives of ui/", { timeout: 60_000 }, async () => {
const allowed: string[] = [];
for (const file of uiFiles())
if (
(await hits(
'export const R = () => <button type="button">x</button>;\n',
`src/components/ui/${file}`,
)) === 0
)
allowed.push(file);
expect(allowed).toEqual([
"AnchoredPopover.tsx",
"Button.tsx",
"Chip.tsx",
"Combobox.tsx",
"DragHandle.tsx",
"FilterSelect.tsx",
"FocusSafeButton.tsx",
"IconButton.tsx",
"Pressable.tsx",
"PriorityStepper.tsx",
"SegmentedToggle.tsx",
"SelectTrigger.tsx",
"SortControl.tsx",
"Tabs.tsx",
"form.tsx",
]);
});
it("is refused spelled as an element with the button role; a component's role is its own", async () => {
expect(await hits('export const F = () => <div role="button" tabIndex={0}>x</div>;\n')).toBe(1);
expect(await hits('export const G = () => <span role="button">x</span>;\n')).toBe(1);
expect(await hits('export const H = () => <div role="group">x</div>;\n')).toBe(0);
expect(
await hits(
'import { Pressable } from "./ui/Pressable";\nexport const I = () => <Pressable role="button">x</Pressable>;\n',
),
).toBe(0);
});
});