mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
Thirty-three hand-rolled underlined text links and buttons (Open schedule, Select all, View all, Retry, Learn more, the device-code copy action, ...) carried three different focus rings, none on half of them, and a 16 px target on a phone. They now render through one InlineAction primitive: tone primary / muted / foreground, size xs / sm, a focus ring, and min-h-11 on a coarse pointer so the desktop layout does not move. A structural test refuses a new hand-rolled copy.
122 lines
4.1 KiB
TypeScript
122 lines
4.1 KiB
TypeScript
/**
|
|
* A small text action inside a line of content goes through `InlineAction`
|
|
* (`src/components/ui/inline-action.tsx`).
|
|
*
|
|
* Thirty-odd hand-rolled copies of the same "underlined small text link"
|
|
* carried three different focus rings, none at all on half of them, and a
|
|
* 16 px touch target on a phone. The primitive owns the 44 px floor on a
|
|
* coarse pointer and the ring; this guard keeps a new copy from appearing
|
|
* beside it.
|
|
*
|
|
* Matched: a `<button>`, `<Link>` or `<a>` whose static `className` combines
|
|
* the inline-action signature — an underline (`underline` / `hover:underline`)
|
|
* with a small text size (`text-xs` / `text-sm`) and an action tone
|
|
* (`text-primary` / `text-muted-foreground`). A full-width row link or a
|
|
* framed button does not have that combination.
|
|
*
|
|
* Not matched (stated, not hidden): a class list assembled at runtime, and a
|
|
* copy without an underline. The guard catches the idiom, not every way to
|
|
* draw a small link.
|
|
*/
|
|
import { describe, expect, it } from "vitest";
|
|
import { readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
import { walkSourceFiles } from "./helpers/source-files";
|
|
|
|
const SRC = join(__dirname, "..");
|
|
|
|
/**
|
|
* Sites that look like an inline action but are something else, each with
|
|
* the reason. Keyed by `path:tag`.
|
|
*/
|
|
const ALLOWED: Record<string, string> = {
|
|
// A full-width footer row of the glucose clinical panel: icon plus a
|
|
// wrapping sentence under a border, read as part of the card body.
|
|
"components/insights/glucose/glucose-clinical-panel.tsx:Link":
|
|
"full-width footer row, not an inline action",
|
|
};
|
|
|
|
const OPENER_RE = /<(button|Link|a)\b((?:[^>{]|\{[^}]*\})*?)>/g;
|
|
const CLASS_RE = /className=(?:"([^"]*)"|\{cn\(\s*"([^"]*)")/;
|
|
|
|
function isInlineActionShape(cls: string): boolean {
|
|
return (
|
|
/(?:^|\s)(?:hover:)?underline(?:\s|$)/.test(cls) &&
|
|
/(?:^|\s)text-(?:xs|sm)(?:\s|$)/.test(cls) &&
|
|
/(?:^|\s)text-(?:primary|muted-foreground)(?:\s|$)/.test(cls)
|
|
);
|
|
}
|
|
|
|
function findHandRolledInlineActions(
|
|
files: { path: string; source: string }[],
|
|
): string[] {
|
|
const hits: string[] = [];
|
|
for (const { path, source } of files) {
|
|
for (const match of source.matchAll(OPENER_RE)) {
|
|
const cls = CLASS_RE.exec(match[2]);
|
|
if (!cls) continue;
|
|
if (!isInlineActionShape(cls[1] ?? cls[2] ?? "")) continue;
|
|
hits.push(`${path}:${match[1]}`);
|
|
}
|
|
}
|
|
return hits;
|
|
}
|
|
|
|
describe("small text actions go through InlineAction", () => {
|
|
const files = walkSourceFiles(SRC, { floor: 1200, extensions: [".tsx"] })
|
|
.filter(
|
|
(p) =>
|
|
(p.startsWith("components/") || p.startsWith("app/")) &&
|
|
!p.startsWith("components/ui/") &&
|
|
!p.includes("__tests__/"),
|
|
)
|
|
.map((path) => ({ path, source: readFileSync(join(SRC, path), "utf8") }));
|
|
|
|
it("walks the UI tree", () => {
|
|
expect(files.length).toBeGreaterThan(800);
|
|
});
|
|
|
|
it("has no hand-rolled copy outside the allowlist", () => {
|
|
const hits = findHandRolledInlineActions(files).filter(
|
|
(key) => !(key in ALLOWED),
|
|
);
|
|
expect(hits).toEqual([]);
|
|
});
|
|
|
|
it("keeps every allowlist entry live", () => {
|
|
const hits = new Set(findHandRolledInlineActions(files));
|
|
for (const key of Object.keys(ALLOWED)) {
|
|
expect(hits.has(key), key).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("recognises the idiom it exists to refuse", () => {
|
|
const planted = [
|
|
{
|
|
path: "components/x.tsx",
|
|
source:
|
|
'<Link href="/a" className="text-primary text-xs underline-offset-2 hover:underline">Go</Link>',
|
|
},
|
|
{
|
|
path: "components/y.tsx",
|
|
source:
|
|
'<button type="button" onClick={() => go()} className="text-muted-foreground hover:text-foreground text-sm underline">Go</button>',
|
|
},
|
|
{
|
|
path: "components/z.tsx",
|
|
source: '<InlineAction tone="muted">Go</InlineAction>',
|
|
},
|
|
];
|
|
expect(findHandRolledInlineActions(planted)).toEqual([
|
|
"components/x.tsx:Link",
|
|
"components/y.tsx:button",
|
|
]);
|
|
});
|
|
|
|
it("is used where the copies were", () => {
|
|
const users = files.filter((f) => f.source.includes("<InlineAction"));
|
|
expect(users.length).toBeGreaterThanOrEqual(25);
|
|
});
|
|
});
|