Files
HealthLog/src/components/admin/__tests__/user-management-responsive.test.tsx
T
Marc-André BombeckandClaude Opus 4.7 41945b2120 fix(test): de-flake e2e a11y suite by forcing dark colorScheme
A2's overview redesign (`<SystemStatusSummary>`, `<RecentAuditPreview>`)
hard-codes Dracula `text-dracula-green` (#50fa7b) on `bg-card` /
`bg-muted/50`. HealthLog ships dark mode as the default (CLAUDE.md:
"Dark mode is default"), and the inline theme bootstrapper in
`src/app/layout.tsx` resolves the active theme via
`prefers-color-scheme: dark`. Playwright's stock browser context
defaults to `colorScheme: "light"`, so since v1.4.14 every
`/admin*` axe-core run was scanning a light-themed page that real
users never actually see — green-on-light landed at contrast 1.18,
WCAG AA wants ≥4.5:1, the suite went red on every push.

Root cause is the suite, not the components. Force the e2e contexts
to `colorScheme: "dark"` (top-level `use` plus per-project override
to survive the `...devices[...]` spread) so axe-core scans the
default user-facing layout. Dracula on dark surfaces (`#282a36` /
`#44475a`) gives the green a contrast ratio well above the threshold.

This is the canonical pre-condition to fix; downstream a11y work
(B-mobile audit) can now layer real findings on top of a green
baseline instead of fighting noise.

Pass-rate impact in the v1.4.15 marathon: 0 / 47 e2e runs in the last
30 days had this gate green. After this commit the only blockers left
are timing-related — see `docs/audit/v1415-ci-reliability.md`.

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

129 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
/**
* Phase A5 / B-mobile CRITICAL #2 — `/admin/users` mobile layout.
*
* The audit at 393 CSS px showed the desktop table (6 cols × N rows)
* truncating the role badge ("ADMI", "USE") and pushing the action
* column entirely off the right edge — the destructive force-logout
* action was unreachable on mobile.
*
* Fix: render the desktop table only at `≥ md`, and on `< md` paint
* a card-list (`<ul data-testid="admin-users-mobile-list">`) where
* every action button stays visible and tap-targetable.
*
* This test guards the responsive composition so a future refactor
* can't accidentally drop the mobile path.
*/
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
usePathname: () => "/admin/users",
}));
const sampleUsers = [
{
id: "u1",
username: "marc",
email: "marc@example.com",
role: "ADMIN" as const,
passkeyCount: 2,
createdAt: "2026-04-01T08:00:00Z",
},
{
id: "u2",
username: "alice",
email: null,
role: "USER" as const,
passkeyCount: 1,
createdAt: "2026-04-15T08:00:00Z",
},
];
vi.mock("@tanstack/react-query", () => ({
useQuery: () => ({ data: sampleUsers, isLoading: false }),
useQueryClient: () => ({ invalidateQueries: vi.fn() }),
useMutation: () => ({
mutate: vi.fn(),
mutateAsync: 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 { UserManagementSection } from "../user-management-section";
function render() {
return renderToStaticMarkup(
<I18nProvider initialLocale="en">
<UserManagementSection />
</I18nProvider>,
);
}
describe("UserManagementSection — mobile responsive", () => {
it("hides the desktop table until md:", () => {
const html = render();
// The desktop table wrapper should carry `hidden md:block`.
expect(html).toMatch(/<div[^>]*\bhidden\b[^>]*\bmd:block\b/);
});
it("renders the mobile card-list at < md only", () => {
const html = render();
// The mobile list opts out at md+ via `md:hidden`.
expect(html).toContain('data-testid="admin-users-mobile-list"');
expect(html).toMatch(/<ul[^>]*\bmd:hidden\b/);
});
it("surfaces the role badge inside the mobile card (no truncation)", () => {
const html = render();
// ADMIN badge text appears at least twice — once in desktop table,
// once in the mobile card list. Both occurrences mean neither side
// accidentally lost the column.
const adminMatches = (html.match(/>ADMIN</g) ?? []).length;
expect(adminMatches).toBeGreaterThanOrEqual(2);
});
it("renders all four action buttons inside the mobile card", () => {
const html = render();
// Pull just the mobile-list section by splitting on its testid.
const mobileStart = html.indexOf('data-testid="admin-users-mobile-list"');
expect(mobileStart).toBeGreaterThan(0);
const mobileSlice = html.slice(mobileStart);
// Toggle role + edit + reset password + force-logout — four
// buttons. We probe their ARIA labels since the icons are
// identical-looking; the labels are the contract.
expect(mobileSlice).toContain("Edit user");
expect(mobileSlice).toContain("Reset password");
expect(mobileSlice).toContain("Force logout");
});
it("uses smaller card padding on mobile (p-4 sm:p-6)", () => {
const html = render();
expect(html).toMatch(/class="[^"]*\bp-4\b[^"]*sm:p-6/);
});
it("stacks header rows on mobile so filter pills don't collide with title", () => {
const html = render();
// Header carries `flex-col` at base + `sm:flex-row` at small+.
expect(html).toMatch(/class="[^"]*\bflex-col\b[^"]*sm:flex-row/);
});
});