212 lines
6.1 KiB
TypeScript
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");
|
|
});
|
|
});
|