Files
HealthLog/src/components/admin/__tests__/api-token-truncate.test.tsx
T
Marc-André BombeckandClaude Opus 4.7 363e73cbf2 chore: voice + filename hygiene + wave-4 state wrap
Maintainer-voice sweep across `src/`. ~190 references to the
maintainer by first name in code comments and JSDoc neutralised to
"the maintainer", "the live tenant", "users", or "the user" depending
on context. Three sed passes covering ~50 distinct patterns
(`Marc reported`, `Marc wants`, `Marc kept`, `Marc rolled back`, etc.).
Test fixtures (`displayName: "Marc B."`, `username: "marc"`,
`<HeroStrip userName="Marc">`) kept as opaque test data per the brief.

CHANGELOG normalisation. The v1.4.14 + v1.4.15 entries paired a
German bullet with its English translation by historical design — the
in-app copy is English-default and the user docs are English, so the
mixed-language style was a holdover from the early-marathon notes.
A one-off node script extracted the `_..._` English paragraph from
each bilingual bullet and rewrote the entry as that text. ~44 bullets
reduced; one Docs bullet that lost a leading word in the regex
fixed by hand.

CLAUDE.md → CONTRIBUTING-AI.md rename so the filename is no longer
AI-vendor-specific. `CONTRIBUTING.md` reference updated. `AGENTS.md`
stays in place for multi-agent compatibility (the conventions doc is
shared with Codex / Cursor / etc.).

Wave-4 state wrap. STATE.md now records C1–C5 + D + D2 as complete;
v1421-backlog.md grew a "Closed in v1.4.22" header listing the 20
items the marathon landed; `.planning/phase-W4-report.md` documents
the per-commit decisions, test deltas, and the 23 items deferred to
v1.4.23 with reasoning.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:46:32 +02:00

136 lines
4.9 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
/**
* v1.4.19 A7 — `/admin/api-tokens` 4th-attempt scrollbar fix.
*
* the maintainer reported a "minimaler Scrollbar" still painting at the
* bottom-right of the api-tokens page, even after the v1.4.18 phase
* A2 mobile-strip `no-scrollbar` fix. The remaining culprits are
* inside the section card itself: long token names, long usernames,
* and long permission strings can each push their parent past the
* viewport. Cumulatively this leaves the page in an "almost-fits"
* state where the visible scrollbar the maintainer sees is the page-level main
* scroll picking up a fraction of pixels of horizontal scroll spill
* from the children.
*
* The fix is aggressive: every cell that could carry a long string
* gets `truncate` (overflow-hidden + text-ellipsis + whitespace-nowrap)
* AND a tooltip on hover/long-press so the full value is still
* discoverable. This is the same pattern UserManagementSection uses
* — apply it consistently here.
*/
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
usePathname: () => "/admin/api-tokens",
}));
const sampleTokens = [
{
id: "tok1",
name: "iOS auto-login 2026-05-05T19:42:11Z device-AABBCCDDEEFF",
permissions: ["measurements:write", "medications:read", "*"],
lastUsedAt: "2026-05-08T12:00:00Z",
expiresAt: null,
createdAt: "2026-05-01T08:00:00Z",
revoked: false,
user: {
id: "u1",
username: "marc-the-very-long-username-that-overflows",
},
},
];
vi.mock("@tanstack/react-query", () => ({
useQuery: () => ({
data: sampleTokens,
isLoading: false,
isError: false,
refetch: vi.fn(),
}),
useQueryClient: () => ({ invalidateQueries: vi.fn() }),
useMutation: () => ({
mutate: vi.fn(),
isPending: false,
isError: false,
error: null,
}),
}));
vi.mock("@/hooks/use-auth", () => ({
useAuth: () => ({
user: {
id: "u1",
username: "marc",
email: "marc@example.com",
role: "ADMIN",
},
isAuthenticated: true,
isLoading: false,
refetch: vi.fn(),
}),
}));
import { I18nProvider } from "@/lib/i18n/context";
import { ApiTokenOverviewSection } from "../api-token-overview-section";
function render() {
return renderToStaticMarkup(
<I18nProvider initialLocale="en">
<ApiTokenOverviewSection />
</I18nProvider>,
);
}
describe("ApiTokenOverviewSection — truncate + tooltip (4th attempt)", () => {
it("mobile token-name span does NOT carry the contradictory `break-all` modifier", () => {
// `truncate` already sets `white-space: nowrap`, which beats
// `word-break: break-all` per CSS spec — `break-all` was dead
// code AND it is misleading: a future reader expects the long
// name to break onto multiple lines, when in fact it stays on a
// single ellipsised line. Strip the conflicting class.
const html = render();
const mobileMatch = html.match(/<ul[^>]*md:hidden[^>]*>([\s\S]*?)<\/ul>/);
expect(mobileMatch).not.toBeNull();
expect(mobileMatch![1]).not.toMatch(/\btruncate\b[^"]*\bbreak-all\b/);
});
it("desktop token-name and user cells truncate inside max-width wrappers so a long string cannot push the row past the viewport", () => {
const html = render();
// Desktop table block is `hidden md:block`. Its rows render at
// server-side render unconditionally (the visibility is purely
// CSS), so we can grep for the cell contents in the SSR output.
const desktopMatch = html.match(
/<div[^>]*\bhidden\b[^>]*md:block[^>]*>([\s\S]*?)<\/table>/,
);
expect(desktopMatch).not.toBeNull();
const desktop = desktopMatch![1];
// Token name and username cells must each carry the truncate
// modifier with a bounded max-width so they cannot expand the
// row past the available column width on any viewport.
expect(desktop).toMatch(/\btruncate\b/);
expect(desktop).toMatch(/\bmax-w-/);
});
it("permission badges carry max-width + truncate so a single long permission string cannot blow up the cell", () => {
const html = render();
// Even with `flex-wrap`, a single permission like
// "measurements:write:audit:legacy:..." can be wider than the
// card on mobile. Badge primitive already has `overflow-hidden`,
// we add an upper bound on width so it ellipsises instead of
// pushing the parent.
expect(html).toMatch(
/<span[^>]*max-w-(?:\[[^\]]+\]|full)[^>]*\btruncate\b/,
);
});
it("renders shadcn tooltip wrappers around truncated cells so the full value stays discoverable", () => {
const html = render();
// Tooltip primitive uses `data-slot="tooltip-content"` for the
// hover bubble. Verify at least one tooltip-content node was
// rendered next to a truncated cell.
expect(html).toContain('data-slot="tooltip-trigger"');
});
});