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 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'); // Click by ref
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
- | | [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) |
50
- |:---|:---:|:---:|:---:|:---:|
51
- | Ref → exact element, no guessing | :white_check_mark: | :heavy_minus_sign: | :x: | :white_check_mark: |
52
- | No vision model in the loop | :white_check_mark: | :heavy_minus_sign: | :white_check_mark: | :white_check_mark: |
53
- | Survives redesigns (semantic, not pixel) | :white_check_mark: | :heavy_minus_sign: | :white_check_mark: | :white_check_mark: |
54
- | Fill 10 form fields in one call | :white_check_mark: | :x: | :x: | :x: |
55
- | Interact with cross-origin iframes | :white_check_mark: | :white_check_mark: | :x: | :x: |
56
- | Playwright engine (auto-wait, locators) | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: |
57
- | Embeddable in your own JS/TS agent loop | :white_check_mark: | :x: | :heavy_minus_sign: | :x: |
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&ensp; :heavy_minus_sign: = Partial&ensp; :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, // default: false (visible window)
136
+ headless: false, // default: false (visible window)
136
137
  executablePath: '...', // optional: specific browser path
137
- cdpPort: 9222, // default: 9222
138
- noSandbox: false, // default: false (set true for Docker/CI)
139
- userDataDir: '...', // optional: custom user data directory
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', // profile accent color (hex)
143
+ profileColor: '#FF4500', // profile accent color (hex)
142
144
  chromeArgs: ['--start-maximized'], // additional Chrome flags
143
145
  });
144
146
 
145
- // Or connect to an already-running Chrome instance
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(); // list all 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.focus(tabId); // bring tab to front
162
- await browser.close(tabId); // close a tab
163
- await browser.stop(); // stop browser + cleanup
164
-
165
- page.id; // CDP target ID (use with focus/close/page)
166
- await page.url(); // current page URL
167
- await page.title(); // current page title
168
- browser.url; // CDP endpoint URL
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, // Only interactive elements (buttons, links, inputs)
184
- compact: true, // Remove structural containers without refs
185
- maxDepth: 6, // Limit tree depth
186
- maxChars: 80000, // Truncate if snapshot exceeds this size
187
- mode: 'aria', // 'aria' (default) or 'role'
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'); // instant fill
215
- await page.type('e3', 'slow typing', { slowly: true }); // keystroke by keystroke
216
- await page.type('e3', 'search', { submit: true }); // type + press Enter
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()` truthy values are `true`, `1`, `'1'`, `'true'`. Type can be omitted and defaults to `'text'`. Empty ref throws.
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 *before* the action that triggers the dialog.
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(); // reload the current page
277
- await page.goBack(); // navigate back in history
278
- await page.goForward(); // navigate forward in history
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' }); // wait for CSS selector
284
- await page.waitFor({ fn: '() => document.readyState === "complete"' }); // custom JS
285
- await page.waitFor({ timeMs: 1000 }); // sleep
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(); // viewport PNG → Buffer
294
- const fullPage = await page.screenshot({ fullPage: true }); // full scrollable page
295
- const element = await page.screenshot({ ref: 'e1' }); // specific element by ref
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' }); // JPEG format
352
+ const jpeg = await page.screenshot({ type: 'jpeg' }); // JPEG format
298
353
 
299
354
  // PDF
300
- const pdf = await page.pdf(); // PDF export (headless only)
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(); // all messages
340
- const errors = await page.consoleLogs({ level: 'error' }); // errors only
341
- const recent = await page.consoleLogs({ clear: true }); // read and clear buffer
342
- const pageErrors = await page.pageErrors(); // uncaught exceptions
343
- const requests = await page.networkRequests({ filter: '/api' }); // filter by URL
344
- const fresh = await page.networkRequests({ clear: true }); // read and clear buffer
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, void 0, { timeout });
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, void 0, { timeout });
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
  *