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