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,414 @@
1
+ // The component layer of `react-x11/test`: which component made this node,
2
+ // with what props and hooks — and set that state through React's own
3
+ // machinery rather than by poking at internals.
4
+ //
5
+ // Two layers, with different requirements:
6
+ //
7
+ // **The fiber layer** (`ownerChainOf`, `sourceOf`, the `ByComponent`
8
+ // queries) reads what development React already records on every fiber: the
9
+ // JSX owner chain (`_debugOwner`) and the call-site Error (`_debugStack`).
10
+ // Every retained node keeps its host fiber (`node._reactFiber`, set in
11
+ // Reconciler.createInstance), so this needs no dependency and no setup —
12
+ // only React in development mode, which a test run is.
13
+ //
14
+ // **The inspection layer** (`inspect`, and the `setHook` it hands out)
15
+ // drives the React DevTools backend — the same code the standalone DevTools
16
+ // app talks to — entirely in this process. `injectIntoDevTools()` parks a
17
+ // full renderer interface on the global hook before any bridge, agent or
18
+ // socket exists, and that interface is the expensive-to-own half of
19
+ // DevTools: hook trees with names and source locations (react-debug-tools),
20
+ // and overrides that go through `enqueueConcurrentRenderForLane` so React
21
+ // actually re-renders. The WebSocket transport of docs/devtools.md is the
22
+ // half this deliberately skips: nothing here opens a socket or listens on
23
+ // anything, and `ws` is never imported. It needs the `react-devtools-core`
24
+ // package (a devDependency here; add it to yours) — the fiber layer works
25
+ // without it.
26
+
27
+ import { Renderer } from '../Reconciler.js';
28
+ import Reflection from 'react-reconciler/reflection.js';
29
+ import { resolveLocation } from '../ClickToComponent.js';
30
+ import { act, mountedEntries } from './harness.js';
31
+
32
+ const { findCurrentFiberUsingSlowPath } = Reflection;
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // The fiber layer
36
+ // ---------------------------------------------------------------------------
37
+
38
+ /** The display name of a fiber's type: function and class components, and
39
+ * the memo/forwardRef wrappers around them. Null for hosts and everything
40
+ * else — which is what lets the owner walks below skip non-components. */
41
+ function fiberTypeName(type) {
42
+ if (typeof type === 'function') return type.displayName || type.name || null;
43
+ if (typeof type === 'object' && type !== null) {
44
+ // memo(Component) keeps the inner type on `.type`, forwardRef on
45
+ // `.render`; either way the name lives on the inner function
46
+ return type.displayName || fiberTypeName(type.render ?? type.type) || null;
47
+ }
48
+ return null;
49
+ }
50
+
51
+ /**
52
+ * The components that created this node, nearest first — `['Label',
53
+ * 'TaskRow', 'App']`. This is React's JSX owner chain (who *wrote* the
54
+ * element), not tree ancestry: content passed as children belongs to the
55
+ * component whose JSX it appears in, which is the answer a failing test
56
+ * wants. Empty outside development mode, where React records no owners.
57
+ */
58
+ export function ownerChainOf(node) {
59
+ const names = [];
60
+ for (
61
+ let owner = node?._reactFiber?._debugOwner;
62
+ owner;
63
+ owner = owner._debugOwner
64
+ ) {
65
+ const name = fiberTypeName(owner.type);
66
+ if (name) names.push(name);
67
+ }
68
+ return names;
69
+ }
70
+
71
+ /**
72
+ * The source location of the JSX that created this node — `{ file, line,
73
+ * column }` — from the call-site Error development React captures on every
74
+ * element. The same resolution click-to-component uses for the editor jump.
75
+ */
76
+ export function sourceOf(node) {
77
+ return resolveLocation(node?._reactFiber?._debugStack) ?? null;
78
+ }
79
+
80
+ function componentMatches(name, expected) {
81
+ if (expected instanceof RegExp) return expected.test(name);
82
+ if (typeof expected === 'function') return Boolean(expected(name));
83
+ return name === String(expected);
84
+ }
85
+
86
+ function nearestMatchingOwner(node, expected) {
87
+ for (
88
+ let owner = node?._reactFiber?._debugOwner;
89
+ owner;
90
+ owner = owner._debugOwner
91
+ ) {
92
+ const name = fiberTypeName(owner.type);
93
+ if (name && componentMatches(name, expected)) return owner;
94
+ }
95
+ return null;
96
+ }
97
+
98
+ // A fiber and its alternate are the same component instance; two nodes
99
+ // created in different renders can hold either half of the pair.
100
+ function sameInstance(a, b) {
101
+ return a === b || (b != null && a === b.alternate);
102
+ }
103
+
104
+ /**
105
+ * The host roots of every rendered instance of a component: for each
106
+ * instance, the topmost host nodes of its output, in paint order. That is
107
+ * the node `within()` wants next — scoping to it covers the instance's
108
+ * whole subtree, slotted children included, the way a container does.
109
+ */
110
+ export function queryAllByComponent(root, expected) {
111
+ const found = [];
112
+ const visit = (node, parentOwner) => {
113
+ if (!node || node.destroyed) return;
114
+ const owner = nearestMatchingOwner(node, expected);
115
+ // A node whose matching owner is the same instance as its host
116
+ // parent's is interior to output already collected. Children created
117
+ // by the instance's own render props inherit it through `parentOwner`.
118
+ if (owner && !sameInstance(owner, parentOwner)) found.push(node);
119
+ for (const child of node.children ?? []) {
120
+ visit(child, owner ?? parentOwner);
121
+ }
122
+ };
123
+ visit(root, null);
124
+ return found;
125
+ }
126
+
127
+ /** What a ByComponent miss prints: the component names that *are* in the
128
+ * tree, which is the list the caller was trying to spell one of. */
129
+ export function componentInventory(root, limit = 12) {
130
+ const names = new Set();
131
+ const visit = (node) => {
132
+ if (!node || node.destroyed) return;
133
+ for (
134
+ let owner = node._reactFiber?._debugOwner;
135
+ owner;
136
+ owner = owner._debugOwner
137
+ ) {
138
+ const name = fiberTypeName(owner.type);
139
+ if (name) names.add(name);
140
+ }
141
+ for (const child of node.children ?? []) visit(child);
142
+ };
143
+ visit(root);
144
+ if (names.size === 0) {
145
+ return ' (no components — is React running in development mode?)';
146
+ }
147
+ return [...names]
148
+ .slice(0, limit)
149
+ .map((name) => ` <${name}>`)
150
+ .join('\n');
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // The inspection layer
155
+ // ---------------------------------------------------------------------------
156
+
157
+ let devtoolsReady = null;
158
+
159
+ /**
160
+ * Install the DevTools global hook and register the renderer, once, and
161
+ * hand back the backend's renderer interface. No socket: installing the
162
+ * hook is `initialize()`; the interface appears at `injectIntoDevTools()`;
163
+ * `connectToDevTools` — the WebSocket half — is simply never called. If the
164
+ * app under test already enabled REACT_X11_DEVTOOLS, its interface is
165
+ * reused rather than injecting the renderer a second time.
166
+ */
167
+ function rendererInterface() {
168
+ if (devtoolsReady) return devtoolsReady;
169
+ devtoolsReady = (async () => {
170
+ let hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
171
+ if (!hook?.rendererInterfaces?.size) {
172
+ // the backend bundle expects browser-ish globals, exactly as
173
+ // DevToolsIntegration.prepare() shims them
174
+ global.self ??= global;
175
+ global.window ??= global;
176
+ let mod;
177
+ try {
178
+ mod = await import('react-devtools-core');
179
+ } catch (err) {
180
+ throw new Error(
181
+ 'react-x11/test: inspect() drives the React DevTools backend ' +
182
+ 'in-process, which needs the react-devtools-core package — ' +
183
+ 'add it to your devDependencies. Original error: ' +
184
+ err.message,
185
+ { cause: err },
186
+ );
187
+ }
188
+ const api = mod.default?.initialize ? mod.default : mod;
189
+ // The backend can also patch console (component stacks appended to
190
+ // console.error, and so on). A test harness must not: every setting
191
+ // that touches console goes in off.
192
+ api.initialize({
193
+ appendComponentStack: false,
194
+ breakOnConsoleErrors: false,
195
+ showInlineWarningsAndErrors: false,
196
+ hideConsoleLogsInStrictMode: false,
197
+ });
198
+ hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
199
+ Renderer.injectIntoDevTools();
200
+ }
201
+ const interfaces = [...hook.rendererInterfaces.values()];
202
+ const ri = interfaces[interfaces.length - 1];
203
+ // The backend queues every commit's tree deltas for a frontend that
204
+ // will never attach here; one flush (to no listeners) discards the
205
+ // queue and stops it growing for the rest of the run.
206
+ ri.flushInitialOperations();
207
+ return ri;
208
+ })();
209
+ return devtoolsReady;
210
+ }
211
+
212
+ /** The reconciler internals the hook holds for our renderer —
213
+ * `scheduleUpdate` and friends. Dev builds only. */
214
+ function rendererInternals() {
215
+ const renderers = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__?.renderers;
216
+ if (!renderers?.size) return null;
217
+ return [...renderers.values()][renderers.size - 1];
218
+ }
219
+
220
+ /**
221
+ * The backend only observes commits, so a root mounted before the hook
222
+ * existed — the normal case, since the hook installs lazily on the first
223
+ * `inspect()` — is invisible until its next commit. One re-render commit
224
+ * per unobserved root brings its whole tree in; `scheduleUpdate` is the
225
+ * same internals call DevTools itself uses to apply an override.
226
+ */
227
+ async function refreshUnobservedRoots(ri) {
228
+ const internals = rendererInternals();
229
+ if (typeof internals?.scheduleUpdate !== 'function') return;
230
+ for (const entry of mountedEntries()) {
231
+ const node = entry.windowNode;
232
+ if (!node || node.destroyed) continue;
233
+ if (ri.getElementIDForHostInstance(node) != null) continue;
234
+ const fiber = node._reactFiber;
235
+ if (!fiber) continue;
236
+ await act(() => internals.scheduleUpdate(fiber), entry);
237
+ }
238
+ }
239
+
240
+ async function resolveElementID(ri, node) {
241
+ let id = ri.getElementIDForHostInstance(node);
242
+ if (id == null) {
243
+ await refreshUnobservedRoots(ri);
244
+ id = ri.getElementIDForHostInstance(node);
245
+ }
246
+ return id ?? null;
247
+ }
248
+
249
+ /**
250
+ * The current-generation fiber of the inspected component, found by
251
+ * walking tree ancestry from the node until the display name DevTools
252
+ * reported matches. Values are read from this fiber, so they are the
253
+ * committed ones — the node's own `_reactFiber` was captured at mount and
254
+ * may be the retired half of the alternate pair, one render behind.
255
+ * Null when no ancestor carries that name (an exotic wrapper, say) — the
256
+ * caller then falls back to DevTools' own value copies.
257
+ */
258
+ function currentComponentFiber(node, displayName) {
259
+ let fiber = node?._reactFiber ?? null;
260
+ if (!fiber || displayName == null) return null;
261
+ try {
262
+ fiber = findCurrentFiberUsingSlowPath(fiber) ?? fiber;
263
+ } catch {
264
+ // detached or mid-update — read from the captured generation instead
265
+ }
266
+ for (let f = fiber; f; f = f.return) {
267
+ if (fiberTypeName(f.type) === displayName) return f;
268
+ }
269
+ return null;
270
+ }
271
+
272
+ /** The value in the component's Nth hook slot — `id` is React's own index
273
+ * over the hooks that occupy one, which is how the override path addresses
274
+ * them too. For useState/useReducer the slot's `memoizedState` *is* the
275
+ * value; only they are read this way. */
276
+ function hookSlotValue(fiber, nativeIndex, fallback) {
277
+ let slot = fiber.memoizedState;
278
+ for (let i = 0; i < nativeIndex && slot; i++) slot = slot.next ?? null;
279
+ return slot ? slot.memoizedState : fallback;
280
+ }
281
+
282
+ function mapHooks(data, fiber) {
283
+ if (!Array.isArray(data)) return [];
284
+ const map = (h) => ({
285
+ id: h.id ?? null,
286
+ name: h.name,
287
+ // live value for the editable hooks; DevTools' copy (primitives exact,
288
+ // deep objects as previews) for the rest
289
+ value:
290
+ h.isStateEditable && h.id != null && fiber
291
+ ? hookSlotValue(fiber, h.id, h.value)
292
+ : h.value,
293
+ editable: h.isStateEditable === true,
294
+ source: h.hookSource
295
+ ? {
296
+ file: h.hookSource.fileName,
297
+ line: h.hookSource.lineNumber,
298
+ column: h.hookSource.columnNumber,
299
+ }
300
+ : null,
301
+ subHooks: Array.isArray(h.subHooks) ? h.subHooks.map(map) : [],
302
+ });
303
+ return data.map(map);
304
+ }
305
+
306
+ let inspectRequestID = 0;
307
+
308
+ /**
309
+ * The React side of a node: the nearest mounted component above it (the
310
+ * one DevTools would select), its live props, its hooks — named and with
311
+ * source locations, via react-debug-tools — and `setHook` to change what
312
+ * useState/useReducer hold.
313
+ *
314
+ * Two costs to know about. Reading hooks **re-invokes the component's
315
+ * render function** (under a stub dispatcher, output discarded) — that is
316
+ * how react-debug-tools recovers hook names, and a render-counting test
317
+ * will see it. And the nearest component is tree ancestry, where
318
+ * `ownerChainOf`/`getByComponent` are JSX ownership; the two differ for
319
+ * content passed as children.
320
+ */
321
+ export async function inspect(target) {
322
+ if (!target || typeof target !== 'object' || !target._reactFiber) {
323
+ throw new Error(
324
+ 'react-x11/test: inspect() takes a rendered node, the kind a query ' +
325
+ 'returns.',
326
+ );
327
+ }
328
+ const ri = await rendererInterface();
329
+ const id = await resolveElementID(ri, target);
330
+ if (id == null) {
331
+ throw new Error(
332
+ 'react-x11/test: inspect() could not map this node to a component — ' +
333
+ 'it may have unmounted, or React is not in development mode.',
334
+ );
335
+ }
336
+ const res = ri.inspectElement(++inspectRequestID, id, null, true);
337
+ if (res.type !== 'full-data') {
338
+ const detail = res.message ? ` — ${res.message}` : '';
339
+ throw new Error(
340
+ `react-x11/test: inspect() failed with "${res.type}"${detail}`,
341
+ );
342
+ }
343
+ const v = res.value;
344
+ const name = ri.getDisplayNameForElementID(id);
345
+ const fiber = currentComponentFiber(target, name);
346
+ const hooks = mapHooks(v.hooks?.data, fiber);
347
+
348
+ return {
349
+ /** DevTools' element id — stable for the life of the instance. */
350
+ id,
351
+ name,
352
+ /** The component's live props object. Treat it as read-only: to render
353
+ * with different props, render with different props. */
354
+ props: fiber ? fiber.memoizedProps : (v.props?.data ?? null),
355
+ /** Class component state; null for function components. */
356
+ state: fiber?.tag === 1 ? fiber.memoizedState : (v.state?.data ?? null),
357
+ /** Legacy class context only — a `useContext` value appears among the
358
+ * hooks instead, and is not settable (wrap a provider). */
359
+ context: v.context?.data ?? null,
360
+ hooks,
361
+ /** Owner names, nearest first — who rendered this component. */
362
+ owners: Array.isArray(v.owners)
363
+ ? v.owners.map((o) => o.displayName).filter(Boolean)
364
+ : [],
365
+
366
+ /**
367
+ * Set a useState/useReducer value: `setHook(id, value)` or
368
+ * `setHook(id, path, value)` for a path inside it. `id` is the hook's
369
+ * `id` as listed in `hooks` — an index the Rules of Hooks fix for the
370
+ * life of the component, so validating against this snapshot cannot go
371
+ * stale. The write goes through the reconciler's own override path
372
+ * (the one DevTools uses), inside `act()`, so React has re-rendered
373
+ * and repainted by the time the promise resolves. A reducer is
374
+ * bypassed, not driven: the value is placed, no action runs.
375
+ */
376
+ async setHook(hookID, ...rest) {
377
+ const [path, value] = rest.length >= 2 ? rest : [[], rest[0]];
378
+ if (!v.canEditHooks) {
379
+ throw new Error(
380
+ 'react-x11/test: this reconciler build exposes no hook ' +
381
+ 'overrides — setHook() needs react-reconciler in development ' +
382
+ 'mode (NODE_ENV not "production").',
383
+ );
384
+ }
385
+ const flat = [];
386
+ const collect = (list) => {
387
+ for (const h of list) {
388
+ flat.push(h);
389
+ collect(h.subHooks);
390
+ }
391
+ };
392
+ collect(hooks);
393
+ const hit = flat.find((h) => h.id === hookID);
394
+ if (!hit || !hit.editable) {
395
+ const editable = flat
396
+ .filter((h) => h.editable)
397
+ .map(
398
+ (h) =>
399
+ ` ${h.id}: ${h.name}` +
400
+ (h.source ? ` (${h.source.file}:${h.source.line})` : ''),
401
+ );
402
+ throw new Error(
403
+ `react-x11/test: hook ${hookID} ` +
404
+ (hit
405
+ ? `is a ${hit.name} hook — only useState/useReducer state ` +
406
+ 'can be set'
407
+ : 'does not exist on this component') +
408
+ `. Editable hooks:\n${editable.join('\n') || ' (none)'}`,
409
+ );
410
+ }
411
+ await act(() => ri.overrideValueAtPath('hooks', id, hookID, path, value));
412
+ },
413
+ };
414
+ }