browserclaw 0.10.0 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -55
- package/dist/index.cjs +113 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -2
- package/dist/index.d.ts +66 -2
- package/dist/index.js +113 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<h2 align="center">🦞 BrowserClaw — Standalone OpenClaw browser module</h2>
|
|
1
|
+
<h2 align="center">🦞 BrowserClaw™ — Standalone OpenClaw browser module</h2>
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
4
|
<a href="https://browserclaw.org"><img src="https://img.shields.io/badge/Live-browserclaw.org-orange" alt="Live" /></a>
|
|
@@ -23,7 +23,7 @@ const { snapshot, refs } = await page.snapshot();
|
|
|
23
23
|
// snapshot: AI-readable text tree
|
|
24
24
|
// refs: { "e1": { role: "link", name: "More info" }, "e2": { role: "button", name: "Submit" } }
|
|
25
25
|
|
|
26
|
-
await page.click('e1');
|
|
26
|
+
await page.click('e1'); // Click by ref
|
|
27
27
|
await page.type('e3', 'hello'); // Type by ref
|
|
28
28
|
await browser.stop();
|
|
29
29
|
```
|
|
@@ -37,6 +37,7 @@ Most browser automation tools were built for humans writing test scripts. AI age
|
|
|
37
37
|
- **browserclaw** gives the AI a **text snapshot** with numbered refs — the AI reads text (what it's best at) and returns a ref ID (deterministic targeting)
|
|
38
38
|
|
|
39
39
|
The snapshot + ref pattern means:
|
|
40
|
+
|
|
40
41
|
1. **Deterministic** — refs resolve to exact elements via Playwright locators, no guessing
|
|
41
42
|
2. **Fast** — text snapshots are tiny compared to screenshots
|
|
42
43
|
3. **Cheap** — no vision API calls, just text in/text out
|
|
@@ -46,15 +47,15 @@ The snapshot + ref pattern means:
|
|
|
46
47
|
|
|
47
48
|
The AI browser automation space is moving fast. Here's how browserclaw compares to the major alternatives.
|
|
48
49
|
|
|
49
|
-
|
|
|
50
|
-
|
|
51
|
-
| Ref → exact element, no guessing
|
|
52
|
-
| No vision model in the loop
|
|
53
|
-
| Survives redesigns (semantic, not pixel) |
|
|
54
|
-
| Fill 10 form fields in one call
|
|
55
|
-
| Interact with cross-origin iframes
|
|
56
|
-
| Playwright engine (auto-wait, locators)
|
|
57
|
-
| Embeddable in your own JS/TS agent loop
|
|
50
|
+
| | [browserclaw](https://github.com/idan-rubin/browserclaw) | [browser-use](https://github.com/browser-use/browser-use) | [Stagehand](https://github.com/browserbase/stagehand) | [Playwright MCP](https://github.com/microsoft/playwright-mcp) |
|
|
51
|
+
| :--------------------------------------- | :------------------------------------------------------: | :-------------------------------------------------------: | :---------------------------------------------------: | :-----------------------------------------------------------: |
|
|
52
|
+
| Ref → exact element, no guessing | :white_check_mark: | :heavy_minus_sign: | :x: | :white_check_mark: |
|
|
53
|
+
| No vision model in the loop | :white_check_mark: | :heavy_minus_sign: | :white_check_mark: | :white_check_mark: |
|
|
54
|
+
| Survives redesigns (semantic, not pixel) | :white_check_mark: | :heavy_minus_sign: | :white_check_mark: | :white_check_mark: |
|
|
55
|
+
| Fill 10 form fields in one call | :white_check_mark: | :x: | :x: | :x: |
|
|
56
|
+
| Interact with cross-origin iframes | :white_check_mark: | :white_check_mark: | :x: | :x: |
|
|
57
|
+
| Playwright engine (auto-wait, locators) | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: |
|
|
58
|
+
| Embeddable in your own JS/TS agent loop | :white_check_mark: | :x: | :heavy_minus_sign: | :x: |
|
|
58
59
|
|
|
59
60
|
:white_check_mark: = Yes  :heavy_minus_sign: = Partial  :x: = No
|
|
60
61
|
|
|
@@ -132,19 +133,22 @@ Requires a Chromium-based browser installed on the system (Chrome, Brave, Edge,
|
|
|
132
133
|
```typescript
|
|
133
134
|
// Launch a new Chrome instance (auto-detects Chrome/Brave/Edge/Chromium)
|
|
134
135
|
const browser = await BrowserClaw.launch({
|
|
135
|
-
headless: false,
|
|
136
|
+
headless: false, // default: false (visible window)
|
|
136
137
|
executablePath: '...', // optional: specific browser path
|
|
137
|
-
cdpPort: 9222,
|
|
138
|
-
noSandbox: false,
|
|
139
|
-
|
|
138
|
+
cdpPort: 9222, // default: 9222
|
|
139
|
+
noSandbox: false, // default: false (set true for Docker/CI)
|
|
140
|
+
ignoreHTTPSErrors: false, // default: false (set true for expired local dev certs)
|
|
141
|
+
userDataDir: '...', // optional: custom user data directory
|
|
140
142
|
profileName: 'browserclaw', // profile name in Chrome title bar
|
|
141
|
-
profileColor: '#FF4500',
|
|
143
|
+
profileColor: '#FF4500', // profile accent color (hex)
|
|
142
144
|
chromeArgs: ['--start-maximized'], // additional Chrome flags
|
|
143
145
|
});
|
|
144
146
|
|
|
145
|
-
//
|
|
146
|
-
// (started with: chrome --remote-debugging-port=9222)
|
|
147
|
+
// Connect to an already-running Chrome instance
|
|
147
148
|
const browser = await BrowserClaw.connect('http://localhost:9222');
|
|
149
|
+
|
|
150
|
+
// Auto-discovery: scans common CDP ports (9222-9226, 9229)
|
|
151
|
+
const browser = await BrowserClaw.connect();
|
|
148
152
|
```
|
|
149
153
|
|
|
150
154
|
`connect()` checks that Chrome is reachable, then the internal CDP connection retries 3 times with increasing timeouts (5 s, 7 s, 9 s) — safe for Docker/CI where Chrome starts slowly.
|
|
@@ -156,16 +160,30 @@ const browser = await BrowserClaw.connect('http://localhost:9222');
|
|
|
156
160
|
```typescript
|
|
157
161
|
const page = await browser.open('https://example.com');
|
|
158
162
|
const current = await browser.currentPage(); // get active tab
|
|
159
|
-
const tabs = await browser.tabs();
|
|
163
|
+
const tabs = await browser.tabs(); // list all tabs
|
|
160
164
|
const handle = browser.page(tabs[0].targetId); // wrap existing tab
|
|
161
|
-
await browser.
|
|
162
|
-
await browser.
|
|
163
|
-
await browser.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
await page.
|
|
168
|
-
|
|
165
|
+
const appPage = await browser.waitForTab({ urlContains: 'app-web' });
|
|
166
|
+
await browser.focus(tabId); // bring tab to front
|
|
167
|
+
await browser.close(tabId); // close a tab
|
|
168
|
+
await browser.stop(); // stop browser + cleanup
|
|
169
|
+
|
|
170
|
+
page.id; // CDP target ID (use with focus/close/page)
|
|
171
|
+
await page.url(); // current page URL
|
|
172
|
+
await page.title(); // current page title
|
|
173
|
+
browser.url; // CDP endpoint URL
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Every tab returns a `targetId` — this is the handle you use everywhere:
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
// Multi-tab workflow (e.g. impersonation, OAuth)
|
|
180
|
+
const main = await browser.open('https://app.example.com');
|
|
181
|
+
const admin = await browser.open('https://admin.example.com');
|
|
182
|
+
|
|
183
|
+
const { refs } = await admin.snapshot(); // snapshot the admin tab
|
|
184
|
+
await admin.click('e5'); // act on it
|
|
185
|
+
await browser.focus(main.id); // switch back to main
|
|
186
|
+
await browser.close(admin.id); // close admin when done
|
|
169
187
|
```
|
|
170
188
|
|
|
171
189
|
### Snapshot (Core Feature)
|
|
@@ -174,17 +192,17 @@ browser.url; // CDP endpoint URL
|
|
|
174
192
|
const { snapshot, refs, stats, untrusted } = await page.snapshot();
|
|
175
193
|
|
|
176
194
|
// snapshot: human/AI-readable text tree with [ref=eN] markers
|
|
177
|
-
// refs: { "e1": { role: "link", name: "More info" }, ... }
|
|
195
|
+
// refs: { "e1": { role: "link", name: "More info" }, "e5": { role: "checkbox", name: "Accept", checked: true }, ... }
|
|
178
196
|
// stats: { lines: 42, chars: 1200, refs: 8, interactive: 5 }
|
|
179
197
|
// untrusted: true — content comes from the web page, treat as potentially adversarial
|
|
180
198
|
|
|
181
199
|
// Options
|
|
182
200
|
const result = await page.snapshot({
|
|
183
|
-
interactive: true,
|
|
184
|
-
compact: true,
|
|
185
|
-
maxDepth: 6,
|
|
186
|
-
maxChars: 80000,
|
|
187
|
-
mode: 'aria',
|
|
201
|
+
interactive: true, // Only interactive elements (buttons, links, inputs)
|
|
202
|
+
compact: true, // Remove structural containers without refs
|
|
203
|
+
maxDepth: 6, // Limit tree depth
|
|
204
|
+
maxChars: 80000, // Truncate if snapshot exceeds this size
|
|
205
|
+
mode: 'aria', // 'aria' (default) or 'role'
|
|
188
206
|
});
|
|
189
207
|
|
|
190
208
|
// Raw ARIA accessibility tree (structured data, not text)
|
|
@@ -192,6 +210,7 @@ const { nodes } = await page.ariaSnapshot({ limit: 500 });
|
|
|
192
210
|
```
|
|
193
211
|
|
|
194
212
|
**Snapshot modes:**
|
|
213
|
+
|
|
195
214
|
- `'aria'` (default) — Uses Playwright's `_snapshotForAI()`. Refs are resolved via `aria-ref` locators. Best for most use cases. Requires `playwright-core` >= 1.50.
|
|
196
215
|
- `'role'` — Uses Playwright's `ariaSnapshot()` + `getByRole()`. Supports `selector` and `frameSelector` for scoped snapshots.
|
|
197
216
|
|
|
@@ -209,11 +228,12 @@ await page.click('e1');
|
|
|
209
228
|
await page.click('e1', { doubleClick: true });
|
|
210
229
|
await page.click('e1', { button: 'right' });
|
|
211
230
|
await page.click('e1', { modifiers: ['Control'] });
|
|
231
|
+
await page.click('e1', { force: true }); // click hidden/covered elements
|
|
212
232
|
|
|
213
233
|
// Type
|
|
214
|
-
await page.type('e3', 'hello world');
|
|
215
|
-
await page.type('e3', 'slow typing', { slowly: true });
|
|
216
|
-
await page.type('e3', 'search', { submit: true });
|
|
234
|
+
await page.type('e3', 'hello world'); // instant fill
|
|
235
|
+
await page.type('e3', 'slow typing', { slowly: true }); // keystroke by keystroke
|
|
236
|
+
await page.type('e3', 'search', { submit: true }); // type + press Enter
|
|
217
237
|
|
|
218
238
|
// Other interactions
|
|
219
239
|
await page.hover('e2');
|
|
@@ -234,7 +254,31 @@ await page.fill([
|
|
|
234
254
|
]);
|
|
235
255
|
```
|
|
236
256
|
|
|
237
|
-
`fill()` field types: `'text'` (default) calls Playwright `fill()` with the string value. `'checkbox'` and `'radio'` call `setChecked()`
|
|
257
|
+
`fill()` field types: `'text'` (default) calls Playwright `fill()` with the string value. `'checkbox'` and `'radio'` call `setChecked()` with `force: true` (works on hidden inputs behind custom styling). Truthy values are `true`, `1`, `'1'`, `'true'`. Type can be omitted and defaults to `'text'`. Empty ref throws.
|
|
258
|
+
|
|
259
|
+
#### No-snapshot actions
|
|
260
|
+
|
|
261
|
+
These methods find and click elements without needing a snapshot first — useful when you know the text or role but don't want the snapshot+ref round-trip.
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
// Click by visible text or title attribute
|
|
265
|
+
await page.clickByText('Submit');
|
|
266
|
+
await page.clickByText('Save Changes', { exact: true });
|
|
267
|
+
|
|
268
|
+
// Click by ARIA role and accessible name
|
|
269
|
+
await page.clickByRole('button', 'Save');
|
|
270
|
+
await page.clickByRole('link', 'Settings');
|
|
271
|
+
await page.clickByRole('button', 'Create', { index: 1 }); // second match
|
|
272
|
+
|
|
273
|
+
// Click by CSS selector
|
|
274
|
+
await page.clickBySelector('#submit-btn');
|
|
275
|
+
|
|
276
|
+
// Click at page coordinates (for canvas elements, custom widgets)
|
|
277
|
+
await page.mouseClick(400, 300);
|
|
278
|
+
|
|
279
|
+
// Press and hold at coordinates (raw CDP events, bypasses automation detection)
|
|
280
|
+
await page.pressAndHold(400, 300, { holdMs: 5000, delay: 150 });
|
|
281
|
+
```
|
|
238
282
|
|
|
239
283
|
#### Highlight
|
|
240
284
|
|
|
@@ -256,7 +300,7 @@ await uploadDone;
|
|
|
256
300
|
|
|
257
301
|
#### Dialog Handling
|
|
258
302
|
|
|
259
|
-
Handle JavaScript dialogs (alert, confirm, prompt). Arm the handler
|
|
303
|
+
Handle JavaScript dialogs (alert, confirm, prompt). Arm the handler _before_ the action that triggers the dialog.
|
|
260
304
|
|
|
261
305
|
```typescript
|
|
262
306
|
const dialogDone = page.armDialog({ accept: true });
|
|
@@ -267,22 +311,33 @@ await dialogDone;
|
|
|
267
311
|
const promptDone = page.armDialog({ accept: true, promptText: 'my answer' });
|
|
268
312
|
await page.click('e6'); // triggers prompt()
|
|
269
313
|
await promptDone;
|
|
314
|
+
|
|
315
|
+
// Persistent handler: called for every dialog until cleared
|
|
316
|
+
await page.onDialog((event) => {
|
|
317
|
+
console.log(`${event.type}: ${event.message}`);
|
|
318
|
+
event.accept(); // or event.dismiss()
|
|
319
|
+
});
|
|
320
|
+
await page.onDialog(undefined); // clear the handler
|
|
270
321
|
```
|
|
271
322
|
|
|
323
|
+
By default, unexpected dialogs are auto-dismissed to prevent `ProtocolError` crashes.
|
|
324
|
+
|
|
272
325
|
### Navigation & Waiting
|
|
273
326
|
|
|
274
327
|
```typescript
|
|
275
328
|
await page.goto('https://example.com');
|
|
276
|
-
await page.reload();
|
|
277
|
-
await page.goBack();
|
|
278
|
-
await page.goForward();
|
|
329
|
+
await page.reload(); // reload the current page
|
|
330
|
+
await page.goBack(); // navigate back in history
|
|
331
|
+
await page.goForward(); // navigate forward in history
|
|
279
332
|
await page.waitFor({ loadState: 'networkidle' });
|
|
280
333
|
await page.waitFor({ text: 'Welcome' });
|
|
281
334
|
await page.waitFor({ textGone: 'Loading...' });
|
|
282
335
|
await page.waitFor({ url: '**/dashboard' });
|
|
283
|
-
await page.waitFor({ selector: '.loaded' });
|
|
284
|
-
await page.waitFor({ fn: '() => document.readyState === "complete"' }); // custom JS
|
|
285
|
-
await page.waitFor({
|
|
336
|
+
await page.waitFor({ selector: '.loaded' }); // wait for CSS selector
|
|
337
|
+
await page.waitFor({ fn: '() => document.readyState === "complete"' }); // custom JS (string)
|
|
338
|
+
await page.waitFor({ fn: () => document.title === 'Done' }); // custom JS (function)
|
|
339
|
+
await page.waitFor({ fn: (name) => document.querySelector('button')?.textContent === name, arg: 'Save' }); // with arg
|
|
340
|
+
await page.waitFor({ timeMs: 1000 }); // sleep
|
|
286
341
|
await page.waitFor({ text: 'Ready', timeoutMs: 5000 }); // custom timeout
|
|
287
342
|
```
|
|
288
343
|
|
|
@@ -290,14 +345,14 @@ await page.waitFor({ text: 'Ready', timeoutMs: 5000 }); // custom timeout
|
|
|
290
345
|
|
|
291
346
|
```typescript
|
|
292
347
|
// Screenshots
|
|
293
|
-
const screenshot = await page.screenshot();
|
|
294
|
-
const fullPage = await page.screenshot({ fullPage: true });
|
|
295
|
-
const element = await page.screenshot({ ref: 'e1' });
|
|
348
|
+
const screenshot = await page.screenshot(); // viewport PNG → Buffer
|
|
349
|
+
const fullPage = await page.screenshot({ fullPage: true }); // full scrollable page
|
|
350
|
+
const element = await page.screenshot({ ref: 'e1' }); // specific element by ref
|
|
296
351
|
const bySelector = await page.screenshot({ element: '.hero' }); // by CSS selector
|
|
297
|
-
const jpeg = await page.screenshot({ type: 'jpeg' });
|
|
352
|
+
const jpeg = await page.screenshot({ type: 'jpeg' }); // JPEG format
|
|
298
353
|
|
|
299
354
|
// PDF
|
|
300
|
-
const pdf = await page.pdf();
|
|
355
|
+
const pdf = await page.pdf(); // PDF export (headless only)
|
|
301
356
|
|
|
302
357
|
// Labeled screenshot — numbered badges on each ref for visual debugging
|
|
303
358
|
const { buffer, labels, skipped } = await page.screenshotWithLabels(['e1', 'e2', 'e3']);
|
|
@@ -331,17 +386,33 @@ console.log(resp.status, resp.body);
|
|
|
331
386
|
|
|
332
387
|
Options: `timeoutMs` (default 30 s), `maxChars` (truncate body).
|
|
333
388
|
|
|
389
|
+
#### Wait For Request
|
|
390
|
+
|
|
391
|
+
Wait for a network request matching a URL pattern and get full request + response details, including POST body.
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
const reqPromise = page.waitForRequest('/api/submit', { method: 'POST' });
|
|
395
|
+
await page.click('e5'); // submit a form
|
|
396
|
+
const req = await reqPromise;
|
|
397
|
+
console.log(req.method, req.postData); // 'POST', '{"name":"Jane"}'
|
|
398
|
+
console.log(req.status, req.ok); // 200, true
|
|
399
|
+
console.log(req.responseBody); // '{"id":123}'
|
|
400
|
+
// { url, method, postData?, status, ok, responseBody?, truncated? }
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
Options: `method` (filter by HTTP method), `timeoutMs` (default 30 s), `maxChars` (truncate response body).
|
|
404
|
+
|
|
334
405
|
### Activity Monitoring
|
|
335
406
|
|
|
336
407
|
Console messages, errors, and network requests are buffered automatically.
|
|
337
408
|
|
|
338
409
|
```typescript
|
|
339
|
-
const logs = await page.consoleLogs();
|
|
340
|
-
const errors = await page.consoleLogs({ level: 'error' });
|
|
341
|
-
const recent = await page.consoleLogs({ clear: true });
|
|
342
|
-
const pageErrors = await page.pageErrors();
|
|
343
|
-
const requests = await page.networkRequests({ filter: '/api' });
|
|
344
|
-
const fresh = await page.networkRequests({ clear: true });
|
|
410
|
+
const logs = await page.consoleLogs(); // all messages
|
|
411
|
+
const errors = await page.consoleLogs({ level: 'error' }); // errors only
|
|
412
|
+
const recent = await page.consoleLogs({ clear: true }); // read and clear buffer
|
|
413
|
+
const pageErrors = await page.pageErrors(); // uncaught exceptions
|
|
414
|
+
const requests = await page.networkRequests({ filter: '/api' }); // filter by URL
|
|
415
|
+
const fresh = await page.networkRequests({ clear: true }); // read and clear buffer
|
|
345
416
|
```
|
|
346
417
|
|
|
347
418
|
### Storage
|
package/dist/index.cjs
CHANGED
|
@@ -3118,7 +3118,7 @@ async function pressAndHoldViaCdp(opts) {
|
|
|
3118
3118
|
async function clickByTextViaPlaywright(opts) {
|
|
3119
3119
|
const page = await getRestoredPageForTarget(opts);
|
|
3120
3120
|
const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
|
|
3121
|
-
const locator = page.getByText(opts.text, { exact: opts.exact }).or(page.getByTitle(opts.text, { exact: opts.exact })).first();
|
|
3121
|
+
const locator = page.getByText(opts.text, { exact: opts.exact }).and(page.locator(":visible")).or(page.getByTitle(opts.text, { exact: opts.exact })).first();
|
|
3122
3122
|
try {
|
|
3123
3123
|
await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
3124
3124
|
} catch (err) {
|
|
@@ -3518,6 +3518,27 @@ async function focusPageByTargetIdViaPlaywright(opts) {
|
|
|
3518
3518
|
}
|
|
3519
3519
|
}
|
|
3520
3520
|
}
|
|
3521
|
+
async function waitForTabViaPlaywright(opts) {
|
|
3522
|
+
if (opts.urlContains === void 0 && opts.titleContains === void 0)
|
|
3523
|
+
throw new Error("urlContains or titleContains is required");
|
|
3524
|
+
const timeout = Math.max(1e3, Math.min(12e4, opts.timeoutMs ?? 3e4));
|
|
3525
|
+
const start = Date.now();
|
|
3526
|
+
const POLL_INTERVAL_MS = 250;
|
|
3527
|
+
while (Date.now() - start < timeout) {
|
|
3528
|
+
const tabs = await listPagesViaPlaywright({ cdpUrl: opts.cdpUrl });
|
|
3529
|
+
const match = tabs.find((t) => {
|
|
3530
|
+
if (opts.urlContains !== void 0 && !t.url.includes(opts.urlContains)) return false;
|
|
3531
|
+
if (opts.titleContains !== void 0 && !t.title.includes(opts.titleContains)) return false;
|
|
3532
|
+
return true;
|
|
3533
|
+
});
|
|
3534
|
+
if (match) return match;
|
|
3535
|
+
await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL_MS));
|
|
3536
|
+
}
|
|
3537
|
+
const criteria = [];
|
|
3538
|
+
if (opts.urlContains !== void 0) criteria.push(`url contains "${opts.urlContains}"`);
|
|
3539
|
+
if (opts.titleContains !== void 0) criteria.push(`title contains "${opts.titleContains}"`);
|
|
3540
|
+
throw new Error(`Timed out waiting for tab: ${criteria.join(", ")}`);
|
|
3541
|
+
}
|
|
3521
3542
|
async function resizeViewportViaPlaywright(opts) {
|
|
3522
3543
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3523
3544
|
ensurePageState(page);
|
|
@@ -3561,10 +3582,10 @@ async function waitForViaPlaywright(opts) {
|
|
|
3561
3582
|
}
|
|
3562
3583
|
if (opts.fn !== void 0) {
|
|
3563
3584
|
if (typeof opts.fn === "function") {
|
|
3564
|
-
await page.waitForFunction(opts.fn,
|
|
3585
|
+
await page.waitForFunction(opts.fn, opts.arg, { timeout });
|
|
3565
3586
|
} else {
|
|
3566
3587
|
const fn = opts.fn.trim();
|
|
3567
|
-
if (fn !== "") await page.waitForFunction(fn,
|
|
3588
|
+
if (fn !== "") await page.waitForFunction(fn, opts.arg, { timeout });
|
|
3568
3589
|
}
|
|
3569
3590
|
}
|
|
3570
3591
|
}
|
|
@@ -3677,6 +3698,7 @@ async function executeSingleAction(action, cdpUrl, targetId, evaluateEnabled, de
|
|
|
3677
3698
|
url: action.url,
|
|
3678
3699
|
loadState: action.loadState,
|
|
3679
3700
|
fn: action.fn,
|
|
3701
|
+
arg: action.arg,
|
|
3680
3702
|
timeoutMs: action.timeoutMs
|
|
3681
3703
|
});
|
|
3682
3704
|
break;
|
|
@@ -4152,6 +4174,40 @@ async function responseBodyViaPlaywright(opts) {
|
|
|
4152
4174
|
truncated
|
|
4153
4175
|
};
|
|
4154
4176
|
}
|
|
4177
|
+
async function waitForRequestViaPlaywright(opts) {
|
|
4178
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
4179
|
+
ensurePageState(page);
|
|
4180
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
|
|
4181
|
+
const pattern = opts.url.trim();
|
|
4182
|
+
if (!pattern) throw new Error("url is required");
|
|
4183
|
+
const upperMethod = opts.method !== void 0 ? opts.method.toUpperCase() : void 0;
|
|
4184
|
+
const response = await page.waitForResponse(
|
|
4185
|
+
(resp) => matchUrlPattern(pattern, resp.url()) && (upperMethod === void 0 || resp.request().method() === upperMethod),
|
|
4186
|
+
{ timeout }
|
|
4187
|
+
);
|
|
4188
|
+
const request = response.request();
|
|
4189
|
+
let responseBody;
|
|
4190
|
+
let truncated = false;
|
|
4191
|
+
try {
|
|
4192
|
+
responseBody = await response.text();
|
|
4193
|
+
const maxChars = typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars) ? Math.max(1, Math.min(5e6, Math.floor(opts.maxChars))) : 2e5;
|
|
4194
|
+
if (responseBody.length > maxChars) {
|
|
4195
|
+
responseBody = responseBody.slice(0, maxChars);
|
|
4196
|
+
truncated = true;
|
|
4197
|
+
}
|
|
4198
|
+
} catch (err) {
|
|
4199
|
+
console.warn("[browserclaw] response body unavailable:", err instanceof Error ? err.message : String(err));
|
|
4200
|
+
}
|
|
4201
|
+
return {
|
|
4202
|
+
url: response.url(),
|
|
4203
|
+
method: request.method(),
|
|
4204
|
+
postData: request.postData() ?? void 0,
|
|
4205
|
+
status: response.status(),
|
|
4206
|
+
ok: response.ok(),
|
|
4207
|
+
responseBody,
|
|
4208
|
+
truncated
|
|
4209
|
+
};
|
|
4210
|
+
}
|
|
4155
4211
|
|
|
4156
4212
|
// src/capture/screenshot.ts
|
|
4157
4213
|
async function takeScreenshotViaPlaywright(opts) {
|
|
@@ -5588,6 +5644,35 @@ var CrawlPage = class {
|
|
|
5588
5644
|
maxChars: opts?.maxChars
|
|
5589
5645
|
});
|
|
5590
5646
|
}
|
|
5647
|
+
/**
|
|
5648
|
+
* Wait for a network request matching a URL pattern and return request + response details.
|
|
5649
|
+
*
|
|
5650
|
+
* Unlike `networkRequests()` which only captures metadata, this method captures
|
|
5651
|
+
* the full request body (POST data) and response body.
|
|
5652
|
+
*
|
|
5653
|
+
* @param url - URL string or pattern to match (supports `*` wildcards and substring matching)
|
|
5654
|
+
* @param opts - Options (method filter, timeoutMs, maxChars for response body)
|
|
5655
|
+
* @returns Request method, postData, response status, and response body
|
|
5656
|
+
*
|
|
5657
|
+
* @example
|
|
5658
|
+
* ```ts
|
|
5659
|
+
* const reqPromise = page.waitForRequest('/api/submit', { method: 'POST' });
|
|
5660
|
+
* await page.click('e5'); // submit a form
|
|
5661
|
+
* const req = await reqPromise;
|
|
5662
|
+
* console.log(req.postData); // form body
|
|
5663
|
+
* console.log(req.status, req.responseBody); // response
|
|
5664
|
+
* ```
|
|
5665
|
+
*/
|
|
5666
|
+
async waitForRequest(url, opts) {
|
|
5667
|
+
return waitForRequestViaPlaywright({
|
|
5668
|
+
cdpUrl: this.cdpUrl,
|
|
5669
|
+
targetId: this.targetId,
|
|
5670
|
+
url,
|
|
5671
|
+
method: opts?.method,
|
|
5672
|
+
timeoutMs: opts?.timeoutMs,
|
|
5673
|
+
maxChars: opts?.maxChars
|
|
5674
|
+
});
|
|
5675
|
+
}
|
|
5591
5676
|
/**
|
|
5592
5677
|
* Get console messages captured from the page.
|
|
5593
5678
|
*
|
|
@@ -6110,6 +6195,31 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
6110
6195
|
async tabs() {
|
|
6111
6196
|
return listPagesViaPlaywright({ cdpUrl: this.cdpUrl });
|
|
6112
6197
|
}
|
|
6198
|
+
/**
|
|
6199
|
+
* Wait for a tab matching the given criteria and return a page handle.
|
|
6200
|
+
*
|
|
6201
|
+
* Polls open tabs until one matches, then focuses it and returns a CrawlPage.
|
|
6202
|
+
*
|
|
6203
|
+
* @param opts - Match criteria (urlContains, titleContains) and timeout
|
|
6204
|
+
* @returns A CrawlPage for the matched tab
|
|
6205
|
+
*
|
|
6206
|
+
* @example
|
|
6207
|
+
* ```ts
|
|
6208
|
+
* await page.click('e5'); // opens a new tab
|
|
6209
|
+
* const appPage = await browser.waitForTab({ urlContains: 'app-web' });
|
|
6210
|
+
* const { snapshot } = await appPage.snapshot();
|
|
6211
|
+
* ```
|
|
6212
|
+
*/
|
|
6213
|
+
async waitForTab(opts) {
|
|
6214
|
+
const tab = await waitForTabViaPlaywright({
|
|
6215
|
+
cdpUrl: this.cdpUrl,
|
|
6216
|
+
urlContains: opts.urlContains,
|
|
6217
|
+
titleContains: opts.titleContains,
|
|
6218
|
+
timeoutMs: opts.timeoutMs
|
|
6219
|
+
});
|
|
6220
|
+
await focusPageByTargetIdViaPlaywright({ cdpUrl: this.cdpUrl, targetId: tab.targetId });
|
|
6221
|
+
return new CrawlPage(this.cdpUrl, tab.targetId, this.ssrfPolicy);
|
|
6222
|
+
}
|
|
6113
6223
|
/**
|
|
6114
6224
|
* Bring a tab to the foreground.
|
|
6115
6225
|
*
|