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.
Files changed (91) hide show
  1. package/README.md +87 -54
  2. package/docs/README.md +410 -0
  3. package/docs/accessibility.md +628 -0
  4. package/docs/appearance.md +416 -0
  5. package/docs/architecture/accessibility.md +470 -0
  6. package/docs/architecture/animation.md +707 -0
  7. package/docs/architecture/container-queries.md +525 -0
  8. package/docs/architecture/custom-layout.md +334 -0
  9. package/docs/architecture/drag-and-drop.md +941 -0
  10. package/docs/architecture/element-layer-contents.md +622 -0
  11. package/docs/architecture/frame-pacing.md +255 -0
  12. package/docs/architecture/grid-layout.md +358 -0
  13. package/docs/architecture/protocol-efficiency.md +357 -0
  14. package/docs/architecture/video.md +1149 -0
  15. package/docs/architecture/windowed-regions.md +854 -0
  16. package/docs/click-to-component.md +125 -0
  17. package/docs/clipboard.md +281 -0
  18. package/docs/components.md +1749 -0
  19. package/docs/dbus.md +277 -0
  20. package/docs/debugging.md +464 -0
  21. package/docs/desktop-calendar.md +284 -0
  22. package/docs/desktop.md +648 -0
  23. package/docs/devtools.md +250 -0
  24. package/docs/drag-and-drop.md +663 -0
  25. package/docs/ecosystem/animation.md +394 -0
  26. package/docs/ecosystem/data-fetching.md +201 -0
  27. package/docs/ecosystem/dev-tooling.md +436 -0
  28. package/docs/ecosystem/forms.md +313 -0
  29. package/docs/ecosystem/headless.md +219 -0
  30. package/docs/ecosystem/i18n.md +89 -0
  31. package/docs/ecosystem/icons.md +410 -0
  32. package/docs/ecosystem/layout.md +216 -0
  33. package/docs/ecosystem/routing.md +146 -0
  34. package/docs/ecosystem/state.md +361 -0
  35. package/docs/ecosystem/testing.md +500 -0
  36. package/docs/ecosystem/theming.md +540 -0
  37. package/docs/ecosystem.md +473 -0
  38. package/docs/elements.md +2762 -0
  39. package/docs/embedding.md +288 -0
  40. package/docs/events.md +1025 -0
  41. package/docs/extending.md +2242 -0
  42. package/docs/eyedropper.md +223 -0
  43. package/docs/filedialog.md +258 -0
  44. package/docs/frame.md +580 -0
  45. package/docs/gl.md +170 -0
  46. package/docs/globalmenu.md +278 -0
  47. package/docs/glx.md +137 -0
  48. package/docs/macos.md +3295 -0
  49. package/docs/notifications.md +164 -0
  50. package/docs/packaging.md +611 -0
  51. package/docs/permissions.md +148 -0
  52. package/docs/react-features.md +430 -0
  53. package/docs/remote.md +223 -0
  54. package/docs/scale.md +339 -0
  55. package/docs/security.md +132 -0
  56. package/docs/styling.md +1928 -0
  57. package/docs/system.md +599 -0
  58. package/docs/testing.md +514 -0
  59. package/docs/typescript.md +117 -0
  60. package/docs/uri-schemes.md +380 -0
  61. package/docs/wayland-backend.md +591 -0
  62. package/docs/wayland.md +545 -0
  63. package/docs/windows-embedding.md +271 -0
  64. package/docs/windows-gl.md +221 -0
  65. package/docs/windows-integrations.md +233 -0
  66. package/docs/windows.md +1404 -0
  67. package/package.json +10 -3
  68. package/src/bus.js +39 -26
  69. package/src/cli/create.d.ts +43 -0
  70. package/src/cli/create.js +305 -0
  71. package/src/cli/env.js +484 -0
  72. package/src/cli/index.js +29 -2
  73. package/src/cli/templates/AGENTS.template.md +195 -0
  74. package/src/cli/templates/DESIGN.template.md +269 -0
  75. package/src/desktopintegration.js +2 -1
  76. package/src/desktopsettings.js +193 -24
  77. package/src/filedialog.js +3 -3
  78. package/src/keyboardstate.js +28 -0
  79. package/src/node.d.ts +10 -10
  80. package/src/portal.js +31 -0
  81. package/src/screencolor.js +5 -10
  82. package/src/types/system.d.ts +8 -6
  83. package/src/wayland/app.js +59 -0
  84. package/src/wayland/clipboard.js +14 -7
  85. package/src/wayland/connection.js +3 -0
  86. package/src/wayland/context2d.js +129 -10
  87. package/src/wayland/protocols/index.json +6 -0
  88. package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
  89. package/src/wayland/seat.js +28 -1
  90. package/src/wayland/swapchain.js +23 -0
  91. 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.