browserscale-ts 1.2.1 → 1.4.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 +6 -5
- package/dist/browser.d.ts +1 -1
- package/dist/browserscale.browser.js +342 -52
- package/dist/client.d.ts +170 -15
- package/dist/client.js +281 -14
- package/dist/errors.d.ts +33 -6
- package/dist/errors.js +11 -3
- package/dist/gen/wrc_pb.d.ts +630 -14
- package/dist/gen/wrc_pb.js +146 -66
- package/dist/index.d.ts +2 -2
- package/dist/internal/convert.js +21 -0
- package/dist/options.d.ts +81 -2
- package/dist/types.d.ts +66 -9
- package/package.json +1 -1
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,
|
|
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, 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";
|
|
@@ -462,27 +462,43 @@ export declare class CloudBrowser {
|
|
|
462
462
|
*/
|
|
463
463
|
getDOMHash(frameId?: string): Promise<string>;
|
|
464
464
|
/**
|
|
465
|
-
* Returns a compact,
|
|
466
|
-
*
|
|
467
|
-
*
|
|
465
|
+
* Returns a compact, frame-aware view of the visible page — the first thing
|
|
466
|
+
* to reach for on an unfamiliar page, and the cheapest way to re-read the
|
|
467
|
+
* current state afterwards.
|
|
468
468
|
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
* visible and interactable.
|
|
469
|
+
* Each frame opens with header lines carrying the URL, the title and the
|
|
470
|
+
* scroll position, then one line per visible element:
|
|
472
471
|
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
472
|
+
* ```
|
|
473
|
+
* input#email[47] type="email" name="loginId" value="a@b.com" required click "E-Mail"
|
|
474
|
+
* ```
|
|
475
475
|
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
476
|
+
* It spans every frame, pierces open and closed shadow roots, enumerates
|
|
477
|
+
* `<select>` options, and reports live form state: `value=` is what is typed
|
|
478
|
+
* in right now (passwords as a length), `checked=` for boxes. The trailing
|
|
479
|
+
* quoted string is always the label or text, never the value, so an empty and
|
|
480
|
+
* a prefilled field stay distinguishable. Because the headers already carry
|
|
481
|
+
* URL, title and scroll offset, this replaces the usual handful of
|
|
482
|
+
* {@link evaluate} probes after each step.
|
|
483
|
+
*
|
|
484
|
+
* On what to do with the result: backendNodeId (the `47` above) is a handle
|
|
485
|
+
* for this session and can be passed straight to click/fill via
|
|
486
|
+
* {@link node}. It does not survive a new document, so for anything you write
|
|
487
|
+
* into a script, target with {@link css} or {@link js} instead — those calls
|
|
488
|
+
* return the backendNodeId they resolved to, which lets you confirm the
|
|
489
|
+
* durable anchor hits the element you saw.
|
|
490
|
+
*
|
|
491
|
+
* @param opts - optional format and budget overrides; see {@link GetObservationOpts}
|
|
492
|
+
*
|
|
493
|
+
* @returns the observation in the requested format, ready to hand to a model
|
|
478
494
|
*
|
|
479
495
|
* @throws UNKNOWN_ERROR - the observation could not be produced
|
|
480
496
|
*
|
|
481
497
|
* @example
|
|
482
|
-
* const obs = await browser.getObservation(
|
|
483
|
-
* console.log(obs
|
|
498
|
+
* const obs = await browser.getObservation();
|
|
499
|
+
* console.log(obs);
|
|
484
500
|
*/
|
|
485
|
-
getObservation(opts?: GetObservationOpts): Promise<
|
|
501
|
+
getObservation(opts?: GetObservationOpts): Promise<string>;
|
|
486
502
|
/**
|
|
487
503
|
* Captures a single image of the page's current frame and returns it as
|
|
488
504
|
* base64-encoded image bytes.
|
|
@@ -818,6 +834,36 @@ export declare class CloudBrowser {
|
|
|
818
834
|
* await browser.insertText("hello world");
|
|
819
835
|
*/
|
|
820
836
|
insertText(text: string): Promise<void>;
|
|
837
|
+
/**
|
|
838
|
+
* Types `text` into the currently focused element as a per-key stream of real
|
|
839
|
+
* keyboard events (keyDown/char/keyUp with the context's QWERTZ/QWERTY layout
|
|
840
|
+
* and human cadence) — unlike {@link insertText}, a single IME-style commit
|
|
841
|
+
* with no key events.
|
|
842
|
+
*
|
|
843
|
+
* `type` is intentionally UNtargeted and loose: it does not locate or focus
|
|
844
|
+
* any element and does NOT pin focus, so the page is free to route keys and
|
|
845
|
+
* move focus between fields mid-stream — ideal for one-time-code / OTP inputs
|
|
846
|
+
* that auto-advance to the next box on each digit. To type one specific field
|
|
847
|
+
* that must stay focused for the whole value, use {@link fill} instead
|
|
848
|
+
* (strict, target-bound, per-key focus-verified).
|
|
849
|
+
*
|
|
850
|
+
* Nothing is focused for you: {@link click} (or {@link fill}) the field first,
|
|
851
|
+
* or otherwise ensure focus, before calling `type`.
|
|
852
|
+
*
|
|
853
|
+
* @param text - the text to type as real key events
|
|
854
|
+
* @param opts - optional: `clearFirst` clears the focused field (Ctrl+A,
|
|
855
|
+
* Delete) before typing
|
|
856
|
+
*
|
|
857
|
+
* @throws UNKNOWN_ERROR - the page/context was torn down mid-stream
|
|
858
|
+
*
|
|
859
|
+
* @example
|
|
860
|
+
* // OTP field that auto-advances across boxes.
|
|
861
|
+
* await browser.click(css("input.otp-0"));
|
|
862
|
+
* await browser.type("123456");
|
|
863
|
+
*/
|
|
864
|
+
type(text: string, opts?: {
|
|
865
|
+
clearFirst?: boolean;
|
|
866
|
+
}): Promise<void>;
|
|
821
867
|
/**
|
|
822
868
|
* Fires a single key-down event.
|
|
823
869
|
*
|
|
@@ -901,4 +947,113 @@ export declare class CloudBrowser {
|
|
|
901
947
|
timeoutMs?: number;
|
|
902
948
|
retryAmount?: number;
|
|
903
949
|
}): Promise<string>;
|
|
950
|
+
/**
|
|
951
|
+
* Returns the ICE servers (TURN URL + short-lived credentials) to put in
|
|
952
|
+
* your `RTCPeerConnection` BEFORE creating the offer, so it can gather relay
|
|
953
|
+
* candidates.
|
|
954
|
+
*
|
|
955
|
+
* Live streaming is a two-step, client-offerer handshake: call
|
|
956
|
+
* `getStreamConfig`, build your peer with the returned servers, create an
|
|
957
|
+
* offer, then pass its SDP to {@link startStream} and apply the answer.
|
|
958
|
+
*
|
|
959
|
+
* @returns the ICE servers for the client `RTCPeerConnection`
|
|
960
|
+
*
|
|
961
|
+
* @throws UNKNOWN_ERROR - TURN is not configured on the server
|
|
962
|
+
*
|
|
963
|
+
* @example
|
|
964
|
+
* const ice = await browser.getStreamConfig();
|
|
965
|
+
* const pc = new RTCPeerConnection({ iceServers: ice });
|
|
966
|
+
*/
|
|
967
|
+
getStreamConfig(): Promise<IceServer[]>;
|
|
968
|
+
/**
|
|
969
|
+
* Answers your WebRTC SDP offer and starts streaming the page as a video
|
|
970
|
+
* track, returning the SDP answer to set as your peer's remote description.
|
|
971
|
+
* The browser is the answerer; you are the offerer (see
|
|
972
|
+
* {@link getStreamConfig} for the credentials to build the offer).
|
|
973
|
+
*
|
|
974
|
+
* @param offerSdp - your `RTCPeerConnection`'s SDP offer
|
|
975
|
+
*
|
|
976
|
+
* @returns the SDP answer to apply as the remote description
|
|
977
|
+
*
|
|
978
|
+
* @throws UNKNOWN_ERROR - the offer was empty, TURN is unconfigured, or the
|
|
979
|
+
* browser could not negotiate the stream
|
|
980
|
+
*
|
|
981
|
+
* @example
|
|
982
|
+
* const answer = await browser.startStream(offer.sdp);
|
|
983
|
+
* await pc.setRemoteDescription({ type: "answer", sdp: answer });
|
|
984
|
+
*/
|
|
985
|
+
startStream(offerSdp: string): Promise<string>;
|
|
986
|
+
/**
|
|
987
|
+
* Tears down the live video stream for the session's page. Safe to call even
|
|
988
|
+
* if no stream is running.
|
|
989
|
+
*
|
|
990
|
+
* @throws UNKNOWN_ERROR - the stream could not be stopped
|
|
991
|
+
*
|
|
992
|
+
* @example
|
|
993
|
+
* await browser.stopStream();
|
|
994
|
+
*/
|
|
995
|
+
stopStream(): Promise<void>;
|
|
996
|
+
/**
|
|
997
|
+
* Registers a one-shot "reaction": a background poller (one shared loop per
|
|
998
|
+
* page) watches for the `match` locator and, as soon as it matches, clicks it
|
|
999
|
+
* with the full smart-click machinery (scroll, human path, occlusion gate,
|
|
1000
|
+
* evade) — then removes itself. The poller yields to any in-flight input
|
|
1001
|
+
* action and only fires while the pointer is idle, so a reaction naturally
|
|
1002
|
+
* slots into the gaps of a retrying foreground action (e.g. it dismisses a
|
|
1003
|
+
* newsletter modal blocking a {@link CloudBrowser.click}, after which the
|
|
1004
|
+
* click's own retry succeeds). Reactions are scoped to the page and torn
|
|
1005
|
+
* down automatically when the page/session ends.
|
|
1006
|
+
*
|
|
1007
|
+
* `match` must be a `css()` or `js()` {@link Locator} — `node()`/`at()` are
|
|
1008
|
+
* rejected. Use `.inAllFrames()` to watch every frame and `.visible(false)`
|
|
1009
|
+
* to opt out of the default visibility gate. Pass a `ReactionOpts` to click a
|
|
1010
|
+
* different target (`on`), change the button/click count, or the poll cadence.
|
|
1011
|
+
*
|
|
1012
|
+
* @param match - the css()/js() locator to watch for
|
|
1013
|
+
* @param opts - optional reaction customization; see {@link ReactionOpts}
|
|
1014
|
+
*
|
|
1015
|
+
* @returns the reactionId (pass to {@link CloudBrowser.removeReaction})
|
|
1016
|
+
*
|
|
1017
|
+
* @throws {@link BrowserScaleError} - `match` (or `opts.on`) is not a css()/js()
|
|
1018
|
+
* locator, or a server/transport error
|
|
1019
|
+
*
|
|
1020
|
+
* @example
|
|
1021
|
+
* // Auto-dismiss a consent button whenever it appears, in any frame.
|
|
1022
|
+
* const id = await browser.addReaction(css("button#accept").inAllFrames());
|
|
1023
|
+
*
|
|
1024
|
+
* @example
|
|
1025
|
+
* // Watch for a newsletter modal, but click its close "X" instead.
|
|
1026
|
+
* const id = await browser.addReaction(css("#newsletter-modal"), {
|
|
1027
|
+
* on: css(".modal-close"),
|
|
1028
|
+
* });
|
|
1029
|
+
*/
|
|
1030
|
+
addReaction(match: Locator, opts?: ReactionOpts): Promise<string>;
|
|
1031
|
+
/**
|
|
1032
|
+
* Removes a pending reaction by id. Returns `false` if the reaction had
|
|
1033
|
+
* already fired (one-shot) or was never registered.
|
|
1034
|
+
*
|
|
1035
|
+
* @param reactionId - id returned by {@link CloudBrowser.addReaction}
|
|
1036
|
+
*
|
|
1037
|
+
* @returns true if a pending reaction with this id existed and was removed
|
|
1038
|
+
*
|
|
1039
|
+
* @example
|
|
1040
|
+
* const removed = await browser.removeReaction(id);
|
|
1041
|
+
*/
|
|
1042
|
+
removeReaction(reactionId: string): Promise<boolean>;
|
|
1043
|
+
/**
|
|
1044
|
+
* Returns the still-pending reactions registered for the current page.
|
|
1045
|
+
* Reactions that have already fired (one-shot) are not included.
|
|
1046
|
+
*
|
|
1047
|
+
* @returns the pending reactions for the page
|
|
1048
|
+
*
|
|
1049
|
+
* @example
|
|
1050
|
+
* const pending = await browser.listReactions();
|
|
1051
|
+
* for (const r of pending) console.log(r.reactionId, r.matchSelector);
|
|
1052
|
+
*/
|
|
1053
|
+
listReactions(): Promise<ReactionInfo[]>;
|
|
1054
|
+
/**
|
|
1055
|
+
* @internal — a reaction match/action must be a css()/js() locator: node()
|
|
1056
|
+
* and at() are rejected (a reaction watches for a condition, like a wait).
|
|
1057
|
+
*/
|
|
1058
|
+
private assertReactionLocator;
|
|
904
1059
|
}
|
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
|
/**
|
|
@@ -701,38 +705,70 @@ export class CloudBrowser {
|
|
|
701
705
|
return resp.hash;
|
|
702
706
|
}
|
|
703
707
|
/**
|
|
704
|
-
* Returns a compact,
|
|
705
|
-
*
|
|
706
|
-
*
|
|
708
|
+
* Returns a compact, frame-aware view of the visible page — the first thing
|
|
709
|
+
* to reach for on an unfamiliar page, and the cheapest way to re-read the
|
|
710
|
+
* current state afterwards.
|
|
711
|
+
*
|
|
712
|
+
* Each frame opens with header lines carrying the URL, the title and the
|
|
713
|
+
* scroll position, then one line per visible element:
|
|
714
|
+
*
|
|
715
|
+
* ```
|
|
716
|
+
* input#email[47] type="email" name="loginId" value="a@b.com" required click "E-Mail"
|
|
717
|
+
* ```
|
|
718
|
+
*
|
|
719
|
+
* It spans every frame, pierces open and closed shadow roots, enumerates
|
|
720
|
+
* `<select>` options, and reports live form state: `value=` is what is typed
|
|
721
|
+
* in right now (passwords as a length), `checked=` for boxes. The trailing
|
|
722
|
+
* quoted string is always the label or text, never the value, so an empty and
|
|
723
|
+
* a prefilled field stay distinguishable. Because the headers already carry
|
|
724
|
+
* URL, title and scroll offset, this replaces the usual handful of
|
|
725
|
+
* {@link evaluate} probes after each step.
|
|
707
726
|
*
|
|
708
|
-
*
|
|
709
|
-
*
|
|
710
|
-
*
|
|
727
|
+
* On what to do with the result: backendNodeId (the `47` above) is a handle
|
|
728
|
+
* for this session and can be passed straight to click/fill via
|
|
729
|
+
* {@link node}. It does not survive a new document, so for anything you write
|
|
730
|
+
* into a script, target with {@link css} or {@link js} instead — those calls
|
|
731
|
+
* return the backendNodeId they resolved to, which lets you confirm the
|
|
732
|
+
* durable anchor hits the element you saw.
|
|
711
733
|
*
|
|
712
|
-
* @param opts - optional
|
|
713
|
-
* omit either to use the server default
|
|
734
|
+
* @param opts - optional format and budget overrides; see {@link GetObservationOpts}
|
|
714
735
|
*
|
|
715
|
-
* @returns
|
|
716
|
-
* and a `json` payload of the structured observation
|
|
736
|
+
* @returns the observation in the requested format, ready to hand to a model
|
|
717
737
|
*
|
|
718
738
|
* @throws UNKNOWN_ERROR - the observation could not be produced
|
|
719
739
|
*
|
|
720
740
|
* @example
|
|
721
|
-
* const obs = await browser.getObservation(
|
|
722
|
-
* console.log(obs
|
|
741
|
+
* const obs = await browser.getObservation();
|
|
742
|
+
* console.log(obs);
|
|
723
743
|
*/
|
|
724
744
|
async getObservation(opts) {
|
|
725
745
|
const req = create(GetObservationRequestSchema, {
|
|
726
746
|
sessionId: this.sessionId,
|
|
727
747
|
apiKey: this.apiKey,
|
|
728
748
|
});
|
|
749
|
+
if (opts?.format !== undefined)
|
|
750
|
+
req.format = opts.format;
|
|
729
751
|
if (opts?.maxElementsPerFrame !== undefined) {
|
|
730
752
|
req.maxElementsPerFrame = opts.maxElementsPerFrame;
|
|
731
753
|
}
|
|
732
754
|
if (opts?.maxTextLength !== undefined)
|
|
733
755
|
req.maxTextLength = opts.maxTextLength;
|
|
756
|
+
if (opts?.maxTotalTokens !== undefined)
|
|
757
|
+
req.maxTotalTokens = opts.maxTotalTokens;
|
|
758
|
+
if (opts?.includeBounds !== undefined)
|
|
759
|
+
req.includeBounds = opts.includeBounds;
|
|
760
|
+
if (opts?.viewportOnly !== undefined)
|
|
761
|
+
req.viewportOnly = opts.viewportOnly;
|
|
762
|
+
if (opts?.backendNodeId !== undefined)
|
|
763
|
+
req.backendNodeId = opts.backendNodeId;
|
|
764
|
+
if (opts?.selector !== undefined)
|
|
765
|
+
req.selector = opts.selector;
|
|
766
|
+
if (opts?.jsExpression !== undefined)
|
|
767
|
+
req.jsExpression = opts.jsExpression;
|
|
768
|
+
if (opts?.frameId !== undefined)
|
|
769
|
+
req.frameId = opts.frameId;
|
|
734
770
|
const resp = await this.client.getObservation(req);
|
|
735
|
-
return
|
|
771
|
+
return resp.observation;
|
|
736
772
|
}
|
|
737
773
|
/**
|
|
738
774
|
* Captures a single image of the page's current frame and returns it as
|
|
@@ -1243,6 +1279,43 @@ export class CloudBrowser {
|
|
|
1243
1279
|
text,
|
|
1244
1280
|
}));
|
|
1245
1281
|
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Types `text` into the currently focused element as a per-key stream of real
|
|
1284
|
+
* keyboard events (keyDown/char/keyUp with the context's QWERTZ/QWERTY layout
|
|
1285
|
+
* and human cadence) — unlike {@link insertText}, a single IME-style commit
|
|
1286
|
+
* with no key events.
|
|
1287
|
+
*
|
|
1288
|
+
* `type` is intentionally UNtargeted and loose: it does not locate or focus
|
|
1289
|
+
* any element and does NOT pin focus, so the page is free to route keys and
|
|
1290
|
+
* move focus between fields mid-stream — ideal for one-time-code / OTP inputs
|
|
1291
|
+
* that auto-advance to the next box on each digit. To type one specific field
|
|
1292
|
+
* that must stay focused for the whole value, use {@link fill} instead
|
|
1293
|
+
* (strict, target-bound, per-key focus-verified).
|
|
1294
|
+
*
|
|
1295
|
+
* Nothing is focused for you: {@link click} (or {@link fill}) the field first,
|
|
1296
|
+
* or otherwise ensure focus, before calling `type`.
|
|
1297
|
+
*
|
|
1298
|
+
* @param text - the text to type as real key events
|
|
1299
|
+
* @param opts - optional: `clearFirst` clears the focused field (Ctrl+A,
|
|
1300
|
+
* Delete) before typing
|
|
1301
|
+
*
|
|
1302
|
+
* @throws UNKNOWN_ERROR - the page/context was torn down mid-stream
|
|
1303
|
+
*
|
|
1304
|
+
* @example
|
|
1305
|
+
* // OTP field that auto-advances across boxes.
|
|
1306
|
+
* await browser.click(css("input.otp-0"));
|
|
1307
|
+
* await browser.type("123456");
|
|
1308
|
+
*/
|
|
1309
|
+
async type(text, opts) {
|
|
1310
|
+
const req = create(TypeRequestSchema, {
|
|
1311
|
+
sessionId: this.sessionId,
|
|
1312
|
+
apiKey: this.apiKey,
|
|
1313
|
+
text,
|
|
1314
|
+
});
|
|
1315
|
+
if (opts?.clearFirst)
|
|
1316
|
+
req.clearFirst = true;
|
|
1317
|
+
await this.client.type(req);
|
|
1318
|
+
}
|
|
1246
1319
|
/**
|
|
1247
1320
|
* Fires a single key-down event.
|
|
1248
1321
|
*
|
|
@@ -1358,4 +1431,198 @@ export class CloudBrowser {
|
|
|
1358
1431
|
}));
|
|
1359
1432
|
return resp.result;
|
|
1360
1433
|
}
|
|
1434
|
+
// ──────────────────────────────────────────────────────────────────
|
|
1435
|
+
// Live streaming (WebRTC)
|
|
1436
|
+
// ──────────────────────────────────────────────────────────────────
|
|
1437
|
+
/**
|
|
1438
|
+
* Returns the ICE servers (TURN URL + short-lived credentials) to put in
|
|
1439
|
+
* your `RTCPeerConnection` BEFORE creating the offer, so it can gather relay
|
|
1440
|
+
* candidates.
|
|
1441
|
+
*
|
|
1442
|
+
* Live streaming is a two-step, client-offerer handshake: call
|
|
1443
|
+
* `getStreamConfig`, build your peer with the returned servers, create an
|
|
1444
|
+
* offer, then pass its SDP to {@link startStream} and apply the answer.
|
|
1445
|
+
*
|
|
1446
|
+
* @returns the ICE servers for the client `RTCPeerConnection`
|
|
1447
|
+
*
|
|
1448
|
+
* @throws UNKNOWN_ERROR - TURN is not configured on the server
|
|
1449
|
+
*
|
|
1450
|
+
* @example
|
|
1451
|
+
* const ice = await browser.getStreamConfig();
|
|
1452
|
+
* const pc = new RTCPeerConnection({ iceServers: ice });
|
|
1453
|
+
*/
|
|
1454
|
+
async getStreamConfig() {
|
|
1455
|
+
const resp = await this.client.getStreamConfig(create(GetStreamConfigRequestSchema, {
|
|
1456
|
+
sessionId: this.sessionId,
|
|
1457
|
+
apiKey: this.apiKey,
|
|
1458
|
+
}));
|
|
1459
|
+
return resp.iceServers.map((s) => ({
|
|
1460
|
+
urls: s.urls,
|
|
1461
|
+
username: s.username ?? "",
|
|
1462
|
+
credential: s.credential ?? "",
|
|
1463
|
+
}));
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Answers your WebRTC SDP offer and starts streaming the page as a video
|
|
1467
|
+
* track, returning the SDP answer to set as your peer's remote description.
|
|
1468
|
+
* The browser is the answerer; you are the offerer (see
|
|
1469
|
+
* {@link getStreamConfig} for the credentials to build the offer).
|
|
1470
|
+
*
|
|
1471
|
+
* @param offerSdp - your `RTCPeerConnection`'s SDP offer
|
|
1472
|
+
*
|
|
1473
|
+
* @returns the SDP answer to apply as the remote description
|
|
1474
|
+
*
|
|
1475
|
+
* @throws UNKNOWN_ERROR - the offer was empty, TURN is unconfigured, or the
|
|
1476
|
+
* browser could not negotiate the stream
|
|
1477
|
+
*
|
|
1478
|
+
* @example
|
|
1479
|
+
* const answer = await browser.startStream(offer.sdp);
|
|
1480
|
+
* await pc.setRemoteDescription({ type: "answer", sdp: answer });
|
|
1481
|
+
*/
|
|
1482
|
+
async startStream(offerSdp) {
|
|
1483
|
+
const resp = await this.client.startStream(create(StartStreamRequestSchema, {
|
|
1484
|
+
sessionId: this.sessionId,
|
|
1485
|
+
apiKey: this.apiKey,
|
|
1486
|
+
offerSdp,
|
|
1487
|
+
}));
|
|
1488
|
+
return resp.answerSdp;
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Tears down the live video stream for the session's page. Safe to call even
|
|
1492
|
+
* if no stream is running.
|
|
1493
|
+
*
|
|
1494
|
+
* @throws UNKNOWN_ERROR - the stream could not be stopped
|
|
1495
|
+
*
|
|
1496
|
+
* @example
|
|
1497
|
+
* await browser.stopStream();
|
|
1498
|
+
*/
|
|
1499
|
+
async stopStream() {
|
|
1500
|
+
await this.client.stopStream(create(StopStreamRequestSchema, {
|
|
1501
|
+
sessionId: this.sessionId,
|
|
1502
|
+
apiKey: this.apiKey,
|
|
1503
|
+
}));
|
|
1504
|
+
}
|
|
1505
|
+
// ──────────────────────────────────────────────────────────────────
|
|
1506
|
+
// Reactions
|
|
1507
|
+
// ──────────────────────────────────────────────────────────────────
|
|
1508
|
+
/**
|
|
1509
|
+
* Registers a one-shot "reaction": a background poller (one shared loop per
|
|
1510
|
+
* page) watches for the `match` locator and, as soon as it matches, clicks it
|
|
1511
|
+
* with the full smart-click machinery (scroll, human path, occlusion gate,
|
|
1512
|
+
* evade) — then removes itself. The poller yields to any in-flight input
|
|
1513
|
+
* action and only fires while the pointer is idle, so a reaction naturally
|
|
1514
|
+
* slots into the gaps of a retrying foreground action (e.g. it dismisses a
|
|
1515
|
+
* newsletter modal blocking a {@link CloudBrowser.click}, after which the
|
|
1516
|
+
* click's own retry succeeds). Reactions are scoped to the page and torn
|
|
1517
|
+
* down automatically when the page/session ends.
|
|
1518
|
+
*
|
|
1519
|
+
* `match` must be a `css()` or `js()` {@link Locator} — `node()`/`at()` are
|
|
1520
|
+
* rejected. Use `.inAllFrames()` to watch every frame and `.visible(false)`
|
|
1521
|
+
* to opt out of the default visibility gate. Pass a `ReactionOpts` to click a
|
|
1522
|
+
* different target (`on`), change the button/click count, or the poll cadence.
|
|
1523
|
+
*
|
|
1524
|
+
* @param match - the css()/js() locator to watch for
|
|
1525
|
+
* @param opts - optional reaction customization; see {@link ReactionOpts}
|
|
1526
|
+
*
|
|
1527
|
+
* @returns the reactionId (pass to {@link CloudBrowser.removeReaction})
|
|
1528
|
+
*
|
|
1529
|
+
* @throws {@link BrowserScaleError} - `match` (or `opts.on`) is not a css()/js()
|
|
1530
|
+
* locator, or a server/transport error
|
|
1531
|
+
*
|
|
1532
|
+
* @example
|
|
1533
|
+
* // Auto-dismiss a consent button whenever it appears, in any frame.
|
|
1534
|
+
* const id = await browser.addReaction(css("button#accept").inAllFrames());
|
|
1535
|
+
*
|
|
1536
|
+
* @example
|
|
1537
|
+
* // Watch for a newsletter modal, but click its close "X" instead.
|
|
1538
|
+
* const id = await browser.addReaction(css("#newsletter-modal"), {
|
|
1539
|
+
* on: css(".modal-close"),
|
|
1540
|
+
* });
|
|
1541
|
+
*/
|
|
1542
|
+
async addReaction(match, opts) {
|
|
1543
|
+
this.assertReactionLocator("addReaction", match, "match");
|
|
1544
|
+
const req = create(AddReactionRequestSchema, {
|
|
1545
|
+
sessionId: this.sessionId,
|
|
1546
|
+
apiKey: this.apiKey,
|
|
1547
|
+
});
|
|
1548
|
+
if (match.selector)
|
|
1549
|
+
req.matchSelector = match.selector;
|
|
1550
|
+
if (match.jsExpression)
|
|
1551
|
+
req.matchJsExpression = match.jsExpression;
|
|
1552
|
+
if (match.frameId)
|
|
1553
|
+
req.frameId = match.frameId;
|
|
1554
|
+
if (match.visibleFlag !== undefined)
|
|
1555
|
+
req.visible = match.visibleFlag;
|
|
1556
|
+
if (opts?.on) {
|
|
1557
|
+
this.assertReactionLocator("addReaction", opts.on, "on");
|
|
1558
|
+
if (opts.on.selector)
|
|
1559
|
+
req.actionSelector = opts.on.selector;
|
|
1560
|
+
if (opts.on.jsExpression)
|
|
1561
|
+
req.actionJsExpression = opts.on.jsExpression;
|
|
1562
|
+
}
|
|
1563
|
+
if (opts?.button)
|
|
1564
|
+
req.button = opts.button;
|
|
1565
|
+
if (opts?.clickCount)
|
|
1566
|
+
req.clickCount = opts.clickCount;
|
|
1567
|
+
if (opts?.intervalMs)
|
|
1568
|
+
req.interval = opts.intervalMs;
|
|
1569
|
+
const resp = await this.client.addReaction(req);
|
|
1570
|
+
return resp.reactionId;
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* Removes a pending reaction by id. Returns `false` if the reaction had
|
|
1574
|
+
* already fired (one-shot) or was never registered.
|
|
1575
|
+
*
|
|
1576
|
+
* @param reactionId - id returned by {@link CloudBrowser.addReaction}
|
|
1577
|
+
*
|
|
1578
|
+
* @returns true if a pending reaction with this id existed and was removed
|
|
1579
|
+
*
|
|
1580
|
+
* @example
|
|
1581
|
+
* const removed = await browser.removeReaction(id);
|
|
1582
|
+
*/
|
|
1583
|
+
async removeReaction(reactionId) {
|
|
1584
|
+
const resp = await this.client.removeReaction(create(RemoveReactionRequestSchema, {
|
|
1585
|
+
sessionId: this.sessionId,
|
|
1586
|
+
apiKey: this.apiKey,
|
|
1587
|
+
reactionId,
|
|
1588
|
+
}));
|
|
1589
|
+
return resp.removed;
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* Returns the still-pending reactions registered for the current page.
|
|
1593
|
+
* Reactions that have already fired (one-shot) are not included.
|
|
1594
|
+
*
|
|
1595
|
+
* @returns the pending reactions for the page
|
|
1596
|
+
*
|
|
1597
|
+
* @example
|
|
1598
|
+
* const pending = await browser.listReactions();
|
|
1599
|
+
* for (const r of pending) console.log(r.reactionId, r.matchSelector);
|
|
1600
|
+
*/
|
|
1601
|
+
async listReactions() {
|
|
1602
|
+
const resp = await this.client.listReactions(create(ListReactionsRequestSchema, {
|
|
1603
|
+
sessionId: this.sessionId,
|
|
1604
|
+
apiKey: this.apiKey,
|
|
1605
|
+
}));
|
|
1606
|
+
return resp.reactions.map((r) => ({
|
|
1607
|
+
reactionId: r.reactionId,
|
|
1608
|
+
matchSelector: r.matchSelector ?? "",
|
|
1609
|
+
matchJsExpression: r.matchJsExpression ?? "",
|
|
1610
|
+
actionSelector: r.actionSelector ?? "",
|
|
1611
|
+
actionJsExpression: r.actionJsExpression ?? "",
|
|
1612
|
+
frameId: r.frameId,
|
|
1613
|
+
visible: r.visible,
|
|
1614
|
+
}));
|
|
1615
|
+
}
|
|
1616
|
+
/**
|
|
1617
|
+
* @internal — a reaction match/action must be a css()/js() locator: node()
|
|
1618
|
+
* and at() are rejected (a reaction watches for a condition, like a wait).
|
|
1619
|
+
*/
|
|
1620
|
+
assertReactionLocator(cmd, l, role) {
|
|
1621
|
+
if (l.backendNodeId !== 0 || l.x !== undefined || l.y !== undefined) {
|
|
1622
|
+
throw new BrowserScaleError(`browserscale.${cmd}: ${role} must be a css()/js() locator (node()/at() are not valid)`);
|
|
1623
|
+
}
|
|
1624
|
+
if (!l.selector && !l.jsExpression) {
|
|
1625
|
+
throw new BrowserScaleError(`browserscale.${cmd}: ${role} must have a CSS selector or JS expression`);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1361
1628
|
}
|
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.
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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
|
-
/**
|
|
72
|
+
/** Machine-stable failure code (see the class doc for the full set). */
|
|
69
73
|
readonly code: string;
|
|
70
|
-
/**
|
|
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.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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
|
}
|