@ghia/native 0.0.0 → 0.11.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/LICENSE +201 -0
- package/browser.mjs +7 -0
- package/dist/aria.d.ts +72 -0
- package/dist/aria.d.ts.map +1 -0
- package/dist/aria.js +2 -0
- package/dist/aria.js.map +1 -0
- package/dist/automation/client.d.ts +243 -0
- package/dist/automation/client.d.ts.map +1 -0
- package/dist/automation/client.js +801 -0
- package/dist/automation/client.js.map +1 -0
- package/dist/automation/index.d.ts +5 -0
- package/dist/automation/index.d.ts.map +1 -0
- package/dist/automation/index.js +4 -0
- package/dist/automation/index.js.map +1 -0
- package/dist/automation/protocol.d.ts +309 -0
- package/dist/automation/protocol.d.ts.map +1 -0
- package/dist/automation/protocol.js +320 -0
- package/dist/automation/protocol.js.map +1 -0
- package/dist/blob-images.d.ts +32 -0
- package/dist/blob-images.d.ts.map +1 -0
- package/dist/blob-images.js +55 -0
- package/dist/blob-images.js.map +1 -0
- package/dist/bounds.d.ts +61 -0
- package/dist/bounds.d.ts.map +1 -0
- package/dist/bounds.js +85 -0
- package/dist/bounds.js.map +1 -0
- package/dist/clipboard-global.d.ts +11 -0
- package/dist/clipboard-global.d.ts.map +1 -0
- package/dist/clipboard-global.js +2 -0
- package/dist/clipboard-global.js.map +1 -0
- package/dist/clipboard.d.ts +19 -0
- package/dist/clipboard.d.ts.map +1 -0
- package/dist/clipboard.js +41 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/cpu-throttle.d.ts +6 -0
- package/dist/cpu-throttle.d.ts.map +1 -0
- package/dist/cpu-throttle.js +42 -0
- package/dist/cpu-throttle.js.map +1 -0
- package/dist/events/data-transfer.d.ts +23 -0
- package/dist/events/data-transfer.d.ts.map +1 -0
- package/dist/events/data-transfer.js +97 -0
- package/dist/events/data-transfer.js.map +1 -0
- package/dist/events/dispatch.d.ts +144 -0
- package/dist/events/dispatch.d.ts.map +1 -0
- package/dist/events/dispatch.js +241 -0
- package/dist/events/dispatch.js.map +1 -0
- package/dist/events/drag.d.ts +21 -0
- package/dist/events/drag.d.ts.map +1 -0
- package/dist/events/drag.js +111 -0
- package/dist/events/drag.js.map +1 -0
- package/dist/events/event-props.d.ts +19 -0
- package/dist/events/event-props.d.ts.map +1 -0
- package/dist/events/event-props.js +104 -0
- package/dist/events/event-props.js.map +1 -0
- package/dist/events/keyboard-defaults.d.ts +18 -0
- package/dist/events/keyboard-defaults.d.ts.map +1 -0
- package/dist/events/keyboard-defaults.js +121 -0
- package/dist/events/keyboard-defaults.js.map +1 -0
- package/dist/events/listeners.d.ts +18 -0
- package/dist/events/listeners.d.ts.map +1 -0
- package/dist/events/listeners.js +49 -0
- package/dist/events/listeners.js.map +1 -0
- package/dist/events/route.d.ts +17 -0
- package/dist/events/route.d.ts.map +1 -0
- package/dist/events/route.js +36 -0
- package/dist/events/route.js.map +1 -0
- package/dist/events/types.d.ts +269 -0
- package/dist/events/types.d.ts.map +1 -0
- package/dist/events/types.js +2 -0
- package/dist/events/types.js.map +1 -0
- package/dist/events/window-target.d.ts +120 -0
- package/dist/events/window-target.d.ts.map +1 -0
- package/dist/events/window-target.js +311 -0
- package/dist/events/window-target.js.map +1 -0
- package/dist/eyedropper.d.ts +33 -0
- package/dist/eyedropper.d.ts.map +1 -0
- package/dist/eyedropper.js +66 -0
- package/dist/eyedropper.js.map +1 -0
- package/dist/focus.d.ts +18 -0
- package/dist/focus.d.ts.map +1 -0
- package/dist/focus.js +26 -0
- package/dist/focus.js.map +1 -0
- package/dist/fonts.d.ts +12 -0
- package/dist/fonts.d.ts.map +1 -0
- package/dist/fonts.js +17 -0
- package/dist/fonts.js.map +1 -0
- package/dist/forms.d.ts +10 -0
- package/dist/forms.d.ts.map +1 -0
- package/dist/forms.js +9 -0
- package/dist/forms.js.map +1 -0
- package/dist/host-runtime.d.ts +108 -0
- package/dist/host-runtime.d.ts.map +1 -0
- package/dist/host-runtime.js +213 -0
- package/dist/host-runtime.js.map +1 -0
- package/dist/host.d.ts +2403 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +30 -0
- package/dist/host.js.map +1 -0
- package/dist/idle-gc.d.ts +36 -0
- package/dist/idle-gc.d.ts.map +1 -0
- package/dist/idle-gc.js +70 -0
- package/dist/idle-gc.js.map +1 -0
- package/dist/keyframes.d.ts +108 -0
- package/dist/keyframes.d.ts.map +1 -0
- package/dist/keyframes.js +107 -0
- package/dist/keyframes.js.map +1 -0
- package/dist/layout-animation.d.ts +38 -0
- package/dist/layout-animation.d.ts.map +1 -0
- package/dist/layout-animation.js +41 -0
- package/dist/layout-animation.js.map +1 -0
- package/dist/match-media.d.ts +62 -0
- package/dist/match-media.d.ts.map +1 -0
- package/dist/match-media.js +272 -0
- package/dist/match-media.js.map +1 -0
- package/dist/media-query.d.ts +21 -0
- package/dist/media-query.d.ts.map +1 -0
- package/dist/media-query.js +599 -0
- package/dist/media-query.js.map +1 -0
- package/dist/motion-bindings.d.ts +36 -0
- package/dist/motion-bindings.d.ts.map +1 -0
- package/dist/motion-bindings.js +107 -0
- package/dist/motion-bindings.js.map +1 -0
- package/dist/motion-config.d.ts +20 -0
- package/dist/motion-config.d.ts.map +1 -0
- package/dist/motion-config.js +51 -0
- package/dist/motion-config.js.map +1 -0
- package/dist/motion-span.d.ts +57 -0
- package/dist/motion-span.d.ts.map +1 -0
- package/dist/motion-span.js +150 -0
- package/dist/motion-span.js.map +1 -0
- package/dist/motion-value.d.ts +269 -0
- package/dist/motion-value.d.ts.map +1 -0
- package/dist/motion-value.js +941 -0
- package/dist/motion-value.js.map +1 -0
- package/dist/motion-variants.d.ts +190 -0
- package/dist/motion-variants.d.ts.map +1 -0
- package/dist/motion-variants.js +249 -0
- package/dist/motion-variants.js.map +1 -0
- package/dist/mutations.d.ts +65 -0
- package/dist/mutations.d.ts.map +1 -0
- package/dist/mutations.js +320 -0
- package/dist/mutations.js.map +1 -0
- package/dist/nativeview.d.ts +10 -0
- package/dist/nativeview.d.ts.map +1 -0
- package/dist/nativeview.js +92 -0
- package/dist/nativeview.js.map +1 -0
- package/dist/observers.d.ts +92 -0
- package/dist/observers.d.ts.map +1 -0
- package/dist/observers.js +579 -0
- package/dist/observers.js.map +1 -0
- package/dist/presence.d.ts +63 -0
- package/dist/presence.d.ts.map +1 -0
- package/dist/presence.js +54 -0
- package/dist/presence.js.map +1 -0
- package/dist/renderer-state.d.ts +155 -0
- package/dist/renderer-state.d.ts.map +1 -0
- package/dist/renderer-state.js +302 -0
- package/dist/renderer-state.js.map +1 -0
- package/dist/reorder.d.ts +63 -0
- package/dist/reorder.d.ts.map +1 -0
- package/dist/reorder.js +303 -0
- package/dist/reorder.js.map +1 -0
- package/dist/runtime.d.ts +23 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +157 -0
- package/dist/runtime.js.map +1 -0
- package/dist/screen.d.ts +8 -0
- package/dist/screen.d.ts.map +1 -0
- package/dist/screen.js +16 -0
- package/dist/screen.js.map +1 -0
- package/dist/scroll.d.ts +79 -0
- package/dist/scroll.d.ts.map +1 -0
- package/dist/scroll.js +170 -0
- package/dist/scroll.js.map +1 -0
- package/dist/style-images.d.ts +20 -0
- package/dist/style-images.d.ts.map +1 -0
- package/dist/style-images.js +57 -0
- package/dist/style-images.js.map +1 -0
- package/dist/svg.d.ts +16 -0
- package/dist/svg.d.ts.map +1 -0
- package/dist/svg.js +119 -0
- package/dist/svg.js.map +1 -0
- package/dist/testing.d.ts +811 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +1187 -0
- package/dist/testing.js.map +1 -0
- package/dist/text-match.d.ts +8 -0
- package/dist/text-match.d.ts.map +1 -0
- package/dist/text-match.js +10 -0
- package/dist/text-match.js.map +1 -0
- package/dist/text-selection.d.ts +34 -0
- package/dist/text-selection.d.ts.map +1 -0
- package/dist/text-selection.js +23 -0
- package/dist/text-selection.js.map +1 -0
- package/dist/universal-style.d.ts +22 -0
- package/dist/universal-style.d.ts.map +1 -0
- package/dist/universal-style.js +58 -0
- package/dist/universal-style.js.map +1 -0
- package/dist/web-animations.d.ts +300 -0
- package/dist/web-animations.d.ts.map +1 -0
- package/dist/web-animations.js +1032 -0
- package/dist/web-animations.js.map +1 -0
- package/dist/webview.d.ts +39 -0
- package/dist/webview.d.ts.map +1 -0
- package/dist/webview.js +26 -0
- package/dist/webview.js.map +1 -0
- package/gpuix-native.darwin-arm64.node +4 -0
- package/index.cjs +789 -0
- package/index.d.ts +2603 -0
- package/index.js +729 -0
- package/package.json +96 -4
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
/// Playwright-like automation client for GPUIX.
|
|
2
|
+
///
|
|
3
|
+
/// In-process tests talk to TestRenderer through the same typed method catalog
|
|
4
|
+
/// as a live app on SSE stdin/stdout. Locators query the retained tree.
|
|
5
|
+
import { AutomationError, createSseDecoder, encodeSse, methods, parseResponse, parseWireMessage, PROTOCOL_VERSION, } from "./protocol.js";
|
|
6
|
+
import { matchableText } from "../text-match.js";
|
|
7
|
+
function importNodeModule(specifier) {
|
|
8
|
+
return import(specifier);
|
|
9
|
+
}
|
|
10
|
+
class ValidatedAutomationBackend {
|
|
11
|
+
closed = false;
|
|
12
|
+
async call(method, params) {
|
|
13
|
+
if (this.closed) {
|
|
14
|
+
throw new AutomationError("Closed", "Automation session is closed");
|
|
15
|
+
}
|
|
16
|
+
const parsedParams = methods[method].params.parse(params);
|
|
17
|
+
const result = await this.request(method, parsedParams);
|
|
18
|
+
return methods[method].result.parse(result);
|
|
19
|
+
}
|
|
20
|
+
closeSession() {
|
|
21
|
+
if (this.closed)
|
|
22
|
+
return false;
|
|
23
|
+
this.closed = true;
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class InProcessBackend extends ValidatedAutomationBackend {
|
|
28
|
+
renderer;
|
|
29
|
+
constructor(renderer) {
|
|
30
|
+
super();
|
|
31
|
+
this.renderer = renderer;
|
|
32
|
+
}
|
|
33
|
+
request(method, params) {
|
|
34
|
+
return this.handlers[method](params);
|
|
35
|
+
}
|
|
36
|
+
async close() {
|
|
37
|
+
this.closeSession();
|
|
38
|
+
}
|
|
39
|
+
handlers = {
|
|
40
|
+
initialize: () => ({
|
|
41
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
42
|
+
pid: typeof process === "undefined" ? 0 : process.pid,
|
|
43
|
+
capabilities: typeof window !== "undefined"
|
|
44
|
+
? ["input", "clock", "tree"]
|
|
45
|
+
: ["input", "screenshot", "clock", "tree"],
|
|
46
|
+
window: (() => {
|
|
47
|
+
return {
|
|
48
|
+
width: typeof window === "undefined" ? 800 : window.innerWidth,
|
|
49
|
+
height: typeof window === "undefined" ? 600 : window.innerHeight,
|
|
50
|
+
};
|
|
51
|
+
})(),
|
|
52
|
+
}),
|
|
53
|
+
cancel: () => ({ ok: true }),
|
|
54
|
+
click: (params) => {
|
|
55
|
+
this.renderer.nativeSimulateClick(params.x, params.y, params.button, params.modifiers);
|
|
56
|
+
return { ok: true };
|
|
57
|
+
},
|
|
58
|
+
mouseDown: (params) => {
|
|
59
|
+
this.renderer.nativeSimulateMouseDown(params.x, params.y, params.button, params.modifiers);
|
|
60
|
+
return { ok: true };
|
|
61
|
+
},
|
|
62
|
+
mouseUp: (params) => {
|
|
63
|
+
this.renderer.nativeSimulateMouseUp(params.x, params.y, params.button, params.modifiers);
|
|
64
|
+
return { ok: true };
|
|
65
|
+
},
|
|
66
|
+
mouseMove: (params) => {
|
|
67
|
+
this.renderer.nativeSimulateMouseMove(params.x, params.y, params.modifiers);
|
|
68
|
+
return { ok: true };
|
|
69
|
+
},
|
|
70
|
+
scrollWheel: (params) => {
|
|
71
|
+
this.renderer.nativeSimulateScrollWheel(params.x, params.y, params.deltaX, params.deltaY, params.modifiers);
|
|
72
|
+
return { ok: true };
|
|
73
|
+
},
|
|
74
|
+
keystrokes: (params) => {
|
|
75
|
+
if (params.elementId == null) {
|
|
76
|
+
this.renderer.simulateKeystrokes(params.keys);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
this.renderer.nativeSimulateKeystrokes(params.elementId, params.keys);
|
|
80
|
+
}
|
|
81
|
+
return { ok: true };
|
|
82
|
+
},
|
|
83
|
+
keyDown: (params) => {
|
|
84
|
+
this.renderer.nativeSimulateKeyDown(params.elementId ?? 0, params.key, params.isHeld);
|
|
85
|
+
return { ok: true };
|
|
86
|
+
},
|
|
87
|
+
keyUp: (params) => {
|
|
88
|
+
this.renderer.nativeSimulateKeyUp(params.elementId ?? 0, params.key);
|
|
89
|
+
return { ok: true };
|
|
90
|
+
},
|
|
91
|
+
focus: (params) => {
|
|
92
|
+
this.renderer.focusElement?.(params.elementId);
|
|
93
|
+
return { ok: true };
|
|
94
|
+
},
|
|
95
|
+
blur: () => {
|
|
96
|
+
this.renderer.blur?.();
|
|
97
|
+
return { ok: true };
|
|
98
|
+
},
|
|
99
|
+
scrollTo: (params) => {
|
|
100
|
+
this.renderer.scrollTo(params.elementId, params.x, params.y);
|
|
101
|
+
return { ok: true };
|
|
102
|
+
},
|
|
103
|
+
getScrollOffset: (params) => ({
|
|
104
|
+
offset: this.renderer.getScrollOffset(params.elementId),
|
|
105
|
+
}),
|
|
106
|
+
getTree: () => {
|
|
107
|
+
const raw = JSON.parse(this.renderer.getAutomationTree());
|
|
108
|
+
return { tree: raw === null ? null : raw };
|
|
109
|
+
},
|
|
110
|
+
getPaintedText: () => ({ text: this.renderer.getPaintedText() }),
|
|
111
|
+
getAllText: () => ({ text: this.renderer.getAllText() }),
|
|
112
|
+
getBounds: (params) => {
|
|
113
|
+
const rect = this.renderer.getElementBounds(params.elementId);
|
|
114
|
+
if (!rect)
|
|
115
|
+
return { bounds: null };
|
|
116
|
+
return { bounds: rect };
|
|
117
|
+
},
|
|
118
|
+
getSelectedText: () => ({ text: this.renderer.getSelectedText() }),
|
|
119
|
+
clearSelection: () => {
|
|
120
|
+
this.renderer.clearSelection();
|
|
121
|
+
return { ok: true };
|
|
122
|
+
},
|
|
123
|
+
screenshot: (params) => {
|
|
124
|
+
this.renderer.captureScreenshot(params.path);
|
|
125
|
+
return { path: params.path };
|
|
126
|
+
},
|
|
127
|
+
clockPause: () => ({ nowMs: this.renderer.clockPause() }),
|
|
128
|
+
clockSet: (params) => ({ nowMs: this.renderer.clockSet(params.nowMs) }),
|
|
129
|
+
clockFastForward: (params) => ({
|
|
130
|
+
nowMs: this.renderer.clockFastForward(params.deltaMs),
|
|
131
|
+
}),
|
|
132
|
+
clockResume: () => ({ nowMs: this.renderer.clockResume() }),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
class PendingAutomationResponse {
|
|
136
|
+
promise;
|
|
137
|
+
resolve;
|
|
138
|
+
reject;
|
|
139
|
+
constructor() {
|
|
140
|
+
let resolveResponse;
|
|
141
|
+
let rejectResponse;
|
|
142
|
+
this.promise = new Promise((resolve, reject) => {
|
|
143
|
+
resolveResponse = resolve;
|
|
144
|
+
rejectResponse = reject;
|
|
145
|
+
});
|
|
146
|
+
this.resolve = resolveResponse;
|
|
147
|
+
this.reject = rejectResponse;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export class SseBackend extends ValidatedAutomationBackend {
|
|
151
|
+
write;
|
|
152
|
+
onClose;
|
|
153
|
+
nextId = 1;
|
|
154
|
+
pending = new Map();
|
|
155
|
+
constructor(write, feed, onClose) {
|
|
156
|
+
super();
|
|
157
|
+
this.write = write;
|
|
158
|
+
this.onClose = onClose;
|
|
159
|
+
const decoder = createSseDecoder((message) => {
|
|
160
|
+
if ("method" in message)
|
|
161
|
+
return;
|
|
162
|
+
if ("event" in message)
|
|
163
|
+
return;
|
|
164
|
+
const waiter = this.pending.get(message.id);
|
|
165
|
+
if (!waiter)
|
|
166
|
+
return;
|
|
167
|
+
this.pending.delete(message.id);
|
|
168
|
+
waiter.resolve(message);
|
|
169
|
+
});
|
|
170
|
+
feed((chunk) => decoder.feed(chunk));
|
|
171
|
+
}
|
|
172
|
+
async request(method, params) {
|
|
173
|
+
const id = this.nextId++;
|
|
174
|
+
const request = { id, method, params };
|
|
175
|
+
const pending = new PendingAutomationResponse();
|
|
176
|
+
this.pending.set(id, pending);
|
|
177
|
+
try {
|
|
178
|
+
this.write(encodeSse(request));
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
this.pending.delete(id);
|
|
182
|
+
pending.reject(error);
|
|
183
|
+
}
|
|
184
|
+
const response = await pending.promise;
|
|
185
|
+
if ("error" in response) {
|
|
186
|
+
throw new AutomationError(response.error.code, response.error.message, response.error.data);
|
|
187
|
+
}
|
|
188
|
+
return response.result;
|
|
189
|
+
}
|
|
190
|
+
async close() {
|
|
191
|
+
if (!this.closeSession())
|
|
192
|
+
return;
|
|
193
|
+
for (const [id, waiter] of this.pending) {
|
|
194
|
+
waiter.reject(new AutomationError("Closed", `Request ${id} cancelled`));
|
|
195
|
+
}
|
|
196
|
+
this.pending.clear();
|
|
197
|
+
await this.onClose?.();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/** `text` as a sequence of key presses, one per character. */
|
|
201
|
+
function toKeystrokes(text) {
|
|
202
|
+
return [...text]
|
|
203
|
+
.map((ch) => {
|
|
204
|
+
if (ch === " ")
|
|
205
|
+
return "Space";
|
|
206
|
+
if (ch === "\n")
|
|
207
|
+
return "Enter";
|
|
208
|
+
if (ch === "\t")
|
|
209
|
+
return "Tab";
|
|
210
|
+
return ch;
|
|
211
|
+
})
|
|
212
|
+
.join(" ");
|
|
213
|
+
}
|
|
214
|
+
function matches(node, selector) {
|
|
215
|
+
if (selector.testId != null && node.testId !== selector.testId)
|
|
216
|
+
return false;
|
|
217
|
+
if (selector.type != null && node.type !== selector.type)
|
|
218
|
+
return false;
|
|
219
|
+
if (selector.text != null &&
|
|
220
|
+
!matchableText(node.text ?? "").includes(matchableText(selector.text))) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
function isRawText(node) {
|
|
226
|
+
return (node.type === "text" &&
|
|
227
|
+
node.text != null &&
|
|
228
|
+
!node.children?.length &&
|
|
229
|
+
node.style === undefined &&
|
|
230
|
+
!node.events?.length &&
|
|
231
|
+
node.customProps === undefined);
|
|
232
|
+
}
|
|
233
|
+
function nearestAncestorWithBounds(root, id) {
|
|
234
|
+
const walk = (node, holder) => {
|
|
235
|
+
const nearest = node.bounds ? node : holder;
|
|
236
|
+
if (node.id === id)
|
|
237
|
+
return nearest;
|
|
238
|
+
for (const child of node.children ?? []) {
|
|
239
|
+
const found = walk(child, nearest);
|
|
240
|
+
if (found !== null)
|
|
241
|
+
return found;
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
};
|
|
245
|
+
return (root && walk(root)) ?? undefined;
|
|
246
|
+
}
|
|
247
|
+
function collect(node, selector) {
|
|
248
|
+
if (!node)
|
|
249
|
+
return [];
|
|
250
|
+
const roots = selector.parent
|
|
251
|
+
? collect(node, selector.parent)
|
|
252
|
+
: [node];
|
|
253
|
+
const found = [];
|
|
254
|
+
const walk = (current) => {
|
|
255
|
+
if (matches(current, selector))
|
|
256
|
+
found.push(current);
|
|
257
|
+
for (const child of current.children ?? [])
|
|
258
|
+
walk(child);
|
|
259
|
+
};
|
|
260
|
+
for (const root of roots) {
|
|
261
|
+
if (selector.parent) {
|
|
262
|
+
for (const child of root.children ?? [])
|
|
263
|
+
walk(child);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
walk(root);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return found;
|
|
270
|
+
}
|
|
271
|
+
function centerOf(bounds) {
|
|
272
|
+
return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
|
|
273
|
+
}
|
|
274
|
+
function collectText(node) {
|
|
275
|
+
let text = node.text ?? "";
|
|
276
|
+
for (const child of node.children ?? [])
|
|
277
|
+
text += collectText(child);
|
|
278
|
+
return text;
|
|
279
|
+
}
|
|
280
|
+
export class Locator {
|
|
281
|
+
app;
|
|
282
|
+
selector;
|
|
283
|
+
constructor(app, selector) {
|
|
284
|
+
this.app = app;
|
|
285
|
+
this.selector = selector;
|
|
286
|
+
}
|
|
287
|
+
getByTestId(testId) {
|
|
288
|
+
return new Locator(this.app, { testId, parent: this.selector });
|
|
289
|
+
}
|
|
290
|
+
getByText(text) {
|
|
291
|
+
return new Locator(this.app, { text, parent: this.selector });
|
|
292
|
+
}
|
|
293
|
+
getByType(type) {
|
|
294
|
+
return new Locator(this.app, { type, parent: this.selector });
|
|
295
|
+
}
|
|
296
|
+
async all() {
|
|
297
|
+
const { tree } = await this.app.call("getTree", {});
|
|
298
|
+
return collect(tree, this.selector);
|
|
299
|
+
}
|
|
300
|
+
async count() {
|
|
301
|
+
return (await this.all()).length;
|
|
302
|
+
}
|
|
303
|
+
async element() {
|
|
304
|
+
const found = await this.all();
|
|
305
|
+
if (found.length === 0) {
|
|
306
|
+
throw new AutomationError("NotFound", "Locator did not match any element");
|
|
307
|
+
}
|
|
308
|
+
if (found.length > 1) {
|
|
309
|
+
throw new AutomationError("Ambiguous", `Locator matched ${found.length} elements`);
|
|
310
|
+
}
|
|
311
|
+
return found[0];
|
|
312
|
+
}
|
|
313
|
+
async bounds() {
|
|
314
|
+
const node = await this.element();
|
|
315
|
+
if (node.bounds)
|
|
316
|
+
return node.bounds;
|
|
317
|
+
const { bounds } = await this.app.call("getBounds", { elementId: node.id });
|
|
318
|
+
if (bounds)
|
|
319
|
+
return bounds;
|
|
320
|
+
// A text node has no box of its own in the DOM, and its text is laid out
|
|
321
|
+
// as part of the paragraph of the element that holds it.
|
|
322
|
+
if (isRawText(node)) {
|
|
323
|
+
const { tree } = await this.app.call("getTree", {});
|
|
324
|
+
const holder = nearestAncestorWithBounds(tree, node.id);
|
|
325
|
+
if (holder?.bounds)
|
|
326
|
+
return holder.bounds;
|
|
327
|
+
}
|
|
328
|
+
throw new AutomationError("NotFound", "Element has no painted bounds");
|
|
329
|
+
}
|
|
330
|
+
/** The centre of the last painted bounds, in window coordinates. */
|
|
331
|
+
async center() {
|
|
332
|
+
return centerOf(await this.bounds());
|
|
333
|
+
}
|
|
334
|
+
async click(options = {}) {
|
|
335
|
+
const point = await this.center();
|
|
336
|
+
await this.app.call("click", { ...point, ...options });
|
|
337
|
+
}
|
|
338
|
+
/** Move the pointer to the centre, so hover styles and tooltips fire. */
|
|
339
|
+
async hover(options = {}) {
|
|
340
|
+
await this.app.mouse.move(await this.center(), options);
|
|
341
|
+
}
|
|
342
|
+
/** Send one wheel event over the centre of this element. Deltas have the
|
|
343
|
+
* DOM's sign: positive `deltaY` scrolls down, positive `deltaX` scrolls right. */
|
|
344
|
+
async wheel(deltaX, deltaY, options = {}) {
|
|
345
|
+
await this.app.mouse.wheel(await this.center(), deltaX, deltaY, options);
|
|
346
|
+
}
|
|
347
|
+
/** Press on this element, travel to `target`, release there. */
|
|
348
|
+
async dragTo(target, options = {}) {
|
|
349
|
+
await this.app.mouse.drag(this, target, options);
|
|
350
|
+
}
|
|
351
|
+
/** Press on this element and release `dx`/`dy` pixels away. */
|
|
352
|
+
async dragBy(dx, dy, options = {}) {
|
|
353
|
+
const start = await this.center();
|
|
354
|
+
const offset = options.offset ?? { x: 0, y: 0 };
|
|
355
|
+
await this.app.mouse.drag(this, { x: start.x + offset.x + dx, y: start.y + offset.y + dy }, options);
|
|
356
|
+
}
|
|
357
|
+
async fill(text) {
|
|
358
|
+
const node = await this.element();
|
|
359
|
+
const replacement = text.length === 0 ? "Backspace" : toKeystrokes(text);
|
|
360
|
+
await this.app.call("keystrokes", {
|
|
361
|
+
elementId: node.id,
|
|
362
|
+
keys: `ControlOrMeta+a ${replacement}`,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Presses keys on this element in Playwright's `keyboard.press` grammar:
|
|
367
|
+
* a DOM `key` (`"Enter"`, `"a"`, `"ArrowDown"`) or `code` (`"Space"`,
|
|
368
|
+
* `"KeyA"`), modifiers joined with `+` (`"Shift+Tab"`, `"Meta+a"`,
|
|
369
|
+
* `"ControlOrMeta+Shift+P"`). Several presses are separated by spaces.
|
|
370
|
+
* Each press is a keydown then a keyup.
|
|
371
|
+
*/
|
|
372
|
+
async press(key) {
|
|
373
|
+
const node = await this.element();
|
|
374
|
+
await this.app.call("keystrokes", {
|
|
375
|
+
elementId: node.id,
|
|
376
|
+
keys: key,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Own text plus every descendant's, concatenated in document order, like
|
|
381
|
+
* DOM `textContent`. `<text>{value}</text>` puts the string on a child node,
|
|
382
|
+
* so reading only `node.text` returned an empty string for every wrapper.
|
|
383
|
+
*/
|
|
384
|
+
async textContent() {
|
|
385
|
+
return collectText(await this.element());
|
|
386
|
+
}
|
|
387
|
+
async waitFor(options = {}) {
|
|
388
|
+
const timeoutMs = options.timeoutMs ?? 5000;
|
|
389
|
+
const started = Date.now();
|
|
390
|
+
for (;;) {
|
|
391
|
+
const found = await this.all();
|
|
392
|
+
if (found.length === 1)
|
|
393
|
+
return found[0];
|
|
394
|
+
if (Date.now() - started >= timeoutMs) {
|
|
395
|
+
throw new AutomationError(found.length === 0 ? "Timeout" : "Ambiguous", `waitFor timed out after ${timeoutMs}ms`);
|
|
396
|
+
}
|
|
397
|
+
await new Promise((resolve) => setTimeout(resolve, 16));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
export class App {
|
|
402
|
+
backend;
|
|
403
|
+
clock;
|
|
404
|
+
/**
|
|
405
|
+
* Raw pointer input in window coordinates. Prefer a locator when the target
|
|
406
|
+
* is an element; use this for empty space, marquee selection, and gestures
|
|
407
|
+
* that end outside every hitbox.
|
|
408
|
+
*/
|
|
409
|
+
mouse;
|
|
410
|
+
constructor(backend) {
|
|
411
|
+
this.backend = backend;
|
|
412
|
+
this.mouse = {
|
|
413
|
+
move: async (target, options = {}) => {
|
|
414
|
+
const point = await this.resolvePoint(target);
|
|
415
|
+
await this.call("mouseMove", {
|
|
416
|
+
...point,
|
|
417
|
+
modifiers: options.modifiers,
|
|
418
|
+
});
|
|
419
|
+
},
|
|
420
|
+
down: async (target, options = {}) => {
|
|
421
|
+
const point = await this.resolvePoint(target);
|
|
422
|
+
await this.call("mouseDown", { ...point, ...options });
|
|
423
|
+
},
|
|
424
|
+
up: async (target, options = {}) => {
|
|
425
|
+
const point = await this.resolvePoint(target);
|
|
426
|
+
await this.call("mouseUp", { ...point, ...options });
|
|
427
|
+
},
|
|
428
|
+
click: async (target, options = {}) => {
|
|
429
|
+
const point = await this.resolvePoint(target);
|
|
430
|
+
await this.call("click", { ...point, ...options });
|
|
431
|
+
},
|
|
432
|
+
wheel: async (target, deltaX, deltaY, options = {}) => {
|
|
433
|
+
const point = await this.resolvePoint(target);
|
|
434
|
+
await this.call("scrollWheel", {
|
|
435
|
+
...point,
|
|
436
|
+
deltaX,
|
|
437
|
+
deltaY,
|
|
438
|
+
modifiers: options.modifiers,
|
|
439
|
+
});
|
|
440
|
+
},
|
|
441
|
+
drag: async (from, to, options = {}) => {
|
|
442
|
+
const offset = options.offset ?? { x: 0, y: 0 };
|
|
443
|
+
const origin = await this.resolvePoint(from);
|
|
444
|
+
const start = { x: origin.x + offset.x, y: origin.y + offset.y };
|
|
445
|
+
// Resolve the destination before the press. A locator target can move
|
|
446
|
+
// under a live preview, and the caller means where it is now.
|
|
447
|
+
const end = await this.resolvePoint(to);
|
|
448
|
+
const button = options.button ?? 0;
|
|
449
|
+
const modifiers = options.modifiers;
|
|
450
|
+
const steps = Math.max(1, Math.floor(options.steps ?? 8));
|
|
451
|
+
await this.call("mouseMove", { ...start, modifiers });
|
|
452
|
+
await this.call("mouseDown", { ...start, button, modifiers });
|
|
453
|
+
for (let step = 1; step <= steps; step += 1) {
|
|
454
|
+
const t = step / steps;
|
|
455
|
+
await this.call("mouseMove", {
|
|
456
|
+
x: start.x + (end.x - start.x) * t,
|
|
457
|
+
y: start.y + (end.y - start.y) * t,
|
|
458
|
+
modifiers,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
await this.call("mouseUp", { ...end, button, modifiers });
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
this.clock = {
|
|
465
|
+
pause: async () => (await this.call("clockPause", {})).nowMs,
|
|
466
|
+
set: async (nowMs) => (await this.call("clockSet", { nowMs })).nowMs,
|
|
467
|
+
fastForward: async (deltaMs) => (await this.call("clockFastForward", { deltaMs })).nowMs,
|
|
468
|
+
resume: async () => (await this.call("clockResume", {})).nowMs,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
call(method, params) {
|
|
472
|
+
return this.backend.call(method, params);
|
|
473
|
+
}
|
|
474
|
+
async resolvePoint(target) {
|
|
475
|
+
return target instanceof Locator ? target.center() : target;
|
|
476
|
+
}
|
|
477
|
+
getByTestId(testId) {
|
|
478
|
+
return new Locator(this, { testId });
|
|
479
|
+
}
|
|
480
|
+
getByText(text) {
|
|
481
|
+
return new Locator(this, { text });
|
|
482
|
+
}
|
|
483
|
+
getByType(type) {
|
|
484
|
+
return new Locator(this, { type });
|
|
485
|
+
}
|
|
486
|
+
async screenshot(options) {
|
|
487
|
+
const { path: saved } = await this.call("screenshot", { path: options.path });
|
|
488
|
+
return saved;
|
|
489
|
+
}
|
|
490
|
+
async captureFrames(dir, timesMs) {
|
|
491
|
+
if (typeof process === "undefined") {
|
|
492
|
+
throw new AutomationError("Unsupported", "Browser frame capture must use the controlling browser automation client");
|
|
493
|
+
}
|
|
494
|
+
const { mkdir } = await importNodeModule("node:fs/promises");
|
|
495
|
+
const path = await importNodeModule("node:path");
|
|
496
|
+
await mkdir(dir, { recursive: true });
|
|
497
|
+
await this.clock.pause();
|
|
498
|
+
const paths = [];
|
|
499
|
+
for (const nowMs of timesMs) {
|
|
500
|
+
await this.clock.set(nowMs);
|
|
501
|
+
const file = path.join(dir, `t${nowMs}.png`);
|
|
502
|
+
await this.screenshot({ path: file });
|
|
503
|
+
paths.push(file);
|
|
504
|
+
}
|
|
505
|
+
return paths;
|
|
506
|
+
}
|
|
507
|
+
async close() {
|
|
508
|
+
await this.backend.close();
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
export function liveRendererAsTest(renderer) {
|
|
512
|
+
const afterInput = () => {
|
|
513
|
+
renderer.tick?.();
|
|
514
|
+
};
|
|
515
|
+
// The live renderer's move takes the held button as an argument; the buttons held by
|
|
516
|
+
// simulated presses are kept here so that every move reports them, as the test renderer's does.
|
|
517
|
+
const held = new Set();
|
|
518
|
+
return {
|
|
519
|
+
nativeSimulateClick(x, y, button, modifiers) {
|
|
520
|
+
held.delete(button ?? 0);
|
|
521
|
+
renderer.simulateClick(x, y, button, modifiers);
|
|
522
|
+
afterInput();
|
|
523
|
+
},
|
|
524
|
+
nativeSimulateMouseDown(x, y, button, modifiers) {
|
|
525
|
+
held.add(button ?? 0);
|
|
526
|
+
renderer.simulateMouseDown(x, y, button, modifiers);
|
|
527
|
+
afterInput();
|
|
528
|
+
},
|
|
529
|
+
nativeSimulateMouseUp(x, y, button, modifiers) {
|
|
530
|
+
held.delete(button ?? 0);
|
|
531
|
+
renderer.simulateMouseUp(x, y, button, modifiers);
|
|
532
|
+
afterInput();
|
|
533
|
+
},
|
|
534
|
+
nativeSimulateMouseMove(x, y, modifiers) {
|
|
535
|
+
const pressedButton = held.size === 0 ? undefined : Math.min(...held);
|
|
536
|
+
renderer.simulateMouseMove(x, y, pressedButton, modifiers);
|
|
537
|
+
afterInput();
|
|
538
|
+
},
|
|
539
|
+
nativeSimulateScrollWheel(x, y, deltaX, deltaY, modifiers) {
|
|
540
|
+
renderer.simulateScrollWheel(x, y, deltaX, deltaY, modifiers);
|
|
541
|
+
afterInput();
|
|
542
|
+
},
|
|
543
|
+
simulateKeystrokes(keys) {
|
|
544
|
+
if (!renderer.simulateKeystrokes) {
|
|
545
|
+
throw new AutomationError("Unsupported", "keystrokes are not live yet");
|
|
546
|
+
}
|
|
547
|
+
renderer.simulateKeystrokes(keys);
|
|
548
|
+
afterInput();
|
|
549
|
+
},
|
|
550
|
+
nativeSimulateKeystrokes(elementId, keys) {
|
|
551
|
+
if (!renderer.simulateKeystrokes) {
|
|
552
|
+
throw new AutomationError("Unsupported", "keystrokes are not live yet");
|
|
553
|
+
}
|
|
554
|
+
renderer.focusElement(elementId);
|
|
555
|
+
renderer.simulateKeystrokes(keys);
|
|
556
|
+
afterInput();
|
|
557
|
+
},
|
|
558
|
+
nativeSimulateKeyDown(elementId, key, isHeld) {
|
|
559
|
+
if (!renderer.simulateKeyDown) {
|
|
560
|
+
throw new AutomationError("Unsupported", "keyDown is not live yet");
|
|
561
|
+
}
|
|
562
|
+
if (elementId > 0)
|
|
563
|
+
renderer.focusElement(elementId);
|
|
564
|
+
renderer.simulateKeyDown(key, isHeld);
|
|
565
|
+
afterInput();
|
|
566
|
+
},
|
|
567
|
+
nativeSimulateKeyUp(elementId, key) {
|
|
568
|
+
if (!renderer.simulateKeyUp) {
|
|
569
|
+
throw new AutomationError("Unsupported", "keyUp is not live yet");
|
|
570
|
+
}
|
|
571
|
+
if (elementId > 0)
|
|
572
|
+
renderer.focusElement(elementId);
|
|
573
|
+
renderer.simulateKeyUp(key);
|
|
574
|
+
afterInput();
|
|
575
|
+
},
|
|
576
|
+
scrollTo: (id, x, y) => renderer.scrollTo(id, x, y),
|
|
577
|
+
getScrollOffset: (id) => {
|
|
578
|
+
const offset = renderer.getScrollOffset(id);
|
|
579
|
+
return offset ? [offset[0], offset[1]] : null;
|
|
580
|
+
},
|
|
581
|
+
getAllText: () => renderer.getAllText(),
|
|
582
|
+
getPaintedText: () => renderer.getPaintedText(),
|
|
583
|
+
getSelectedText: () => renderer.getSelectedText(),
|
|
584
|
+
clearSelection: () => renderer.clearSelection(),
|
|
585
|
+
captureScreenshot(file) {
|
|
586
|
+
if (!renderer.captureScreenshot) {
|
|
587
|
+
throw new AutomationError("Unsupported", "Browser screenshots must use the controlling browser automation client");
|
|
588
|
+
}
|
|
589
|
+
renderer.captureScreenshot(file);
|
|
590
|
+
},
|
|
591
|
+
getAutomationTree: () => renderer.getAutomationTree(),
|
|
592
|
+
getElementBounds: (id) => renderer.getElementBounds(id),
|
|
593
|
+
clockPause: () => renderer.clockPause(),
|
|
594
|
+
clockSet: (nowMs) => renderer.clockSet(nowMs),
|
|
595
|
+
clockFastForward: (deltaMs) => renderer.clockFastForward(deltaMs),
|
|
596
|
+
clockResume: () => renderer.clockResume(),
|
|
597
|
+
focusElement: (id) => renderer.focusElement(id),
|
|
598
|
+
blur: () => renderer.blur(),
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
function splitPress(press) {
|
|
602
|
+
const parts = [];
|
|
603
|
+
let building = "";
|
|
604
|
+
for (const ch of press) {
|
|
605
|
+
if (ch === "+" && building) {
|
|
606
|
+
parts.push(building);
|
|
607
|
+
building = "";
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
building += ch;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
parts.push(building);
|
|
614
|
+
return parts;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* The `KeyboardEvent` a browser page gets for one press in the
|
|
618
|
+
* `keyboard.press` grammar. Key names are passed through as the DOM spells
|
|
619
|
+
* them: unlike native, this side has no key table to reject a typo with.
|
|
620
|
+
*/
|
|
621
|
+
export function browserKeystrokeInit(press, isHeld = false) {
|
|
622
|
+
const parts = splitPress(press);
|
|
623
|
+
const name = parts.pop();
|
|
624
|
+
const held = new Set();
|
|
625
|
+
for (const modifier of parts) {
|
|
626
|
+
switch (modifier) {
|
|
627
|
+
case "Shift":
|
|
628
|
+
case "Alt":
|
|
629
|
+
case "Control":
|
|
630
|
+
case "Meta":
|
|
631
|
+
held.add(modifier);
|
|
632
|
+
break;
|
|
633
|
+
case "ControlOrMeta":
|
|
634
|
+
held.add(typeof navigator !== "undefined" && navigator.platform.includes("Mac")
|
|
635
|
+
? "Meta"
|
|
636
|
+
: "Control");
|
|
637
|
+
break;
|
|
638
|
+
default:
|
|
639
|
+
throw new AutomationError("Protocol", `"${modifier}" in "${press}" is not a modifier. The modifiers are Shift, Control, Alt, Meta and ControlOrMeta`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (name === "") {
|
|
643
|
+
throw new AutomationError("Protocol", `No key in "${press}"`);
|
|
644
|
+
}
|
|
645
|
+
let key = name;
|
|
646
|
+
let code;
|
|
647
|
+
if (name === "Space" || name === " ") {
|
|
648
|
+
key = " ";
|
|
649
|
+
code = "Space";
|
|
650
|
+
}
|
|
651
|
+
else if (/^Key[A-Z]$/.test(name)) {
|
|
652
|
+
code = name;
|
|
653
|
+
key = held.has("Shift") ? name.slice(3) : name.slice(3).toLowerCase();
|
|
654
|
+
}
|
|
655
|
+
else if (/^Digit[0-9]$/.test(name)) {
|
|
656
|
+
code = name;
|
|
657
|
+
key = name.slice(5);
|
|
658
|
+
}
|
|
659
|
+
else if (name.length > 1) {
|
|
660
|
+
code = name;
|
|
661
|
+
}
|
|
662
|
+
return {
|
|
663
|
+
key,
|
|
664
|
+
...(code === undefined ? {} : { code }),
|
|
665
|
+
altKey: held.has("Alt"),
|
|
666
|
+
bubbles: true,
|
|
667
|
+
ctrlKey: held.has("Control"),
|
|
668
|
+
metaKey: held.has("Meta"),
|
|
669
|
+
repeat: isHeld,
|
|
670
|
+
shiftKey: held.has("Shift"),
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* The hidden element GPUI's browser platform appends to `<body>`.
|
|
675
|
+
*
|
|
676
|
+
* A GPUI web app has two event surfaces: the `<canvas>` takes pointer events,
|
|
677
|
+
* and this element takes **every keyboard and IME event**. `gpui_web` attaches
|
|
678
|
+
* its `keydown` / `keyup` listeners here, not to the window or the canvas, so
|
|
679
|
+
* dispatching a synthetic `KeyboardEvent` at this element is the only way to
|
|
680
|
+
* type into a browser GPUIX app.
|
|
681
|
+
*
|
|
682
|
+
* Match on the attribute alone. The element used to be an `<input>` and is now
|
|
683
|
+
* a `<textarea>`, because a single-line input strips newlines from an assigned
|
|
684
|
+
* value and would desynchronise the mirror from the document. See
|
|
685
|
+
* zed-industries/zed#63201 and `gpui_web/src/ime_mirror.rs`. A tag-qualified
|
|
686
|
+
* selector silently turns every keystroke into an "unavailable" error.
|
|
687
|
+
*/
|
|
688
|
+
export const IME_MIRROR_SELECTOR = "[data-gpui-input]";
|
|
689
|
+
function dispatchBrowserKeystroke({ keystroke, type, isHeld = false, }) {
|
|
690
|
+
const mirror = document.querySelector(IME_MIRROR_SELECTOR);
|
|
691
|
+
if (!mirror) {
|
|
692
|
+
throw new AutomationError("Unsupported", `No GPUI keyboard target: nothing matches "${IME_MIRROR_SELECTOR}". ` +
|
|
693
|
+
"Call this only after render() has painted a frame in a browser page.");
|
|
694
|
+
}
|
|
695
|
+
mirror.dispatchEvent(new KeyboardEvent(type, browserKeystrokeInit(keystroke, isHeld)));
|
|
696
|
+
}
|
|
697
|
+
export function browserRendererAsTest(renderer) {
|
|
698
|
+
const live = liveRendererAsTest(renderer);
|
|
699
|
+
const keystrokes = (keys) => {
|
|
700
|
+
for (const key of keys.split(/\s+/).filter(Boolean)) {
|
|
701
|
+
dispatchBrowserKeystroke({ keystroke: key, type: "keydown" });
|
|
702
|
+
dispatchBrowserKeystroke({ keystroke: key, type: "keyup" });
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
return {
|
|
706
|
+
...live,
|
|
707
|
+
simulateKeystrokes: keystrokes,
|
|
708
|
+
nativeSimulateKeystrokes(elementId, keys) {
|
|
709
|
+
renderer.focusElement(elementId);
|
|
710
|
+
keystrokes(keys);
|
|
711
|
+
},
|
|
712
|
+
nativeSimulateKeyDown(elementId, key, isHeld) {
|
|
713
|
+
if (elementId > 0)
|
|
714
|
+
renderer.focusElement(elementId);
|
|
715
|
+
dispatchBrowserKeystroke({ keystroke: key, type: "keydown", isHeld });
|
|
716
|
+
},
|
|
717
|
+
nativeSimulateKeyUp(elementId, key) {
|
|
718
|
+
if (elementId > 0)
|
|
719
|
+
renderer.focusElement(elementId);
|
|
720
|
+
dispatchBrowserKeystroke({ keystroke: key, type: "keyup" });
|
|
721
|
+
},
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
export async function connectTest(renderer) {
|
|
725
|
+
const app = new App(new InProcessBackend(renderer));
|
|
726
|
+
await app.call("initialize", {
|
|
727
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
728
|
+
client: "@ghia/native/automation",
|
|
729
|
+
});
|
|
730
|
+
return app;
|
|
731
|
+
}
|
|
732
|
+
export async function connectStdio(options) {
|
|
733
|
+
const app = new App(new SseBackend(options.write, options.feed, options.close));
|
|
734
|
+
await app.call("initialize", {
|
|
735
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
736
|
+
client: "@ghia/native/automation",
|
|
737
|
+
});
|
|
738
|
+
return app;
|
|
739
|
+
}
|
|
740
|
+
export async function launch(options) {
|
|
741
|
+
const { spawn } = await importNodeModule("node:child_process");
|
|
742
|
+
const child = spawn(options.command, options.args ?? [], {
|
|
743
|
+
cwd: options.cwd,
|
|
744
|
+
env: {
|
|
745
|
+
...process.env,
|
|
746
|
+
...options.env,
|
|
747
|
+
},
|
|
748
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
749
|
+
});
|
|
750
|
+
const app = await connectStdio({
|
|
751
|
+
write: (chunk) => {
|
|
752
|
+
child.stdin.write(chunk);
|
|
753
|
+
},
|
|
754
|
+
feed: (listener) => {
|
|
755
|
+
child.stdout.on("data", (buf) => listener(buf.toString("utf8")));
|
|
756
|
+
},
|
|
757
|
+
close: async () => {
|
|
758
|
+
child.kill();
|
|
759
|
+
},
|
|
760
|
+
});
|
|
761
|
+
return app;
|
|
762
|
+
}
|
|
763
|
+
export function handleAutomationRequest(raw, backend) {
|
|
764
|
+
const request = parseWireMessage(raw);
|
|
765
|
+
if (!("method" in request)) {
|
|
766
|
+
throw new AutomationError("Protocol", "Server expected a request");
|
|
767
|
+
}
|
|
768
|
+
return backend.call(request.method, request.params).then((result) => encodeSse({ id: request.id, result }), (error) => {
|
|
769
|
+
const failure = error instanceof AutomationError
|
|
770
|
+
? error
|
|
771
|
+
: new AutomationError("Protocol", String(error));
|
|
772
|
+
return encodeSse({
|
|
773
|
+
id: request.id,
|
|
774
|
+
error: {
|
|
775
|
+
code: failure.code,
|
|
776
|
+
message: failure.message,
|
|
777
|
+
data: failure.data,
|
|
778
|
+
},
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
export function serveAutomationStdio(backend) {
|
|
783
|
+
const decoder = createSseDecoder((message) => {
|
|
784
|
+
if (!("method" in message))
|
|
785
|
+
return;
|
|
786
|
+
void handleAutomationRequest(message, backend).then((reply) => {
|
|
787
|
+
process.stdout.write(reply);
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
process.stdin.setEncoding("utf8");
|
|
791
|
+
process.stdin.on("data", (chunk) => {
|
|
792
|
+
decoder.feed(chunk);
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
export function isServerEvent(message) {
|
|
796
|
+
return (typeof message === "object" &&
|
|
797
|
+
message !== null &&
|
|
798
|
+
"event" in message &&
|
|
799
|
+
!("method" in message));
|
|
800
|
+
}
|
|
801
|
+
//# sourceMappingURL=client.js.map
|