browserscale-ts 1.2.1 → 1.3.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.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Transport } from "@connectrpc/connect";
2
2
  import type { Locator } from "./locator.ts";
3
- import type { DOMResult, DragResult, ElementResult, EvaluateResult, InterceptedRequest, InterceptedResponse, InspectResult, NavigateResult, ObservationResult, PageInfo, ScreenshotResult, ReadCanvasResult, SelectOptionResult, WaitResult } from "./types.ts";
4
- import type { ClickOpts, FillOpts, GetDOMOpts, GetObservationOpts, LoadHTMLOpts, NavigateOpts, ScreenshotOpts, ReadCanvasOpts, SelectOpts, WaitOpts } from "./options.ts";
3
+ import type { DOMResult, DragResult, ElementResult, EvaluateResult, InterceptedRequest, InterceptedResponse, InspectResult, NavigateResult, ObservationResult, PageInfo, ScreenshotResult, ReadCanvasResult, SelectOptionResult, WaitResult, IceServer, ReactionInfo } from "./types.ts";
4
+ import type { ClickOpts, FillOpts, GetDOMOpts, GetObservationOpts, LoadHTMLOpts, NavigateOpts, ReactionOpts, ScreenshotOpts, ReadCanvasOpts, SelectOpts, WaitOpts } from "./options.ts";
5
5
  import type { HeaderModification, RequestPattern } from "./network.ts";
6
6
  import type { CookieParam } from "./cookies.ts";
7
7
  import type { StorageOriginEntry } from "./storage.ts";
@@ -818,6 +818,36 @@ export declare class CloudBrowser {
818
818
  * await browser.insertText("hello world");
819
819
  */
820
820
  insertText(text: string): Promise<void>;
821
+ /**
822
+ * Types `text` into the currently focused element as a per-key stream of real
823
+ * keyboard events (keyDown/char/keyUp with the context's QWERTZ/QWERTY layout
824
+ * and human cadence) — unlike {@link insertText}, a single IME-style commit
825
+ * with no key events.
826
+ *
827
+ * `type` is intentionally UNtargeted and loose: it does not locate or focus
828
+ * any element and does NOT pin focus, so the page is free to route keys and
829
+ * move focus between fields mid-stream — ideal for one-time-code / OTP inputs
830
+ * that auto-advance to the next box on each digit. To type one specific field
831
+ * that must stay focused for the whole value, use {@link fill} instead
832
+ * (strict, target-bound, per-key focus-verified).
833
+ *
834
+ * Nothing is focused for you: {@link click} (or {@link fill}) the field first,
835
+ * or otherwise ensure focus, before calling `type`.
836
+ *
837
+ * @param text - the text to type as real key events
838
+ * @param opts - optional: `clearFirst` clears the focused field (Ctrl+A,
839
+ * Delete) before typing
840
+ *
841
+ * @throws UNKNOWN_ERROR - the page/context was torn down mid-stream
842
+ *
843
+ * @example
844
+ * // OTP field that auto-advances across boxes.
845
+ * await browser.click(css("input.otp-0"));
846
+ * await browser.type("123456");
847
+ */
848
+ type(text: string, opts?: {
849
+ clearFirst?: boolean;
850
+ }): Promise<void>;
821
851
  /**
822
852
  * Fires a single key-down event.
823
853
  *
@@ -901,4 +931,113 @@ export declare class CloudBrowser {
901
931
  timeoutMs?: number;
902
932
  retryAmount?: number;
903
933
  }): Promise<string>;
934
+ /**
935
+ * Returns the ICE servers (TURN URL + short-lived credentials) to put in
936
+ * your `RTCPeerConnection` BEFORE creating the offer, so it can gather relay
937
+ * candidates.
938
+ *
939
+ * Live streaming is a two-step, client-offerer handshake: call
940
+ * `getStreamConfig`, build your peer with the returned servers, create an
941
+ * offer, then pass its SDP to {@link startStream} and apply the answer.
942
+ *
943
+ * @returns the ICE servers for the client `RTCPeerConnection`
944
+ *
945
+ * @throws UNKNOWN_ERROR - TURN is not configured on the server
946
+ *
947
+ * @example
948
+ * const ice = await browser.getStreamConfig();
949
+ * const pc = new RTCPeerConnection({ iceServers: ice });
950
+ */
951
+ getStreamConfig(): Promise<IceServer[]>;
952
+ /**
953
+ * Answers your WebRTC SDP offer and starts streaming the page as a video
954
+ * track, returning the SDP answer to set as your peer's remote description.
955
+ * The browser is the answerer; you are the offerer (see
956
+ * {@link getStreamConfig} for the credentials to build the offer).
957
+ *
958
+ * @param offerSdp - your `RTCPeerConnection`'s SDP offer
959
+ *
960
+ * @returns the SDP answer to apply as the remote description
961
+ *
962
+ * @throws UNKNOWN_ERROR - the offer was empty, TURN is unconfigured, or the
963
+ * browser could not negotiate the stream
964
+ *
965
+ * @example
966
+ * const answer = await browser.startStream(offer.sdp);
967
+ * await pc.setRemoteDescription({ type: "answer", sdp: answer });
968
+ */
969
+ startStream(offerSdp: string): Promise<string>;
970
+ /**
971
+ * Tears down the live video stream for the session's page. Safe to call even
972
+ * if no stream is running.
973
+ *
974
+ * @throws UNKNOWN_ERROR - the stream could not be stopped
975
+ *
976
+ * @example
977
+ * await browser.stopStream();
978
+ */
979
+ stopStream(): Promise<void>;
980
+ /**
981
+ * Registers a one-shot "reaction": a background poller (one shared loop per
982
+ * page) watches for the `match` locator and, as soon as it matches, clicks it
983
+ * with the full smart-click machinery (scroll, human path, occlusion gate,
984
+ * evade) — then removes itself. The poller yields to any in-flight input
985
+ * action and only fires while the pointer is idle, so a reaction naturally
986
+ * slots into the gaps of a retrying foreground action (e.g. it dismisses a
987
+ * newsletter modal blocking a {@link CloudBrowser.click}, after which the
988
+ * click's own retry succeeds). Reactions are scoped to the page and torn
989
+ * down automatically when the page/session ends.
990
+ *
991
+ * `match` must be a `css()` or `js()` {@link Locator} — `node()`/`at()` are
992
+ * rejected. Use `.inAllFrames()` to watch every frame and `.visible(false)`
993
+ * to opt out of the default visibility gate. Pass a `ReactionOpts` to click a
994
+ * different target (`on`), change the button/click count, or the poll cadence.
995
+ *
996
+ * @param match - the css()/js() locator to watch for
997
+ * @param opts - optional reaction customization; see {@link ReactionOpts}
998
+ *
999
+ * @returns the reactionId (pass to {@link CloudBrowser.removeReaction})
1000
+ *
1001
+ * @throws {@link BrowserScaleError} - `match` (or `opts.on`) is not a css()/js()
1002
+ * locator, or a server/transport error
1003
+ *
1004
+ * @example
1005
+ * // Auto-dismiss a consent button whenever it appears, in any frame.
1006
+ * const id = await browser.addReaction(css("button#accept").inAllFrames());
1007
+ *
1008
+ * @example
1009
+ * // Watch for a newsletter modal, but click its close "X" instead.
1010
+ * const id = await browser.addReaction(css("#newsletter-modal"), {
1011
+ * on: css(".modal-close"),
1012
+ * });
1013
+ */
1014
+ addReaction(match: Locator, opts?: ReactionOpts): Promise<string>;
1015
+ /**
1016
+ * Removes a pending reaction by id. Returns `false` if the reaction had
1017
+ * already fired (one-shot) or was never registered.
1018
+ *
1019
+ * @param reactionId - id returned by {@link CloudBrowser.addReaction}
1020
+ *
1021
+ * @returns true if a pending reaction with this id existed and was removed
1022
+ *
1023
+ * @example
1024
+ * const removed = await browser.removeReaction(id);
1025
+ */
1026
+ removeReaction(reactionId: string): Promise<boolean>;
1027
+ /**
1028
+ * Returns the still-pending reactions registered for the current page.
1029
+ * Reactions that have already fired (one-shot) are not included.
1030
+ *
1031
+ * @returns the pending reactions for the page
1032
+ *
1033
+ * @example
1034
+ * const pending = await browser.listReactions();
1035
+ * for (const r of pending) console.log(r.reactionId, r.matchSelector);
1036
+ */
1037
+ listReactions(): Promise<ReactionInfo[]>;
1038
+ /**
1039
+ * @internal — a reaction match/action must be a css()/js() locator: node()
1040
+ * and at() are rejected (a reaction watches for a condition, like a wait).
1041
+ */
1042
+ private assertReactionLocator;
904
1043
  }
package/dist/client.js CHANGED
@@ -2,7 +2,7 @@ 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, PressKeyRequestSchema, ReleaseKeyRequestSchema, GetSelectionRequestSchema, SolveCaptchaRequestSchema, } 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, InspectAtPositionRequestSchema, HighlightNodeRequestSchema, InsertTextRequestSchema, TypeRequestSchema, PressKeyRequestSchema, ReleaseKeyRequestSchema, GetSelectionRequestSchema, SolveCaptchaRequestSchema, GetStreamConfigRequestSchema, StartStreamRequestSchema, StopStreamRequestSchema, AddReactionRequestSchema, RemoveReactionRequestSchema, ListReactionsRequestSchema, } from "./gen/wrc_pb.js";
6
6
  import { DefaultWaitTimeoutMs } from "./defaults.js";
7
7
  import { BrowserScaleError } from "./errors.js";
8
8
  import { cookieParamFromProto, cookieParamsToProto, elementFields, headerModsToProto, headersToProto, interceptedRequestFromProto, interceptedResponseFromProto, pageInfoFromProto, rectFromProto, splitRequestPatterns, storageEntriesToProto, storageEntryFromProto, unwrapClick, unwrapDrag, unwrapFill, unwrapMove, unwrapScroll, unwrapSelect, unwrapWait, } from "./internal/convert.js";
@@ -437,6 +437,10 @@ export class CloudBrowser {
437
437
  });
438
438
  if (opts?.clearFirst)
439
439
  req.clearFirst = true;
440
+ if (opts?.timeoutMs !== undefined)
441
+ req.timeout = opts.timeoutMs;
442
+ if (opts?.steadyMs !== undefined)
443
+ req.steadyTime = opts.steadyMs;
440
444
  return unwrapFill(await this.client.fill(req));
441
445
  }
442
446
  /**
@@ -1243,6 +1247,43 @@ export class CloudBrowser {
1243
1247
  text,
1244
1248
  }));
1245
1249
  }
1250
+ /**
1251
+ * Types `text` into the currently focused element as a per-key stream of real
1252
+ * keyboard events (keyDown/char/keyUp with the context's QWERTZ/QWERTY layout
1253
+ * and human cadence) — unlike {@link insertText}, a single IME-style commit
1254
+ * with no key events.
1255
+ *
1256
+ * `type` is intentionally UNtargeted and loose: it does not locate or focus
1257
+ * any element and does NOT pin focus, so the page is free to route keys and
1258
+ * move focus between fields mid-stream — ideal for one-time-code / OTP inputs
1259
+ * that auto-advance to the next box on each digit. To type one specific field
1260
+ * that must stay focused for the whole value, use {@link fill} instead
1261
+ * (strict, target-bound, per-key focus-verified).
1262
+ *
1263
+ * Nothing is focused for you: {@link click} (or {@link fill}) the field first,
1264
+ * or otherwise ensure focus, before calling `type`.
1265
+ *
1266
+ * @param text - the text to type as real key events
1267
+ * @param opts - optional: `clearFirst` clears the focused field (Ctrl+A,
1268
+ * Delete) before typing
1269
+ *
1270
+ * @throws UNKNOWN_ERROR - the page/context was torn down mid-stream
1271
+ *
1272
+ * @example
1273
+ * // OTP field that auto-advances across boxes.
1274
+ * await browser.click(css("input.otp-0"));
1275
+ * await browser.type("123456");
1276
+ */
1277
+ async type(text, opts) {
1278
+ const req = create(TypeRequestSchema, {
1279
+ sessionId: this.sessionId,
1280
+ apiKey: this.apiKey,
1281
+ text,
1282
+ });
1283
+ if (opts?.clearFirst)
1284
+ req.clearFirst = true;
1285
+ await this.client.type(req);
1286
+ }
1246
1287
  /**
1247
1288
  * Fires a single key-down event.
1248
1289
  *
@@ -1358,4 +1399,198 @@ export class CloudBrowser {
1358
1399
  }));
1359
1400
  return resp.result;
1360
1401
  }
1402
+ // ──────────────────────────────────────────────────────────────────
1403
+ // Live streaming (WebRTC)
1404
+ // ──────────────────────────────────────────────────────────────────
1405
+ /**
1406
+ * Returns the ICE servers (TURN URL + short-lived credentials) to put in
1407
+ * your `RTCPeerConnection` BEFORE creating the offer, so it can gather relay
1408
+ * candidates.
1409
+ *
1410
+ * Live streaming is a two-step, client-offerer handshake: call
1411
+ * `getStreamConfig`, build your peer with the returned servers, create an
1412
+ * offer, then pass its SDP to {@link startStream} and apply the answer.
1413
+ *
1414
+ * @returns the ICE servers for the client `RTCPeerConnection`
1415
+ *
1416
+ * @throws UNKNOWN_ERROR - TURN is not configured on the server
1417
+ *
1418
+ * @example
1419
+ * const ice = await browser.getStreamConfig();
1420
+ * const pc = new RTCPeerConnection({ iceServers: ice });
1421
+ */
1422
+ async getStreamConfig() {
1423
+ const resp = await this.client.getStreamConfig(create(GetStreamConfigRequestSchema, {
1424
+ sessionId: this.sessionId,
1425
+ apiKey: this.apiKey,
1426
+ }));
1427
+ return resp.iceServers.map((s) => ({
1428
+ urls: s.urls,
1429
+ username: s.username ?? "",
1430
+ credential: s.credential ?? "",
1431
+ }));
1432
+ }
1433
+ /**
1434
+ * Answers your WebRTC SDP offer and starts streaming the page as a video
1435
+ * track, returning the SDP answer to set as your peer's remote description.
1436
+ * The browser is the answerer; you are the offerer (see
1437
+ * {@link getStreamConfig} for the credentials to build the offer).
1438
+ *
1439
+ * @param offerSdp - your `RTCPeerConnection`'s SDP offer
1440
+ *
1441
+ * @returns the SDP answer to apply as the remote description
1442
+ *
1443
+ * @throws UNKNOWN_ERROR - the offer was empty, TURN is unconfigured, or the
1444
+ * browser could not negotiate the stream
1445
+ *
1446
+ * @example
1447
+ * const answer = await browser.startStream(offer.sdp);
1448
+ * await pc.setRemoteDescription({ type: "answer", sdp: answer });
1449
+ */
1450
+ async startStream(offerSdp) {
1451
+ const resp = await this.client.startStream(create(StartStreamRequestSchema, {
1452
+ sessionId: this.sessionId,
1453
+ apiKey: this.apiKey,
1454
+ offerSdp,
1455
+ }));
1456
+ return resp.answerSdp;
1457
+ }
1458
+ /**
1459
+ * Tears down the live video stream for the session's page. Safe to call even
1460
+ * if no stream is running.
1461
+ *
1462
+ * @throws UNKNOWN_ERROR - the stream could not be stopped
1463
+ *
1464
+ * @example
1465
+ * await browser.stopStream();
1466
+ */
1467
+ async stopStream() {
1468
+ await this.client.stopStream(create(StopStreamRequestSchema, {
1469
+ sessionId: this.sessionId,
1470
+ apiKey: this.apiKey,
1471
+ }));
1472
+ }
1473
+ // ──────────────────────────────────────────────────────────────────
1474
+ // Reactions
1475
+ // ──────────────────────────────────────────────────────────────────
1476
+ /**
1477
+ * Registers a one-shot "reaction": a background poller (one shared loop per
1478
+ * page) watches for the `match` locator and, as soon as it matches, clicks it
1479
+ * with the full smart-click machinery (scroll, human path, occlusion gate,
1480
+ * evade) — then removes itself. The poller yields to any in-flight input
1481
+ * action and only fires while the pointer is idle, so a reaction naturally
1482
+ * slots into the gaps of a retrying foreground action (e.g. it dismisses a
1483
+ * newsletter modal blocking a {@link CloudBrowser.click}, after which the
1484
+ * click's own retry succeeds). Reactions are scoped to the page and torn
1485
+ * down automatically when the page/session ends.
1486
+ *
1487
+ * `match` must be a `css()` or `js()` {@link Locator} — `node()`/`at()` are
1488
+ * rejected. Use `.inAllFrames()` to watch every frame and `.visible(false)`
1489
+ * to opt out of the default visibility gate. Pass a `ReactionOpts` to click a
1490
+ * different target (`on`), change the button/click count, or the poll cadence.
1491
+ *
1492
+ * @param match - the css()/js() locator to watch for
1493
+ * @param opts - optional reaction customization; see {@link ReactionOpts}
1494
+ *
1495
+ * @returns the reactionId (pass to {@link CloudBrowser.removeReaction})
1496
+ *
1497
+ * @throws {@link BrowserScaleError} - `match` (or `opts.on`) is not a css()/js()
1498
+ * locator, or a server/transport error
1499
+ *
1500
+ * @example
1501
+ * // Auto-dismiss a consent button whenever it appears, in any frame.
1502
+ * const id = await browser.addReaction(css("button#accept").inAllFrames());
1503
+ *
1504
+ * @example
1505
+ * // Watch for a newsletter modal, but click its close "X" instead.
1506
+ * const id = await browser.addReaction(css("#newsletter-modal"), {
1507
+ * on: css(".modal-close"),
1508
+ * });
1509
+ */
1510
+ async addReaction(match, opts) {
1511
+ this.assertReactionLocator("addReaction", match, "match");
1512
+ const req = create(AddReactionRequestSchema, {
1513
+ sessionId: this.sessionId,
1514
+ apiKey: this.apiKey,
1515
+ });
1516
+ if (match.selector)
1517
+ req.matchSelector = match.selector;
1518
+ if (match.jsExpression)
1519
+ req.matchJsExpression = match.jsExpression;
1520
+ if (match.frameId)
1521
+ req.frameId = match.frameId;
1522
+ if (match.visibleFlag !== undefined)
1523
+ req.visible = match.visibleFlag;
1524
+ if (opts?.on) {
1525
+ this.assertReactionLocator("addReaction", opts.on, "on");
1526
+ if (opts.on.selector)
1527
+ req.actionSelector = opts.on.selector;
1528
+ if (opts.on.jsExpression)
1529
+ req.actionJsExpression = opts.on.jsExpression;
1530
+ }
1531
+ if (opts?.button)
1532
+ req.button = opts.button;
1533
+ if (opts?.clickCount)
1534
+ req.clickCount = opts.clickCount;
1535
+ if (opts?.intervalMs)
1536
+ req.interval = opts.intervalMs;
1537
+ const resp = await this.client.addReaction(req);
1538
+ return resp.reactionId;
1539
+ }
1540
+ /**
1541
+ * Removes a pending reaction by id. Returns `false` if the reaction had
1542
+ * already fired (one-shot) or was never registered.
1543
+ *
1544
+ * @param reactionId - id returned by {@link CloudBrowser.addReaction}
1545
+ *
1546
+ * @returns true if a pending reaction with this id existed and was removed
1547
+ *
1548
+ * @example
1549
+ * const removed = await browser.removeReaction(id);
1550
+ */
1551
+ async removeReaction(reactionId) {
1552
+ const resp = await this.client.removeReaction(create(RemoveReactionRequestSchema, {
1553
+ sessionId: this.sessionId,
1554
+ apiKey: this.apiKey,
1555
+ reactionId,
1556
+ }));
1557
+ return resp.removed;
1558
+ }
1559
+ /**
1560
+ * Returns the still-pending reactions registered for the current page.
1561
+ * Reactions that have already fired (one-shot) are not included.
1562
+ *
1563
+ * @returns the pending reactions for the page
1564
+ *
1565
+ * @example
1566
+ * const pending = await browser.listReactions();
1567
+ * for (const r of pending) console.log(r.reactionId, r.matchSelector);
1568
+ */
1569
+ async listReactions() {
1570
+ const resp = await this.client.listReactions(create(ListReactionsRequestSchema, {
1571
+ sessionId: this.sessionId,
1572
+ apiKey: this.apiKey,
1573
+ }));
1574
+ return resp.reactions.map((r) => ({
1575
+ reactionId: r.reactionId,
1576
+ matchSelector: r.matchSelector ?? "",
1577
+ matchJsExpression: r.matchJsExpression ?? "",
1578
+ actionSelector: r.actionSelector ?? "",
1579
+ actionJsExpression: r.actionJsExpression ?? "",
1580
+ frameId: r.frameId,
1581
+ visible: r.visible,
1582
+ }));
1583
+ }
1584
+ /**
1585
+ * @internal — a reaction match/action must be a css()/js() locator: node()
1586
+ * and at() are rejected (a reaction watches for a condition, like a wait).
1587
+ */
1588
+ assertReactionLocator(cmd, l, role) {
1589
+ if (l.backendNodeId !== 0 || l.x !== undefined || l.y !== undefined) {
1590
+ throw new BrowserScaleError(`browserscale.${cmd}: ${role} must be a css()/js() locator (node()/at() are not valid)`);
1591
+ }
1592
+ if (!l.selector && !l.jsExpression) {
1593
+ throw new BrowserScaleError(`browserscale.${cmd}: ${role} must have a CSS selector or JS expression`);
1594
+ }
1595
+ }
1361
1596
  }
package/dist/errors.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { DragResult, ElementResult, OccluderInfo, SelectOptionResult, WaitConditionStatus, WaitResult } from "./types.ts";
1
+ import type { DragResult, ElementRef, ElementResult, OccluderInfo, SelectOptionResult, WaitConditionStatus, WaitResult } from "./types.ts";
2
2
  /**
3
3
  * BrowserScaleError is the base class for every error thrown by the SDK. It
4
4
  * wraps either:
@@ -60,21 +60,48 @@ export declare class ClickError extends BrowserScaleError {
60
60
  }
61
61
  /**
62
62
  * FillError is thrown by {@link CloudBrowser.fill} when the field could not be
63
- * focused/typed. Fill focuses with the exact same smart click as
64
- * {@link CloudBrowser.click}, so a pre-typing failure is a click failure:
65
- * `code` mirrors it and the full click diagnostics live under `clickError`.
63
+ * focused/typed. The click-phase codes (`"not_found"`,
64
+ * `"occluded_no_reachable_point"`, `"occluded_after_evade"`) mirror the
65
+ * underlying focus click, with diagnostics under `clickError`. The focus codes
66
+ * are `"focus_stolen"` (another element took focus — `focusedElement` names it;
67
+ * fill is strictly target-bound and will not type into the thief) and
68
+ * `"focus_lost"` (focus left the target and nothing is focused). For untargeted
69
+ * stream typing that lets focus move (e.g. OTP), use {@link CloudBrowser.type}.
66
70
  */
67
71
  export declare class FillError extends BrowserScaleError {
68
- /** Mirrored from the underlying click failure. */
72
+ /** Machine-stable failure code (see the class doc for the full set). */
69
73
  readonly code: string;
70
- /** The underlying click-core failure that prevented focusing/typing. */
74
+ /**
75
+ * The underlying click-core failure that prevented focusing/typing. Present
76
+ * for the click-phase codes; undefined for `"focus_stolen"`/`"focus_lost"`.
77
+ */
71
78
  readonly clickError?: ClickError;
79
+ /**
80
+ * Node that held focus when fill gave up (0 if nothing was focused), for the
81
+ * `"focus_stolen"`/`"focus_lost"` codes.
82
+ */
83
+ readonly focusedBackendNodeId?: number;
84
+ /**
85
+ * The element that grabbed focus instead of the target (`"focus_stolen"`), so
86
+ * you can act on it (e.g. a consent button).
87
+ */
88
+ readonly focusedElement?: ElementRef;
89
+ /**
90
+ * The fill target's own state at the point of failure (the focus codes):
91
+ * whether it is still an editable text sink and its current text length.
92
+ */
93
+ readonly targetEditable?: boolean;
94
+ readonly targetValueLength?: number;
72
95
  /** Resolved element + coordinates at the failed action (success is false). */
73
96
  readonly result: ElementResult;
74
97
  constructor(init: {
75
98
  code: string;
76
99
  message: string;
77
100
  clickError?: ClickError;
101
+ focusedBackendNodeId?: number;
102
+ focusedElement?: ElementRef;
103
+ targetEditable?: boolean;
104
+ targetValueLength?: number;
78
105
  result: ElementResult;
79
106
  });
80
107
  }
package/dist/errors.js CHANGED
@@ -48,9 +48,13 @@ export class ClickError extends BrowserScaleError {
48
48
  }
49
49
  /**
50
50
  * FillError is thrown by {@link CloudBrowser.fill} when the field could not be
51
- * focused/typed. Fill focuses with the exact same smart click as
52
- * {@link CloudBrowser.click}, so a pre-typing failure is a click failure:
53
- * `code` mirrors it and the full click diagnostics live under `clickError`.
51
+ * focused/typed. The click-phase codes (`"not_found"`,
52
+ * `"occluded_no_reachable_point"`, `"occluded_after_evade"`) mirror the
53
+ * underlying focus click, with diagnostics under `clickError`. The focus codes
54
+ * are `"focus_stolen"` (another element took focus — `focusedElement` names it;
55
+ * fill is strictly target-bound and will not type into the thief) and
56
+ * `"focus_lost"` (focus left the target and nothing is focused). For untargeted
57
+ * stream typing that lets focus move (e.g. OTP), use {@link CloudBrowser.type}.
54
58
  */
55
59
  export class FillError extends BrowserScaleError {
56
60
  constructor(init) {
@@ -58,6 +62,10 @@ export class FillError extends BrowserScaleError {
58
62
  this.name = "FillError";
59
63
  this.code = init.code;
60
64
  this.clickError = init.clickError;
65
+ this.focusedBackendNodeId = init.focusedBackendNodeId;
66
+ this.focusedElement = init.focusedElement;
67
+ this.targetEditable = init.targetEditable;
68
+ this.targetValueLength = init.targetValueLength;
61
69
  this.result = init.result;
62
70
  }
63
71
  }