react-x11 2.51.0 → 2.52.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 +87 -54
- package/docs/README.md +410 -0
- package/docs/accessibility.md +628 -0
- package/docs/appearance.md +416 -0
- package/docs/architecture/accessibility.md +470 -0
- package/docs/architecture/animation.md +707 -0
- package/docs/architecture/container-queries.md +525 -0
- package/docs/architecture/custom-layout.md +334 -0
- package/docs/architecture/drag-and-drop.md +941 -0
- package/docs/architecture/element-layer-contents.md +622 -0
- package/docs/architecture/frame-pacing.md +255 -0
- package/docs/architecture/grid-layout.md +358 -0
- package/docs/architecture/protocol-efficiency.md +357 -0
- package/docs/architecture/video.md +1149 -0
- package/docs/architecture/windowed-regions.md +854 -0
- package/docs/click-to-component.md +125 -0
- package/docs/clipboard.md +281 -0
- package/docs/components.md +1749 -0
- package/docs/dbus.md +277 -0
- package/docs/debugging.md +464 -0
- package/docs/desktop-calendar.md +284 -0
- package/docs/desktop.md +648 -0
- package/docs/devtools.md +250 -0
- package/docs/drag-and-drop.md +663 -0
- package/docs/ecosystem/animation.md +394 -0
- package/docs/ecosystem/data-fetching.md +201 -0
- package/docs/ecosystem/dev-tooling.md +436 -0
- package/docs/ecosystem/forms.md +313 -0
- package/docs/ecosystem/headless.md +219 -0
- package/docs/ecosystem/i18n.md +89 -0
- package/docs/ecosystem/icons.md +410 -0
- package/docs/ecosystem/layout.md +216 -0
- package/docs/ecosystem/routing.md +146 -0
- package/docs/ecosystem/state.md +361 -0
- package/docs/ecosystem/testing.md +500 -0
- package/docs/ecosystem/theming.md +540 -0
- package/docs/ecosystem.md +473 -0
- package/docs/elements.md +2762 -0
- package/docs/embedding.md +288 -0
- package/docs/events.md +1025 -0
- package/docs/extending.md +2242 -0
- package/docs/eyedropper.md +223 -0
- package/docs/filedialog.md +258 -0
- package/docs/frame.md +580 -0
- package/docs/gl.md +170 -0
- package/docs/globalmenu.md +278 -0
- package/docs/glx.md +137 -0
- package/docs/macos.md +3295 -0
- package/docs/notifications.md +164 -0
- package/docs/packaging.md +611 -0
- package/docs/permissions.md +148 -0
- package/docs/react-features.md +430 -0
- package/docs/remote.md +223 -0
- package/docs/scale.md +339 -0
- package/docs/security.md +132 -0
- package/docs/styling.md +1928 -0
- package/docs/system.md +599 -0
- package/docs/testing.md +514 -0
- package/docs/typescript.md +117 -0
- package/docs/uri-schemes.md +380 -0
- package/docs/wayland-backend.md +591 -0
- package/docs/wayland.md +545 -0
- package/docs/windows-embedding.md +271 -0
- package/docs/windows-gl.md +221 -0
- package/docs/windows-integrations.md +233 -0
- package/docs/windows.md +1404 -0
- package/package.json +10 -3
- package/src/bus.js +39 -26
- package/src/cli/create.d.ts +43 -0
- package/src/cli/create.js +305 -0
- package/src/cli/env.js +484 -0
- package/src/cli/index.js +29 -2
- package/src/cli/templates/AGENTS.template.md +195 -0
- package/src/cli/templates/DESIGN.template.md +269 -0
- package/src/desktopintegration.js +2 -1
- package/src/desktopsettings.js +193 -24
- package/src/filedialog.js +3 -3
- package/src/keyboardstate.js +28 -0
- package/src/node.d.ts +10 -10
- package/src/portal.js +31 -0
- package/src/screencolor.js +5 -10
- package/src/types/system.d.ts +8 -6
- package/src/wayland/app.js +59 -0
- package/src/wayland/clipboard.js +14 -7
- package/src/wayland/connection.js +3 -0
- package/src/wayland/context2d.js +129 -10
- package/src/wayland/protocols/index.json +6 -0
- package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
- package/src/wayland/seat.js +28 -1
- package/src/wayland/swapchain.js +23 -0
- package/src/wayland/xkb.js +137 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
None of Testing Library works here, and no amount of shimming will change
|
|
4
|
+
that — the queries are built on a document, and there is no accessibility
|
|
5
|
+
tree behind react-x11 today. All five failure modes are recorded in the
|
|
6
|
+
[negative-results register](../ecosystem.md#render-time); do not spend an
|
|
7
|
+
afternoon on a jsdom shim, because it only makes the query fail differently.
|
|
8
|
+
|
|
9
|
+
What replaces it ships in the box: **[`react-x11/test`](../testing.md)** —
|
|
10
|
+
`renderX11`, `screen`, `userEvent`, `waitFor` and pixel assertions, over a
|
|
11
|
+
real X server running in your test process. Read that page first; everything
|
|
12
|
+
below is the hand-rolled version it replaces, kept because it is still the
|
|
13
|
+
right thing to reach for when you want the connection itself.
|
|
14
|
+
|
|
15
|
+
## The harness, by hand {#harness}
|
|
16
|
+
|
|
17
|
+
node-x11 ships a pure-JS X server that runs in your test process, so a
|
|
18
|
+
react-x11 test needs no `$DISPLAY`, no Xvfb, and no native dependency. This
|
|
19
|
+
is what `renderX11` does for you; here it is spelled out, for a test that
|
|
20
|
+
needs to hold the pieces directly.
|
|
21
|
+
|
|
22
|
+
```jsx
|
|
23
|
+
// helpers/harness.jsx
|
|
24
|
+
import React from 'react';
|
|
25
|
+
import { createRoot } from 'react-x11';
|
|
26
|
+
import xserver from 'x11/lib/xserver/index.js';
|
|
27
|
+
|
|
28
|
+
export async function createHeadlessRoot() {
|
|
29
|
+
const server = xserver.createServer({ width: 640, height: 480 });
|
|
30
|
+
const [serverEnd, clientEnd] = xserver.createStreamPair();
|
|
31
|
+
server.addClientStream(serverEnd);
|
|
32
|
+
// `stream` reaches a server that is not on the other end of $DISPLAY,
|
|
33
|
+
// which is the whole trick; the root owns the connection and closes it
|
|
34
|
+
// on unmount.
|
|
35
|
+
return await createRoot({ stream: clientEnd });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The render callback hands back the ntk window instance. `root.render`
|
|
39
|
+
* is synchronous, so this stays a plain helper — only obtaining the root
|
|
40
|
+
* is async, and that happens once. */
|
|
41
|
+
export const render = (root, element) =>
|
|
42
|
+
new Promise((resolve) => root.render(element, (wnd) => resolve(wnd)));
|
|
43
|
+
|
|
44
|
+
export const tick = () => new Promise((r) => setImmediate(r));
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```jsx
|
|
48
|
+
import test from 'node:test';
|
|
49
|
+
import assert from 'node:assert/strict';
|
|
50
|
+
import { createHeadlessRoot, render, tick } from './helpers/harness.jsx';
|
|
51
|
+
|
|
52
|
+
test('paints a box', async () => {
|
|
53
|
+
const root = await createHeadlessRoot();
|
|
54
|
+
const wnd = await render(
|
|
55
|
+
root,
|
|
56
|
+
<window width={320} height={240} title="probe">
|
|
57
|
+
<box style={{ flexGrow: 1, backgroundColor: '#204080' }} />
|
|
58
|
+
</window>,
|
|
59
|
+
);
|
|
60
|
+
await tick(); // layout runs on the frame clock, after the commit
|
|
61
|
+
|
|
62
|
+
const node = wnd._reactX11Node; // the root drawn node
|
|
63
|
+
assert.equal(node.children[0].kind, 'box');
|
|
64
|
+
assert.deepEqual(node.children[0].abs, {
|
|
65
|
+
x: 0,
|
|
66
|
+
y: 0,
|
|
67
|
+
width: 320,
|
|
68
|
+
height: 240,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await root.unmount();
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**The `tick()` is not optional.** `render`'s callback resolves at commit
|
|
76
|
+
time; layout runs a task later on the frame clock, so every `abs` rect is
|
|
77
|
+
`{x: 0, y: 0, width: 0, height: 0}` until you yield once. Assert on `kind`
|
|
78
|
+
and `props` immediately if you like, but never on geometry.
|
|
79
|
+
|
|
80
|
+
`wnd._reactX11Node` is the root of the drawn tree. Each node carries `kind`
|
|
81
|
+
(the element name), `props`, `children`, and `abs` — its laid-out rect. That
|
|
82
|
+
is the queryable surface: walk it. It is not a DOM and it does not pretend to
|
|
83
|
+
be one. `ownerDocument` and `getClientRects()` do exist on nodes, but only
|
|
84
|
+
because React DevTools' `measureHostInstance` dereferences them.
|
|
85
|
+
|
|
86
|
+
Text is a two-level structure: a `<text>` node holds `textchunk` children
|
|
87
|
+
whose `.text` is the string. A collector is three lines:
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
export function textOf(node) {
|
|
91
|
+
const out = [];
|
|
92
|
+
(function walk(n) {
|
|
93
|
+
if (n.kind === 'textchunk') out.push(n.text);
|
|
94
|
+
(n.children ?? []).forEach(walk);
|
|
95
|
+
})(node);
|
|
96
|
+
return out.join('');
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
If you want to run without any X server at all — faster, and enough for most
|
|
101
|
+
component tests — write a small mock ntk app that records canvas operations
|
|
102
|
+
instead of painting them, and hand it to `createRoot({ app })`. That is what
|
|
103
|
+
the renderer's unit tests do; there is no mock published in the package, so
|
|
104
|
+
it is yours to write and yours to keep minimal.
|
|
105
|
+
|
|
106
|
+
## Driving events {#driving-events}
|
|
107
|
+
|
|
108
|
+
`@testing-library/user-event` throws on `setup()` — see the
|
|
109
|
+
[register](../ecosystem.md#render-time). Emit the raw ntk event on the window
|
|
110
|
+
instead. react-x11 subscribes to the ntk window's emitter, so this exercises
|
|
111
|
+
the whole synthetic-event path: hit testing, capture, bubble, default
|
|
112
|
+
actions.
|
|
113
|
+
|
|
114
|
+
```jsx
|
|
115
|
+
test('a synthetic click reaches the handler', async () => {
|
|
116
|
+
const root = await createHeadlessRoot();
|
|
117
|
+
let clicks = 0;
|
|
118
|
+
const wnd = await render(
|
|
119
|
+
root,
|
|
120
|
+
<window width={200} height={100}>
|
|
121
|
+
<box style={{ flexGrow: 1 }} onClick={() => (clicks += 1)} />
|
|
122
|
+
</window>,
|
|
123
|
+
);
|
|
124
|
+
await tick();
|
|
125
|
+
|
|
126
|
+
wnd.emit('mousedown', { x: 20, y: 20, button: 1, buttons: 1 });
|
|
127
|
+
wnd.emit('mouseup', { x: 20, y: 20, button: 1, buttons: 0 });
|
|
128
|
+
await tick();
|
|
129
|
+
|
|
130
|
+
assert.equal(clicks, 1);
|
|
131
|
+
await root.unmount();
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Keyboard input is the same shape — `wnd.emit('keydown', { keycode, codepoint,
|
|
136
|
+
buttons })` — with the keysym registered in `app.X.keycode2keysyms` so the
|
|
137
|
+
renderer can resolve it. Note `button` is the X button number, so left is
|
|
138
|
+
`1`, not `0`.
|
|
139
|
+
|
|
140
|
+
## Runners {#runners}
|
|
141
|
+
|
|
142
|
+
**`node:test` (built-in) is the baseline.** react-x11's own suite uses it, it
|
|
143
|
+
needs no transform, and it handles the ESM graph without configuration. Run `.jsx` files through
|
|
144
|
+
`tsx`: `node --import tsx --test 'test/**/*.test.jsx'`.
|
|
145
|
+
|
|
146
|
+
### Vitest {#vitest}
|
|
147
|
+
|
|
148
|
+
**Out of the box.** vitest@4.1.10.
|
|
149
|
+
|
|
150
|
+
The familiar choice for anyone arriving from a web project: native ESM,
|
|
151
|
+
built-in JSX and TypeScript transform, watch mode, snapshots. A `.test.jsx`
|
|
152
|
+
file that mounts a tree, drives a prop update and asserts on both the node
|
|
153
|
+
tree and the paint-op log runs green with no flags.
|
|
154
|
+
|
|
155
|
+
The one setting that matters:
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
// vitest.config.mjs
|
|
159
|
+
import { defineConfig } from 'vitest/config';
|
|
160
|
+
|
|
161
|
+
export default defineConfig({
|
|
162
|
+
test: { environment: 'node' }, // the default; never 'jsdom'
|
|
163
|
+
esbuild: { jsx: 'automatic' },
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Never `environment: 'jsdom'`.** react-x11 needs no DOM, and a jsdom
|
|
168
|
+
environment _defines_ `window` and `document`, which silently flips
|
|
169
|
+
environment detection in third-party libraries — `@tanstack/query-core`
|
|
170
|
+
decides it is in a browser and changes retry and refetch behaviour, for one.
|
|
171
|
+
`'node'` is Vitest's default; just do not cargo-cult a web project's config
|
|
172
|
+
across.
|
|
173
|
+
|
|
174
|
+
- Vitest runs test files in worker threads by default. The in-process X
|
|
175
|
+
server works there, but `pool: 'forks'` is the escape hatch if long-lived
|
|
176
|
+
server streams misbehave.
|
|
177
|
+
- `vi.useFakeTimers()` shares the [sinon caveat](#sinon): pass an explicit
|
|
178
|
+
`toFake` list, or React's scheduling and the renderer's paint scheduling
|
|
179
|
+
freeze.
|
|
180
|
+
|
|
181
|
+
### Jest {#jest}
|
|
182
|
+
|
|
183
|
+
**Works, but only in real ESM mode.** jest@30.4.2.
|
|
184
|
+
|
|
185
|
+
Jest's default CJS path cannot work at all. `babel-jest` down-compiles to
|
|
186
|
+
CommonJS, and `react-x11/src/Reconciler.js` and `src/DevToolsIntegration.js`
|
|
187
|
+
both call `createRequire(import.meta.url)`, which has no CJS equivalent — so
|
|
188
|
+
following Jest's own suggestions leads to
|
|
189
|
+
`SyntaxError: Cannot use 'import.meta' outside a module`. Both dead ends are
|
|
190
|
+
in the [register](../ecosystem.md#install-and-import-time).
|
|
191
|
+
|
|
192
|
+
The configuration that does work:
|
|
193
|
+
|
|
194
|
+
```js
|
|
195
|
+
// jest.config.mjs — run with NODE_OPTIONS=--experimental-vm-modules
|
|
196
|
+
export default {
|
|
197
|
+
extensionsToTreatAsEsm: ['.jsx'],
|
|
198
|
+
transform: {
|
|
199
|
+
'\\.jsx$': [
|
|
200
|
+
'babel-jest',
|
|
201
|
+
{
|
|
202
|
+
presets: [
|
|
203
|
+
[
|
|
204
|
+
'@babel/preset-env',
|
|
205
|
+
{ modules: false, targets: { node: 'current' } },
|
|
206
|
+
],
|
|
207
|
+
['@babel/preset-react', { runtime: 'automatic' }],
|
|
208
|
+
],
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`modules: false` is the load-bearing part: stop transforming to CJS and Jest
|
|
216
|
+
runs the same ESM graph everything else does. If you have the choice,
|
|
217
|
+
`node:test` or Vitest need none of this.
|
|
218
|
+
|
|
219
|
+
## msw {#msw}
|
|
220
|
+
|
|
221
|
+
**Out of the box — `msw/node`.** msw@2.15.0.
|
|
222
|
+
|
|
223
|
+
Request-level API mocking. In Node, `setupServer` patches the runtime's
|
|
224
|
+
request internals (`http`, `https`, `fetch`/undici, `XMLHttpRequest`) so
|
|
225
|
+
handlers written as `http.get(url, resolver)` intercept real requests — no
|
|
226
|
+
service worker, no DOM, despite the name.
|
|
227
|
+
|
|
228
|
+
A react-x11 app's data layer is ordinary Node networking, so msw slots in
|
|
229
|
+
with zero adapter: components genuinely call `fetch`, msw answers, state
|
|
230
|
+
updates, the renderer repaints.
|
|
231
|
+
|
|
232
|
+
```jsx
|
|
233
|
+
import { setupServer } from 'msw/node';
|
|
234
|
+
import { http, HttpResponse } from 'msw';
|
|
235
|
+
|
|
236
|
+
const server = setupServer(
|
|
237
|
+
http.get('https://api.example.test/user', () =>
|
|
238
|
+
HttpResponse.json({ name: 'Ada' }),
|
|
239
|
+
),
|
|
240
|
+
);
|
|
241
|
+
before(() => server.listen({ onUnhandledRequest: 'error' }));
|
|
242
|
+
after(() => server.close());
|
|
243
|
+
|
|
244
|
+
function User() {
|
|
245
|
+
const [name, setName] = React.useState('loading');
|
|
246
|
+
React.useEffect(() => {
|
|
247
|
+
let alive = true;
|
|
248
|
+
fetch('https://api.example.test/user')
|
|
249
|
+
.then((r) => r.json())
|
|
250
|
+
.then((d) => alive && setName(d.name));
|
|
251
|
+
return () => {
|
|
252
|
+
alive = false;
|
|
253
|
+
};
|
|
254
|
+
}, []);
|
|
255
|
+
return <text>{name}</text>;
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
- The fetch → `setState` → commit round trip is asynchronous, and a single
|
|
260
|
+
`setImmediate` tick is **not** enough. Poll the tree with a deadline.
|
|
261
|
+
- `server.listen()` patches process-wide networking. Call `server.close()` in
|
|
262
|
+
`after`, or the X server harness and msw can both be left holding process
|
|
263
|
+
state between files.
|
|
264
|
+
- Requests to the in-process X server are plain streams, not HTTP, so msw
|
|
265
|
+
never sees or interferes with the X11 connection.
|
|
266
|
+
- `onUnhandledRequest: 'error'` turns any unmocked request into a loud
|
|
267
|
+
failure, which is the right default for hermetic UI tests.
|
|
268
|
+
- nock also works. Prefer it only if you specifically need its scope-level
|
|
269
|
+
assertions (`scope.done()`) on legacy `http.request`-based clients.
|
|
270
|
+
|
|
271
|
+
## fast-check {#fast-check}
|
|
272
|
+
|
|
273
|
+
**Out of the box.** fast-check@4.9.0.
|
|
274
|
+
|
|
275
|
+
Property-based testing: describe generators, state a property, and
|
|
276
|
+
fast-check runs it against hundreds of random inputs, then _shrinks_ any
|
|
277
|
+
failure to a minimal counterexample and prints a replay seed.
|
|
278
|
+
|
|
279
|
+
This is a good fit because the renderer has oracles that cannot lie:
|
|
280
|
+
|
|
281
|
+
- **Convergence** — `mount(A)` then `update(A → B)` must produce the same
|
|
282
|
+
tree as a fresh `mount(B)`.
|
|
283
|
+
- **Damage soundness** — a partial repaint must equal a forced full repaint.
|
|
284
|
+
|
|
285
|
+
```jsx
|
|
286
|
+
import fc from 'fast-check';
|
|
287
|
+
|
|
288
|
+
const { node } = fc.letrec((tie) => ({
|
|
289
|
+
node: fc.oneof(
|
|
290
|
+
{ maxDepth: 3 },
|
|
291
|
+
fc.record({
|
|
292
|
+
kind: fc.constant('text'),
|
|
293
|
+
label: fc.constantFrom('a', 'hello x11'),
|
|
294
|
+
}),
|
|
295
|
+
fc.record({
|
|
296
|
+
kind: fc.constant('box'),
|
|
297
|
+
style: fc.record(
|
|
298
|
+
{
|
|
299
|
+
width: fc.integer({ min: 10, max: 120 }),
|
|
300
|
+
flexDirection: fc.constantFrom('row', 'column'),
|
|
301
|
+
},
|
|
302
|
+
{ requiredKeys: [] },
|
|
303
|
+
),
|
|
304
|
+
children: fc.array(tie('node'), { maxLength: 3 }),
|
|
305
|
+
}),
|
|
306
|
+
),
|
|
307
|
+
}));
|
|
308
|
+
|
|
309
|
+
const toEl = (s, key) =>
|
|
310
|
+
s.kind === 'text' ? (
|
|
311
|
+
<text key={key}>{s.label}</text>
|
|
312
|
+
) : (
|
|
313
|
+
<box key={key} style={s.style}>
|
|
314
|
+
{s.children.map(toEl)}
|
|
315
|
+
</box>
|
|
316
|
+
);
|
|
317
|
+
const win = (kids) => (
|
|
318
|
+
<window width={320} height={240}>
|
|
319
|
+
{kids.map(toEl)}
|
|
320
|
+
</window>
|
|
321
|
+
);
|
|
322
|
+
const snap = (n) => ({
|
|
323
|
+
kind: n.kind,
|
|
324
|
+
abs: { ...n.abs },
|
|
325
|
+
children: n.children.map(snap),
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
await fc.assert(
|
|
329
|
+
fc.asyncProperty(
|
|
330
|
+
fc.array(node, { maxLength: 4 }),
|
|
331
|
+
fc.array(node, { maxLength: 4 }),
|
|
332
|
+
async (a, b) => {
|
|
333
|
+
const root1 = await createHeadlessRoot();
|
|
334
|
+
const w1 = await render(root1, win(a));
|
|
335
|
+
await tick();
|
|
336
|
+
await render(root1, win(b)); // update path
|
|
337
|
+
await tick();
|
|
338
|
+
const root2 = await createHeadlessRoot();
|
|
339
|
+
const w2 = await render(root2, win(b)); // fresh-mount path
|
|
340
|
+
await tick();
|
|
341
|
+
assert.deepStrictEqual(snap(w1._reactX11Node), snap(w2._reactX11Node));
|
|
342
|
+
},
|
|
343
|
+
),
|
|
344
|
+
{ numRuns: 30 },
|
|
345
|
+
);
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
- Compare structure (`kind` plus `abs` rects), not paint-op logs: op logs
|
|
349
|
+
accumulate across paints on the update path, so they only match after
|
|
350
|
+
forcing a full repaint on both sides.
|
|
351
|
+
- Keep trees shallow. Each run is a real yoga layout pass, so budget runs
|
|
352
|
+
accordingly in CI.
|
|
353
|
+
- Include a mid-sequence unmount/remount and `key` permutations in generators
|
|
354
|
+
before trusting the property. Index keys exercise much less of
|
|
355
|
+
`insertBefore`/`removeChild` than keyed reorders do.
|
|
356
|
+
- Sixty lines of seeded PRNG would also do this job with no dependency. What
|
|
357
|
+
fast-check buys is shrinking and `{ seed, path }` replay, which is worth
|
|
358
|
+
one dev-dependency for a differential suite meant to run forever.
|
|
359
|
+
|
|
360
|
+
## sinon {#sinon}
|
|
361
|
+
|
|
362
|
+
**Out of the box, with one required restriction.** sinon@22.1.0.
|
|
363
|
+
|
|
364
|
+
Spies, stubs and `@sinonjs/fake-timers`. Fake timers work — a toast component
|
|
365
|
+
that auto-dismisses via `setTimeout` dismisses on `clock.tick(1000)` — but
|
|
366
|
+
the default configuration is a trap.
|
|
367
|
+
|
|
368
|
+
**Never fake `setImmediate` or `queueMicrotask`.** Sinon's default `toFake`
|
|
369
|
+
list includes them; React's scheduler drives work with them in Node, and the
|
|
370
|
+
renderer schedules paints via `setImmediate` when there is no frame clock.
|
|
371
|
+
Fake them and every render, effect flush and repaint freezes until a
|
|
372
|
+
`clock.tick`, which usually presents as a test that hangs at the first
|
|
373
|
+
`await`. Always pass an explicit list:
|
|
374
|
+
|
|
375
|
+
```js
|
|
376
|
+
const clock = sinon.useFakeTimers({
|
|
377
|
+
toFake: ['setTimeout', 'clearTimeout', 'Date'],
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Faking `Date` is more interesting than it looks: the renderer's animation
|
|
382
|
+
clock reads `Date.now()` at call time, so a fake `Date` does reach style
|
|
383
|
+
transitions.
|
|
384
|
+
|
|
385
|
+
```jsx
|
|
386
|
+
test('toast auto-dismisses', async () => {
|
|
387
|
+
const clock = sinon.useFakeTimers({
|
|
388
|
+
toFake: ['setTimeout', 'clearTimeout', 'Date'],
|
|
389
|
+
});
|
|
390
|
+
try {
|
|
391
|
+
const root = await createHeadlessRoot();
|
|
392
|
+
const wnd = await render(
|
|
393
|
+
root,
|
|
394
|
+
<window width={100} height={50}>
|
|
395
|
+
<Toast />
|
|
396
|
+
</window>,
|
|
397
|
+
);
|
|
398
|
+
await tick(); // mount + passive effect
|
|
399
|
+
clock.tick(1000); // fires the timeout synchronously
|
|
400
|
+
await tick();
|
|
401
|
+
await tick(); // setState -> commit -> repaint: two tasks
|
|
402
|
+
assert.strictEqual(wnd._reactX11Node.children.length, 0);
|
|
403
|
+
} finally {
|
|
404
|
+
clock.restore(); // always; a leaked fake Date poisons the file
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
- Two `setImmediate` ticks after `clock.tick`, not one — the timer callback's
|
|
410
|
+
`setState` commits on a later task than the tick itself.
|
|
411
|
+
- `clock.restore()` in `finally`, every time. A leaked fake `Date` breaks
|
|
412
|
+
every subsequent test's transitions and timestamps in the same process.
|
|
413
|
+
- The text-input caret blink uses `setInterval`; faking `setInterval` without
|
|
414
|
+
ticking leaves the caret timer pending. Mostly harmless, but it shows up in
|
|
415
|
+
`clock.countTimers()`.
|
|
416
|
+
- **Versus `node:test` mock timers**: both pass on the same component.
|
|
417
|
+
`t.mock.timers.enable({ apis: ['setTimeout'] })` plus
|
|
418
|
+
`t.mock.timers.tick(1000)` does the plain-timer case with zero dependencies
|
|
419
|
+
and auto-restores. Prefer it, and reach for sinon when you need fake `Date`
|
|
420
|
+
(the animation clock), `clock.countTimers()`, or spies and stubs.
|
|
421
|
+
|
|
422
|
+
## pixelmatch {#pixelmatch}
|
|
423
|
+
|
|
424
|
+
**Out of the box.** pixelmatch@7.2.0.
|
|
425
|
+
|
|
426
|
+
The standard perceptual image-diff primitive: two RGBA buffers plus an output
|
|
427
|
+
buffer in, changed-pixel count and a painted diff image out. Anti-aliasing
|
|
428
|
+
aware, zero dependencies.
|
|
429
|
+
|
|
430
|
+
react-x11 can screenshot itself with no display — render through the harness
|
|
431
|
+
above and read pixels back with `getImageData`. `jest-image-snapshot` cannot
|
|
432
|
+
do this for you (it wants a Buffer and receives a window object; see the
|
|
433
|
+
[register](../ecosystem.md#render-time)), but the pipeline underneath it is
|
|
434
|
+
four lines.
|
|
435
|
+
|
|
436
|
+
Two impedance notes at the seam. `ctx.getImageData(x, y, w, h)` resolves to a
|
|
437
|
+
canvas `ImageData`, and pixelmatch wants the bytes, not the object: hand it
|
|
438
|
+
`.data`, which is already straight RGBA in a `Uint8ClampedArray`, opaque
|
|
439
|
+
pixels at alpha 255, so there is nothing to swap or fix up. And pixel
|
|
440
|
+
readback races the paint: poll a known pixel before taking the comparison
|
|
441
|
+
frame.
|
|
442
|
+
|
|
443
|
+
```jsx
|
|
444
|
+
import fs from 'node:fs';
|
|
445
|
+
import pixelmatch from 'pixelmatch';
|
|
446
|
+
import { PNG } from 'pngjs';
|
|
447
|
+
|
|
448
|
+
const ctx = wnd.getContext('2d');
|
|
449
|
+
const diff = new PNG({ width: 320, height: 240 });
|
|
450
|
+
|
|
451
|
+
const before = (await ctx.getImageData(0, 0, 320, 240)).data;
|
|
452
|
+
// ...re-render with the changed prop, wait for the repaint...
|
|
453
|
+
const after = (await ctx.getImageData(0, 0, 320, 240)).data;
|
|
454
|
+
const changed = pixelmatch(before, after, diff.data, 320, 240, {
|
|
455
|
+
threshold: 0.1,
|
|
456
|
+
});
|
|
457
|
+
if (changed > 0) fs.writeFileSync('diff.png', PNG.sync.write(diff));
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
- **Fonts are the determinism problem.** Glyph indices travel on the wire, so
|
|
461
|
+
pin every family with ntk's `StaticFontSource` and never let a snapshot
|
|
462
|
+
script fall through to `FontconfigFontSource`. Pinned, two renders of the
|
|
463
|
+
same tree diff to exactly zero pixels; unpinned, the diff is noise.
|
|
464
|
+
- `threshold` is a per-pixel colour distance, not a pass/fail budget. Gate on
|
|
465
|
+
the returned count.
|
|
466
|
+
- Buffers must have identical dimensions; pixelmatch throws otherwise. A
|
|
467
|
+
window resize between shots needs a re-read, not a crop.
|
|
468
|
+
- fontkit version bumps change hinting and advance rounding. Treat pixel
|
|
469
|
+
diffs as tolerance-based regression testing, and use tree or op-log
|
|
470
|
+
assertions for exact CI gating.
|
|
471
|
+
- Against a _real_ X server under Xvfb, expect nonzero noise from the
|
|
472
|
+
server's own rendering. The zero-noise floor is specific to the in-process
|
|
473
|
+
JS server.
|
|
474
|
+
|
|
475
|
+
## react-test-renderer {#react-test-renderer}
|
|
476
|
+
|
|
477
|
+
**Works, but do not build on it.** react-test-renderer@19.2.8.
|
|
478
|
+
|
|
479
|
+
React's own headless renderer treats _any_ string element type as a host
|
|
480
|
+
element, so react-x11 vocabulary passes through untouched and coarse JSON
|
|
481
|
+
snapshots work today. Two things keep it from being a recommendation:
|
|
482
|
+
|
|
483
|
+
1. **It is deprecated.** Importing it prints
|
|
484
|
+
`react-test-renderer is deprecated`. It still ships and works in 19.2.x,
|
|
485
|
+
but the React team explicitly discourages new usage.
|
|
486
|
+
2. **It bypasses the actual renderer.** No prop validation, no style
|
|
487
|
+
resolution, no yoga layout, no paint. A tree that snapshots fine under
|
|
488
|
+
react-test-renderer can still throw
|
|
489
|
+
`react-x11: <box width=…> is a style property` or lay out wrong under
|
|
490
|
+
react-x11.
|
|
491
|
+
|
|
492
|
+
The harness above is barely more code and exercises the real
|
|
493
|
+
`createInstance`/`commitUpdate` path, with layout, for the same "no X server"
|
|
494
|
+
cost. Acceptable as a bridge for teams porting an existing
|
|
495
|
+
react-test-renderer suite; wrong foundation for new react-x11 tests.
|
|
496
|
+
|
|
497
|
+
- Set `globalThis.IS_REACT_ACT_ENVIRONMENT = true` in test setup or every
|
|
498
|
+
`act` call warns. Harmless, but noisy.
|
|
499
|
+
- `toJSON()` records the raw props object with none of react-x11's
|
|
500
|
+
resolution, so snapshots are of _input_, not of what would render.
|