browserscale-ts 1.7.1 → 1.9.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 CHANGED
@@ -2,20 +2,17 @@ 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, GetAuthSessionRequestSchema, SetAuthSessionRequestSchema, InspectAtPositionRequestSchema, HighlightNodeRequestSchema, InsertTextRequestSchema, TypeRequestSchema, PressKeyRequestSchema, ReleaseKeyRequestSchema, GetSelectionRequestSchema, SolveCaptchaRequestSchema, GetStreamConfigRequestSchema, StartStreamRequestSchema, StopStreamRequestSchema, AddReactionRequestSchema, RemoveReactionRequestSchema, ListReactionsRequestSchema, StartNetworkCaptureRequestSchema, StopNetworkCaptureRequestSchema, StartDomMirrorRequestSchema, StopDomMirrorRequestSchema, GetDomChildrenRequestSchema, ReleaseDomSubtreeRequestSchema, RevealDomNodeRequestSchema, GetDomRevisionRequestSchema, StreamDomEventsRequestSchema, StreamNetworkExchangesRequestSchema, RunScriptRequestSchema, StartScriptRequestSchema, StopScriptsRequestSchema, ListScriptRunsRequestSchema, StreamScriptEventsRequestSchema, } from "./gen/wrc_pb.js";
6
- import { DefaultWaitTimeoutMs } from "./defaults.js";
7
- import { BrowserScaleError } from "./errors.js";
5
+ SetProxyRequestSchema, GetPagesRequestSchema, GetUsageRequestSchema, NavigateRequestSchema, LoadHTMLRequestSchema, EvaluateRequestSchema, WaitForAnyParamsSchema, WaitConditionSchema, ClickRequestSchema, FillRequestSchema, MoveToRequestSchema, ScrollToRequestSchema, DragRequestSchema, SelectOptionRequestSchema, GetDOMRequestSchema, GetDOMHashRequestSchema, GetObservationRequestSchema, ScreenshotRequestSchema, ReadCanvasRequestSchema, SetBlockListRequestSchema, SetStaticPathsRequestSchema, WaitForAnyRequestRequestSchema, WaitForAnyResponseRequestSchema, ModifyRequestRequestSchema, GetCookiesRequestSchema, SetCookiesRequestSchema, ClearCookiesRequestSchema, GetStorageRequestSchema, SetStorageRequestSchema, ClearStorageRequestSchema, GetAuthSessionRequestSchema, SetAuthSessionRequestSchema, InspectAtPositionRequestSchema, HighlightNodeRequestSchema, InsertTextRequestSchema, TypeRequestSchema, PressKeyRequestSchema, ReleaseKeyRequestSchema, GetSelectionRequestSchema, SolveCaptchaRequestSchema, GetStreamConfigRequestSchema, StartStreamRequestSchema, StopStreamRequestSchema, AddReactionRequestSchema, RemoveReactionRequestSchema, ListReactionsRequestSchema, StartNetworkCaptureRequestSchema, StopNetworkCaptureRequestSchema, StartDomMirrorRequestSchema, StopDomMirrorRequestSchema, GetDomChildrenRequestSchema, ReleaseDomSubtreeRequestSchema, RevealDomNodeRequestSchema, GetDomRevisionRequestSchema, StreamDomEventsRequestSchema, StreamNetworkExchangesRequestSchema, RunScriptRequestSchema, StartScriptRequestSchema, StopScriptsRequestSchema, ListScriptRunsRequestSchema, StreamScriptEventsRequestSchema, } from "./gen/wrc_pb.js";
6
+ import { BrowserScaleError, CommandError, throwCommandError } from "./errors.js";
8
7
  import { NetworkCapture } from "./network-capture.js";
9
8
  import { ScriptFollow, ScriptRun, scriptLogEntryFromProto, } from "./scripts.js";
10
9
  import { DomMirror, } from "./dom-mirror.js";
11
- import { authSessionFromProto, authSessionToProto, cookieParamFromProto, cookieParamsToProto, elementFields, headerModsToProto, headersToProto, interceptedRequestFromProto, interceptedResponseFromProto, pageInfoFromProto, rectFromProto, splitRequestPatterns, storageEntriesToProto, storageEntryFromProto, unwrapClick, unwrapDrag, unwrapFill, unwrapMove, unwrapScroll, unwrapSelect, unwrapWait, } from "./internal/convert.js";
10
+ import { authSessionFromProto, authSessionToProto, cookieParamFromProto, cookieParamsToProto, elementFields, headerModsToProto, headersToProto, interceptedRequestFromProto, interceptedResponseFromProto, pageInfoFromProto, rectFromProto, sessionUsageFromProto, splitRequestPatterns, storageEntriesToProto, storageEntryFromProto, unwrapClick, unwrapDrag, unwrapFill, unwrapMove, unwrapScroll, unwrapSelect, unwrapWait, } from "./internal/convert.js";
12
11
  /**
13
12
  * CloudBrowser is the SDK-side handle for an active browserscale browser session.
14
13
  *
15
14
  * One CloudBrowser corresponds to exactly one browser context, which
16
- * always has at least one page. The session is implicitly bound to its
17
- * primary page server-side — the proto's page_id field is currently
18
- * ignored server-side, so the SDK never sets it.
15
+ * always has at least one page; its commands act on the primary page.
19
16
  *
20
17
  * Construct via rentBrowser() / createWebSocketBrowser() — never
21
18
  * directly.
@@ -52,15 +49,25 @@ export class CloudBrowser {
52
49
  * {@link createWebSocketBrowser} the rental stays untouched; only the
53
50
  * transport is closed.
54
51
  *
55
- * @throws UNKNOWN_ERROR - the stop API or the transport close failed
52
+ * @returns SessionUsage what the session consumed over its whole life, read
53
+ * as it was torn down - no {@link CloudBrowser.getUsage} call is needed
54
+ * before stopping. Undefined when the server could not report it, and for
55
+ * a handle that owns no rental.
56
+ *
57
+ * Rejects only when the stop API or the transport close fails. The session is
58
+ * released either way; retrying a stop is safe.
56
59
  *
57
60
  * @example
58
- * await browser.stopBrowser();
61
+ * const usage = await browser.stopBrowser();
62
+ * if (usage) {
63
+ * console.log(`ran ${usage.wallTime}s, ${usage.cpuTime}s CPU, peak ${usage.peakMemory} bytes`);
64
+ * }
59
65
  */
60
66
  async stopBrowser() {
61
67
  if (this._stopFn) {
62
- await this._stopFn();
68
+ return await this._stopFn();
63
69
  }
70
+ return undefined;
64
71
  }
65
72
  // ──────────────────────────────────────────────────────────────────
66
73
  // Context-level
@@ -77,7 +84,9 @@ export class CloudBrowser {
77
84
  * @param proxyUsername - proxy auth user; empty for unauthenticated proxies
78
85
  * @param proxyPassword - proxy auth password; empty for unauthenticated proxies
79
86
  *
80
- * @throws UNKNOWN_ERROR - the proxy could not be applied
87
+ * Rejects only on a transport failure - a dead session, a page that is gone,
88
+ * a broken connection. This call has no semantic failure of its own, so there
89
+ * are no error codes to branch on.
81
90
  *
82
91
  * @example
83
92
  * await browser.setProxy("proxy.example.com", 8080, "user", "pass");
@@ -95,7 +104,8 @@ export class CloudBrowser {
95
104
  if (proxyPassword)
96
105
  req.proxyPassword = proxyPassword;
97
106
  }
98
- await this.client.setProxy(req);
107
+ const res = await this.client.setProxy(req);
108
+ throwCommandError("setProxy", res.error);
99
109
  }
100
110
  /**
101
111
  * Returns all open pages (tabs and popups) for this session's browser
@@ -107,7 +117,9 @@ export class CloudBrowser {
107
117
  *
108
118
  * @returns PageInfo[] for every page currently open in the context
109
119
  *
110
- * @throws UNKNOWN_ERROR - the pages could not be enumerated
120
+ * Rejects only on a transport failure - a dead session, a page that is gone,
121
+ * a broken connection. This call has no semantic failure of its own, so there
122
+ * are no error codes to branch on.
111
123
  *
112
124
  * @example
113
125
  * const pages = await browser.getPages();
@@ -118,8 +130,40 @@ export class CloudBrowser {
118
130
  sessionId: this.sessionId,
119
131
  apiKey: this.apiKey,
120
132
  }));
133
+ throwCommandError("getPages", resp.error);
121
134
  return resp.pages.map(pageInfoFromProto);
122
135
  }
136
+ /**
137
+ * Reports what this session's browser has consumed so far.
138
+ *
139
+ * Everything but minMemory and averageMemory only ever grows, so polling and
140
+ * diffing two readings gives the cost of what ran in between. The final
141
+ * figures need no call of their own: {@link CloudBrowser.stopBrowser} resolves
142
+ * with them.
143
+ *
144
+ * @returns SessionUsage as of now
145
+ *
146
+ * Rejects only on a transport failure - a dead session, a broken connection.
147
+ * This call has no semantic failure of its own, so there are no error codes to
148
+ * branch on.
149
+ *
150
+ * @example
151
+ * const before = await browser.getUsage();
152
+ * await browser.navigate("https://example.com");
153
+ * const after = await browser.getUsage();
154
+ * console.log(`navigation cost ${after.cpuTime - before.cpuTime}s of CPU`);
155
+ */
156
+ async getUsage() {
157
+ const resp = await this.client.getUsage(create(GetUsageRequestSchema, {
158
+ sessionId: this.sessionId,
159
+ apiKey: this.apiKey,
160
+ }));
161
+ throwCommandError("getUsage", resp.error);
162
+ if (!resp.usage) {
163
+ throw new BrowserScaleError("getUsage: the server sent no usage");
164
+ }
165
+ return sessionUsageFromProto(resp.usage);
166
+ }
123
167
  // ──────────────────────────────────────────────────────────────────
124
168
  // Page navigation / content
125
169
  // ──────────────────────────────────────────────────────────────────
@@ -136,7 +180,16 @@ export class CloudBrowser {
136
180
  * @returns NavigateResult with the final resolved URL and the frameId
137
181
  * of the main frame after navigation
138
182
  *
139
- * @throws UNKNOWN_ERROR - the navigation failed or timed out
183
+ * @throws timeout - nothing committed before the deadline; the page may still
184
+ * be loading, so a longer timeout can be the whole fix
185
+ * @throws net_error - the URL never loaded: DNS, TLS, a refused connection, or
186
+ * a proxy that could not reach it. The message carries the underlying net
187
+ * error name, which is what separates a bad proxy from a bad host - worth
188
+ * logging, since the two need different fixes
189
+ * @throws crashed - the renderer died mid-navigation; the page is unusable and
190
+ * has to be navigated again
191
+ *
192
+ * @see {@link CommandError} for reading the code off the rejection
140
193
  *
141
194
  * @example
142
195
  * await browser.navigate("https://example.com");
@@ -150,6 +203,7 @@ export class CloudBrowser {
150
203
  if (opts?.timeoutMs)
151
204
  req.timeout = opts.timeoutMs;
152
205
  const resp = await this.client.navigate(req);
206
+ throwCommandError("navigate", resp.error);
153
207
  return { frameId: resp.frameId, url: resp.url };
154
208
  }
155
209
  /**
@@ -164,7 +218,11 @@ export class CloudBrowser {
164
218
  * @param html - response body to serve
165
219
  * @param opts - optional headers and statusCode (default 200)
166
220
  *
167
- * @throws UNKNOWN_ERROR - the interceptor could not be installed
221
+ * @throws timeout - the page never requested the URL, so the prepared
222
+ * response had nobody to hand it to; usually the navigation was cancelled
223
+ * or redirected away before reaching it
224
+ *
225
+ * @see {@link CommandError} for reading the code off the rejection
168
226
  *
169
227
  * @example
170
228
  * await browser.loadHTML("https://example.com", "<h1>hi</h1>");
@@ -180,7 +238,8 @@ export class CloudBrowser {
180
238
  });
181
239
  if (opts?.statusCode)
182
240
  req.statusCode = opts.statusCode;
183
- await this.client.loadHTML(req);
241
+ const res = await this.client.loadHTML(req);
242
+ throwCommandError("loadHTML", res.error);
184
243
  }
185
244
  // ──────────────────────────────────────────────────────────────────
186
245
  // Evaluation
@@ -197,16 +256,34 @@ export class CloudBrowser {
197
256
  * The generic T is a TypeScript hint only — there is no runtime
198
257
  * validation that the JS expression actually returned that type.
199
258
  *
259
+ * A falsy answer and a broken expression are different outcomes. Returning
260
+ * null, false or undefined is a successful evaluation and resolves normally;
261
+ * an expression that throws or will not compile rejects with a
262
+ * {@link CommandError}, so a typo can never read as "the page says null".
263
+ *
200
264
  * @param expression - JavaScript expression evaluated in the main frame
201
265
  *
202
266
  * @returns EvaluateResult with either value (non-Element) or element
203
267
  * metadata (Element)
204
268
  *
205
- * @throws UNKNOWN_ERROR - the expression threw or could not be compiled
269
+ * @throws {@link CommandError} - code `"threw"` (the expression raised; the
270
+ * message carries the exception text), `"not_run"` (it could not be
271
+ * compiled, or execution never started), `"aborted"` (the browser stopped
272
+ * execution) or `"no_context"` (the frame had no live script context)
206
273
  *
207
274
  * @example
208
275
  * const res = await browser.evaluate<string>("document.title");
209
276
  * console.log(res.value);
277
+ *
278
+ * @example
279
+ * // Telling a false answer from a broken expression.
280
+ * try {
281
+ * const res = await browser.evaluate<boolean>("window.__ready === true");
282
+ * if (!res.value) { /* legitimately not ready yet *\/ }
283
+ * } catch (e) {
284
+ * if (e instanceof CommandError) throw new Error(`expression broken: ${e.message}`);
285
+ * throw e;
286
+ * }
210
287
  */
211
288
  async evaluate(expression) {
212
289
  return this._evaluate("", expression);
@@ -239,6 +316,15 @@ export class CloudBrowser {
239
316
  if (frameId)
240
317
  req.frameId = frameId;
241
318
  const resp = await this.client.evaluate(req);
319
+ // An expression that threw arrives as success=false in the payload, not as a
320
+ // transport error, so it is raised here rather than by the call above.
321
+ if (resp.error) {
322
+ throw new CommandError({
323
+ command: "evaluate",
324
+ code: resp.error.code,
325
+ message: resp.error.message,
326
+ });
327
+ }
242
328
  let value = null;
243
329
  if (resp.result !== "") {
244
330
  try {
@@ -249,6 +335,7 @@ export class CloudBrowser {
249
335
  }
250
336
  }
251
337
  return {
338
+ success: resp.success,
252
339
  value: value,
253
340
  backendNodeId: resp.backendNodeId,
254
341
  isVisible: resp.isVisible,
@@ -340,7 +427,8 @@ export class CloudBrowser {
340
427
  sessionId: this.sessionId,
341
428
  apiKey: this.apiKey,
342
429
  conditions: pbConds,
343
- timeout: opts?.timeoutMs ?? DefaultWaitTimeoutMs,
430
+ // Left unset unless the caller passed one, so the API applies its default.
431
+ timeout: opts?.timeoutMs,
344
432
  });
345
433
  if (frameId)
346
434
  req.frameId = frameId;
@@ -660,7 +748,9 @@ export class CloudBrowser {
660
748
  * @returns DOMResult with the JSON string in `.dom` (the `.hash` field
661
749
  * is populated by {@link getDOMHash}, not by this call)
662
750
  *
663
- * @throws UNKNOWN_ERROR - the DOM could not be retrieved
751
+ * Rejects only on a transport failure - a dead session, a page that is gone,
752
+ * a broken connection. This call has no semantic failure of its own, so there
753
+ * are no error codes to branch on.
664
754
  *
665
755
  * @example
666
756
  * const { dom } = await browser.getDOM();
@@ -675,6 +765,7 @@ export class CloudBrowser {
675
765
  if (opts?.depth !== undefined)
676
766
  req.depth = opts.depth;
677
767
  const resp = await this.client.getDOM(req);
768
+ throwCommandError("getDOM", resp.error);
678
769
  return { hash: "", dom: resp.dom };
679
770
  }
680
771
  /**
@@ -689,7 +780,9 @@ export class CloudBrowser {
689
780
  *
690
781
  * @returns 16-char hex string (the first 8 bytes of sha256 of the DOM JSON)
691
782
  *
692
- * @throws UNKNOWN_ERROR - the hash could not be computed
783
+ * Rejects only on a transport failure. The hash is computed from a serialized
784
+ * tree, so there is no semantic failure of its own and no error codes to branch
785
+ * on.
693
786
  *
694
787
  * @example
695
788
  * const hash = await browser.getDOMHash();
@@ -738,7 +831,11 @@ export class CloudBrowser {
738
831
  *
739
832
  * @returns the observation in the requested format, ready to hand to a model
740
833
  *
741
- * @throws UNKNOWN_ERROR - the observation could not be produced
834
+ * @throws not_found - the requested scope root is not on the page, so there
835
+ * was nothing to observe. Distinct from an observation that comes back
836
+ * empty, which means the scope exists and holds nothing worth reporting
837
+ *
838
+ * @see {@link CommandError} for reading the code off the rejection
742
839
  *
743
840
  * @example
744
841
  * const obs = await browser.getObservation();
@@ -771,6 +868,7 @@ export class CloudBrowser {
771
868
  if (opts?.frameId !== undefined)
772
869
  req.frameId = opts.frameId;
773
870
  const resp = await this.client.getObservation(req);
871
+ throwCommandError("getObservation", resp.error);
774
872
  return resp.observation;
775
873
  }
776
874
  /**
@@ -787,7 +885,11 @@ export class CloudBrowser {
787
885
  * @returns ScreenshotResult with the base64 image in `dataBase64` and the
788
886
  * physical pixel `width`/`height`
789
887
  *
790
- * @throws UNKNOWN_ERROR - the screenshot could not be captured
888
+ * @throws capture_failed - the page had no frame to copy. A page that has not
889
+ * produced one yet, or is not being composited at the moment, has nothing to
890
+ * hand over; retrying after it renders usually works
891
+ *
892
+ * @see {@link CommandError} for reading the code off the rejection
791
893
  *
792
894
  * @example
793
895
  * const shot = await browser.screenshot({ format: "png" });
@@ -803,6 +905,7 @@ export class CloudBrowser {
803
905
  if (opts?.quality !== undefined)
804
906
  req.quality = opts.quality;
805
907
  const resp = await this.client.screenshot(req);
908
+ throwCommandError("screenshot", resp.error);
806
909
  return {
807
910
  dataBase64: resp.dataBase64,
808
911
  width: resp.width,
@@ -826,11 +929,15 @@ export class CloudBrowser {
826
929
  * canvas `width`/`height`, resolved `frameId`/`backendNodeId` and the
827
930
  * `originClean` flag
828
931
  *
829
- * @throws INVALID_LOCATOR - target is empty, uses at(x,y), or has multiple targets
830
- * @throws ELEMENT_NOT_FOUND - no element matched the locator
831
- * @throws FRAME_NOT_FOUND - the requested frame does not exist
832
- * @throws TIMEOUT - the operation exceeded the server-side timeout
833
- * @throws PAGE_NOT_ALIVE - the page has been closed
932
+ * @throws not_found - no element matched the locator
933
+ * @throws not_element - the expression was truthy but did not yield an element
934
+ * @throws not_readable - the target was found but is not a readable canvas
935
+ *
936
+ * A target that is empty, uses `at(x, y)` or names several things at once is
937
+ * rejected before anything is sent. A closed page or a frame that is gone is a
938
+ * transport failure rather than a code.
939
+ *
940
+ * @see {@link CommandError} for reading the code off the rejection
834
941
  *
835
942
  * @example
836
943
  * const res = await browser.readCanvas(css("#game canvas"));
@@ -862,6 +969,7 @@ export class CloudBrowser {
862
969
  req.sh = opts.sh;
863
970
  }
864
971
  const resp = await this.client.readCanvas(req);
972
+ throwCommandError("readCanvas", resp.error);
865
973
  return {
866
974
  success: resp.success,
867
975
  frameId: resp.frameId,
@@ -885,7 +993,9 @@ export class CloudBrowser {
885
993
  *
886
994
  * @param patterns - URL wildcards to block; empty array clears the list
887
995
  *
888
- * @throws UNKNOWN_ERROR - the blocklist could not be applied
996
+ * Rejects only on a transport failure - a dead session, a page that is gone,
997
+ * a broken connection. This call has no semantic failure of its own, so there
998
+ * are no error codes to branch on.
889
999
  *
890
1000
  * @example
891
1001
  * await browser.setBlockList([
@@ -894,11 +1004,12 @@ export class CloudBrowser {
894
1004
  * ]);
895
1005
  */
896
1006
  async setBlockList(patterns) {
897
- await this.client.setBlockList(create(SetBlockListRequestSchema, {
1007
+ const res = await this.client.setBlockList(create(SetBlockListRequestSchema, {
898
1008
  sessionId: this.sessionId,
899
1009
  apiKey: this.apiKey,
900
1010
  patterns,
901
1011
  }));
1012
+ throwCommandError("setBlockList", res.error);
902
1013
  }
903
1014
  /**
904
1015
  * Configures the session to serve cached static responses for requests
@@ -912,18 +1023,21 @@ export class CloudBrowser {
912
1023
  * @param blobName - server-side identifier of the snapshot to serve from
913
1024
  * @param patterns - URL wildcards to redirect to the cache; empty disables
914
1025
  *
915
- * @throws UNKNOWN_ERROR - the static paths could not be configured
1026
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1027
+ * a broken connection. This call has no semantic failure of its own, so there
1028
+ * are no error codes to branch on.
916
1029
  *
917
1030
  * @example
918
1031
  * await browser.setStaticPaths("snap-2026-05", ["*.example.com/*"]);
919
1032
  */
920
1033
  async setStaticPaths(blobName, patterns) {
921
- await this.client.setStaticPaths(create(SetStaticPathsRequestSchema, {
1034
+ const res = await this.client.setStaticPaths(create(SetStaticPathsRequestSchema, {
922
1035
  sessionId: this.sessionId,
923
1036
  apiKey: this.apiKey,
924
1037
  blobName,
925
1038
  patterns,
926
1039
  }));
1040
+ throwCommandError("setStaticPaths", res.error);
927
1041
  }
928
1042
  /**
929
1043
  * Blocks until the next request whose URL matches one of the supplied
@@ -940,7 +1054,12 @@ export class CloudBrowser {
940
1054
  * (the captured method/URL/headers/body; null if intercepted with
941
1055
  * no body)
942
1056
  *
943
- * @throws UNKNOWN_ERROR - the wait timed out or no patterns were supplied
1057
+ * @throws timeout - no request matched any pattern before the deadline.
1058
+ * Nothing occurring is an answer, and it stays distinguishable from a
1059
+ * connection that died on the way. Supplying no patterns is a caller mistake
1060
+ * rather than an outcome, and rejects separately
1061
+ *
1062
+ * @see {@link CommandError} for reading the code off the rejection
944
1063
  *
945
1064
  * @example
946
1065
  * const { index, request } = await browser.waitForAnyRequest(
@@ -963,6 +1082,7 @@ export class CloudBrowser {
963
1082
  if (opts?.timeoutMs)
964
1083
  req.timeout = opts.timeoutMs;
965
1084
  const resp = await this.client.waitForAnyRequest(req);
1085
+ throwCommandError("waitForAnyRequest", resp.error);
966
1086
  return {
967
1087
  index: resp.index,
968
1088
  request: interceptedRequestFromProto(resp.request),
@@ -981,6 +1101,13 @@ export class CloudBrowser {
981
1101
  * @returns object with `index` (matched pattern index) and `response`
982
1102
  * (the captured status/headers/body; null if no body was returned)
983
1103
  *
1104
+ * @throws timeout - no response matched any pattern before the deadline.
1105
+ * Nothing occurring is an answer, and it stays distinguishable from a
1106
+ * connection that died on the way. Supplying no patterns is a caller mistake
1107
+ * rather than an outcome, and rejects separately
1108
+ *
1109
+ * @see {@link CommandError} for reading the code off the rejection
1110
+ *
984
1111
  * @example
985
1112
  * const { index, response } = await browser.waitForAnyResponse(
986
1113
  * [{ url: "*\/api/login" }],
@@ -1002,6 +1129,7 @@ export class CloudBrowser {
1002
1129
  if (opts?.timeoutMs)
1003
1130
  req.timeout = opts.timeoutMs;
1004
1131
  const resp = await this.client.waitForAnyResponse(req);
1132
+ throwCommandError("waitForAnyResponse", resp.error);
1005
1133
  return {
1006
1134
  index: resp.index,
1007
1135
  response: interceptedResponseFromProto(resp.response),
@@ -1023,7 +1151,10 @@ export class CloudBrowser {
1023
1151
  * were actually sent on the wire after modifications were applied;
1024
1152
  * null when no request payload was reported
1025
1153
  *
1026
- * @throws UNKNOWN_ERROR - no matching request appeared within the timeout
1154
+ * @throws timeout - no matching request appeared before the deadline, so
1155
+ * nothing was modified
1156
+ *
1157
+ * @see {@link CommandError} for reading the code off the rejection
1027
1158
  *
1028
1159
  * @example
1029
1160
  * const req = await browser.modifyRequest("*\/api/me", {
@@ -1047,6 +1178,7 @@ export class CloudBrowser {
1047
1178
  if (opts?.timeoutMs)
1048
1179
  req.timeout = opts.timeoutMs;
1049
1180
  const resp = await this.client.modifyRequest(req);
1181
+ throwCommandError("modifyRequest", resp.error);
1050
1182
  return interceptedRequestFromProto(resp.request);
1051
1183
  }
1052
1184
  // ──────────────────────────────────────────────────────────────────
@@ -1076,7 +1208,9 @@ export class CloudBrowser {
1076
1208
  * @returns NetworkCapture handle for stopping the capture and inspecting how
1077
1209
  * it ended
1078
1210
  *
1079
- * @throws UNKNOWN_ERROR - the capture could not be started
1211
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1212
+ * a broken connection. This call has no semantic failure of its own, so there
1213
+ * are no error codes to branch on.
1080
1214
  *
1081
1215
  * @example
1082
1216
  * const capture = await browser.captureNetwork(
@@ -1112,29 +1246,34 @@ export class CloudBrowser {
1112
1246
  *
1113
1247
  * @param opts - which requests to capture and whether to keep bodies
1114
1248
  *
1115
- * @throws UNKNOWN_ERROR - the capture could not be started
1249
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1250
+ * a broken connection. This call has no semantic failure of its own, so there
1251
+ * are no error codes to branch on.
1116
1252
  */
1117
1253
  async startNetworkCapture(opts) {
1118
- await this.client.startNetworkCapture(create(StartNetworkCaptureRequestSchema, {
1254
+ const res = await this.client.startNetworkCapture(create(StartNetworkCaptureRequestSchema, {
1119
1255
  sessionId: this.sessionId,
1120
1256
  apiKey: this.apiKey,
1121
1257
  patterns: opts.patterns ?? [],
1122
1258
  bodies: opts.bodies ?? "none",
1123
1259
  bodyPatterns: opts.bodyPatterns ?? [],
1124
1260
  }));
1261
+ throwCommandError("startNetworkCapture", res.error);
1125
1262
  }
1126
1263
  /**
1127
1264
  * Disarms the session's capture.
1128
1265
  *
1129
1266
  * @returns whether a capture was running
1130
1267
  *
1131
- * @throws UNKNOWN_ERROR - the capture could not be stopped
1268
+ * Rejects only on a transport failure. Stopping a capture that is not running
1269
+ * is a no-op rather than a failure, so there are no error codes to branch on.
1132
1270
  */
1133
1271
  async stopNetworkCapture() {
1134
1272
  const resp = await this.client.stopNetworkCapture(create(StopNetworkCaptureRequestSchema, {
1135
1273
  sessionId: this.sessionId,
1136
1274
  apiKey: this.apiKey,
1137
1275
  }));
1276
+ throwCommandError("stopNetworkCapture", resp.error);
1138
1277
  return resp.stopped;
1139
1278
  }
1140
1279
  /**
@@ -1151,7 +1290,8 @@ export class CloudBrowser {
1151
1290
  * @returns NetworkCapture attached to whatever capture is running; onExchange
1152
1291
  * simply never fires when none is
1153
1292
  *
1154
- * @throws UNKNOWN_ERROR - the subscription could not be opened
1293
+ * Rejects only on a transport failure: opening the subscription has no semantic
1294
+ * failure of its own.
1155
1295
  */
1156
1296
  async streamNetworkExchanges(onExchange) {
1157
1297
  return this.subscribeNetworkExchanges(onExchange);
@@ -1233,7 +1373,10 @@ export class CloudBrowser {
1233
1373
  * @param onResync called when the copy had to be rebuilt, after the new tree
1234
1374
  * is in place. Rebuilding is automatic; this is for telling the user why
1235
1375
  * their expanded nodes collapsed.
1236
- * @throws UNKNOWN_ERROR - the mirror could not be started
1376
+ * @throws mirror_failed - the page could not be serialized, usually a document
1377
+ * that went away while the tree was being built. No mirror is left running
1378
+ *
1379
+ * @see {@link CommandError} for reading the code off the rejection
1237
1380
  */
1238
1381
  async mirrorDom(opts, onChange, onResync) {
1239
1382
  const abort = new AbortController();
@@ -1282,6 +1425,11 @@ export class CloudBrowser {
1282
1425
  * Starts (or restarts) the page's mirror and returns the main document,
1283
1426
  * without subscribing to changes. {@link CloudBrowser.mirrorDom} is what you
1284
1427
  * normally want; this is the raw command.
1428
+ *
1429
+ * @throws mirror_failed - the page could not be serialized, usually a document
1430
+ * that went away while the tree was being built. No mirror is left running
1431
+ *
1432
+ * @see {@link CommandError} for reading the code off the rejection
1285
1433
  */
1286
1434
  async startDomMirror(opts = {}) {
1287
1435
  const req = create(StartDomMirrorRequestSchema, {
@@ -1293,6 +1441,7 @@ export class CloudBrowser {
1293
1441
  if (opts.pierce !== undefined)
1294
1442
  req.pierce = opts.pierce;
1295
1443
  const resp = await this.client.startDomMirror(req);
1444
+ throwCommandError("startDomMirror", resp.error);
1296
1445
  return {
1297
1446
  root: resp.root,
1298
1447
  frameId: resp.frameId,
@@ -1301,10 +1450,11 @@ export class CloudBrowser {
1301
1450
  }
1302
1451
  /** Stops the page's mirror, every frame of it. Idempotent. */
1303
1452
  async stopDomMirror() {
1304
- await this.client.stopDomMirror(create(StopDomMirrorRequestSchema, {
1453
+ const res = await this.client.stopDomMirror(create(StopDomMirrorRequestSchema, {
1305
1454
  sessionId: this.sessionId,
1306
1455
  apiKey: this.apiKey,
1307
1456
  }));
1457
+ throwCommandError("stopDomMirror", res.error);
1308
1458
  }
1309
1459
  /**
1310
1460
  * Fetches a node's children and starts reporting changes inside them.
@@ -1312,6 +1462,17 @@ export class CloudBrowser {
1312
1462
  *
1313
1463
  * On an `<iframe>` the one child is the document it hosts, and this call is
1314
1464
  * what starts mirroring that frame.
1465
+ *
1466
+ * An id that is simply unknown is not a failure: the call resolves with an
1467
+ * empty result.
1468
+ *
1469
+ * @throws not_mirrored - the page has no mirror, or frameId is not part of the
1470
+ * one it has; start a mirror first, and after a resync fetch the current tree
1471
+ * before addressing nodes again
1472
+ * @throws mirror_failed - the subtree could not be serialized, usually a
1473
+ * document that went away mid-read
1474
+ *
1475
+ * @see {@link CommandError} for reading the code off the rejection
1315
1476
  */
1316
1477
  async getDomChildren(backendNodeId, frameId = "", depth) {
1317
1478
  const req = create(GetDomChildrenRequestSchema, {
@@ -1324,11 +1485,19 @@ export class CloudBrowser {
1324
1485
  if (depth !== undefined)
1325
1486
  req.depth = depth;
1326
1487
  const resp = await this.client.getDomChildren(req);
1488
+ throwCommandError("getDomChildren", resp.error);
1327
1489
  return { children: resp.children, seq: Number(resp.seq) };
1328
1490
  }
1329
1491
  /**
1330
1492
  * Stops reporting changes inside a node, and inside any frame below it.
1331
1493
  * {@link DomMirror.collapse} calls this.
1494
+ *
1495
+ * @throws not_mirrored - the page has no mirror, or frameId is not part of
1496
+ * the one it has; this is what replaying ids from a tree that has since
1497
+ * been resynced looks like, so fetch the current tree and address the node
1498
+ * again
1499
+ *
1500
+ * @see {@link CommandError} for reading the code off the rejection
1332
1501
  */
1333
1502
  async releaseDomSubtree(backendNodeId, frameId = "") {
1334
1503
  const req = create(ReleaseDomSubtreeRequestSchema, {
@@ -1338,13 +1507,24 @@ export class CloudBrowser {
1338
1507
  });
1339
1508
  if (frameId)
1340
1509
  req.frameId = frameId;
1341
- await this.client.releaseDomSubtree(req);
1510
+ const res = await this.client.releaseDomSubtree(req);
1511
+ throwCommandError("releaseDomSubtree", res.error);
1342
1512
  }
1343
1513
  /**
1344
1514
  * Returns the chain from the main document down to a node, each ancestor
1345
1515
  * with its own children, crossing into frames where it has to and starting
1346
1516
  * the ones it passes through. {@link DomMirror.reveal} calls this and
1347
1517
  * splices it in.
1518
+ *
1519
+ * An id that is simply unknown is not a failure: the call resolves with an
1520
+ * empty result.
1521
+ *
1522
+ * @throws not_mirrored - the page has no mirror, or frameId is not part of the
1523
+ * one it has
1524
+ * @throws mirror_failed - the path could not be serialized, usually a document
1525
+ * that went away mid-read
1526
+ *
1527
+ * @see {@link CommandError} for reading the code off the rejection
1348
1528
  */
1349
1529
  async revealDomNode(backendNodeId, frameId = "") {
1350
1530
  const req = create(RevealDomNodeRequestSchema, {
@@ -1355,6 +1535,7 @@ export class CloudBrowser {
1355
1535
  if (frameId)
1356
1536
  req.frameId = frameId;
1357
1537
  const resp = await this.client.revealDomNode(req);
1538
+ throwCommandError("revealDomNode", resp.error);
1358
1539
  return { path: resp.path, seq: Number(resp.seq) };
1359
1540
  }
1360
1541
  /**
@@ -1366,6 +1547,10 @@ export class CloudBrowser {
1366
1547
  * whole tree just to hash it. The two answer different questions: a hash
1367
1548
  * compares content, a revision only says whether this document moved since
1368
1549
  * you last asked.
1550
+ *
1551
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1552
+ * a broken connection. This call has no semantic failure of its own, so there
1553
+ * are no error codes to branch on.
1369
1554
  */
1370
1555
  async getDomRevision(frameId = "") {
1371
1556
  const req = create(GetDomRevisionRequestSchema, {
@@ -1375,6 +1560,7 @@ export class CloudBrowser {
1375
1560
  if (frameId)
1376
1561
  req.frameId = frameId;
1377
1562
  const resp = await this.client.getDomRevision(req);
1563
+ throwCommandError("getDomRevision", resp.error);
1378
1564
  return Number(resp.revision);
1379
1565
  }
1380
1566
  // ──────────────────────────────────────────────────────────────────
@@ -1385,7 +1571,9 @@ export class CloudBrowser {
1385
1571
  *
1386
1572
  * @returns CookieParam[], one per cookie in the context
1387
1573
  *
1388
- * @throws UNKNOWN_ERROR - the cookies could not be read
1574
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1575
+ * a broken connection. This call has no semantic failure of its own, so there
1576
+ * are no error codes to branch on.
1389
1577
  *
1390
1578
  * @example
1391
1579
  * const cookies = await browser.getCookies();
@@ -1396,6 +1584,7 @@ export class CloudBrowser {
1396
1584
  sessionId: this.sessionId,
1397
1585
  apiKey: this.apiKey,
1398
1586
  }));
1587
+ throwCommandError("getCookies", resp.error);
1399
1588
  return resp.cookies.map(cookieParamFromProto);
1400
1589
  }
1401
1590
  /**
@@ -1406,7 +1595,9 @@ export class CloudBrowser {
1406
1595
  *
1407
1596
  * @param cookies - cookies to write; empty array is a no-op
1408
1597
  *
1409
- * @throws UNKNOWN_ERROR - the cookies could not be written
1598
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1599
+ * a broken connection. This call has no semantic failure of its own, so there
1600
+ * are no error codes to branch on.
1410
1601
  *
1411
1602
  * @example
1412
1603
  * await browser.setCookies([
@@ -1414,25 +1605,29 @@ export class CloudBrowser {
1414
1605
  * ]);
1415
1606
  */
1416
1607
  async setCookies(cookies) {
1417
- await this.client.setCookies(create(SetCookiesRequestSchema, {
1608
+ const res = await this.client.setCookies(create(SetCookiesRequestSchema, {
1418
1609
  sessionId: this.sessionId,
1419
1610
  apiKey: this.apiKey,
1420
1611
  cookies: cookieParamsToProto(cookies),
1421
1612
  }));
1613
+ throwCommandError("setCookies", res.error);
1422
1614
  }
1423
1615
  /**
1424
1616
  * Deletes every cookie in the browser context.
1425
1617
  *
1426
- * @throws UNKNOWN_ERROR - the cookies could not be cleared
1618
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1619
+ * a broken connection. This call has no semantic failure of its own, so there
1620
+ * are no error codes to branch on.
1427
1621
  *
1428
1622
  * @example
1429
1623
  * await browser.clearCookies();
1430
1624
  */
1431
1625
  async clearCookies() {
1432
- await this.client.clearCookies(create(ClearCookiesRequestSchema, {
1626
+ const res = await this.client.clearCookies(create(ClearCookiesRequestSchema, {
1433
1627
  sessionId: this.sessionId,
1434
1628
  apiKey: this.apiKey,
1435
1629
  }));
1630
+ throwCommandError("clearCookies", res.error);
1436
1631
  }
1437
1632
  // ──────────────────────────────────────────────────────────────────
1438
1633
  // Storage (localStorage)
@@ -1450,7 +1645,9 @@ export class CloudBrowser {
1450
1645
  *
1451
1646
  * @returns StorageOriginEntry[], one per origin with localStorage data
1452
1647
  *
1453
- * @throws UNKNOWN_ERROR - the storage could not be read
1648
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1649
+ * a broken connection. This call has no semantic failure of its own, so there
1650
+ * are no error codes to branch on.
1454
1651
  *
1455
1652
  * @example
1456
1653
  * const storage = await browser.getStorage();
@@ -1466,6 +1663,7 @@ export class CloudBrowser {
1466
1663
  if (origin)
1467
1664
  req.origin = origin;
1468
1665
  const resp = await this.client.getStorage(req);
1666
+ throwCommandError("getStorage", resp.error);
1469
1667
  return resp.storage.map(storageEntryFromProto);
1470
1668
  }
1471
1669
  /**
@@ -1479,7 +1677,9 @@ export class CloudBrowser {
1479
1677
  *
1480
1678
  * @param storage - entries to write, grouped by origin
1481
1679
  *
1482
- * @throws UNKNOWN_ERROR - the storage could not be written
1680
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1681
+ * a broken connection. This call has no semantic failure of its own, so there
1682
+ * are no error codes to branch on.
1483
1683
  *
1484
1684
  * @example
1485
1685
  * await browser.setStorage([
@@ -1493,11 +1693,12 @@ export class CloudBrowser {
1493
1693
  * ]);
1494
1694
  */
1495
1695
  async setStorage(storage) {
1496
- await this.client.setStorage(create(SetStorageRequestSchema, {
1696
+ const res = await this.client.setStorage(create(SetStorageRequestSchema, {
1497
1697
  sessionId: this.sessionId,
1498
1698
  apiKey: this.apiKey,
1499
1699
  storage: storageEntriesToProto(storage),
1500
1700
  }));
1701
+ throwCommandError("setStorage", res.error);
1501
1702
  }
1502
1703
  /**
1503
1704
  * Deletes localStorage in the browser context.
@@ -1505,7 +1706,9 @@ export class CloudBrowser {
1505
1706
  * @param origin - if set, only this origin's storage is deleted
1506
1707
  * (e.g. "https://example.com"); omit to delete all origins
1507
1708
  *
1508
- * @throws UNKNOWN_ERROR - the storage could not be cleared
1709
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1710
+ * a broken connection. This call has no semantic failure of its own, so there
1711
+ * are no error codes to branch on.
1509
1712
  *
1510
1713
  * @example
1511
1714
  * // Wipe one origin.
@@ -1521,7 +1724,8 @@ export class CloudBrowser {
1521
1724
  });
1522
1725
  if (origin)
1523
1726
  req.origin = origin;
1524
- await this.client.clearStorage(req);
1727
+ const res = await this.client.clearStorage(req);
1728
+ throwCommandError("clearStorage", res.error);
1525
1729
  }
1526
1730
  // ──────────────────────────────────────────────────────────────────
1527
1731
  // Auth / DBSC (portable signed-in persona)
@@ -1536,7 +1740,9 @@ export class CloudBrowser {
1536
1740
  *
1537
1741
  * @returns AuthSession, or undefined when there is nothing to export
1538
1742
  *
1539
- * @throws UNKNOWN_ERROR - the auth session could not be read
1743
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1744
+ * a broken connection. This call has no semantic failure of its own, so there
1745
+ * are no error codes to branch on.
1540
1746
  *
1541
1747
  * @example
1542
1748
  * const auth = await browser.getAuthSession();
@@ -1547,6 +1753,7 @@ export class CloudBrowser {
1547
1753
  sessionId: this.sessionId,
1548
1754
  apiKey: this.apiKey,
1549
1755
  }));
1756
+ throwCommandError("getAuthSession", resp.error);
1550
1757
  return resp.session ? authSessionFromProto(resp.session) : undefined;
1551
1758
  }
1552
1759
  /**
@@ -1558,18 +1765,21 @@ export class CloudBrowser {
1558
1765
  *
1559
1766
  * @param session - session as returned by getAuthSession()
1560
1767
  *
1561
- * @throws UNKNOWN_ERROR - the auth session could not be written
1768
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1769
+ * a broken connection. This call has no semantic failure of its own, so there
1770
+ * are no error codes to branch on.
1562
1771
  *
1563
1772
  * @example
1564
1773
  * await browser.setAuthSession(saved);
1565
1774
  * await browser.navigate("https://mail.google.com");
1566
1775
  */
1567
1776
  async setAuthSession(session) {
1568
- await this.client.setAuthSession(create(SetAuthSessionRequestSchema, {
1777
+ const res = await this.client.setAuthSession(create(SetAuthSessionRequestSchema, {
1569
1778
  sessionId: this.sessionId,
1570
1779
  apiKey: this.apiKey,
1571
1780
  session: authSessionToProto(session),
1572
1781
  }));
1782
+ throwCommandError("setAuthSession", res.error);
1573
1783
  }
1574
1784
  // ──────────────────────────────────────────────────────────────────
1575
1785
  // Devtools / live-UI helpers
@@ -1589,7 +1799,9 @@ export class CloudBrowser {
1589
1799
  * @returns InspectResult with the resolved backendNodeId, frameId, tag
1590
1800
  * name, trimmed textContent, visibility and bounds
1591
1801
  *
1592
- * @throws UNKNOWN_ERROR - the hit-test failed
1802
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1803
+ * a broken connection. This call has no semantic failure of its own, so there
1804
+ * are no error codes to branch on.
1593
1805
  *
1594
1806
  * @example
1595
1807
  * const r = await browser.inspectAtPosition(200, 300);
@@ -1602,6 +1814,7 @@ export class CloudBrowser {
1602
1814
  x,
1603
1815
  y,
1604
1816
  }));
1817
+ throwCommandError("inspectAtPosition", resp.error);
1605
1818
  return {
1606
1819
  backendNodeId: resp.backendNodeId,
1607
1820
  frameId: resp.frameId,
@@ -1621,7 +1834,9 @@ export class CloudBrowser {
1621
1834
  * @param frameId - id of the frame the node lives in; empty string
1622
1835
  * targets the main frame
1623
1836
  *
1624
- * @throws UNKNOWN_ERROR - the highlight could not be applied
1837
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1838
+ * a broken connection. This call has no semantic failure of its own, so there
1839
+ * are no error codes to branch on.
1625
1840
  *
1626
1841
  * @example
1627
1842
  * await browser.highlightNode(res.backendNodeId, res.frameId);
@@ -1634,7 +1849,8 @@ export class CloudBrowser {
1634
1849
  });
1635
1850
  if (frameId)
1636
1851
  req.frameId = frameId;
1637
- await this.client.highlightNode(req);
1852
+ const res = await this.client.highlightNode(req);
1853
+ throwCommandError("highlight", res.error);
1638
1854
  }
1639
1855
  /**
1640
1856
  * Pastes text at the current caret using IME-style input.
@@ -1646,17 +1862,22 @@ export class CloudBrowser {
1646
1862
  *
1647
1863
  * @param text - the text to insert at the caret
1648
1864
  *
1649
- * @throws UNKNOWN_ERROR - the text could not be inserted
1865
+ * @throws no_focus - nothing in the page holds focus, so there is no caret to
1866
+ * insert at; click the field first
1867
+ * @throws busy - another action is already running on this page
1868
+ *
1869
+ * @see {@link CommandError} for reading the code off the rejection
1650
1870
  *
1651
1871
  * @example
1652
1872
  * await browser.insertText("hello world");
1653
1873
  */
1654
1874
  async insertText(text) {
1655
- await this.client.insertText(create(InsertTextRequestSchema, {
1875
+ const res = await this.client.insertText(create(InsertTextRequestSchema, {
1656
1876
  sessionId: this.sessionId,
1657
1877
  apiKey: this.apiKey,
1658
1878
  text,
1659
1879
  }));
1880
+ throwCommandError("insertText", res.error);
1660
1881
  }
1661
1882
  /**
1662
1883
  * Types `text` into the currently focused element as a per-key stream of real
@@ -1678,7 +1899,10 @@ export class CloudBrowser {
1678
1899
  * @param opts - optional: `clearFirst` clears the focused field (Ctrl+A,
1679
1900
  * Delete) before typing
1680
1901
  *
1681
- * @throws UNKNOWN_ERROR - the page/context was torn down mid-stream
1902
+ * `type` has no semantic failure of its own: the keys land wherever focus
1903
+ * happens to be, so there is no target it can miss. Only the page or context
1904
+ * being torn down mid-stream surfaces, and that is a transport failure rather
1905
+ * than a code.
1682
1906
  *
1683
1907
  * @example
1684
1908
  * // OTP field that auto-advances across boxes.
@@ -1693,7 +1917,7 @@ export class CloudBrowser {
1693
1917
  });
1694
1918
  if (opts?.clearFirst)
1695
1919
  req.clearFirst = true;
1696
- await this.client.type(req);
1920
+ throwCommandError("type", (await this.client.type(req)).error);
1697
1921
  }
1698
1922
  /**
1699
1923
  * Fires a single key-down event.
@@ -1708,7 +1932,11 @@ export class CloudBrowser {
1708
1932
  * `modifiers` (bit-flag: Alt=1, Ctrl=2, Meta=4, Shift=8),
1709
1933
  * `location` (0=standard, 1=left, 2=right, 3=numpad)
1710
1934
  *
1711
- * @throws UNKNOWN_ERROR - the event could not be dispatched
1935
+ * @throws no_focus - nothing in the page holds focus, so the key has nowhere
1936
+ * to go; click the field first
1937
+ * @throws busy - another action is already running on this page
1938
+ *
1939
+ * @see {@link CommandError} for reading the code off the rejection
1712
1940
  *
1713
1941
  * @example
1714
1942
  * // Ctrl+A
@@ -1727,7 +1955,8 @@ export class CloudBrowser {
1727
1955
  req.modifiers = opts.modifiers;
1728
1956
  if (opts?.location !== undefined)
1729
1957
  req.location = opts.location;
1730
- await this.client.pressKey(req);
1958
+ const res = await this.client.pressKey(req);
1959
+ throwCommandError("pressKey", res.error);
1731
1960
  }
1732
1961
  /**
1733
1962
  * Fires a single key-up event.
@@ -1753,7 +1982,8 @@ export class CloudBrowser {
1753
1982
  req.modifiers = opts.modifiers;
1754
1983
  if (opts?.location !== undefined)
1755
1984
  req.location = opts.location;
1756
- await this.client.releaseKey(req);
1985
+ const res = await this.client.releaseKey(req);
1986
+ throwCommandError("releaseKey", res.error);
1757
1987
  }
1758
1988
  /**
1759
1989
  * Returns the current text selection.
@@ -1764,7 +1994,9 @@ export class CloudBrowser {
1764
1994
  *
1765
1995
  * @returns the selected text, or `""` when nothing is selected
1766
1996
  *
1767
- * @throws UNKNOWN_ERROR - the selection could not be read
1997
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1998
+ * a broken connection. This call has no semantic failure of its own, so there
1999
+ * are no error codes to branch on.
1768
2000
  *
1769
2001
  * @example
1770
2002
  * const sel = await browser.getSelection();
@@ -1775,6 +2007,7 @@ export class CloudBrowser {
1775
2007
  sessionId: this.sessionId,
1776
2008
  apiKey: this.apiKey,
1777
2009
  }));
2010
+ throwCommandError("getSelection", resp.error);
1778
2011
  return resp.text;
1779
2012
  }
1780
2013
  // ──────────────────────────────────────────────────────────────────
@@ -1795,8 +2028,10 @@ export class CloudBrowser {
1795
2028
  *
1796
2029
  * @returns empty string on success — the solution is applied server-side
1797
2030
  *
1798
- * @throws UNKNOWN_ERROR - no captcha appeared within timeoutMs, or the
1799
- * detected captcha could not be solved within retryAmount attempts
2031
+ * Rejects when no captcha appeared within imeoutMs, or when the one that did
2032
+ * could not be solved within
2033
+ etryAmount attempts. Neither carries a code:
2034
+ * solving runs outside the page, so there is no per-command code set here.
1800
2035
  *
1801
2036
  * @example
1802
2037
  * await browser.solveCaptcha({ retryAmount: 2 });
@@ -1824,7 +2059,8 @@ export class CloudBrowser {
1824
2059
  *
1825
2060
  * @returns the ICE servers for the client `RTCPeerConnection`
1826
2061
  *
1827
- * @throws UNKNOWN_ERROR - TURN is not configured on the server
2062
+ * Rejects when TURN is not configured on the server. That is a deployment
2063
+ * condition rather than a per-call outcome, so it carries no code.
1828
2064
  *
1829
2065
  * @example
1830
2066
  * const ice = await browser.getStreamConfig();
@@ -1851,8 +2087,16 @@ export class CloudBrowser {
1851
2087
  * @returns the SDP answer to apply as the remote description, plus the
1852
2088
  * viewport to map input coordinates into
1853
2089
  *
1854
- * @throws UNKNOWN_ERROR - the offer was empty, TURN is unconfigured, or the
1855
- * browser could not negotiate the stream
2090
+ * @throws already_active - a stream is already running on this session; stop it
2091
+ * before starting another
2092
+ * @throws negotiation_failed - the browser could not agree on a connection. The
2093
+ * message carries the negotiator's own diagnostic, which is usually where the
2094
+ * actual cause is
2095
+ *
2096
+ * An empty offer or an unconfigured TURN setup is a caller mistake rather than
2097
+ * an outcome, and rejects separately.
2098
+ *
2099
+ * @see {@link CommandError} for reading the code off the rejection
1856
2100
  *
1857
2101
  * @example
1858
2102
  * const { answerSdp, viewport } = await browser.startStream(offer.sdp);
@@ -1864,6 +2108,7 @@ export class CloudBrowser {
1864
2108
  apiKey: this.apiKey,
1865
2109
  offerSdp,
1866
2110
  }));
2111
+ throwCommandError("startStream", resp.error);
1867
2112
  return {
1868
2113
  answerSdp: resp.answerSdp,
1869
2114
  viewport: resp.viewport
@@ -1875,25 +2120,27 @@ export class CloudBrowser {
1875
2120
  * Tears down the live video stream for the session's page. Safe to call even
1876
2121
  * if no stream is running.
1877
2122
  *
1878
- * @throws UNKNOWN_ERROR - the stream could not be stopped
2123
+ * Rejects only on a transport failure. Stopping a stream that is not running is
2124
+ * a no-op rather than a failure, so there are no error codes to branch on.
1879
2125
  *
1880
2126
  * @example
1881
2127
  * await browser.stopStream();
1882
2128
  */
1883
2129
  async stopStream() {
1884
- await this.client.stopStream(create(StopStreamRequestSchema, {
2130
+ const res = await this.client.stopStream(create(StopStreamRequestSchema, {
1885
2131
  sessionId: this.sessionId,
1886
2132
  apiKey: this.apiKey,
1887
2133
  }));
2134
+ throwCommandError("stopStream", res.error);
1888
2135
  }
1889
2136
  // ──────────────────────────────────────────────────────────────────
1890
2137
  // Reactions
1891
2138
  // ──────────────────────────────────────────────────────────────────
1892
2139
  /**
1893
- * Registers a one-shot "reaction": a background poller (one shared loop per
1894
- * page) watches for the `match` locator and, as soon as it matches, clicks it
1895
- * with the full smart-click machinery (scroll, human path, occlusion gate,
1896
- * evade) — then removes itself. The poller yields to any in-flight input
2140
+ * Registers a one-shot "reaction": the browser watches for the `match`
2141
+ * locator in the background and, as soon as it matches, clicks it the same
2142
+ * way {@link CloudBrowser.click} does (scroll, human path, occlusion check) —
2143
+ * then removes itself. The reaction yields to any in-flight input
1897
2144
  * action and only fires while the pointer is idle, so a reaction naturally
1898
2145
  * slots into the gaps of a retrying foreground action (e.g. it dismisses a
1899
2146
  * newsletter modal blocking a {@link CloudBrowser.click}, after which the
@@ -1951,6 +2198,7 @@ export class CloudBrowser {
1951
2198
  if (opts?.intervalMs)
1952
2199
  req.interval = opts.intervalMs;
1953
2200
  const resp = await this.client.addReaction(req);
2201
+ throwCommandError("addReaction", resp.error);
1954
2202
  return resp.reactionId;
1955
2203
  }
1956
2204
  /**
@@ -1961,6 +2209,10 @@ export class CloudBrowser {
1961
2209
  *
1962
2210
  * @returns true if a pending reaction with this id existed and was removed
1963
2211
  *
2212
+ * Removing an id that is not registered is a no-op rather than an error, so the
2213
+ * returned boolean - not a rejection - is what tells you whether anything was
2214
+ * there. Rejects only on a transport failure.
2215
+ *
1964
2216
  * @example
1965
2217
  * const removed = await browser.removeReaction(id);
1966
2218
  */
@@ -1970,6 +2222,7 @@ export class CloudBrowser {
1970
2222
  apiKey: this.apiKey,
1971
2223
  reactionId,
1972
2224
  }));
2225
+ throwCommandError("removeReaction", resp.error);
1973
2226
  return resp.removed;
1974
2227
  }
1975
2228
  /**
@@ -1978,6 +2231,10 @@ export class CloudBrowser {
1978
2231
  *
1979
2232
  * @returns the pending reactions for the page
1980
2233
  *
2234
+ * Rejects only on a transport failure - a dead session, a page that is gone,
2235
+ * a broken connection. This call has no semantic failure of its own, so there
2236
+ * are no error codes to branch on.
2237
+ *
1981
2238
  * @example
1982
2239
  * const pending = await browser.listReactions();
1983
2240
  * for (const r of pending) console.log(r.reactionId, r.matchSelector);
@@ -1987,6 +2244,7 @@ export class CloudBrowser {
1987
2244
  sessionId: this.sessionId,
1988
2245
  apiKey: this.apiKey,
1989
2246
  }));
2247
+ throwCommandError("listReactions", resp.error);
1990
2248
  return resp.reactions.map((r) => ({
1991
2249
  reactionId: r.reactionId,
1992
2250
  matchSelector: r.matchSelector ?? "",
@@ -2023,7 +2281,10 @@ export class CloudBrowser {
2023
2281
  * @returns the return value and the script's whole console output. A script
2024
2282
  * that threw is reported as `success: false`, not as a rejection
2025
2283
  *
2026
- * @throws UNKNOWN_ERROR - the script could not be delivered to the browser
2284
+ * Rejects only on a transport failure. A script that fails to compile or throws
2285
+ * is not a rejection: the returned result has success: false and
2286
+ esult holds
2287
+ * the message, so a broken script stays distinguishable from a broken connection.
2027
2288
  *
2028
2289
  * @example
2029
2290
  * const result = await browser.runScript(`
@@ -2071,7 +2332,8 @@ export class CloudBrowser {
2071
2332
  *
2072
2333
  * @returns ScriptRun handle for awaiting or cancelling the run
2073
2334
  *
2074
- * @throws UNKNOWN_ERROR - the run could not be started
2335
+ * Rejects only on a transport failure: a script that fails to compile or throws
2336
+ * surfaces on the run itself rather than here.
2075
2337
  *
2076
2338
  * @example
2077
2339
  * const run = await browser.startScript(source, (ev) => {
@@ -2156,7 +2418,8 @@ export class CloudBrowser {
2156
2418
  *
2157
2419
  * @returns ScriptFollow handle for stopping the subscription
2158
2420
  *
2159
- * @throws UNKNOWN_ERROR - the subscription could not be opened
2421
+ * Rejects only on a transport failure: opening the subscription has no semantic
2422
+ * failure of its own.
2160
2423
  *
2161
2424
  * @example
2162
2425
  * const follow = await browser.followScript(runId, (ev) => {
@@ -2189,7 +2452,9 @@ export class CloudBrowser {
2189
2452
  * @returns how many runs were cancelled; 0 when the id named nothing in
2190
2453
  * flight
2191
2454
  *
2192
- * @throws UNKNOWN_ERROR - the cancel could not be delivered
2455
+ * Rejects only on a transport failure. Cancelling runs that have already
2456
+ * finished, or none at all, is a no-op - read the returned count to learn how
2457
+ * many were actually stopped.
2193
2458
  *
2194
2459
  * @example
2195
2460
  * await browser.stopScripts(""); // abandon everything running
@@ -2212,7 +2477,9 @@ export class CloudBrowser {
2212
2477
  *
2213
2478
  * @returns one entry per run still executing
2214
2479
  *
2215
- * @throws UNKNOWN_ERROR - the session could not be queried
2480
+ * Rejects only on a transport failure - a dead session, a broken connection.
2481
+ * This call has no semantic failure of its own, so there are no error codes to
2482
+ * branch on.
2216
2483
  *
2217
2484
  * @example
2218
2485
  * for (const run of await browser.listScriptRuns()) {