browserclaw 0.9.7 → 0.10.1

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
@@ -184,6 +184,8 @@ interface LaunchOptions {
184
184
  profileColor?: string;
185
185
  /** Additional Chrome command-line arguments (e.g. `['--start-maximized']`). */
186
186
  chromeArgs?: string[];
187
+ /** Ignore HTTPS certificate errors (e.g. expired local dev certs). Default: `false` */
188
+ ignoreHTTPSErrors?: boolean;
187
189
  /**
188
190
  * SSRF policy controlling which URLs navigation is allowed to reach.
189
191
  * Defaults to trusted-network mode (private/internal addresses allowed).
@@ -249,6 +251,10 @@ interface RoleRefInfo {
249
251
  name?: string;
250
252
  /** Disambiguation index when multiple elements share the same role + name */
251
253
  nth?: number;
254
+ /** Whether the element is disabled (from `[disabled]` or `aria-disabled`) */
255
+ disabled?: boolean;
256
+ /** Whether a checkbox/radio/switch is checked */
257
+ checked?: boolean | 'mixed';
252
258
  }
253
259
  /** Map of ref IDs (e.g. `'e1'`, `'e2'`) to their element information. */
254
260
  type RoleRefs = Record<string, RoleRefInfo>;
@@ -375,6 +381,8 @@ interface ClickOptions {
375
381
  delayMs?: number;
376
382
  /** Timeout in milliseconds. Default: `8000` */
377
383
  timeoutMs?: number;
384
+ /** Force click even if element is hidden or covered. Dispatches the event regardless of visibility. */
385
+ force?: boolean;
378
386
  }
379
387
  /** Options for type actions. */
380
388
  interface TypeOptions {
@@ -402,8 +410,8 @@ interface WaitOptions {
402
410
  url?: string;
403
411
  /** Wait for a specific page load state */
404
412
  loadState?: 'load' | 'domcontentloaded' | 'networkidle';
405
- /** Wait until a JavaScript function returns truthy (evaluated in browser context) */
406
- fn?: string;
413
+ /** Wait until a JavaScript function returns truthy (evaluated in browser context). Accepts a string or a serializable function. */
414
+ fn?: string | (() => unknown);
407
415
  /** Timeout for each condition in milliseconds. Default: `20000` */
408
416
  timeoutMs?: number;
409
417
  }
@@ -774,6 +782,7 @@ declare class CrawlPage {
774
782
  * ```
775
783
  */
776
784
  clickByRole(role: string, name?: string, opts?: {
785
+ index?: number;
777
786
  button?: 'left' | 'right' | 'middle';
778
787
  modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[];
779
788
  timeoutMs?: number;
package/dist/index.d.ts CHANGED
@@ -184,6 +184,8 @@ interface LaunchOptions {
184
184
  profileColor?: string;
185
185
  /** Additional Chrome command-line arguments (e.g. `['--start-maximized']`). */
186
186
  chromeArgs?: string[];
187
+ /** Ignore HTTPS certificate errors (e.g. expired local dev certs). Default: `false` */
188
+ ignoreHTTPSErrors?: boolean;
187
189
  /**
188
190
  * SSRF policy controlling which URLs navigation is allowed to reach.
189
191
  * Defaults to trusted-network mode (private/internal addresses allowed).
@@ -249,6 +251,10 @@ interface RoleRefInfo {
249
251
  name?: string;
250
252
  /** Disambiguation index when multiple elements share the same role + name */
251
253
  nth?: number;
254
+ /** Whether the element is disabled (from `[disabled]` or `aria-disabled`) */
255
+ disabled?: boolean;
256
+ /** Whether a checkbox/radio/switch is checked */
257
+ checked?: boolean | 'mixed';
252
258
  }
253
259
  /** Map of ref IDs (e.g. `'e1'`, `'e2'`) to their element information. */
254
260
  type RoleRefs = Record<string, RoleRefInfo>;
@@ -375,6 +381,8 @@ interface ClickOptions {
375
381
  delayMs?: number;
376
382
  /** Timeout in milliseconds. Default: `8000` */
377
383
  timeoutMs?: number;
384
+ /** Force click even if element is hidden or covered. Dispatches the event regardless of visibility. */
385
+ force?: boolean;
378
386
  }
379
387
  /** Options for type actions. */
380
388
  interface TypeOptions {
@@ -402,8 +410,8 @@ interface WaitOptions {
402
410
  url?: string;
403
411
  /** Wait for a specific page load state */
404
412
  loadState?: 'load' | 'domcontentloaded' | 'networkidle';
405
- /** Wait until a JavaScript function returns truthy (evaluated in browser context) */
406
- fn?: string;
413
+ /** Wait until a JavaScript function returns truthy (evaluated in browser context). Accepts a string or a serializable function. */
414
+ fn?: string | (() => unknown);
407
415
  /** Timeout for each condition in milliseconds. Default: `20000` */
408
416
  timeoutMs?: number;
409
417
  }
@@ -774,6 +782,7 @@ declare class CrawlPage {
774
782
  * ```
775
783
  */
776
784
  clickByRole(role: string, name?: string, opts?: {
785
+ index?: number;
777
786
  button?: 'left' | 'right' | 'middle';
778
787
  modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[];
779
788
  timeoutMs?: number;
package/dist/index.js CHANGED
@@ -1396,6 +1396,9 @@ async function launchChrome(opts = {}) {
1396
1396
  if (opts.noSandbox === true) {
1397
1397
  args.push("--no-sandbox", "--disable-setuid-sandbox");
1398
1398
  }
1399
+ if (opts.ignoreHTTPSErrors === true) {
1400
+ args.push("--ignore-certificate-errors");
1401
+ }
1399
1402
  if (process.platform === "linux") args.push("--disable-dev-shm-usage");
1400
1403
  const extraArgs = Array.isArray(opts.chromeArgs) ? opts.chromeArgs.filter((a) => typeof a === "string" && a.trim().length > 0) : [];
1401
1404
  if (extraArgs.length) args.push(...extraArgs);
@@ -2364,7 +2367,14 @@ function toAIFriendlyError(error, selector) {
2364
2367
  `Element "${selector}" is not interactable (hidden or covered). Try scrolling it into view, closing overlays, or re-snapshotting.`
2365
2368
  );
2366
2369
  }
2367
- return error instanceof Error ? error : new Error(message);
2370
+ const timeoutMatch = /Timeout (\d+)ms exceeded/.exec(message);
2371
+ if (timeoutMatch) {
2372
+ return new Error(
2373
+ `Element "${selector}" timed out after ${timeoutMatch[1]}ms \u2014 element may be hidden or not interactable. Run a new snapshot to see current page elements.`
2374
+ );
2375
+ }
2376
+ const cleaned = message.replace(/locator\([^)]*\)\./g, "").replace(/waiting for locator\([^)]*\)/g, "").trim();
2377
+ return new Error(cleaned || message);
2368
2378
  }
2369
2379
  function normalizeTimeoutMs(timeoutMs, fallback, maxMs = 12e4) {
2370
2380
  return Math.max(500, Math.min(maxMs, timeoutMs ?? fallback));
@@ -3062,6 +3072,7 @@ function requiresInspectableBrowserNavigationRedirects(ssrfPolicy) {
3062
3072
 
3063
3073
  // src/actions/interaction.ts
3064
3074
  var MAX_CLICK_DELAY_MS = 5e3;
3075
+ var DEFAULT_SCROLL_TIMEOUT_MS = 2e4;
3065
3076
  var CHECKABLE_ROLES = /* @__PURE__ */ new Set(["menuitemcheckbox", "menuitemradio", "checkbox", "switch"]);
3066
3077
  function resolveLocator(page, resolved) {
3067
3078
  if (resolved.ref !== void 0 && resolved.ref !== "") return refLocator(page, resolved.ref);
@@ -3096,8 +3107,9 @@ async function pressAndHoldViaCdp(opts) {
3096
3107
  async function clickByTextViaPlaywright(opts) {
3097
3108
  const page = await getRestoredPageForTarget(opts);
3098
3109
  const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
3110
+ const locator = page.getByText(opts.text, { exact: opts.exact }).and(page.locator(":visible")).or(page.getByTitle(opts.text, { exact: opts.exact })).first();
3099
3111
  try {
3100
- await page.getByText(opts.text, { exact: opts.exact }).click({ timeout, button: opts.button, modifiers: opts.modifiers });
3112
+ await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
3101
3113
  } catch (err) {
3102
3114
  throw toAIFriendlyError(err, `text="${opts.text}"`);
3103
3115
  }
@@ -3105,13 +3117,12 @@ async function clickByTextViaPlaywright(opts) {
3105
3117
  async function clickByRoleViaPlaywright(opts) {
3106
3118
  const page = await getRestoredPageForTarget(opts);
3107
3119
  const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
3120
+ const label = `role=${opts.role}${opts.name !== void 0 && opts.name !== "" ? ` name="${opts.name}"` : ""}`;
3121
+ const locator = page.getByRole(opts.role, { name: opts.name }).nth(opts.index ?? 0);
3108
3122
  try {
3109
- await page.getByRole(opts.role, { name: opts.name }).click({ timeout, button: opts.button, modifiers: opts.modifiers });
3123
+ await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
3110
3124
  } catch (err) {
3111
- throw toAIFriendlyError(
3112
- err,
3113
- `role=${opts.role}${opts.name !== void 0 && opts.name !== "" ? ` name="${opts.name}"` : ""}`
3114
- );
3125
+ throw toAIFriendlyError(err, label);
3115
3126
  }
3116
3127
  }
3117
3128
  async function clickViaPlaywright(opts) {
@@ -3132,7 +3143,7 @@ async function clickViaPlaywright(opts) {
3132
3143
  try {
3133
3144
  const delayMs = resolveBoundedDelayMs(opts.delayMs, "click delayMs", MAX_CLICK_DELAY_MS);
3134
3145
  if (delayMs > 0) {
3135
- await locator.hover({ timeout });
3146
+ await locator.hover({ timeout, force: opts.force });
3136
3147
  await new Promise((resolve2) => setTimeout(resolve2, delayMs));
3137
3148
  }
3138
3149
  let ariaCheckedBefore;
@@ -3140,9 +3151,9 @@ async function clickViaPlaywright(opts) {
3140
3151
  ariaCheckedBefore = await locator.getAttribute("aria-checked", { timeout }).catch(() => void 0);
3141
3152
  }
3142
3153
  if (opts.doubleClick === true) {
3143
- await locator.dblclick({ timeout, button: opts.button, modifiers: opts.modifiers });
3154
+ await locator.dblclick({ timeout, button: opts.button, modifiers: opts.modifiers, force: opts.force });
3144
3155
  } else {
3145
- await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
3156
+ await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers, force: opts.force });
3146
3157
  }
3147
3158
  if (checkableRole && opts.doubleClick !== true && ariaCheckedBefore !== void 0) {
3148
3159
  const POLL_INTERVAL_MS = 50;
@@ -3236,7 +3247,7 @@ async function fillFormViaPlaywright(opts) {
3236
3247
  if (type === "checkbox" || type === "radio") {
3237
3248
  const checked = rawValue === true || rawValue === 1 || rawValue === "1" || rawValue === "true";
3238
3249
  try {
3239
- await locator.setChecked(checked, { timeout });
3250
+ await locator.setChecked(checked, { timeout, force: true });
3240
3251
  } catch (err) {
3241
3252
  throw toAIFriendlyError(err, ref);
3242
3253
  }
@@ -3255,7 +3266,13 @@ async function scrollIntoViewViaPlaywright(opts) {
3255
3266
  const label = resolved.ref ?? resolved.selector ?? "";
3256
3267
  const locator = resolveLocator(page, resolved);
3257
3268
  try {
3258
- await locator.scrollIntoViewIfNeeded({ timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4) });
3269
+ await locator.waitFor({
3270
+ state: "attached",
3271
+ timeout: normalizeTimeoutMs(opts.timeoutMs, DEFAULT_SCROLL_TIMEOUT_MS)
3272
+ });
3273
+ await locator.evaluate((el) => {
3274
+ el.scrollIntoView({ block: "center", behavior: "instant" });
3275
+ });
3259
3276
  } catch (err) {
3260
3277
  throw toAIFriendlyError(err, label);
3261
3278
  }
@@ -3531,9 +3548,13 @@ async function waitForViaPlaywright(opts) {
3531
3548
  if (opts.loadState !== void 0) {
3532
3549
  await page.waitForLoadState(opts.loadState, { timeout });
3533
3550
  }
3534
- if (opts.fn !== void 0 && opts.fn !== "") {
3535
- const fn = opts.fn.trim();
3536
- if (fn !== "") await page.waitForFunction(fn, void 0, { timeout });
3551
+ if (opts.fn !== void 0) {
3552
+ if (typeof opts.fn === "function") {
3553
+ await page.waitForFunction(opts.fn, { timeout });
3554
+ } else {
3555
+ const fn = opts.fn.trim();
3556
+ if (fn !== "") await page.waitForFunction(fn, void 0, { timeout });
3557
+ }
3537
3558
  }
3538
3559
  }
3539
3560
 
@@ -4243,6 +4264,13 @@ async function traceStopViaPlaywright(opts) {
4243
4264
  }
4244
4265
 
4245
4266
  // src/snapshot/ref-map.ts
4267
+ function parseStateFromSuffix(suffix) {
4268
+ const state = {};
4269
+ if (/\[disabled\]/i.test(suffix)) state.disabled = true;
4270
+ if (/\[checked\s*=\s*"?mixed"?\]/i.test(suffix)) state.checked = "mixed";
4271
+ else if (/\[checked\]/i.test(suffix)) state.checked = true;
4272
+ return state;
4273
+ }
4246
4274
  var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
4247
4275
  "button",
4248
4276
  "link",
@@ -4401,7 +4429,8 @@ function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
4401
4429
  const ref = nextRef();
4402
4430
  const nth = tracker.getNextIndex(role, name);
4403
4431
  tracker.trackRef(role, name, ref);
4404
- refs[ref] = { role, name, nth };
4432
+ const state = parseStateFromSuffix(suffix);
4433
+ refs[ref] = { role, name, nth, ...state };
4405
4434
  let enhanced = `${prefix}${roleRaw}`;
4406
4435
  if (name !== void 0 && name !== "") enhanced += ` "${name}"`;
4407
4436
  enhanced += ` [ref=${ref}]`;
@@ -4438,7 +4467,8 @@ function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
4438
4467
  const ref = nextRef();
4439
4468
  const nth = tracker.getNextIndex(role, name);
4440
4469
  tracker.trackRef(role, name, ref);
4441
- refs[ref] = { role, name, nth };
4470
+ const state = parseStateFromSuffix(suffix);
4471
+ refs[ref] = { role, name, nth, ...state };
4442
4472
  let enhanced = `${prefix}${roleRaw}`;
4443
4473
  if (name !== "") enhanced += ` "${name}"`;
4444
4474
  enhanced += ` [ref=${ref}]`;
@@ -4476,12 +4506,13 @@ function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
4476
4506
  if (!INTERACTIVE_ROLES.has(role)) continue;
4477
4507
  const ref = parseAiSnapshotRef(suffix);
4478
4508
  const prefix = /^(\s*-\s*)/.exec(line)?.[1] ?? "";
4509
+ const state = parseStateFromSuffix(suffix);
4479
4510
  if (ref !== null) {
4480
- refs[ref] = { role, ...name !== void 0 && name !== "" ? { name } : {} };
4511
+ refs[ref] = { role, ...name !== void 0 && name !== "" ? { name } : {}, ...state };
4481
4512
  out2.push(`${prefix}${roleRaw}${name !== void 0 && name !== "" ? ` "${name}"` : ""}${suffix}`);
4482
4513
  } else {
4483
4514
  const generatedRef = nextInteractiveRef();
4484
- refs[generatedRef] = { role, ...name !== void 0 && name !== "" ? { name } : {} };
4515
+ refs[generatedRef] = { role, ...name !== void 0 && name !== "" ? { name } : {}, ...state };
4485
4516
  let enhanced = `${prefix}${roleRaw}`;
4486
4517
  if (name !== void 0 && name !== "") enhanced += ` "${name}"`;
4487
4518
  enhanced += ` [ref=${generatedRef}]`;
@@ -4519,12 +4550,13 @@ function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
4519
4550
  const isStructural = STRUCTURAL_ROLES.has(role);
4520
4551
  if (options.compact === true && isStructural && name === "") continue;
4521
4552
  const ref = parseAiSnapshotRef(suffix);
4553
+ const state = parseStateFromSuffix(suffix);
4522
4554
  if (ref !== null) {
4523
- refs[ref] = { role, ...name !== "" ? { name } : {} };
4555
+ refs[ref] = { role, ...name !== "" ? { name } : {}, ...state };
4524
4556
  out.push(line);
4525
4557
  } else if (INTERACTIVE_ROLES.has(role)) {
4526
4558
  const generatedRef = nextGeneratedRef();
4527
- refs[generatedRef] = { role, ...name !== "" ? { name } : {} };
4559
+ refs[generatedRef] = { role, ...name !== "" ? { name } : {}, ...state };
4528
4560
  let enhanced = `${prefix}${roleRaw}`;
4529
4561
  if (name !== "") enhanced += ` "${name}"`;
4530
4562
  enhanced += ` [ref=${generatedRef}]`;
@@ -4890,7 +4922,8 @@ var CrawlPage = class {
4890
4922
  button: opts?.button,
4891
4923
  modifiers: opts?.modifiers,
4892
4924
  delayMs: opts?.delayMs,
4893
- timeoutMs: opts?.timeoutMs
4925
+ timeoutMs: opts?.timeoutMs,
4926
+ force: opts?.force
4894
4927
  });
4895
4928
  }
4896
4929
  /**
@@ -4916,7 +4949,8 @@ var CrawlPage = class {
4916
4949
  button: opts?.button,
4917
4950
  modifiers: opts?.modifiers,
4918
4951
  delayMs: opts?.delayMs,
4919
- timeoutMs: opts?.timeoutMs
4952
+ timeoutMs: opts?.timeoutMs,
4953
+ force: opts?.force
4920
4954
  });
4921
4955
  }
4922
4956
  /**
@@ -5019,6 +5053,7 @@ var CrawlPage = class {
5019
5053
  targetId: this.targetId,
5020
5054
  role,
5021
5055
  name,
5056
+ index: opts?.index,
5022
5057
  button: opts?.button,
5023
5058
  modifiers: opts?.modifiers,
5024
5059
  timeoutMs: opts?.timeoutMs