wrc-ts 0.1.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/client.js ADDED
@@ -0,0 +1,1264 @@
1
+ import { createClient } from "@connectrpc/connect";
2
+ import { create } from "@bufbuild/protobuf";
3
+ import { WRC,
4
+ // request / response schemas
5
+ SetProxyRequestSchema, GetPagesRequestSchema, NavigateRequestSchema, LoadHTMLRequestSchema, EvaluateRequestSchema, WaitForAnyParamsSchema, WaitConditionSchema, ClickRequestSchema, FillRequestSchema, MoveToRequestSchema, ScrollToRequestSchema, DragRequestSchema, SelectOptionRequestSchema, GetDOMRequestSchema, GetDOMHashRequestSchema, GetObservationRequestSchema, SetBlockListRequestSchema, SetStaticPathsRequestSchema, WaitForAnyRequestRequestSchema, WaitForAnyResponseRequestSchema, ModifyRequestRequestSchema, GetCookiesRequestSchema, SetCookiesRequestSchema, ClearCookiesRequestSchema, GetStorageRequestSchema, SetStorageRequestSchema, ClearStorageRequestSchema, InspectAtPositionRequestSchema, HighlightNodeRequestSchema, InsertTextRequestSchema, PressKeyRequestSchema, ReleaseKeyRequestSchema, GetSelectionRequestSchema, SolveCaptchaRequestSchema, } from "./gen/wrc_pb.js";
6
+ import { DefaultWaitTimeoutMs } from "./defaults.js";
7
+ import { WRCError } from "./errors.js";
8
+ import { cookieParamFromProto, cookieParamsToProto, dragResultFromProto, elementFields, elementResultFromProto, headerModsToProto, headersToProto, interceptedRequestFromProto, interceptedResponseFromProto, pageInfoFromProto, rectFromProto, splitRequestPatterns, storageEntriesToProto, storageEntryFromProto, waitResultFromProto, } from "./internal/convert.js";
9
+ /**
10
+ * CloudBrowser is the SDK-side handle for an active WRC browser session.
11
+ *
12
+ * One CloudBrowser corresponds to exactly one browser context, which
13
+ * always has at least one page. The session is implicitly bound to its
14
+ * primary page server-side — the proto's page_id field is currently
15
+ * ignored server-side, so the SDK never sets it.
16
+ *
17
+ * Construct via rentBrowser() / createWebSocketBrowser() — never
18
+ * directly.
19
+ */
20
+ export class CloudBrowser {
21
+ constructor(transport, sessionId, apiKey, fingerprint, stopFn) {
22
+ this._transport = transport;
23
+ this.sessionId = sessionId;
24
+ this.apiKey = apiKey;
25
+ this._fingerprint = fingerprint;
26
+ this.client = createClient(WRC, transport);
27
+ this._stopFn = stopFn;
28
+ }
29
+ // ──────────────────────────────────────────────────────────────────
30
+ // Lifecycle / metadata
31
+ // ──────────────────────────────────────────────────────────────────
32
+ /** Returns the unique server-assigned id for this browser session. */
33
+ getSessionId() {
34
+ return this.sessionId;
35
+ }
36
+ /** Returns the API key used to rent this session. */
37
+ getApiKey() {
38
+ return this.apiKey;
39
+ }
40
+ /** Returns the browser fingerprint id in use for this session. */
41
+ getFingerprint() {
42
+ return this._fingerprint;
43
+ }
44
+ /**
45
+ * Releases the session and closes the underlying transport.
46
+ *
47
+ * For sessions created via {@link rentBrowser} this also calls the WRC
48
+ * stop endpoint to release the rental. For sessions attached with
49
+ * {@link createWebSocketBrowser} the rental stays untouched; only the
50
+ * transport is closed.
51
+ *
52
+ * @throws UNKNOWN_ERROR - the stop API or the transport close failed
53
+ *
54
+ * @example
55
+ * await browser.stopBrowser();
56
+ */
57
+ async stopBrowser() {
58
+ if (this._stopFn) {
59
+ await this._stopFn();
60
+ }
61
+ }
62
+ // ──────────────────────────────────────────────────────────────────
63
+ // Context-level
64
+ // ──────────────────────────────────────────────────────────────────
65
+ /**
66
+ * Changes the runtime proxy for this session.
67
+ *
68
+ * Takes effect for new requests immediately; in-flight requests keep
69
+ * their original routing. Pass an empty proxyHost to clear the proxy and
70
+ * route directly.
71
+ *
72
+ * @param proxyHost - upstream proxy host; empty disables the proxy
73
+ * @param proxyPort - upstream proxy port; ignored when proxyHost is empty
74
+ * @param proxyUsername - proxy auth user; empty for unauthenticated proxies
75
+ * @param proxyPassword - proxy auth password; empty for unauthenticated proxies
76
+ *
77
+ * @throws UNKNOWN_ERROR - the proxy could not be applied
78
+ *
79
+ * @example
80
+ * await browser.setProxy("proxy.example.com", 8080, "user", "pass");
81
+ */
82
+ async setProxy(proxyHost, proxyPort, proxyUsername = "", proxyPassword = "") {
83
+ const req = create(SetProxyRequestSchema, {
84
+ sessionId: this.sessionId,
85
+ apiKey: this.apiKey,
86
+ });
87
+ if (proxyHost) {
88
+ req.proxyHost = proxyHost;
89
+ req.proxyPort = proxyPort;
90
+ if (proxyUsername)
91
+ req.proxyUsername = proxyUsername;
92
+ if (proxyPassword)
93
+ req.proxyPassword = proxyPassword;
94
+ }
95
+ await this.client.setProxy(req);
96
+ }
97
+ /**
98
+ * Returns all open pages (tabs and popups) for this session's browser
99
+ * context.
100
+ *
101
+ * Each PageInfo carries the page's URL, title, viewport and a full
102
+ * nested frame tree (out-of-process iframes are children of the page's
103
+ * main frame).
104
+ *
105
+ * @returns PageInfo[] for every page currently open in the context
106
+ *
107
+ * @throws UNKNOWN_ERROR - the pages could not be enumerated
108
+ *
109
+ * @example
110
+ * const pages = await browser.getPages();
111
+ * for (const p of pages) console.log(p.url, p.title);
112
+ */
113
+ async getPages() {
114
+ const resp = await this.client.getPages(create(GetPagesRequestSchema, {
115
+ sessionId: this.sessionId,
116
+ apiKey: this.apiKey,
117
+ }));
118
+ return resp.pages.map(pageInfoFromProto);
119
+ }
120
+ // ──────────────────────────────────────────────────────────────────
121
+ // Page navigation / content
122
+ // ──────────────────────────────────────────────────────────────────
123
+ /**
124
+ * Navigates the page to url.
125
+ *
126
+ * Returns once the primary main-frame navigation commits (the response
127
+ * is received and a new document is selected), before DOMContentLoaded or
128
+ * load fire. Cross-origin redirects are followed.
129
+ *
130
+ * @param url - destination URL
131
+ * @param opts - optional navigation customization (e.g. timeoutMs)
132
+ *
133
+ * @returns NavigateResult with the final resolved URL and the frameId
134
+ * of the main frame after navigation
135
+ *
136
+ * @throws UNKNOWN_ERROR - the navigation failed or timed out
137
+ *
138
+ * @example
139
+ * await browser.navigate("https://example.com");
140
+ */
141
+ async navigate(url, opts) {
142
+ const req = create(NavigateRequestSchema, {
143
+ sessionId: this.sessionId,
144
+ apiKey: this.apiKey,
145
+ url,
146
+ });
147
+ if (opts?.timeoutMs)
148
+ req.timeout = opts.timeoutMs;
149
+ const resp = await this.client.navigate(req);
150
+ return { frameId: resp.frameId, url: resp.url };
151
+ }
152
+ /**
153
+ * Serves a synthetic response for the next navigation to url.
154
+ *
155
+ * Registers a one-shot interceptor that intercepts the next request to
156
+ * url and replies with the supplied html and headers instead of going to
157
+ * the network. Useful for snapshotted pages, test fixtures, and offline
158
+ * replays. Pair with {@link navigate} to trigger the load.
159
+ *
160
+ * @param url - URL pattern that, when navigated to, returns the html
161
+ * @param html - response body to serve
162
+ * @param opts - optional headers and statusCode (default 200)
163
+ *
164
+ * @throws UNKNOWN_ERROR - the interceptor could not be installed
165
+ *
166
+ * @example
167
+ * await browser.loadHTML("https://example.com", "<h1>hi</h1>");
168
+ * await browser.navigate("https://example.com");
169
+ */
170
+ async loadHTML(url, html, opts) {
171
+ const req = create(LoadHTMLRequestSchema, {
172
+ sessionId: this.sessionId,
173
+ apiKey: this.apiKey,
174
+ url,
175
+ html,
176
+ headers: headersToProto(opts?.headers),
177
+ });
178
+ if (opts?.statusCode)
179
+ req.statusCode = opts.statusCode;
180
+ await this.client.loadHTML(req);
181
+ }
182
+ // ──────────────────────────────────────────────────────────────────
183
+ // Evaluation
184
+ // ──────────────────────────────────────────────────────────────────
185
+ /**
186
+ * Runs a JavaScript expression in the page's main frame.
187
+ *
188
+ * The expression's return value is JSON-serialized server-side and
189
+ * parsed eagerly into `.value`. When the expression returns a DOM
190
+ * element the `.value` is null and the element metadata
191
+ * (backendNodeId, isVisible, bounds) is populated instead — use
192
+ * {@link node} in subsequent calls to act on it.
193
+ *
194
+ * The generic T is a TypeScript hint only — there is no runtime
195
+ * validation that the JS expression actually returned that type.
196
+ *
197
+ * @param expression - JavaScript expression evaluated in the main frame
198
+ *
199
+ * @returns EvaluateResult with either value (non-Element) or element
200
+ * metadata (Element)
201
+ *
202
+ * @throws UNKNOWN_ERROR - the expression threw or could not be compiled
203
+ *
204
+ * @example
205
+ * const res = await browser.evaluate<string>("document.title");
206
+ * console.log(res.value);
207
+ */
208
+ async evaluate(expression) {
209
+ return this._evaluate("", expression);
210
+ }
211
+ /**
212
+ * Runs a JavaScript expression in the given frame.
213
+ *
214
+ * Same semantics as {@link evaluate} but targets a specific frame
215
+ * instead of the main frame. Useful for evaluating inside OOPIFs (out-
216
+ * of-process iframes) found via {@link getPages}. ALL_FRAMES is not
217
+ * supported here.
218
+ *
219
+ * @inheritDoc CloudBrowser.evaluate
220
+ * @param frameId - id of the frame to evaluate in
221
+ *
222
+ * @example
223
+ * const pages = await browser.getPages();
224
+ * const iframeId = pages[0].frameTree.children[0].frameId;
225
+ * await browser.evaluateInFrame(iframeId, "location.href");
226
+ */
227
+ async evaluateInFrame(frameId, expression) {
228
+ return this._evaluate(frameId, expression);
229
+ }
230
+ async _evaluate(frameId, expression) {
231
+ const req = create(EvaluateRequestSchema, {
232
+ sessionId: this.sessionId,
233
+ apiKey: this.apiKey,
234
+ expression,
235
+ });
236
+ if (frameId)
237
+ req.frameId = frameId;
238
+ const resp = await this.client.evaluate(req);
239
+ let value = null;
240
+ if (resp.result !== "") {
241
+ try {
242
+ value = JSON.parse(resp.result);
243
+ }
244
+ catch {
245
+ value = resp.result;
246
+ }
247
+ }
248
+ return {
249
+ value: value,
250
+ backendNodeId: resp.backendNodeId,
251
+ isVisible: resp.isVisible,
252
+ bounds: rectFromProto(resp.bounds),
253
+ };
254
+ }
255
+ // ──────────────────────────────────────────────────────────────────
256
+ // Waiting
257
+ // ──────────────────────────────────────────────────────────────────
258
+ /**
259
+ * Blocks until the given locator matches.
260
+ *
261
+ * Shortcut for `waitAny([condition], opts)`. See {@link waitAny} for
262
+ * timeout handling, per-locator visible/steady defaults and the list of
263
+ * locators that are not valid wait conditions.
264
+ *
265
+ * @inheritDoc CloudBrowser.waitAny
266
+ * @param condition - the single locator to wait for
267
+ *
268
+ * @example
269
+ * await browser.wait(css(".success"));
270
+ */
271
+ async wait(condition, opts) {
272
+ return this.waitAny([condition], opts);
273
+ }
274
+ /**
275
+ * Blocks until any of the supplied locators matches.
276
+ *
277
+ * When several conditions are supplied, the first one to match wins;
278
+ * the others are abandoned. The returned WaitResult's `.index` points
279
+ * to the entry in `conditions` that matched.
280
+ *
281
+ * Defaults applied automatically:
282
+ * - timeout: 30000 ms — override via `opts.timeoutMs`
283
+ * - per-locator visible/steady: `true` / `500` for css() and js()
284
+ * locators. For js() expressions returning a non-Element value both
285
+ * flags are no-ops. Override with `.visible(false)` / `.steady(ms)`
286
+ * on individual locators.
287
+ *
288
+ * {@link node} and {@link at} are not valid wait conditions — they only
289
+ * make sense as action targets — and throw at send time.
290
+ *
291
+ * @param conditions - one or more {@link Locator}s to wait for; must be non-empty
292
+ * @param opts - optional wait customization (timeoutMs)
293
+ *
294
+ * @returns WaitResult for the first matching condition
295
+ *
296
+ * @throws UNKNOWN_ERROR - the wait timed out or a condition was invalid
297
+ *
298
+ * @example
299
+ * const r = await browser.waitAny(
300
+ * [css(".success"), js("window.__ready === true")],
301
+ * { timeoutMs: 5000 },
302
+ * );
303
+ * console.log("matched index:", r.index);
304
+ */
305
+ async waitAny(conditions, opts) {
306
+ if (conditions.length === 0) {
307
+ throw new WRCError("wrc.waitAny: at least one condition required");
308
+ }
309
+ const pbConds = conditions.map((cond, i) => {
310
+ if (!cond.selector && !cond.jsExpression) {
311
+ throw new WRCError(`wrc.waitAny: condition ${i} has neither selector nor JS expression (node()/at() are not valid wait conditions)`);
312
+ }
313
+ const pc = create(WaitConditionSchema);
314
+ if (cond.selector)
315
+ pc.selector = cond.selector;
316
+ if (cond.jsExpression)
317
+ pc.jsExpression = cond.jsExpression;
318
+ if (cond.visibleFlag !== undefined)
319
+ pc.visible = cond.visibleFlag;
320
+ if (cond.steadyMs !== undefined)
321
+ pc.steadyTime = cond.steadyMs;
322
+ return pc;
323
+ });
324
+ // Call-level frameId: take from first condition that has one (Wait
325
+ // doesn't accept a separate frame override yet — mirrors Go wait.go).
326
+ let frameId = "";
327
+ for (const c of conditions) {
328
+ if (c.frameId) {
329
+ frameId = c.frameId;
330
+ break;
331
+ }
332
+ }
333
+ const req = create(WaitForAnyParamsSchema, {
334
+ sessionId: this.sessionId,
335
+ apiKey: this.apiKey,
336
+ conditions: pbConds,
337
+ timeout: opts?.timeoutMs ?? DefaultWaitTimeoutMs,
338
+ });
339
+ if (frameId)
340
+ req.frameId = frameId;
341
+ const resp = await this.client.waitForAny(req);
342
+ return waitResultFromProto(resp);
343
+ }
344
+ // ──────────────────────────────────────────────────────────────────
345
+ // Element actions
346
+ // ──────────────────────────────────────────────────────────────────
347
+ /**
348
+ * Triggers a single left mouse click on the given target.
349
+ *
350
+ * The browser scrolls the element into view if needed, moves the
351
+ * cursor along a human-like path, then dispatches a full
352
+ * mouseDown+mouseUp at a randomized point inside the element's
353
+ * bounding rect.
354
+ *
355
+ * For right-click, double-click, press/release-only, or to override
356
+ * the target frame, pass a `ClickOpts` object as the second argument.
357
+ *
358
+ * @param target - locator describing what to click; {@link at} is also valid
359
+ * @param opts - optional click customization; see {@link ClickOpts}
360
+ *
361
+ * @returns ElementResult with success, resolved frameId, backendNodeId,
362
+ * post-scroll isVisible, element bounds, and the root-viewport
363
+ * (rootX, rootY) where the click landed
364
+ *
365
+ * @throws INVALID_LOCATOR - target is empty or has multiple targets set
366
+ * @throws ELEMENT_NOT_FOUND - no element matched the locator
367
+ * @throws FRAME_NOT_FOUND - the requested frame does not exist
368
+ * @throws CLICK_FAILED - the click could not be dispatched
369
+ * @throws TIMEOUT - the operation exceeded the server-side timeout
370
+ * @throws PAGE_NOT_ALIVE - the page has been closed
371
+ *
372
+ * @example
373
+ * await browser.click(css("button.submit"));
374
+ *
375
+ * @example
376
+ * // Right double-click on a context menu trigger.
377
+ * await browser.click(css("li.menu"), { button: "right", clickCount: 2 });
378
+ */
379
+ async click(target, opts) {
380
+ target.validateTarget("click", true);
381
+ const req = create(ClickRequestSchema, {
382
+ sessionId: this.sessionId,
383
+ apiKey: this.apiKey,
384
+ ...elementFields(target, opts?.inFrame),
385
+ });
386
+ if (opts?.button)
387
+ req.button = opts.button;
388
+ if (opts?.clickCount)
389
+ req.clickCount = opts.clickCount;
390
+ if (opts?.action)
391
+ req.action = opts.action;
392
+ const resp = await this.client.click(req);
393
+ return elementResultFromProto(resp);
394
+ }
395
+ /**
396
+ * Clicks the target and types text into it, appending to any existing
397
+ * content.
398
+ *
399
+ * The browser scrolls the element into view, moves the cursor along
400
+ * a human-like path, clicks to focus, then types the text character-
401
+ * by-character with QWERTZ keyboard simulation and human-like timing.
402
+ *
403
+ * To overwrite the field instead of appending, pass `{ clearFirst: true }`.
404
+ *
405
+ * {@link at} is not a valid target — fill requires an actual element.
406
+ *
407
+ * @param target - locator describing the input element
408
+ * @param text - text to type into the element
409
+ * @param opts - optional fill customization; see {@link FillOpts}
410
+ *
411
+ * @returns ElementResult with success, resolved frameId, backendNodeId
412
+ * and the root-viewport (rootX, rootY) where the element was clicked
413
+ *
414
+ * @throws INVALID_LOCATOR - target is empty or has multiple targets set
415
+ * @throws ELEMENT_NOT_FOUND - no element matched the locator
416
+ * @throws FRAME_NOT_FOUND - the requested frame does not exist
417
+ * @throws FILL_FAILED - the input could not be filled
418
+ * @throws TIMEOUT - the operation exceeded the server-side timeout
419
+ * @throws PAGE_NOT_ALIVE - the page has been closed
420
+ *
421
+ * @example
422
+ * await browser.fill(css("input[name=email]"), "user@example.com");
423
+ *
424
+ * @example
425
+ * // Wipe the field first, then type fresh content.
426
+ * await browser.fill(css("input[name=email]"), "user@example.com", { clearFirst: true });
427
+ */
428
+ async fill(target, text, opts) {
429
+ target.validateTarget("fill", false);
430
+ const req = create(FillRequestSchema, {
431
+ sessionId: this.sessionId,
432
+ apiKey: this.apiKey,
433
+ text,
434
+ ...elementFields(target, opts?.inFrame),
435
+ });
436
+ if (opts?.clearFirst)
437
+ req.clearFirst = true;
438
+ const resp = await this.client.fill(req);
439
+ return elementResultFromProto(resp);
440
+ }
441
+ /**
442
+ * Moves the mouse cursor over the given target.
443
+ *
444
+ * The browser scrolls the target into view first if necessary, then
445
+ * animates the cursor along a human-like path to the element's center
446
+ * (or to the viewport coordinate when target is {@link at}).
447
+ *
448
+ * @param target - locator describing where to move; {@link at} is also valid
449
+ *
450
+ * @returns ElementResult with the resolved frameId, backendNodeId,
451
+ * post-scroll isVisible, element bounds and the root-viewport
452
+ * (rootX, rootY) where the cursor ended up
453
+ *
454
+ * @throws UNKNOWN_ERROR - the move could not be completed
455
+ *
456
+ * @example
457
+ * await browser.moveTo(css("nav .menu"));
458
+ */
459
+ async moveTo(target) {
460
+ target.validateTarget("moveTo", true);
461
+ const req = create(MoveToRequestSchema, {
462
+ sessionId: this.sessionId,
463
+ apiKey: this.apiKey,
464
+ ...elementFields(target),
465
+ });
466
+ const resp = await this.client.moveTo(req);
467
+ return elementResultFromProto(resp);
468
+ }
469
+ /**
470
+ * Scrolls the given element into view.
471
+ *
472
+ * Whatever scroll container is closest to the element does the
473
+ * scrolling — nested scroll containers and out-of-process iframe chains
474
+ * are walked automatically. {@link at} is not a valid target here;
475
+ * scrolling needs a real element.
476
+ *
477
+ * @param target - locator describing the element to bring into view;
478
+ * {@link at} is rejected
479
+ *
480
+ * @returns ElementResult with the resolved frameId, backendNodeId,
481
+ * post-scroll isVisible and the element's bounds after the scroll
482
+ *
483
+ * @throws UNKNOWN_ERROR - the element could not be scrolled into view
484
+ *
485
+ * @example
486
+ * await browser.scrollTo(css("#footer"));
487
+ */
488
+ async scrollTo(target) {
489
+ target.validateTarget("scrollTo", false);
490
+ const req = create(ScrollToRequestSchema, {
491
+ sessionId: this.sessionId,
492
+ apiKey: this.apiKey,
493
+ ...elementFields(target),
494
+ });
495
+ const resp = await this.client.scrollTo(req);
496
+ return elementResultFromProto(resp);
497
+ }
498
+ /**
499
+ * Picks up the target and drops it at an offset relative to the pickup
500
+ * point.
501
+ *
502
+ * The browser presses the left mouse button at a pickup point inside
503
+ * the element, drags along a human-like path to (pickupX+offsetX,
504
+ * pickupY+offsetY), then releases. {@link at} is not a valid target —
505
+ * drag needs a real element.
506
+ *
507
+ * @param target - locator describing the element to pick up
508
+ * @param offsetX - horizontal distance to drag, in CSS pixels
509
+ * @param offsetY - vertical distance to drag, in CSS pixels
510
+ *
511
+ * @returns DragResult with the resolved frameId, backendNodeId and the
512
+ * final cursor position (rootX, rootY) where the drop happened
513
+ *
514
+ * @throws UNKNOWN_ERROR - the drag could not be performed
515
+ *
516
+ * @example
517
+ * await browser.dragBy(css(".slider .handle"), 120, 0);
518
+ */
519
+ async dragBy(target, offsetX, offsetY) {
520
+ return this._drag(target, { offsetX, offsetY });
521
+ }
522
+ /**
523
+ * Picks up the target and drops it at absolute root-viewport coordinates.
524
+ *
525
+ * Same gesture as {@link dragBy}, but the drop destination is in page
526
+ * coordinates rather than relative to the pickup point.
527
+ *
528
+ * @param target - locator describing the element to pick up
529
+ * @param absoluteX - horizontal drop coordinate in the root viewport
530
+ * @param absoluteY - vertical drop coordinate in the root viewport
531
+ *
532
+ * @returns DragResult with the resolved frameId, backendNodeId and the
533
+ * final cursor position (rootX, rootY) where the drop happened
534
+ *
535
+ * @throws UNKNOWN_ERROR - the drag could not be performed
536
+ *
537
+ * @example
538
+ * await browser.dragTo(css(".card"), 800, 400);
539
+ */
540
+ async dragTo(target, absoluteX, absoluteY) {
541
+ return this._drag(target, { absoluteX, absoluteY });
542
+ }
543
+ async _drag(target, spec) {
544
+ target.validateTarget("drag", false);
545
+ const req = create(DragRequestSchema, {
546
+ sessionId: this.sessionId,
547
+ apiKey: this.apiKey,
548
+ ...elementFields(target),
549
+ });
550
+ if (spec.offsetX !== undefined)
551
+ req.offsetX = spec.offsetX;
552
+ if (spec.offsetY !== undefined)
553
+ req.offsetY = spec.offsetY;
554
+ if (spec.absoluteX !== undefined)
555
+ req.absoluteX = spec.absoluteX;
556
+ if (spec.absoluteY !== undefined)
557
+ req.absoluteY = spec.absoluteY;
558
+ const resp = await this.client.drag(req);
559
+ return dragResultFromProto(resp);
560
+ }
561
+ /**
562
+ * Picks the `<option>` at the zero-based index inside the targeted
563
+ * `<select>` element.
564
+ *
565
+ * Sets the option as selected on the targeted `<select>`, then fires
566
+ * the standard input + change events (unless suppressed via
567
+ * `opts.fireEvents = false`).
568
+ *
569
+ * {@link at} is not a valid target — select requires an actual
570
+ * `<select>` element.
571
+ *
572
+ * @param target - locator describing the `<select>` element
573
+ * @param index - zero-based option index
574
+ * @param opts - optional select customization; see {@link SelectOpts}
575
+ *
576
+ * @returns SelectOptionResult with the resolved selectedIndex,
577
+ * selectedValue and selectedText after the change
578
+ *
579
+ * @throws INVALID_LOCATOR - target is empty or has multiple targets set
580
+ * @throws ELEMENT_NOT_FOUND - no element matched the locator
581
+ * @throws FRAME_NOT_FOUND - the requested frame does not exist
582
+ * @throws SELECT_FAILED - the option could not be selected
583
+ * (out of range, or element is not a `<select>`)
584
+ * @throws TIMEOUT - the operation exceeded the server-side timeout
585
+ * @throws PAGE_NOT_ALIVE - the page has been closed
586
+ *
587
+ * @example
588
+ * await browser.selectByIndex(css("select#country"), 2);
589
+ *
590
+ * @example
591
+ * // Pick the option silently, no input/change events.
592
+ * await browser.selectByIndex(css("select#hidden"), 0, { fireEvents: false });
593
+ */
594
+ async selectByIndex(target, index, opts) {
595
+ return this._select(target, opts, req => { req.index = index; });
596
+ }
597
+ /**
598
+ * Picks the `<option>` whose `value` attribute matches the given
599
+ * string exactly.
600
+ *
601
+ * @inheritDoc {@link CloudBrowser.selectByIndex}
602
+ * @param value - the `value` attribute to match
603
+ *
604
+ * @example
605
+ * await browser.selectByValue(css("select#country"), "DE");
606
+ */
607
+ async selectByValue(target, value, opts) {
608
+ return this._select(target, opts, req => { req.value = value; });
609
+ }
610
+ /**
611
+ * Picks the `<option>` whose visible (trimmed) text matches the given
612
+ * string exactly.
613
+ *
614
+ * @inheritDoc {@link CloudBrowser.selectByIndex}
615
+ * @param text - the visible option text to match
616
+ *
617
+ * @example
618
+ * await browser.selectByText(css("select#country"), "Germany");
619
+ */
620
+ async selectByText(target, text, opts) {
621
+ return this._select(target, opts, req => { req.text = text; });
622
+ }
623
+ async _select(target, opts, withKey) {
624
+ target.validateTarget("selectOption", false);
625
+ const req = create(SelectOptionRequestSchema, {
626
+ sessionId: this.sessionId,
627
+ apiKey: this.apiKey,
628
+ ...elementFields(target, opts?.inFrame),
629
+ });
630
+ withKey(req);
631
+ if (opts?.fireEvents === false)
632
+ req.fireEvents = false;
633
+ const resp = await this.client.selectOption(req);
634
+ return {
635
+ selectedIndex: resp.selectedIndex,
636
+ selectedValue: resp.selectedValue,
637
+ selectedText: resp.selectedText,
638
+ };
639
+ }
640
+ // ──────────────────────────────────────────────────────────────────
641
+ // DOM / observation
642
+ // ──────────────────────────────────────────────────────────────────
643
+ /**
644
+ * Returns the DOM in CDP DOM.Node shape for the requested frame.
645
+ *
646
+ * The shape matches Chrome DevTools' Protocol DOM.Node — useful for
647
+ * piping into agent loops or visualizers that already speak CDP. For a
648
+ * much smaller agent-oriented payload, prefer {@link getObservation}
649
+ * instead. The cheap polling endpoint is {@link getDOMHash}.
650
+ *
651
+ * @param frameId - id of the frame to dump; empty string targets the main frame
652
+ * @param opts - optional `depth`: -1 full tree (default), 0 root only,
653
+ * N root + N descendant levels
654
+ *
655
+ * @returns DOMResult with the JSON string in `.dom` (the `.hash` field
656
+ * is populated by {@link getDOMHash}, not by this call)
657
+ *
658
+ * @throws UNKNOWN_ERROR - the DOM could not be retrieved
659
+ *
660
+ * @example
661
+ * const { dom } = await browser.getDOM();
662
+ */
663
+ async getDOM(frameId = "", opts) {
664
+ const req = create(GetDOMRequestSchema, {
665
+ sessionId: this.sessionId,
666
+ apiKey: this.apiKey,
667
+ });
668
+ if (frameId)
669
+ req.frameId = frameId;
670
+ if (opts?.depth !== undefined)
671
+ req.depth = opts.depth;
672
+ const resp = await this.client.getDOM(req);
673
+ return { hash: "", dom: resp.dom };
674
+ }
675
+ /**
676
+ * Returns sha256[:8] of the full-tree DOM JSON for cheap polling-based
677
+ * change detection.
678
+ *
679
+ * Computing a hash is much cheaper than transferring the full tree —
680
+ * pair this with {@link getDOM} only when the hash differs from your
681
+ * last snapshot.
682
+ *
683
+ * @param frameId - id of the frame to hash; empty string targets the main frame
684
+ *
685
+ * @returns 16-char hex string (the first 8 bytes of sha256 of the DOM JSON)
686
+ *
687
+ * @throws UNKNOWN_ERROR - the hash could not be computed
688
+ *
689
+ * @example
690
+ * const hash = await browser.getDOMHash();
691
+ * if (hash !== lastHash) {
692
+ * // DOM changed → re-fetch
693
+ * }
694
+ */
695
+ async getDOMHash(frameId = "") {
696
+ const req = create(GetDOMHashRequestSchema, {
697
+ sessionId: this.sessionId,
698
+ apiKey: this.apiKey,
699
+ });
700
+ if (frameId)
701
+ req.frameId = frameId;
702
+ const resp = await this.client.getDOMHash(req);
703
+ return resp.hash;
704
+ }
705
+ /**
706
+ * Returns a compact, agent-friendly description of every interactable
707
+ * element currently visible on the page, together with a truncated view
708
+ * of the surrounding text.
709
+ *
710
+ * Intended as input for LLM/agent loops where a full DOM dump would be
711
+ * too large; the server filters down to elements that are actually
712
+ * visible and interactable.
713
+ *
714
+ * @param opts - optional caps: `maxElementsPerFrame`, `maxTextLength`;
715
+ * omit either to use the server default
716
+ *
717
+ * @returns ObservationResult with both a human-readable `text` rendering
718
+ * and a `json` payload of the structured observation
719
+ *
720
+ * @throws UNKNOWN_ERROR - the observation could not be produced
721
+ *
722
+ * @example
723
+ * const obs = await browser.getObservation({ maxElementsPerFrame: 200 });
724
+ * console.log(obs.text);
725
+ */
726
+ async getObservation(opts) {
727
+ const req = create(GetObservationRequestSchema, {
728
+ sessionId: this.sessionId,
729
+ apiKey: this.apiKey,
730
+ });
731
+ if (opts?.maxElementsPerFrame !== undefined) {
732
+ req.maxElementsPerFrame = opts.maxElementsPerFrame;
733
+ }
734
+ if (opts?.maxTextLength !== undefined)
735
+ req.maxTextLength = opts.maxTextLength;
736
+ const resp = await this.client.getObservation(req);
737
+ return { text: resp.observationText, json: resp.observationJson };
738
+ }
739
+ // ──────────────────────────────────────────────────────────────────
740
+ // Network interception
741
+ // ──────────────────────────────────────────────────────────────────
742
+ /**
743
+ * Replaces the session's URL blocklist.
744
+ *
745
+ * Any request whose URL matches one of the supplied patterns is blocked
746
+ * before it leaves the browser. Patterns are simple URL wildcards (`*`
747
+ * matches any character span). Pass an empty array to clear the
748
+ * blocklist and let everything through.
749
+ *
750
+ * @param patterns - URL wildcards to block; empty array clears the list
751
+ *
752
+ * @throws UNKNOWN_ERROR - the blocklist could not be applied
753
+ *
754
+ * @example
755
+ * await browser.setBlockList([
756
+ * "*.doubleclick.net/*",
757
+ * "*googletagmanager.com*",
758
+ * ]);
759
+ */
760
+ async setBlockList(patterns) {
761
+ await this.client.setBlockList(create(SetBlockListRequestSchema, {
762
+ sessionId: this.sessionId,
763
+ apiKey: this.apiKey,
764
+ patterns,
765
+ }));
766
+ }
767
+ /**
768
+ * Configures the session to serve cached static responses for requests
769
+ * matching the given patterns from blobName.
770
+ *
771
+ * Useful for replaying frozen page assets (HTML/JS/CSS/images) without
772
+ * hitting the origin every time. The cache backend itself is configured
773
+ * server-side. Pass an empty patterns array to disable caching for this
774
+ * session.
775
+ *
776
+ * @param blobName - server-side identifier of the snapshot to serve from
777
+ * @param patterns - URL wildcards to redirect to the cache; empty disables
778
+ *
779
+ * @throws UNKNOWN_ERROR - the static paths could not be configured
780
+ *
781
+ * @example
782
+ * await browser.setStaticPaths("snap-2026-05", ["*.example.com/*"]);
783
+ */
784
+ async setStaticPaths(blobName, patterns) {
785
+ await this.client.setStaticPaths(create(SetStaticPathsRequestSchema, {
786
+ sessionId: this.sessionId,
787
+ apiKey: this.apiKey,
788
+ blobName,
789
+ patterns,
790
+ }));
791
+ }
792
+ /**
793
+ * Blocks until the next request whose URL matches one of the supplied
794
+ * patterns is observed.
795
+ *
796
+ * Returns the matched pattern's index and the captured request. When
797
+ * `patterns[i].abort` is true the request is dropped with an empty 200
798
+ * response instead of being sent to the network.
799
+ *
800
+ * @param patterns - one or more URL patterns (with optional abort flags)
801
+ * @param opts - optional `timeoutMs`; omit to use the server default
802
+ *
803
+ * @returns object with `index` (matched pattern index) and `request`
804
+ * (the captured method/URL/headers/body; null if intercepted with
805
+ * no body)
806
+ *
807
+ * @throws UNKNOWN_ERROR - the wait timed out or no patterns were supplied
808
+ *
809
+ * @example
810
+ * const { index, request } = await browser.waitForAnyRequest(
811
+ * [{ url: "*\/api/login" }],
812
+ * { timeoutMs: 5000 },
813
+ * );
814
+ * console.log(index, request?.method, request?.url);
815
+ */
816
+ async waitForAnyRequest(patterns, opts) {
817
+ if (patterns.length === 0) {
818
+ throw new WRCError("wrc.waitForAnyRequest: at least one pattern required");
819
+ }
820
+ const { urls, aborts } = splitRequestPatterns(patterns);
821
+ const req = create(WaitForAnyRequestRequestSchema, {
822
+ sessionId: this.sessionId,
823
+ apiKey: this.apiKey,
824
+ patterns: urls,
825
+ abortFlags: aborts,
826
+ });
827
+ if (opts?.timeoutMs)
828
+ req.timeout = opts.timeoutMs;
829
+ const resp = await this.client.waitForAnyRequest(req);
830
+ return {
831
+ index: resp.index,
832
+ request: interceptedRequestFromProto(resp.request),
833
+ };
834
+ }
835
+ /**
836
+ * Blocks until the next response whose URL matches one of the supplied
837
+ * patterns is observed.
838
+ *
839
+ * Same shape as {@link waitForAnyRequest} but on the response phase.
840
+ * When `patterns[i].abort` is true the page receives an empty 200
841
+ * instead of the real response.
842
+ *
843
+ * @inheritDoc CloudBrowser.waitForAnyRequest
844
+ *
845
+ * @returns object with `index` (matched pattern index) and `response`
846
+ * (the captured status/headers/body; null if no body was returned)
847
+ *
848
+ * @example
849
+ * const { index, response } = await browser.waitForAnyResponse(
850
+ * [{ url: "*\/api/login" }],
851
+ * { timeoutMs: 5000 },
852
+ * );
853
+ * console.log(index, response?.statusCode);
854
+ */
855
+ async waitForAnyResponse(patterns, opts) {
856
+ if (patterns.length === 0) {
857
+ throw new WRCError("wrc.waitForAnyResponse: at least one pattern required");
858
+ }
859
+ const { urls, aborts } = splitRequestPatterns(patterns);
860
+ const req = create(WaitForAnyResponseRequestSchema, {
861
+ sessionId: this.sessionId,
862
+ apiKey: this.apiKey,
863
+ patterns: urls,
864
+ abortFlags: aborts,
865
+ });
866
+ if (opts?.timeoutMs)
867
+ req.timeout = opts.timeoutMs;
868
+ const resp = await this.client.waitForAnyResponse(req);
869
+ return {
870
+ index: resp.index,
871
+ response: interceptedResponseFromProto(resp.response),
872
+ };
873
+ }
874
+ /**
875
+ * Waits for the next request whose URL matches urlPattern, applies the
876
+ * supplied header modifications (and optional body replacement), then
877
+ * forwards the modified request.
878
+ *
879
+ * One-shot: consumes the first matching request. Each modification is a
880
+ * plain {@link HeaderModification} object literal.
881
+ *
882
+ * @param urlPattern - URL wildcard to wait for
883
+ * @param opts - optional `body` (replacement request body), `modifications`
884
+ * (header changes), and `timeoutMs` (per-call timeout)
885
+ *
886
+ * @returns InterceptedRequest carrying the method/URL/headers/body that
887
+ * were actually sent on the wire after modifications were applied;
888
+ * null when no request payload was reported
889
+ *
890
+ * @throws UNKNOWN_ERROR - no matching request appeared within the timeout
891
+ *
892
+ * @example
893
+ * const req = await browser.modifyRequest("*\/api/me", {
894
+ * modifications: [
895
+ * { action: "add", name: "X-Trace", value: "abc123" },
896
+ * { action: "remove", name: "Cookie" },
897
+ * ],
898
+ * timeoutMs: 5000,
899
+ * });
900
+ * console.log("forwarded headers:", req?.headers);
901
+ */
902
+ async modifyRequest(urlPattern, opts) {
903
+ const req = create(ModifyRequestRequestSchema, {
904
+ sessionId: this.sessionId,
905
+ apiKey: this.apiKey,
906
+ urlPattern,
907
+ modifications: headerModsToProto(opts?.modifications),
908
+ });
909
+ if (opts?.body)
910
+ req.body = opts.body;
911
+ if (opts?.timeoutMs)
912
+ req.timeout = opts.timeoutMs;
913
+ const resp = await this.client.modifyRequest(req);
914
+ return interceptedRequestFromProto(resp.request);
915
+ }
916
+ // ──────────────────────────────────────────────────────────────────
917
+ // Cookies
918
+ // ──────────────────────────────────────────────────────────────────
919
+ /**
920
+ * Returns all cookies currently stored in this session's browser context.
921
+ *
922
+ * @returns CookieParam[], one per cookie in the context
923
+ *
924
+ * @throws UNKNOWN_ERROR - the cookies could not be read
925
+ *
926
+ * @example
927
+ * const cookies = await browser.getCookies();
928
+ * for (const c of cookies) console.log(c.name, "=", c.value);
929
+ */
930
+ async getCookies() {
931
+ const resp = await this.client.getCookies(create(GetCookiesRequestSchema, {
932
+ sessionId: this.sessionId,
933
+ apiKey: this.apiKey,
934
+ }));
935
+ return resp.cookies.map(cookieParamFromProto);
936
+ }
937
+ /**
938
+ * Writes the supplied cookies into the browser context.
939
+ *
940
+ * Existing cookies with the same (name, domain, path) tuple are
941
+ * overwritten. Pass an empty array for a no-op.
942
+ *
943
+ * @param cookies - cookies to write; empty array is a no-op
944
+ *
945
+ * @throws UNKNOWN_ERROR - the cookies could not be written
946
+ *
947
+ * @example
948
+ * await browser.setCookies([
949
+ * { name: "auth", value: "tok", domain: "example.com", path: "/" },
950
+ * ]);
951
+ */
952
+ async setCookies(cookies) {
953
+ await this.client.setCookies(create(SetCookiesRequestSchema, {
954
+ sessionId: this.sessionId,
955
+ apiKey: this.apiKey,
956
+ cookies: cookieParamsToProto(cookies),
957
+ }));
958
+ }
959
+ /**
960
+ * Deletes every cookie in the browser context.
961
+ *
962
+ * @throws UNKNOWN_ERROR - the cookies could not be cleared
963
+ *
964
+ * @example
965
+ * await browser.clearCookies();
966
+ */
967
+ async clearCookies() {
968
+ await this.client.clearCookies(create(ClearCookiesRequestSchema, {
969
+ sessionId: this.sessionId,
970
+ apiKey: this.apiKey,
971
+ }));
972
+ }
973
+ // ──────────────────────────────────────────────────────────────────
974
+ // Storage (localStorage)
975
+ // ──────────────────────────────────────────────────────────────────
976
+ /**
977
+ * Returns the localStorage contents of this session's browser context,
978
+ * grouped by origin.
979
+ *
980
+ * The storage database is read directly in the browser process, so no
981
+ * page needs to be open. Only first-party localStorage is included —
982
+ * sessionStorage is per-tab and not covered.
983
+ *
984
+ * @param origin - if set, only this origin is returned
985
+ * (e.g. "https://example.com"); omit to get all origins
986
+ *
987
+ * @returns StorageOriginEntry[], one per origin with localStorage data
988
+ *
989
+ * @throws UNKNOWN_ERROR - the storage could not be read
990
+ *
991
+ * @example
992
+ * const storage = await browser.getStorage();
993
+ * for (const e of storage) {
994
+ * for (const { key, value } of e.items) console.log(e.origin, key, "=", value);
995
+ * }
996
+ */
997
+ async getStorage(origin) {
998
+ const req = create(GetStorageRequestSchema, {
999
+ sessionId: this.sessionId,
1000
+ apiKey: this.apiKey,
1001
+ });
1002
+ if (origin)
1003
+ req.origin = origin;
1004
+ const resp = await this.client.getStorage(req);
1005
+ return resp.storage.map(storageEntryFromProto);
1006
+ }
1007
+ /**
1008
+ * Writes localStorage entries into the browser context, grouped by
1009
+ * origin.
1010
+ *
1011
+ * Accepts the same structure getStorage() returns, so a dump can be
1012
+ * fed back verbatim. Existing keys are overwritten. Works without any
1013
+ * open page; pages that are already open will not observe the writes
1014
+ * until they reload.
1015
+ *
1016
+ * @param storage - entries to write, grouped by origin
1017
+ *
1018
+ * @throws UNKNOWN_ERROR - the storage could not be written
1019
+ *
1020
+ * @example
1021
+ * await browser.setStorage([
1022
+ * {
1023
+ * origin: "https://example.com",
1024
+ * items: [
1025
+ * { key: "token", value: "abc123" },
1026
+ * { key: "theme", value: "dark" },
1027
+ * ],
1028
+ * },
1029
+ * ]);
1030
+ */
1031
+ async setStorage(storage) {
1032
+ await this.client.setStorage(create(SetStorageRequestSchema, {
1033
+ sessionId: this.sessionId,
1034
+ apiKey: this.apiKey,
1035
+ storage: storageEntriesToProto(storage),
1036
+ }));
1037
+ }
1038
+ /**
1039
+ * Deletes localStorage in the browser context.
1040
+ *
1041
+ * @param origin - if set, only this origin's storage is deleted
1042
+ * (e.g. "https://example.com"); omit to delete all origins
1043
+ *
1044
+ * @throws UNKNOWN_ERROR - the storage could not be cleared
1045
+ *
1046
+ * @example
1047
+ * // Wipe one origin.
1048
+ * await browser.clearStorage("https://example.com");
1049
+ *
1050
+ * // Wipe everything.
1051
+ * await browser.clearStorage();
1052
+ */
1053
+ async clearStorage(origin) {
1054
+ const req = create(ClearStorageRequestSchema, {
1055
+ sessionId: this.sessionId,
1056
+ apiKey: this.apiKey,
1057
+ });
1058
+ if (origin)
1059
+ req.origin = origin;
1060
+ await this.client.clearStorage(req);
1061
+ }
1062
+ // ──────────────────────────────────────────────────────────────────
1063
+ // Devtools / live-UI helpers
1064
+ // ──────────────────────────────────────────────────────────────────
1065
+ /**
1066
+ * Hit-tests at the viewport-relative (x, y) and returns the topmost
1067
+ * element under that point.
1068
+ *
1069
+ * Mirrors what the live-UI overlay does on hover. Elements with
1070
+ * pointer-events:none are skipped — the result is the actual click
1071
+ * target, not the visually-topmost node. A `backendNodeId === 0` in
1072
+ * the result means nothing was found.
1073
+ *
1074
+ * @param x - viewport-relative x in CSS pixels
1075
+ * @param y - viewport-relative y in CSS pixels
1076
+ *
1077
+ * @returns InspectResult with the resolved backendNodeId, frameId, tag
1078
+ * name, trimmed textContent, visibility and bounds
1079
+ *
1080
+ * @throws UNKNOWN_ERROR - the hit-test failed
1081
+ *
1082
+ * @example
1083
+ * const r = await browser.inspectAtPosition(200, 300);
1084
+ * console.log(r.tagName, r.textContent);
1085
+ */
1086
+ async inspectAtPosition(x, y) {
1087
+ const resp = await this.client.inspectAtPosition(create(InspectAtPositionRequestSchema, {
1088
+ sessionId: this.sessionId,
1089
+ apiKey: this.apiKey,
1090
+ x,
1091
+ y,
1092
+ }));
1093
+ return {
1094
+ backendNodeId: resp.backendNodeId,
1095
+ frameId: resp.frameId,
1096
+ tagName: resp.tagName,
1097
+ textContent: resp.textContent,
1098
+ isVisible: resp.isVisible,
1099
+ bounds: rectFromProto(resp.bounds),
1100
+ };
1101
+ }
1102
+ /**
1103
+ * Paints a debug overlay over the node identified by backendNodeId.
1104
+ *
1105
+ * Useful for visual debugging of agent flows — the overlay stays until
1106
+ * the next call. Pass backendNodeId <= 0 to clear any current highlights.
1107
+ *
1108
+ * @param backendNodeId - id of the node to highlight, or <= 0 to clear
1109
+ * @param frameId - id of the frame the node lives in; empty string
1110
+ * targets the main frame
1111
+ *
1112
+ * @throws UNKNOWN_ERROR - the highlight could not be applied
1113
+ *
1114
+ * @example
1115
+ * await browser.highlightNode(res.backendNodeId, res.frameId);
1116
+ */
1117
+ async highlightNode(backendNodeId, frameId = "") {
1118
+ const req = create(HighlightNodeRequestSchema, {
1119
+ sessionId: this.sessionId,
1120
+ apiKey: this.apiKey,
1121
+ backendNodeId,
1122
+ });
1123
+ if (frameId)
1124
+ req.frameId = frameId;
1125
+ await this.client.highlightNode(req);
1126
+ }
1127
+ /**
1128
+ * Pastes text at the current caret using IME-style input.
1129
+ *
1130
+ * No individual key events are dispatched; the entire string is
1131
+ * committed at once via Input.insertText. Whatever element currently has
1132
+ * focus receives the text. Use {@link click} or {@link fill} first if
1133
+ * you need a specific element to be focused.
1134
+ *
1135
+ * @param text - the text to insert at the caret
1136
+ *
1137
+ * @throws UNKNOWN_ERROR - the text could not be inserted
1138
+ *
1139
+ * @example
1140
+ * await browser.insertText("hello world");
1141
+ */
1142
+ async insertText(text) {
1143
+ await this.client.insertText(create(InsertTextRequestSchema, {
1144
+ sessionId: this.sessionId,
1145
+ apiKey: this.apiKey,
1146
+ text,
1147
+ }));
1148
+ }
1149
+ /**
1150
+ * Fires a single key-down event.
1151
+ *
1152
+ * Only the keydown half is dispatched — pair with {@link releaseKey} for
1153
+ * a full press cycle. The event targets whichever element currently has
1154
+ * focus.
1155
+ *
1156
+ * @param key - DOM KeyboardEvent.key value (e.g. `"Enter"`, `"a"`, `"ArrowLeft"`)
1157
+ * @param opts - optional key customization:
1158
+ * `code` (DOM KeyboardEvent.code, e.g. `"KeyA"`),
1159
+ * `modifiers` (bit-flag: Alt=1, Ctrl=2, Meta=4, Shift=8),
1160
+ * `location` (0=standard, 1=left, 2=right, 3=numpad)
1161
+ *
1162
+ * @throws UNKNOWN_ERROR - the event could not be dispatched
1163
+ *
1164
+ * @example
1165
+ * // Ctrl+A
1166
+ * await browser.pressKey("a", { code: "KeyA", modifiers: 2 });
1167
+ * await browser.releaseKey("a", { code: "KeyA", modifiers: 2 });
1168
+ */
1169
+ async pressKey(key, opts) {
1170
+ const req = create(PressKeyRequestSchema, {
1171
+ sessionId: this.sessionId,
1172
+ apiKey: this.apiKey,
1173
+ key,
1174
+ });
1175
+ if (opts?.code)
1176
+ req.code = opts.code;
1177
+ if (opts?.modifiers !== undefined)
1178
+ req.modifiers = opts.modifiers;
1179
+ if (opts?.location !== undefined)
1180
+ req.location = opts.location;
1181
+ await this.client.pressKey(req);
1182
+ }
1183
+ /**
1184
+ * Fires a single key-up event.
1185
+ *
1186
+ * Mirror of {@link pressKey}. Same parameter semantics; use this to
1187
+ * close a press cycle that was started with pressKey.
1188
+ *
1189
+ * @inheritDoc CloudBrowser.pressKey
1190
+ *
1191
+ * @example
1192
+ * await browser.pressKey("Shift", { code: "ShiftLeft", location: 1 });
1193
+ * await browser.releaseKey("Shift", { code: "ShiftLeft", location: 1 });
1194
+ */
1195
+ async releaseKey(key, opts) {
1196
+ const req = create(ReleaseKeyRequestSchema, {
1197
+ sessionId: this.sessionId,
1198
+ apiKey: this.apiKey,
1199
+ key,
1200
+ });
1201
+ if (opts?.code)
1202
+ req.code = opts.code;
1203
+ if (opts?.modifiers !== undefined)
1204
+ req.modifiers = opts.modifiers;
1205
+ if (opts?.location !== undefined)
1206
+ req.location = opts.location;
1207
+ await this.client.releaseKey(req);
1208
+ }
1209
+ /**
1210
+ * Returns the current text selection.
1211
+ *
1212
+ * Walks every frame and returns the first non-empty selection found —
1213
+ * useful for "copy what the user highlighted" flows. Returns an empty
1214
+ * string when nothing is selected anywhere.
1215
+ *
1216
+ * @returns the selected text, or `""` when nothing is selected
1217
+ *
1218
+ * @throws UNKNOWN_ERROR - the selection could not be read
1219
+ *
1220
+ * @example
1221
+ * const sel = await browser.getSelection();
1222
+ * console.log("user selected:", sel);
1223
+ */
1224
+ async getSelection() {
1225
+ const resp = await this.client.getSelection(create(GetSelectionRequestSchema, {
1226
+ sessionId: this.sessionId,
1227
+ apiKey: this.apiKey,
1228
+ }));
1229
+ return resp.text;
1230
+ }
1231
+ // ──────────────────────────────────────────────────────────────────
1232
+ // Captcha solver
1233
+ // ──────────────────────────────────────────────────────────────────
1234
+ /**
1235
+ * Detects and solves the first supported bot-challenge it finds
1236
+ * anywhere on the page.
1237
+ *
1238
+ * Detection covers the common challenge types you run into in the
1239
+ * wild. The challenge is completed in-page server-side (the resulting
1240
+ * token / bypass cookies are wired into the page automatically), so
1241
+ * callers can ignore the returned string.
1242
+ *
1243
+ * @param opts - optional `timeoutMs` (how long to wait for a captcha to
1244
+ * appear; omit for server default 60s) and `retryAmount` (failures
1245
+ * tolerated before giving up)
1246
+ *
1247
+ * @returns empty string on success — the solution is applied server-side
1248
+ *
1249
+ * @throws UNKNOWN_ERROR - no captcha appeared within timeoutMs, or the
1250
+ * detected captcha could not be solved within retryAmount attempts
1251
+ *
1252
+ * @example
1253
+ * await browser.solveCaptcha({ retryAmount: 2 });
1254
+ */
1255
+ async solveCaptcha(opts) {
1256
+ const resp = await this.client.solveCaptcha(create(SolveCaptchaRequestSchema, {
1257
+ sessionId: this.sessionId,
1258
+ apiKey: this.apiKey,
1259
+ timeoutMs: opts?.timeoutMs ?? 0,
1260
+ retryAmount: opts?.retryAmount ?? 0,
1261
+ }));
1262
+ return resp.result;
1263
+ }
1264
+ }