Under a busy machine (the gate's lanes side by side, 12 busy cores) these went red at random: a press landed while the button was still busy, an answer was settled before it was asked for, a page grew past one wait's budget, a lazy chunk was transformed inside a 1 s wait, focus was taken a frame after the press. They now wait for the condition they depend on, preload the chunks they open, and the tests that do heavy work (whole-repo scans, the type-aware linter, module resets) carry an explicit budget naming that work. The whole suite four times under 12 busy-loop processes: red 4/4 before, 0/4 after.
94 lines
3.7 KiB
TypeScript
94 lines
3.7 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", () => {
|
||
// Each new path's first lint makes the type-aware project service load it (0.4–1 s apiece, a repeat
|
||
// ~0): this case lints four, ~1.4 s idle, past 5 s on a busy machine.
|
||
it(
|
||
"is refused in a feature component, the lab and lib, raw or focus-safe",
|
||
{ timeout: 30_000 },
|
||
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);
|
||
});
|
||
});
|