feat(housekeeping): finish accessible command deck
This commit is contained in:
1 parent
85ad0452d3
commit
a113e48880
55 files changed
+17064
-509
No files matched your search
@@ -0,0 +1,210 @@
|
||||
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}
|
||||
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");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user