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