@houwert/conductor 0.16.0 → 0.17.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/dist/commands/assert-visible.js +2 -1
- package/dist/commands/clipboard.js +84 -0
- package/dist/commands/crashes.js +262 -0
- package/dist/commands/debug.js +244 -0
- package/dist/commands/flow-record.js +58 -0
- package/dist/commands/gestures.js +196 -0
- package/dist/commands/inspect.js +41 -1
- package/dist/commands/metro.js +109 -0
- package/dist/commands/network.js +194 -0
- package/dist/commands/profile.js +300 -0
- package/dist/commands/run-sequence.js +124 -0
- package/dist/commands/scroll-until-visible.js +17 -0
- package/dist/commands/start-device.js +18 -0
- package/dist/commands/tap.js +2 -1
- package/dist/commands/workspace.js +162 -0
- package/dist/drivers/android.js +9 -0
- package/dist/drivers/direct-ios-selector.js +70 -0
- package/dist/drivers/element-resolver.js +189 -0
- package/dist/drivers/flow-recorder.js +135 -0
- package/dist/drivers/flow-runner.js +5 -3
- package/dist/drivers/ios.js +87 -2
- package/dist/drivers/metro-cdp.js +291 -0
- package/dist/drivers/metro-scripts.js +399 -0
- package/dist/drivers/wait.js +20 -2
- package/dist/index.js +262 -1
- package/dist/runner.js +18 -0
- package/package.json +1 -1
- package/proto/conductor_android.proto +29 -0
package/dist/drivers/ios.js
CHANGED
|
@@ -16,6 +16,13 @@ const promises_1 = __importDefault(require("fs/promises"));
|
|
|
16
16
|
const os_1 = __importDefault(require("os"));
|
|
17
17
|
const path_1 = __importDefault(require("path"));
|
|
18
18
|
const child_process_1 = require("child_process");
|
|
19
|
+
/**
|
|
20
|
+
* How long a captured view hierarchy may be reused. Bounds the staleness of a
|
|
21
|
+
* cached snapshot when the screen changes without a driver-issued command
|
|
22
|
+
* (timer-driven UI, async animation). Mirrors the equivalent cache TTL in the
|
|
23
|
+
* agent-device runner.
|
|
24
|
+
*/
|
|
25
|
+
const HIERARCHY_CACHE_TTL_MS = 750;
|
|
19
26
|
class IOSDriver {
|
|
20
27
|
constructor(port = 1075, host = '127.0.0.1', deviceId, platform = 'ios') {
|
|
21
28
|
this.port = port;
|
|
@@ -23,6 +30,13 @@ class IOSDriver {
|
|
|
23
30
|
this.deviceId = deviceId;
|
|
24
31
|
this.platform = platform;
|
|
25
32
|
this._recordingProcess = null;
|
|
33
|
+
/**
|
|
34
|
+
* Short-lived cache of the most recent view hierarchy, keyed by request
|
|
35
|
+
* params. Only served when the caller explicitly opts in (e.g. the first
|
|
36
|
+
* probe of a wait loop) and dropped by any UI-mutating command, so polling
|
|
37
|
+
* loops and post-action reads always see a fresh tree.
|
|
38
|
+
*/
|
|
39
|
+
this.hierarchyCache = null;
|
|
26
40
|
}
|
|
27
41
|
request(method, path, body) {
|
|
28
42
|
return new Promise((resolve, reject) => {
|
|
@@ -71,6 +85,10 @@ class IOSDriver {
|
|
|
71
85
|
throw new Error('IOSDriver: deviceId is required for this operation');
|
|
72
86
|
return this.deviceId;
|
|
73
87
|
}
|
|
88
|
+
/** Drop the cached view hierarchy — call after any command that mutates the UI. */
|
|
89
|
+
invalidateHierarchyCache() {
|
|
90
|
+
this.hierarchyCache = null;
|
|
91
|
+
}
|
|
74
92
|
simctl(args) {
|
|
75
93
|
const _id = this.requireDeviceId();
|
|
76
94
|
return new Promise((resolve, reject) => {
|
|
@@ -111,9 +129,10 @@ class IOSDriver {
|
|
|
111
129
|
}
|
|
112
130
|
async tap(x, y, duration) {
|
|
113
131
|
await this.post('touch', { x, y, ...(duration !== undefined ? { duration } : {}) });
|
|
132
|
+
this.invalidateHierarchyCache();
|
|
114
133
|
}
|
|
115
134
|
async swipe(startX, startY, endX, endY, duration, appIds) {
|
|
116
|
-
await this.post('
|
|
135
|
+
await this.post('swipe', {
|
|
117
136
|
startX,
|
|
118
137
|
startY,
|
|
119
138
|
endX,
|
|
@@ -121,15 +140,29 @@ class IOSDriver {
|
|
|
121
140
|
duration,
|
|
122
141
|
...(appIds ? { appIds } : {}),
|
|
123
142
|
});
|
|
143
|
+
this.invalidateHierarchyCache();
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Multi-finger gesture playback. `paths` is one entry per finger; each entry's
|
|
147
|
+
* `steps` are the (x, y, dt) frames making up that finger's path. The driver
|
|
148
|
+
* synthesizes a multi-finger XCSynthesizedEventRecord when paths.length > 1.
|
|
149
|
+
* `dt` is the delay in seconds since the previous step (or the initial offset
|
|
150
|
+
* for the first step).
|
|
151
|
+
*/
|
|
152
|
+
async gesturePath(paths) {
|
|
153
|
+
await this.post('gesturePath', { paths });
|
|
124
154
|
}
|
|
125
155
|
async inputText(text, appIds = []) {
|
|
126
156
|
await this.post('inputText', { text, appIds });
|
|
157
|
+
this.invalidateHierarchyCache();
|
|
127
158
|
}
|
|
128
159
|
async pressKey(key) {
|
|
129
160
|
await this.post('pressKey', { key });
|
|
161
|
+
this.invalidateHierarchyCache();
|
|
130
162
|
}
|
|
131
163
|
async pressButton(button) {
|
|
132
164
|
await this.post('pressButton', { button });
|
|
165
|
+
this.invalidateHierarchyCache();
|
|
133
166
|
}
|
|
134
167
|
async launchApp(bundleId, args) {
|
|
135
168
|
if (args && Object.keys(args).length > 0) {
|
|
@@ -145,9 +178,11 @@ class IOSDriver {
|
|
|
145
178
|
else {
|
|
146
179
|
await this.post('launchApp', { bundleId });
|
|
147
180
|
}
|
|
181
|
+
this.invalidateHierarchyCache();
|
|
148
182
|
}
|
|
149
183
|
async terminateApp(appId) {
|
|
150
184
|
await this.post('terminateApp', { appId });
|
|
185
|
+
this.invalidateHierarchyCache();
|
|
151
186
|
}
|
|
152
187
|
async clearAppState(bundleId) {
|
|
153
188
|
const deviceId = this.requireDeviceId();
|
|
@@ -167,11 +202,13 @@ class IOSDriver {
|
|
|
167
202
|
finally {
|
|
168
203
|
await promises_1.default.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
|
|
169
204
|
}
|
|
205
|
+
this.invalidateHierarchyCache();
|
|
170
206
|
}
|
|
171
207
|
async uninstallApp(bundleId) {
|
|
172
208
|
const deviceId = this.requireDeviceId();
|
|
173
209
|
await this.simctl(['terminate', deviceId, bundleId]).catch(() => { });
|
|
174
210
|
await this.simctl(['uninstall', deviceId, bundleId]);
|
|
211
|
+
this.invalidateHierarchyCache();
|
|
175
212
|
}
|
|
176
213
|
async clearKeychain() {
|
|
177
214
|
const deviceId = this.requireDeviceId();
|
|
@@ -180,6 +217,28 @@ class IOSDriver {
|
|
|
180
217
|
async openLink(url) {
|
|
181
218
|
const deviceId = this.requireDeviceId();
|
|
182
219
|
await this.simctl(['openurl', deviceId, url]);
|
|
220
|
+
this.invalidateHierarchyCache();
|
|
221
|
+
}
|
|
222
|
+
/** Read the simulator's clipboard. Uses `xcrun simctl pbpaste <udid>`. */
|
|
223
|
+
async clipboardRead() {
|
|
224
|
+
const deviceId = this.requireDeviceId();
|
|
225
|
+
return this.simctlCapture(['pbpaste', deviceId]);
|
|
226
|
+
}
|
|
227
|
+
/** Write to the simulator's clipboard. Uses `xcrun simctl pbcopy <udid>` over stdin. */
|
|
228
|
+
async clipboardWrite(text) {
|
|
229
|
+
const deviceId = this.requireDeviceId();
|
|
230
|
+
await new Promise((resolve, reject) => {
|
|
231
|
+
const proc = (0, child_process_1.spawn)('xcrun', ['simctl', 'pbcopy', deviceId], {
|
|
232
|
+
stdio: ['pipe', 'ignore', 'pipe'],
|
|
233
|
+
});
|
|
234
|
+
let stderr = '';
|
|
235
|
+
proc.stderr?.on('data', (c) => {
|
|
236
|
+
stderr += c.toString();
|
|
237
|
+
});
|
|
238
|
+
proc.on('close', (code) => code === 0 ? resolve() : reject(new Error(`xcrun simctl pbcopy failed: ${stderr.trim()}`)));
|
|
239
|
+
proc.on('error', reject);
|
|
240
|
+
proc.stdin?.end(text);
|
|
241
|
+
});
|
|
183
242
|
}
|
|
184
243
|
async setLocation(latitude, longitude) {
|
|
185
244
|
const deviceId = this.requireDeviceId();
|
|
@@ -187,6 +246,7 @@ class IOSDriver {
|
|
|
187
246
|
}
|
|
188
247
|
async setOrientation(orientation) {
|
|
189
248
|
await this.post('setOrientation', { orientation });
|
|
249
|
+
this.invalidateHierarchyCache();
|
|
190
250
|
}
|
|
191
251
|
async setPermissions(appId, permissions) {
|
|
192
252
|
// All iOS permissions the XCTest runner's interruption monitor can handle.
|
|
@@ -298,7 +358,14 @@ class IOSDriver {
|
|
|
298
358
|
this._recordingProcess = null;
|
|
299
359
|
}
|
|
300
360
|
}
|
|
301
|
-
async viewHierarchy(excludeKeyboardElements = false, appIds = []) {
|
|
361
|
+
async viewHierarchy(excludeKeyboardElements = false, appIds = [], opts = {}) {
|
|
362
|
+
const key = `${excludeKeyboardElements}:${appIds.join(',')}`;
|
|
363
|
+
if (opts.cache &&
|
|
364
|
+
this.hierarchyCache &&
|
|
365
|
+
this.hierarchyCache.key === key &&
|
|
366
|
+
Date.now() - this.hierarchyCache.at < HIERARCHY_CACHE_TTL_MS) {
|
|
367
|
+
return this.hierarchyCache.value;
|
|
368
|
+
}
|
|
302
369
|
const { status, data } = await this.request('POST', '/viewHierarchy', {
|
|
303
370
|
appIds,
|
|
304
371
|
excludeKeyboardElements,
|
|
@@ -306,6 +373,24 @@ class IOSDriver {
|
|
|
306
373
|
if (status < 200 || status >= 300) {
|
|
307
374
|
throw new Error(`iOS driver viewHierarchy failed (HTTP ${status}): ${data.toString('utf-8').slice(0, 200)}`);
|
|
308
375
|
}
|
|
376
|
+
const value = JSON.parse(data.toString('utf-8'));
|
|
377
|
+
this.hierarchyCache = { key, value, at: Date.now() };
|
|
378
|
+
return value;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Resolve a single element directly via the runner instead of dumping and
|
|
382
|
+
* matching the whole view hierarchy. Returns `matchCount` so callers can
|
|
383
|
+
* fall back to the snapshot path when the result is ambiguous (>1) or empty.
|
|
384
|
+
*/
|
|
385
|
+
async queryElement(selectorKey, selectorValue, appIds = []) {
|
|
386
|
+
const { status, data } = await this.request('POST', '/queryElement', {
|
|
387
|
+
selectorKey,
|
|
388
|
+
selectorValue,
|
|
389
|
+
appIds,
|
|
390
|
+
});
|
|
391
|
+
if (status < 200 || status >= 300) {
|
|
392
|
+
throw new Error(`iOS driver queryElement failed (HTTP ${status}): ${data.toString('utf-8').slice(0, 200)}`);
|
|
393
|
+
}
|
|
309
394
|
return JSON.parse(data.toString('utf-8'));
|
|
310
395
|
}
|
|
311
396
|
async screenshot(_opts = {}) {
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MetroCdpClient = void 0;
|
|
7
|
+
exports.resolveDebuggerUrl = resolveDebuggerUrl;
|
|
8
|
+
exports.cdpCall = cdpCall;
|
|
9
|
+
/**
|
|
10
|
+
* One-shot CDP client to Metro's debugger endpoint.
|
|
11
|
+
*
|
|
12
|
+
* Sibling to `MetroLogSource` in log-sources/metro.ts: that class stays connected
|
|
13
|
+
* to stream `Runtime.consoleAPICalled`; this one opens a short-lived socket for
|
|
14
|
+
* request/response calls (`Page.reload`, `Runtime.evaluate`, etc.).
|
|
15
|
+
*
|
|
16
|
+
* Reuses `fetchTargets()` / target selection from `log-sources/metro.ts` and
|
|
17
|
+
* `metro-discovery.ts` — do not duplicate discovery logic here.
|
|
18
|
+
*/
|
|
19
|
+
const ws_1 = __importDefault(require("ws"));
|
|
20
|
+
const metro_js_1 = require("./log-sources/metro.js");
|
|
21
|
+
const metro_discovery_js_1 = require("./log-sources/metro-discovery.js");
|
|
22
|
+
/**
|
|
23
|
+
* Resolve a Metro target's `webSocketDebuggerUrl` honoring deviceId / targetIndex.
|
|
24
|
+
* Throws with a clear message if Metro is unreachable or no target matches.
|
|
25
|
+
*/
|
|
26
|
+
async function resolveDebuggerUrl(opts) {
|
|
27
|
+
const port = opts.port ?? 8081;
|
|
28
|
+
const host = opts.host ?? 'localhost';
|
|
29
|
+
const targets = await (0, metro_js_1.fetchTargets)(port, host);
|
|
30
|
+
const withWs = targets.filter((t) => t.webSocketDebuggerUrl);
|
|
31
|
+
if (withWs.length === 0) {
|
|
32
|
+
throw new Error(`Metro on ${host}:${port} returned no debugger targets. Is an app running on a device/simulator?`);
|
|
33
|
+
}
|
|
34
|
+
if (opts.targetIndex !== undefined) {
|
|
35
|
+
if (opts.targetIndex < 0 || opts.targetIndex >= withWs.length) {
|
|
36
|
+
throw new Error(`--target ${opts.targetIndex} is out of range (have ${withWs.length}).`);
|
|
37
|
+
}
|
|
38
|
+
return withWs[opts.targetIndex].webSocketDebuggerUrl;
|
|
39
|
+
}
|
|
40
|
+
if (opts.deviceId && opts.platform) {
|
|
41
|
+
const displayName = await (0, metro_discovery_js_1.getDeviceDisplayName)(opts.platform, opts.deviceId);
|
|
42
|
+
if (displayName) {
|
|
43
|
+
const target = (0, metro_discovery_js_1.selectTargetForDevice)(withWs, displayName);
|
|
44
|
+
if (target)
|
|
45
|
+
return target.webSocketDebuggerUrl;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Prefer the Hermes/React target by title, otherwise first.
|
|
49
|
+
const target = withWs.find((t) => t.title && /hermes|react/i.test(t.title)) ?? withWs[0];
|
|
50
|
+
return target.webSocketDebuggerUrl;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Open a short-lived CDP socket, send a single method, return the result.
|
|
54
|
+
* Closes the socket whether the call succeeds or throws.
|
|
55
|
+
*/
|
|
56
|
+
async function cdpCall(method, params, opts) {
|
|
57
|
+
const wsUrl = await resolveDebuggerUrl(opts);
|
|
58
|
+
return cdpCallOnUrl(wsUrl, method, params, opts.timeoutMs ?? 10000);
|
|
59
|
+
}
|
|
60
|
+
async function cdpCallOnUrl(wsUrl, method, params, timeoutMs) {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const ws = new ws_1.default(wsUrl);
|
|
63
|
+
let settled = false;
|
|
64
|
+
const timer = setTimeout(() => {
|
|
65
|
+
if (settled)
|
|
66
|
+
return;
|
|
67
|
+
settled = true;
|
|
68
|
+
ws.terminate();
|
|
69
|
+
reject(new Error(`CDP ${method} timed out after ${timeoutMs}ms`));
|
|
70
|
+
}, timeoutMs);
|
|
71
|
+
function finish(err, value) {
|
|
72
|
+
if (settled)
|
|
73
|
+
return;
|
|
74
|
+
settled = true;
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
ws.removeAllListeners();
|
|
77
|
+
try {
|
|
78
|
+
ws.close();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// ignore
|
|
82
|
+
}
|
|
83
|
+
if (err)
|
|
84
|
+
reject(err);
|
|
85
|
+
else
|
|
86
|
+
resolve(value);
|
|
87
|
+
}
|
|
88
|
+
const req = { id: 1, method };
|
|
89
|
+
if (params)
|
|
90
|
+
req.params = params;
|
|
91
|
+
ws.on('open', () => {
|
|
92
|
+
ws.send(JSON.stringify(req));
|
|
93
|
+
});
|
|
94
|
+
ws.on('message', (data) => {
|
|
95
|
+
try {
|
|
96
|
+
const msg = JSON.parse(data.toString());
|
|
97
|
+
if (msg.id !== req.id)
|
|
98
|
+
return;
|
|
99
|
+
if (msg.error) {
|
|
100
|
+
finish(new Error(`CDP ${method}: ${msg.error.message}`));
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
finish(null, msg.result);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
finish(err instanceof Error ? err : new Error(String(err)));
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
ws.on('error', (err) => finish(err));
|
|
111
|
+
ws.on('close', () => {
|
|
112
|
+
if (!settled)
|
|
113
|
+
finish(new Error(`CDP socket closed before ${method} completed`));
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Stateful CDP client. Open once, issue many calls. Used by the debugger
|
|
119
|
+
* commands that need session-scoped state (loaded scripts, enabled domains).
|
|
120
|
+
*/
|
|
121
|
+
class MetroCdpClient {
|
|
122
|
+
constructor() {
|
|
123
|
+
this.ws = null;
|
|
124
|
+
this.nextId = 1;
|
|
125
|
+
this.pending = new Map();
|
|
126
|
+
this.events = new Map();
|
|
127
|
+
this.enabledDomains = new Set();
|
|
128
|
+
this.loadedScripts = new Map();
|
|
129
|
+
this.bindings = new Set();
|
|
130
|
+
this.callbackPending = new Map();
|
|
131
|
+
}
|
|
132
|
+
async connect(opts) {
|
|
133
|
+
const wsUrl = await resolveDebuggerUrl(opts);
|
|
134
|
+
await new Promise((resolve, reject) => {
|
|
135
|
+
const ws = new ws_1.default(wsUrl);
|
|
136
|
+
ws.on('open', () => {
|
|
137
|
+
this.ws = ws;
|
|
138
|
+
resolve();
|
|
139
|
+
});
|
|
140
|
+
ws.on('message', (data) => this.handleMessage(data.toString()));
|
|
141
|
+
ws.on('error', (err) => {
|
|
142
|
+
if (!this.ws)
|
|
143
|
+
reject(err);
|
|
144
|
+
});
|
|
145
|
+
ws.on('close', () => {
|
|
146
|
+
this.ws = null;
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
handleMessage(raw) {
|
|
151
|
+
try {
|
|
152
|
+
const msg = JSON.parse(raw);
|
|
153
|
+
if (typeof msg.id === 'number') {
|
|
154
|
+
const slot = this.pending.get(msg.id);
|
|
155
|
+
if (slot) {
|
|
156
|
+
this.pending.delete(msg.id);
|
|
157
|
+
if (msg.error)
|
|
158
|
+
slot.reject(new Error(msg.error.message));
|
|
159
|
+
else
|
|
160
|
+
slot.resolve(msg.result);
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (msg.method) {
|
|
165
|
+
if (msg.method === 'Debugger.scriptParsed') {
|
|
166
|
+
const p = msg.params;
|
|
167
|
+
if (p?.scriptId)
|
|
168
|
+
this.loadedScripts.set(p.scriptId, { url: p.url });
|
|
169
|
+
}
|
|
170
|
+
const handlers = this.events.get(msg.method);
|
|
171
|
+
if (handlers)
|
|
172
|
+
for (const h of handlers)
|
|
173
|
+
h(msg.params);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// ignore parse errors
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
on(method, handler) {
|
|
181
|
+
const arr = this.events.get(method) ?? [];
|
|
182
|
+
arr.push(handler);
|
|
183
|
+
this.events.set(method, arr);
|
|
184
|
+
}
|
|
185
|
+
async send(method, params) {
|
|
186
|
+
if (!this.ws)
|
|
187
|
+
throw new Error('CDP client not connected');
|
|
188
|
+
const id = this.nextId++;
|
|
189
|
+
return new Promise((resolve, reject) => {
|
|
190
|
+
this.pending.set(id, {
|
|
191
|
+
resolve: (v) => resolve(v),
|
|
192
|
+
reject,
|
|
193
|
+
});
|
|
194
|
+
this.ws.send(JSON.stringify({ id, method, params }));
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
async enableDomain(domain) {
|
|
198
|
+
if (this.enabledDomains.has(domain))
|
|
199
|
+
return;
|
|
200
|
+
await this.send(`${domain}.enable`);
|
|
201
|
+
this.enabledDomains.add(domain);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Install a `Runtime.addBinding` callback. The injected JS calls
|
|
205
|
+
* `globalThis.__conductor_callback(JSON.stringify({ requestId, ... }))` and
|
|
206
|
+
* this method routes the payload back to the awaiter keyed on `requestId`.
|
|
207
|
+
*
|
|
208
|
+
* Returns a function that, given a requestId, returns a promise resolving
|
|
209
|
+
* to the next payload tagged with that requestId. Useful for async fiber
|
|
210
|
+
* walkers that can't return synchronously from `Runtime.evaluate`.
|
|
211
|
+
*/
|
|
212
|
+
async installCallbackBinding(bindingName = '__conductor_callback') {
|
|
213
|
+
await this.enableDomain('Runtime');
|
|
214
|
+
if (!this.bindings.has(bindingName)) {
|
|
215
|
+
await this.send('Runtime.addBinding', { name: bindingName });
|
|
216
|
+
this.bindings.add(bindingName);
|
|
217
|
+
this.on('Runtime.bindingCalled', (params) => {
|
|
218
|
+
const p = params;
|
|
219
|
+
if (p.name !== bindingName)
|
|
220
|
+
return;
|
|
221
|
+
try {
|
|
222
|
+
const parsed = JSON.parse(p.payload);
|
|
223
|
+
if (parsed.requestId && this.callbackPending.has(parsed.requestId)) {
|
|
224
|
+
const slot = this.callbackPending.get(parsed.requestId);
|
|
225
|
+
this.callbackPending.delete(parsed.requestId);
|
|
226
|
+
slot.resolve(parsed);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// ignore malformed payloads
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return (requestId, timeoutMs = 5000) => new Promise((resolve, reject) => {
|
|
235
|
+
const timer = setTimeout(() => {
|
|
236
|
+
this.callbackPending.delete(requestId);
|
|
237
|
+
reject(new Error(`callback ${requestId} timed out after ${timeoutMs}ms`));
|
|
238
|
+
}, timeoutMs);
|
|
239
|
+
this.callbackPending.set(requestId, {
|
|
240
|
+
resolve: (v) => {
|
|
241
|
+
clearTimeout(timer);
|
|
242
|
+
resolve(v);
|
|
243
|
+
},
|
|
244
|
+
reject,
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Evaluate a JS expression in the app's runtime. Returns the value or throws
|
|
250
|
+
* a thrown JS exception's description. Awaits promises by default.
|
|
251
|
+
*/
|
|
252
|
+
async evaluate(expression, returnByValue = true) {
|
|
253
|
+
await this.enableDomain('Runtime');
|
|
254
|
+
const result = await this.send('Runtime.evaluate', {
|
|
255
|
+
expression,
|
|
256
|
+
returnByValue,
|
|
257
|
+
awaitPromise: true,
|
|
258
|
+
generatePreview: false,
|
|
259
|
+
});
|
|
260
|
+
if (result.exceptionDetails) {
|
|
261
|
+
const msg = result.exceptionDetails.exception?.description ??
|
|
262
|
+
result.exceptionDetails.text ??
|
|
263
|
+
'evaluation threw';
|
|
264
|
+
throw new Error(msg);
|
|
265
|
+
}
|
|
266
|
+
return (result.result.value ?? result.result.description);
|
|
267
|
+
}
|
|
268
|
+
getEnabledDomains() {
|
|
269
|
+
return new Set(this.enabledDomains);
|
|
270
|
+
}
|
|
271
|
+
getLoadedScripts() {
|
|
272
|
+
return this.loadedScripts;
|
|
273
|
+
}
|
|
274
|
+
isConnected() {
|
|
275
|
+
return this.ws !== null;
|
|
276
|
+
}
|
|
277
|
+
close() {
|
|
278
|
+
if (this.ws) {
|
|
279
|
+
try {
|
|
280
|
+
this.ws.close();
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
// ignore
|
|
284
|
+
}
|
|
285
|
+
this.ws = null;
|
|
286
|
+
}
|
|
287
|
+
this.pending.clear();
|
|
288
|
+
this.events.clear();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
exports.MetroCdpClient = MetroCdpClient;
|