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,455 @@
1
+ // renderX11 / act / cleanup — the harness this repo's own tests kept
2
+ // rebuilding by hand, made supported.
3
+ //
4
+ // The unusual capability it publishes: a UI test here runs a **real X11
5
+ // protocol connection to a real X server**, in-process, in pure JavaScript,
6
+ // with no $DISPLAY and no xvfb — so it reads pixels back with GetImage and
7
+ // injects input through the server's own grab and focus machinery. That is
8
+ // node-x11's `lib/xserver`, and nothing about it is specific to CI.
9
+
10
+ import React from 'react';
11
+ import { createRoot } from '../Reconciler.js';
12
+ import { setAnimationClock } from '../nodes.js';
13
+ import { setAppearanceForTests } from '../appearance.js';
14
+
15
+ // A test process must not register with the desktop's live AT-SPI registry —
16
+ // a suite on a developer desktop would otherwise parade hundreds of phantom
17
+ // applications through a running screen reader. NO_AT_BRIDGE is the
18
+ // ecosystem-wide way to say so (GTK's own test suite sets it); `??=` so an
19
+ // explicit environment still wins, and an explicit AT_SPI_BUS_ADDRESS (the
20
+ // hermetic bridge tests) overrides it inside startA11y anyway.
21
+ process.env.NO_AT_BRIDGE ??= '1';
22
+
23
+ // x11 and pngjs arrive through ntk rather than as direct dependencies, so
24
+ // they are imported lazily: an app that never runs a pixel test should not
25
+ // pay for them, and a hoisting layout that hides them should say so clearly
26
+ // rather than fail with MODULE_NOT_FOUND from inside our code.
27
+ async function optional(specifier, why) {
28
+ try {
29
+ return await import(specifier);
30
+ } catch (err) {
31
+ throw new Error(
32
+ `react-x11/test: could not load "${specifier}", which ${why}. It ships ` +
33
+ `inside ntk's dependency tree, so this usually means a package ` +
34
+ `manager that does not hoist — add "${specifier.split('/')[0]}" to ` +
35
+ `your own devDependencies.\n${err.message}`,
36
+ { cause: err },
37
+ );
38
+ }
39
+ }
40
+
41
+ /** Everything renderX11 created, so cleanup() can take it all down. */
42
+ const mounted = new Set();
43
+
44
+ const DEFAULT_WIDTH = 640;
45
+ const DEFAULT_HEIGHT = 480;
46
+ // The screen is deliberately bigger than the window, because on a real
47
+ // display it is: a press "outside the application" — what dismisses a menu —
48
+ // has nowhere to land otherwise.
49
+ const DEFAULT_SCREEN = { width: 1280, height: 800 };
50
+
51
+ /**
52
+ * Drain in-flight request/reply chains. A GetInputFocus round trip is the
53
+ * cheapest request with a reply, and its reply cannot arrive before every
54
+ * request queued ahead of it has been processed — which is what makes it a
55
+ * barrier rather than a sleep.
56
+ */
57
+ export function settle(app, roundTrips = 2) {
58
+ const X = app?.X;
59
+ if (typeof X?.GetInputFocus !== 'function') return Promise.resolve();
60
+ let chain = Promise.resolve();
61
+ for (let i = 0; i < roundTrips; i++) {
62
+ chain = chain.then(
63
+ () =>
64
+ new Promise((resolve) => {
65
+ // resolve either way: a closing connection must not hang a test
66
+ X.GetInputFocus(() => resolve());
67
+ }),
68
+ );
69
+ }
70
+ return chain;
71
+ }
72
+
73
+ async function createXServerApp({ screen, fonts }) {
74
+ const [{ default: xserver }, ntk] = await Promise.all([
75
+ optional('x11/lib/xserver/index.js', 'is the in-process X server'),
76
+ import('ntk'),
77
+ ]);
78
+ const server = xserver.createServer({
79
+ width: screen.width,
80
+ height: screen.height,
81
+ });
82
+ const [serverEnd, clientEnd] = xserver.createStreamPair();
83
+ server.addClientStream(serverEnd);
84
+
85
+ let fontSource;
86
+ if (fonts) {
87
+ const { readFileSync } = await import('node:fs');
88
+ fontSource = new ntk.StaticFontSource();
89
+ for (const [family, file] of Object.entries(fonts)) {
90
+ // `sans-serif: '/path/Inter.ttf'` reads as "this family is that file",
91
+ // which is both the registration and the alias
92
+ const registered = `react-x11-test:${family}`;
93
+ fontSource.add(readFileSync(file), { family: registered });
94
+ fontSource.alias(family, registered);
95
+ }
96
+ }
97
+ const app = await ntk.createClient({ stream: clientEnd, fontSource });
98
+ // fireEvent injects through the server, and all it is handed is a node —
99
+ // so the connection carries the way back to the server that owns it
100
+ app._reactX11TestServer = server;
101
+ return { server, app };
102
+ }
103
+
104
+ /**
105
+ * Mount a tree against a real in-process X server (or, with
106
+ * `backend: 'mock'`, against a fake 2d context that logs draw operations
107
+ * and needs no server at all).
108
+ *
109
+ * The element is wrapped in a `<window>` of the given size unless it *is* a
110
+ * `<window>`, the same way Testing Library's `render` puts your component in
111
+ * a container div. Pass `wrap: false` when a component of yours renders the
112
+ * window itself.
113
+ *
114
+ * ```js
115
+ * const { app, server, ctx, getByText, unmount } = await renderX11(<App />, {
116
+ * fonts: { 'sans-serif': '/usr/share/fonts/.../DejaVuSans.ttf' },
117
+ * });
118
+ * ```
119
+ *
120
+ * Pixel assertions need `fonts`: font-family resolution otherwise shells out
121
+ * to `fc-match`, which is a different answer on every machine and no answer
122
+ * at all in a container.
123
+ */
124
+ export async function renderX11(element, options = {}) {
125
+ const {
126
+ width = DEFAULT_WIDTH,
127
+ height = DEFAULT_HEIGHT,
128
+ backend = 'xserver',
129
+ screen: screenSize = {
130
+ width: Math.max(
131
+ DEFAULT_SCREEN.width,
132
+ (options.width ?? DEFAULT_WIDTH) + 200,
133
+ ),
134
+ height: Math.max(
135
+ DEFAULT_SCREEN.height,
136
+ (options.height ?? DEFAULT_HEIGHT) + 200,
137
+ ),
138
+ },
139
+ fonts = null,
140
+ wrap = element?.type !== 'window',
141
+ title = 'react-x11 test',
142
+ colorScheme = 'light',
143
+ a11y = false,
144
+ ...rootOptions
145
+ } = options;
146
+
147
+ let server = null;
148
+ let app = options.app ?? null;
149
+ if (!app) {
150
+ if (backend === 'mock') {
151
+ const { createMockApp } = await import('./mock-app.js');
152
+ app = createMockApp();
153
+ } else {
154
+ ({ server, app } = await createXServerApp({ screen: screenSize, fonts }));
155
+ }
156
+ }
157
+
158
+ // **Pinned, and light by default.** react-x11's built-in palette follows
159
+ // the desktop, so without this every pixel assertion in this suite and in
160
+ // every application's would be a function of whose machine ran it — green
161
+ // on a light desktop, red on a dark one, with nothing in the test saying
162
+ // so. After the app exists, so it wins over `createMockApp`'s own pin.
163
+ // `colorScheme: 'dark'` renders the other palette; `'system'` releases the
164
+ // pin, for a test that is *about* what the desktop reports.
165
+ setAppearanceForTests(
166
+ colorScheme === 'system'
167
+ ? null
168
+ : colorScheme === 'dark'
169
+ ? { colorScheme: 'dark' }
170
+ : {},
171
+ );
172
+
173
+ const root = await createRoot({ app, ...rootOptions });
174
+
175
+ // Installed **before** the mount, so the initial tree is baselined and
176
+ // the first render is not reported as a wall of changes — and after
177
+ // everything that can fail without an entry to hang the teardown on.
178
+ // `cleanup()` takes it down with the render it belongs to.
179
+ let at = null;
180
+ if (a11y) {
181
+ const { installA11ySpy } = await import('./a11y.js');
182
+ at = installA11ySpy();
183
+ }
184
+
185
+ const tree = wrap
186
+ ? React.createElement('window', { width, height, title }, element)
187
+ : element;
188
+
189
+ const entry = {
190
+ root,
191
+ app,
192
+ server,
193
+ ownsApp: !options.app,
194
+ unmounted: false,
195
+ windowNode: null,
196
+ at,
197
+ };
198
+ mounted.add(entry);
199
+
200
+ await act(() => {
201
+ // the render callback hands back the root child's public instance, which
202
+ // for a `<window>` is the live ntk window — the only supported way in
203
+ root.render(tree, (instance) => {
204
+ entry.windowNode = instance?._reactX11Node ?? null;
205
+ });
206
+ }, entry);
207
+
208
+ const windowNode = entry.windowNode;
209
+ if (!windowNode?.isWindow) {
210
+ throw new Error(
211
+ 'react-x11/test: renderX11 found no <window> at the root of the tree. ' +
212
+ 'Pass `wrap: false` only when your component renders one itself.',
213
+ );
214
+ }
215
+
216
+ // **The harness plays the window manager**, because there is not one.
217
+ //
218
+ // Deciding which window gets the keyboard is a window manager's job, and
219
+ // the in-process server has no WM — so the input focus stays wherever the
220
+ // server started it (PointerRoot), and a key press goes to whatever the
221
+ // pointer happens to be over. That makes `fireEvent.key` work only after
222
+ // something moved the pointer, which is a rule nobody should have to
223
+ // learn. One SetInputFocus at mount makes keyboard delivery deterministic
224
+ // and matches what every real session does.
225
+ if (server && typeof app.X?.SetInputFocus === 'function') {
226
+ app.X.SetInputFocus(windowNode.window.id, 2 /* RevertToParent */);
227
+ // And **wait for the event mask to reach the server before any input can
228
+ // be injected.** ntk grows a window's mask lazily, from a `newListener`
229
+ // hook: subscribing to `mousemove` is what selects PointerMotion. That
230
+ // ChangeWindowAttributes is queued like any other request, but injection
231
+ // is not a request — `server.injectPointerMove` runs *inside* the server
232
+ // against whatever state it has right now. Get that ordering wrong and
233
+ // the first hover of a test silently delivers nothing, which is a
234
+ // miserable thing to debug. A macrotask plus a round trip is the barrier.
235
+ await new Promise((resolve) => setImmediate(resolve));
236
+ await settle(app);
237
+ }
238
+
239
+ const api = {
240
+ root,
241
+ app,
242
+ server,
243
+ /** The `WindowNode` the tree mounted into — the paint and event root. */
244
+ windowNode,
245
+ /** The live ntk `Window`. */
246
+ window: windowNode?.window ?? null,
247
+ /** Its 2d context, which is what pixel assertions read from. */
248
+ get ctx() {
249
+ return windowNode?.window?.getContext?.('2d') ?? null;
250
+ },
251
+ /** The assistive-technology spy, when `a11y: true` asked for one —
252
+ * what a screen reader would have been told (docs/accessibility.md). */
253
+ at,
254
+ /** Re-render into the same root, then settle. */
255
+ rerender: (next) =>
256
+ act(() => {
257
+ root.render(
258
+ wrap
259
+ ? React.createElement('window', { width, height, title }, next)
260
+ : next,
261
+ );
262
+ }, entry),
263
+ unmount: () => unmountEntry(entry),
264
+ };
265
+ return api;
266
+ }
267
+
268
+ /**
269
+ * Every window node in a tree: the root one, plus nested `<window>`s and
270
+ * every `<popup>` — a menu or a dialog is its own paint root and schedules
271
+ * its own frames, so a flush that skipped them would leave a popup blank.
272
+ */
273
+ export function windowNodesOf(node, out = []) {
274
+ if (!node || node.destroyed) return out;
275
+ if (node.isWindow) out.push(node);
276
+ for (const child of node.children ?? []) windowNodesOf(child, out);
277
+ return out;
278
+ }
279
+
280
+ /**
281
+ * Flush everything that stands between a state update and a pixel.
282
+ *
283
+ * Three separate clocks, and a wrapper that drives only the first stays
284
+ * flaky on pixel assertions:
285
+ *
286
+ * 1. **React's** — `React.act`, with `IS_REACT_ACT_ENVIRONMENT` set, so
287
+ * effects and passive work run before this returns.
288
+ * 2. **ntk's frame clock** — painting is scheduled through
289
+ * `requestAnimationFrame`, which is paced behind a server fence. Driving
290
+ * events synthetically can leave a frame scheduled and never run, so the
291
+ * frame is run directly instead of waited for.
292
+ * 3. **The X connection** — a round trip, so the server has actually
293
+ * processed the drawing before anything reads pixels back.
294
+ */
295
+ export async function act(fn, only = null) {
296
+ const entries = only ? [only] : [...mounted];
297
+ const previous = globalThis.IS_REACT_ACT_ENVIRONMENT;
298
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
299
+ try {
300
+ await React.act(async () => {
301
+ await fn?.();
302
+ });
303
+ // Anything just injected is **on the wire, not yet in the tree**: the
304
+ // server writes events asynchronously, so a round trip on its own can
305
+ // overtake them and `act` would return before the pointer move had been
306
+ // dispatched at all — which looks exactly like a hover that does not
307
+ // work. A macrotask lets those writes flush, the round trip confirms the
308
+ // server consumed them, and the empty `act` afterwards flushes whatever
309
+ // React work the dispatch produced.
310
+ // Twice, because the path is two stages: the first round gets the event
311
+ // off the wire and dispatched, the second lets the React update that
312
+ // dispatch produced — `Button`'s hover state is React state, not a style
313
+ // block — commit and settle. One round leaves that update in flight, and
314
+ // React says so out loud ("An update to Button inside a test was not
315
+ // wrapped in act(...)"), which is how this was found.
316
+ for (let round = 0; round < 2; round++) {
317
+ await React.act(async () => {
318
+ await new Promise((resolve) => setImmediate(resolve));
319
+ for (const entry of entries) await settle(entry.app);
320
+ });
321
+ }
322
+ } finally {
323
+ globalThis.IS_REACT_ACT_ENVIRONMENT = previous;
324
+ }
325
+ for (const entry of entries) flushFrames(entry);
326
+ for (const entry of entries) await settle(entry.app);
327
+ }
328
+
329
+ /** Run the frame the scheduler would have run, for every window in a root. */
330
+ function flushFrames(entry) {
331
+ for (const node of windowNodesOf(entry.windowNode)) {
332
+ if (!node.window) continue;
333
+ // clearing `_scheduled` first: the scheduler treats it as "a frame is
334
+ // already coming", and a frame run out of band has to hand that back or
335
+ // the next invalidate schedules nothing
336
+ node._scheduled = false;
337
+ node.flush();
338
+ }
339
+ }
340
+
341
+ async function unmountEntry(entry) {
342
+ if (entry.unmounted) return;
343
+ entry.unmounted = true;
344
+ mounted.delete(entry);
345
+ // before the unmount, so the teardown's own hook traffic is not recorded
346
+ entry.at?.uninstall();
347
+ try {
348
+ entry.root.unmount?.();
349
+ } catch {
350
+ // a root whose connection already died: nothing left to unmount
351
+ }
352
+ await settle(entry.app);
353
+ if (entry.ownsApp) {
354
+ try {
355
+ await entry.app.close?.();
356
+ } catch {
357
+ // likewise
358
+ }
359
+ }
360
+ entry.server?.close?.();
361
+ }
362
+
363
+ /**
364
+ * Unmount everything `renderX11` mounted, restore the animation clock, and
365
+ * close the connections and servers. Call it from an `afterEach`.
366
+ *
367
+ * Servers leaking is survivable at fifteen test files and is not at a
368
+ * hundred and fifty — each one holds a socket pair and a paint surface.
369
+ */
370
+ export async function cleanup() {
371
+ restoreFrameClock();
372
+ for (const entry of [...mounted]) await unmountEntry(entry);
373
+ // Released **after** the roots are down, not before: releasing publishes a
374
+ // change, every mounted root repaints on one, and a repaint scheduled onto
375
+ // a connection that is about to close lands after it has.
376
+ setAppearanceForTests(null);
377
+ }
378
+
379
+ // --- the animation clock ---------------------------------------------------
380
+
381
+ let clockInstalled = false;
382
+
383
+ /**
384
+ * Drive transitions from a number you control instead of from wall-clock
385
+ * time. `setAnimationClock` exists for exactly this; it was reachable only
386
+ * through a deep import into `src/nodes.js`.
387
+ *
388
+ * ```js
389
+ * const clock = withFrameClock();
390
+ * fireEvent.mouseEnter(button); // starts a 120ms hover transition
391
+ * clock.advance(60); await act(); // half way
392
+ * clock.advance(60); await act(); // arrived
393
+ * clock.restore();
394
+ * ```
395
+ */
396
+ export function withFrameClock(startAt = 0) {
397
+ let time = startAt;
398
+ setAnimationClock(() => time);
399
+ clockInstalled = true;
400
+ return {
401
+ get now() {
402
+ return time;
403
+ },
404
+ advance(ms) {
405
+ time += ms;
406
+ return time;
407
+ },
408
+ set(ms) {
409
+ time = ms;
410
+ return time;
411
+ },
412
+ restore: restoreFrameClock,
413
+ };
414
+ }
415
+
416
+ function restoreFrameClock() {
417
+ if (!clockInstalled) return;
418
+ clockInstalled = false;
419
+ setAnimationClock(() => Date.now());
420
+ }
421
+
422
+ /**
423
+ * Retry `fn` until it stops throwing, flushing between attempts.
424
+ *
425
+ * The tool for anything whose arrival is not a single deterministic step:
426
+ * an async rich-content reflow, an image decode, a timer, or a repaint whose
427
+ * cause travelled through the X connection and back. Each attempt is
428
+ * preceded by an `act()`, so waiting also *advances* — this is not a sleep
429
+ * with a condition on it.
430
+ *
431
+ * ```js
432
+ * await waitFor(async () => {
433
+ * assert.notDeepStrictEqual(await pixelAt(ctx, x, y), before);
434
+ * });
435
+ * ```
436
+ */
437
+ export async function waitFor(fn, { timeout = 2000, interval = 10 } = {}) {
438
+ const deadline = Date.now() + timeout;
439
+ let last;
440
+ for (;;) {
441
+ try {
442
+ return await fn();
443
+ } catch (err) {
444
+ last = err;
445
+ }
446
+ if (Date.now() >= deadline) throw last;
447
+ await act();
448
+ await new Promise((resolve) => setTimeout(resolve, interval));
449
+ }
450
+ }
451
+
452
+ /** Internal: the entries `fireEvent` and the queries resolve against. */
453
+ export function mountedEntries() {
454
+ return [...mounted];
455
+ }