browser-pilot 0.0.12 → 0.0.13

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/browser.cjs CHANGED
@@ -216,7 +216,7 @@ async function createCDPClient(wsUrl, options = {}) {
216
216
  throw new Error("CDP client is not connected");
217
217
  }
218
218
  const id = ++messageId;
219
- const effectiveSessionId = sessionId ?? currentSessionId;
219
+ const effectiveSessionId = sessionId === null ? void 0 : sessionId ?? currentSessionId;
220
220
  const request = { id, method };
221
221
  if (params !== void 0) {
222
222
  request.params = params;
@@ -1396,6 +1396,9 @@ var BatchExecutor = class {
1396
1396
  const snapshot = await this.page.snapshot();
1397
1397
  return { value: snapshot };
1398
1398
  }
1399
+ case "forms": {
1400
+ return { value: await this.page.forms() };
1401
+ }
1399
1402
  case "screenshot": {
1400
1403
  const data = await this.page.screenshot({
1401
1404
  format: step.format,
@@ -1415,6 +1418,21 @@ var BatchExecutor = class {
1415
1418
  const text = await this.page.text(selector);
1416
1419
  return { text, selectorUsed: selector };
1417
1420
  }
1421
+ case "newTab": {
1422
+ const { targetId } = await this.page.cdpClient.send(
1423
+ "Target.createTarget",
1424
+ {
1425
+ url: step.url ?? "about:blank"
1426
+ },
1427
+ null
1428
+ );
1429
+ return { value: { targetId } };
1430
+ }
1431
+ case "closeTab": {
1432
+ const targetId = step.targetId ?? this.page.targetId;
1433
+ await this.page.cdpClient.send("Target.closeTarget", { targetId }, null);
1434
+ return { value: { targetId, closedCurrent: targetId === this.page.targetId } };
1435
+ }
1418
1436
  case "switchFrame": {
1419
1437
  if (!step.selector) throw new Error("switchFrame requires selector");
1420
1438
  await this.page.switchToFrame(step.selector, { timeout, optional });
@@ -1529,10 +1547,15 @@ var BatchExecutor = class {
1529
1547
  snap: "snapshot",
1530
1548
  accessibility: "snapshot",
1531
1549
  a11y: "snapshot",
1550
+ formslist: "forms",
1532
1551
  image: "screenshot",
1533
1552
  pic: "screenshot",
1534
1553
  frame: "switchFrame",
1535
1554
  iframe: "switchFrame",
1555
+ newtab: "newTab",
1556
+ opentab: "newTab",
1557
+ createtab: "newTab",
1558
+ closetab: "closeTab",
1536
1559
  assert_visible: "assertVisible",
1537
1560
  assert_exists: "assertExists",
1538
1561
  assert_text: "assertText",
@@ -1546,7 +1569,7 @@ var BatchExecutor = class {
1546
1569
  };
1547
1570
  const suggestion = aliases[action.toLowerCase()];
1548
1571
  const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
1549
- const valid = "goto, click, fill, type, select, check, uncheck, submit, press, shortcut, focus, hover, scroll, wait, snapshot, screenshot, evaluate, text, switchFrame, switchToMain, assertVisible, assertExists, assertText, assertUrl, assertValue";
1572
+ const valid = "goto, click, fill, type, select, check, uncheck, submit, press, shortcut, focus, hover, scroll, wait, snapshot, forms, screenshot, evaluate, text, newTab, closeTab, switchFrame, switchToMain, assertVisible, assertExists, assertText, assertUrl, assertValue";
1550
1573
  throw new Error(`Unknown action "${action}".${hint}
1551
1574
 
1552
1575
  Valid actions: ${valid}`);
@@ -2964,6 +2987,285 @@ var RequestInterceptor = class {
2964
2987
  }
2965
2988
  };
2966
2989
 
2990
+ // src/browser/special-selectors.ts
2991
+ function stripQuotes(value) {
2992
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
2993
+ return value.slice(1, -1);
2994
+ }
2995
+ return value;
2996
+ }
2997
+ function parseTextSelector(selector) {
2998
+ if (!selector.startsWith("text:")) return null;
2999
+ let raw = selector.slice(5).trim();
3000
+ let exact = false;
3001
+ if (raw.startsWith("=")) {
3002
+ exact = true;
3003
+ raw = raw.slice(1).trim();
3004
+ }
3005
+ const query = stripQuotes(raw);
3006
+ if (!query) return null;
3007
+ return { query, exact };
3008
+ }
3009
+ function parseRoleSelector(selector) {
3010
+ if (!selector.startsWith("role:")) return null;
3011
+ const body = selector.slice(5);
3012
+ const separator = body.indexOf(":");
3013
+ const role = (separator === -1 ? body : body.slice(0, separator)).trim().toLowerCase();
3014
+ const name = separator === -1 ? void 0 : stripQuotes(body.slice(separator + 1).trim());
3015
+ if (!role) return null;
3016
+ return { role, name: name || void 0 };
3017
+ }
3018
+ var SPECIAL_SELECTOR_SCRIPT = `
3019
+ function bpNormalizeSpace(value) {
3020
+ return String(value == null ? '' : value).replace(/\\s+/g, ' ').trim();
3021
+ }
3022
+
3023
+ function bpCollectElements(root) {
3024
+ var elements = [];
3025
+
3026
+ function visit(node) {
3027
+ if (!node || typeof node.querySelectorAll !== 'function') return;
3028
+ var matches = node.querySelectorAll('*');
3029
+ for (var i = 0; i < matches.length; i++) {
3030
+ var el = matches[i];
3031
+ elements.push(el);
3032
+ if (el.shadowRoot) {
3033
+ visit(el.shadowRoot);
3034
+ }
3035
+ }
3036
+ }
3037
+
3038
+ if (root && root.documentElement) {
3039
+ elements.push(root.documentElement);
3040
+ }
3041
+
3042
+ visit(root);
3043
+ return elements;
3044
+ }
3045
+
3046
+ function bpIsVisible(el) {
3047
+ if (!el) return false;
3048
+ var style = getComputedStyle(el);
3049
+ if (style.display === 'none') return false;
3050
+ if (style.visibility === 'hidden') return false;
3051
+ if (parseFloat(style.opacity || '1') === 0) return false;
3052
+ var rect = el.getBoundingClientRect();
3053
+ return rect.width > 0 && rect.height > 0;
3054
+ }
3055
+
3056
+ function bpInferRole(el) {
3057
+ if (!el || !el.tagName) return '';
3058
+
3059
+ var explicitRole = bpNormalizeSpace(el.getAttribute && el.getAttribute('role'));
3060
+ if (explicitRole) return explicitRole.toLowerCase();
3061
+
3062
+ var tag = el.tagName.toLowerCase();
3063
+ if (tag === 'button') return 'button';
3064
+ if (tag === 'a' && el.hasAttribute('href')) return 'link';
3065
+ if (tag === 'textarea') return 'textbox';
3066
+ if (tag === 'select') return el.multiple ? 'listbox' : 'combobox';
3067
+ if (tag === 'option') return 'option';
3068
+ if (tag === 'summary') return 'button';
3069
+
3070
+ if (tag === 'input') {
3071
+ var type = (el.type || 'text').toLowerCase();
3072
+ if (type === 'checkbox') return 'checkbox';
3073
+ if (type === 'radio') return 'radio';
3074
+ if (type === 'search') return 'searchbox';
3075
+ if (type === 'number') return 'spinbutton';
3076
+ if (type === 'button' || type === 'submit' || type === 'reset' || type === 'image') {
3077
+ return 'button';
3078
+ }
3079
+ return 'textbox';
3080
+ }
3081
+
3082
+ return '';
3083
+ }
3084
+
3085
+ function bpTextFromIdRefs(refs) {
3086
+ if (!refs) return '';
3087
+ var ids = refs.split(/\\s+/).filter(Boolean);
3088
+ var parts = [];
3089
+ for (var i = 0; i < ids.length; i++) {
3090
+ var node = document.getElementById(ids[i]);
3091
+ if (!node) continue;
3092
+ var text = bpNormalizeSpace(node.innerText || node.textContent || '');
3093
+ if (text) parts.push(text);
3094
+ }
3095
+ return bpNormalizeSpace(parts.join(' '));
3096
+ }
3097
+
3098
+ function bpAccessibleName(el) {
3099
+ if (!el) return '';
3100
+
3101
+ var labelledBy = bpTextFromIdRefs(el.getAttribute && el.getAttribute('aria-labelledby'));
3102
+ if (labelledBy) return labelledBy;
3103
+
3104
+ var ariaLabel = bpNormalizeSpace(el.getAttribute && el.getAttribute('aria-label'));
3105
+ if (ariaLabel) return ariaLabel;
3106
+
3107
+ if (el.labels && el.labels.length) {
3108
+ var labels = [];
3109
+ for (var i = 0; i < el.labels.length; i++) {
3110
+ var labelText = bpNormalizeSpace(el.labels[i].innerText || el.labels[i].textContent || '');
3111
+ if (labelText) labels.push(labelText);
3112
+ }
3113
+ if (labels.length) return bpNormalizeSpace(labels.join(' '));
3114
+ }
3115
+
3116
+ if (el.id) {
3117
+ var fallbackLabel = document.querySelector('label[for="' + el.id.replace(/"/g, '\\\\"') + '"]');
3118
+ if (fallbackLabel) {
3119
+ var fallbackText = bpNormalizeSpace(
3120
+ fallbackLabel.innerText || fallbackLabel.textContent || ''
3121
+ );
3122
+ if (fallbackText) return fallbackText;
3123
+ }
3124
+ }
3125
+
3126
+ var type = (el.type || '').toLowerCase();
3127
+ if (
3128
+ el.tagName === 'INPUT' &&
3129
+ (type === 'submit' || type === 'button' || type === 'reset' || type === 'image')
3130
+ ) {
3131
+ var inputValue = bpNormalizeSpace(el.value || el.getAttribute('value'));
3132
+ if (inputValue) return inputValue;
3133
+ }
3134
+
3135
+ var alt = bpNormalizeSpace(el.getAttribute && el.getAttribute('alt'));
3136
+ if (alt) return alt;
3137
+
3138
+ var text = bpNormalizeSpace(el.innerText || el.textContent || '');
3139
+ if (text) return text;
3140
+
3141
+ var placeholder = bpNormalizeSpace(el.getAttribute && el.getAttribute('placeholder'));
3142
+ if (placeholder) return placeholder;
3143
+
3144
+ var title = bpNormalizeSpace(el.getAttribute && el.getAttribute('title'));
3145
+ if (title) return title;
3146
+
3147
+ var value = bpNormalizeSpace(el.value);
3148
+ if (value) return value;
3149
+
3150
+ return bpNormalizeSpace(el.name || el.id || '');
3151
+ }
3152
+
3153
+ function bpIsInteractive(role, el) {
3154
+ if (
3155
+ role === 'button' ||
3156
+ role === 'link' ||
3157
+ role === 'textbox' ||
3158
+ role === 'checkbox' ||
3159
+ role === 'radio' ||
3160
+ role === 'combobox' ||
3161
+ role === 'listbox' ||
3162
+ role === 'option' ||
3163
+ role === 'searchbox' ||
3164
+ role === 'spinbutton' ||
3165
+ role === 'switch' ||
3166
+ role === 'tab'
3167
+ ) {
3168
+ return true;
3169
+ }
3170
+
3171
+ if (!el || !el.tagName) return false;
3172
+ var tag = el.tagName.toLowerCase();
3173
+ return tag === 'button' || tag === 'a' || tag === 'input' || tag === 'select' || tag === 'textarea';
3174
+ }
3175
+
3176
+ function bpFindByText(query, exact, includeHidden) {
3177
+ var needle = bpNormalizeSpace(query).toLowerCase();
3178
+ if (!needle) return null;
3179
+
3180
+ var best = null;
3181
+ var bestScore = -1;
3182
+ var elements = bpCollectElements(document);
3183
+
3184
+ for (var i = 0; i < elements.length; i++) {
3185
+ var el = elements[i];
3186
+ if (!includeHidden && !bpIsVisible(el)) continue;
3187
+
3188
+ var text = bpAccessibleName(el);
3189
+ if (!text) continue;
3190
+
3191
+ var haystack = text.toLowerCase();
3192
+ var matched = exact ? haystack === needle : haystack.includes(needle);
3193
+ if (!matched) continue;
3194
+
3195
+ var role = bpInferRole(el);
3196
+ var score = 0;
3197
+ if (bpIsInteractive(role, el)) score += 100;
3198
+ if (haystack === needle) score += 50;
3199
+ if (role === 'button' || role === 'link') score += 10;
3200
+
3201
+ if (score > bestScore) {
3202
+ best = el;
3203
+ bestScore = score;
3204
+ }
3205
+ }
3206
+
3207
+ return best;
3208
+ }
3209
+
3210
+ function bpFindByRole(role, name, includeHidden) {
3211
+ var targetRole = bpNormalizeSpace(role).toLowerCase();
3212
+ if (!targetRole) return null;
3213
+
3214
+ var nameNeedle = bpNormalizeSpace(name).toLowerCase();
3215
+ var best = null;
3216
+ var bestScore = -1;
3217
+ var elements = bpCollectElements(document);
3218
+
3219
+ for (var i = 0; i < elements.length; i++) {
3220
+ var el = elements[i];
3221
+ if (!includeHidden && !bpIsVisible(el)) continue;
3222
+
3223
+ var actualRole = bpInferRole(el);
3224
+ if (actualRole !== targetRole) continue;
3225
+
3226
+ var accessibleName = bpAccessibleName(el);
3227
+ if (nameNeedle) {
3228
+ var loweredName = accessibleName.toLowerCase();
3229
+ if (!loweredName.includes(nameNeedle)) continue;
3230
+ }
3231
+
3232
+ var score = 0;
3233
+ if (accessibleName) score += 10;
3234
+ if (nameNeedle && accessibleName.toLowerCase() === nameNeedle) score += 20;
3235
+
3236
+ if (score > bestScore) {
3237
+ best = el;
3238
+ bestScore = score;
3239
+ }
3240
+ }
3241
+
3242
+ return best;
3243
+ }
3244
+ `;
3245
+ function buildSpecialSelectorLookupExpression(selector, options = {}) {
3246
+ const includeHidden = options.includeHidden === true;
3247
+ const text = parseTextSelector(selector);
3248
+ if (text) {
3249
+ return `(() => {
3250
+ ${SPECIAL_SELECTOR_SCRIPT}
3251
+ return bpFindByText(${JSON.stringify(text.query)}, ${text.exact}, ${includeHidden});
3252
+ })()`;
3253
+ }
3254
+ const role = parseRoleSelector(selector);
3255
+ if (role) {
3256
+ return `(() => {
3257
+ ${SPECIAL_SELECTOR_SCRIPT}
3258
+ return bpFindByRole(${JSON.stringify(role.role)}, ${JSON.stringify(role.name ?? "")}, ${includeHidden});
3259
+ })()`;
3260
+ }
3261
+ return null;
3262
+ }
3263
+ function buildSpecialSelectorPredicateExpression(selector, options = {}) {
3264
+ const lookup = buildSpecialSelectorLookupExpression(selector, options);
3265
+ if (!lookup) return null;
3266
+ return `(() => !!(${lookup}))()`;
3267
+ }
3268
+
2967
3269
  // src/wait/strategies.ts
2968
3270
  var DEEP_QUERY_SCRIPT = `
2969
3271
  function deepQuery(selector, root = document) {
@@ -2997,18 +3299,19 @@ function deepQuery(selector, root = document) {
2997
3299
  }
2998
3300
  `;
2999
3301
  async function isElementVisible(cdp, selector, contextId) {
3302
+ const specialExpression = buildSpecialSelectorPredicateExpression(selector);
3000
3303
  const params = {
3001
- expression: `(() => {
3002
- ${DEEP_QUERY_SCRIPT}
3003
- const el = deepQuery(${JSON.stringify(selector)});
3004
- if (!el) return false;
3005
- const style = getComputedStyle(el);
3006
- if (style.display === 'none') return false;
3007
- if (style.visibility === 'hidden') return false;
3008
- if (parseFloat(style.opacity) === 0) return false;
3009
- const rect = el.getBoundingClientRect();
3010
- return rect.width > 0 && rect.height > 0;
3011
- })()`,
3304
+ expression: specialExpression ?? `(() => {
3305
+ ${DEEP_QUERY_SCRIPT}
3306
+ const el = deepQuery(${JSON.stringify(selector)});
3307
+ if (!el) return false;
3308
+ const style = getComputedStyle(el);
3309
+ if (style.display === 'none') return false;
3310
+ if (style.visibility === 'hidden') return false;
3311
+ if (parseFloat(style.opacity) === 0) return false;
3312
+ const rect = el.getBoundingClientRect();
3313
+ return rect.width > 0 && rect.height > 0;
3314
+ })()`,
3012
3315
  returnByValue: true
3013
3316
  };
3014
3317
  if (contextId !== void 0) {
@@ -3018,11 +3321,14 @@ async function isElementVisible(cdp, selector, contextId) {
3018
3321
  return result.result.value === true;
3019
3322
  }
3020
3323
  async function isElementAttached(cdp, selector, contextId) {
3324
+ const specialExpression = buildSpecialSelectorPredicateExpression(selector, {
3325
+ includeHidden: true
3326
+ });
3021
3327
  const params = {
3022
- expression: `(() => {
3023
- ${DEEP_QUERY_SCRIPT}
3024
- return deepQuery(${JSON.stringify(selector)}) !== null;
3025
- })()`,
3328
+ expression: specialExpression ?? `(() => {
3329
+ ${DEEP_QUERY_SCRIPT}
3330
+ return deepQuery(${JSON.stringify(selector)}) !== null;
3331
+ })()`,
3026
3332
  returnByValue: true
3027
3333
  };
3028
3334
  if (contextId !== void 0) {
@@ -3642,6 +3948,9 @@ var Page = class {
3642
3948
  timeout: options.timeout ?? DEFAULT_TIMEOUT2
3643
3949
  });
3644
3950
  } catch (e) {
3951
+ if (e instanceof ActionabilityError && e.failureType === "hitTarget" && await this.tryClickAssociatedLabel(objectId)) {
3952
+ return true;
3953
+ }
3645
3954
  if (options.optional) return false;
3646
3955
  throw e;
3647
3956
  }
@@ -4030,7 +4339,12 @@ var Page = class {
4030
4339
  returnByValue: true
4031
4340
  });
4032
4341
  if (!after.result.value) {
4033
- throw new Error("Clicking the checkbox did not change its state");
4342
+ if (await this.tryToggleViaLabel(object.objectId, true)) {
4343
+ return true;
4344
+ }
4345
+ throw new Error(
4346
+ "Clicking the checkbox did not change its state. Tried the associated label too."
4347
+ );
4034
4348
  }
4035
4349
  return true;
4036
4350
  });
@@ -4082,7 +4396,12 @@ var Page = class {
4082
4396
  returnByValue: true
4083
4397
  });
4084
4398
  if (after.result.value) {
4085
- throw new Error("Clicking the checkbox did not change its state");
4399
+ if (await this.tryToggleViaLabel(object.objectId, false)) {
4400
+ return true;
4401
+ }
4402
+ throw new Error(
4403
+ "Clicking the checkbox did not change its state. Tried the associated label too."
4404
+ );
4086
4405
  }
4087
4406
  return true;
4088
4407
  });
@@ -4406,7 +4725,7 @@ var Page = class {
4406
4725
  }
4407
4726
  const result = await this.cdp.send("Runtime.evaluate", params);
4408
4727
  if (result.exceptionDetails) {
4409
- throw new Error(`Evaluation failed: ${result.exceptionDetails.text}`);
4728
+ throw new Error(this.formatEvaluationError(result.exceptionDetails));
4410
4729
  }
4411
4730
  return result.result.value;
4412
4731
  }
@@ -4458,6 +4777,75 @@ var Page = class {
4458
4777
  return result.result.value ?? "";
4459
4778
  });
4460
4779
  }
4780
+ /**
4781
+ * Enumerate form controls on the page with labels and current state.
4782
+ */
4783
+ async forms() {
4784
+ const result = await this.evaluateInFrame(
4785
+ `(() => {
4786
+ function normalize(value) {
4787
+ return String(value == null ? '' : value).replace(/\\s+/g, ' ').trim();
4788
+ }
4789
+
4790
+ function labelFor(el) {
4791
+ if (!el) return '';
4792
+ if (el.labels && el.labels.length) {
4793
+ return normalize(
4794
+ Array.from(el.labels)
4795
+ .map((label) => label.innerText || label.textContent || '')
4796
+ .join(' ')
4797
+ );
4798
+ }
4799
+ var ariaLabel = normalize(el.getAttribute && el.getAttribute('aria-label'));
4800
+ if (ariaLabel) return ariaLabel;
4801
+ if (el.id) {
4802
+ var byFor = document.querySelector('label[for="' + el.id.replace(/"/g, '\\\\"') + '"]');
4803
+ if (byFor) return normalize(byFor.innerText || byFor.textContent || '');
4804
+ }
4805
+ var closest = el.closest && el.closest('label');
4806
+ if (closest) return normalize(closest.innerText || closest.textContent || '');
4807
+ return '';
4808
+ }
4809
+
4810
+ return Array.from(document.querySelectorAll('input, select, textarea')).map((el) => {
4811
+ var tag = el.tagName.toLowerCase();
4812
+ var type = tag === 'input' ? (el.type || 'text').toLowerCase() : tag;
4813
+ var value = null;
4814
+
4815
+ if (tag === 'select') {
4816
+ value = el.multiple
4817
+ ? Array.from(el.selectedOptions).map((opt) => opt.value)
4818
+ : el.value || null;
4819
+ } else if (tag === 'textarea' || tag === 'input') {
4820
+ value = typeof el.value === 'string' ? el.value : null;
4821
+ }
4822
+
4823
+ return {
4824
+ tag: tag,
4825
+ type: type,
4826
+ id: el.id || undefined,
4827
+ name: el.getAttribute('name') || undefined,
4828
+ value: value,
4829
+ checked: 'checked' in el ? !!el.checked : undefined,
4830
+ required: !!el.required,
4831
+ disabled: !!el.disabled,
4832
+ label: labelFor(el) || undefined,
4833
+ placeholder: normalize(el.getAttribute && el.getAttribute('placeholder')) || undefined,
4834
+ options:
4835
+ tag === 'select'
4836
+ ? Array.from(el.options).map((opt) => ({
4837
+ value: opt.value || '',
4838
+ text: normalize(opt.text || opt.label || ''),
4839
+ selected: !!opt.selected,
4840
+ disabled: !!opt.disabled,
4841
+ }))
4842
+ : undefined,
4843
+ };
4844
+ });
4845
+ })()`
4846
+ );
4847
+ return result.result.value ?? [];
4848
+ }
4461
4849
  // ============ File Handling ============
4462
4850
  /**
4463
4851
  * Set files on a file input
@@ -4930,7 +5318,8 @@ var Page = class {
4930
5318
  /**
4931
5319
  * Get an accessibility tree snapshot of the page
4932
5320
  */
4933
- async snapshot() {
5321
+ async snapshot(options = {}) {
5322
+ const roleFilter = new Set((options.roles ?? []).map((role) => role.trim().toLowerCase()));
4934
5323
  const [url, title, axTree] = await Promise.all([
4935
5324
  this.url(),
4936
5325
  this.title(),
@@ -4951,7 +5340,7 @@ var Page = class {
4951
5340
  const buildNode = (nodeId) => {
4952
5341
  const node = nodeMap.get(nodeId);
4953
5342
  if (!node) return null;
4954
- const role = node.role?.value ?? "generic";
5343
+ const role = (node.role?.value ?? "generic").toLowerCase();
4955
5344
  const name = node.name?.value;
4956
5345
  const value = node.value?.value;
4957
5346
  const ref = nodeRefs.get(nodeId);
@@ -4967,7 +5356,7 @@ var Page = class {
4967
5356
  return {
4968
5357
  role,
4969
5358
  name,
4970
- value,
5359
+ value: value !== void 0 ? String(value) : void 0,
4971
5360
  ref,
4972
5361
  children: children.length > 0 ? children : void 0,
4973
5362
  disabled,
@@ -4975,7 +5364,24 @@ var Page = class {
4975
5364
  };
4976
5365
  };
4977
5366
  const rootNodes = nodes.filter((n) => !n.parentId || !nodeMap.has(n.parentId));
4978
- const accessibilityTree = rootNodes.map((n) => buildNode(n.nodeId)).filter((n) => n !== null);
5367
+ let accessibilityTree = rootNodes.map((n) => buildNode(n.nodeId)).filter((n) => n !== null);
5368
+ if (roleFilter.size > 0) {
5369
+ const filteredAccessibilityTree = [];
5370
+ for (const node of nodes) {
5371
+ if (!roleFilter.has((node.role?.value ?? "generic").toLowerCase())) {
5372
+ continue;
5373
+ }
5374
+ const snapshotNode = buildNode(node.nodeId);
5375
+ if (!snapshotNode) {
5376
+ continue;
5377
+ }
5378
+ filteredAccessibilityTree.push({
5379
+ ...snapshotNode,
5380
+ children: void 0
5381
+ });
5382
+ }
5383
+ accessibilityTree = filteredAccessibilityTree;
5384
+ }
4979
5385
  const interactiveRoles = /* @__PURE__ */ new Set([
4980
5386
  "button",
4981
5387
  "link",
@@ -4997,37 +5403,44 @@ var Page = class {
4997
5403
  ]);
4998
5404
  const interactiveElements = [];
4999
5405
  for (const node of nodes) {
5000
- const role = node.role?.value;
5001
- if (role && interactiveRoles.has(role)) {
5406
+ const role = (node.role?.value ?? "").toLowerCase();
5407
+ if (role && interactiveRoles.has(role) && (roleFilter.size === 0 || roleFilter.has(role))) {
5002
5408
  const ref = nodeRefs.get(node.nodeId);
5003
5409
  const name = node.name?.value ?? "";
5004
5410
  const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
5411
+ const checked = node.properties?.find((p) => p.name === "checked")?.value.value;
5412
+ const value = node.value?.value;
5005
5413
  const selector = node.backendDOMNodeId ? `[data-backend-node-id="${node.backendDOMNodeId}"]` : `[aria-label="${name}"]`;
5006
5414
  interactiveElements.push({
5007
5415
  ref,
5008
5416
  role,
5009
5417
  name,
5010
5418
  selector,
5011
- disabled
5419
+ disabled,
5420
+ checked,
5421
+ value: value !== void 0 ? String(value) : void 0
5012
5422
  });
5013
5423
  }
5014
5424
  }
5425
+ const formatNode = (node, depth = 0) => {
5426
+ let line = `${" ".repeat(depth)}- ${node.role}`;
5427
+ if (node.name) line += ` "${node.name}"`;
5428
+ line += ` ref:${node.ref}`;
5429
+ if (node.disabled) line += " (disabled)";
5430
+ if (node.checked !== void 0) line += node.checked ? " (checked)" : " (unchecked)";
5431
+ return line;
5432
+ };
5015
5433
  const formatTree = (nodes2, depth = 0) => {
5016
5434
  const lines = [];
5017
5435
  for (const node of nodes2) {
5018
- let line = `${" ".repeat(depth)}- ${node.role}`;
5019
- if (node.name) line += ` "${node.name}"`;
5020
- line += ` [ref=${node.ref}]`;
5021
- if (node.disabled) line += " (disabled)";
5022
- if (node.checked !== void 0) line += node.checked ? " (checked)" : " (unchecked)";
5023
- lines.push(line);
5436
+ lines.push(formatNode(node, depth));
5024
5437
  if (node.children) {
5025
5438
  lines.push(formatTree(node.children, depth + 1));
5026
5439
  }
5027
5440
  }
5028
5441
  return lines.join("\n");
5029
5442
  };
5030
- const text = formatTree(accessibilityTree);
5443
+ const text = roleFilter.size > 0 ? accessibilityTree.map((node) => formatNode(node)).join("\n") : formatTree(accessibilityTree);
5031
5444
  const result = {
5032
5445
  url,
5033
5446
  title,
@@ -5036,7 +5449,9 @@ var Page = class {
5036
5449
  interactiveElements,
5037
5450
  text
5038
5451
  };
5039
- this.lastSnapshot = result;
5452
+ if (roleFilter.size === 0) {
5453
+ this.lastSnapshot = result;
5454
+ }
5040
5455
  return result;
5041
5456
  }
5042
5457
  /**
@@ -5591,7 +6006,7 @@ var Page = class {
5591
6006
  }
5592
6007
  /**
5593
6008
  * Find an element using single or multiple selectors
5594
- * Supports ref: prefix for ref-based selectors (e.g., "ref:e4")
6009
+ * Supports ref:, text:, and role: selectors.
5595
6010
  */
5596
6011
  async findElement(selectors, options = {}) {
5597
6012
  const { timeout = DEFAULT_TIMEOUT2 } = options;
@@ -5658,11 +6073,11 @@ var Page = class {
5658
6073
  }
5659
6074
  }
5660
6075
  }
5661
- const cssSelectors = selectorList.filter((s) => !s.startsWith("ref:"));
5662
- if (cssSelectors.length === 0) {
6076
+ const runtimeSelectors = selectorList.filter((s) => !s.startsWith("ref:"));
6077
+ if (runtimeSelectors.length === 0) {
5663
6078
  return null;
5664
6079
  }
5665
- const result = await waitForAnyElement(this.cdp, cssSelectors, {
6080
+ const result = await waitForAnyElement(this.cdp, runtimeSelectors, {
5666
6081
  state: "visible",
5667
6082
  timeout,
5668
6083
  contextId: this.currentFrameContextId ?? void 0
@@ -5670,6 +6085,14 @@ var Page = class {
5670
6085
  if (!result.success || !result.selector) {
5671
6086
  return null;
5672
6087
  }
6088
+ const specialSelectorMatch = await this.resolveSpecialSelector(result.selector);
6089
+ if (specialSelectorMatch) {
6090
+ this._lastMatchedSelector = result.selector;
6091
+ return {
6092
+ ...specialSelectorMatch,
6093
+ waitedMs: result.waitedMs
6094
+ };
6095
+ }
5673
6096
  await this.ensureRootNode();
5674
6097
  const queryResult = await this.cdp.send("DOM.querySelector", {
5675
6098
  nodeId: this.rootNodeId,
@@ -5716,6 +6139,122 @@ var Page = class {
5716
6139
  waitedMs: result.waitedMs
5717
6140
  };
5718
6141
  }
6142
+ formatEvaluationError(details) {
6143
+ const description = typeof details.exception?.description === "string" && details.exception.description || typeof details.exception?.value === "string" && details.exception.value || details.text || "Uncaught";
6144
+ return `Evaluation failed: ${description}`;
6145
+ }
6146
+ async resolveSpecialSelector(selector, options = {}) {
6147
+ const expression = buildSpecialSelectorLookupExpression(selector, options);
6148
+ if (!expression) return null;
6149
+ const result = await this.evaluateInFrame(expression, {
6150
+ returnByValue: false
6151
+ });
6152
+ if (!result.result.objectId) {
6153
+ return null;
6154
+ }
6155
+ const resolved = await this.objectIdToNode(result.result.objectId);
6156
+ if (!resolved) {
6157
+ return null;
6158
+ }
6159
+ return {
6160
+ nodeId: resolved.nodeId,
6161
+ backendNodeId: resolved.backendNodeId,
6162
+ selector,
6163
+ waitedMs: 0
6164
+ };
6165
+ }
6166
+ async readCheckedState(objectId) {
6167
+ const result = await this.cdp.send("Runtime.callFunctionOn", {
6168
+ objectId,
6169
+ functionDeclaration: "function() { return !!this.checked; }",
6170
+ returnByValue: true
6171
+ });
6172
+ return result.result.value === true;
6173
+ }
6174
+ async readInputType(objectId) {
6175
+ const result = await this.cdp.send(
6176
+ "Runtime.callFunctionOn",
6177
+ {
6178
+ objectId,
6179
+ functionDeclaration: 'function() { return this instanceof HTMLInputElement ? String(this.type || "").toLowerCase() : null; }',
6180
+ returnByValue: true
6181
+ }
6182
+ );
6183
+ return result.result.value ?? null;
6184
+ }
6185
+ async getAssociatedLabelNodeId(objectId) {
6186
+ const result = await this.cdp.send("Runtime.callFunctionOn", {
6187
+ objectId,
6188
+ functionDeclaration: `function() {
6189
+ if (!(this instanceof HTMLInputElement)) return null;
6190
+
6191
+ if (this.id) {
6192
+ var labels = Array.from(document.querySelectorAll('label'));
6193
+ for (var i = 0; i < labels.length; i++) {
6194
+ if (labels[i].htmlFor === this.id) return labels[i];
6195
+ }
6196
+ }
6197
+
6198
+ return this.closest('label');
6199
+ }`,
6200
+ returnByValue: false
6201
+ });
6202
+ if (!result.result.objectId) {
6203
+ return null;
6204
+ }
6205
+ return (await this.objectIdToNode(result.result.objectId))?.nodeId ?? null;
6206
+ }
6207
+ async objectIdToNode(objectId) {
6208
+ const describeResult = await this.cdp.send("DOM.describeNode", {
6209
+ objectId,
6210
+ depth: 0
6211
+ });
6212
+ const backendNodeId = describeResult.node.backendNodeId;
6213
+ if (!backendNodeId) {
6214
+ return null;
6215
+ }
6216
+ if (describeResult.node.nodeId) {
6217
+ return {
6218
+ nodeId: describeResult.node.nodeId,
6219
+ backendNodeId
6220
+ };
6221
+ }
6222
+ await this.ensureRootNode();
6223
+ const pushResult = await this.cdp.send(
6224
+ "DOM.pushNodesByBackendIdsToFrontend",
6225
+ {
6226
+ backendNodeIds: [backendNodeId]
6227
+ }
6228
+ );
6229
+ const nodeId = pushResult.nodeIds?.[0];
6230
+ if (!nodeId) {
6231
+ return null;
6232
+ }
6233
+ return { nodeId, backendNodeId };
6234
+ }
6235
+ async tryClickAssociatedLabel(objectId) {
6236
+ const inputType = await this.readInputType(objectId);
6237
+ if (inputType !== "checkbox" && inputType !== "radio") {
6238
+ return false;
6239
+ }
6240
+ const labelNodeId = await this.getAssociatedLabelNodeId(objectId);
6241
+ if (!labelNodeId) {
6242
+ return false;
6243
+ }
6244
+ try {
6245
+ await this.scrollIntoView(labelNodeId);
6246
+ await this.clickElement(labelNodeId);
6247
+ return true;
6248
+ } catch {
6249
+ return false;
6250
+ }
6251
+ }
6252
+ async tryToggleViaLabel(objectId, desiredChecked) {
6253
+ if (!await this.tryClickAssociatedLabel(objectId)) {
6254
+ return false;
6255
+ }
6256
+ return await this.readCheckedState(objectId) === desiredChecked;
6257
+ }
5719
6258
  /**
5720
6259
  * Ensure we have a valid root node ID
5721
6260
  */
@@ -6133,6 +6672,7 @@ var Browser = class _Browser {
6133
6672
  cdp;
6134
6673
  providerSession;
6135
6674
  pages = /* @__PURE__ */ new Map();
6675
+ pageCounter = 0;
6136
6676
  constructor(cdp, _provider, providerSession, _options) {
6137
6677
  this.cdp = cdp;
6138
6678
  this.providerSession = providerSession;
@@ -6162,7 +6702,11 @@ var Browser = class _Browser {
6162
6702
  const pageName = name ?? "default";
6163
6703
  const cached = this.pages.get(pageName);
6164
6704
  if (cached) return cached;
6165
- const targets = await this.cdp.send("Target.getTargets");
6705
+ const targets = await this.cdp.send(
6706
+ "Target.getTargets",
6707
+ void 0,
6708
+ null
6709
+ );
6166
6710
  let pageTargets = targets.targetInfos.filter((t) => t.type === "page");
6167
6711
  if (options?.targetUrl) {
6168
6712
  const urlFilter = options.targetUrl;
@@ -6184,16 +6728,24 @@ var Browser = class _Browser {
6184
6728
  targetId = options.targetId;
6185
6729
  } else {
6186
6730
  console.warn(`[browser-pilot] Target ${options.targetId} no longer exists, falling back`);
6187
- targetId = pickBestTarget(pageTargets) ?? (await this.cdp.send("Target.createTarget", {
6188
- url: "about:blank"
6189
- })).targetId;
6731
+ targetId = pickBestTarget(pageTargets) ?? (await this.cdp.send(
6732
+ "Target.createTarget",
6733
+ {
6734
+ url: "about:blank"
6735
+ },
6736
+ null
6737
+ )).targetId;
6190
6738
  }
6191
6739
  } else if (pageTargets.length > 0) {
6192
6740
  targetId = pickBestTarget(pageTargets);
6193
6741
  } else {
6194
- const result = await this.cdp.send("Target.createTarget", {
6195
- url: "about:blank"
6196
- });
6742
+ const result = await this.cdp.send(
6743
+ "Target.createTarget",
6744
+ {
6745
+ url: "about:blank"
6746
+ },
6747
+ null
6748
+ );
6197
6749
  targetId = result.targetId;
6198
6750
  }
6199
6751
  await this.cdp.attachToTarget(targetId);
@@ -6221,13 +6773,17 @@ var Browser = class _Browser {
6221
6773
  * Create a new page (tab)
6222
6774
  */
6223
6775
  async newPage(url = "about:blank") {
6224
- const result = await this.cdp.send("Target.createTarget", {
6225
- url
6226
- });
6776
+ const result = await this.cdp.send(
6777
+ "Target.createTarget",
6778
+ {
6779
+ url
6780
+ },
6781
+ null
6782
+ );
6227
6783
  await this.cdp.attachToTarget(result.targetId);
6228
6784
  const page = new Page(this.cdp, result.targetId);
6229
6785
  await page.init();
6230
- const name = `page-${this.pages.size + 1}`;
6786
+ const name = `page-${++this.pageCounter}`;
6231
6787
  this.pages.set(name, page);
6232
6788
  return page;
6233
6789
  }
@@ -6237,14 +6793,30 @@ var Browser = class _Browser {
6237
6793
  async closePage(name) {
6238
6794
  const page = this.pages.get(name);
6239
6795
  if (!page) return;
6240
- const targets = await this.cdp.send("Target.getTargets");
6241
- const pageTargets = targets.targetInfos.filter((t) => t.type === "page");
6242
- if (pageTargets.length > 0) {
6243
- await this.cdp.send("Target.closeTarget", {
6244
- targetId: pageTargets[0].targetId
6245
- });
6246
- }
6796
+ const targetId = page.targetId;
6797
+ await this.cdp.send("Target.closeTarget", { targetId }, null);
6247
6798
  this.pages.delete(name);
6799
+ const deadline = Date.now() + 5e3;
6800
+ while (Date.now() < deadline) {
6801
+ const { targetInfos } = await this.cdp.send(
6802
+ "Target.getTargets",
6803
+ void 0,
6804
+ null
6805
+ );
6806
+ if (!targetInfos.some((t) => t.targetId === targetId)) return;
6807
+ await new Promise((r) => setTimeout(r, 50));
6808
+ }
6809
+ }
6810
+ /**
6811
+ * List all page targets in the connected browser.
6812
+ */
6813
+ async listTargets() {
6814
+ const { targetInfos } = await this.cdp.send(
6815
+ "Target.getTargets",
6816
+ void 0,
6817
+ null
6818
+ );
6819
+ return targetInfos.filter((target) => target.type === "page");
6248
6820
  }
6249
6821
  /**
6250
6822
  * Get the WebSocket URL for this browser connection