Files
ApiCore/ApiCore-Frontend/scripts/codemod-t5-focus.spec.mjs
T
Nick.RosenloecherandClaude Opus 5.5 ce68729233 feat(t5): Fokusring für alle, 44-px-Ziele bei Touch, 16-px-Schrift (T5a A5)
- globals.css: eine Basisregel :where(:focus-visible) mit --focus-width
  und --focus-offset (Sonne 3 px), Firefox-:-moz-focusring gleichgezogen;
  --text-xs 0.8125rem.
- Fokus-Codemod (scripts/codemod-t5-focus.mjs): eigene Ringe und
  outline-none raus, sm:h-/sm:size- zu pointer-fine:, Label-Zeilen mit
  focus-ring-within; Skript-Fokus (Lücken-Liste, „Gespeichert“) bleibt.
- Button/Input 44 px, kompakt nur mit Maus; Icon-Knöpfe mit unsichtbarer
  44-px-Trefferfläche bei Touch, Nachbarn mit genug Abstand.
- 16 px in Feldern, Labels, Auswahl, Leerzuständen, Listentiteln.
- .holo-stage auf allen Beute-Bühnen, .stage-card holt den Seitenring.
- Baukasten-Wächter (src/styles/baukasten-guard.spec.ts) mit den Regeln
  aus A3–A5, Ausnahmen mit Grund; Kalender-Auswahl als Inset-Ring.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-16 18:06:20 +02:00

122 lines
5.4 KiB
JavaScript

import { readdirSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { transform } from "./codemod-t5-focus.mjs";
describe("codemod t5 focus and sizes (spec 4.5, 4.6, 10.8 step 5)", () => {
it("drops outline-none and the component focus ring classes", () => {
expect(
transform(
'"min-h-11 rounded-lg px-2 outline-none hover:bg-muted focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:opacity-60"',
),
).toBe('"min-h-11 rounded-lg px-2 hover:bg-muted disabled:opacity-60"');
expect(transform('className="px-2 focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none"')).toBe(
'className="px-2"',
);
});
it("drops the own outline rings, including the stage edge ring of the hive glass", () => {
expect(transform('"rounded-full focus-visible:outline-2 focus-visible:outline-ring"')).toBe('"rounded-full"');
expect(
transform("`h-11 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-holo-edge-strong ${X}`"),
).toBe("`h-11 ${X}`");
});
it("keeps the focus: outline of elements focused by script after a click", () => {
// :focus-visible does not match a script focus that follows a pointer click.
const scripted = '"rounded-xl border p-3 focus:outline-2 focus:outline-ring"';
expect(transform(scripted)).toBe(scripted);
});
it("drops the skip link's own focus ring", () => {
expect(transform('"sr-only focus:not-sr-only focus:ring-3 focus:ring-ring/50 focus:outline-none"')).toBe(
'"sr-only focus:not-sr-only"',
);
});
it("drops variant focus colours on the destructive button and badges", () => {
expect(transform('"bg-destructive/10 text-destructive focus-visible:border-destructive/40 focus-visible:ring-destructive/20"')).toBe(
'"bg-destructive/10 text-destructive"',
);
expect(transform('"bg-warning/15 focus-visible:ring-warning/30 [a]:hover:bg-warning/25"')).toBe(
'"bg-warning/15 [a]:hover:bg-warning/25"',
);
});
it("a row with an inner focus target keeps one ring on the outer element", () => {
expect(transform('"flex gap-3 rounded-xl border p-3 has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-ring"')).toBe(
'"flex gap-3 rounded-xl border p-3 focus-ring-within"',
);
expect(transform('"flex min-h-14 px-3 has-[:focus-visible]:ring-3 has-[:focus-visible]:ring-ring/50"')).toBe(
'"flex min-h-14 px-3 focus-ring-within"',
);
expect(transform('"flex min-h-11 px-1 has-focus-visible:ring-3 has-focus-visible:ring-ring/50"')).toBe(
'"flex min-h-11 px-1 focus-ring-within"',
);
});
it("selection outlines that are not focus styles stay", () => {
const calendar = 'isSelected && "outline-2 -outline-offset-2 outline-foreground",';
expect(transform(calendar)).toBe(calendar);
const canvas = 'selected && "outline outline-2 outline-foreground",';
expect(transform(canvas)).toBe(canvas);
const menuItem = '"relative flex rounded-md text-sm outline-hidden select-none focus:bg-accent"';
expect(transform(menuItem)).toBe(menuItem);
});
it("a class string that only held focus classes disappears with its line", () => {
expect(transform('cn(\n "min-h-11 px-2",\n "focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50",\n x,\n)')).toBe(
'cn(\n "min-h-11 px-2",\n x,\n)',
);
});
it("sm:h-* and sm:min-h-* become pointer-fine, also behind a further variant", () => {
expect(transform('"h-10 sm:h-8 gap-1.5"')).toBe('"h-10 pointer-fine:h-8 gap-1.5"');
expect(transform('"min-h-11 sm:min-h-9 rounded-lg"')).toBe('"min-h-11 pointer-fine:min-h-9 rounded-lg"');
expect(transform('"data-[size=default]:h-11 sm:data-[size=default]:h-9"')).toBe(
'"data-[size=default]:h-11 pointer-fine:data-[size=default]:h-9"',
);
});
it("sm:size-* becomes pointer-fine:size-*", () => {
expect(transform('className="size-11 sm:size-8"')).toBe('className="size-11 pointer-fine:size-8"');
});
it("other sm: classes stay", () => {
const layout = '"hidden sm:flex sm:hidden sm:gap-2 sm:px-4 sm:max-w-xs sm:max-h-96"';
expect(transform(layout)).toBe(layout);
});
it("keeps CRLF line endings", () => {
expect(transform('a\r\nclassName="px-2 outline-none"\r\n')).toBe('a\r\nclassName="px-2"\r\n');
});
it("the focus codemod is idempotent and keeps pointer-fine classes", () => {
const source = [
'"h-11 sm:h-8 outline-none focus-visible:ring-3 focus-visible:ring-ring/50"',
'"pointer-fine:h-9 has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-ring"',
].join("\n");
const once = transform(source);
expect(once).toBe('"h-11 pointer-fine:h-8"\n"pointer-fine:h-9 focus-ring-within"');
expect(transform(once)).toBe(once);
});
it("leaves nothing to migrate in src", () => {
const files = sourceFiles(join(process.cwd(), "src"));
expect(files.length).toBeGreaterThan(100);
const pending = files.filter((file) => {
const text = readFileSync(file, "utf8");
return transform(text) !== text;
});
expect(pending).toEqual([]);
});
});
function sourceFiles(dir) {
return readdirSync(dir).flatMap((name) => {
const path = join(dir, name);
if (statSync(path).isDirectory()) return sourceFiles(path);
return /\.tsx?$/.test(name) && !/\.spec\./.test(name) ? [path] : [];
});
}