@nativedesktop/test 0.4.1 → 0.4.3
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/package.json +3 -3
- package/src/attach.ts +157 -0
- package/src/errors.ts +121 -0
- package/src/expect.ts +274 -0
- package/src/fixtures/gestures-tree.json +291 -0
- package/src/index.ts +40 -2
- package/src/keyboard.ts +143 -0
- package/src/launch.ts +95 -1
- package/src/locator.test.ts +306 -0
- package/src/locator.ts +539 -0
- package/src/matcher.test.ts +83 -0
- package/src/matcher.ts +179 -0
- package/src/selectors.test.ts +93 -0
- package/src/selectors.ts +229 -0
- package/src/snapshot.ts +72 -0
- package/src/socket.ts +7 -1
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
{
|
|
2
|
+
"coordinateSpace": "logical-window-topleft",
|
|
3
|
+
"root": {
|
|
4
|
+
"ref": 11,
|
|
5
|
+
"type": "Window",
|
|
6
|
+
"testID": null,
|
|
7
|
+
"text": "ND Gestures",
|
|
8
|
+
"visible": true,
|
|
9
|
+
"geometry": {
|
|
10
|
+
"x": 0,
|
|
11
|
+
"y": 0,
|
|
12
|
+
"w": 640,
|
|
13
|
+
"h": 560
|
|
14
|
+
},
|
|
15
|
+
"children": [
|
|
16
|
+
{
|
|
17
|
+
"ref": 10,
|
|
18
|
+
"type": "Box",
|
|
19
|
+
"testID": null,
|
|
20
|
+
"text": null,
|
|
21
|
+
"visible": true,
|
|
22
|
+
"geometry": {
|
|
23
|
+
"x": 0,
|
|
24
|
+
"y": 32,
|
|
25
|
+
"w": 640,
|
|
26
|
+
"h": 528
|
|
27
|
+
},
|
|
28
|
+
"children": [
|
|
29
|
+
{
|
|
30
|
+
"ref": 1,
|
|
31
|
+
"type": "Slider",
|
|
32
|
+
"testID": "volume-slider",
|
|
33
|
+
"text": null,
|
|
34
|
+
"visible": true,
|
|
35
|
+
"geometry": {
|
|
36
|
+
"x": 16,
|
|
37
|
+
"y": 48,
|
|
38
|
+
"w": 608,
|
|
39
|
+
"h": 16
|
|
40
|
+
},
|
|
41
|
+
"children": [],
|
|
42
|
+
"itemCount": null,
|
|
43
|
+
"rows": null,
|
|
44
|
+
"role": "slider",
|
|
45
|
+
"enabled": true,
|
|
46
|
+
"focused": false,
|
|
47
|
+
"value": 20,
|
|
48
|
+
"checked": null,
|
|
49
|
+
"selected": null,
|
|
50
|
+
"expanded": null,
|
|
51
|
+
"placeholder": null,
|
|
52
|
+
"label": null,
|
|
53
|
+
"options": null
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"ref": 2,
|
|
57
|
+
"type": "Label",
|
|
58
|
+
"testID": "volume-label",
|
|
59
|
+
"text": "Volume: 20",
|
|
60
|
+
"visible": true,
|
|
61
|
+
"geometry": {
|
|
62
|
+
"x": 14,
|
|
63
|
+
"y": 72,
|
|
64
|
+
"w": 72,
|
|
65
|
+
"h": 16
|
|
66
|
+
},
|
|
67
|
+
"children": [],
|
|
68
|
+
"itemCount": null,
|
|
69
|
+
"rows": null,
|
|
70
|
+
"role": "label",
|
|
71
|
+
"enabled": true,
|
|
72
|
+
"focused": false,
|
|
73
|
+
"value": "Volume: 20",
|
|
74
|
+
"checked": null,
|
|
75
|
+
"selected": null,
|
|
76
|
+
"expanded": null,
|
|
77
|
+
"placeholder": null,
|
|
78
|
+
"label": null,
|
|
79
|
+
"options": null
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"ref": 3,
|
|
83
|
+
"type": "Checkbox",
|
|
84
|
+
"testID": "agree-check",
|
|
85
|
+
"text": "I agree",
|
|
86
|
+
"visible": true,
|
|
87
|
+
"geometry": {
|
|
88
|
+
"x": 16,
|
|
89
|
+
"y": 96,
|
|
90
|
+
"w": 62,
|
|
91
|
+
"h": 16
|
|
92
|
+
},
|
|
93
|
+
"children": [],
|
|
94
|
+
"itemCount": null,
|
|
95
|
+
"rows": null,
|
|
96
|
+
"role": "checkbox",
|
|
97
|
+
"enabled": true,
|
|
98
|
+
"focused": false,
|
|
99
|
+
"value": false,
|
|
100
|
+
"checked": false,
|
|
101
|
+
"selected": null,
|
|
102
|
+
"expanded": null,
|
|
103
|
+
"placeholder": null,
|
|
104
|
+
"label": null,
|
|
105
|
+
"options": null
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"ref": 4,
|
|
109
|
+
"type": "Label",
|
|
110
|
+
"testID": "agree-label",
|
|
111
|
+
"text": "Agreed: no",
|
|
112
|
+
"visible": true,
|
|
113
|
+
"geometry": {
|
|
114
|
+
"x": 14,
|
|
115
|
+
"y": 120,
|
|
116
|
+
"w": 70,
|
|
117
|
+
"h": 16
|
|
118
|
+
},
|
|
119
|
+
"children": [],
|
|
120
|
+
"itemCount": null,
|
|
121
|
+
"rows": null,
|
|
122
|
+
"role": "label",
|
|
123
|
+
"enabled": true,
|
|
124
|
+
"focused": false,
|
|
125
|
+
"value": "Agreed: no",
|
|
126
|
+
"checked": null,
|
|
127
|
+
"selected": null,
|
|
128
|
+
"expanded": null,
|
|
129
|
+
"placeholder": null,
|
|
130
|
+
"label": null,
|
|
131
|
+
"options": null
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"ref": 5,
|
|
135
|
+
"type": "TextInput",
|
|
136
|
+
"testID": "name-input",
|
|
137
|
+
"text": "",
|
|
138
|
+
"visible": true,
|
|
139
|
+
"geometry": {
|
|
140
|
+
"x": 16,
|
|
141
|
+
"y": 144,
|
|
142
|
+
"w": 608,
|
|
143
|
+
"h": 24
|
|
144
|
+
},
|
|
145
|
+
"children": [],
|
|
146
|
+
"itemCount": null,
|
|
147
|
+
"rows": null,
|
|
148
|
+
"role": "textbox",
|
|
149
|
+
"enabled": true,
|
|
150
|
+
"focused": false,
|
|
151
|
+
"value": "",
|
|
152
|
+
"checked": null,
|
|
153
|
+
"selected": null,
|
|
154
|
+
"expanded": null,
|
|
155
|
+
"placeholder": "Type here",
|
|
156
|
+
"label": null,
|
|
157
|
+
"options": null
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"ref": 6,
|
|
161
|
+
"type": "Label",
|
|
162
|
+
"testID": "echo-label",
|
|
163
|
+
"text": "Echo: ",
|
|
164
|
+
"visible": true,
|
|
165
|
+
"geometry": {
|
|
166
|
+
"x": 14,
|
|
167
|
+
"y": 176,
|
|
168
|
+
"w": 41,
|
|
169
|
+
"h": 16
|
|
170
|
+
},
|
|
171
|
+
"children": [],
|
|
172
|
+
"itemCount": null,
|
|
173
|
+
"rows": null,
|
|
174
|
+
"role": "label",
|
|
175
|
+
"enabled": true,
|
|
176
|
+
"focused": false,
|
|
177
|
+
"value": "Echo: ",
|
|
178
|
+
"checked": null,
|
|
179
|
+
"selected": null,
|
|
180
|
+
"expanded": null,
|
|
181
|
+
"placeholder": null,
|
|
182
|
+
"label": null,
|
|
183
|
+
"options": null
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"ref": 7,
|
|
187
|
+
"type": "Table",
|
|
188
|
+
"testID": "people-table",
|
|
189
|
+
"text": null,
|
|
190
|
+
"visible": true,
|
|
191
|
+
"geometry": {
|
|
192
|
+
"x": 16,
|
|
193
|
+
"y": 200,
|
|
194
|
+
"w": 608,
|
|
195
|
+
"h": 295
|
|
196
|
+
},
|
|
197
|
+
"children": [],
|
|
198
|
+
"itemCount": 3,
|
|
199
|
+
"rows": null,
|
|
200
|
+
"role": "table",
|
|
201
|
+
"enabled": true,
|
|
202
|
+
"focused": false,
|
|
203
|
+
"value": 3,
|
|
204
|
+
"checked": null,
|
|
205
|
+
"selected": null,
|
|
206
|
+
"expanded": null,
|
|
207
|
+
"placeholder": null,
|
|
208
|
+
"label": null,
|
|
209
|
+
"options": null
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"ref": 8,
|
|
213
|
+
"type": "Label",
|
|
214
|
+
"testID": "activated-label",
|
|
215
|
+
"text": "Activated: -1",
|
|
216
|
+
"visible": true,
|
|
217
|
+
"geometry": {
|
|
218
|
+
"x": 14,
|
|
219
|
+
"y": 504,
|
|
220
|
+
"w": 80,
|
|
221
|
+
"h": 16
|
|
222
|
+
},
|
|
223
|
+
"children": [],
|
|
224
|
+
"itemCount": null,
|
|
225
|
+
"rows": null,
|
|
226
|
+
"role": "label",
|
|
227
|
+
"enabled": true,
|
|
228
|
+
"focused": false,
|
|
229
|
+
"value": "Activated: -1",
|
|
230
|
+
"checked": null,
|
|
231
|
+
"selected": null,
|
|
232
|
+
"expanded": null,
|
|
233
|
+
"placeholder": null,
|
|
234
|
+
"label": null,
|
|
235
|
+
"options": null
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"ref": 9,
|
|
239
|
+
"type": "Label",
|
|
240
|
+
"testID": "hover-target",
|
|
241
|
+
"text": "Hover / right-click target",
|
|
242
|
+
"visible": true,
|
|
243
|
+
"geometry": {
|
|
244
|
+
"x": 14,
|
|
245
|
+
"y": 528,
|
|
246
|
+
"w": 152,
|
|
247
|
+
"h": 16
|
|
248
|
+
},
|
|
249
|
+
"children": [],
|
|
250
|
+
"itemCount": null,
|
|
251
|
+
"rows": null,
|
|
252
|
+
"role": "label",
|
|
253
|
+
"enabled": true,
|
|
254
|
+
"focused": false,
|
|
255
|
+
"value": "Hover / right-click target",
|
|
256
|
+
"checked": null,
|
|
257
|
+
"selected": null,
|
|
258
|
+
"expanded": null,
|
|
259
|
+
"placeholder": null,
|
|
260
|
+
"label": null,
|
|
261
|
+
"options": null
|
|
262
|
+
}
|
|
263
|
+
],
|
|
264
|
+
"itemCount": null,
|
|
265
|
+
"rows": null,
|
|
266
|
+
"role": "group",
|
|
267
|
+
"enabled": true,
|
|
268
|
+
"focused": false,
|
|
269
|
+
"value": null,
|
|
270
|
+
"checked": null,
|
|
271
|
+
"selected": null,
|
|
272
|
+
"expanded": null,
|
|
273
|
+
"placeholder": null,
|
|
274
|
+
"label": null,
|
|
275
|
+
"options": null
|
|
276
|
+
}
|
|
277
|
+
],
|
|
278
|
+
"itemCount": null,
|
|
279
|
+
"rows": null,
|
|
280
|
+
"role": "window",
|
|
281
|
+
"enabled": true,
|
|
282
|
+
"focused": false,
|
|
283
|
+
"value": null,
|
|
284
|
+
"checked": null,
|
|
285
|
+
"selected": null,
|
|
286
|
+
"expanded": null,
|
|
287
|
+
"placeholder": null,
|
|
288
|
+
"label": null,
|
|
289
|
+
"options": null
|
|
290
|
+
}
|
|
291
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @nativedesktop/test — §1.4: launchApp/AppHandle over the automation socket.
|
|
2
2
|
// socket.ts's AutomationClient is the repo's ONE copy of the wire framing;
|
|
3
3
|
// packages/mcp and the drive scripts import it from here rather than forking.
|
|
4
|
-
export { AutomationClient } from "./socket.ts";
|
|
4
|
+
export { AutomationClient, AutomationRpcError } from "./socket.ts";
|
|
5
5
|
export { launchApp, AppHandle, killAll } from "./launch.ts";
|
|
6
6
|
export type { LaunchOptions } from "./launch.ts";
|
|
7
7
|
|
|
@@ -22,6 +22,44 @@ export type { DialogScript } from "./dialogs.ts";
|
|
|
22
22
|
export { pngSize } from "./png.ts";
|
|
23
23
|
export type { PngSize } from "./png.ts";
|
|
24
24
|
|
|
25
|
+
// Locators: Playwright semantics over getTree plus the action RPCs. A Locator
|
|
26
|
+
// is lazy and re-resolves on every use, so a drive script never holds a ref
|
|
27
|
+
// across a re-render.
|
|
28
|
+
export { AttachedApp, connectApp } from "./attach.ts";
|
|
29
|
+
export { Locator, LocatorFactory, callHost } from "./locator.ts";
|
|
30
|
+
export type { ActionOptions, BoundingBox, LocatorClient, RoleOptions, TextOptions, WaitForState } from "./locator.ts";
|
|
31
|
+
export { expect, LocatorAssertions, ValueAssertions } from "./expect.ts";
|
|
32
|
+
export type { ExpectOptions } from "./expect.ts";
|
|
33
|
+
export { Keyboard, Mouse, toChord } from "./keyboard.ts";
|
|
34
|
+
export { LocatorError, StrictModeError, TimeoutError, describeNode, rankCandidates } from "./errors.ts";
|
|
35
|
+
export { formatSelector, parseSelector } from "./selectors.ts";
|
|
36
|
+
export type { SelectorPart, TextSpec } from "./selectors.ts";
|
|
37
|
+
export {
|
|
38
|
+
allNodes,
|
|
39
|
+
asNdNode,
|
|
40
|
+
hasRealSize,
|
|
41
|
+
matchPart,
|
|
42
|
+
nodeChecked,
|
|
43
|
+
nodeName,
|
|
44
|
+
nodeText,
|
|
45
|
+
renderValue,
|
|
46
|
+
selectNodes,
|
|
47
|
+
subtreeText,
|
|
48
|
+
} from "./matcher.ts";
|
|
49
|
+
export type { NdNode } from "./matcher.ts";
|
|
50
|
+
export { renderSnapshot } from "./snapshot.ts";
|
|
51
|
+
export type { SnapshotOptions } from "./snapshot.ts";
|
|
52
|
+
|
|
25
53
|
// Tree types a caller needs to type find()/tree() results, re-exported so
|
|
26
54
|
// nothing outside this package has to import @nativedesktop/react/rpc itself.
|
|
27
|
-
export type {
|
|
55
|
+
export type {
|
|
56
|
+
Geometry,
|
|
57
|
+
GetTreeResult,
|
|
58
|
+
JsonNode,
|
|
59
|
+
ScreenshotResult,
|
|
60
|
+
SetWindowFrameParams,
|
|
61
|
+
WaitCondition,
|
|
62
|
+
WaitForResult,
|
|
63
|
+
WindowInfo,
|
|
64
|
+
WindowsResult,
|
|
65
|
+
} from "@nativedesktop/react/rpc";
|
package/src/keyboard.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Playwright's Keyboard and Mouse over the `keys`, `pointer` and `drag` RPCs.
|
|
2
|
+
//
|
|
3
|
+
// The host chord spec takes lowercase modifier tokens (cmd/ctrl/shift/alt)
|
|
4
|
+
// plus one key token, and lowercases every part it parses, so an uppercase
|
|
5
|
+
// letter has to arrive as an explicit shift. Names outside the host's table
|
|
6
|
+
// (Home/End/PageUp/PageDown/F1-F12) are passed through lowercased rather than
|
|
7
|
+
// rejected here, so the client does not have to ship a new release when the
|
|
8
|
+
// host's table grows.
|
|
9
|
+
import type { LocatorClient } from "./locator.ts";
|
|
10
|
+
|
|
11
|
+
const MODIFIERS: Record<string, string> = {
|
|
12
|
+
meta: "cmd",
|
|
13
|
+
cmd: "cmd",
|
|
14
|
+
command: "cmd",
|
|
15
|
+
control: "ctrl",
|
|
16
|
+
ctrl: "ctrl",
|
|
17
|
+
shift: "shift",
|
|
18
|
+
alt: "alt",
|
|
19
|
+
option: "alt",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const KEY_NAMES: Record<string, string> = {
|
|
23
|
+
arrowleft: "left",
|
|
24
|
+
arrowright: "right",
|
|
25
|
+
arrowup: "up",
|
|
26
|
+
arrowdown: "down",
|
|
27
|
+
esc: "escape",
|
|
28
|
+
escape: "escape",
|
|
29
|
+
enter: "enter",
|
|
30
|
+
return: "enter",
|
|
31
|
+
tab: "tab",
|
|
32
|
+
space: "space",
|
|
33
|
+
" ": "space",
|
|
34
|
+
backspace: "backspace",
|
|
35
|
+
delete: "delete",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** "Meta+Shift+A" -> "cmd+shift+a"; "ArrowDown" -> "down"; "a" -> "a". */
|
|
39
|
+
export function toChord(key: string): string {
|
|
40
|
+
const parts = key.split("+").filter((p) => p.length > 0);
|
|
41
|
+
const mods: string[] = [];
|
|
42
|
+
let token = "";
|
|
43
|
+
for (const raw of parts) {
|
|
44
|
+
const mod = MODIFIERS[raw.toLowerCase()];
|
|
45
|
+
if (mod) {
|
|
46
|
+
if (!mods.includes(mod)) mods.push(mod);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
token = raw;
|
|
50
|
+
}
|
|
51
|
+
if (!token) throw new Error(`press(${JSON.stringify(key)}): no key left after the modifiers`);
|
|
52
|
+
if (token.length === 1 && token !== token.toLowerCase()) {
|
|
53
|
+
if (!mods.includes("shift")) mods.push("shift");
|
|
54
|
+
token = token.toLowerCase();
|
|
55
|
+
}
|
|
56
|
+
const named = KEY_NAMES[token.toLowerCase()];
|
|
57
|
+
const resolved = named ?? (token.length === 1 ? token : token.toLowerCase());
|
|
58
|
+
return mods.length ? [...mods, resolved].join("+") : resolved;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class Keyboard {
|
|
62
|
+
constructor(
|
|
63
|
+
private readonly client: LocatorClient,
|
|
64
|
+
private readonly window?: number,
|
|
65
|
+
) {}
|
|
66
|
+
|
|
67
|
+
/** One chord, in Playwright key names: press("Meta+A"), press("Escape"). */
|
|
68
|
+
async press(key: string): Promise<void> {
|
|
69
|
+
await this.client.callRpc("keys", { keys: toChord(key), window: this.window });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Types the literal text into the focused widget, one keystroke per
|
|
73
|
+
* character. A single named key would be read as a chord by the host, so
|
|
74
|
+
* route those through press(). */
|
|
75
|
+
async type(text: string): Promise<void> {
|
|
76
|
+
if (!text) return;
|
|
77
|
+
await this.client.callRpc("keys", { keys: text, window: this.window });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
insertText(text: string): Promise<void> {
|
|
81
|
+
return this.type(text);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface MouseButtonOptions {
|
|
86
|
+
button?: "left" | "right";
|
|
87
|
+
clickCount?: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class Mouse {
|
|
91
|
+
private x = 0;
|
|
92
|
+
private y = 0;
|
|
93
|
+
|
|
94
|
+
constructor(
|
|
95
|
+
private readonly client: LocatorClient,
|
|
96
|
+
private readonly window?: number,
|
|
97
|
+
) {}
|
|
98
|
+
|
|
99
|
+
async move(x: number, y: number): Promise<void> {
|
|
100
|
+
this.x = x;
|
|
101
|
+
this.y = y;
|
|
102
|
+
await this.client.callRpc("pointer", { phase: "move", x, y, window: this.window });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async down(opts: MouseButtonOptions = {}): Promise<void> {
|
|
106
|
+
await this.client.callRpc("pointer", { phase: "down", x: this.x, y: this.y, ...opts, window: this.window });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async up(opts: MouseButtonOptions = {}): Promise<void> {
|
|
110
|
+
await this.client.callRpc("pointer", { phase: "up", x: this.x, y: this.y, ...opts, window: this.window });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async click(x: number, y: number, opts: MouseButtonOptions = {}): Promise<void> {
|
|
114
|
+
this.x = x;
|
|
115
|
+
this.y = y;
|
|
116
|
+
await this.down(opts);
|
|
117
|
+
await this.up(opts);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async dblclick(x: number, y: number, opts: MouseButtonOptions = {}): Promise<void> {
|
|
121
|
+
await this.click(x, y, opts);
|
|
122
|
+
await this.click(x, y, { ...opts, clickCount: 2 });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** One posted down/dragged/up batch, so a native tracking loop consumes it
|
|
126
|
+
* as a real gesture. */
|
|
127
|
+
async dragTo(
|
|
128
|
+
from: { x: number; y: number },
|
|
129
|
+
to: { x: number; y: number },
|
|
130
|
+
opts: { steps?: number; durationMs?: number; button?: "left" | "right" } = {},
|
|
131
|
+
): Promise<void> {
|
|
132
|
+
this.x = to.x;
|
|
133
|
+
this.y = to.y;
|
|
134
|
+
await this.client.callRpc("drag", {
|
|
135
|
+
fromX: from.x,
|
|
136
|
+
fromY: from.y,
|
|
137
|
+
toX: to.x,
|
|
138
|
+
toY: to.y,
|
|
139
|
+
...opts,
|
|
140
|
+
window: this.window,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
package/src/launch.ts
CHANGED
|
@@ -20,15 +20,26 @@ import {
|
|
|
20
20
|
type ScreenshotResult,
|
|
21
21
|
type ScrollResult,
|
|
22
22
|
type SetValueResult,
|
|
23
|
+
type SetWindowFrameParams,
|
|
23
24
|
type TypeResult,
|
|
24
25
|
type WaitCondition,
|
|
25
26
|
type WaitForResult,
|
|
26
27
|
type WebViewEvalResult,
|
|
27
28
|
type WebViewInfo,
|
|
29
|
+
type WindowInfo,
|
|
28
30
|
type WindowsResult,
|
|
29
31
|
} from "@nativedesktop/react/rpc";
|
|
30
32
|
import { TimedClient } from "./client.ts";
|
|
31
33
|
import { type DialogScript, dialogScriptEnv } from "./dialogs.ts";
|
|
34
|
+
import type { Keyboard, Mouse } from "./keyboard.ts";
|
|
35
|
+
import {
|
|
36
|
+
callHost,
|
|
37
|
+
Locator,
|
|
38
|
+
LocatorFactory,
|
|
39
|
+
type LocatorClient,
|
|
40
|
+
type RoleOptions,
|
|
41
|
+
type TextOptions,
|
|
42
|
+
} from "./locator.ts";
|
|
32
43
|
import { findAllNodes, findMatchingNode, findNode, resolveTarget, type Target } from "./query.ts";
|
|
33
44
|
import { registerProcess, unregisterProcess } from "./registry.ts";
|
|
34
45
|
import { type ScreenshotOptions, takeScreenshot } from "./screenshot.ts";
|
|
@@ -64,6 +75,7 @@ export interface LaunchOptions {
|
|
|
64
75
|
const DEFAULT_READY_MARKERS = ["ND_AUTOMATION_LISTENING", "ND_COMMIT_APPLIED"];
|
|
65
76
|
const DEFAULT_READY_TIMEOUT_MS = 20_000;
|
|
66
77
|
const DEFAULT_RPC_TIMEOUT_MS = 8_000;
|
|
78
|
+
const DEFAULT_ACTION_TIMEOUT_MS = 5_000;
|
|
67
79
|
const DEFAULT_RETRIES = 2;
|
|
68
80
|
const MAX_STDERR_LINES = 5000;
|
|
69
81
|
const STDERR_DRAIN_GRACE_MS = 1000;
|
|
@@ -168,9 +180,13 @@ interface LiveHost {
|
|
|
168
180
|
exitedPromise: Promise<number>;
|
|
169
181
|
}
|
|
170
182
|
|
|
171
|
-
export class AppHandle {
|
|
183
|
+
export class AppHandle implements LocatorClient {
|
|
184
|
+
/** Deadline every locator action, reader and expect matcher inherits. */
|
|
185
|
+
actionTimeout = DEFAULT_ACTION_TIMEOUT_MS;
|
|
186
|
+
|
|
172
187
|
private live?: LiveHost;
|
|
173
188
|
private client?: TimedClient;
|
|
189
|
+
private factory?: LocatorFactory;
|
|
174
190
|
|
|
175
191
|
private constructor(private readonly config: ResolvedConfig) {}
|
|
176
192
|
|
|
@@ -346,6 +362,84 @@ export class AppHandle {
|
|
|
346
362
|
return tail(this.live?.stderrLines ?? [], n);
|
|
347
363
|
}
|
|
348
364
|
|
|
365
|
+
isAlive(): boolean {
|
|
366
|
+
return Boolean(this.live) && !this.live!.exited;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// --- locators --------------------------------------------------------------
|
|
370
|
+
|
|
371
|
+
/** Untyped RPC entry point the Locator layer dispatches through, so one
|
|
372
|
+
* call site serves every action method. Typed calls should keep going
|
|
373
|
+
* through `rpc.call`. */
|
|
374
|
+
callRpc(method: string, params?: Record<string, unknown>): Promise<unknown> {
|
|
375
|
+
return this.rpc.call(method as "click", params as never);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private get locators(): LocatorFactory {
|
|
379
|
+
return (this.factory ??= new LocatorFactory(this));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
get keyboard(): Keyboard {
|
|
383
|
+
return this.locators.keyboard;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
get mouse(): Mouse {
|
|
387
|
+
return this.locators.mouse;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
locator(selector: string): Locator {
|
|
391
|
+
return this.locators.locator(selector);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
getByTestId(testId: string): Locator {
|
|
395
|
+
return this.locators.getByTestId(testId);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
getByRole(role: string, opts?: RoleOptions): Locator {
|
|
399
|
+
return this.locators.getByRole(role, opts);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
getByText(text: string | RegExp, opts?: TextOptions): Locator {
|
|
403
|
+
return this.locators.getByText(text, opts);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
getByLabel(text: string | RegExp, opts?: TextOptions): Locator {
|
|
407
|
+
return this.locators.getByLabel(text, opts);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
getByPlaceholder(text: string | RegExp, opts?: TextOptions): Locator {
|
|
411
|
+
return this.locators.getByPlaceholder(text, opts);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** A locator factory, keyboard and mouse bound to one window: by index in
|
|
415
|
+
* `windows()` order, or by a substring of its title. */
|
|
416
|
+
async window(titleOrIndex: string | number): Promise<LocatorFactory> {
|
|
417
|
+
const { windows } = await this.windows();
|
|
418
|
+
const info =
|
|
419
|
+
typeof titleOrIndex === "number"
|
|
420
|
+
? windows[titleOrIndex]
|
|
421
|
+
: windows.find((w) => (w.title ?? "").includes(titleOrIndex));
|
|
422
|
+
if (!info) {
|
|
423
|
+
const known = windows.map((w) => JSON.stringify(w.title ?? "")).join(", ") || "(none)";
|
|
424
|
+
throw new Error(`window(${JSON.stringify(titleOrIndex)}): no such window. Open windows: ${known}`);
|
|
425
|
+
}
|
|
426
|
+
return new LocatorFactory(this, info.ref);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Resizes a window in logical units, keeping its origin. The answer is
|
|
430
|
+
* that window's updated WindowInfo, whose `geometry` is the same w/h
|
|
431
|
+
* Geometry a node carries. */
|
|
432
|
+
async setWindowSize(width: number, height: number, opts: { window?: number } = {}): Promise<WindowInfo> {
|
|
433
|
+
return (await callHost(this, "setWindowFrame", { window: opts.window, width, height })) as WindowInfo;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Moves and/or resizes a window; omitted components keep their current
|
|
437
|
+
* value. GTK ignores x/y (client-side placement is not a Wayland
|
|
438
|
+
* capability). */
|
|
439
|
+
async setWindowFrame(frame: SetWindowFrameParams): Promise<WindowInfo> {
|
|
440
|
+
return (await callHost(this, "setWindowFrame", { ...frame })) as WindowInfo;
|
|
441
|
+
}
|
|
442
|
+
|
|
349
443
|
// --- tree queries ----------------------------------------------------------
|
|
350
444
|
|
|
351
445
|
async tree(window?: number): Promise<GetTreeResult> {
|