122 lines
3.4 KiB
TypeScript
122 lines
3.4 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
CommandDeck,
|
|
commandDeckKeyAction,
|
|
flattenHousekeepingSearchResults,
|
|
isCommandDeckShortcut,
|
|
nextCommandDeckIndex,
|
|
projectHousekeepingSearchHref,
|
|
restoreCommandDeckFocus,
|
|
} from "./command-deck";
|
|
|
|
vi.mock("@/actions/housekeeping-search", () => ({
|
|
executeHousekeepingSearch: vi.fn(),
|
|
}));
|
|
|
|
const labels = {
|
|
trigger: "Search housekeeping",
|
|
placeholder: "Search navigation, commands, and entities",
|
|
navigation: "Navigation",
|
|
commands: "Commands",
|
|
entities: "Entities",
|
|
loading: "Searching…",
|
|
empty: "No results",
|
|
partial: "Some providers are unavailable",
|
|
close: "Close",
|
|
};
|
|
|
|
describe("CommandDeck", () => {
|
|
it("renders an enabled accessible cmdk trigger", () => {
|
|
const html = renderToStaticMarkup(
|
|
<CommandDeck labels={labels} search={vi.fn()} />,
|
|
);
|
|
expect(html).toContain("Search housekeeping");
|
|
expect(html).toContain('aria-haspopup="dialog"');
|
|
expect(html).not.toContain("disabled");
|
|
expect(html).toContain("Ctrl+K");
|
|
});
|
|
|
|
it("models shortcut, arrows, Enter, and Escape deterministically", () => {
|
|
expect(
|
|
isCommandDeckShortcut({ key: "k", ctrlKey: true, metaKey: false }),
|
|
).toBe(true);
|
|
expect(
|
|
isCommandDeckShortcut({ key: "K", ctrlKey: false, metaKey: true }),
|
|
).toBe(true);
|
|
expect(
|
|
isCommandDeckShortcut({
|
|
key: "k",
|
|
ctrlKey: true,
|
|
metaKey: false,
|
|
altKey: true,
|
|
}),
|
|
).toBe(false);
|
|
expect(commandDeckKeyAction("Escape", true)).toBe("close");
|
|
expect(commandDeckKeyAction("ArrowDown", true)).toBe("next");
|
|
expect(commandDeckKeyAction("ArrowUp", true)).toBe("previous");
|
|
expect(commandDeckKeyAction("Enter", true)).toBe("activate");
|
|
expect(commandDeckKeyAction("Enter", false)).toBe(null);
|
|
expect(nextCommandDeckIndex(-1, 3, "next")).toBe(0);
|
|
expect(nextCommandDeckIndex(2, 3, "next")).toBe(0);
|
|
expect(nextCommandDeckIndex(0, 3, "previous")).toBe(2);
|
|
});
|
|
|
|
it("restores trigger focus and keeps canonical result hrefs", () => {
|
|
const focus = vi.fn();
|
|
restoreCommandDeckFocus({ focus });
|
|
expect(focus).toHaveBeenCalledOnce();
|
|
expect(projectHousekeepingSearchHref("/ase/people/users")).toBe(
|
|
"/ase/people/users",
|
|
);
|
|
});
|
|
|
|
it("keeps grouped result order and uses cmdk input/items in the client module", () => {
|
|
const flattened = flattenHousekeepingSearchResults({
|
|
navigation: [
|
|
{
|
|
id: "route:people.users",
|
|
domain: "people",
|
|
type: "navigation",
|
|
title: "Users",
|
|
href: "/ase/people/users",
|
|
capability: { mode: "any", slugs: ["users.view"] },
|
|
},
|
|
],
|
|
commands: [
|
|
{
|
|
id: "command:people.user.refresh",
|
|
commandId: "people.user.refresh",
|
|
domain: "people",
|
|
type: "command",
|
|
title: "User Refresh",
|
|
href: "/ase/people",
|
|
capability: { mode: "any", slugs: ["users.view"] },
|
|
risk: "safe",
|
|
requiresReason: false,
|
|
},
|
|
],
|
|
entities: [],
|
|
errors: [],
|
|
correlationId: "deck",
|
|
});
|
|
expect(flattened.map((entry) => entry.group)).toEqual([
|
|
"navigation",
|
|
"commands",
|
|
]);
|
|
|
|
const source = readFileSync(
|
|
resolve(
|
|
process.cwd(),
|
|
"src/features/housekeeping/foundation/shell/command-deck.tsx",
|
|
),
|
|
"utf8",
|
|
);
|
|
expect(source).toContain("CommandInput");
|
|
expect(source).toContain("CommandItem");
|
|
expect(source).toContain("onSelect");
|
|
});
|
|
});
|