返回 DeepSeek-Reasonix
context-menu-keyboard.test.tsx
根目录 / desktop / frontend / src / __tests__ / context-menu-keyboard.test.tsx
1 import { strict as assert } from "node:assert";
2 import { JSDOM } from "jsdom";
3
4 const { window } = new JSDOM('<!doctype html><html><body><button id="trigger">Trigger</button><button id="outside">Outside</button><div id="root"></div></body></html>');
5 Object.assign(globalThis, {
6 window, document: window.document, HTMLElement: window.HTMLElement, Node: window.Node,
7 IS_REACT_ACT_ENVIRONMENT: true,
8 });
9 const { createElement, act, StrictMode } = await import("react");
10 const { createRoot } = await import("react-dom/client");
11 const { ContextMenu } = await import("../components/ContextMenu");
12 const root = createRoot(document.getElementById("root")!);
13 const trigger = document.getElementById("trigger")!;
14 const outside = document.getElementById("outside")!;
15 const point = { left: 20, top: 20, keyboardTarget: trigger };
16 let open = true;
17 let selected = "";
18 const close = () => { open = false; render(); };
19 const items = [
20 { key: "disabled", label: "Disabled", disabled: true, onSelect() {} },
21 { key: "first", label: "First", onSelect() { selected = "first"; close(); } },
22 { type: "separator" as const, key: "separator" },
23 { key: "last", label: "Last", onSelect() { selected = "last"; close(); } },
24 ];
25 function render(keyboard = true) {
26 root.render(createElement(StrictMode, null, createElement(ContextMenu, {
27 open, point: keyboard ? point : { left: 20, top: 20 }, items, onClose: close,
28 })));
29 }
30 async function key(key: string) {
31 await act(async () => {
32 document.activeElement!.dispatchEvent(new window.KeyboardEvent("keydown", { key, bubbles: true, cancelable: true }));
33 });
34 }
35 trigger.focus();
36 await act(async () => render());
37 assert.equal(document.activeElement?.textContent, "First", "skip disabled item under StrictMode");
38 for (const [pressed, expected] of [["ArrowDown", "Last"], ["ArrowDown", "First"], ["ArrowUp", "Last"], ["Home", "First"], ["End", "Last"]]) {
39 await key(pressed);
40 assert.equal(document.activeElement?.textContent, expected, pressed);
41 }
42 await act(async () => (document.activeElement as HTMLButtonElement).click());
43 assert.equal(selected, "last");
44 assert.equal(document.querySelector('[role="menu"]'), null);
45 assert.equal(document.activeElement, trigger, "selection restores focus");
46 for (const pressed of ["Escape", "Tab"]) {
47 open = true;
48 await act(async () => render());
49 await key(pressed);
50 assert.equal(document.activeElement, trigger, pressed);
51 assert.equal(document.querySelector('[role="menu"]'), null);
52 }
53 open = true;
54 await act(async () => render());
55 await act(async () => { outside.focus(); close(); });
56 assert.equal(document.activeElement, outside, "close does not steal externally moved focus");
57 trigger.focus();
58 open = true;
59 await act(async () => render(false));
60 assert.equal(document.activeElement, trigger, "pointer menu preserves focus and selection");
61 await act(async () => { open = false; render(false); });
62 open = true;
63 await act(async () => render());
64 await act(async () => { trigger.remove(); root.unmount(); });
65 assert.equal(document.querySelector('[role="menu"]'), null, "unmount tolerates removed trigger");
66 console.log("context-menu keyboard: all assertions passed");
67
67 lines Plain Text