react-x11 2.52.0 → 2.53.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 (112) hide show
  1. package/README.md +10 -10
  2. package/docs/README.md +160 -314
  3. package/docs/accessibility.md +176 -229
  4. package/docs/appearance.md +77 -176
  5. package/docs/click-to-component.md +18 -51
  6. package/docs/clipboard.md +40 -59
  7. package/docs/components.md +518 -1036
  8. package/docs/dbus.md +61 -133
  9. package/docs/debugging.md +152 -245
  10. package/docs/desktop-calendar.md +51 -88
  11. package/docs/desktop.md +222 -347
  12. package/docs/devtools.md +50 -167
  13. package/docs/drag-and-drop.md +126 -204
  14. package/docs/ecosystem/animation.md +91 -125
  15. package/docs/ecosystem/data-fetching.md +38 -53
  16. package/docs/ecosystem/dev-tooling.md +159 -202
  17. package/docs/ecosystem/forms.md +61 -97
  18. package/docs/ecosystem/headless.md +64 -89
  19. package/docs/ecosystem/i18n.md +18 -21
  20. package/docs/ecosystem/icons.md +93 -126
  21. package/docs/ecosystem/layout.md +42 -52
  22. package/docs/ecosystem/routing.md +18 -29
  23. package/docs/ecosystem/state.md +71 -88
  24. package/docs/ecosystem/testing.md +131 -157
  25. package/docs/ecosystem/theming.md +119 -165
  26. package/docs/ecosystem.md +145 -179
  27. package/docs/elements.md +1091 -1774
  28. package/docs/embedding.md +83 -132
  29. package/docs/events.md +412 -695
  30. package/docs/extending.md +784 -1326
  31. package/docs/eyedropper.md +63 -122
  32. package/docs/filedialog.md +71 -107
  33. package/docs/frame.md +195 -339
  34. package/docs/gl.md +65 -62
  35. package/docs/globalmenu.md +63 -146
  36. package/docs/macos.md +531 -3234
  37. package/docs/notifications.md +33 -47
  38. package/docs/packaging.md +166 -309
  39. package/docs/permissions.md +9 -14
  40. package/docs/react-features.md +81 -106
  41. package/docs/remote.md +32 -76
  42. package/docs/scale.md +72 -186
  43. package/docs/styling.md +329 -547
  44. package/docs/system.md +200 -280
  45. package/docs/testing.md +63 -83
  46. package/docs/typescript.md +6 -48
  47. package/docs/uri-schemes.md +67 -107
  48. package/docs/wayland-backend.md +200 -459
  49. package/docs/windows-integrations.md +75 -131
  50. package/docs/windows.md +343 -1367
  51. package/package.json +3 -2
  52. package/src/Reconciler.js +2 -2
  53. package/src/a11y.js +1 -1
  54. package/src/backend/context2d.js +3 -3
  55. package/src/bootstrap.js +1 -1
  56. package/src/cocoa/app.js +6 -6
  57. package/src/cocoa/bezels.js +1 -1
  58. package/src/cocoa/fonts.js +1 -1
  59. package/src/cocoa/globalmenu.js +1 -1
  60. package/src/cocoa/keymap.js +1 -1
  61. package/src/cocoa/panewindow.js +1 -1
  62. package/src/cocoa/player.js +1 -1
  63. package/src/cocoa/presenter.js +2 -2
  64. package/src/cocoa/promotion.js +1 -1
  65. package/src/cocoa/relaunch.js +1 -1
  66. package/src/cocoa/threaded.js +1 -1
  67. package/src/cocoa/video.js +1 -1
  68. package/src/cocoa/window.js +4 -4
  69. package/src/components/Menu.js +7 -2
  70. package/src/components/Switch.js +1 -1
  71. package/src/components/native.js +1 -1
  72. package/src/dnd.js +2 -2
  73. package/src/glnodes.js +1 -1
  74. package/src/grid.js +1 -1
  75. package/src/layouts.js +1 -1
  76. package/src/nodes/animation.js +1 -1
  77. package/src/nodes/invalidate.js +1 -1
  78. package/src/nodes/layouthost.js +1 -1
  79. package/src/nodes/video.js +19 -8
  80. package/src/nodes/window/droptarget.js +1 -1
  81. package/src/pacing.js +1 -1
  82. package/src/refresh/index.js +2 -1
  83. package/src/types/video.d.ts +5 -2
  84. package/src/videobackend.js +28 -0
  85. package/src/videoframes.js +29 -16
  86. package/src/wayland/app.js +27 -0
  87. package/src/wayland/video.js +376 -0
  88. package/src/win32/a11y.js +1 -1
  89. package/src/win32/app.js +2 -2
  90. package/src/win32/fonts.js +1 -1
  91. package/src/win32/glarea.js +4 -4
  92. package/src/win32/overlay.js +1 -1
  93. package/src/win32/panewindow.js +3 -3
  94. package/src/win32/shell.js +1 -1
  95. package/src/win32/window.js +2 -2
  96. package/src/windowid.js +1 -1
  97. package/src/xvideo.js +189 -0
  98. package/docs/architecture/accessibility.md +0 -470
  99. package/docs/architecture/animation.md +0 -707
  100. package/docs/architecture/container-queries.md +0 -525
  101. package/docs/architecture/custom-layout.md +0 -334
  102. package/docs/architecture/drag-and-drop.md +0 -941
  103. package/docs/architecture/element-layer-contents.md +0 -622
  104. package/docs/architecture/frame-pacing.md +0 -255
  105. package/docs/architecture/grid-layout.md +0 -358
  106. package/docs/architecture/protocol-efficiency.md +0 -357
  107. package/docs/architecture/video.md +0 -1149
  108. package/docs/architecture/windowed-regions.md +0 -854
  109. package/docs/glx.md +0 -137
  110. package/docs/wayland.md +0 -545
  111. package/docs/windows-embedding.md +0 -271
  112. package/docs/windows-gl.md +0 -221
@@ -1,23 +1,21 @@
1
1
  # Testing
2
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.
3
+ Testing Library does not work here and no shim will fix it: its queries are
4
+ built on a DOM document. Its five failure modes are in the
5
+ [negative-results register](../ecosystem.md#render-time); a jsdom shim only
6
+ makes the query fail differently.
8
7
 
9
- What replaces it ships in the box: **[`react-x11/test`](../testing.md)** —
8
+ The replacement ships in the box: **[`react-x11/test`](../testing.md)** —
10
9
  `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.
10
+ real X server running in your test process. Read that page first. Below is
11
+ the hand-rolled version, for a test that wants the connection itself, and
12
+ the third-party runners and tools that work alongside it.
14
13
 
15
14
  ## The harness, by hand {#harness}
16
15
 
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.
16
+ node-x11 ships a pure-JS X server that runs in your test process, so a test
17
+ needs no `$DISPLAY`, no Xvfb, and no native dependency. This is what
18
+ `renderX11` does, spelled out for a test that holds the pieces directly.
21
19
 
22
20
  ```jsx
23
21
  // helpers/harness.jsx
@@ -72,19 +70,17 @@ test('paints a box', async () => {
72
70
  });
73
71
  ```
74
72
 
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
73
+ **The `tick()` is not optional.** `render`'s callback resolves at commit;
74
+ layout runs a task later on the frame clock, so every `abs` rect is
77
75
  `{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.
76
+ and `props` immediately, never on geometry.
79
77
 
80
78
  `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
79
+ (the element name), `props`, `children`, and `abs` (its laid-out rect):
80
+ walk it. It is not a DOM; `ownerDocument` and `getClientRects()` exist only
84
81
  because React DevTools' `measureHostInstance` dereferences them.
85
82
 
86
- Text is a two-level structure: a `<text>` node holds `textchunk` children
87
- whose `.text` is the string. A collector is three lines:
83
+ A `<text>` node holds `textchunk` children whose `.text` is the string:
88
84
 
89
85
  ```js
90
86
  export function textOf(node) {
@@ -97,19 +93,20 @@ export function textOf(node) {
97
93
  }
98
94
  ```
99
95
 
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.
96
+ To run with no X server at all — faster, and enough for most component
97
+ tests — pass `createRoot({ app })` a mock ntk app that records canvas
98
+ operations instead of painting them. `react-x11/test` exports the one the
99
+ renderer's unit tests use, `createMockApp()`, and
100
+ `renderX11(el, { backend: 'mock' })` builds it for you
101
+ ([mock backend](../testing.md#the-mock-backend)).
105
102
 
106
103
  ## Driving events {#driving-events}
107
104
 
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.
105
+ `@testing-library/user-event` throws on `setup()`
106
+ ([register](../ecosystem.md#render-time)); use `react-x11/test`'s
107
+ `userEvent`, or emit the raw ntk event on the window. react-x11 subscribes
108
+ to the ntk window's emitter, so this exercises the whole synthetic-event
109
+ path: hit testing, capture, bubble, default actions.
113
110
 
114
111
  ```jsx
115
112
  test('a synthetic click reaches the handler', async () => {
@@ -132,27 +129,25 @@ test('a synthetic click reaches the handler', async () => {
132
129
  });
133
130
  ```
134
131
 
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`.
132
+ Keyboard input is the same shape — `wnd.emit('keydown', { keycode,
133
+ codepoint, buttons })` — with the keysym registered in
134
+ `app.X.keycode2keysyms` so the renderer can resolve it. `button` is the X
135
+ button number: left is `1`, not `0`.
139
136
 
140
137
  ## Runners {#runners}
141
138
 
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
139
+ **`node:test` (built-in) is the baseline.** react-x11's own suite uses it,
140
+ and it handles the ESM graph without configuration. Run `.jsx` files through
144
141
  `tsx`: `node --import tsx --test 'test/**/*.test.jsx'`.
145
142
 
146
143
  ### Vitest {#vitest}
147
144
 
148
145
  **Out of the box.** vitest@4.1.10.
149
146
 
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:
147
+ Native ESM, built-in JSX and TypeScript transform, watch mode, snapshots. A
148
+ `.test.jsx` that mounts a tree, drives a prop update and asserts on the node
149
+ tree and the paint-op log runs green with no flags. The one setting that
150
+ matters:
156
151
 
157
152
  ```js
158
153
  // vitest.config.mjs
@@ -164,15 +159,14 @@ export default defineConfig({
164
159
  });
165
160
  ```
166
161
 
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.
162
+ **Never `environment: 'jsdom'`.** It _defines_ `window` and `document`,
163
+ which silently flips environment detection in third-party libraries —
164
+ `@tanstack/query-core` decides it is in a browser and changes retry and
165
+ refetch behaviour, for one. `'node'` is the default; do not copy a web
166
+ project's config across.
173
167
 
174
168
  - 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
169
+ server works there; `pool: 'forks'` is the escape hatch if long-lived
176
170
  server streams misbehave.
177
171
  - `vi.useFakeTimers()` shares the [sinon caveat](#sinon): pass an explicit
178
172
  `toFake` list, or React's scheduling and the renderer's paint scheduling
@@ -182,14 +176,12 @@ across.
182
176
 
183
177
  **Works, but only in real ESM mode.** jest@30.4.2.
184
178
 
185
- Jest's default CJS path cannot work at all. `babel-jest` down-compiles to
179
+ Jest's default CJS path cannot work: `babel-jest` down-compiles to
186
180
  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
181
+ call `createRequire(import.meta.url)`, which has no CJS equivalent, so Jest's
182
+ own suggestions lead to
189
183
  `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:
184
+ in the [register](../ecosystem.md#install-and-import-time). What works:
193
185
 
194
186
  ```js
195
187
  // jest.config.mjs — run with NODE_OPTIONS=--experimental-vm-modules
@@ -212,22 +204,19 @@ export default {
212
204
  };
213
205
  ```
214
206
 
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.
207
+ `modules: false` is the load-bearing part: with no CJS transform, Jest runs
208
+ the same ESM graph as everything else. `node:test` and Vitest need none of
209
+ this.
218
210
 
219
211
  ## msw {#msw}
220
212
 
221
213
  **Out of the box — `msw/node`.** msw@2.15.0.
222
214
 
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.
215
+ Request-level API mocking. In Node, `setupServer` patches `http`, `https`,
216
+ `fetch`/undici and `XMLHttpRequest`, so `http.get(url, resolver)` handlers
217
+ intercept real requests — no service worker, no DOM. A react-x11 app's data
218
+ layer is ordinary Node networking: components call `fetch`, msw answers, the
219
+ renderer repaints.
231
220
 
232
221
  ```jsx
233
222
  import { setupServer } from 'msw/node';
@@ -256,27 +245,24 @@ function User() {
256
245
  }
257
246
  ```
258
247
 
259
- - The fetch → `setState` → commit round trip is asynchronous, and a single
248
+ - The fetch → `setState` → commit round trip is asynchronous, and one
260
249
  `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.
250
+ - `server.listen()` patches process-wide networking. Call `server.close()`
251
+ in `after`, or the harness and msw can leave process state between files.
252
+ - The in-process X server connection is a plain stream, not HTTP; msw never
253
+ touches it.
254
+ - `onUnhandledRequest: 'error'` makes any unmocked request a loud failure —
255
+ the right default for hermetic UI tests.
256
+ - nock also works; prefer it only for its scope-level assertions
257
+ (`scope.done()`) on legacy `http.request`-based clients.
270
258
 
271
259
  ## fast-check {#fast-check}
272
260
 
273
261
  **Out of the box.** fast-check@4.9.0.
274
262
 
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:
263
+ Property-based testing: generators plus a property, run against hundreds
264
+ of random inputs, with any failure _shrunk_ to a minimal counterexample and
265
+ a replay seed. The renderer has two oracles that cannot lie:
280
266
 
281
267
  - **Convergence** — `mount(A)` then `update(A → B)` must produce the same
282
268
  tree as a fresh `mount(B)`.
@@ -346,31 +332,28 @@ await fc.assert(
346
332
  ```
347
333
 
348
334
  - 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.
335
+ accumulate across paints on the update path, so they only match after a
336
+ forced full repaint on both sides.
337
+ - Keep trees shallow; each run is a real yoga layout pass.
338
+ - Include a mid-sequence unmount/remount and `key` permutations in the
339
+ generators; index keys exercise much less of `insertBefore`/`removeChild`
340
+ than keyed reorders.
341
+ - A seeded PRNG would also do with no dependency; fast-check buys shrinking
342
+ and `{ seed, path }` replay.
359
343
 
360
344
  ## sinon {#sinon}
361
345
 
362
346
  **Out of the box, with one required restriction.** sinon@22.1.0.
363
347
 
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.
348
+ Spies, stubs and `@sinonjs/fake-timers`. Fake timers work — a toast that
349
+ auto-dismisses via `setTimeout` dismisses on `clock.tick(1000)` — but the
350
+ default configuration is a trap.
367
351
 
368
352
  **Never fake `setImmediate` or `queueMicrotask`.** Sinon's default `toFake`
369
353
  list includes them; React's scheduler drives work with them in Node, and the
370
354
  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:
355
+ Faked, every render, effect flush and repaint freezes until a `clock.tick` —
356
+ usually a test that hangs at the first `await`. Pass an explicit list:
374
357
 
375
358
  ```js
376
359
  const clock = sinon.useFakeTimers({
@@ -378,9 +361,8 @@ const clock = sinon.useFakeTimers({
378
361
  });
379
362
  ```
380
363
 
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.
364
+ The renderer's animation clock reads `Date.now()` at call time, so a fake
365
+ `Date` reaches style transitions.
384
366
 
385
367
  ```jsx
386
368
  test('toast auto-dismisses', async () => {
@@ -406,39 +388,34 @@ test('toast auto-dismisses', async () => {
406
388
  });
407
389
  ```
408
390
 
409
- - Two `setImmediate` ticks after `clock.tick`, not one — the timer callback's
410
- `setState` commits on a later task than the tick itself.
391
+ - Two `setImmediate` ticks after `clock.tick`, not one — the timer
392
+ callback's `setState` commits a task later.
411
393
  - `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
394
+ later tests' transitions and timestamps in the same process.
395
+ - The text-input caret blink uses `setInterval`; faking it without ticking
396
+ leaves the caret timer pending — harmless, but it shows in
415
397
  `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.
398
+ - **Versus `node:test` mock timers**: `t.mock.timers.enable({ apis:
399
+ ['setTimeout'] })` plus `t.mock.timers.tick(1000)` handles the plain-timer
400
+ case with no dependency and auto-restores. Prefer it; use sinon for fake
401
+ `Date` (the animation clock), `clock.countTimers()`, or spies and stubs.
421
402
 
422
403
  ## pixelmatch {#pixelmatch}
423
404
 
424
405
  **Out of the box.** pixelmatch@7.2.0.
425
406
 
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.
407
+ Perceptual image diff: two RGBA buffers plus an output buffer in,
408
+ changed-pixel count and a painted diff image out. Anti-aliasing aware, zero
409
+ dependencies. Render through the harness and read pixels back with
410
+ `getImageData` — no display needed. `jest-image-snapshot` cannot do this (it
411
+ wants a Buffer and receives a window object;
412
+ [register](../ecosystem.md#render-time)), but the pipeline under it is four
413
+ lines.
435
414
 
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.
415
+ `ctx.getImageData(x, y, w, h)` resolves to a canvas `ImageData`; hand
416
+ pixelmatch its `.data`, already straight RGBA in a `Uint8ClampedArray` with
417
+ opaque pixels at alpha 255. Readback races the paint: poll a known pixel
418
+ before taking the comparison frame.
442
419
 
443
420
  ```jsx
444
421
  import fs from 'node:fs';
@@ -457,44 +434,41 @@ const changed = pixelmatch(before, after, diff.data, 320, 240, {
457
434
  if (changed > 0) fs.writeFileSync('diff.png', PNG.sync.write(diff));
458
435
  ```
459
436
 
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.
437
+ - **Fonts are the determinism problem.** Glyph indices travel on the wire,
438
+ so pin every family with ntk's `StaticFontSource`; never let a snapshot script fall through to
439
+ `FontconfigFontSource`. Pinned, two renders of the same tree diff to zero
440
+ pixels; unpinned, the diff is noise.
441
+ - `threshold` is a per-pixel colour distance, not a pass/fail budget; gate
442
+ on the returned count.
443
+ - Buffers must have identical dimensions, or pixelmatch throws. After a
444
+ window resize, re-read rather than crop.
468
445
  - 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.
446
+ diffs as tolerance-based regression tests; gate CI exactly on tree or
447
+ op-log assertions.
448
+ - Under Xvfb, expect nonzero noise from the server's own rendering; the
449
+ zero-noise floor is specific to the in-process JS server.
474
450
 
475
451
  ## react-test-renderer {#react-test-renderer}
476
452
 
477
453
  **Works, but do not build on it.** react-test-renderer@19.2.8.
478
454
 
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:
455
+ React's headless renderer treats _any_ string element type as a host
456
+ element, so react-x11 vocabulary passes through and coarse JSON snapshots
457
+ work. But:
482
458
 
483
459
  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.
460
+ `react-test-renderer is deprecated`; it still ships in 19.2.x, but React
461
+ discourages new usage.
462
+ 2. **It bypasses the actual renderer.** No prop validation, style
463
+ resolution, yoga layout or paint, so a tree that snapshots fine can still
464
+ throw `react-x11: <box width=…> is a style property` or lay out wrong
465
+ under react-x11.
491
466
 
492
467
  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.
468
+ `createInstance`/`commitUpdate` path with layout. Use react-test-renderer
469
+ only as a bridge when porting an existing suite.
470
+
471
+ - Set `globalThis.IS_REACT_ACT_ENVIRONMENT = true` in test setup, or every
472
+ `act` call warns.
473
+ - `toJSON()` records the raw props, so snapshots are of _input_, not of what
474
+ would render.