browserscale-ts 1.4.0 → 1.7.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 +34 -3
- package/dist/auth-session.d.ts +37 -0
- package/dist/auth-session.js +1 -0
- package/dist/browser.d.ts +5 -1
- package/dist/browser.js +5 -0
- package/dist/browserscale.browser.js +1662 -42
- package/dist/client.d.ts +350 -7
- package/dist/client.js +637 -8
- package/dist/dom-mirror.d.ts +271 -0
- package/dist/dom-mirror.js +613 -0
- package/dist/gen/wrc_pb.d.ts +1490 -168
- package/dist/gen/wrc_pb.js +234 -39
- package/dist/index.d.ts +32 -1
- package/dist/index.js +61 -0
- package/dist/internal/convert.d.ts +11 -2
- package/dist/internal/convert.js +84 -1
- package/dist/network-capture.d.ts +84 -0
- package/dist/network-capture.js +107 -0
- package/dist/scripts.d.ts +205 -0
- package/dist/scripts.js +234 -0
- package/dist/types.d.ts +157 -0
- package/dist/ws-transport.d.ts +15 -1
- package/dist/ws-transport.js +155 -10
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -2,10 +2,13 @@ import { createClient } from "@connectrpc/connect";
|
|
|
2
2
|
import { create } from "@bufbuild/protobuf";
|
|
3
3
|
import { Browser,
|
|
4
4
|
// request / response schemas
|
|
5
|
-
SetProxyRequestSchema, GetPagesRequestSchema, NavigateRequestSchema, LoadHTMLRequestSchema, EvaluateRequestSchema, WaitForAnyParamsSchema, WaitConditionSchema, ClickRequestSchema, FillRequestSchema, MoveToRequestSchema, ScrollToRequestSchema, DragRequestSchema, SelectOptionRequestSchema, GetDOMRequestSchema, GetDOMHashRequestSchema, GetObservationRequestSchema, ScreenshotRequestSchema, ReadCanvasRequestSchema, SetBlockListRequestSchema, SetStaticPathsRequestSchema, WaitForAnyRequestRequestSchema, WaitForAnyResponseRequestSchema, ModifyRequestRequestSchema, GetCookiesRequestSchema, SetCookiesRequestSchema, ClearCookiesRequestSchema, GetStorageRequestSchema, SetStorageRequestSchema, ClearStorageRequestSchema, InspectAtPositionRequestSchema, HighlightNodeRequestSchema, InsertTextRequestSchema, TypeRequestSchema, PressKeyRequestSchema, ReleaseKeyRequestSchema, GetSelectionRequestSchema, SolveCaptchaRequestSchema, GetStreamConfigRequestSchema, StartStreamRequestSchema, StopStreamRequestSchema, AddReactionRequestSchema, RemoveReactionRequestSchema, ListReactionsRequestSchema, } from "./gen/wrc_pb.js";
|
|
5
|
+
SetProxyRequestSchema, GetPagesRequestSchema, NavigateRequestSchema, LoadHTMLRequestSchema, EvaluateRequestSchema, WaitForAnyParamsSchema, WaitConditionSchema, ClickRequestSchema, FillRequestSchema, MoveToRequestSchema, ScrollToRequestSchema, DragRequestSchema, SelectOptionRequestSchema, GetDOMRequestSchema, GetDOMHashRequestSchema, GetObservationRequestSchema, ScreenshotRequestSchema, ReadCanvasRequestSchema, SetBlockListRequestSchema, SetStaticPathsRequestSchema, WaitForAnyRequestRequestSchema, WaitForAnyResponseRequestSchema, ModifyRequestRequestSchema, GetCookiesRequestSchema, SetCookiesRequestSchema, ClearCookiesRequestSchema, GetStorageRequestSchema, SetStorageRequestSchema, ClearStorageRequestSchema, GetAuthSessionRequestSchema, SetAuthSessionRequestSchema, InspectAtPositionRequestSchema, HighlightNodeRequestSchema, InsertTextRequestSchema, TypeRequestSchema, PressKeyRequestSchema, ReleaseKeyRequestSchema, GetSelectionRequestSchema, SolveCaptchaRequestSchema, GetStreamConfigRequestSchema, StartStreamRequestSchema, StopStreamRequestSchema, AddReactionRequestSchema, RemoveReactionRequestSchema, ListReactionsRequestSchema, StartNetworkCaptureRequestSchema, StopNetworkCaptureRequestSchema, StartDomMirrorRequestSchema, StopDomMirrorRequestSchema, GetDomChildrenRequestSchema, ReleaseDomSubtreeRequestSchema, RevealDomNodeRequestSchema, GetDomRevisionRequestSchema, StreamDomEventsRequestSchema, StreamNetworkExchangesRequestSchema, RunScriptRequestSchema, StartScriptRequestSchema, StopScriptsRequestSchema, ListScriptRunsRequestSchema, StreamScriptEventsRequestSchema, } from "./gen/wrc_pb.js";
|
|
6
6
|
import { DefaultWaitTimeoutMs } from "./defaults.js";
|
|
7
7
|
import { BrowserScaleError } from "./errors.js";
|
|
8
|
-
import {
|
|
8
|
+
import { NetworkCapture } from "./network-capture.js";
|
|
9
|
+
import { ScriptFollow, ScriptRun, scriptLogEntryFromProto, } from "./scripts.js";
|
|
10
|
+
import { DomMirror, } from "./dom-mirror.js";
|
|
11
|
+
import { authSessionFromProto, authSessionToProto, cookieParamFromProto, cookieParamsToProto, elementFields, headerModsToProto, headersToProto, interceptedRequestFromProto, interceptedResponseFromProto, pageInfoFromProto, rectFromProto, splitRequestPatterns, storageEntriesToProto, storageEntryFromProto, unwrapClick, unwrapDrag, unwrapFill, unwrapMove, unwrapScroll, unwrapSelect, unwrapWait, } from "./internal/convert.js";
|
|
9
12
|
/**
|
|
10
13
|
* CloudBrowser is the SDK-side handle for an active browserscale browser session.
|
|
11
14
|
*
|
|
@@ -1047,6 +1050,334 @@ export class CloudBrowser {
|
|
|
1047
1050
|
return interceptedRequestFromProto(resp.request);
|
|
1048
1051
|
}
|
|
1049
1052
|
// ──────────────────────────────────────────────────────────────────
|
|
1053
|
+
// Network capture
|
|
1054
|
+
// ──────────────────────────────────────────────────────────────────
|
|
1055
|
+
/**
|
|
1056
|
+
* Starts capturing the session's network traffic and returns a live view of
|
|
1057
|
+
* it.
|
|
1058
|
+
*
|
|
1059
|
+
* Every request matching `opts.patterns` is reported once it completes, and
|
|
1060
|
+
* "every request" is literal: capture sits in the browser process rather than
|
|
1061
|
+
* in a page, so cross-process iframes, workers and service workers are
|
|
1062
|
+
* included, the headers are the ones actually put on the wire (Cookie and
|
|
1063
|
+
* Sec-* included), and each hop of a redirect chain arrives as its own
|
|
1064
|
+
* exchange. Requests are never paused, so the page loads at full speed.
|
|
1065
|
+
*
|
|
1066
|
+
* This resolves as soon as the capture is running; onExchange then fires in
|
|
1067
|
+
* the background while you drive the browser. The capture is armed only after
|
|
1068
|
+
* the subscription exists, so nothing that happens after this resolves is
|
|
1069
|
+
* missed. Call {@link NetworkCapture.stop} when done — it disarms the capture
|
|
1070
|
+
* server-side, which an aborted transport alone does not.
|
|
1071
|
+
*
|
|
1072
|
+
* @param opts - which requests to capture and whether to keep bodies
|
|
1073
|
+
* @param onExchange - called per exchange; see {@link NetworkExchangeHandler}
|
|
1074
|
+
* for the ordering and blocking rules
|
|
1075
|
+
*
|
|
1076
|
+
* @returns NetworkCapture handle for stopping the capture and inspecting how
|
|
1077
|
+
* it ended
|
|
1078
|
+
*
|
|
1079
|
+
* @throws UNKNOWN_ERROR - the capture could not be started
|
|
1080
|
+
*
|
|
1081
|
+
* @example
|
|
1082
|
+
* const capture = await browser.captureNetwork(
|
|
1083
|
+
* { patterns: ["*\/api/*"], bodies: "text" },
|
|
1084
|
+
* (ex) => console.log(ex.statusCode, ex.method, ex.url),
|
|
1085
|
+
* );
|
|
1086
|
+
* try {
|
|
1087
|
+
* await browser.navigate("https://example.com");
|
|
1088
|
+
* } finally {
|
|
1089
|
+
* await capture.stop();
|
|
1090
|
+
* }
|
|
1091
|
+
*/
|
|
1092
|
+
async captureNetwork(opts, onExchange) {
|
|
1093
|
+
const capture = await this.subscribeNetworkExchanges(onExchange);
|
|
1094
|
+
try {
|
|
1095
|
+
await this.startNetworkCapture(opts);
|
|
1096
|
+
}
|
|
1097
|
+
catch (err) {
|
|
1098
|
+
// Not armed yet, so this only tears down the local subscription.
|
|
1099
|
+
await capture.stop();
|
|
1100
|
+
throw err;
|
|
1101
|
+
}
|
|
1102
|
+
capture.arm();
|
|
1103
|
+
return capture;
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Arms a capture without subscribing to it.
|
|
1107
|
+
*
|
|
1108
|
+
* Use it when the reader lives somewhere else — another tab, or a later
|
|
1109
|
+
* {@link createWebSocketBrowser} against the same session. Most callers want
|
|
1110
|
+
* {@link CloudBrowser.captureNetwork} instead, which arms and subscribes
|
|
1111
|
+
* together. Calling this again replaces the running capture.
|
|
1112
|
+
*
|
|
1113
|
+
* @param opts - which requests to capture and whether to keep bodies
|
|
1114
|
+
*
|
|
1115
|
+
* @throws UNKNOWN_ERROR - the capture could not be started
|
|
1116
|
+
*/
|
|
1117
|
+
async startNetworkCapture(opts) {
|
|
1118
|
+
await this.client.startNetworkCapture(create(StartNetworkCaptureRequestSchema, {
|
|
1119
|
+
sessionId: this.sessionId,
|
|
1120
|
+
apiKey: this.apiKey,
|
|
1121
|
+
patterns: opts.patterns ?? [],
|
|
1122
|
+
bodies: opts.bodies ?? "none",
|
|
1123
|
+
bodyPatterns: opts.bodyPatterns ?? [],
|
|
1124
|
+
}));
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Disarms the session's capture.
|
|
1128
|
+
*
|
|
1129
|
+
* @returns whether a capture was running
|
|
1130
|
+
*
|
|
1131
|
+
* @throws UNKNOWN_ERROR - the capture could not be stopped
|
|
1132
|
+
*/
|
|
1133
|
+
async stopNetworkCapture() {
|
|
1134
|
+
const resp = await this.client.stopNetworkCapture(create(StopNetworkCaptureRequestSchema, {
|
|
1135
|
+
sessionId: this.sessionId,
|
|
1136
|
+
apiKey: this.apiKey,
|
|
1137
|
+
}));
|
|
1138
|
+
return resp.stopped;
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Subscribes to the session's capture without arming one, for reading a
|
|
1142
|
+
* capture that {@link CloudBrowser.startNetworkCapture} armed elsewhere.
|
|
1143
|
+
* Several readers can watch the same capture, each with its own buffer.
|
|
1144
|
+
*
|
|
1145
|
+
* Stopping the returned view detaches this reader and leaves the capture
|
|
1146
|
+
* running, since other readers may still be attached.
|
|
1147
|
+
*
|
|
1148
|
+
* @param onExchange - called per exchange; see {@link NetworkExchangeHandler}
|
|
1149
|
+
* for the ordering and blocking rules
|
|
1150
|
+
*
|
|
1151
|
+
* @returns NetworkCapture attached to whatever capture is running; onExchange
|
|
1152
|
+
* simply never fires when none is
|
|
1153
|
+
*
|
|
1154
|
+
* @throws UNKNOWN_ERROR - the subscription could not be opened
|
|
1155
|
+
*/
|
|
1156
|
+
async streamNetworkExchanges(onExchange) {
|
|
1157
|
+
return this.subscribeNetworkExchanges(onExchange);
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Opens the stream and waits for the server to acknowledge the subscription
|
|
1161
|
+
* before resolving.
|
|
1162
|
+
*
|
|
1163
|
+
* Merely calling the streaming method does not wait for the server to start
|
|
1164
|
+
* handling it, so arming a capture straight after could outrun the
|
|
1165
|
+
* subscription and lose the first exchanges. The response headers arrive once
|
|
1166
|
+
* the handler is subscribed, and onHeader reports exactly that — over native
|
|
1167
|
+
* gRPC as well as over the WebSocket transport, which forwards the event as
|
|
1168
|
+
* its own frame.
|
|
1169
|
+
*/
|
|
1170
|
+
async subscribeNetworkExchanges(onExchange) {
|
|
1171
|
+
const abort = new AbortController();
|
|
1172
|
+
let subscribed;
|
|
1173
|
+
const ready = new Promise((resolve) => {
|
|
1174
|
+
subscribed = resolve;
|
|
1175
|
+
});
|
|
1176
|
+
const stream = this.client.streamNetworkExchanges(create(StreamNetworkExchangesRequestSchema, {
|
|
1177
|
+
sessionId: this.sessionId,
|
|
1178
|
+
apiKey: this.apiKey,
|
|
1179
|
+
}), { signal: abort.signal, onHeader: () => subscribed() });
|
|
1180
|
+
const capture = new NetworkCapture({
|
|
1181
|
+
stream,
|
|
1182
|
+
onExchange,
|
|
1183
|
+
abort,
|
|
1184
|
+
disarm: () => this.stopNetworkCapture(),
|
|
1185
|
+
});
|
|
1186
|
+
// A stream that dies before it is acknowledged would leave the wait above
|
|
1187
|
+
// hanging, so race the two outcomes.
|
|
1188
|
+
const ended = capture.wait().then(() => {
|
|
1189
|
+
throw new BrowserScaleError("browserscale.captureNetwork: stream closed before the subscription was established");
|
|
1190
|
+
});
|
|
1191
|
+
ended.catch(() => { }); // the loser of the race must not look unhandled
|
|
1192
|
+
await Promise.race([ready, ended]);
|
|
1193
|
+
return capture;
|
|
1194
|
+
}
|
|
1195
|
+
// ──────────────────────────────────────────────────────────────────
|
|
1196
|
+
// DOM mirror
|
|
1197
|
+
// ──────────────────────────────────────────────────────────────────
|
|
1198
|
+
/**
|
|
1199
|
+
* Starts a live copy of a frame's DOM and keeps it up to date.
|
|
1200
|
+
*
|
|
1201
|
+
* The browser sends the top of the tree once, then reports only what changed
|
|
1202
|
+
* in the part you expanded. Everything else costs a child count per batch, no
|
|
1203
|
+
* matter how much churns inside it — which is what makes this usable on a
|
|
1204
|
+
* page that rewrites a list sixty times a second, where re-fetching the
|
|
1205
|
+
* document on a timer is not.
|
|
1206
|
+
*
|
|
1207
|
+
* Expand and collapse as the user opens and closes nodes; that is what moves
|
|
1208
|
+
* the boundary of what gets reported. The returned {@link DomMirror} holds
|
|
1209
|
+
* the tree and exposes `expand`, `collapse` and `reveal`.
|
|
1210
|
+
*
|
|
1211
|
+
* The handler runs after each applied batch. `mirror.root` is a new object
|
|
1212
|
+
* whenever anything under it changed and the untouched parts keep their
|
|
1213
|
+
* identity, so rendering straight from it with memoized components is cheap.
|
|
1214
|
+
*
|
|
1215
|
+
* One mirror covers the whole page as ONE tree. An `<iframe>` is an
|
|
1216
|
+
* ordinary element whose single child is the document it hosts; expanding it
|
|
1217
|
+
* fetches that document and starts mirroring the frame, however deeply
|
|
1218
|
+
* nested and whether or not it is cross-origin. Unlike the inlining
|
|
1219
|
+
* {@link CloudBrowser.getDOM} does, these regions stay live — and frames
|
|
1220
|
+
* nobody opened cost nothing.
|
|
1221
|
+
*
|
|
1222
|
+
* ```ts
|
|
1223
|
+
* const mirror = await browser.mirrorDom({ pierce: true }, () => {
|
|
1224
|
+
* render(mirror.root);
|
|
1225
|
+
* });
|
|
1226
|
+
* await mirror.expand(bodyNode);
|
|
1227
|
+
* // ...
|
|
1228
|
+
* await mirror.stop();
|
|
1229
|
+
* ```
|
|
1230
|
+
*
|
|
1231
|
+
* @param opts initial depth and whether to pierce shadow roots
|
|
1232
|
+
* @param onChange called after every change, including the first snapshot
|
|
1233
|
+
* @param onResync called when the copy had to be rebuilt, after the new tree
|
|
1234
|
+
* is in place. Rebuilding is automatic; this is for telling the user why
|
|
1235
|
+
* their expanded nodes collapsed.
|
|
1236
|
+
* @throws UNKNOWN_ERROR - the mirror could not be started
|
|
1237
|
+
*/
|
|
1238
|
+
async mirrorDom(opts, onChange, onResync) {
|
|
1239
|
+
const abort = new AbortController();
|
|
1240
|
+
let subscribed;
|
|
1241
|
+
const ready = new Promise((resolve) => {
|
|
1242
|
+
subscribed = resolve;
|
|
1243
|
+
});
|
|
1244
|
+
const stream = this.client.streamDomEvents(create(StreamDomEventsRequestSchema, {
|
|
1245
|
+
sessionId: this.sessionId,
|
|
1246
|
+
apiKey: this.apiKey,
|
|
1247
|
+
}), { signal: abort.signal, onHeader: () => subscribed() });
|
|
1248
|
+
const mirror = new DomMirror({
|
|
1249
|
+
stream,
|
|
1250
|
+
transport: {
|
|
1251
|
+
start: (o) => this.startDomMirror(o),
|
|
1252
|
+
stop: () => this.stopDomMirror(),
|
|
1253
|
+
children: (id, frameId, depth) => this.getDomChildren(id, frameId, depth),
|
|
1254
|
+
release: (id, frameId) => this.releaseDomSubtree(id, frameId),
|
|
1255
|
+
reveal: (id, frameId) => this.revealDomNode(id, frameId),
|
|
1256
|
+
},
|
|
1257
|
+
options: opts,
|
|
1258
|
+
onChange,
|
|
1259
|
+
onResync,
|
|
1260
|
+
abort,
|
|
1261
|
+
});
|
|
1262
|
+
// A stream that dies before it is acknowledged would leave the wait below
|
|
1263
|
+
// hanging, so race the two outcomes.
|
|
1264
|
+
const ended = mirror.wait().then(() => {
|
|
1265
|
+
throw new BrowserScaleError("browserscale.mirrorDom: stream closed before the subscription was established");
|
|
1266
|
+
});
|
|
1267
|
+
ended.catch(() => { });
|
|
1268
|
+
await Promise.race([ready, ended]);
|
|
1269
|
+
// Snapshot only now: taken before the subscription exists, changes between
|
|
1270
|
+
// the two would be lost with nothing to indicate it.
|
|
1271
|
+
try {
|
|
1272
|
+
mirror.install(await this.startDomMirror(opts));
|
|
1273
|
+
}
|
|
1274
|
+
catch (err) {
|
|
1275
|
+
abort.abort();
|
|
1276
|
+
throw err;
|
|
1277
|
+
}
|
|
1278
|
+
onChange(mirror);
|
|
1279
|
+
return mirror;
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Starts (or restarts) the page's mirror and returns the main document,
|
|
1283
|
+
* without subscribing to changes. {@link CloudBrowser.mirrorDom} is what you
|
|
1284
|
+
* normally want; this is the raw command.
|
|
1285
|
+
*/
|
|
1286
|
+
async startDomMirror(opts = {}) {
|
|
1287
|
+
const req = create(StartDomMirrorRequestSchema, {
|
|
1288
|
+
sessionId: this.sessionId,
|
|
1289
|
+
apiKey: this.apiKey,
|
|
1290
|
+
});
|
|
1291
|
+
if (opts.depth !== undefined)
|
|
1292
|
+
req.depth = opts.depth;
|
|
1293
|
+
if (opts.pierce !== undefined)
|
|
1294
|
+
req.pierce = opts.pierce;
|
|
1295
|
+
const resp = await this.client.startDomMirror(req);
|
|
1296
|
+
return {
|
|
1297
|
+
root: resp.root,
|
|
1298
|
+
frameId: resp.frameId,
|
|
1299
|
+
seq: Number(resp.seq),
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
/** Stops the page's mirror, every frame of it. Idempotent. */
|
|
1303
|
+
async stopDomMirror() {
|
|
1304
|
+
await this.client.stopDomMirror(create(StopDomMirrorRequestSchema, {
|
|
1305
|
+
sessionId: this.sessionId,
|
|
1306
|
+
apiKey: this.apiKey,
|
|
1307
|
+
}));
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Fetches a node's children and starts reporting changes inside them.
|
|
1311
|
+
* {@link DomMirror.expand} calls this and folds the result into the tree.
|
|
1312
|
+
*
|
|
1313
|
+
* On an `<iframe>` the one child is the document it hosts, and this call is
|
|
1314
|
+
* what starts mirroring that frame.
|
|
1315
|
+
*/
|
|
1316
|
+
async getDomChildren(backendNodeId, frameId = "", depth) {
|
|
1317
|
+
const req = create(GetDomChildrenRequestSchema, {
|
|
1318
|
+
sessionId: this.sessionId,
|
|
1319
|
+
apiKey: this.apiKey,
|
|
1320
|
+
backendNodeId,
|
|
1321
|
+
});
|
|
1322
|
+
if (frameId)
|
|
1323
|
+
req.frameId = frameId;
|
|
1324
|
+
if (depth !== undefined)
|
|
1325
|
+
req.depth = depth;
|
|
1326
|
+
const resp = await this.client.getDomChildren(req);
|
|
1327
|
+
return { children: resp.children, seq: Number(resp.seq) };
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Stops reporting changes inside a node, and inside any frame below it.
|
|
1331
|
+
* {@link DomMirror.collapse} calls this.
|
|
1332
|
+
*/
|
|
1333
|
+
async releaseDomSubtree(backendNodeId, frameId = "") {
|
|
1334
|
+
const req = create(ReleaseDomSubtreeRequestSchema, {
|
|
1335
|
+
sessionId: this.sessionId,
|
|
1336
|
+
apiKey: this.apiKey,
|
|
1337
|
+
backendNodeId,
|
|
1338
|
+
});
|
|
1339
|
+
if (frameId)
|
|
1340
|
+
req.frameId = frameId;
|
|
1341
|
+
await this.client.releaseDomSubtree(req);
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* Returns the chain from the main document down to a node, each ancestor
|
|
1345
|
+
* with its own children, crossing into frames where it has to and starting
|
|
1346
|
+
* the ones it passes through. {@link DomMirror.reveal} calls this and
|
|
1347
|
+
* splices it in.
|
|
1348
|
+
*/
|
|
1349
|
+
async revealDomNode(backendNodeId, frameId = "") {
|
|
1350
|
+
const req = create(RevealDomNodeRequestSchema, {
|
|
1351
|
+
sessionId: this.sessionId,
|
|
1352
|
+
apiKey: this.apiKey,
|
|
1353
|
+
backendNodeId,
|
|
1354
|
+
});
|
|
1355
|
+
if (frameId)
|
|
1356
|
+
req.frameId = frameId;
|
|
1357
|
+
const resp = await this.client.revealDomNode(req);
|
|
1358
|
+
return { path: resp.path, seq: Number(resp.seq) };
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* A frame's mutation counter, incremented on every change the document sees.
|
|
1362
|
+
* O(1) in the browser and the change detector to poll if you are not
|
|
1363
|
+
* consuming mirror events.
|
|
1364
|
+
*
|
|
1365
|
+
* Prefer this over {@link CloudBrowser.getDOMHash}, which serializes the
|
|
1366
|
+
* whole tree just to hash it. The two answer different questions: a hash
|
|
1367
|
+
* compares content, a revision only says whether this document moved since
|
|
1368
|
+
* you last asked.
|
|
1369
|
+
*/
|
|
1370
|
+
async getDomRevision(frameId = "") {
|
|
1371
|
+
const req = create(GetDomRevisionRequestSchema, {
|
|
1372
|
+
sessionId: this.sessionId,
|
|
1373
|
+
apiKey: this.apiKey,
|
|
1374
|
+
});
|
|
1375
|
+
if (frameId)
|
|
1376
|
+
req.frameId = frameId;
|
|
1377
|
+
const resp = await this.client.getDomRevision(req);
|
|
1378
|
+
return Number(resp.revision);
|
|
1379
|
+
}
|
|
1380
|
+
// ──────────────────────────────────────────────────────────────────
|
|
1050
1381
|
// Cookies
|
|
1051
1382
|
// ──────────────────────────────────────────────────────────────────
|
|
1052
1383
|
/**
|
|
@@ -1193,6 +1524,54 @@ export class CloudBrowser {
|
|
|
1193
1524
|
await this.client.clearStorage(req);
|
|
1194
1525
|
}
|
|
1195
1526
|
// ──────────────────────────────────────────────────────────────────
|
|
1527
|
+
// Auth / DBSC (portable signed-in persona)
|
|
1528
|
+
// ──────────────────────────────────────────────────────────────────
|
|
1529
|
+
/**
|
|
1530
|
+
* Exports the signed-in primary account and DBSC sessions of this
|
|
1531
|
+
* browser context.
|
|
1532
|
+
*
|
|
1533
|
+
* State is read in the browser process, so no page needs to be open.
|
|
1534
|
+
* Returns undefined when the context has neither a signed-in account
|
|
1535
|
+
* nor DBSC sessions.
|
|
1536
|
+
*
|
|
1537
|
+
* @returns AuthSession, or undefined when there is nothing to export
|
|
1538
|
+
*
|
|
1539
|
+
* @throws UNKNOWN_ERROR - the auth session could not be read
|
|
1540
|
+
*
|
|
1541
|
+
* @example
|
|
1542
|
+
* const auth = await browser.getAuthSession();
|
|
1543
|
+
* if (auth) await fs.writeFile("auth.json", JSON.stringify(auth));
|
|
1544
|
+
*/
|
|
1545
|
+
async getAuthSession() {
|
|
1546
|
+
const resp = await this.client.getAuthSession(create(GetAuthSessionRequestSchema, {
|
|
1547
|
+
sessionId: this.sessionId,
|
|
1548
|
+
apiKey: this.apiKey,
|
|
1549
|
+
}));
|
|
1550
|
+
return resp.session ? authSessionFromProto(resp.session) : undefined;
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Imports an auth session so the context comes up signed in (and syncing
|
|
1554
|
+
* if syncConsent) with its DBSC sessions restored.
|
|
1555
|
+
*
|
|
1556
|
+
* Call it before navigating. Pair with setCookies() / setStorage() to
|
|
1557
|
+
* restore a full persona.
|
|
1558
|
+
*
|
|
1559
|
+
* @param session - session as returned by getAuthSession()
|
|
1560
|
+
*
|
|
1561
|
+
* @throws UNKNOWN_ERROR - the auth session could not be written
|
|
1562
|
+
*
|
|
1563
|
+
* @example
|
|
1564
|
+
* await browser.setAuthSession(saved);
|
|
1565
|
+
* await browser.navigate("https://mail.google.com");
|
|
1566
|
+
*/
|
|
1567
|
+
async setAuthSession(session) {
|
|
1568
|
+
await this.client.setAuthSession(create(SetAuthSessionRequestSchema, {
|
|
1569
|
+
sessionId: this.sessionId,
|
|
1570
|
+
apiKey: this.apiKey,
|
|
1571
|
+
session: authSessionToProto(session),
|
|
1572
|
+
}));
|
|
1573
|
+
}
|
|
1574
|
+
// ──────────────────────────────────────────────────────────────────
|
|
1196
1575
|
// Devtools / live-UI helpers
|
|
1197
1576
|
// ──────────────────────────────────────────────────────────────────
|
|
1198
1577
|
/**
|
|
@@ -1464,20 +1843,20 @@ export class CloudBrowser {
|
|
|
1464
1843
|
}
|
|
1465
1844
|
/**
|
|
1466
1845
|
* Answers your WebRTC SDP offer and starts streaming the page as a video
|
|
1467
|
-
* track
|
|
1468
|
-
* The browser is the answerer; you are the offerer (see
|
|
1846
|
+
* track. The browser is the answerer; you are the offerer (see
|
|
1469
1847
|
* {@link getStreamConfig} for the credentials to build the offer).
|
|
1470
1848
|
*
|
|
1471
1849
|
* @param offerSdp - your `RTCPeerConnection`'s SDP offer
|
|
1472
1850
|
*
|
|
1473
|
-
* @returns the SDP answer to apply as the remote description
|
|
1851
|
+
* @returns the SDP answer to apply as the remote description, plus the
|
|
1852
|
+
* viewport to map input coordinates into
|
|
1474
1853
|
*
|
|
1475
1854
|
* @throws UNKNOWN_ERROR - the offer was empty, TURN is unconfigured, or the
|
|
1476
1855
|
* browser could not negotiate the stream
|
|
1477
1856
|
*
|
|
1478
1857
|
* @example
|
|
1479
|
-
* const
|
|
1480
|
-
* await pc.setRemoteDescription({ type: "answer", sdp:
|
|
1858
|
+
* const { answerSdp, viewport } = await browser.startStream(offer.sdp);
|
|
1859
|
+
* await pc.setRemoteDescription({ type: "answer", sdp: answerSdp });
|
|
1481
1860
|
*/
|
|
1482
1861
|
async startStream(offerSdp) {
|
|
1483
1862
|
const resp = await this.client.startStream(create(StartStreamRequestSchema, {
|
|
@@ -1485,7 +1864,12 @@ export class CloudBrowser {
|
|
|
1485
1864
|
apiKey: this.apiKey,
|
|
1486
1865
|
offerSdp,
|
|
1487
1866
|
}));
|
|
1488
|
-
return
|
|
1867
|
+
return {
|
|
1868
|
+
answerSdp: resp.answerSdp,
|
|
1869
|
+
viewport: resp.viewport
|
|
1870
|
+
? { width: resp.viewport.width, height: resp.viewport.height }
|
|
1871
|
+
: null,
|
|
1872
|
+
};
|
|
1489
1873
|
}
|
|
1490
1874
|
/**
|
|
1491
1875
|
* Tears down the live video stream for the session's page. Safe to call even
|
|
@@ -1613,6 +1997,236 @@ export class CloudBrowser {
|
|
|
1613
1997
|
visible: r.visible,
|
|
1614
1998
|
}));
|
|
1615
1999
|
}
|
|
2000
|
+
// ──────────────────────────────────────────────────────────────────
|
|
2001
|
+
// Scripts
|
|
2002
|
+
// ──────────────────────────────────────────────────────────────────
|
|
2003
|
+
/**
|
|
2004
|
+
* Runs `source` in the session's browser and waits for it to finish.
|
|
2005
|
+
*
|
|
2006
|
+
* The script executes in a V8 isolate inside the browser process, not in the
|
|
2007
|
+
* page, and reaches the same operations this SDK exposes through a `browser`
|
|
2008
|
+
* object it is handed. The difference is cost: each call is a function call in
|
|
2009
|
+
* the browser rather than a network round trip, so work that is chatty by
|
|
2010
|
+
* nature — polling for a selector, walking a list, following pagination —
|
|
2011
|
+
* runs in microseconds per step instead of tens of milliseconds.
|
|
2012
|
+
*
|
|
2013
|
+
* This waits for as long as the script runs, and cannot be bounded: the run
|
|
2014
|
+
* id needed to cancel only arrives with the reply. Use
|
|
2015
|
+
* {@link CloudBrowser.startScript} when the script may outlive the caller's
|
|
2016
|
+
* patience, or {@link CloudBrowser.stopScripts} to abandon what this session
|
|
2017
|
+
* is running.
|
|
2018
|
+
*
|
|
2019
|
+
* @param source - JavaScript to execute; its return value comes back as JSON
|
|
2020
|
+
*
|
|
2021
|
+
* @returns the return value and the script's whole console output. A script
|
|
2022
|
+
* that threw is reported as `success: false`, not as a rejection
|
|
2023
|
+
*
|
|
2024
|
+
* @throws UNKNOWN_ERROR - the script could not be delivered to the browser
|
|
2025
|
+
*
|
|
2026
|
+
* @example
|
|
2027
|
+
* const result = await browser.runScript(`
|
|
2028
|
+
* await browser.navigate("https://example.com");
|
|
2029
|
+
* const items = [];
|
|
2030
|
+
* for (const el of await browser.getDOM().querySelectorAll("h1")) {
|
|
2031
|
+
* items.push(el.textContent);
|
|
2032
|
+
* }
|
|
2033
|
+
* return items;
|
|
2034
|
+
* `);
|
|
2035
|
+
* console.log(result.success, result.result);
|
|
2036
|
+
*/
|
|
2037
|
+
async runScript(source) {
|
|
2038
|
+
const resp = await this.client.runScript(create(RunScriptRequestSchema, {
|
|
2039
|
+
sessionId: this.sessionId,
|
|
2040
|
+
apiKey: this.apiKey,
|
|
2041
|
+
source,
|
|
2042
|
+
}));
|
|
2043
|
+
return {
|
|
2044
|
+
success: resp.success,
|
|
2045
|
+
result: resp.result,
|
|
2046
|
+
runId: resp.runId,
|
|
2047
|
+
log: resp.log.map(scriptLogEntryFromProto),
|
|
2048
|
+
truncated: resp.truncated,
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
/**
|
|
2052
|
+
* Launches `source` in the session's browser and resolves as soon as the run
|
|
2053
|
+
* is under way.
|
|
2054
|
+
*
|
|
2055
|
+
* The counterpart to {@link CloudBrowser.runScript}, for scripts that are not
|
|
2056
|
+
* worth waiting on: a watcher that runs for the life of the session, work
|
|
2057
|
+
* that should survive this page. Output arrives at `onEvent` while the caller
|
|
2058
|
+
* gets on with something else, and {@link ScriptRun.wait} collects the
|
|
2059
|
+
* outcome if it is wanted.
|
|
2060
|
+
*
|
|
2061
|
+
* Subscribing has to happen before the launch, because a detached run's
|
|
2062
|
+
* output is not kept anywhere — the browser rejects a start with nobody
|
|
2063
|
+
* listening rather than discard the script's log and result. This call does
|
|
2064
|
+
* both in that order, so nothing the script prints is missed.
|
|
2065
|
+
*
|
|
2066
|
+
* @param source - JavaScript to execute
|
|
2067
|
+
* @param onEvent - called per log line and once for the outcome; see
|
|
2068
|
+
* {@link ScriptEventHandler} for the ordering and blocking rules
|
|
2069
|
+
*
|
|
2070
|
+
* @returns ScriptRun handle for awaiting or cancelling the run
|
|
2071
|
+
*
|
|
2072
|
+
* @throws UNKNOWN_ERROR - the run could not be started
|
|
2073
|
+
*
|
|
2074
|
+
* @example
|
|
2075
|
+
* const run = await browser.startScript(source, (ev) => {
|
|
2076
|
+
* if (ev.log) console.log(ev.log.level, ev.log.message);
|
|
2077
|
+
* });
|
|
2078
|
+
* const outcome = await run.wait();
|
|
2079
|
+
*/
|
|
2080
|
+
async startScript(source, onEvent) {
|
|
2081
|
+
// Subscribe unfiltered: the run id this handle filters on does not exist
|
|
2082
|
+
// yet. Events for it pile up in the server's per-reader buffer between the
|
|
2083
|
+
// subscription and the launch, which is what that buffer is for.
|
|
2084
|
+
const abort = new AbortController();
|
|
2085
|
+
let subscribed;
|
|
2086
|
+
const ready = new Promise((resolve) => {
|
|
2087
|
+
subscribed = resolve;
|
|
2088
|
+
});
|
|
2089
|
+
const stream = this.client.streamScriptEvents(create(StreamScriptEventsRequestSchema, {
|
|
2090
|
+
sessionId: this.sessionId,
|
|
2091
|
+
apiKey: this.apiKey,
|
|
2092
|
+
}), { signal: abort.signal, onHeader: () => subscribed() });
|
|
2093
|
+
// Opening a stream does not wait for the server to start handling it. For a
|
|
2094
|
+
// capture that would only cost the first few events; here it would fail the
|
|
2095
|
+
// launch outright, since the browser refuses to start a run before a
|
|
2096
|
+
// subscription exists. The headers arrive once it is subscribed.
|
|
2097
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
2098
|
+
const first = iterator.next();
|
|
2099
|
+
// A stream that fails or closes before it is acknowledged would leave the
|
|
2100
|
+
// wait below hanging, so race the two outcomes. An event arriving ahead of
|
|
2101
|
+
// the headers is not a failure — it proves the subscription exists.
|
|
2102
|
+
const ended = first.then((result) => {
|
|
2103
|
+
if (result.done) {
|
|
2104
|
+
throw new BrowserScaleError("browserscale.startScript: stream closed before the subscription was established");
|
|
2105
|
+
}
|
|
2106
|
+
});
|
|
2107
|
+
ended.catch(() => { }); // the loser of the race must not look unhandled
|
|
2108
|
+
try {
|
|
2109
|
+
await Promise.race([ready, ended]);
|
|
2110
|
+
}
|
|
2111
|
+
catch (err) {
|
|
2112
|
+
abort.abort();
|
|
2113
|
+
throw err;
|
|
2114
|
+
}
|
|
2115
|
+
let runId;
|
|
2116
|
+
try {
|
|
2117
|
+
const resp = await this.client.startScript(create(StartScriptRequestSchema, {
|
|
2118
|
+
sessionId: this.sessionId,
|
|
2119
|
+
apiKey: this.apiKey,
|
|
2120
|
+
source,
|
|
2121
|
+
}));
|
|
2122
|
+
runId = resp.runId;
|
|
2123
|
+
}
|
|
2124
|
+
catch (err) {
|
|
2125
|
+
abort.abort();
|
|
2126
|
+
throw err;
|
|
2127
|
+
}
|
|
2128
|
+
// Resume from the pending read rather than iterating the stream again: the
|
|
2129
|
+
// first next() is already in flight and its value would otherwise be lost.
|
|
2130
|
+
const resumed = resumeStream(iterator, first);
|
|
2131
|
+
return new ScriptRun({
|
|
2132
|
+
runId,
|
|
2133
|
+
stream: resumed,
|
|
2134
|
+
onEvent,
|
|
2135
|
+
abort,
|
|
2136
|
+
cancel: (id) => this.stopScripts(id),
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
/**
|
|
2140
|
+
* Watches script output in this session without starting anything.
|
|
2141
|
+
*
|
|
2142
|
+
* For the case {@link CloudBrowser.startScript} cannot cover: a run somebody
|
|
2143
|
+
* else launched, or one this page started before it reloaded. Several readers
|
|
2144
|
+
* can watch the same session, each with its own buffer.
|
|
2145
|
+
*
|
|
2146
|
+
* Only output produced from now on arrives — lines printed before the
|
|
2147
|
+
* subscription existed are not kept. A run that has already finished is
|
|
2148
|
+
* therefore invisible here; {@link CloudBrowser.listScriptRuns} is how you
|
|
2149
|
+
* tell that apart from a run that is merely quiet.
|
|
2150
|
+
*
|
|
2151
|
+
* @param runId - run to follow, or `""` to follow every run in the session
|
|
2152
|
+
* @param onEvent - called per event; see {@link ScriptEventHandler} for the
|
|
2153
|
+
* ordering and blocking rules
|
|
2154
|
+
*
|
|
2155
|
+
* @returns ScriptFollow handle for stopping the subscription
|
|
2156
|
+
*
|
|
2157
|
+
* @throws UNKNOWN_ERROR - the subscription could not be opened
|
|
2158
|
+
*
|
|
2159
|
+
* @example
|
|
2160
|
+
* const follow = await browser.followScript(runId, (ev) => {
|
|
2161
|
+
* if (ev.log) console.log(ev.log.message);
|
|
2162
|
+
* });
|
|
2163
|
+
* try {
|
|
2164
|
+
* await follow.wait();
|
|
2165
|
+
* } finally {
|
|
2166
|
+
* await follow.stop();
|
|
2167
|
+
* }
|
|
2168
|
+
*/
|
|
2169
|
+
async followScript(runId, onEvent) {
|
|
2170
|
+
const abort = new AbortController();
|
|
2171
|
+
const stream = this.client.streamScriptEvents(create(StreamScriptEventsRequestSchema, {
|
|
2172
|
+
sessionId: this.sessionId,
|
|
2173
|
+
apiKey: this.apiKey,
|
|
2174
|
+
runId,
|
|
2175
|
+
}), { signal: abort.signal });
|
|
2176
|
+
return new ScriptFollow({ stream, onEvent, abort });
|
|
2177
|
+
}
|
|
2178
|
+
/**
|
|
2179
|
+
* Cancels runs in this session and reports how many it ended.
|
|
2180
|
+
*
|
|
2181
|
+
* An empty `runId` cancels every run in the session, which is the only form
|
|
2182
|
+
* available to a caller that never learned an id — notably one abandoning a
|
|
2183
|
+
* {@link CloudBrowser.runScript}.
|
|
2184
|
+
*
|
|
2185
|
+
* @param runId - run to cancel, or `""` for all of them
|
|
2186
|
+
*
|
|
2187
|
+
* @returns how many runs were cancelled; 0 when the id named nothing in
|
|
2188
|
+
* flight
|
|
2189
|
+
*
|
|
2190
|
+
* @throws UNKNOWN_ERROR - the cancel could not be delivered
|
|
2191
|
+
*
|
|
2192
|
+
* @example
|
|
2193
|
+
* await browser.stopScripts(""); // abandon everything running
|
|
2194
|
+
*/
|
|
2195
|
+
async stopScripts(runId) {
|
|
2196
|
+
const resp = await this.client.stopScripts(create(StopScriptsRequestSchema, {
|
|
2197
|
+
sessionId: this.sessionId,
|
|
2198
|
+
apiKey: this.apiKey,
|
|
2199
|
+
runId,
|
|
2200
|
+
}));
|
|
2201
|
+
return resp.stopped;
|
|
2202
|
+
}
|
|
2203
|
+
/**
|
|
2204
|
+
* Reports the scripts still running in this session.
|
|
2205
|
+
*
|
|
2206
|
+
* Only runs in flight — a finished run is reported once on the event stream
|
|
2207
|
+
* and then forgotten, so this is not a history. Its use is finding work this
|
|
2208
|
+
* caller did not start: a script a previous page left behind, which
|
|
2209
|
+
* {@link CloudBrowser.stopScripts} needs an id to name.
|
|
2210
|
+
*
|
|
2211
|
+
* @returns one entry per run still executing
|
|
2212
|
+
*
|
|
2213
|
+
* @throws UNKNOWN_ERROR - the session could not be queried
|
|
2214
|
+
*
|
|
2215
|
+
* @example
|
|
2216
|
+
* for (const run of await browser.listScriptRuns()) {
|
|
2217
|
+
* console.log(run.runId, run.runningMs);
|
|
2218
|
+
* }
|
|
2219
|
+
*/
|
|
2220
|
+
async listScriptRuns() {
|
|
2221
|
+
const resp = await this.client.listScriptRuns(create(ListScriptRunsRequestSchema, {
|
|
2222
|
+
sessionId: this.sessionId,
|
|
2223
|
+
apiKey: this.apiKey,
|
|
2224
|
+
}));
|
|
2225
|
+
return resp.runs.map((run) => ({
|
|
2226
|
+
runId: run.runId,
|
|
2227
|
+
runningMs: Number(run.runningMs),
|
|
2228
|
+
}));
|
|
2229
|
+
}
|
|
1616
2230
|
/**
|
|
1617
2231
|
* @internal — a reaction match/action must be a css()/js() locator: node()
|
|
1618
2232
|
* and at() are rejected (a reaction watches for a condition, like a wait).
|
|
@@ -1626,3 +2240,18 @@ export class CloudBrowser {
|
|
|
1626
2240
|
}
|
|
1627
2241
|
}
|
|
1628
2242
|
}
|
|
2243
|
+
/**
|
|
2244
|
+
* Continues an async iterator whose first read is already in flight.
|
|
2245
|
+
*
|
|
2246
|
+
* startScript has to know the subscription exists before it launches anything,
|
|
2247
|
+
* and the only way to make the client send the request is to start reading. That
|
|
2248
|
+
* read cannot be discarded — it may already hold the run's first log line — so
|
|
2249
|
+
* the reader is handed a stream that replays it before continuing.
|
|
2250
|
+
*/
|
|
2251
|
+
async function* resumeStream(iterator, first) {
|
|
2252
|
+
let result = await first;
|
|
2253
|
+
while (!result.done) {
|
|
2254
|
+
yield result.value;
|
|
2255
|
+
result = await iterator.next();
|
|
2256
|
+
}
|
|
2257
|
+
}
|