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.
- package/README.md +330 -81
- package/package.json +132 -14
- package/src/ClickToComponent.js +9 -2
- package/src/DevToolsIntegration.js +440 -32
- package/src/Reconciler.js +463 -165
- package/src/a11y.js +1299 -0
- package/src/acceleratorhooks.js +86 -0
- package/src/accelerators.js +128 -0
- package/src/activate.js +168 -0
- package/src/anchor.js +360 -0
- package/src/appcontext.js +161 -0
- package/src/appearance.js +748 -0
- package/src/appearancehooks.js +96 -0
- package/src/apphooks.js +73 -0
- package/src/application.js +748 -0
- package/src/atspi.js +1953 -0
- package/src/bus.js +545 -0
- package/src/bushooks.js +136 -0
- package/src/clientmessage.js +140 -0
- package/src/clipboard.js +121 -0
- package/src/components/Button.js +148 -33
- package/src/components/Checkbox.js +66 -32
- package/src/components/Dialog.js +96 -29
- package/src/components/FileDialog.js +491 -0
- package/src/components/Icon.js +329 -0
- package/src/components/Menu.js +797 -133
- package/src/components/PasswordInput.js +434 -0
- package/src/components/ProgressBar.js +91 -12
- package/src/components/Radio.js +78 -35
- package/src/components/Select.js +255 -65
- package/src/components/Slider.js +102 -41
- package/src/components/SplitPane.js +206 -0
- package/src/components/Switch.js +100 -22
- package/src/components/Table.js +529 -0
- package/src/components/Tabs.js +211 -0
- package/src/components/Tooltip.js +528 -54
- package/src/components/anchor.js +154 -107
- package/src/components/change.js +34 -0
- package/src/components/dnd.js +103 -0
- package/src/components/index.js +16 -3
- package/src/components/keys.js +17 -20
- package/src/components/scribble.js +247 -0
- package/src/components/theme.js +350 -42
- package/src/components/typeahead.js +12 -2
- package/src/compose.js +868 -0
- package/src/compositing.js +224 -0
- package/src/dbusmenu.js +384 -0
- package/src/debug.d.ts +63 -0
- package/src/debug.js +642 -0
- package/src/decorations.js +486 -0
- package/src/desktopsettings.js +209 -0
- package/src/desktopsettingshooks.js +62 -0
- package/src/dnd.js +1725 -0
- package/src/editmenu.js +272 -0
- package/src/errors.js +98 -0
- package/src/events.js +1406 -162
- package/src/extensions.js +45 -0
- package/src/filedialog.js +375 -0
- package/src/filedialoghooks.js +132 -0
- package/src/fonthooks.js +64 -0
- package/src/fonts.js +301 -0
- package/src/foreignnodes.js +519 -0
- package/src/frame/child.js +33 -0
- package/src/frame/childmain.js +274 -0
- package/src/frame/env.js +140 -0
- package/src/frame/index.js +444 -0
- package/src/frame/lifecycle.js +67 -0
- package/src/frame/protocol.js +179 -0
- package/src/frames.js +73 -0
- package/src/glbackend.js +114 -0
- package/src/glnodes.js +125 -67
- package/src/globalmenu.js +683 -0
- package/src/host.d.ts +88 -0
- package/src/host.js +42 -0
- package/src/idle.js +490 -0
- package/src/idlehooks.js +100 -0
- package/src/imagesource.js +349 -0
- package/src/index.d.ts +475 -0
- package/src/index.js +83 -9
- package/src/inputtime.js +206 -0
- package/src/jsx-dev-runtime.d.ts +21 -0
- package/src/jsx-dev-runtime.js +2 -0
- package/src/jsx-runtime.d.ts +39 -0
- package/src/jsx-runtime.js +6 -0
- package/src/keyboard.js +256 -0
- package/src/keyboardstate.js +278 -0
- package/src/keyboardstatehooks.js +58 -0
- package/src/keysyms.d.ts +149 -0
- package/src/keysyms.js +270 -0
- package/src/locale.js +170 -0
- package/src/localehooks.js +47 -0
- package/src/menuitem.js +223 -0
- package/src/node.d.ts +597 -0
- package/src/node.js +44 -0
- package/src/nodes.js +9546 -690
- package/src/ntk.d.ts +44 -0
- package/src/ntk.js +25 -0
- package/src/paintcache.js +366 -0
- package/src/palette.js +380 -0
- package/src/pastestate.js +66 -0
- package/src/portal.js +461 -0
- package/src/priority.js +26 -0
- package/src/refresh/index.d.ts +40 -0
- package/src/refresh/index.js +122 -0
- package/src/refresh/loader.d.ts +37 -0
- package/src/refresh/loader.js +401 -0
- package/src/refresh/register.d.ts +5 -0
- package/src/refresh/register.js +13 -0
- package/src/registry.js +232 -0
- package/src/scale.js +626 -0
- package/src/scalehooks.js +27 -0
- package/src/screencolor.js +640 -0
- package/src/screencolorhooks.js +101 -0
- package/src/screens.js +754 -0
- package/src/screenshooks.js +137 -0
- package/src/startup.js +302 -0
- package/src/style.d.ts +126 -0
- package/src/style.js +33 -0
- package/src/styles.js +1482 -6
- package/src/svgnodes.js +306 -0
- package/src/testing/a11y.js +484 -0
- package/src/testing/components.js +414 -0
- package/src/testing/events.js +407 -0
- package/src/testing/harness.js +455 -0
- package/src/testing/index.d.ts +558 -0
- package/src/testing/index.js +85 -0
- package/src/testing/mock-app.js +463 -0
- package/src/testing/pixels.js +152 -0
- package/src/testing/queries.js +224 -0
- package/src/textrange.js +83 -0
- package/src/textselection.js +439 -0
- package/src/trace-registry.js +63 -0
- package/src/transfer.js +93 -0
- package/src/types/appearance.d.ts +84 -0
- package/src/types/application.d.ts +173 -0
- package/src/types/components.d.ts +820 -0
- package/src/types/dbus.d.ts +177 -0
- package/src/types/elements.d.ts +897 -0
- package/src/types/events.d.ts +549 -0
- package/src/types/filedialog.d.ts +260 -0
- package/src/types/fonts.d.ts +124 -0
- package/src/types/frame.d.ts +146 -0
- package/src/types/globalmenu.d.ts +42 -0
- package/src/types/nodes.d.ts +199 -0
- package/src/types/screencolor.d.ts +84 -0
- package/src/types/style.d.ts +434 -0
- package/src/types/system.d.ts +287 -0
- package/src/windowid.js +151 -0
- package/src/windowstate.js +393 -0
- package/src/xsettings.js +336 -0
- package/src/yoga.d.ts +55 -0
- package/src/yoga.js +135 -0
- package/src/components/Canvas3D.js +0 -28
- package/src/geometry3d.js +0 -223
- package/src/pointer3d.js +0 -158
- package/src/raycast3d.js +0 -146
- package/src/richnodes.js +0 -436
- 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
|
+
}
|