react-x11 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +330 -81
  2. package/package.json +132 -14
  3. package/src/ClickToComponent.js +9 -2
  4. package/src/DevToolsIntegration.js +440 -32
  5. package/src/Reconciler.js +463 -165
  6. package/src/a11y.js +1299 -0
  7. package/src/acceleratorhooks.js +86 -0
  8. package/src/accelerators.js +128 -0
  9. package/src/activate.js +168 -0
  10. package/src/anchor.js +360 -0
  11. package/src/appcontext.js +161 -0
  12. package/src/appearance.js +748 -0
  13. package/src/appearancehooks.js +96 -0
  14. package/src/apphooks.js +73 -0
  15. package/src/application.js +748 -0
  16. package/src/atspi.js +1953 -0
  17. package/src/bus.js +545 -0
  18. package/src/bushooks.js +136 -0
  19. package/src/clientmessage.js +140 -0
  20. package/src/clipboard.js +121 -0
  21. package/src/components/Button.js +148 -33
  22. package/src/components/Checkbox.js +66 -32
  23. package/src/components/Dialog.js +96 -29
  24. package/src/components/FileDialog.js +491 -0
  25. package/src/components/Icon.js +329 -0
  26. package/src/components/Menu.js +797 -133
  27. package/src/components/PasswordInput.js +434 -0
  28. package/src/components/ProgressBar.js +91 -12
  29. package/src/components/Radio.js +78 -35
  30. package/src/components/Select.js +255 -65
  31. package/src/components/Slider.js +102 -41
  32. package/src/components/SplitPane.js +206 -0
  33. package/src/components/Switch.js +100 -22
  34. package/src/components/Table.js +529 -0
  35. package/src/components/Tabs.js +211 -0
  36. package/src/components/Tooltip.js +528 -54
  37. package/src/components/anchor.js +154 -107
  38. package/src/components/change.js +34 -0
  39. package/src/components/dnd.js +103 -0
  40. package/src/components/index.js +16 -3
  41. package/src/components/keys.js +17 -20
  42. package/src/components/scribble.js +247 -0
  43. package/src/components/theme.js +350 -42
  44. package/src/components/typeahead.js +12 -2
  45. package/src/compose.js +868 -0
  46. package/src/compositing.js +224 -0
  47. package/src/dbusmenu.js +384 -0
  48. package/src/debug.d.ts +63 -0
  49. package/src/debug.js +642 -0
  50. package/src/decorations.js +486 -0
  51. package/src/desktopsettings.js +209 -0
  52. package/src/desktopsettingshooks.js +62 -0
  53. package/src/dnd.js +1725 -0
  54. package/src/editmenu.js +272 -0
  55. package/src/errors.js +98 -0
  56. package/src/events.js +1406 -162
  57. package/src/extensions.js +45 -0
  58. package/src/filedialog.js +375 -0
  59. package/src/filedialoghooks.js +132 -0
  60. package/src/fonthooks.js +64 -0
  61. package/src/fonts.js +301 -0
  62. package/src/foreignnodes.js +519 -0
  63. package/src/frame/child.js +33 -0
  64. package/src/frame/childmain.js +274 -0
  65. package/src/frame/env.js +140 -0
  66. package/src/frame/index.js +444 -0
  67. package/src/frame/lifecycle.js +67 -0
  68. package/src/frame/protocol.js +179 -0
  69. package/src/frames.js +73 -0
  70. package/src/glbackend.js +114 -0
  71. package/src/glnodes.js +125 -67
  72. package/src/globalmenu.js +683 -0
  73. package/src/host.d.ts +88 -0
  74. package/src/host.js +42 -0
  75. package/src/idle.js +490 -0
  76. package/src/idlehooks.js +100 -0
  77. package/src/imagesource.js +349 -0
  78. package/src/index.d.ts +475 -0
  79. package/src/index.js +83 -9
  80. package/src/inputtime.js +206 -0
  81. package/src/jsx-dev-runtime.d.ts +21 -0
  82. package/src/jsx-dev-runtime.js +2 -0
  83. package/src/jsx-runtime.d.ts +39 -0
  84. package/src/jsx-runtime.js +6 -0
  85. package/src/keyboard.js +256 -0
  86. package/src/keyboardstate.js +278 -0
  87. package/src/keyboardstatehooks.js +58 -0
  88. package/src/keysyms.d.ts +149 -0
  89. package/src/keysyms.js +270 -0
  90. package/src/locale.js +170 -0
  91. package/src/localehooks.js +47 -0
  92. package/src/menuitem.js +223 -0
  93. package/src/node.d.ts +597 -0
  94. package/src/node.js +44 -0
  95. package/src/nodes.js +9546 -690
  96. package/src/ntk.d.ts +44 -0
  97. package/src/ntk.js +25 -0
  98. package/src/paintcache.js +366 -0
  99. package/src/palette.js +380 -0
  100. package/src/pastestate.js +66 -0
  101. package/src/portal.js +461 -0
  102. package/src/priority.js +26 -0
  103. package/src/refresh/index.d.ts +40 -0
  104. package/src/refresh/index.js +122 -0
  105. package/src/refresh/loader.d.ts +37 -0
  106. package/src/refresh/loader.js +401 -0
  107. package/src/refresh/register.d.ts +5 -0
  108. package/src/refresh/register.js +13 -0
  109. package/src/registry.js +232 -0
  110. package/src/scale.js +626 -0
  111. package/src/scalehooks.js +27 -0
  112. package/src/screencolor.js +640 -0
  113. package/src/screencolorhooks.js +101 -0
  114. package/src/screens.js +754 -0
  115. package/src/screenshooks.js +137 -0
  116. package/src/startup.js +302 -0
  117. package/src/style.d.ts +126 -0
  118. package/src/style.js +33 -0
  119. package/src/styles.js +1482 -6
  120. package/src/svgnodes.js +306 -0
  121. package/src/testing/a11y.js +484 -0
  122. package/src/testing/components.js +414 -0
  123. package/src/testing/events.js +407 -0
  124. package/src/testing/harness.js +455 -0
  125. package/src/testing/index.d.ts +558 -0
  126. package/src/testing/index.js +85 -0
  127. package/src/testing/mock-app.js +463 -0
  128. package/src/testing/pixels.js +152 -0
  129. package/src/testing/queries.js +224 -0
  130. package/src/textrange.js +83 -0
  131. package/src/textselection.js +439 -0
  132. package/src/trace-registry.js +63 -0
  133. package/src/transfer.js +93 -0
  134. package/src/types/appearance.d.ts +84 -0
  135. package/src/types/application.d.ts +173 -0
  136. package/src/types/components.d.ts +820 -0
  137. package/src/types/dbus.d.ts +177 -0
  138. package/src/types/elements.d.ts +897 -0
  139. package/src/types/events.d.ts +549 -0
  140. package/src/types/filedialog.d.ts +260 -0
  141. package/src/types/fonts.d.ts +124 -0
  142. package/src/types/frame.d.ts +146 -0
  143. package/src/types/globalmenu.d.ts +42 -0
  144. package/src/types/nodes.d.ts +199 -0
  145. package/src/types/screencolor.d.ts +84 -0
  146. package/src/types/style.d.ts +434 -0
  147. package/src/types/system.d.ts +287 -0
  148. package/src/windowid.js +151 -0
  149. package/src/windowstate.js +393 -0
  150. package/src/xsettings.js +336 -0
  151. package/src/yoga.d.ts +55 -0
  152. package/src/yoga.js +135 -0
  153. package/src/components/Canvas3D.js +0 -28
  154. package/src/geometry3d.js +0 -223
  155. package/src/pointer3d.js +0 -158
  156. package/src/raycast3d.js +0 -146
  157. package/src/richnodes.js +0 -436
  158. package/src/scene3d.js +0 -683
@@ -0,0 +1,407 @@
1
+ // fireEvent / userEvent — input injected through the **X server**, not
2
+ // emitted on the ntk window.
3
+ //
4
+ // This is the part the repo's own tests never had. Driving `wnd.emit(...)`
5
+ // skips `src/events.js` entirely: keycode→keysym resolution, modifier masks,
6
+ // wheel-from-buttons, click detail, pointer capture — and, on the server
7
+ // side, the grab and focus machinery that decides which window an event even
8
+ // reaches. Going through `server.injectButton` means a `<popup grab>` is
9
+ // dismissed by a real press outside it, `trapFocus` is exercised by real
10
+ // focus changes, and a test that passes has actually proved the path a user
11
+ // takes.
12
+ //
13
+ // Everything here is synchronous injection; the caller wraps it in `act()`
14
+ // to flush what it caused. `userEvent` does that itself, which is why it is
15
+ // async and `fireEvent` is not.
16
+
17
+ import { act } from './harness.js';
18
+ import { keysymOf, MOD, XK_RETURN } from '../keysyms.js';
19
+
20
+ /** The window node that owns a drawn node (a node, or a window itself). */
21
+ function rootWindow(node) {
22
+ return node?.isWindow ? node : (node?.root ?? null);
23
+ }
24
+
25
+ /** A node's centre in **screen** coordinates, which is what injection takes. */
26
+ export function screenPointOf(node, { dx = 0, dy = 0 } = {}) {
27
+ const win = rootWindow(node);
28
+ if (!win?.window) {
29
+ throw new Error(
30
+ 'react-x11/test: the target is not inside a realized window — did you ' +
31
+ 'await renderX11(), or has it unmounted?',
32
+ );
33
+ }
34
+ const origin = win.window._screenOrigin ?? {
35
+ x: win.window.x ?? 0,
36
+ y: win.window.y ?? 0,
37
+ };
38
+ if (node.isWindow) {
39
+ return {
40
+ x: origin.x + (node.window.width ?? 0) / 2 + dx,
41
+ y: origin.y + (node.window.height ?? 0) / 2 + dy,
42
+ server: serverOf(node),
43
+ };
44
+ }
45
+ const abs = node.abs;
46
+ if (!abs || (abs.width === 0 && abs.height === 0)) {
47
+ throw new Error(
48
+ `react-x11/test: <${node.kind}> has no laid-out rect yet. Layout runs ` +
49
+ 'on the frame clock, so `await act()` before pointing at it.',
50
+ );
51
+ }
52
+ return {
53
+ x: Math.round(origin.x + abs.x + abs.width / 2 + dx),
54
+ y: Math.round(origin.y + abs.y + abs.height / 2 + dy),
55
+ server: serverOf(node),
56
+ };
57
+ }
58
+
59
+ function serverOf(node) {
60
+ const server = rootWindow(node)?.app?._reactX11TestServer;
61
+ if (!server) {
62
+ throw new Error(
63
+ 'react-x11/test: fireEvent needs the in-process X server, so it needs a ' +
64
+ "root created by renderX11() with the default backend ('xserver'). " +
65
+ "The 'mock' backend has no server to inject into.",
66
+ );
67
+ }
68
+ return server;
69
+ }
70
+
71
+ /** Press the keycodes for a modifier mask, innermost last. */
72
+ function modifierKeycodes(server, mask) {
73
+ const codes = [];
74
+ for (let row = 0; row < 8; row++) {
75
+ if (!(mask & (1 << row))) continue;
76
+ const keycode = server.keymap.modifiers[row]?.find(Boolean);
77
+ if (keycode) codes.push(keycode);
78
+ }
79
+ return codes;
80
+ }
81
+
82
+ function maskOf(modifiers) {
83
+ if (typeof modifiers === 'number') return modifiers;
84
+ let mask = 0;
85
+ for (const name of modifiers ?? []) {
86
+ const bit = MOD[name];
87
+ if (bit === undefined) {
88
+ throw new Error(
89
+ `react-x11/test: unknown modifier "${name}". Known: ${Object.keys(MOD).join(', ')}.`,
90
+ );
91
+ }
92
+ mask |= bit;
93
+ }
94
+ return mask;
95
+ }
96
+
97
+ /**
98
+ * The keycode that produces a keysym, and whether Shift is needed to reach
99
+ * it. Anything the server's default US layout does not carry is bound to a
100
+ * spare keycode on the spot — that is how a test types `é` without the test
101
+ * author having to own a keymap. Both sides are updated together: the
102
+ * server's `keymap.syms` and the client's `keycode2keysyms` cache, which is
103
+ * what ntk decodes the event against.
104
+ */
105
+ function resolveKeysym(server, app, keysym) {
106
+ const km = server.keymap;
107
+ for (let kc = km.minKeycode; kc <= km.maxKeycode; kc++) {
108
+ const syms = km.syms[kc];
109
+ if (syms[0] === keysym) return { keycode: kc, shift: false };
110
+ if (syms[1] === keysym) return { keycode: kc, shift: true };
111
+ }
112
+ // a spare keycode, counting down from the top so the standard layout and
113
+ // anything bound earlier are left alone
114
+ for (let kc = km.maxKeycode; kc >= km.minKeycode; kc--) {
115
+ if (km.syms[kc][0] || km.syms[kc][1]) continue;
116
+ km.syms[kc] = [keysym, keysym];
117
+ if (app?.X?.keycode2keysyms) app.X.keycode2keysyms[kc] = [keysym, keysym];
118
+ return { keycode: kc, shift: false };
119
+ }
120
+ throw new Error(
121
+ `react-x11/test: no keycode left to bind keysym 0x${keysym.toString(16)}.`,
122
+ );
123
+ }
124
+
125
+ /**
126
+ * Synchronous input injection. Each call is one user action; wrap it in
127
+ * `await act()` to see what it caused.
128
+ *
129
+ * ```js
130
+ * fireEvent.click(getByRole('button'));
131
+ * await act();
132
+ * ```
133
+ */
134
+ export const fireEvent = {
135
+ /** Move the pointer over a node — real EnterNotify/LeaveNotify follow. */
136
+ mouseMove(node, options = {}) {
137
+ const { x, y, server } = screenPointOf(node, options);
138
+ server.injectPointerMove(x, y);
139
+ },
140
+
141
+ mouseEnter(node, options = {}) {
142
+ fireEvent.mouseMove(node, options);
143
+ },
144
+
145
+ /** Move the pointer off the node, which is what produces a leave. */
146
+ mouseLeave(node, options = {}) {
147
+ const { server } = screenPointOf(node, options);
148
+ server.injectPointerMove(0, 0);
149
+ },
150
+
151
+ mouseDown(node, options = {}) {
152
+ const { x, y, server } = screenPointOf(node, options);
153
+ const modifiers = modifierKeycodes(server, maskOf(options.modifiers));
154
+ for (const kc of modifiers) server.injectKey(kc, true);
155
+ server.injectPointerMove(x, y);
156
+ server.injectButton(options.button ?? 1, true);
157
+ for (const kc of modifiers.reverse()) server.injectKey(kc, false);
158
+ },
159
+
160
+ mouseUp(node, options = {}) {
161
+ const { server } = screenPointOf(node, options);
162
+ server.injectButton(options.button ?? 1, false);
163
+ },
164
+
165
+ /** Press and release — the renderer synthesises the click from the pair. */
166
+ click(node, options = {}) {
167
+ const { x, y, server } = screenPointOf(node, options);
168
+ const modifiers = modifierKeycodes(server, maskOf(options.modifiers));
169
+ for (const kc of modifiers) server.injectKey(kc, true);
170
+ server.injectPointerMove(x, y);
171
+ const button = options.button ?? 1;
172
+ server.injectButton(button, true);
173
+ server.injectButton(button, false);
174
+ for (const kc of modifiers.reverse()) server.injectKey(kc, false);
175
+ },
176
+
177
+ /** Two clicks close enough in time and space to count as a double. */
178
+ doubleClick(node, options = {}) {
179
+ fireEvent.click(node, options);
180
+ fireEvent.click(node, options);
181
+ },
182
+
183
+ contextMenu(node, options = {}) {
184
+ fireEvent.click(node, { ...options, button: 3 });
185
+ },
186
+
187
+ /**
188
+ * A scroll, in **notches** — one click of a wheel, which the renderer
189
+ * turns into the 48 pixels a notch is worth. The core protocol has no
190
+ * wheel, so a whole notch is injected as the press of button 4/5
191
+ * (vertical) or 6/7 (horizontal) it really is.
192
+ *
193
+ * `{ smooth: true }` is a touchpad instead, and takes fractions:
194
+ * `fireEvent.wheel(list, { deltaY: 0.25, smooth: true })` is a quarter of
195
+ * a notch. That one does **not** go through the server — the in-process X
196
+ * server has no XInput extension, so there are no valuators to inject —
197
+ * and is delivered as the `wheel` event ntk would have derived from them.
198
+ * The only thing in this module that skips the server, and it skips it
199
+ * because nothing below the toolkit can express the gesture.
200
+ */
201
+ wheel(node, { deltaY = 1, deltaX = 0, smooth = false, ...options } = {}) {
202
+ const { x, y, server } = screenPointOf(node, options);
203
+ if (smooth) {
204
+ const wnd = rootWindow(node).window;
205
+ const origin = wnd._screenOrigin ?? { x: wnd.x ?? 0, y: wnd.y ?? 0 };
206
+ wnd.emit('wheel', {
207
+ name: 'wheel',
208
+ x: x - origin.x,
209
+ y: y - origin.y,
210
+ rootx: x,
211
+ rooty: y,
212
+ buttons: maskOf(options.modifiers),
213
+ deltaX,
214
+ deltaY,
215
+ deltaMode: 'line',
216
+ smooth: true,
217
+ source: 'valuator',
218
+ });
219
+ return;
220
+ }
221
+ server.injectPointerMove(x, y);
222
+ const button = deltaX ? (deltaX > 0 ? 7 : 6) : deltaY > 0 ? 5 : 4;
223
+ const notches = Math.max(1, Math.abs(deltaX || deltaY));
224
+ for (let i = 0; i < notches; i++) {
225
+ server.injectButton(button, true);
226
+ server.injectButton(button, false);
227
+ }
228
+ },
229
+
230
+ /**
231
+ * A key, by keysym — `fireEvent.key(XK_RETURN)` or
232
+ * `fireEvent.key(XK_TAB, { target, modifiers: ['Shift'] })`. Keys go to
233
+ * whatever holds the X input focus, so `target` is only needed to say
234
+ * *which* connection when a test holds more than one root.
235
+ */
236
+ key(
237
+ keysym,
238
+ { target = null, modifiers = [], press = true, release = true } = {},
239
+ ) {
240
+ const node = target ?? currentTarget();
241
+ const server = serverOf(node);
242
+ const app = rootWindow(node)?.app;
243
+ const { keycode, shift } = resolveKeysym(server, app, keysym);
244
+ const mask = maskOf(modifiers) | (shift ? MOD.Shift : 0);
245
+ const mods = modifierKeycodes(server, mask);
246
+ if (press) {
247
+ for (const kc of mods) server.injectKey(kc, true);
248
+ server.injectKey(keycode, true);
249
+ }
250
+ if (release) {
251
+ server.injectKey(keycode, false);
252
+ for (const kc of [...mods].reverse()) server.injectKey(kc, false);
253
+ }
254
+ },
255
+
256
+ /** A single character, resolved to a keysym by the Latin-1/Unicode rule. */
257
+ char(char, options = {}) {
258
+ fireEvent.key(keysymOf(char), options);
259
+ },
260
+
261
+ /** A press at a raw **screen** coordinate — see `userEvent.clickOutside`. */
262
+ screenClick(x, y, { target = null, button = 1 } = {}) {
263
+ const server = serverOf(target ?? currentTarget());
264
+ server.injectPointerMove(x, y);
265
+ server.injectButton(button, true);
266
+ server.injectButton(button, false);
267
+ },
268
+ };
269
+
270
+ /**
271
+ * A screen point that is inside none of this root's windows — the root
272
+ * window, in other words, which is where a press has to land for a
273
+ * `<popup grab>` to hear about it.
274
+ *
275
+ * The reason is X's, not ours: react-x11's popups grab with **owner
276
+ * events**, so a press on one of the client's *own* windows is delivered
277
+ * normally (that is what keeps submenus and the owner window working). Only
278
+ * a press somewhere the client does not own is redirected to the grab
279
+ * holder, and that is what `onDismiss` reports. A test that clicks another
280
+ * widget expecting the menu to close is testing the wrong thing.
281
+ */
282
+ export function pointOutsideWindows(node) {
283
+ const win = rootWindow(node);
284
+ const server = serverOf(node);
285
+ const boxes = [];
286
+ for (let n = win; n; n = null) {
287
+ collectWindowBoxes(n, boxes);
288
+ }
289
+ const clear = (x, y) =>
290
+ !boxes.some(
291
+ (b) => x >= b.x && x < b.x + b.width && y >= b.y && y < b.y + b.height,
292
+ );
293
+ // walk in from the bottom-right corner, which is where a window is least
294
+ // likely to be
295
+ for (let i = 0; i < 64; i++) {
296
+ const x = server.width - 2 - i * 8;
297
+ const y = server.height - 2 - i * 8;
298
+ if (x < 0 || y < 0) break;
299
+ if (clear(x, y)) return { x, y };
300
+ }
301
+ throw new Error(
302
+ 'react-x11/test: every screen point is covered by a window of this root, ' +
303
+ 'so there is nowhere "outside" to press. Give renderX11 a bigger ' +
304
+ '`screen`, or a smaller window.',
305
+ );
306
+ }
307
+
308
+ function collectWindowBoxes(node, out) {
309
+ if (!node || node.destroyed) return;
310
+ if (node.isWindow && node.window) {
311
+ const origin = node.window._screenOrigin ?? {
312
+ x: node.window.x ?? 0,
313
+ y: node.window.y ?? 0,
314
+ };
315
+ out.push({
316
+ x: origin.x,
317
+ y: origin.y,
318
+ width: node.window.width ?? 0,
319
+ height: node.window.height ?? 0,
320
+ });
321
+ }
322
+ for (const child of node.children ?? []) collectWindowBoxes(child, out);
323
+ }
324
+
325
+ // `fireEvent.key` with no explicit target still needs a connection; the most
326
+ // recent render is the only sensible default and matches `screen`.
327
+ let defaultTarget = null;
328
+ export function setDefaultTarget(node) {
329
+ defaultTarget = node;
330
+ }
331
+ function currentTarget() {
332
+ if (!defaultTarget) {
333
+ throw new Error(
334
+ 'react-x11/test: fireEvent.key has no target — call renderX11() first, ' +
335
+ 'or pass { target: node }.',
336
+ );
337
+ }
338
+ return defaultTarget;
339
+ }
340
+
341
+ /**
342
+ * Higher-level interactions, each one `act`-wrapped so the tree has caught
343
+ * up when they resolve. This is the layer to reach for by default;
344
+ * `fireEvent` is there for when a test needs one half of a gesture.
345
+ */
346
+ export const userEvent = {
347
+ async click(node, options) {
348
+ await act(() => fireEvent.click(node, options));
349
+ },
350
+
351
+ async doubleClick(node, options) {
352
+ await act(() => fireEvent.doubleClick(node, options));
353
+ },
354
+
355
+ async hover(node, options) {
356
+ await act(() => fireEvent.mouseMove(node, options));
357
+ },
358
+
359
+ /**
360
+ * Press somewhere outside every window of this root — how a user dismisses
361
+ * a menu, a `Select` or a tooltip. See {@link pointOutsideWindows} for why
362
+ * clicking another widget does not do it.
363
+ */
364
+ async clickOutside({ target = null, button = 1 } = {}) {
365
+ const node = target ?? currentTarget();
366
+ const { x, y } = pointOutsideWindows(node);
367
+ await act(() => fireEvent.screenClick(x, y, { target: node, button }));
368
+ },
369
+
370
+ async unhover(node, options) {
371
+ await act(() => fireEvent.mouseLeave(node, options));
372
+ },
373
+
374
+ async wheel(node, options) {
375
+ await act(() => fireEvent.wheel(node, options));
376
+ },
377
+
378
+ /**
379
+ * Type into a control: click it to take focus, then send each character as
380
+ * a real key. `\n` is Return, so `type(input, 'hello\n')` submits.
381
+ *
382
+ * Characters outside the server's US layout are bound to a spare keycode
383
+ * on the fly, so accented and non-Latin text works without a keymap.
384
+ */
385
+ async type(node, text, { skipClick = false } = {}) {
386
+ if (!skipClick) await userEvent.click(node);
387
+ for (const char of String(text)) {
388
+ const keysym = char === '\n' ? XK_RETURN : keysymOf(char);
389
+ await act(() => fireEvent.key(keysym, { target: node }));
390
+ }
391
+ },
392
+
393
+ /** Tab to the next focusable node (Shift+Tab for the previous). */
394
+ async tab({ shift = false, target = null } = {}) {
395
+ await act(() =>
396
+ fireEvent.key(0xff09 /* XK_Tab */, {
397
+ target,
398
+ modifiers: shift ? ['Shift'] : [],
399
+ }),
400
+ );
401
+ },
402
+
403
+ /** A key by keysym, flushed. */
404
+ async key(keysym, options) {
405
+ await act(() => fireEvent.key(keysym, options));
406
+ },
407
+ };