browserclaw 0.10.1 → 0.10.3
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 +129 -4
- 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 +129 -4
- 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
|
@@ -3085,6 +3085,17 @@ function requiresInspectableBrowserNavigationRedirects(ssrfPolicy) {
|
|
|
3085
3085
|
var MAX_CLICK_DELAY_MS = 5e3;
|
|
3086
3086
|
var DEFAULT_SCROLL_TIMEOUT_MS = 2e4;
|
|
3087
3087
|
var CHECKABLE_ROLES = /* @__PURE__ */ new Set(["menuitemcheckbox", "menuitemradio", "checkbox", "switch"]);
|
|
3088
|
+
async function setCheckedViaEvaluate(locator, checked) {
|
|
3089
|
+
await locator.evaluate((el, desired) => {
|
|
3090
|
+
const input = el;
|
|
3091
|
+
const desc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "checked");
|
|
3092
|
+
if (desc?.set) desc.set.call(input, desired);
|
|
3093
|
+
else input.checked = desired;
|
|
3094
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
3095
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
3096
|
+
input.click();
|
|
3097
|
+
}, checked);
|
|
3098
|
+
}
|
|
3088
3099
|
function resolveLocator(page, resolved) {
|
|
3089
3100
|
if (resolved.ref !== void 0 && resolved.ref !== "") return refLocator(page, resolved.ref);
|
|
3090
3101
|
const sel = resolved.selector ?? "";
|
|
@@ -3259,8 +3270,12 @@ async function fillFormViaPlaywright(opts) {
|
|
|
3259
3270
|
const checked = rawValue === true || rawValue === 1 || rawValue === "1" || rawValue === "true";
|
|
3260
3271
|
try {
|
|
3261
3272
|
await locator.setChecked(checked, { timeout, force: true });
|
|
3262
|
-
} catch
|
|
3263
|
-
|
|
3273
|
+
} catch {
|
|
3274
|
+
try {
|
|
3275
|
+
await setCheckedViaEvaluate(locator, checked);
|
|
3276
|
+
} catch (err) {
|
|
3277
|
+
throw toAIFriendlyError(err, ref);
|
|
3278
|
+
}
|
|
3264
3279
|
}
|
|
3265
3280
|
continue;
|
|
3266
3281
|
}
|
|
@@ -3518,6 +3533,27 @@ async function focusPageByTargetIdViaPlaywright(opts) {
|
|
|
3518
3533
|
}
|
|
3519
3534
|
}
|
|
3520
3535
|
}
|
|
3536
|
+
async function waitForTabViaPlaywright(opts) {
|
|
3537
|
+
if (opts.urlContains === void 0 && opts.titleContains === void 0)
|
|
3538
|
+
throw new Error("urlContains or titleContains is required");
|
|
3539
|
+
const timeout = Math.max(1e3, Math.min(12e4, opts.timeoutMs ?? 3e4));
|
|
3540
|
+
const start = Date.now();
|
|
3541
|
+
const POLL_INTERVAL_MS = 250;
|
|
3542
|
+
while (Date.now() - start < timeout) {
|
|
3543
|
+
const tabs = await listPagesViaPlaywright({ cdpUrl: opts.cdpUrl });
|
|
3544
|
+
const match = tabs.find((t) => {
|
|
3545
|
+
if (opts.urlContains !== void 0 && !t.url.includes(opts.urlContains)) return false;
|
|
3546
|
+
if (opts.titleContains !== void 0 && !t.title.includes(opts.titleContains)) return false;
|
|
3547
|
+
return true;
|
|
3548
|
+
});
|
|
3549
|
+
if (match) return match;
|
|
3550
|
+
await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL_MS));
|
|
3551
|
+
}
|
|
3552
|
+
const criteria = [];
|
|
3553
|
+
if (opts.urlContains !== void 0) criteria.push(`url contains "${opts.urlContains}"`);
|
|
3554
|
+
if (opts.titleContains !== void 0) criteria.push(`title contains "${opts.titleContains}"`);
|
|
3555
|
+
throw new Error(`Timed out waiting for tab: ${criteria.join(", ")}`);
|
|
3556
|
+
}
|
|
3521
3557
|
async function resizeViewportViaPlaywright(opts) {
|
|
3522
3558
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3523
3559
|
ensurePageState(page);
|
|
@@ -3561,10 +3597,10 @@ async function waitForViaPlaywright(opts) {
|
|
|
3561
3597
|
}
|
|
3562
3598
|
if (opts.fn !== void 0) {
|
|
3563
3599
|
if (typeof opts.fn === "function") {
|
|
3564
|
-
await page.waitForFunction(opts.fn, { timeout });
|
|
3600
|
+
await page.waitForFunction(opts.fn, opts.arg, { timeout });
|
|
3565
3601
|
} else {
|
|
3566
3602
|
const fn = opts.fn.trim();
|
|
3567
|
-
if (fn !== "") await page.waitForFunction(fn,
|
|
3603
|
+
if (fn !== "") await page.waitForFunction(fn, opts.arg, { timeout });
|
|
3568
3604
|
}
|
|
3569
3605
|
}
|
|
3570
3606
|
}
|
|
@@ -3677,6 +3713,7 @@ async function executeSingleAction(action, cdpUrl, targetId, evaluateEnabled, de
|
|
|
3677
3713
|
url: action.url,
|
|
3678
3714
|
loadState: action.loadState,
|
|
3679
3715
|
fn: action.fn,
|
|
3716
|
+
arg: action.arg,
|
|
3680
3717
|
timeoutMs: action.timeoutMs
|
|
3681
3718
|
});
|
|
3682
3719
|
break;
|
|
@@ -4152,6 +4189,40 @@ async function responseBodyViaPlaywright(opts) {
|
|
|
4152
4189
|
truncated
|
|
4153
4190
|
};
|
|
4154
4191
|
}
|
|
4192
|
+
async function waitForRequestViaPlaywright(opts) {
|
|
4193
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
4194
|
+
ensurePageState(page);
|
|
4195
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
|
|
4196
|
+
const pattern = opts.url.trim();
|
|
4197
|
+
if (!pattern) throw new Error("url is required");
|
|
4198
|
+
const upperMethod = opts.method !== void 0 ? opts.method.toUpperCase() : void 0;
|
|
4199
|
+
const response = await page.waitForResponse(
|
|
4200
|
+
(resp) => matchUrlPattern(pattern, resp.url()) && (upperMethod === void 0 || resp.request().method() === upperMethod),
|
|
4201
|
+
{ timeout }
|
|
4202
|
+
);
|
|
4203
|
+
const request = response.request();
|
|
4204
|
+
let responseBody;
|
|
4205
|
+
let truncated = false;
|
|
4206
|
+
try {
|
|
4207
|
+
responseBody = await response.text();
|
|
4208
|
+
const maxChars = typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars) ? Math.max(1, Math.min(5e6, Math.floor(opts.maxChars))) : 2e5;
|
|
4209
|
+
if (responseBody.length > maxChars) {
|
|
4210
|
+
responseBody = responseBody.slice(0, maxChars);
|
|
4211
|
+
truncated = true;
|
|
4212
|
+
}
|
|
4213
|
+
} catch (err) {
|
|
4214
|
+
console.warn("[browserclaw] response body unavailable:", err instanceof Error ? err.message : String(err));
|
|
4215
|
+
}
|
|
4216
|
+
return {
|
|
4217
|
+
url: response.url(),
|
|
4218
|
+
method: request.method(),
|
|
4219
|
+
postData: request.postData() ?? void 0,
|
|
4220
|
+
status: response.status(),
|
|
4221
|
+
ok: response.ok(),
|
|
4222
|
+
responseBody,
|
|
4223
|
+
truncated
|
|
4224
|
+
};
|
|
4225
|
+
}
|
|
4155
4226
|
|
|
4156
4227
|
// src/capture/screenshot.ts
|
|
4157
4228
|
async function takeScreenshotViaPlaywright(opts) {
|
|
@@ -5588,6 +5659,35 @@ var CrawlPage = class {
|
|
|
5588
5659
|
maxChars: opts?.maxChars
|
|
5589
5660
|
});
|
|
5590
5661
|
}
|
|
5662
|
+
/**
|
|
5663
|
+
* Wait for a network request matching a URL pattern and return request + response details.
|
|
5664
|
+
*
|
|
5665
|
+
* Unlike `networkRequests()` which only captures metadata, this method captures
|
|
5666
|
+
* the full request body (POST data) and response body.
|
|
5667
|
+
*
|
|
5668
|
+
* @param url - URL string or pattern to match (supports `*` wildcards and substring matching)
|
|
5669
|
+
* @param opts - Options (method filter, timeoutMs, maxChars for response body)
|
|
5670
|
+
* @returns Request method, postData, response status, and response body
|
|
5671
|
+
*
|
|
5672
|
+
* @example
|
|
5673
|
+
* ```ts
|
|
5674
|
+
* const reqPromise = page.waitForRequest('/api/submit', { method: 'POST' });
|
|
5675
|
+
* await page.click('e5'); // submit a form
|
|
5676
|
+
* const req = await reqPromise;
|
|
5677
|
+
* console.log(req.postData); // form body
|
|
5678
|
+
* console.log(req.status, req.responseBody); // response
|
|
5679
|
+
* ```
|
|
5680
|
+
*/
|
|
5681
|
+
async waitForRequest(url, opts) {
|
|
5682
|
+
return waitForRequestViaPlaywright({
|
|
5683
|
+
cdpUrl: this.cdpUrl,
|
|
5684
|
+
targetId: this.targetId,
|
|
5685
|
+
url,
|
|
5686
|
+
method: opts?.method,
|
|
5687
|
+
timeoutMs: opts?.timeoutMs,
|
|
5688
|
+
maxChars: opts?.maxChars
|
|
5689
|
+
});
|
|
5690
|
+
}
|
|
5591
5691
|
/**
|
|
5592
5692
|
* Get console messages captured from the page.
|
|
5593
5693
|
*
|
|
@@ -6110,6 +6210,31 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
6110
6210
|
async tabs() {
|
|
6111
6211
|
return listPagesViaPlaywright({ cdpUrl: this.cdpUrl });
|
|
6112
6212
|
}
|
|
6213
|
+
/**
|
|
6214
|
+
* Wait for a tab matching the given criteria and return a page handle.
|
|
6215
|
+
*
|
|
6216
|
+
* Polls open tabs until one matches, then focuses it and returns a CrawlPage.
|
|
6217
|
+
*
|
|
6218
|
+
* @param opts - Match criteria (urlContains, titleContains) and timeout
|
|
6219
|
+
* @returns A CrawlPage for the matched tab
|
|
6220
|
+
*
|
|
6221
|
+
* @example
|
|
6222
|
+
* ```ts
|
|
6223
|
+
* await page.click('e5'); // opens a new tab
|
|
6224
|
+
* const appPage = await browser.waitForTab({ urlContains: 'app-web' });
|
|
6225
|
+
* const { snapshot } = await appPage.snapshot();
|
|
6226
|
+
* ```
|
|
6227
|
+
*/
|
|
6228
|
+
async waitForTab(opts) {
|
|
6229
|
+
const tab = await waitForTabViaPlaywright({
|
|
6230
|
+
cdpUrl: this.cdpUrl,
|
|
6231
|
+
urlContains: opts.urlContains,
|
|
6232
|
+
titleContains: opts.titleContains,
|
|
6233
|
+
timeoutMs: opts.timeoutMs
|
|
6234
|
+
});
|
|
6235
|
+
await focusPageByTargetIdViaPlaywright({ cdpUrl: this.cdpUrl, targetId: tab.targetId });
|
|
6236
|
+
return new CrawlPage(this.cdpUrl, tab.targetId, this.ssrfPolicy);
|
|
6237
|
+
}
|
|
6113
6238
|
/**
|
|
6114
6239
|
* Bring a tab to the foreground.
|
|
6115
6240
|
*
|