browserclaw 0.10.1 → 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/dist/index.d.cts CHANGED
@@ -77,6 +77,7 @@ type BatchAction = {
77
77
  url?: string;
78
78
  loadState?: 'load' | 'domcontentloaded' | 'networkidle';
79
79
  fn?: string;
80
+ arg?: unknown;
80
81
  targetId?: string;
81
82
  timeoutMs?: number;
82
83
  } | {
@@ -411,7 +412,9 @@ interface WaitOptions {
411
412
  /** Wait for a specific page load state */
412
413
  loadState?: 'load' | 'domcontentloaded' | 'networkidle';
413
414
  /** Wait until a JavaScript function returns truthy (evaluated in browser context). Accepts a string or a serializable function. */
414
- fn?: string | (() => unknown);
415
+ fn?: string | ((arg?: unknown) => unknown);
416
+ /** Serializable argument passed to `fn` in the browser context. Use an array or object to pass multiple values. */
417
+ arg?: unknown;
415
418
  /** Timeout for each condition in milliseconds. Default: `20000` */
416
419
  timeoutMs?: number;
417
420
  }
@@ -540,6 +543,23 @@ interface DialogEvent {
540
543
  }
541
544
  /** Callback for persistent dialog handling. */
542
545
  type DialogHandler = (event: DialogEvent) => void | Promise<void>;
546
+ /** Result of waiting for a network request to complete. */
547
+ interface RequestResult {
548
+ /** The final request URL */
549
+ url: string;
550
+ /** HTTP method (e.g. `'GET'`, `'POST'`) */
551
+ method: string;
552
+ /** Request body (for POST/PUT/PATCH requests) */
553
+ postData?: string;
554
+ /** HTTP response status code */
555
+ status: number;
556
+ /** Whether the response status was 2xx */
557
+ ok: boolean;
558
+ /** Response body text */
559
+ responseBody?: string;
560
+ /** Whether the response body was truncated due to maxChars */
561
+ truncated?: boolean;
562
+ }
543
563
  /** Result of intercepting a response body. */
544
564
  interface ResponseBodyResult {
545
565
  /** The response URL */
@@ -1165,6 +1185,30 @@ declare class CrawlPage {
1165
1185
  timeoutMs?: number;
1166
1186
  maxChars?: number;
1167
1187
  }): Promise<ResponseBodyResult>;
1188
+ /**
1189
+ * Wait for a network request matching a URL pattern and return request + response details.
1190
+ *
1191
+ * Unlike `networkRequests()` which only captures metadata, this method captures
1192
+ * the full request body (POST data) and response body.
1193
+ *
1194
+ * @param url - URL string or pattern to match (supports `*` wildcards and substring matching)
1195
+ * @param opts - Options (method filter, timeoutMs, maxChars for response body)
1196
+ * @returns Request method, postData, response status, and response body
1197
+ *
1198
+ * @example
1199
+ * ```ts
1200
+ * const reqPromise = page.waitForRequest('/api/submit', { method: 'POST' });
1201
+ * await page.click('e5'); // submit a form
1202
+ * const req = await reqPromise;
1203
+ * console.log(req.postData); // form body
1204
+ * console.log(req.status, req.responseBody); // response
1205
+ * ```
1206
+ */
1207
+ waitForRequest(url: string, opts?: {
1208
+ method?: string;
1209
+ timeoutMs?: number;
1210
+ maxChars?: number;
1211
+ }): Promise<RequestResult>;
1168
1212
  /**
1169
1213
  * Get console messages captured from the page.
1170
1214
  *
@@ -1535,6 +1579,26 @@ declare class BrowserClaw {
1535
1579
  * @returns Array of tab information objects
1536
1580
  */
1537
1581
  tabs(): Promise<BrowserTab[]>;
1582
+ /**
1583
+ * Wait for a tab matching the given criteria and return a page handle.
1584
+ *
1585
+ * Polls open tabs until one matches, then focuses it and returns a CrawlPage.
1586
+ *
1587
+ * @param opts - Match criteria (urlContains, titleContains) and timeout
1588
+ * @returns A CrawlPage for the matched tab
1589
+ *
1590
+ * @example
1591
+ * ```ts
1592
+ * await page.click('e5'); // opens a new tab
1593
+ * const appPage = await browser.waitForTab({ urlContains: 'app-web' });
1594
+ * const { snapshot } = await appPage.snapshot();
1595
+ * ```
1596
+ */
1597
+ waitForTab(opts: {
1598
+ urlContains?: string;
1599
+ titleContains?: string;
1600
+ timeoutMs?: number;
1601
+ }): Promise<CrawlPage>;
1538
1602
  /**
1539
1603
  * Bring a tab to the foreground.
1540
1604
  *
@@ -1793,4 +1857,4 @@ declare function waitForChallengeViaPlaywright(opts: {
1793
1857
  pollMs?: number;
1794
1858
  }): Promise<ChallengeWaitResult>;
1795
1859
 
1796
- export { type AriaNode, type AriaSnapshotResult, type BatchAction, type BatchActionResult, BrowserClaw, type BrowserNavigationPolicyOptions, type BrowserNavigationRequestLike, type BrowserTab, BrowserTabNotFoundError, type ChallengeInfo, type ChallengeKind, type ChallengeWaitResult, type ChromeExecutable, type ChromeKind, type ClickOptions, type ColorScheme, type ConnectOptions, type ConsoleMessage, type ContextState, type CookieData, CrawlPage, type DialogEvent, type DialogHandler, type DialogOptions, type DownloadResult, type FormField, type FrameEvalResult, type GeolocationOptions, type HttpCredentials, InvalidBrowserNavigationUrlError, type LaunchOptions, type LookupFn, type NetworkRequest, type PageError, type PinnedHostname, type ResponseBodyResult, type RoleRefInfo, type RoleRefs, STEALTH_SCRIPT, type ScreenshotOptions, type SnapshotOptions, type SnapshotResult, type SnapshotStats, type SsrfPolicy, type StorageKind, type TraceStartOptions, type TypeOptions, type UntrustedContentMeta, type WaitOptions, assertBrowserNavigationAllowed, assertBrowserNavigationRedirectChainAllowed, assertBrowserNavigationResultAllowed, assertSafeUploadPaths, batchViaPlaywright, createPinnedLookup, detectChallengeViaPlaywright, ensureContextState, executeSingleAction, forceDisconnectPlaywrightForTarget, getChromeWebSocketUrl, getRestoredPageForTarget, isChromeCdpReady, isChromeReachable, normalizeCdpHttpBaseForJsonEndpoints, parseRoleRef, pressAndHoldViaCdp, requireRef, requireRefOrSelector, requiresInspectableBrowserNavigationRedirects, resolveBoundedDelayMs, resolveInteractionTimeoutMs, resolvePageByTargetIdOrThrow, resolvePinnedHostnameWithPolicy, resolveStrictExistingUploadPaths, sanitizeUntrustedFileName, setDialogHandler, waitForChallengeViaPlaywright, withBrowserNavigationPolicy, withPageScopedCdpClient, withPlaywrightPageCdpSession, writeViaSiblingTempPath };
1860
+ export { type AriaNode, type AriaSnapshotResult, type BatchAction, type BatchActionResult, BrowserClaw, type BrowserNavigationPolicyOptions, type BrowserNavigationRequestLike, type BrowserTab, BrowserTabNotFoundError, type ChallengeInfo, type ChallengeKind, type ChallengeWaitResult, type ChromeExecutable, type ChromeKind, type ClickOptions, type ColorScheme, type ConnectOptions, type ConsoleMessage, type ContextState, type CookieData, CrawlPage, type DialogEvent, type DialogHandler, type DialogOptions, type DownloadResult, type FormField, type FrameEvalResult, type GeolocationOptions, type HttpCredentials, InvalidBrowserNavigationUrlError, type LaunchOptions, type LookupFn, type NetworkRequest, type PageError, type PinnedHostname, type RequestResult, type ResponseBodyResult, type RoleRefInfo, type RoleRefs, STEALTH_SCRIPT, type ScreenshotOptions, type SnapshotOptions, type SnapshotResult, type SnapshotStats, type SsrfPolicy, type StorageKind, type TraceStartOptions, type TypeOptions, type UntrustedContentMeta, type WaitOptions, assertBrowserNavigationAllowed, assertBrowserNavigationRedirectChainAllowed, assertBrowserNavigationResultAllowed, assertSafeUploadPaths, batchViaPlaywright, createPinnedLookup, detectChallengeViaPlaywright, ensureContextState, executeSingleAction, forceDisconnectPlaywrightForTarget, getChromeWebSocketUrl, getRestoredPageForTarget, isChromeCdpReady, isChromeReachable, normalizeCdpHttpBaseForJsonEndpoints, parseRoleRef, pressAndHoldViaCdp, requireRef, requireRefOrSelector, requiresInspectableBrowserNavigationRedirects, resolveBoundedDelayMs, resolveInteractionTimeoutMs, resolvePageByTargetIdOrThrow, resolvePinnedHostnameWithPolicy, resolveStrictExistingUploadPaths, sanitizeUntrustedFileName, setDialogHandler, waitForChallengeViaPlaywright, withBrowserNavigationPolicy, withPageScopedCdpClient, withPlaywrightPageCdpSession, writeViaSiblingTempPath };
package/dist/index.d.ts CHANGED
@@ -77,6 +77,7 @@ type BatchAction = {
77
77
  url?: string;
78
78
  loadState?: 'load' | 'domcontentloaded' | 'networkidle';
79
79
  fn?: string;
80
+ arg?: unknown;
80
81
  targetId?: string;
81
82
  timeoutMs?: number;
82
83
  } | {
@@ -411,7 +412,9 @@ interface WaitOptions {
411
412
  /** Wait for a specific page load state */
412
413
  loadState?: 'load' | 'domcontentloaded' | 'networkidle';
413
414
  /** Wait until a JavaScript function returns truthy (evaluated in browser context). Accepts a string or a serializable function. */
414
- fn?: string | (() => unknown);
415
+ fn?: string | ((arg?: unknown) => unknown);
416
+ /** Serializable argument passed to `fn` in the browser context. Use an array or object to pass multiple values. */
417
+ arg?: unknown;
415
418
  /** Timeout for each condition in milliseconds. Default: `20000` */
416
419
  timeoutMs?: number;
417
420
  }
@@ -540,6 +543,23 @@ interface DialogEvent {
540
543
  }
541
544
  /** Callback for persistent dialog handling. */
542
545
  type DialogHandler = (event: DialogEvent) => void | Promise<void>;
546
+ /** Result of waiting for a network request to complete. */
547
+ interface RequestResult {
548
+ /** The final request URL */
549
+ url: string;
550
+ /** HTTP method (e.g. `'GET'`, `'POST'`) */
551
+ method: string;
552
+ /** Request body (for POST/PUT/PATCH requests) */
553
+ postData?: string;
554
+ /** HTTP response status code */
555
+ status: number;
556
+ /** Whether the response status was 2xx */
557
+ ok: boolean;
558
+ /** Response body text */
559
+ responseBody?: string;
560
+ /** Whether the response body was truncated due to maxChars */
561
+ truncated?: boolean;
562
+ }
543
563
  /** Result of intercepting a response body. */
544
564
  interface ResponseBodyResult {
545
565
  /** The response URL */
@@ -1165,6 +1185,30 @@ declare class CrawlPage {
1165
1185
  timeoutMs?: number;
1166
1186
  maxChars?: number;
1167
1187
  }): Promise<ResponseBodyResult>;
1188
+ /**
1189
+ * Wait for a network request matching a URL pattern and return request + response details.
1190
+ *
1191
+ * Unlike `networkRequests()` which only captures metadata, this method captures
1192
+ * the full request body (POST data) and response body.
1193
+ *
1194
+ * @param url - URL string or pattern to match (supports `*` wildcards and substring matching)
1195
+ * @param opts - Options (method filter, timeoutMs, maxChars for response body)
1196
+ * @returns Request method, postData, response status, and response body
1197
+ *
1198
+ * @example
1199
+ * ```ts
1200
+ * const reqPromise = page.waitForRequest('/api/submit', { method: 'POST' });
1201
+ * await page.click('e5'); // submit a form
1202
+ * const req = await reqPromise;
1203
+ * console.log(req.postData); // form body
1204
+ * console.log(req.status, req.responseBody); // response
1205
+ * ```
1206
+ */
1207
+ waitForRequest(url: string, opts?: {
1208
+ method?: string;
1209
+ timeoutMs?: number;
1210
+ maxChars?: number;
1211
+ }): Promise<RequestResult>;
1168
1212
  /**
1169
1213
  * Get console messages captured from the page.
1170
1214
  *
@@ -1535,6 +1579,26 @@ declare class BrowserClaw {
1535
1579
  * @returns Array of tab information objects
1536
1580
  */
1537
1581
  tabs(): Promise<BrowserTab[]>;
1582
+ /**
1583
+ * Wait for a tab matching the given criteria and return a page handle.
1584
+ *
1585
+ * Polls open tabs until one matches, then focuses it and returns a CrawlPage.
1586
+ *
1587
+ * @param opts - Match criteria (urlContains, titleContains) and timeout
1588
+ * @returns A CrawlPage for the matched tab
1589
+ *
1590
+ * @example
1591
+ * ```ts
1592
+ * await page.click('e5'); // opens a new tab
1593
+ * const appPage = await browser.waitForTab({ urlContains: 'app-web' });
1594
+ * const { snapshot } = await appPage.snapshot();
1595
+ * ```
1596
+ */
1597
+ waitForTab(opts: {
1598
+ urlContains?: string;
1599
+ titleContains?: string;
1600
+ timeoutMs?: number;
1601
+ }): Promise<CrawlPage>;
1538
1602
  /**
1539
1603
  * Bring a tab to the foreground.
1540
1604
  *
@@ -1793,4 +1857,4 @@ declare function waitForChallengeViaPlaywright(opts: {
1793
1857
  pollMs?: number;
1794
1858
  }): Promise<ChallengeWaitResult>;
1795
1859
 
1796
- export { type AriaNode, type AriaSnapshotResult, type BatchAction, type BatchActionResult, BrowserClaw, type BrowserNavigationPolicyOptions, type BrowserNavigationRequestLike, type BrowserTab, BrowserTabNotFoundError, type ChallengeInfo, type ChallengeKind, type ChallengeWaitResult, type ChromeExecutable, type ChromeKind, type ClickOptions, type ColorScheme, type ConnectOptions, type ConsoleMessage, type ContextState, type CookieData, CrawlPage, type DialogEvent, type DialogHandler, type DialogOptions, type DownloadResult, type FormField, type FrameEvalResult, type GeolocationOptions, type HttpCredentials, InvalidBrowserNavigationUrlError, type LaunchOptions, type LookupFn, type NetworkRequest, type PageError, type PinnedHostname, type ResponseBodyResult, type RoleRefInfo, type RoleRefs, STEALTH_SCRIPT, type ScreenshotOptions, type SnapshotOptions, type SnapshotResult, type SnapshotStats, type SsrfPolicy, type StorageKind, type TraceStartOptions, type TypeOptions, type UntrustedContentMeta, type WaitOptions, assertBrowserNavigationAllowed, assertBrowserNavigationRedirectChainAllowed, assertBrowserNavigationResultAllowed, assertSafeUploadPaths, batchViaPlaywright, createPinnedLookup, detectChallengeViaPlaywright, ensureContextState, executeSingleAction, forceDisconnectPlaywrightForTarget, getChromeWebSocketUrl, getRestoredPageForTarget, isChromeCdpReady, isChromeReachable, normalizeCdpHttpBaseForJsonEndpoints, parseRoleRef, pressAndHoldViaCdp, requireRef, requireRefOrSelector, requiresInspectableBrowserNavigationRedirects, resolveBoundedDelayMs, resolveInteractionTimeoutMs, resolvePageByTargetIdOrThrow, resolvePinnedHostnameWithPolicy, resolveStrictExistingUploadPaths, sanitizeUntrustedFileName, setDialogHandler, waitForChallengeViaPlaywright, withBrowserNavigationPolicy, withPageScopedCdpClient, withPlaywrightPageCdpSession, writeViaSiblingTempPath };
1860
+ export { type AriaNode, type AriaSnapshotResult, type BatchAction, type BatchActionResult, BrowserClaw, type BrowserNavigationPolicyOptions, type BrowserNavigationRequestLike, type BrowserTab, BrowserTabNotFoundError, type ChallengeInfo, type ChallengeKind, type ChallengeWaitResult, type ChromeExecutable, type ChromeKind, type ClickOptions, type ColorScheme, type ConnectOptions, type ConsoleMessage, type ContextState, type CookieData, CrawlPage, type DialogEvent, type DialogHandler, type DialogOptions, type DownloadResult, type FormField, type FrameEvalResult, type GeolocationOptions, type HttpCredentials, InvalidBrowserNavigationUrlError, type LaunchOptions, type LookupFn, type NetworkRequest, type PageError, type PinnedHostname, type RequestResult, type ResponseBodyResult, type RoleRefInfo, type RoleRefs, STEALTH_SCRIPT, type ScreenshotOptions, type SnapshotOptions, type SnapshotResult, type SnapshotStats, type SsrfPolicy, type StorageKind, type TraceStartOptions, type TypeOptions, type UntrustedContentMeta, type WaitOptions, assertBrowserNavigationAllowed, assertBrowserNavigationRedirectChainAllowed, assertBrowserNavigationResultAllowed, assertSafeUploadPaths, batchViaPlaywright, createPinnedLookup, detectChallengeViaPlaywright, ensureContextState, executeSingleAction, forceDisconnectPlaywrightForTarget, getChromeWebSocketUrl, getRestoredPageForTarget, isChromeCdpReady, isChromeReachable, normalizeCdpHttpBaseForJsonEndpoints, parseRoleRef, pressAndHoldViaCdp, requireRef, requireRefOrSelector, requiresInspectableBrowserNavigationRedirects, resolveBoundedDelayMs, resolveInteractionTimeoutMs, resolvePageByTargetIdOrThrow, resolvePinnedHostnameWithPolicy, resolveStrictExistingUploadPaths, sanitizeUntrustedFileName, setDialogHandler, waitForChallengeViaPlaywright, withBrowserNavigationPolicy, withPageScopedCdpClient, withPlaywrightPageCdpSession, writeViaSiblingTempPath };
package/dist/index.js CHANGED
@@ -3507,6 +3507,27 @@ async function focusPageByTargetIdViaPlaywright(opts) {
3507
3507
  }
3508
3508
  }
3509
3509
  }
3510
+ async function waitForTabViaPlaywright(opts) {
3511
+ if (opts.urlContains === void 0 && opts.titleContains === void 0)
3512
+ throw new Error("urlContains or titleContains is required");
3513
+ const timeout = Math.max(1e3, Math.min(12e4, opts.timeoutMs ?? 3e4));
3514
+ const start = Date.now();
3515
+ const POLL_INTERVAL_MS = 250;
3516
+ while (Date.now() - start < timeout) {
3517
+ const tabs = await listPagesViaPlaywright({ cdpUrl: opts.cdpUrl });
3518
+ const match = tabs.find((t) => {
3519
+ if (opts.urlContains !== void 0 && !t.url.includes(opts.urlContains)) return false;
3520
+ if (opts.titleContains !== void 0 && !t.title.includes(opts.titleContains)) return false;
3521
+ return true;
3522
+ });
3523
+ if (match) return match;
3524
+ await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL_MS));
3525
+ }
3526
+ const criteria = [];
3527
+ if (opts.urlContains !== void 0) criteria.push(`url contains "${opts.urlContains}"`);
3528
+ if (opts.titleContains !== void 0) criteria.push(`title contains "${opts.titleContains}"`);
3529
+ throw new Error(`Timed out waiting for tab: ${criteria.join(", ")}`);
3530
+ }
3510
3531
  async function resizeViewportViaPlaywright(opts) {
3511
3532
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3512
3533
  ensurePageState(page);
@@ -3550,10 +3571,10 @@ async function waitForViaPlaywright(opts) {
3550
3571
  }
3551
3572
  if (opts.fn !== void 0) {
3552
3573
  if (typeof opts.fn === "function") {
3553
- await page.waitForFunction(opts.fn, { timeout });
3574
+ await page.waitForFunction(opts.fn, opts.arg, { timeout });
3554
3575
  } else {
3555
3576
  const fn = opts.fn.trim();
3556
- if (fn !== "") await page.waitForFunction(fn, void 0, { timeout });
3577
+ if (fn !== "") await page.waitForFunction(fn, opts.arg, { timeout });
3557
3578
  }
3558
3579
  }
3559
3580
  }
@@ -3666,6 +3687,7 @@ async function executeSingleAction(action, cdpUrl, targetId, evaluateEnabled, de
3666
3687
  url: action.url,
3667
3688
  loadState: action.loadState,
3668
3689
  fn: action.fn,
3690
+ arg: action.arg,
3669
3691
  timeoutMs: action.timeoutMs
3670
3692
  });
3671
3693
  break;
@@ -4141,6 +4163,40 @@ async function responseBodyViaPlaywright(opts) {
4141
4163
  truncated
4142
4164
  };
4143
4165
  }
4166
+ async function waitForRequestViaPlaywright(opts) {
4167
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
4168
+ ensurePageState(page);
4169
+ const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
4170
+ const pattern = opts.url.trim();
4171
+ if (!pattern) throw new Error("url is required");
4172
+ const upperMethod = opts.method !== void 0 ? opts.method.toUpperCase() : void 0;
4173
+ const response = await page.waitForResponse(
4174
+ (resp) => matchUrlPattern(pattern, resp.url()) && (upperMethod === void 0 || resp.request().method() === upperMethod),
4175
+ { timeout }
4176
+ );
4177
+ const request = response.request();
4178
+ let responseBody;
4179
+ let truncated = false;
4180
+ try {
4181
+ responseBody = await response.text();
4182
+ const maxChars = typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars) ? Math.max(1, Math.min(5e6, Math.floor(opts.maxChars))) : 2e5;
4183
+ if (responseBody.length > maxChars) {
4184
+ responseBody = responseBody.slice(0, maxChars);
4185
+ truncated = true;
4186
+ }
4187
+ } catch (err) {
4188
+ console.warn("[browserclaw] response body unavailable:", err instanceof Error ? err.message : String(err));
4189
+ }
4190
+ return {
4191
+ url: response.url(),
4192
+ method: request.method(),
4193
+ postData: request.postData() ?? void 0,
4194
+ status: response.status(),
4195
+ ok: response.ok(),
4196
+ responseBody,
4197
+ truncated
4198
+ };
4199
+ }
4144
4200
 
4145
4201
  // src/capture/screenshot.ts
4146
4202
  async function takeScreenshotViaPlaywright(opts) {
@@ -5577,6 +5633,35 @@ var CrawlPage = class {
5577
5633
  maxChars: opts?.maxChars
5578
5634
  });
5579
5635
  }
5636
+ /**
5637
+ * Wait for a network request matching a URL pattern and return request + response details.
5638
+ *
5639
+ * Unlike `networkRequests()` which only captures metadata, this method captures
5640
+ * the full request body (POST data) and response body.
5641
+ *
5642
+ * @param url - URL string or pattern to match (supports `*` wildcards and substring matching)
5643
+ * @param opts - Options (method filter, timeoutMs, maxChars for response body)
5644
+ * @returns Request method, postData, response status, and response body
5645
+ *
5646
+ * @example
5647
+ * ```ts
5648
+ * const reqPromise = page.waitForRequest('/api/submit', { method: 'POST' });
5649
+ * await page.click('e5'); // submit a form
5650
+ * const req = await reqPromise;
5651
+ * console.log(req.postData); // form body
5652
+ * console.log(req.status, req.responseBody); // response
5653
+ * ```
5654
+ */
5655
+ async waitForRequest(url, opts) {
5656
+ return waitForRequestViaPlaywright({
5657
+ cdpUrl: this.cdpUrl,
5658
+ targetId: this.targetId,
5659
+ url,
5660
+ method: opts?.method,
5661
+ timeoutMs: opts?.timeoutMs,
5662
+ maxChars: opts?.maxChars
5663
+ });
5664
+ }
5580
5665
  /**
5581
5666
  * Get console messages captured from the page.
5582
5667
  *
@@ -6099,6 +6184,31 @@ var BrowserClaw = class _BrowserClaw {
6099
6184
  async tabs() {
6100
6185
  return listPagesViaPlaywright({ cdpUrl: this.cdpUrl });
6101
6186
  }
6187
+ /**
6188
+ * Wait for a tab matching the given criteria and return a page handle.
6189
+ *
6190
+ * Polls open tabs until one matches, then focuses it and returns a CrawlPage.
6191
+ *
6192
+ * @param opts - Match criteria (urlContains, titleContains) and timeout
6193
+ * @returns A CrawlPage for the matched tab
6194
+ *
6195
+ * @example
6196
+ * ```ts
6197
+ * await page.click('e5'); // opens a new tab
6198
+ * const appPage = await browser.waitForTab({ urlContains: 'app-web' });
6199
+ * const { snapshot } = await appPage.snapshot();
6200
+ * ```
6201
+ */
6202
+ async waitForTab(opts) {
6203
+ const tab = await waitForTabViaPlaywright({
6204
+ cdpUrl: this.cdpUrl,
6205
+ urlContains: opts.urlContains,
6206
+ titleContains: opts.titleContains,
6207
+ timeoutMs: opts.timeoutMs
6208
+ });
6209
+ await focusPageByTargetIdViaPlaywright({ cdpUrl: this.cdpUrl, targetId: tab.targetId });
6210
+ return new CrawlPage(this.cdpUrl, tab.targetId, this.ssrfPolicy);
6211
+ }
6102
6212
  /**
6103
6213
  * Bring a tab to the foreground.
6104
6214
  *