Skip to content
Source
Sign inSign up

contextMenuFallback.keyboard.browser.test.ts

118 lines3.7 KB

1import { expect, it } from "vite-plus/test";
2import {
3 dismissContextMenu,
4 isContextMenuOpen,
5 showContextMenuFallback,
6} from "./contextMenuFallback";
7​
8const items = [
9 { id: "heading", label: "Thread actions", header: true },
10 { id: "disabled", label: "Unavailable", disabled: true },
11 { id: "rename", label: "Rename" },
12 { id: "pin", label: "Pinned", checked: true, separatorBefore: true },
13 {
14 id: "copy",
15 label: "Copy",
16 children: [
17 { id: "copy-unavailable", label: "Unavailable copy", disabled: true },
18 { id: "path", label: "Path" },
19 { id: "branch", label: "Branch" },
20 ],
21 },
22];
23​
24function setup() {
25 const origin = document.createElement("button");
26 origin.textContent = "Selected thread";
27 document.body.append(origin);
28 origin.focus();
29 return {
30 origin,
31 [Symbol.dispose]() {
32 dismissContextMenu();
33 origin.remove();
34 },
35 };
37​
38function press(key: string, shiftKey = false) {
39 const event = new KeyboardEvent("keydown", { key, shiftKey, bubbles: true, cancelable: true });
40 document.activeElement!.dispatchEvent(event);
41 return event;
43​
44const label = () => document.activeElement?.textContent;
45​
46it("starts a keyboard-opened menu on its first action and wraps through enabled actions", () => {
47 using _fixture = setup();
48 void showContextMenuFallback(items);
49 expect(label()).toBe("Rename");
50 press("ArrowUp");
51 expect(label()).toBe("Copy");
52 press("ArrowDown");
53 expect(label()).toBe("Rename");
54 press("End");
55 expect(label()).toBe("Copy");
56 press("Home");
57 expect(label()).toBe("Rename");
58});
59​
60it("opens and cancels one submenu level at a time, then returns to the thread", async () => {
61 using fixture = setup();
62 const result = showContextMenuFallback(items);
63 press("End");
64 const parent = document.activeElement;
65 press("ArrowRight");
66 expect(label()).toBe("Path");
67 press("ArrowLeft");
68 expect(document.activeElement).toBe(parent);
69 press("Enter");
70 expect(label()).toBe("Path");
71 press("Escape");
72 expect(document.activeElement).toBe(parent);
73 expect(isContextMenuOpen()).toBe(true);
74 press("Escape");
75 expect(document.activeElement).toBe(fixture.origin);
76 await expect(result).resolves.toBeNull();
77});
78​
79it("activates the focused action with Space or Enter", async () => {
80 using _fixture = setup();
81 const first = showContextMenuFallback(items);
82 press(" ");
83 await expect(first).resolves.toBe("rename");
84 const second = showContextMenuFallback(items);
85 press("Enter");
86 await expect(second).resolves.toBe("rename");
87});
88​
89it("closes on Tab and returns focus to the opener so Tab continues from there", async () => {
90 using fixture = setup();
91 const result = showContextMenuFallback(items);
92 expect(press("Tab").defaultPrevented).toBe(false);
93 await expect(result).resolves.toBeNull();
94 expect(document.activeElement).toBe(fixture.origin);
95});
96​
97it("closes without taking focus back when focus moves elsewhere", async () => {
98 using _fixture = setup();
99 const result = showContextMenuFallback(items);
100 const destination = document.createElement("button");
101 document.body.append(destination);
102 using _destination = { [Symbol.dispose]: () => destination.remove() };
103 destination.focus();
104 await expect(result).resolves.toBeNull();
105 expect(document.activeElement).toBe(destination);
106});
107​
108it("takes thumb Back and Forward presses so the app router stays put", () => {
109 using _fixture = setup();
110 void showContextMenuFallback(items);
111 for (const button of [3, 4]) {
112 const event = new MouseEvent("mouseup", { button, bubbles: true, cancelable: true });
113 document.activeElement!.dispatchEvent(event);
114 // Chromium runs history back/forward on a thumb-button mouseup nobody cancelled.
115 expect(event.defaultPrevented).toBe(true);
116 }
117 expect(isContextMenuOpen()).toBe(true);
118});