react-x11 2.51.1 → 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 +9 -2
  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,514 @@
1
+ # Testing
2
+
3
+ `react-x11/test` renders your app through a **real X11 protocol connection
4
+ to a real X server, in this process** — no `$DISPLAY`, no Xvfb, no native
5
+ dependency, works on macOS. So a test can read pixels back with `GetImage`
6
+ and inject input through the server's own grab and focus machinery, which
7
+ means a passing test has exercised the path a user actually takes.
8
+
9
+ That server is node-x11's `lib/xserver`, written in JavaScript. It arrives
10
+ with ntk, so there is nothing new to install.
11
+
12
+ **The harness drives the X11 backend**, on every platform including macOS —
13
+ there is no `'cocoa'` backend for `renderX11()`, and `createRoot()`'s own
14
+ choice does not apply here because the harness always passes it an `app`.
15
+ That is deliberate rather than pending: the point of this harness is a real
16
+ protocol connection to a real server in-process, and macOS has no protocol
17
+ to connect to. What it means in practice is that an app targeting both
18
+ backends tests its **tree, its queries and its behaviour** here — all of
19
+ which are shared — while anything that is genuinely per-backend (a native
20
+ bezel's pixels, an `NSMenu`, a pasteboard flavour) is covered by the
21
+ renderer's own Cocoa tests against a fake bridge, and by looking at a Mac.
22
+
23
+ ```js
24
+ import { test, afterEach } from 'node:test';
25
+ import assert from 'node:assert/strict';
26
+ import {
27
+ renderX11,
28
+ cleanup,
29
+ screen,
30
+ userEvent,
31
+ expectPixel,
32
+ } from 'react-x11/test';
33
+
34
+ afterEach(cleanup);
35
+
36
+ test('adding a task', async () => {
37
+ const { ctx } = await renderX11(<Tasks />, {
38
+ fonts: { 'sans-serif': '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf' },
39
+ });
40
+
41
+ await userEvent.type(screen.getByRole('textbox'), 'buy milk\n');
42
+
43
+ screen.getByText('buy milk');
44
+ await expectPixel(ctx, 12, 40, '#2980b9', { tolerance: 8 });
45
+ });
46
+ ```
47
+
48
+ The vocabulary is Testing Library's, because that is the one React
49
+ developers already have. It is not Testing Library — there is no document
50
+ for it to query — but `getByRole` / `userEvent.click` / `waitFor` mean here
51
+ what they mean there.
52
+
53
+ ## `renderX11(element, options)`
54
+
55
+ Mounts the tree and returns a handle **plus the queries bound to it**, so
56
+ `const { getByText } = await renderX11(<App />)` reads the familiar way.
57
+
58
+ | option | |
59
+ | ----------------- | -------------------------------------------------------------------------------- |
60
+ | `width`, `height` | the window (default 640×480) |
61
+ | `screen` | the display (defaults comfortably larger than the window — see below) |
62
+ | `backend` | `'xserver'` (default) or `'mock'` |
63
+ | `fonts` | `{ family: '/path/to.ttf' }` — **required for text pixels** |
64
+ | `wrap` | wrap in a `<window>`; default true unless the element is one |
65
+ | `app` | render into a connection you already have |
66
+ | `scale` | pin the display scale — `2` renders as a retina panel would; default 1 |
67
+ | `colorScheme` | `'light'` (default), `'dark'`, or `'system'` to follow the real desktop |
68
+ | `appearance` | pin the rest of the desktop's appearance — [see below](#the-desktops-appearance) |
69
+
70
+ | on the result | |
71
+ | --------------------- | --------------------------------------------------------------------- |
72
+ | `ctx` | the window's 2d context — what the pixel helpers read |
73
+ | `window` | the live ntk `Window` |
74
+ | `windowNode` | the `WindowNode`: the paint, layout and event root |
75
+ | `server`, `app` | the X server and the connection, for anything this API does not cover |
76
+ | `rerender`, `unmount` | re-render into the same root; take it down |
77
+
78
+ **`fonts` is not optional for pixel assertions involving text.** Family
79
+ resolution otherwise shells out to `fc-match`, which gives a different answer
80
+ on every machine and no answer at all in a container — see the `sans-serif`
81
+ note in [AGENTS.md](../AGENTS.md).
82
+
83
+ **The screen is bigger than the window on purpose**, because on a real
84
+ display it is. A press "outside the application" is what dismisses a menu,
85
+ and it needs somewhere to land.
86
+
87
+ ## Queries
88
+
89
+ `getBy*` (exactly one, or throw), `queryBy*` (one or null — the way to
90
+ assert absence), `getAllBy*`, and `findBy*` — `getBy*` under
91
+ [`waitFor`](#act-and-the-three-clocks), with its options, so it retries until
92
+ the node appears and resolves once it is laid out. Four of each: `ByText`,
93
+ `ByRole`, `ByTestName`, `ByPlaceholder`.
94
+
95
+ ```js
96
+ getByText(/guestbook/); // substring, case-insensitive
97
+ getByText('Save', { exact: true });
98
+ getByRole('button', { name: 'Save' });
99
+ getByTestName('name-field'); // matches a `data-testname` prop
100
+ getByPlaceholder('Your name');
101
+ ```
102
+
103
+ - `screen` is the same queries bound to the most recent render, **popups
104
+ included** — a `<popup>` is a child node of the window that opened it, so a
105
+ menu or a dialog is queryable without any special casing.
106
+ - `within(node)` scopes them to a subtree.
107
+ - `all(predicate?)` is the escape hatch: every node under the root, in paint
108
+ order, matched on anything — `screen.all((n) => n.kind === 'gauge')`.
109
+ `node.kind` is the element name, which is how an element with no text and
110
+ no role of its own is still reachable.
111
+ - A miss prints the tree it searched, rather than leaving you with
112
+ `undefined is not an object`.
113
+
114
+ **About roles.** `roleOf` answers from the same model the AT-SPI bridge
115
+ publishes ([accessibility.md](accessibility.md)): an explicit `role` prop
116
+ if there is one, else the element's own semantics — `textinput` and
117
+ `textarea` report `textbox`, `window` and `popup` `window`, `image` `img`,
118
+ `text` `text`; kinds with no accessibility meaning (`box`, `canvas`)
119
+ answer their kind. The widgets name themselves — `Button` is `button`,
120
+ `Checkbox` `checkbox`, `Switch` `switch`, `Radio` `radio`, `Select`
121
+ `combobox` (its options `option`), `Slider` `slider`, `Tabs` `tablist`/
122
+ `tab`/`tabpanel`, `Dialog` `dialog` — so what a
123
+ test selects by is exactly what a screen reader hears.
124
+
125
+ **And the bridge itself.** Importing `react-x11/test` sets `NO_AT_BRIDGE`,
126
+ so a test run on a desktop never registers phantom applications with the
127
+ live screen-reader registry. Tests that exercise the bridge deliberately
128
+ point `AT_SPI_BUS_ADDRESS` at an in-process bus instead — see
129
+ [accessibility.md](accessibility.md#testing-and-verifying).
130
+
131
+ ## Components
132
+
133
+ The queries above see what a user sees. Sometimes a test needs the React
134
+ side instead — _which component_ made this node, with what props and state —
135
+ and this is the layer for it. Prefer the user-visible queries when either
136
+ would do; reach here when the question is genuinely about a component.
137
+
138
+ ```js
139
+ const row = screen.getByComponent('TaskRow'); // also RegExp or predicate
140
+ within(row).getByText('buy milk'); // scoped to that instance
141
+
142
+ ownerChainOf(node); // ['Label', 'TaskRow', 'App'] — who wrote the JSX
143
+ sourceOf(node); // { file, line, column } of the JSX that made it
144
+ ```
145
+
146
+ `getByComponent` returns, per rendered instance, the topmost host nodes of
147
+ its output — the container `within()` wants next. Ownership is React's JSX
148
+ owner (_who wrote the element_), so content passed as children belongs to
149
+ the component whose JSX it appears in, and a miss lists the component names
150
+ that are in the tree. All of this reads debug info development React records
151
+ on every fiber (`_debugOwner`, `_debugStack` — the same data
152
+ [click-to-component](click-to-component.md) navigates by), so it costs no
153
+ setup and no dependency, and returns nothing in production builds.
154
+
155
+ ### `inspect(node)` and `setHook`
156
+
157
+ ```js
158
+ const c = await inspect(screen.getByText('buy milk'));
159
+ c.name; // 'TaskRow' — the nearest mounted component
160
+ c.props; // its live props object (treat as read-only)
161
+ c.hooks; // [{ id, name: 'State', value, editable, source, subHooks }]
162
+
163
+ await c.setHook(0, 41); // set a useState/useReducer value…
164
+ await c.setHook(1, ['filter', 'text'], 'milk'); // …or a path inside one
165
+ ```
166
+
167
+ This drives the **React DevTools backend in-process**: the same
168
+ react-debug-tools inspection and the same override path the DevTools app
169
+ uses, minus its WebSocket — nothing listens on anything, and `ws` is never
170
+ loaded ([devtools.md](devtools.md) has the wiring). It needs the
171
+ `react-devtools-core` package in your devDependencies; the queries above
172
+ work without it.
173
+
174
+ What to know before leaning on it:
175
+
176
+ - **Reading hooks re-runs the component.** That is how react-debug-tools
177
+ recovers hook names: the render function is invoked once under a stub
178
+ dispatcher, output discarded, `console` muted. A render-counting test
179
+ will see it.
180
+ - **Values:** `props` and editable hook values are the live objects;
181
+ everything else is DevTools' copy — primitives exact, deep objects as
182
+ the same previews the DevTools panel shows.
183
+ - **Only `useState`/`useReducer` are settable** (`editable: true`; a
184
+ reducer is bypassed, not driven — no action runs). Hooks are addressed
185
+ by their `id`, an index the Rules of Hooks fix for the life of the
186
+ component. Anything else — an Effect, a Memo, a `useContext` value —
187
+ `setHook` refuses by name; for context, wrap a provider.
188
+ - **Nearest component is tree ancestry** (what the DevTools panel would
189
+ select), where `getByComponent`/`ownerChainOf` are JSX ownership; the
190
+ two differ for content passed as children.
191
+ - `setHook` resolves after React re-rendered and repainted (it is
192
+ `act`-wrapped), so a query straight after it sees the result.
193
+
194
+ ## Driving it
195
+
196
+ `userEvent` is the layer to reach for: each call injects input **and**
197
+ flushes what it caused, so it is async and there is nothing to remember.
198
+ `fireEvent` is the synchronous half, for when a test needs one side of a
199
+ gesture — wrap it in `act()` yourself.
200
+
201
+ ```js
202
+ await userEvent.click(getByRole('button'));
203
+ await userEvent.click(node, { modifiers: ['Shift'], button: 3 });
204
+ await userEvent.hover(node);
205
+ await userEvent.type(input, 'héllo\n'); // \n is Return
206
+ await userEvent.tab({ shift: true });
207
+ await userEvent.key(XK_ESCAPE); // react-x11/keysyms
208
+ await userEvent.key(XK_DEAD_ACUTE); // then type 'e' for é — events.md#composition
209
+ await userEvent.wheel(list, { deltaY: 3 }); // three notches, as buttons 4-7
210
+ await userEvent.wheel(list, { deltaY: 0.25, smooth: true }); // a touchpad
211
+ await userEvent.clickOutside(); // dismisses a menu — see below
212
+ ```
213
+
214
+ Two things this buys that emitting on the ntk window never could:
215
+
216
+ **Grabs are real.** A `<popup grab>` is dismissed by a real press outside
217
+ it. But note _where_ outside: react-x11's popups grab with **owner events**,
218
+ so a press on one of the client's own windows is delivered normally — that is
219
+ what keeps submenus and the owner window working. Only a press somewhere the
220
+ client does not own reaches the grab holder. `userEvent.clickOutside()` is
221
+ how a test says that; clicking another widget and expecting the menu to close
222
+ is testing the wrong thing.
223
+
224
+ **Focus is real**, so `trapFocus`, `autoFocus` and Tab traversal are
225
+ exercised rather than simulated. Deciding which window gets the keyboard is a
226
+ window manager's job and there is no window manager, so `renderX11` plays the
227
+ part with one `SetInputFocus` at mount — otherwise keys would go to whatever
228
+ the pointer happened to be over, which is a rule nobody should have to learn.
229
+
230
+ **A smooth scroll is the one exception**, and says so. Whole notches go
231
+ through the server as the button presses X really uses, but a touchpad's
232
+ fraction of a notch is XI2, and the in-process server has no XInput — there
233
+ are no valuators to inject. `{ smooth: true }` therefore delivers the `wheel`
234
+ event ntk would have derived from them, straight to the window. Everything
235
+ above it is still the real path: the dispatch, the chain, the default action.
236
+
237
+ **Typing works for text the layout does not have.** `userEvent.type(input,
238
+ 'héllo')` binds a spare keycode for `é` on both the server's keymap and the
239
+ client's cache, so a test can type real text without owning a keymap.
240
+
241
+ **Drags are real too.** Because the pointer events are genuine, a
242
+ drag-and-drop gesture is just three of them — with a real 4px threshold, so
243
+ move before releasing:
244
+
245
+ ```js
246
+ await act(async () => {
247
+ fireEvent.mouseDown(card);
248
+ fireEvent.mouseMove(card, { dx: 8 }); // past the threshold, still on the card
249
+ fireEvent.mouseMove(bin);
250
+ fireEvent.mouseUp(bin);
251
+ });
252
+ ```
253
+
254
+ `onDragStart`, `onDrop` and `onDragEnd` all run, and `e.items` carries the
255
+ payload by reference. A drop from _another application_ needs a second X
256
+ connection playing the foreign source — see
257
+ [drag-and-drop.md](drag-and-drop.md).
258
+
259
+ **A pointer event aims at a node's rect**, at its centre moved by
260
+ `{ dx, dy }`, so a node with no rect throws and says why. Only a node no
261
+ frame has laid out yet is told to `await act()`. The rest keep no rect
262
+ however many frames run, and the error says what to do instead:
263
+
264
+ - laid out at 0×0: give it a size, or point at an ancestor or a child that
265
+ has one. When the size is content still arriving, such as an image that
266
+ is loading, `waitFor` it;
267
+ - `display: 'none'`, on the node or on an ancestor, hidden by React in an
268
+ `<Activity mode="hidden">` or a `<Suspense>` showing its fallback, or in a
269
+ `<window hidden>` or a `<popup hidden>`: show it first;
270
+ - a nested `<text>` or an SVG shape, laid out as part of its paragraph or its
271
+ `<svg>`: point at that, with `{ dx, dy }`;
272
+ - unmounted: query for whatever replaced it.
273
+
274
+ A hidden or unmounted node throws even where it still has a rect. It keeps
275
+ the one it was last laid out at, and what is there now is whatever was
276
+ behind it: a button in a tab that was hidden sits exactly where the shown
277
+ tab's button is, and a click aimed at it would land on that one.
278
+
279
+ `mouseUp` and `mouseLeave`, and so `userEvent.unhover`, aim at nothing, so
280
+ the node they are handed only names the connection and can be one the press
281
+ or the hover took away. A release lets go wherever the pointer is, so move
282
+ it first to release somewhere else. A leave moves the pointer to the
283
+ screen's origin — which under `renderX11`, with no window manager to place
284
+ the window, is the window's own top-left corner, so a node there is not
285
+ left.
286
+
287
+ ## What a screen reader would hear
288
+
289
+ `renderX11(element, { a11y: true })` adds `at` to the result: the
290
+ assistive-technology spy, an in-process log of the same semantic feed the
291
+ AT-SPI bridge serves to Orca — focus, state changes, text edits,
292
+ announcements — observed at the hook seam, before any D-Bus. Synchronous,
293
+ bus-free, and it runs wherever the suite runs.
294
+
295
+ ```js
296
+ const { at } = await renderX11(<App />, { a11y: true });
297
+ await userEvent.tab();
298
+ assert.equal(at.focused().utterance, 'Save, button');
299
+ await userEvent.key(XK_SPACE);
300
+ assert.ok(at.since().some((e) => e.type === 'state' && e.state === 'checked'));
301
+ ```
302
+
303
+ A composition is in there as itself: a dead key logs one `preedit` entry
304
+ (`preedit: "´"`) and no insertion, and the letter that finishes it logs
305
+ `preedit: cleared` and one `insert: "é"` — the same line the bridge draws
306
+ with `:system`, which is how a test catches a screen reader that would echo
307
+ a half-typed accent.
308
+
309
+ A [registered element](extending.md#text-a-screen-reader-can-read) that
310
+ reports its own text is observed through the same seam, so a package
311
+ shipping an editor or a document view can assert what it says with no bus
312
+ and no desktop — `test/a11y-custom-text.test.js` is the worked pair.
313
+
314
+ `at.events()` / `at.since()` are precise facts, `at.transcript()` the
315
+ one-line summaries, `at.focusables()` every keyboard stop in Tab order with
316
+ its utterance — a nameless control reads `"(no accessible name)"`, which
317
+ turns the most-missed accessibility bug into a one-line audit. The four
318
+ canonical tests (focus order, keyboard trap, nothing nameless, states
319
+ announced) live in `test/a11y-spy.test.js`, written to be copied; the model
320
+ behind the wording is [accessibility.md](accessibility.md)'s own, not an
321
+ imitation of Orca's.
322
+
323
+ ## `act`, and the three clocks
324
+
325
+ `act(fn)` flushes everything between a state update and a pixel. There are
326
+ three separate clocks in the way, and a wrapper that drives only the first
327
+ stays flaky on pixel assertions:
328
+
329
+ 1. **React's** — `React.act` with `IS_REACT_ACT_ENVIRONMENT` set.
330
+ 2. **The wire** — events the injection produced are not in the tree yet.
331
+ The server writes them asynchronously, so a round trip on its own can
332
+ overtake them and `act` would return before the pointer move had been
333
+ dispatched at all, which looks exactly like a hover that does not work.
334
+ 3. **ntk's frame clock** — painting is scheduled through
335
+ `requestAnimationFrame`, paced behind the display's vertical blank (or a
336
+ server round trip where there is no Present extension). Pointer motion
337
+ waits for a frame too: ntk delivers a window's moves merged, once a frame,
338
+ so a move that lands just after a frame, such as the second of two hovers
339
+ in a row, waits out the rest of the frame interval. Synthetic input can
340
+ also leave a frame scheduled and never run. So `act` dispatches what each
341
+ window holds and runs the frame directly rather than waiting for either,
342
+ then round-trips so the server has actually processed the drawing.
343
+
344
+ `waitFor(fn)` retries until `fn` stops throwing, `act`-ing between attempts —
345
+ so waiting also _advances_. Reach for it when the thing you are asserting
346
+ arrives on a clock none of those three is: a timer, an fs read, an image
347
+ decode, another process. A `Button`'s hover needs no waiting, though it is an
348
+ event, a re-render of React state and a frame: `act` runs all three, so the
349
+ repaint is on screen when `userEvent.hover` resolves.
350
+
351
+ It `act`s once more after the attempt that passes, so what it resolves with
352
+ is on screen. That matters for a node. What an attempt finds can have arrived
353
+ in the pause before it — an fs read, a timer — and that commits outside
354
+ `act`, on React's own scheduler, while the frame that lays it out runs on the
355
+ third clock, which need not have ticked since. Without that last `act`, a
356
+ row found that way could have no rect yet, and the click after it would throw
357
+ `has no laid-out rect yet`.
358
+
359
+ ```js
360
+ const row = await waitFor(() => screen.getByText('notes.txt'));
361
+ await userEvent.click(row); // laid out: waitFor ran the frame
362
+ ```
363
+
364
+ ## When the connection ends
365
+
366
+ `act()`, `waitFor` and `cleanup()` each drain the connection with a round
367
+ trip, and a connection that has gone has nothing left to drain, so there they
368
+ resolve without one. A test can close its app, dispose of it or have the
369
+ server drop it, and still get through its own teardown.
370
+
371
+ What an app does when its display goes away — `createRoot`'s
372
+ [`onDisconnect`](README.md) — is tested by ending the connection from the
373
+ server's side, the way `xkill` does. An option `renderX11` does not know is
374
+ handed to `createRoot`:
375
+
376
+ ```js
377
+ const seen = [];
378
+ const { app, window } = await renderX11(<App />, {
379
+ onDisconnect: (reason) => seen.push(reason),
380
+ });
381
+ app.X.KillClient(window.id);
382
+ await act();
383
+ assert.deepEqual(seen, ['closed']);
384
+ ```
385
+
386
+ `app.close()` is not the same test. Over the in-process server, a client that
387
+ closed its own connection never hears it end, so `onDisconnect` never fires.
388
+
389
+ ## Pixels
390
+
391
+ ```js
392
+ await expectPixel(ctx, 10, 10, '#2980b9', { tolerance: 8 });
393
+ await waitForPixel(ctx, 10, 10, '#c0392b', { timeout: 2000 });
394
+ const [r, g, b] = await pixelAt(ctx, 10, 10);
395
+ const n = await countPixels(ctx, { width: 80, height: 40 }, '#c0392b');
396
+ await toPNG(ctx, '/tmp/shot.png', { width: 320, height: 240 });
397
+ ```
398
+
399
+ A pixel proves the tree, the layout, the clip, the paint order, the colour
400
+ parsing and the wire encoding at once — and it is the only assertion that
401
+ catches the class of bug where everything commits correctly and nothing
402
+ reaches the screen. Two details it demands: the readback is straight
403
+ **RGBA**, as on a canvas, and antialiasing means every comparison takes a
404
+ **tolerance**.
405
+
406
+ `toPNG` is also how a pull request gets a screenshot rendered by its own
407
+ code, which this repo asks for ([AGENTS.md](../AGENTS.md), "Pull requests").
408
+
409
+ ## Animations
410
+
411
+ ```js
412
+ const clock = withFrameClock(); // install it BEFORE the render
413
+ await rerender(<Fading on />);
414
+ clock.advance(100);
415
+ await act(); // part way
416
+ clock.advance(200);
417
+ await act(); // arrived
418
+ clock.restore();
419
+ ```
420
+
421
+ Install it **before** the render. Every timestamp in the tree has to come
422
+ from the same clock: mixing a real start time with a frozen `now` gives a
423
+ transition that never progresses, which is a confusing way to spend an hour.
424
+
425
+ ## The desktop's appearance
426
+
427
+ The built-in palette follows the desktop, so `renderX11` **pins** it: an
428
+ unpinned suite renders in whatever colours the machine running it is in, and
429
+ a pixel assertion passes on a light desktop and fails on a dark one with
430
+ nothing in the test to say why. The pin is light unless the render says
431
+ otherwise, and it covers everything `useSystemAppearance()` reports — so a
432
+ component that follows reduced motion, contrast or the accent is tested the
433
+ way one that follows the scheme is:
434
+
435
+ ```jsx
436
+ await renderX11(<App />); // light
437
+ await renderX11(<App />, { colorScheme: 'dark' });
438
+ await renderX11(<Spinner />, { appearance: { reducedMotion: true } });
439
+ await renderX11(<App />, { colorScheme: 'system' }); // the real desktop
440
+ ```
441
+
442
+ `appearance` pins from the **first** render, which matters wherever the first
443
+ render decides something: a loop that never starts is a different path from
444
+ one that starts and is stopped. To change a value under a mounted tree — the
445
+ case a component that follows the desktop has to get right — use
446
+ `setAppearance`:
447
+
448
+ ```jsx
449
+ import { renderX11, setAppearance, screen } from 'react-x11/test';
450
+
451
+ await renderX11(<Spinner />, {
452
+ colorScheme: 'dark',
453
+ appearance: { reducedMotion: true },
454
+ });
455
+ screen.getByText('paused');
456
+ await setAppearance({ reducedMotion: false });
457
+ screen.getByText('spinning'); // re-rendered and repainted, and still dark
458
+ ```
459
+
460
+ It merges over the pin, so naming one value keeps the rest, and it flushes the
461
+ way `userEvent` does: the re-render, the palette's repaint and the frame are
462
+ done when it resolves. The appearance is the desktop's, so there is one per
463
+ process — every mounted root sees the change, as every window would.
464
+
465
+ `reducedMotion` is the appearance's answer, and it is not the one core's own
466
+ motion follows: `ProgressBar` and the style loops read
467
+ `useDesktopSettings().animations`, a separate setting from a separate source
468
+ ([system.md](system.md)) that the two can disagree on. The in-process server
469
+ has no settings daemon, so it stays at its default — on — whatever is pinned
470
+ here.
471
+
472
+ Three mistakes throw rather than pass quietly:
473
+
474
+ - **A key outside `useSystemAppearance()`'s fields.** `reduceMotion: true`
475
+ would otherwise pin nothing and render the component the way it always
476
+ does. `source` is not settable either: under a pin it reads `'test'`.
477
+ - **`colorScheme: 'system'` with `appearance`.** The first follows the real
478
+ desktop and the second pins it; name `colorScheme` inside `appearance`
479
+ instead.
480
+ - **`setAppearance` before `renderX11`.** The render pins as it mounts and
481
+ would replace what was set, so it throws and says to pass `appearance`.
482
+
483
+ `cleanup()` releases the pin, and only the pin: what a real desktop answered
484
+ is not the harness's to throw away. A pinned value is never written to the
485
+ cache a real app starts its first frame from ([appearance.md](appearance.md)),
486
+ so a suite run on a developer's desktop leaves that desktop's remembered
487
+ answer as it found it — and an answer the desktop sends while a pin is held,
488
+ from a probe a `'system'` test started, is dropped rather than painted into
489
+ the next test.
490
+
491
+ ## The mock backend
492
+
493
+ `backend: 'mock'` swaps the server for a fake 2d context that records draw
494
+ operations. No server, no connection, much faster, and no pixels — good for
495
+ asserting on layout and on the node tree, which is most tests. Input
496
+ injection is not available and says so.
497
+
498
+ With no connection there is also nothing to embed into, so the mock answers
499
+ the way the Cocoa backend does: `useSupports('embedding')` is false, and a
500
+ `<foreign>` in the tree lays out as an empty box and reports one `onError`
501
+ rather than failing the mount. A component that asks before rendering one
502
+ mounts its fallback here; one that does not still mounts.
503
+
504
+ A batch records as the fills it stands for: `ctx.fillRects([[x, y, w, h], …])`
505
+ leaves one `fillRect` operation per rectangle, because the only difference
506
+ between them is a request count and there is no server here to send it to. So
507
+ an assertion on what a drawing painted holds whether or not it batched.
508
+
509
+ ## Testing Library itself
510
+
511
+ It does not work here and no shim will fix it: its queries are built on a
512
+ document. The five failure modes are recorded in the
513
+ [negative-results register](ecosystem.md#render-time). This page is the
514
+ replacement, not a workaround for it.
@@ -0,0 +1,117 @@
1
+ # TypeScript
2
+
3
+ Types ship with the package. There is no `@types/react-x11`, and nothing to
4
+ build — the declarations are hand-written `.d.ts` files next to the
5
+ JavaScript they describe.
6
+
7
+ ## Setup
8
+
9
+ ```jsonc
10
+ // tsconfig.json
11
+ {
12
+ "compilerOptions": {
13
+ "jsx": "react-jsx",
14
+ "jsxImportSource": "react-x11",
15
+ "moduleResolution": "bundler", // or "node16" / "nodenext"
16
+ },
17
+ }
18
+ ```
19
+
20
+ `jsxImportSource` is the whole configuration. Without it JSX resolves
21
+ against React's namespace, which describes the DOM.
22
+
23
+ ## Why a JSX runtime and not an augmentation
24
+
25
+ The usual way a library adds elements to JSX is to merge into React's
26
+ namespace:
27
+
28
+ ```ts
29
+ declare module 'react' {
30
+ namespace JSX {
31
+ interface IntrinsicElements {
32
+ window: WindowProps;
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ That cannot work here. Five element names — `text`, `image`, `canvas`,
39
+ `html`, `svg` — are also HTML or SVG element names, already declared by
40
+ `@types/react` with completely different props, and declaration merging
41
+ cannot replace an existing member:
42
+
43
+ ```
44
+ Interface 'IntrinsicElements' incorrectly extends interface 'ReactX11Elements'.
45
+ The types of 'text.ref' are incompatible between these types.
46
+ Type 'Ref<SVGTextElement>' is not assignable to type 'Ref<DrawnNode>'.
47
+ ```
48
+
49
+ So react-x11 provides its own JSX source instead — `react-x11/jsx-runtime`,
50
+ which re-exports React's actual runtime and carries a `JSX` namespace of its
51
+ own. That turns out to be the better answer regardless: the namespace holds
52
+ the X11 elements _and nothing else_, so
53
+
54
+ ```tsx
55
+ <div /> // error: Property 'div' does not exist on type 'JSX.IntrinsicElements'
56
+ ```
57
+
58
+ is caught at compile time rather than throwing at runtime. An augmentation
59
+ could never have done that, because `@types/react` declares every HTML and
60
+ SVG tag unconditionally.
61
+
62
+ If you need extra elements of your own, `IntrinsicElements` is an interface,
63
+ so merging still works:
64
+
65
+ ```ts
66
+ declare module 'react-x11/jsx-runtime' {
67
+ namespace JSX {
68
+ interface IntrinsicElements {
69
+ 'my-element': { value?: number };
70
+ }
71
+ }
72
+ }
73
+ ```
74
+
75
+ ## What is typed
76
+
77
+ | | |
78
+ | --------------------------- | ----------------------------------------------------------------------------- |
79
+ | `src/types/style.d.ts` | `Style`, `StyleProp`, the yoga enums, state blocks, transitions, size queries |
80
+ | `src/types/events.d.ts` | `SyntheticEvent` and the mouse / wheel / keyboard / focus shapes |
81
+ | `src/types/nodes.d.ts` | what a `ref` gives you: `DrawnNode`, `ScrollableNode`, ntk's `Window` |
82
+ | `src/types/elements.d.ts` | every host element's props, including the 3D scene |
83
+ | `src/types/components.d.ts` | the widget set, `Theme`, the anchoring helpers |
84
+ | `src/index.d.ts` | the entry: `createRoot`, `render`, and re-exports of the above |
85
+
86
+ A few deliberate looseness decisions:
87
+
88
+ - **`Color` is `string`.** It is a CSS colour or a `$token` resolved against
89
+ the nearest `theme`; narrowing it would reject valid values without
90
+ catching much.
91
+ - **`ctx` and `gl` are `any`.** They are ntk's 2d context and its indirect
92
+ GLX context, and ntk ships no types. Typing them here would mean
93
+ maintaining a second, drifting copy of ntk's API.
94
+ - **`Renderer` is `any`.** It is the react-reconciler instance, an escape
95
+ hatch rather than a supported surface.
96
+
97
+ Style values are as precise as the runtime is: `flexDirection` only takes
98
+ the four yoga values, a state block only takes paint properties (the same
99
+ rule `createStyles` enforces at runtime), and `width` takes
100
+ `number | '50%' | 'auto'`.
101
+
102
+ ## Keeping them honest
103
+
104
+ Hand-written declarations drift. `test/types/api.tsx` is compiled by
105
+ `npm run typecheck` — in CI alongside lint — and exercises the API the way
106
+ the examples do, including `@ts-expect-error` lines for things that must
107
+ _not_ compile:
108
+
109
+ ```tsx
110
+ // @ts-expect-error — layout properties may not go in a state block
111
+ createStyles({ bad: { ':hover': { padding: 4 } } });
112
+ ```
113
+
114
+ Those fail the build if the rejection stops happening, which is the half of
115
+ a type test that usually goes missing.
116
+
117
+ When you change a prop, change the declaration and add a line to that file.