browserclaw 0.9.6 → 0.10.0

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.cjs CHANGED
@@ -1407,6 +1407,9 @@ async function launchChrome(opts = {}) {
1407
1407
  if (opts.noSandbox === true) {
1408
1408
  args.push("--no-sandbox", "--disable-setuid-sandbox");
1409
1409
  }
1410
+ if (opts.ignoreHTTPSErrors === true) {
1411
+ args.push("--ignore-certificate-errors");
1412
+ }
1410
1413
  if (process.platform === "linux") args.push("--disable-dev-shm-usage");
1411
1414
  const extraArgs = Array.isArray(opts.chromeArgs) ? opts.chromeArgs.filter((a) => typeof a === "string" && a.trim().length > 0) : [];
1412
1415
  if (extraArgs.length) args.push(...extraArgs);
@@ -1730,9 +1733,11 @@ async function withPlaywrightPageCdpSession(page, fn) {
1730
1733
  const CDP_SESSION_TIMEOUT_MS = 1e4;
1731
1734
  const session = await Promise.race([
1732
1735
  page.context().newCDPSession(page),
1733
- new Promise(
1734
- (_, reject) => setTimeout(() => reject(new Error("newCDPSession timed out after 10s")), CDP_SESSION_TIMEOUT_MS)
1735
- )
1736
+ new Promise((_, reject) => {
1737
+ setTimeout(() => {
1738
+ reject(new Error("newCDPSession timed out after 10s"));
1739
+ }, CDP_SESSION_TIMEOUT_MS);
1740
+ })
1736
1741
  ]);
1737
1742
  try {
1738
1743
  return await fn(session);
@@ -2373,7 +2378,14 @@ function toAIFriendlyError(error, selector) {
2373
2378
  `Element "${selector}" is not interactable (hidden or covered). Try scrolling it into view, closing overlays, or re-snapshotting.`
2374
2379
  );
2375
2380
  }
2376
- return error instanceof Error ? error : new Error(message);
2381
+ const timeoutMatch = /Timeout (\d+)ms exceeded/.exec(message);
2382
+ if (timeoutMatch) {
2383
+ return new Error(
2384
+ `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.`
2385
+ );
2386
+ }
2387
+ const cleaned = message.replace(/locator\([^)]*\)\./g, "").replace(/waiting for locator\([^)]*\)/g, "").trim();
2388
+ return new Error(cleaned || message);
2377
2389
  }
2378
2390
  function normalizeTimeoutMs(timeoutMs, fallback, maxMs = 12e4) {
2379
2391
  return Math.max(500, Math.min(maxMs, timeoutMs ?? fallback));
@@ -3071,6 +3083,7 @@ function requiresInspectableBrowserNavigationRedirects(ssrfPolicy) {
3071
3083
 
3072
3084
  // src/actions/interaction.ts
3073
3085
  var MAX_CLICK_DELAY_MS = 5e3;
3086
+ var DEFAULT_SCROLL_TIMEOUT_MS = 2e4;
3074
3087
  var CHECKABLE_ROLES = /* @__PURE__ */ new Set(["menuitemcheckbox", "menuitemradio", "checkbox", "switch"]);
3075
3088
  function resolveLocator(page, resolved) {
3076
3089
  if (resolved.ref !== void 0 && resolved.ref !== "") return refLocator(page, resolved.ref);
@@ -3085,38 +3098,29 @@ async function mouseClickViaPlaywright(opts) {
3085
3098
  delay: opts.delayMs
3086
3099
  });
3087
3100
  }
3088
- var MAX_HOLD_MS = 3e4;
3089
3101
  async function pressAndHoldViaCdp(opts) {
3090
- const holdMs = resolveBoundedDelayMs(opts.holdMs ?? 1e3, "holdMs", MAX_HOLD_MS);
3091
3102
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3092
3103
  ensurePageState(page);
3093
- const pos = { x: opts.x, y: opts.y };
3094
- const btn = { button: "left", clickCount: 1 };
3104
+ const { x, y } = opts;
3095
3105
  await withPageScopedCdpClient({
3096
3106
  cdpUrl: opts.cdpUrl,
3097
3107
  page,
3098
3108
  targetId: opts.targetId,
3099
3109
  fn: async (send) => {
3100
- await send("Input.dispatchMouseEvent", { type: "mouseMoved", ...pos });
3101
- await send("Input.dispatchTouchEvent", {
3102
- type: "touchStart",
3103
- touchPoints: [{ x: pos.x, y: pos.y, id: 1 }]
3104
- });
3105
- await send("Input.dispatchMouseEvent", { type: "mousePressed", ...pos, ...btn });
3106
- await new Promise((r) => setTimeout(r, holdMs));
3107
- await send("Input.dispatchMouseEvent", { type: "mouseReleased", ...pos, ...btn });
3108
- await send("Input.dispatchTouchEvent", {
3109
- type: "touchEnd",
3110
- touchPoints: [{ x: pos.x, y: pos.y, id: 1 }]
3111
- });
3110
+ await send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y, button: "none" });
3111
+ if (opts.delay !== void 0 && opts.delay !== 0) await new Promise((r) => setTimeout(r, opts.delay));
3112
+ await send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 });
3113
+ if (opts.holdMs !== void 0 && opts.holdMs !== 0) await new Promise((r) => setTimeout(r, opts.holdMs));
3114
+ await send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 });
3112
3115
  }
3113
3116
  });
3114
3117
  }
3115
3118
  async function clickByTextViaPlaywright(opts) {
3116
3119
  const page = await getRestoredPageForTarget(opts);
3117
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();
3118
3122
  try {
3119
- await page.getByText(opts.text, { exact: opts.exact }).click({ timeout, button: opts.button, modifiers: opts.modifiers });
3123
+ await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
3120
3124
  } catch (err) {
3121
3125
  throw toAIFriendlyError(err, `text="${opts.text}"`);
3122
3126
  }
@@ -3124,13 +3128,12 @@ async function clickByTextViaPlaywright(opts) {
3124
3128
  async function clickByRoleViaPlaywright(opts) {
3125
3129
  const page = await getRestoredPageForTarget(opts);
3126
3130
  const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
3131
+ const label = `role=${opts.role}${opts.name !== void 0 && opts.name !== "" ? ` name="${opts.name}"` : ""}`;
3132
+ const locator = page.getByRole(opts.role, { name: opts.name }).nth(opts.index ?? 0);
3127
3133
  try {
3128
- await page.getByRole(opts.role, { name: opts.name }).click({ timeout, button: opts.button, modifiers: opts.modifiers });
3134
+ await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
3129
3135
  } catch (err) {
3130
- throw toAIFriendlyError(
3131
- err,
3132
- `role=${opts.role}${opts.name !== void 0 && opts.name !== "" ? ` name="${opts.name}"` : ""}`
3133
- );
3136
+ throw toAIFriendlyError(err, label);
3134
3137
  }
3135
3138
  }
3136
3139
  async function clickViaPlaywright(opts) {
@@ -3151,7 +3154,7 @@ async function clickViaPlaywright(opts) {
3151
3154
  try {
3152
3155
  const delayMs = resolveBoundedDelayMs(opts.delayMs, "click delayMs", MAX_CLICK_DELAY_MS);
3153
3156
  if (delayMs > 0) {
3154
- await locator.hover({ timeout });
3157
+ await locator.hover({ timeout, force: opts.force });
3155
3158
  await new Promise((resolve2) => setTimeout(resolve2, delayMs));
3156
3159
  }
3157
3160
  let ariaCheckedBefore;
@@ -3159,9 +3162,9 @@ async function clickViaPlaywright(opts) {
3159
3162
  ariaCheckedBefore = await locator.getAttribute("aria-checked", { timeout }).catch(() => void 0);
3160
3163
  }
3161
3164
  if (opts.doubleClick === true) {
3162
- await locator.dblclick({ timeout, button: opts.button, modifiers: opts.modifiers });
3165
+ await locator.dblclick({ timeout, button: opts.button, modifiers: opts.modifiers, force: opts.force });
3163
3166
  } else {
3164
- await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
3167
+ await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers, force: opts.force });
3165
3168
  }
3166
3169
  if (checkableRole && opts.doubleClick !== true && ariaCheckedBefore !== void 0) {
3167
3170
  const POLL_INTERVAL_MS = 50;
@@ -3255,7 +3258,7 @@ async function fillFormViaPlaywright(opts) {
3255
3258
  if (type === "checkbox" || type === "radio") {
3256
3259
  const checked = rawValue === true || rawValue === 1 || rawValue === "1" || rawValue === "true";
3257
3260
  try {
3258
- await locator.setChecked(checked, { timeout });
3261
+ await locator.setChecked(checked, { timeout, force: true });
3259
3262
  } catch (err) {
3260
3263
  throw toAIFriendlyError(err, ref);
3261
3264
  }
@@ -3274,7 +3277,13 @@ async function scrollIntoViewViaPlaywright(opts) {
3274
3277
  const label = resolved.ref ?? resolved.selector ?? "";
3275
3278
  const locator = resolveLocator(page, resolved);
3276
3279
  try {
3277
- await locator.scrollIntoViewIfNeeded({ timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4) });
3280
+ await locator.waitFor({
3281
+ state: "attached",
3282
+ timeout: normalizeTimeoutMs(opts.timeoutMs, DEFAULT_SCROLL_TIMEOUT_MS)
3283
+ });
3284
+ await locator.evaluate((el) => {
3285
+ el.scrollIntoView({ block: "center", behavior: "instant" });
3286
+ });
3278
3287
  } catch (err) {
3279
3288
  throw toAIFriendlyError(err, label);
3280
3289
  }
@@ -3550,9 +3559,13 @@ async function waitForViaPlaywright(opts) {
3550
3559
  if (opts.loadState !== void 0) {
3551
3560
  await page.waitForLoadState(opts.loadState, { timeout });
3552
3561
  }
3553
- if (opts.fn !== void 0 && opts.fn !== "") {
3554
- const fn = opts.fn.trim();
3555
- if (fn !== "") await page.waitForFunction(fn, void 0, { timeout });
3562
+ if (opts.fn !== void 0) {
3563
+ if (typeof opts.fn === "function") {
3564
+ await page.waitForFunction(opts.fn, void 0, { timeout });
3565
+ } else {
3566
+ const fn = opts.fn.trim();
3567
+ if (fn !== "") await page.waitForFunction(fn, void 0, { timeout });
3568
+ }
3556
3569
  }
3557
3570
  }
3558
3571
 
@@ -4262,6 +4275,13 @@ async function traceStopViaPlaywright(opts) {
4262
4275
  }
4263
4276
 
4264
4277
  // src/snapshot/ref-map.ts
4278
+ function parseStateFromSuffix(suffix) {
4279
+ const state = {};
4280
+ if (/\[disabled\]/i.test(suffix)) state.disabled = true;
4281
+ if (/\[checked\s*=\s*"?mixed"?\]/i.test(suffix)) state.checked = "mixed";
4282
+ else if (/\[checked\]/i.test(suffix)) state.checked = true;
4283
+ return state;
4284
+ }
4265
4285
  var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
4266
4286
  "button",
4267
4287
  "link",
@@ -4420,7 +4440,8 @@ function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
4420
4440
  const ref = nextRef();
4421
4441
  const nth = tracker.getNextIndex(role, name);
4422
4442
  tracker.trackRef(role, name, ref);
4423
- refs[ref] = { role, name, nth };
4443
+ const state = parseStateFromSuffix(suffix);
4444
+ refs[ref] = { role, name, nth, ...state };
4424
4445
  let enhanced = `${prefix}${roleRaw}`;
4425
4446
  if (name !== void 0 && name !== "") enhanced += ` "${name}"`;
4426
4447
  enhanced += ` [ref=${ref}]`;
@@ -4457,7 +4478,8 @@ function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
4457
4478
  const ref = nextRef();
4458
4479
  const nth = tracker.getNextIndex(role, name);
4459
4480
  tracker.trackRef(role, name, ref);
4460
- refs[ref] = { role, name, nth };
4481
+ const state = parseStateFromSuffix(suffix);
4482
+ refs[ref] = { role, name, nth, ...state };
4461
4483
  let enhanced = `${prefix}${roleRaw}`;
4462
4484
  if (name !== "") enhanced += ` "${name}"`;
4463
4485
  enhanced += ` [ref=${ref}]`;
@@ -4495,12 +4517,13 @@ function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
4495
4517
  if (!INTERACTIVE_ROLES.has(role)) continue;
4496
4518
  const ref = parseAiSnapshotRef(suffix);
4497
4519
  const prefix = /^(\s*-\s*)/.exec(line)?.[1] ?? "";
4520
+ const state = parseStateFromSuffix(suffix);
4498
4521
  if (ref !== null) {
4499
- refs[ref] = { role, ...name !== void 0 && name !== "" ? { name } : {} };
4522
+ refs[ref] = { role, ...name !== void 0 && name !== "" ? { name } : {}, ...state };
4500
4523
  out2.push(`${prefix}${roleRaw}${name !== void 0 && name !== "" ? ` "${name}"` : ""}${suffix}`);
4501
4524
  } else {
4502
4525
  const generatedRef = nextInteractiveRef();
4503
- refs[generatedRef] = { role, ...name !== void 0 && name !== "" ? { name } : {} };
4526
+ refs[generatedRef] = { role, ...name !== void 0 && name !== "" ? { name } : {}, ...state };
4504
4527
  let enhanced = `${prefix}${roleRaw}`;
4505
4528
  if (name !== void 0 && name !== "") enhanced += ` "${name}"`;
4506
4529
  enhanced += ` [ref=${generatedRef}]`;
@@ -4538,12 +4561,13 @@ function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
4538
4561
  const isStructural = STRUCTURAL_ROLES.has(role);
4539
4562
  if (options.compact === true && isStructural && name === "") continue;
4540
4563
  const ref = parseAiSnapshotRef(suffix);
4564
+ const state = parseStateFromSuffix(suffix);
4541
4565
  if (ref !== null) {
4542
- refs[ref] = { role, ...name !== "" ? { name } : {} };
4566
+ refs[ref] = { role, ...name !== "" ? { name } : {}, ...state };
4543
4567
  out.push(line);
4544
4568
  } else if (INTERACTIVE_ROLES.has(role)) {
4545
4569
  const generatedRef = nextGeneratedRef();
4546
- refs[generatedRef] = { role, ...name !== "" ? { name } : {} };
4570
+ refs[generatedRef] = { role, ...name !== "" ? { name } : {}, ...state };
4547
4571
  let enhanced = `${prefix}${roleRaw}`;
4548
4572
  if (name !== "") enhanced += ` "${name}"`;
4549
4573
  enhanced += ` [ref=${generatedRef}]`;
@@ -4909,7 +4933,8 @@ var CrawlPage = class {
4909
4933
  button: opts?.button,
4910
4934
  modifiers: opts?.modifiers,
4911
4935
  delayMs: opts?.delayMs,
4912
- timeoutMs: opts?.timeoutMs
4936
+ timeoutMs: opts?.timeoutMs,
4937
+ force: opts?.force
4913
4938
  });
4914
4939
  }
4915
4940
  /**
@@ -4935,7 +4960,8 @@ var CrawlPage = class {
4935
4960
  button: opts?.button,
4936
4961
  modifiers: opts?.modifiers,
4937
4962
  delayMs: opts?.delayMs,
4938
- timeoutMs: opts?.timeoutMs
4963
+ timeoutMs: opts?.timeoutMs,
4964
+ force: opts?.force
4939
4965
  });
4940
4966
  }
4941
4967
  /**
@@ -4974,11 +5000,11 @@ var CrawlPage = class {
4974
5000
  *
4975
5001
  * @param x - X coordinate in CSS pixels
4976
5002
  * @param y - Y coordinate in CSS pixels
4977
- * @param opts - Options (holdMs: hold duration, default 1000, max 30000)
5003
+ * @param opts - Options (delay: ms before press, holdMs: hold duration)
4978
5004
  *
4979
5005
  * @example
4980
5006
  * ```ts
4981
- * await page.pressAndHold(400, 300, { holdMs: 5000 });
5007
+ * await page.pressAndHold(400, 300, { delay: 150, holdMs: 5000 });
4982
5008
  * ```
4983
5009
  */
4984
5010
  async pressAndHold(x, y, opts) {
@@ -4987,6 +5013,7 @@ var CrawlPage = class {
4987
5013
  targetId: this.targetId,
4988
5014
  x,
4989
5015
  y,
5016
+ delay: opts?.delay,
4990
5017
  holdMs: opts?.holdMs
4991
5018
  });
4992
5019
  }
@@ -5037,6 +5064,7 @@ var CrawlPage = class {
5037
5064
  targetId: this.targetId,
5038
5065
  role,
5039
5066
  name,
5067
+ index: opts?.index,
5040
5068
  button: opts?.button,
5041
5069
  modifiers: opts?.modifiers,
5042
5070
  timeoutMs: opts?.timeoutMs