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,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
|
+
}
|