























@@ -1,9 +1,76 @@
1-import { afterEach, describe, expect, it } from "vitest";
1+import { nothing, render } from "lit";
2+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
23import { i18n } from "../../i18n/index.ts";
34import { refreshSlashCommands, resetSlashCommandsForTest } from "../chat/slash-commands.ts";
4-import { getFilteredPaletteItems, getPaletteItems } from "./command-palette.ts";
5+import {
6+getFilteredPaletteItems,
7+getPaletteItems,
8+renderCommandPalette,
9+type CommandPaletteProps,
10+} from "./command-palette.ts";
11+12+let container: HTMLDivElement;
13+14+const showModalDescriptor = Object.getOwnPropertyDescriptor(
15+HTMLDialogElement.prototype,
16+"showModal",
17+);
18+19+function nextFrame() {
20+return new Promise<void>((resolve) => {
21+requestAnimationFrame(() => resolve());
22+});
23+}
24+25+function installDialogPolyfill() {
26+Object.defineProperty(HTMLDialogElement.prototype, "showModal", {
27+configurable: true,
28+value(this: HTMLDialogElement) {
29+this.setAttribute("open", "");
30+},
31+});
32+}
33+34+function restoreShowModalDescriptor() {
35+if (showModalDescriptor) {
36+Object.defineProperty(HTMLDialogElement.prototype, "showModal", showModalDescriptor);
37+return;
38+}
39+delete (HTMLDialogElement.prototype as Partial<HTMLDialogElement>).showModal;
40+}
41+42+function createProps(overrides: Partial<CommandPaletteProps> = {}): CommandPaletteProps {
43+return {
44+open: true,
45+query: "",
46+activeIndex: 0,
47+onToggle: () => undefined,
48+onQueryChange: () => undefined,
49+onActiveIndexChange: () => undefined,
50+onNavigate: () => undefined,
51+onSlashCommand: () => undefined,
52+ ...overrides,
53+};
54+}
55+56+async function renderPalette(overrides: Partial<CommandPaletteProps> = {}) {
57+const props = createProps(overrides);
58+render(renderCommandPalette(props), container);
59+await nextFrame();
60+return props;
61+}
62+63+beforeEach(() => {
64+installDialogPolyfill();
65+container = document.createElement("div");
66+document.body.append(container);
67+});
568669afterEach(async () => {
70+render(nothing, container);
71+container.remove();
72+restoreShowModalDescriptor();
73+vi.restoreAllMocks();
774resetSlashCommandsForTest();
875await i18n.setLocale("en");
976});
@@ -69,4 +136,90 @@ describe("command palette", () => {
69136}),
70137);
71138});
139+140+it("renders a labelled modal combobox with listbox options", async () => {
141+await renderPalette({ query: "overview", activeIndex: 0 });
142+143+const dialog = container.querySelector<HTMLDialogElement>("dialog.cmd-palette-overlay");
144+expect(dialog).not.toBeNull();
145+expect(dialog!.open).toBe(true);
146+expect(dialog!.hasAttribute("role")).toBe(false);
147+expect(dialog!.hasAttribute("aria-modal")).toBe(false);
148+expect(dialog!.getAttribute("aria-labelledby")).toBe("cmd-palette-label");
149+150+const label = container.querySelector<HTMLLabelElement>("#cmd-palette-label");
151+const input = container.querySelector<HTMLInputElement>("#cmd-palette-input");
152+const listbox = container.querySelector<HTMLElement>("#cmd-palette-listbox");
153+expect(label?.textContent).toBe("Type a command…");
154+expect(label?.getAttribute("for")).toBe("cmd-palette-input");
155+expect(input).not.toBeNull();
156+expect(input!.getAttribute("role")).toBe("combobox");
157+expect(input!.getAttribute("aria-autocomplete")).toBe("list");
158+expect(input!.getAttribute("aria-expanded")).toBe("true");
159+expect(input!.getAttribute("aria-controls")).toBe("cmd-palette-listbox");
160+expect(input!.getAttribute("aria-activedescendant")).toBe("cmd-palette-option-nav-overview");
161+expect(document.activeElement).toBe(input);
162+163+expect(listbox).not.toBeNull();
164+expect(listbox!.getAttribute("role")).toBe("listbox");
165+const option = listbox!.querySelector<HTMLElement>("#cmd-palette-option-nav-overview");
166+expect(option).not.toBeNull();
167+expect(option!.getAttribute("role")).toBe("option");
168+expect(option!.getAttribute("aria-selected")).toBe("true");
169+});
170+171+it("traps Tab on the combobox and restores focus on Escape", async () => {
172+const returnTarget = document.createElement("button");
173+returnTarget.textContent = "Open palette";
174+document.body.append(returnTarget);
175+returnTarget.focus();
176+const onToggle = vi.fn();
177+178+await renderPalette({ onToggle });
179+const input = container.querySelector<HTMLInputElement>("#cmd-palette-input");
180+expect(input).not.toBeNull();
181+expect(document.activeElement).toBe(input);
182+183+const tab = new KeyboardEvent("keydown", {
184+key: "Tab",
185+bubbles: true,
186+cancelable: true,
187+});
188+input!.dispatchEvent(tab);
189+expect(tab.defaultPrevented).toBe(true);
190+expect(document.activeElement).toBe(input);
191+192+const escape = new KeyboardEvent("keydown", {
193+key: "Escape",
194+bubbles: true,
195+cancelable: true,
196+});
197+input!.dispatchEvent(escape);
198+expect(escape.defaultPrevented).toBe(true);
199+expect(onToggle).toHaveBeenCalledTimes(1);
200+201+await nextFrame();
202+expect(document.activeElement).toBe(returnTarget);
203+returnTarget.remove();
204+});
205+206+it("does not toggle twice when Escape is followed by dialog cancel", async () => {
207+const onToggle = vi.fn();
208+await renderPalette({ onToggle });
209+const dialog = container.querySelector<HTMLDialogElement>("dialog.cmd-palette-overlay");
210+const input = container.querySelector<HTMLInputElement>("#cmd-palette-input");
211+expect(dialog).not.toBeNull();
212+expect(input).not.toBeNull();
213+214+input!.dispatchEvent(
215+new KeyboardEvent("keydown", {
216+key: "Escape",
217+bubbles: true,
218+cancelable: true,
219+}),
220+);
221+dialog!.dispatchEvent(new Event("cancel", { cancelable: true }));
222+223+expect(onToggle).toHaveBeenCalledTimes(1);
224+});
72225});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。