1// @vitest-environment jsdom
2
3import { act, createElement } from "react";
4import { createRoot, type Root } from "react-dom/client";
5import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
6
7import {
8 areShortcutModifierStatesEqual,
9 shortcutModifierStateAfterKeyboardEvent,
10 useShortcutModifierState,
11 type ShortcutModifierState,
12} from "./shortcutModifierState";
13
14const emptyState = (): ShortcutModifierState => ({
15 metaKey: false,
16 ctrlKey: false,
17 altKey: false,
18 shiftKey: false,
19});
20
21describe("useShortcutModifierState", () => {
22 let root: Root;
23 let container: HTMLDivElement;
24
25 beforeEach(() => {
26 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
27 container = document.createElement("div");
28 document.body.append(container);
29 root = createRoot(container);
30 });
31
32 afterEach(async () => {
33 await act(async () => root.unmount());
34 container.remove();
35 vi.unstubAllGlobals();
36 });
37
38 it.each(["keyup", "paste", "blur"] as const)(
39 "does not render for unchanged modifiers after %s resets the state",
40 async (reset) => {
41 const render = vi.fn();
42 function Consumer() {
43 render(useShortcutModifierState());
44 return null;
45 }
46 await act(async () => {
47 root.render(createElement(Consumer));
48 });
49 const press = async (type: "keydown" | "keyup", key: string, shiftKey = false) => {
50 await act(async () => {
51 window.dispatchEvent(new KeyboardEvent(type, { key, shiftKey }));
52 });
53 };
54
55 await press("keydown", "Shift", true);
56 expect(render).toHaveBeenLastCalledWith({ ...emptyState(), shiftKey: true });
57 if (reset === "keyup") {
58 await press("keyup", "Shift");
59 } else {
60 await act(async () => {
61 window.dispatchEvent(new Event(reset));
62 });
63 }
64 expect(render).toHaveBeenLastCalledWith(emptyState());
65 render.mockClear();
66
67 for (const key of "typing") {
68 await press("keydown", key);
69 await press("keyup", key);
70 }
71 await act(async () => {
72 window.dispatchEvent(new Event("paste"));
73 window.dispatchEvent(new Event("blur"));
74 });
75 expect(render).not.toHaveBeenCalled();
76 await press("keydown", "Shift", true);
77 expect(render).toHaveBeenCalledExactlyOnceWith({ ...emptyState(), shiftKey: true });
78 },
79 );
80});
81
82function keyboardEventLike(type: "keydown" | "keyup", init: Partial<KeyboardEvent>): KeyboardEvent {
83 return {
84 type,
85 key: "",
86 metaKey: false,
87 ctrlKey: false,
88 altKey: false,
89 shiftKey: false,
90 ...init,
91 } as KeyboardEvent;
92}
93
94describe("shortcutModifierState", () => {
95 it("compares modifier states by value", () => {
96 expect(
97 areShortcutModifierStatesEqual(
98 { metaKey: false, ctrlKey: true, altKey: false, shiftKey: true },
99 { metaKey: false, ctrlKey: true, altKey: false, shiftKey: true },
100 ),
101 ).toBe(true);
102 expect(
103 areShortcutModifierStatesEqual(
104 { metaKey: false, ctrlKey: true, altKey: false, shiftKey: true },
105 { metaKey: false, ctrlKey: false, altKey: false, shiftKey: true },
106 ),
107 ).toBe(false);
108 });
109
110 it("preserves the current object when modifier values do not change", () => {
111 const initialState = emptyState();
112 const nextState = shortcutModifierStateAfterKeyboardEvent(
113 initialState,
114 keyboardEventLike("keyup", { key: "Shift" }),
115 );
116 expect(nextState).toBe(initialState);
117 });
118
119 it("tracks bare modifier keydown and keyup events explicitly", () => {
120 let state = emptyState();
121 state = shortcutModifierStateAfterKeyboardEvent(
122 state,
123 keyboardEventLike("keydown", {
124 key: "Meta",
125 metaKey: false,
126 }),
127 );
128 expect(state).toEqual({
129 metaKey: true,
130 ctrlKey: false,
131 altKey: false,
132 shiftKey: false,
133 });
134
135 state = shortcutModifierStateAfterKeyboardEvent(
136 state,
137 keyboardEventLike("keydown", {
138 key: "Shift",
139 metaKey: true,
140 shiftKey: false,
141 }),
142 );
143 expect(state).toEqual({
144 metaKey: true,
145 ctrlKey: false,
146 altKey: false,
147 shiftKey: true,
148 });
149
150 state = shortcutModifierStateAfterKeyboardEvent(
151 state,
152 keyboardEventLike("keyup", {
153 key: "Meta",
154 metaKey: true,
155 shiftKey: true,
156 }),
157 );
158 expect(state).toEqual({
159 metaKey: false,
160 ctrlKey: false,
161 altKey: false,
162 shiftKey: true,
163 });
164
165 state = shortcutModifierStateAfterKeyboardEvent(
166 state,
167 keyboardEventLike("keyup", {
168 key: "Shift",
169 shiftKey: true,
170 }),
171 );
172 expect(state).toEqual({
173 metaKey: false,
174 ctrlKey: false,
175 altKey: false,
176 shiftKey: false,
177 });
178 });
179
180 it("ignores poisoned modifier flags on non-modifier keys", () => {
181 // A dictation paste (synthetic ⌘V) can leave the browser reporting
182 // metaKey=true on later real key events. Enter to submit must not
183 // re-mark ⌘ as held.
184 const state = shortcutModifierStateAfterKeyboardEvent(
185 emptyState(),
186 keyboardEventLike("keydown", { key: "Enter", metaKey: true }),
187 );
188 expect(state).toEqual(emptyState());
189 });
190
191 it("clears a held modifier when a non-modifier key reports it released", () => {
192 const heldMeta: ShortcutModifierState = {
193 metaKey: true,
194 ctrlKey: false,
195 altKey: false,
196 shiftKey: false,
197 };
198 const state = shortcutModifierStateAfterKeyboardEvent(
199 heldMeta,
200 keyboardEventLike("keydown", { key: "a", metaKey: false }),
201 );
202 expect(state).toEqual(emptyState());
203 });
204});