Files
Epicnabbo-Catalogus-Updated…/src/features/housekeeping/foundation/accessibility.test.tsx
T

212 lines
6.1 KiB
TypeScript

import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it, vi } from "vitest";
import { anyCapability, ok } from "./contracts";
import { HousekeepingPageShell } from "./page/housekeeping-page-shell";
import { HousekeepingPageState } from "./page/housekeeping-page-state";
import type { HousekeepingPreferences } from "./preferences/schema";
import { Favorites } from "./shell/favorites";
import { HousekeepingShell } from "./shell/housekeeping-shell";
import { OperationalInbox } from "./shell/operational-inbox";
import { WidgetGrid } from "./shell/widget-grid";
vi.mock("@/actions/housekeeping-search", () => ({
executeHousekeepingSearch: vi.fn(),
}));
const capability = anyCapability("admin.users.view");
const preferences: HousekeepingPreferences = {
schemaVersion: 1,
pinnedRouteIds: ["people.users.list"],
pinnedCommandIds: [],
shortcutOrder: ["people.users.list"],
enabledOptionalWidgetIds: [],
widgetOrder: ["people.queue"],
};
const savePreferences = vi.fn(async (value: HousekeepingPreferences) =>
ok(value, "saved"),
);
const shellLabels = {
skipToContent: "Skip to content",
primaryNavigation: "Primary navigation",
contextualNavigation: "Context navigation",
command: "Search",
preview: "Preview",
backToSite: "Back to site",
operatorRank: "Rank 6",
commandDeck: {
placeholder: "Search",
navigation: "Navigation",
commands: "Commands",
entities: "Entities",
loading: "Loading",
empty: "Empty",
partial: "Partial",
close: "Close",
},
};
describe("housekeeping accessibility contract", () => {
it("renders the shell and page with labelled landmarks and a coherent heading hierarchy", () => {
const html = renderToStaticMarkup(
<HousekeepingShell
actor={{ id: 7, username: "Nora", rank: 6 }}
activeDomainId="operations"
domains={[
{
id: "operations",
href: "/ase/operations",
iconId: "inbox",
label: "Operations",
description: "Daily operations",
items: [],
},
]}
labels={shellLabels}
search={vi.fn()}
surface="canonical"
>
<HousekeepingPageShell title="Operations" description="Daily work">
<h2>Inbox</h2>
</HousekeepingPageShell>
</HousekeepingShell>,
);
expect(html).toContain('data-housekeeping-root="true"');
expect(html).toContain("<header");
expect(html).toContain('<nav aria-label="Primary navigation"');
expect(html).toContain('<nav aria-label="Context navigation"');
expect(html).toContain('<main id="housekeeping-content"');
expect(html).toContain('aria-labelledby="housekeeping-page-title"');
expect(html).toContain('<h1 id="housekeeping-page-title"');
expect(html.match(/aria-current="page"/g)).toHaveLength(1);
expect(html).toContain(">Rank 6</span>");
expect(html).toContain("focus-visible:not-sr-only");
});
it.each([
["loading", "status", "polite"],
["empty", "status", "polite"],
["partial", "status", "polite"],
["error", "alert", "assertive"],
["forbidden", "alert", "assertive"],
["ready", "status", "polite"],
] as const)(
"gives the %s page state explicit semantic announcements",
(state, role, live) => {
const pageState =
state === "partial" ? (
<HousekeepingPageState
state="partial"
partialLabel="Partial"
title={`${state} title`}
description={`${state} description`}
/>
) : (
<HousekeepingPageState
state={state}
title={`${state} title`}
description={`${state} description`}
/>
);
const html = renderToStaticMarkup(pageState);
expect(html).toContain(`data-page-state="${state}"`);
expect(html).toContain(`role="${role}"`);
expect(html).toContain(`aria-live="${live}"`);
},
);
it("provides keyboard reorder instructions for favorites and widgets", () => {
const favorites = renderToStaticMarkup(
<Favorites
preferences={preferences}
routes={[
{
id: "people.users.list",
labelKey: "pages.housekeeping.routes.people.users.list",
href: "/ase/people/users",
capability,
},
]}
surface="canonical"
savePreferences={savePreferences}
titles={{
"pages.housekeeping.routes.people.users.list": "Users",
}}
labels={{
title: "Favorites",
pin: "Pin",
unpin: "Unpin",
error: "Could not save",
reorderInstructions:
"Press Space, then use the arrow keys to reorder.",
}}
/>,
);
const widgets = renderToStaticMarkup(
<WidgetGrid
preferences={preferences}
result={{
widgets: [{ id: "people.queue", data: { count: 1 } }],
errors: [],
}}
savePreferences={savePreferences}
titles={{ "people.queue": "People queue" }}
labels={{
title: "Widgets",
partial: "Partial widgets",
error: "Could not save",
reorderInstructions:
"Press Space, then use the arrow keys to reorder.",
}}
/>,
);
expect(favorites).toContain(
'id="housekeeping-favorites-reorder-instructions"',
);
expect(favorites).toContain(
'aria-describedby="housekeeping-favorites-reorder-instructions"',
);
expect(favorites).toContain(">Users<");
expect(favorites).not.toContain(
">pages.housekeeping.routes.people.users.list<",
);
expect(widgets).toContain('id="housekeeping-widgets-reorder-instructions"');
expect(widgets).toContain(
'aria-describedby="housekeeping-widgets-reorder-instructions"',
);
expect(widgets).toContain(">People queue<");
});
it("announces partial provider failures without exposing internal identifiers", () => {
const html = renderToStaticMarkup(
<OperationalInbox
response={{
items: [],
errors: [{ sourceId: "internal.provider", code: "TIMEOUT" }],
correlationId: "private-correlation",
}}
labels={{
title: "Inbox",
domainFilter: "Domain",
stateFilter: "State",
priorityFilter: "Priority",
all: "All",
loading: "Loading",
empty: "Empty",
partial: "Some data is unavailable",
open: "Open",
}}
surface="canonical"
/>,
);
expect(html).toContain('role="alert"');
expect(html).toContain('aria-live="assertive"');
expect(html).not.toContain("internal.provider");
expect(html).not.toContain("TIMEOUT");
expect(html).not.toContain("private-correlation");
});
});