react-x11 1.2.0 → 2.0.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 (158) hide show
  1. package/README.md +330 -81
  2. package/package.json +132 -14
  3. package/src/ClickToComponent.js +9 -2
  4. package/src/DevToolsIntegration.js +440 -32
  5. package/src/Reconciler.js +463 -165
  6. package/src/a11y.js +1299 -0
  7. package/src/acceleratorhooks.js +86 -0
  8. package/src/accelerators.js +128 -0
  9. package/src/activate.js +168 -0
  10. package/src/anchor.js +360 -0
  11. package/src/appcontext.js +161 -0
  12. package/src/appearance.js +748 -0
  13. package/src/appearancehooks.js +96 -0
  14. package/src/apphooks.js +73 -0
  15. package/src/application.js +748 -0
  16. package/src/atspi.js +1953 -0
  17. package/src/bus.js +545 -0
  18. package/src/bushooks.js +136 -0
  19. package/src/clientmessage.js +140 -0
  20. package/src/clipboard.js +121 -0
  21. package/src/components/Button.js +148 -33
  22. package/src/components/Checkbox.js +66 -32
  23. package/src/components/Dialog.js +96 -29
  24. package/src/components/FileDialog.js +491 -0
  25. package/src/components/Icon.js +329 -0
  26. package/src/components/Menu.js +797 -133
  27. package/src/components/PasswordInput.js +434 -0
  28. package/src/components/ProgressBar.js +91 -12
  29. package/src/components/Radio.js +78 -35
  30. package/src/components/Select.js +255 -65
  31. package/src/components/Slider.js +102 -41
  32. package/src/components/SplitPane.js +206 -0
  33. package/src/components/Switch.js +100 -22
  34. package/src/components/Table.js +529 -0
  35. package/src/components/Tabs.js +211 -0
  36. package/src/components/Tooltip.js +528 -54
  37. package/src/components/anchor.js +154 -107
  38. package/src/components/change.js +34 -0
  39. package/src/components/dnd.js +103 -0
  40. package/src/components/index.js +16 -3
  41. package/src/components/keys.js +17 -20
  42. package/src/components/scribble.js +247 -0
  43. package/src/components/theme.js +350 -42
  44. package/src/components/typeahead.js +12 -2
  45. package/src/compose.js +868 -0
  46. package/src/compositing.js +224 -0
  47. package/src/dbusmenu.js +384 -0
  48. package/src/debug.d.ts +63 -0
  49. package/src/debug.js +642 -0
  50. package/src/decorations.js +486 -0
  51. package/src/desktopsettings.js +209 -0
  52. package/src/desktopsettingshooks.js +62 -0
  53. package/src/dnd.js +1725 -0
  54. package/src/editmenu.js +272 -0
  55. package/src/errors.js +98 -0
  56. package/src/events.js +1406 -162
  57. package/src/extensions.js +45 -0
  58. package/src/filedialog.js +375 -0
  59. package/src/filedialoghooks.js +132 -0
  60. package/src/fonthooks.js +64 -0
  61. package/src/fonts.js +301 -0
  62. package/src/foreignnodes.js +519 -0
  63. package/src/frame/child.js +33 -0
  64. package/src/frame/childmain.js +274 -0
  65. package/src/frame/env.js +140 -0
  66. package/src/frame/index.js +444 -0
  67. package/src/frame/lifecycle.js +67 -0
  68. package/src/frame/protocol.js +179 -0
  69. package/src/frames.js +73 -0
  70. package/src/glbackend.js +114 -0
  71. package/src/glnodes.js +125 -67
  72. package/src/globalmenu.js +683 -0
  73. package/src/host.d.ts +88 -0
  74. package/src/host.js +42 -0
  75. package/src/idle.js +490 -0
  76. package/src/idlehooks.js +100 -0
  77. package/src/imagesource.js +349 -0
  78. package/src/index.d.ts +475 -0
  79. package/src/index.js +83 -9
  80. package/src/inputtime.js +206 -0
  81. package/src/jsx-dev-runtime.d.ts +21 -0
  82. package/src/jsx-dev-runtime.js +2 -0
  83. package/src/jsx-runtime.d.ts +39 -0
  84. package/src/jsx-runtime.js +6 -0
  85. package/src/keyboard.js +256 -0
  86. package/src/keyboardstate.js +278 -0
  87. package/src/keyboardstatehooks.js +58 -0
  88. package/src/keysyms.d.ts +149 -0
  89. package/src/keysyms.js +270 -0
  90. package/src/locale.js +170 -0
  91. package/src/localehooks.js +47 -0
  92. package/src/menuitem.js +223 -0
  93. package/src/node.d.ts +597 -0
  94. package/src/node.js +44 -0
  95. package/src/nodes.js +9546 -690
  96. package/src/ntk.d.ts +44 -0
  97. package/src/ntk.js +25 -0
  98. package/src/paintcache.js +366 -0
  99. package/src/palette.js +380 -0
  100. package/src/pastestate.js +66 -0
  101. package/src/portal.js +461 -0
  102. package/src/priority.js +26 -0
  103. package/src/refresh/index.d.ts +40 -0
  104. package/src/refresh/index.js +122 -0
  105. package/src/refresh/loader.d.ts +37 -0
  106. package/src/refresh/loader.js +401 -0
  107. package/src/refresh/register.d.ts +5 -0
  108. package/src/refresh/register.js +13 -0
  109. package/src/registry.js +232 -0
  110. package/src/scale.js +626 -0
  111. package/src/scalehooks.js +27 -0
  112. package/src/screencolor.js +640 -0
  113. package/src/screencolorhooks.js +101 -0
  114. package/src/screens.js +754 -0
  115. package/src/screenshooks.js +137 -0
  116. package/src/startup.js +302 -0
  117. package/src/style.d.ts +126 -0
  118. package/src/style.js +33 -0
  119. package/src/styles.js +1482 -6
  120. package/src/svgnodes.js +306 -0
  121. package/src/testing/a11y.js +484 -0
  122. package/src/testing/components.js +414 -0
  123. package/src/testing/events.js +407 -0
  124. package/src/testing/harness.js +455 -0
  125. package/src/testing/index.d.ts +558 -0
  126. package/src/testing/index.js +85 -0
  127. package/src/testing/mock-app.js +463 -0
  128. package/src/testing/pixels.js +152 -0
  129. package/src/testing/queries.js +224 -0
  130. package/src/textrange.js +83 -0
  131. package/src/textselection.js +439 -0
  132. package/src/trace-registry.js +63 -0
  133. package/src/transfer.js +93 -0
  134. package/src/types/appearance.d.ts +84 -0
  135. package/src/types/application.d.ts +173 -0
  136. package/src/types/components.d.ts +820 -0
  137. package/src/types/dbus.d.ts +177 -0
  138. package/src/types/elements.d.ts +897 -0
  139. package/src/types/events.d.ts +549 -0
  140. package/src/types/filedialog.d.ts +260 -0
  141. package/src/types/fonts.d.ts +124 -0
  142. package/src/types/frame.d.ts +146 -0
  143. package/src/types/globalmenu.d.ts +42 -0
  144. package/src/types/nodes.d.ts +199 -0
  145. package/src/types/screencolor.d.ts +84 -0
  146. package/src/types/style.d.ts +434 -0
  147. package/src/types/system.d.ts +287 -0
  148. package/src/windowid.js +151 -0
  149. package/src/windowstate.js +393 -0
  150. package/src/xsettings.js +336 -0
  151. package/src/yoga.d.ts +55 -0
  152. package/src/yoga.js +135 -0
  153. package/src/components/Canvas3D.js +0 -28
  154. package/src/geometry3d.js +0 -223
  155. package/src/pointer3d.js +0 -158
  156. package/src/raycast3d.js +0 -146
  157. package/src/richnodes.js +0 -436
  158. package/src/scene3d.js +0 -683
@@ -0,0 +1,558 @@
1
+ /**
2
+ * `react-x11/test` — render, query, drive and assert on a react-x11 tree,
3
+ * against a real X server running in this process. No `$DISPLAY`, no xvfb.
4
+ */
5
+
6
+ import type { ReactNode } from 'react';
7
+ import type { DrawnNode, NtkApp, NtkWindow } from '../types/nodes.js';
8
+ import type { ModifierName } from '../keysyms.js';
9
+
10
+ export * from '../keysyms.js';
11
+
12
+ /** node-x11's in-process X server. Typed loosely — it is not our API. */
13
+ export interface TestServer {
14
+ width: number;
15
+ height: number;
16
+ keymap: {
17
+ minKeycode: number;
18
+ maxKeycode: number;
19
+ syms: number[][];
20
+ modifiers: number[][];
21
+ keycodeForKeysym(keysym: number): number;
22
+ };
23
+ injectPointerMove(x: number, y: number): void;
24
+ injectButton(button: number, isPress: boolean): void;
25
+ injectKey(keycode: number, isPress: boolean): void;
26
+ [key: string]: unknown;
27
+ }
28
+
29
+ /** ntk's 2d context. Pixel helpers take it; `renderX11` hands it back. */
30
+ export type TestContext2D = unknown;
31
+
32
+ export interface RenderX11Options {
33
+ /** Window size (default 640×480). */
34
+ width?: number;
35
+ height?: number;
36
+ /**
37
+ * Screen size. Defaults to comfortably larger than the window, because on
38
+ * a real display it is — and because a press "outside the application",
39
+ * which is what dismisses a menu, needs somewhere to land.
40
+ */
41
+ screen?: { width: number; height: number };
42
+ /**
43
+ * `'xserver'` (default) runs a real in-process X server and real pixels.
44
+ * `'mock'` swaps in a fake 2d context that logs draw operations: faster,
45
+ * no server, and no pixel assertions or input injection.
46
+ */
47
+ backend?: 'xserver' | 'mock';
48
+ /**
49
+ * Font family → font file. **Required for pixel assertions involving
50
+ * text**: family resolution otherwise shells out to `fc-match`, which is a
51
+ * different answer on every machine and no answer at all in a container.
52
+ */
53
+ fonts?: Record<string, string>;
54
+ /**
55
+ * Wrap the element in a `<window>`. Defaults to true unless the element
56
+ * *is* a `<window>`; pass `false` when a component renders one itself.
57
+ */
58
+ wrap?: boolean;
59
+ title?: string;
60
+ /** Render into a connection you already have. */
61
+ app?: NtkApp;
62
+ /**
63
+ * Install the assistive-technology spy before the mount and hand it back
64
+ * as `at`: an in-process log of everything a screen reader would have
65
+ * been told — focus, state changes, text edits, announcements — with no
66
+ * D-Bus anywhere. See docs/accessibility.md.
67
+ */
68
+ a11y?: boolean;
69
+ }
70
+
71
+ /** One recorded assistive-technology fact, plus its one-line `summary`. */
72
+ export interface A11ySpyEvent {
73
+ type:
74
+ | 'focus'
75
+ | 'blur'
76
+ | 'state'
77
+ | 'name'
78
+ | 'value'
79
+ | 'text-insert'
80
+ | 'text-delete'
81
+ | 'preedit'
82
+ | 'caret'
83
+ | 'selection'
84
+ | 'announce'
85
+ | 'window';
86
+ /** The transcript line — `"focus: Save, button"`, `"state: checked"`. */
87
+ summary: string;
88
+ node?: DrawnNode | null;
89
+ /** For `focus`. */
90
+ utterance?: string;
91
+ /** For `state`: the AT-SPI state nick, and whether it turned on. */
92
+ state?: string;
93
+ on?: boolean;
94
+ /** For `name` / `announce` / `text-*` / `preedit` — on a `preedit` this
95
+ * is what the composition now shows, `''` when it was abandoned or has
96
+ * just committed. */
97
+ name?: string;
98
+ text?: string;
99
+ assertive?: boolean;
100
+ /** For `value`. */
101
+ value?: number;
102
+ /** For `text-*` / `caret` / `preedit`. */
103
+ offset?: number;
104
+ /** For `selection`. */
105
+ start?: number;
106
+ end?: number;
107
+ /** For `window`. */
108
+ focused?: boolean;
109
+ }
110
+
111
+ /** A focusable (or focused) node as an AT would describe it. */
112
+ export interface A11yDescription {
113
+ node: DrawnNode;
114
+ name: string;
115
+ /** The AT-SPI role name — `"check box"`, `"entry"`. */
116
+ role: string;
117
+ /** The set state nicks — `"focusable"`, `"checked"`, `"sensitive"`, … */
118
+ states: string[];
119
+ /** `"(no accessible name)"` when there is nothing to say. */
120
+ utterance: string;
121
+ }
122
+
123
+ /**
124
+ * The in-process assistive-technology spy: the same semantic feed the
125
+ * AT-SPI bridge serves, observed at the hook seam with no bus. Assert on
126
+ * `events()`/`since()` for exact facts, on `transcript()` for "what would
127
+ * a user have been told".
128
+ */
129
+ export interface A11ySpy {
130
+ /** Every entry recorded so far, oldest first. */
131
+ events(): A11ySpyEvent[];
132
+ /** Entries since the previous `since()` (or `clear()`). */
133
+ since(): A11ySpyEvent[];
134
+ /** The `summary` of every entry — made for `assert.deepEqual`. */
135
+ transcript(): string[];
136
+ clear(): void;
137
+ /** The focused node as an AT would describe it, or null. */
138
+ focused(): A11yDescription | null;
139
+ /** Every keyboard-reachable node, in Tab order, focus scopes included. */
140
+ focusables(): A11yDescription[];
141
+ /** Put the hook slots back; `cleanup()` does this for you. */
142
+ uninstall(): void;
143
+ }
144
+
145
+ export interface Queries {
146
+ getByText(text: string | RegExp, options?: TextMatchOptions): DrawnNode;
147
+ getAllByText(text: string | RegExp, options?: TextMatchOptions): DrawnNode[];
148
+ queryByText(
149
+ text: string | RegExp,
150
+ options?: TextMatchOptions,
151
+ ): DrawnNode | null;
152
+ queryAllByText(
153
+ text: string | RegExp,
154
+ options?: TextMatchOptions,
155
+ ): DrawnNode[];
156
+ findByText(
157
+ text: string | RegExp,
158
+ options?: TextMatchOptions & WaitOptions,
159
+ ): Promise<DrawnNode>;
160
+
161
+ getByRole(role: string, options?: RoleOptions): DrawnNode;
162
+ getAllByRole(role: string, options?: RoleOptions): DrawnNode[];
163
+ queryByRole(role: string, options?: RoleOptions): DrawnNode | null;
164
+ queryAllByRole(role: string, options?: RoleOptions): DrawnNode[];
165
+ findByRole(
166
+ role: string,
167
+ options?: RoleOptions & WaitOptions,
168
+ ): Promise<DrawnNode>;
169
+
170
+ getByTestName(name: string): DrawnNode;
171
+ getAllByTestName(name: string): DrawnNode[];
172
+ queryByTestName(name: string): DrawnNode | null;
173
+ queryAllByTestName(name: string): DrawnNode[];
174
+ findByTestName(name: string, options?: WaitOptions): Promise<DrawnNode>;
175
+
176
+ getByComponent(component: ComponentMatch): DrawnNode;
177
+ getAllByComponent(component: ComponentMatch): DrawnNode[];
178
+ queryByComponent(component: ComponentMatch): DrawnNode | null;
179
+ queryAllByComponent(component: ComponentMatch): DrawnNode[];
180
+ findByComponent(
181
+ component: ComponentMatch,
182
+ options?: WaitOptions,
183
+ ): Promise<DrawnNode>;
184
+
185
+ getByPlaceholder(
186
+ text: string | RegExp,
187
+ options?: TextMatchOptions,
188
+ ): DrawnNode;
189
+ getAllByPlaceholder(
190
+ text: string | RegExp,
191
+ options?: TextMatchOptions,
192
+ ): DrawnNode[];
193
+ queryByPlaceholder(
194
+ text: string | RegExp,
195
+ options?: TextMatchOptions,
196
+ ): DrawnNode | null;
197
+ queryAllByPlaceholder(
198
+ text: string | RegExp,
199
+ options?: TextMatchOptions,
200
+ ): DrawnNode[];
201
+ findByPlaceholder(
202
+ text: string | RegExp,
203
+ options?: TextMatchOptions & WaitOptions,
204
+ ): Promise<DrawnNode>;
205
+
206
+ /** Everything under here, in paint order — the escape hatch. */
207
+ all(predicate?: (node: DrawnNode) => boolean): DrawnNode[];
208
+ }
209
+
210
+ /**
211
+ * How ByComponent names a component: exact display name, RegExp, or a
212
+ * predicate over the name. Owner-based (who wrote the JSX), so it needs
213
+ * development React.
214
+ */
215
+ export type ComponentMatch = string | RegExp | ((name: string) => boolean);
216
+
217
+ export interface TextMatchOptions {
218
+ /** Exact, case-sensitive match. Default is a trimmed substring match. */
219
+ exact?: boolean;
220
+ /** Restrict to one element kind, e.g. `'textinput'`. */
221
+ selector?: string;
222
+ }
223
+
224
+ export interface RoleOptions {
225
+ /** Also require the node's text to match. */
226
+ name?: string | RegExp;
227
+ exact?: boolean;
228
+ }
229
+
230
+ export interface WaitOptions {
231
+ timeout?: number;
232
+ interval?: number;
233
+ }
234
+
235
+ export interface RenderX11Result extends Queries {
236
+ root: { app: NtkApp; render(element: ReactNode): void; unmount(): void };
237
+ app: NtkApp;
238
+ /** null with `backend: 'mock'`. */
239
+ server: TestServer | null;
240
+ /** The `WindowNode` the tree mounted into — the paint and event root. */
241
+ windowNode: DrawnNode;
242
+ window: NtkWindow | null;
243
+ /** The window's 2d context, which is what pixel assertions read. */
244
+ readonly ctx: TestContext2D;
245
+ /** The assistive-technology spy, when `a11y: true` asked for one. */
246
+ at: A11ySpy | null;
247
+ rerender(element: ReactNode): Promise<void>;
248
+ unmount(): Promise<void>;
249
+ }
250
+
251
+ /** Install a spy outside `renderX11` — install **before** rendering, and
252
+ * uninstall yourself. Prefer `renderX11(el, { a11y: true })`. */
253
+ export function installA11ySpy(): A11ySpy;
254
+
255
+ /** The utterance for a live node, through the shared model. */
256
+ export function nodeUtterance(node: DrawnNode): string;
257
+
258
+ /** The utterance formatter itself, for callers that gathered the parts
259
+ * elsewhere (scripts/a11y-probe.mjs reads them over real D-Bus). */
260
+ export function utteranceOf(parts: {
261
+ name: string;
262
+ role?: string;
263
+ states?: string[];
264
+ value?: { now: number; min?: number; max?: number } | null;
265
+ }): string;
266
+
267
+ /**
268
+ * Mount a tree against a real in-process X server and return a handle plus
269
+ * the queries bound to it.
270
+ */
271
+ export function renderX11(
272
+ element: ReactNode,
273
+ options?: RenderX11Options,
274
+ ): Promise<RenderX11Result>;
275
+
276
+ /**
277
+ * Flush everything between a state update and a pixel: React's work, the
278
+ * events the injection put on the wire, ntk's frame clock, and an X round
279
+ * trip. A wrapper that only calls `React.act` stays flaky on pixels.
280
+ */
281
+ export function act(fn?: () => unknown): Promise<void>;
282
+
283
+ /** Unmount everything, restore the animation clock, close every server. */
284
+ export function cleanup(): Promise<void>;
285
+
286
+ /** Drain in-flight requests on a connection. */
287
+ export function settle(app: NtkApp, roundTrips?: number): Promise<void>;
288
+
289
+ /** Retry until it stops throwing, `act`-ing between attempts. */
290
+ export function waitFor<T>(
291
+ fn: () => T | Promise<T>,
292
+ options?: WaitOptions,
293
+ ): Promise<T>;
294
+
295
+ /** The queries bound to a subtree. */
296
+ export function within(root: DrawnNode): Queries;
297
+
298
+ /** The queries bound to the most recent `renderX11`, popups included. */
299
+ export const screen: Queries;
300
+
301
+ /** The text a node presents, joined across its spans. */
302
+ export function textOf(node: DrawnNode): string;
303
+
304
+ /** The role a node reports: its `role` prop, else its element kind. */
305
+ export function roleOf(node: DrawnNode): string;
306
+
307
+ /**
308
+ * The components that created this node, nearest first — the JSX owner
309
+ * chain, not tree ancestry. Empty outside development React.
310
+ */
311
+ export function ownerChainOf(node: DrawnNode): string[];
312
+
313
+ /** Where the JSX that created this node lives, from React's own call-site
314
+ * capture (development mode). Null when there is no debug info. */
315
+ export function sourceOf(node: DrawnNode): {
316
+ functionName?: string;
317
+ file: string;
318
+ line: number;
319
+ column: number;
320
+ } | null;
321
+
322
+ export interface InspectedHook {
323
+ /** React's index over the hooks that occupy a slot — how `setHook`
324
+ * addresses it. Null for hooks that don't (useContext). */
325
+ id: number | null;
326
+ /** react-debug-tools' name: 'State', 'Reducer', 'Effect', 'Context', or
327
+ * a custom hook's name with its primitives in `subHooks`. */
328
+ name: string;
329
+ /** Live for editable hooks; DevTools' copy (deep objects appear as
330
+ * previews, the way the DevTools panel shows them) for the rest. */
331
+ value: unknown;
332
+ /** True for useState/useReducer — the hooks `setHook` can write. */
333
+ editable: boolean;
334
+ source: { file: string; line: number; column: number } | null;
335
+ subHooks: InspectedHook[];
336
+ }
337
+
338
+ export interface InspectedComponent {
339
+ /** DevTools' element id — stable for the life of the instance. */
340
+ id: number;
341
+ name: string | null;
342
+ /** The live props object. Treat as read-only. */
343
+ props: Record<string, unknown> | null;
344
+ /** Class component state; null for function components. */
345
+ state: unknown;
346
+ /** Legacy class context only; `useContext` values appear in `hooks`. */
347
+ context: unknown;
348
+ hooks: InspectedHook[];
349
+ /** Owner names, nearest first — who rendered this component. */
350
+ owners: string[];
351
+ /** Set a useState/useReducer value (optionally at a path inside it),
352
+ * through React's own override machinery, `act`-wrapped. */
353
+ setHook(hookID: number, value: unknown): Promise<void>;
354
+ setHook(
355
+ hookID: number,
356
+ path: Array<string | number>,
357
+ value: unknown,
358
+ ): Promise<void>;
359
+ }
360
+
361
+ /**
362
+ * The React side of a node: the nearest mounted component above it, its
363
+ * props and named hooks, and `setHook` to change useState/useReducer
364
+ * state. Drives the React DevTools backend in-process — no WebSocket, but
365
+ * `react-devtools-core` must be installed, and reading hooks re-invokes
366
+ * the component's render function (react-debug-tools recovers names that
367
+ * way).
368
+ */
369
+ export function inspect(node: DrawnNode): Promise<InspectedComponent>;
370
+
371
+ export interface PointerOptions {
372
+ /** Offset from the node's centre. */
373
+ dx?: number;
374
+ dy?: number;
375
+ /** X button number: 1 left, 2 middle, 3 right. */
376
+ button?: number;
377
+ modifiers?: ModifierName[] | number;
378
+ }
379
+
380
+ export interface KeyOptions {
381
+ target?: DrawnNode | null;
382
+ modifiers?: ModifierName[] | number;
383
+ press?: boolean;
384
+ release?: boolean;
385
+ }
386
+
387
+ /**
388
+ * Synchronous input injection, through the X server rather than emitted on
389
+ * the ntk window — so grabs, focus and crossing events all happen for real.
390
+ * Wrap in `act()` to flush what it caused, or use `userEvent`, which does.
391
+ */
392
+ export const fireEvent: {
393
+ mouseMove(node: DrawnNode, options?: PointerOptions): void;
394
+ mouseEnter(node: DrawnNode, options?: PointerOptions): void;
395
+ mouseLeave(node: DrawnNode, options?: PointerOptions): void;
396
+ mouseDown(node: DrawnNode, options?: PointerOptions): void;
397
+ mouseUp(node: DrawnNode, options?: PointerOptions): void;
398
+ click(node: DrawnNode, options?: PointerOptions): void;
399
+ doubleClick(node: DrawnNode, options?: PointerOptions): void;
400
+ contextMenu(node: DrawnNode, options?: PointerOptions): void;
401
+ /** A scroll in notches; `smooth` takes fractions of one, as a touchpad
402
+ * measures them. */
403
+ wheel(
404
+ node: DrawnNode,
405
+ options?: PointerOptions & {
406
+ deltaX?: number;
407
+ deltaY?: number;
408
+ smooth?: boolean;
409
+ },
410
+ ): void;
411
+ key(keysym: number, options?: KeyOptions): void;
412
+ char(char: string, options?: KeyOptions): void;
413
+ screenClick(
414
+ x: number,
415
+ y: number,
416
+ options?: { target?: DrawnNode | null; button?: number },
417
+ ): void;
418
+ };
419
+
420
+ /** `fireEvent`, `act`-wrapped: the layer to reach for by default. */
421
+ export const userEvent: {
422
+ click(node: DrawnNode, options?: PointerOptions): Promise<void>;
423
+ doubleClick(node: DrawnNode, options?: PointerOptions): Promise<void>;
424
+ hover(node: DrawnNode, options?: PointerOptions): Promise<void>;
425
+ unhover(node: DrawnNode, options?: PointerOptions): Promise<void>;
426
+ /** A scroll in notches; `smooth` takes fractions of one, as a touchpad
427
+ * measures them. */
428
+ wheel(
429
+ node: DrawnNode,
430
+ options?: PointerOptions & {
431
+ deltaX?: number;
432
+ deltaY?: number;
433
+ smooth?: boolean;
434
+ },
435
+ ): Promise<void>;
436
+ /** Click to focus, then send each character as a real key. */
437
+ type(
438
+ node: DrawnNode,
439
+ text: string,
440
+ options?: { skipClick?: boolean },
441
+ ): Promise<void>;
442
+ tab(options?: { shift?: boolean; target?: DrawnNode | null }): Promise<void>;
443
+ key(keysym: number, options?: KeyOptions): Promise<void>;
444
+ /** Press outside every window of this root — how a menu is dismissed. */
445
+ clickOutside(options?: {
446
+ target?: DrawnNode | null;
447
+ button?: number;
448
+ }): Promise<void>;
449
+ };
450
+
451
+ /** A node's centre in screen coordinates. */
452
+ export function screenPointOf(
453
+ node: DrawnNode,
454
+ options?: { dx?: number; dy?: number },
455
+ ): { x: number; y: number; server: TestServer };
456
+
457
+ /** A screen point inside none of this root's windows. */
458
+ export function pointOutsideWindows(node: DrawnNode): {
459
+ x: number;
460
+ y: number;
461
+ };
462
+
463
+ export interface PixelOptions {
464
+ /** Per-channel tolerance; antialiasing means you always want one. */
465
+ tolerance?: number;
466
+ message?: string;
467
+ }
468
+
469
+ export type Colour = string | [number, number, number];
470
+
471
+ /** `[r, g, b]` at a window coordinate. */
472
+ export function pixelAt(
473
+ ctx: TestContext2D,
474
+ x: number,
475
+ y: number,
476
+ ): Promise<[number, number, number]>;
477
+
478
+ export function expectPixel(
479
+ ctx: TestContext2D,
480
+ x: number,
481
+ y: number,
482
+ want: Colour,
483
+ options?: PixelOptions,
484
+ ): Promise<[number, number, number]>;
485
+
486
+ export function waitForPixel(
487
+ ctx: TestContext2D,
488
+ x: number,
489
+ y: number,
490
+ want: Colour,
491
+ options?: PixelOptions & WaitOptions,
492
+ ): Promise<[number, number, number]>;
493
+
494
+ /** How many pixels in a region are near a colour. */
495
+ export function countPixels(
496
+ ctx: TestContext2D,
497
+ region: { x?: number; y?: number; width: number; height: number },
498
+ want: Colour,
499
+ tolerance?: number,
500
+ ): Promise<number>;
501
+
502
+ export function isNear(
503
+ rgb: [number, number, number],
504
+ want: Colour,
505
+ tolerance?: number,
506
+ ): boolean;
507
+
508
+ export function toRgb(colour: Colour): [number, number, number];
509
+
510
+ /**
511
+ * Write a PNG of the window, or return the bytes when `file` is null.
512
+ * Typed as `Uint8Array` rather than node's `Buffer` so these declarations
513
+ * do not drag in `@types/node` — a `Buffer` *is* one.
514
+ */
515
+ export function toPNG(
516
+ ctx: TestContext2D,
517
+ file: string | null,
518
+ region: { x?: number; y?: number; width: number; height: number },
519
+ ): Promise<Uint8Array>;
520
+
521
+ export interface FrameClock {
522
+ readonly now: number;
523
+ advance(ms: number): number;
524
+ set(ms: number): number;
525
+ restore(): void;
526
+ }
527
+
528
+ /**
529
+ * Drive transitions from a number you control. Install it **before** the
530
+ * render, so every timestamp in the tree comes from the same clock — mixing
531
+ * a real start time with a fake `now` gives a transition that never moves.
532
+ */
533
+ export function withFrameClock(startAt?: number): FrameClock;
534
+
535
+ /** Every window node in a tree, popups included. */
536
+ export function windowNodesOf(node: DrawnNode): DrawnNode[];
537
+
538
+ /** The fake ntk app the op-log tests use — `backend: 'mock'` builds one. */
539
+ export function createMockApp(): NtkApp;
540
+ export function moveMouse(window: unknown, x: number, y: number): void;
541
+ export function pressButton(
542
+ window: unknown,
543
+ x: number,
544
+ y: number,
545
+ options?: { press?: boolean; release?: boolean },
546
+ ): void;
547
+ /** A scroll at a point, in notches — ntk's `wheel` event. */
548
+ export function spinWheel(
549
+ window: unknown,
550
+ x: number,
551
+ y: number,
552
+ options?: {
553
+ deltaX?: number;
554
+ deltaY?: number;
555
+ smooth?: boolean;
556
+ buttons?: number;
557
+ },
558
+ ): void;
@@ -0,0 +1,85 @@
1
+ // `react-x11/test` — the supported way to test a react-x11 app.
2
+ //
3
+ // ```js
4
+ // import { test, afterEach } from 'node:test';
5
+ // import { renderX11, cleanup, screen, userEvent, expectPixel } from 'react-x11/test';
6
+ // import { XK_RETURN } from 'react-x11/keysyms';
7
+ //
8
+ // afterEach(cleanup);
9
+ //
10
+ // test('adding a task', async () => {
11
+ // const { ctx } = await renderX11(<Tasks />, {
12
+ // fonts: { 'sans-serif': '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf' },
13
+ // });
14
+ // await userEvent.type(screen.getByRole('textbox'), 'buy milk\n');
15
+ // screen.getByText('buy milk');
16
+ // await expectPixel(ctx, 12, 40, '#2980b9', { tolerance: 8 });
17
+ // });
18
+ // ```
19
+ //
20
+ // What makes this unusual: there is **no display**. `renderX11` starts
21
+ // node-x11's pure-JavaScript X server in this process, connects a real ntk
22
+ // client to it over a stream pair, and the test drives that server's own
23
+ // input machinery. No `$DISPLAY`, no xvfb, works on macOS, and the pixels
24
+ // are real pixels a real X server composited.
25
+
26
+ import { renderX11 as baseRenderX11, mountedEntries } from './harness.js';
27
+ import { screenFor, within } from './queries.js';
28
+ import { setDefaultTarget } from './events.js';
29
+
30
+ export {
31
+ act,
32
+ cleanup,
33
+ settle,
34
+ waitFor,
35
+ withFrameClock,
36
+ windowNodesOf,
37
+ } from './harness.js';
38
+
39
+ export { within, textOf, roleOf } from './queries.js';
40
+ export { installA11ySpy, nodeUtterance, utteranceOf } from './a11y.js';
41
+ export { inspect, ownerChainOf, sourceOf } from './components.js';
42
+ export {
43
+ fireEvent,
44
+ userEvent,
45
+ screenPointOf,
46
+ pointOutsideWindows,
47
+ } from './events.js';
48
+ export {
49
+ pixelAt,
50
+ expectPixel,
51
+ waitForPixel,
52
+ countPixels,
53
+ isNear,
54
+ toRgb,
55
+ toPNG,
56
+ } from './pixels.js';
57
+ export {
58
+ createMockApp,
59
+ moveMouse,
60
+ pressButton,
61
+ spinWheel,
62
+ } from './mock-app.js';
63
+ export * from '../keysyms.js';
64
+
65
+ /**
66
+ * Mount a tree and return a handle to it, plus the queries bound to it — so
67
+ * `const { getByText } = await renderX11(<App/>)` reads the way Testing
68
+ * Library does, and `screen` works for the common single-render case.
69
+ *
70
+ * See {@link ./harness.js} for the options.
71
+ */
72
+ export async function renderX11(element, options) {
73
+ const result = await baseRenderX11(element, options);
74
+ // fireEvent.key goes to whatever holds the X input focus, so it only needs
75
+ // a connection; the most recent render is the sensible default
76
+ setDefaultTarget(result.windowNode);
77
+ return Object.assign(result, within(result.windowNode));
78
+ }
79
+
80
+ /**
81
+ * The queries, bound to the most recent `renderX11` — including its popups,
82
+ * since a `<popup>` is a child node of the window that opened it and a menu
83
+ * or dialog is exactly what a test wants to query next.
84
+ */
85
+ export const screen = screenFor(() => mountedEntries().at(-1)?.windowNode);