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.
47 lines
2.4 KiB
TypeScript
47 lines
2.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { eslint, uiFiles } from "./lintProbe.testkit";
|
|
|
|
// `local/button-primitive`'s disabled check (eslint.config.js): a raw `<button disabled>` throws a
|
|
// focused keyboard user to <body> the moment it turns disabled. ~60 were swept onto ui/FocusSafeButton;
|
|
// this pins that the check keeping a new one out fires — also inside a control primitive and a test,
|
|
// where the rest of the rule stands aside.
|
|
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 === "local/button-primitive" && m.messageId === "disabled",
|
|
).length;
|
|
}
|
|
|
|
const DISABLED = "export const D = (p: { d: boolean }) => <button disabled={p.d}>x</button>;\n";
|
|
|
|
describe("a raw button that can be disabled", () => {
|
|
it("is refused, statically or not", async () => {
|
|
expect(
|
|
await hits("export const A = ({ p }: { p: boolean }) => <button disabled={p}>x</button>;\n"),
|
|
).toBe(1);
|
|
expect(await hits("export const B = () => <button disabled>x</button>;\n")).toBe(1);
|
|
});
|
|
|
|
it("is refused inside a control primitive and in a test too", async () => {
|
|
expect(await hits(DISABLED, "src/components/ui/Button.tsx")).toBe(1);
|
|
// an existing test file: the project service admits only lintProbe.testkit's probe names
|
|
expect(await hits(DISABLED, "src/modules/feed/Feed.countStale.dom.test.tsx")).toBe(1);
|
|
});
|
|
|
|
it("leaves a raw button with no disabled, and the two native-disabled owners, alone", async () => {
|
|
expect(await hits("export const C = () => <button onClick={() => {}}>x</button>;\n")).toBe(0);
|
|
expect(await hits(DISABLED, "src/components/ui/FocusSafeButton.tsx")).toBe(0);
|
|
expect(await hits(DISABLED, "src/components/ui/AnchoredPopover.tsx")).toBe(0);
|
|
});
|
|
|
|
// The exemption lives in the rule body, where neither the gate snapshot nor docs/gates.md sees it,
|
|
// so this holds it to exactly those two: every other file in ui/ is refused.
|
|
it("is exempt in exactly those two of the ui/ files", { timeout: 60_000 }, async () => {
|
|
const exempt: string[] = [];
|
|
for (const file of uiFiles())
|
|
if ((await hits(DISABLED, `src/components/ui/${file}`)) === 0) exempt.push(file);
|
|
expect(exempt).toEqual(["AnchoredPopover.tsx", "FocusSafeButton.tsx"]);
|
|
});
|
|
});
|