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.
- package/README.md +330 -81
- package/package.json +132 -14
- package/src/ClickToComponent.js +9 -2
- package/src/DevToolsIntegration.js +440 -32
- package/src/Reconciler.js +463 -165
- package/src/a11y.js +1299 -0
- package/src/acceleratorhooks.js +86 -0
- package/src/accelerators.js +128 -0
- package/src/activate.js +168 -0
- package/src/anchor.js +360 -0
- package/src/appcontext.js +161 -0
- package/src/appearance.js +748 -0
- package/src/appearancehooks.js +96 -0
- package/src/apphooks.js +73 -0
- package/src/application.js +748 -0
- package/src/atspi.js +1953 -0
- package/src/bus.js +545 -0
- package/src/bushooks.js +136 -0
- package/src/clientmessage.js +140 -0
- package/src/clipboard.js +121 -0
- package/src/components/Button.js +148 -33
- package/src/components/Checkbox.js +66 -32
- package/src/components/Dialog.js +96 -29
- package/src/components/FileDialog.js +491 -0
- package/src/components/Icon.js +329 -0
- package/src/components/Menu.js +797 -133
- package/src/components/PasswordInput.js +434 -0
- package/src/components/ProgressBar.js +91 -12
- package/src/components/Radio.js +78 -35
- package/src/components/Select.js +255 -65
- package/src/components/Slider.js +102 -41
- package/src/components/SplitPane.js +206 -0
- package/src/components/Switch.js +100 -22
- package/src/components/Table.js +529 -0
- package/src/components/Tabs.js +211 -0
- package/src/components/Tooltip.js +528 -54
- package/src/components/anchor.js +154 -107
- package/src/components/change.js +34 -0
- package/src/components/dnd.js +103 -0
- package/src/components/index.js +16 -3
- package/src/components/keys.js +17 -20
- package/src/components/scribble.js +247 -0
- package/src/components/theme.js +350 -42
- package/src/components/typeahead.js +12 -2
- package/src/compose.js +868 -0
- package/src/compositing.js +224 -0
- package/src/dbusmenu.js +384 -0
- package/src/debug.d.ts +63 -0
- package/src/debug.js +642 -0
- package/src/decorations.js +486 -0
- package/src/desktopsettings.js +209 -0
- package/src/desktopsettingshooks.js +62 -0
- package/src/dnd.js +1725 -0
- package/src/editmenu.js +272 -0
- package/src/errors.js +98 -0
- package/src/events.js +1406 -162
- package/src/extensions.js +45 -0
- package/src/filedialog.js +375 -0
- package/src/filedialoghooks.js +132 -0
- package/src/fonthooks.js +64 -0
- package/src/fonts.js +301 -0
- package/src/foreignnodes.js +519 -0
- package/src/frame/child.js +33 -0
- package/src/frame/childmain.js +274 -0
- package/src/frame/env.js +140 -0
- package/src/frame/index.js +444 -0
- package/src/frame/lifecycle.js +67 -0
- package/src/frame/protocol.js +179 -0
- package/src/frames.js +73 -0
- package/src/glbackend.js +114 -0
- package/src/glnodes.js +125 -67
- package/src/globalmenu.js +683 -0
- package/src/host.d.ts +88 -0
- package/src/host.js +42 -0
- package/src/idle.js +490 -0
- package/src/idlehooks.js +100 -0
- package/src/imagesource.js +349 -0
- package/src/index.d.ts +475 -0
- package/src/index.js +83 -9
- package/src/inputtime.js +206 -0
- package/src/jsx-dev-runtime.d.ts +21 -0
- package/src/jsx-dev-runtime.js +2 -0
- package/src/jsx-runtime.d.ts +39 -0
- package/src/jsx-runtime.js +6 -0
- package/src/keyboard.js +256 -0
- package/src/keyboardstate.js +278 -0
- package/src/keyboardstatehooks.js +58 -0
- package/src/keysyms.d.ts +149 -0
- package/src/keysyms.js +270 -0
- package/src/locale.js +170 -0
- package/src/localehooks.js +47 -0
- package/src/menuitem.js +223 -0
- package/src/node.d.ts +597 -0
- package/src/node.js +44 -0
- package/src/nodes.js +9546 -690
- package/src/ntk.d.ts +44 -0
- package/src/ntk.js +25 -0
- package/src/paintcache.js +366 -0
- package/src/palette.js +380 -0
- package/src/pastestate.js +66 -0
- package/src/portal.js +461 -0
- package/src/priority.js +26 -0
- package/src/refresh/index.d.ts +40 -0
- package/src/refresh/index.js +122 -0
- package/src/refresh/loader.d.ts +37 -0
- package/src/refresh/loader.js +401 -0
- package/src/refresh/register.d.ts +5 -0
- package/src/refresh/register.js +13 -0
- package/src/registry.js +232 -0
- package/src/scale.js +626 -0
- package/src/scalehooks.js +27 -0
- package/src/screencolor.js +640 -0
- package/src/screencolorhooks.js +101 -0
- package/src/screens.js +754 -0
- package/src/screenshooks.js +137 -0
- package/src/startup.js +302 -0
- package/src/style.d.ts +126 -0
- package/src/style.js +33 -0
- package/src/styles.js +1482 -6
- package/src/svgnodes.js +306 -0
- package/src/testing/a11y.js +484 -0
- package/src/testing/components.js +414 -0
- package/src/testing/events.js +407 -0
- package/src/testing/harness.js +455 -0
- package/src/testing/index.d.ts +558 -0
- package/src/testing/index.js +85 -0
- package/src/testing/mock-app.js +463 -0
- package/src/testing/pixels.js +152 -0
- package/src/testing/queries.js +224 -0
- package/src/textrange.js +83 -0
- package/src/textselection.js +439 -0
- package/src/trace-registry.js +63 -0
- package/src/transfer.js +93 -0
- package/src/types/appearance.d.ts +84 -0
- package/src/types/application.d.ts +173 -0
- package/src/types/components.d.ts +820 -0
- package/src/types/dbus.d.ts +177 -0
- package/src/types/elements.d.ts +897 -0
- package/src/types/events.d.ts +549 -0
- package/src/types/filedialog.d.ts +260 -0
- package/src/types/fonts.d.ts +124 -0
- package/src/types/frame.d.ts +146 -0
- package/src/types/globalmenu.d.ts +42 -0
- package/src/types/nodes.d.ts +199 -0
- package/src/types/screencolor.d.ts +84 -0
- package/src/types/style.d.ts +434 -0
- package/src/types/system.d.ts +287 -0
- package/src/windowid.js +151 -0
- package/src/windowstate.js +393 -0
- package/src/xsettings.js +336 -0
- package/src/yoga.d.ts +55 -0
- package/src/yoga.js +135 -0
- package/src/components/Canvas3D.js +0 -28
- package/src/geometry3d.js +0 -223
- package/src/pointer3d.js +0 -158
- package/src/raycast3d.js +0 -146
- package/src/richnodes.js +0 -436
- 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);
|