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/index.cjs CHANGED
@@ -998,6 +998,9 @@ var BatchExecutor = class {
998
998
  const snapshot = await this.page.snapshot();
999
999
  return { value: snapshot };
1000
1000
  }
1001
+ case "forms": {
1002
+ return { value: await this.page.forms() };
1003
+ }
1001
1004
  case "screenshot": {
1002
1005
  const data = await this.page.screenshot({
1003
1006
  format: step.format,
@@ -1017,6 +1020,21 @@ var BatchExecutor = class {
1017
1020
  const text = await this.page.text(selector);
1018
1021
  return { text, selectorUsed: selector };
1019
1022
  }
1023
+ case "newTab": {
1024
+ const { targetId } = await this.page.cdpClient.send(
1025
+ "Target.createTarget",
1026
+ {
1027
+ url: step.url ?? "about:blank"
1028
+ },
1029
+ null
1030
+ );
1031
+ return { value: { targetId } };
1032
+ }
1033
+ case "closeTab": {
1034
+ const targetId = step.targetId ?? this.page.targetId;
1035
+ await this.page.cdpClient.send("Target.closeTarget", { targetId }, null);
1036
+ return { value: { targetId, closedCurrent: targetId === this.page.targetId } };
1037
+ }
1020
1038
  case "switchFrame": {
1021
1039
  if (!step.selector) throw new Error("switchFrame requires selector");
1022
1040
  await this.page.switchToFrame(step.selector, { timeout, optional });
@@ -1131,10 +1149,15 @@ var BatchExecutor = class {
1131
1149
  snap: "snapshot",
1132
1150
  accessibility: "snapshot",
1133
1151
  a11y: "snapshot",
1152
+ formslist: "forms",
1134
1153
  image: "screenshot",
1135
1154
  pic: "screenshot",
1136
1155
  frame: "switchFrame",
1137
1156
  iframe: "switchFrame",
1157
+ newtab: "newTab",
1158
+ opentab: "newTab",
1159
+ createtab: "newTab",
1160
+ closetab: "closeTab",
1138
1161
  assert_visible: "assertVisible",
1139
1162
  assert_exists: "assertExists",
1140
1163
  assert_text: "assertText",
@@ -1148,7 +1171,7 @@ var BatchExecutor = class {
1148
1171
  };
1149
1172
  const suggestion = aliases[action.toLowerCase()];
1150
1173
  const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
1151
- 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";
1174
+ 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";
1152
1175
  throw new Error(`Unknown action "${action}".${hint}
1153
1176
 
1154
1177
  Valid actions: ${valid}`);
@@ -1219,6 +1242,11 @@ var ACTION_ALIASES = {
1219
1242
  pic: "screenshot",
1220
1243
  frame: "switchFrame",
1221
1244
  iframe: "switchFrame",
1245
+ formslist: "forms",
1246
+ newtab: "newTab",
1247
+ opentab: "newTab",
1248
+ createtab: "newTab",
1249
+ closetab: "closeTab",
1222
1250
  assert_visible: "assertVisible",
1223
1251
  assert_exists: "assertExists",
1224
1252
  assert_text: "assertText",
@@ -1255,7 +1283,8 @@ var PROPERTY_ALIASES = {
1255
1283
  button: "key",
1256
1284
  address: "url",
1257
1285
  page: "url",
1258
- path: "url"
1286
+ path: "url",
1287
+ tabId: "targetId"
1259
1288
  };
1260
1289
  var ACTION_RULES = {
1261
1290
  goto: {
@@ -1356,6 +1385,10 @@ var ACTION_RULES = {
1356
1385
  fullPage: { type: "boolean" }
1357
1386
  }
1358
1387
  },
1388
+ forms: {
1389
+ required: {},
1390
+ optional: {}
1391
+ },
1359
1392
  evaluate: {
1360
1393
  required: { value: { type: "string" } },
1361
1394
  optional: {}
@@ -1370,6 +1403,18 @@ var ACTION_RULES = {
1370
1403
  required: { selector: { type: "string|string[]" } },
1371
1404
  optional: {}
1372
1405
  },
1406
+ newTab: {
1407
+ required: {},
1408
+ optional: {
1409
+ url: { type: "string" }
1410
+ }
1411
+ },
1412
+ closeTab: {
1413
+ required: {},
1414
+ optional: {
1415
+ targetId: { type: "string" }
1416
+ }
1417
+ },
1373
1418
  switchToMain: {
1374
1419
  required: {},
1375
1420
  optional: {}
@@ -1412,6 +1457,7 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
1412
1457
  "selector",
1413
1458
  "url",
1414
1459
  "value",
1460
+ "targetId",
1415
1461
  "key",
1416
1462
  "combo",
1417
1463
  "modifiers",
@@ -1566,15 +1612,22 @@ function validateSteps(steps) {
1566
1612
  const rule = ACTION_RULES[action];
1567
1613
  for (const key of Object.keys(obj)) {
1568
1614
  if (key === "action") continue;
1569
- if (!KNOWN_STEP_FIELDS.has(key)) {
1570
- const suggestion = suggestProperty(key);
1571
- errors.push({
1572
- stepIndex: i,
1573
- field: key,
1574
- message: suggestion ? `unknown property "${key}". Did you mean "${suggestion}"?` : `unknown property "${key}".`,
1575
- suggestion: suggestion ? `Did you mean "${suggestion}"?` : void 0
1576
- });
1615
+ if (KNOWN_STEP_FIELDS.has(key)) continue;
1616
+ const canonical = PROPERTY_ALIASES[key];
1617
+ if (canonical) {
1618
+ if (!(canonical in obj)) {
1619
+ obj[canonical] = obj[key];
1620
+ }
1621
+ delete obj[key];
1622
+ continue;
1577
1623
  }
1624
+ const suggestion = suggestProperty(key);
1625
+ errors.push({
1626
+ stepIndex: i,
1627
+ field: key,
1628
+ message: suggestion ? `unknown property "${key}". Did you mean "${suggestion}"?` : `unknown property "${key}".`,
1629
+ suggestion: suggestion ? `Did you mean "${suggestion}"?` : void 0
1630
+ });
1578
1631
  }
1579
1632
  for (const [field, fieldRule] of Object.entries(rule.required)) {
1580
1633
  if (!(field in obj) || obj[field] === void 0) {
@@ -3311,7 +3364,7 @@ async function createCDPClient(wsUrl, options = {}) {
3311
3364
  throw new Error("CDP client is not connected");
3312
3365
  }
3313
3366
  const id = ++messageId;
3314
- const effectiveSessionId = sessionId ?? currentSessionId;
3367
+ const effectiveSessionId = sessionId === null ? void 0 : sessionId ?? currentSessionId;
3315
3368
  const request = { id, method };
3316
3369
  if (params !== void 0) {
3317
3370
  request.params = params;
@@ -3793,6 +3846,285 @@ var RequestInterceptor = class {
3793
3846
  }
3794
3847
  };
3795
3848
 
3849
+ // src/browser/special-selectors.ts
3850
+ function stripQuotes(value) {
3851
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
3852
+ return value.slice(1, -1);
3853
+ }
3854
+ return value;
3855
+ }
3856
+ function parseTextSelector(selector) {
3857
+ if (!selector.startsWith("text:")) return null;
3858
+ let raw = selector.slice(5).trim();
3859
+ let exact = false;
3860
+ if (raw.startsWith("=")) {
3861
+ exact = true;
3862
+ raw = raw.slice(1).trim();
3863
+ }
3864
+ const query = stripQuotes(raw);
3865
+ if (!query) return null;
3866
+ return { query, exact };
3867
+ }
3868
+ function parseRoleSelector(selector) {
3869
+ if (!selector.startsWith("role:")) return null;
3870
+ const body = selector.slice(5);
3871
+ const separator = body.indexOf(":");
3872
+ const role = (separator === -1 ? body : body.slice(0, separator)).trim().toLowerCase();
3873
+ const name = separator === -1 ? void 0 : stripQuotes(body.slice(separator + 1).trim());
3874
+ if (!role) return null;
3875
+ return { role, name: name || void 0 };
3876
+ }
3877
+ var SPECIAL_SELECTOR_SCRIPT = `
3878
+ function bpNormalizeSpace(value) {
3879
+ return String(value == null ? '' : value).replace(/\\s+/g, ' ').trim();
3880
+ }
3881
+
3882
+ function bpCollectElements(root) {
3883
+ var elements = [];
3884
+
3885
+ function visit(node) {
3886
+ if (!node || typeof node.querySelectorAll !== 'function') return;
3887
+ var matches = node.querySelectorAll('*');
3888
+ for (var i = 0; i < matches.length; i++) {
3889
+ var el = matches[i];
3890
+ elements.push(el);
3891
+ if (el.shadowRoot) {
3892
+ visit(el.shadowRoot);
3893
+ }
3894
+ }
3895
+ }
3896
+
3897
+ if (root && root.documentElement) {
3898
+ elements.push(root.documentElement);
3899
+ }
3900
+
3901
+ visit(root);
3902
+ return elements;
3903
+ }
3904
+
3905
+ function bpIsVisible(el) {
3906
+ if (!el) return false;
3907
+ var style = getComputedStyle(el);
3908
+ if (style.display === 'none') return false;
3909
+ if (style.visibility === 'hidden') return false;
3910
+ if (parseFloat(style.opacity || '1') === 0) return false;
3911
+ var rect = el.getBoundingClientRect();
3912
+ return rect.width > 0 && rect.height > 0;
3913
+ }
3914
+
3915
+ function bpInferRole(el) {
3916
+ if (!el || !el.tagName) return '';
3917
+
3918
+ var explicitRole = bpNormalizeSpace(el.getAttribute && el.getAttribute('role'));
3919
+ if (explicitRole) return explicitRole.toLowerCase();
3920
+
3921
+ var tag = el.tagName.toLowerCase();
3922
+ if (tag === 'button') return 'button';
3923
+ if (tag === 'a' && el.hasAttribute('href')) return 'link';
3924
+ if (tag === 'textarea') return 'textbox';
3925
+ if (tag === 'select') return el.multiple ? 'listbox' : 'combobox';
3926
+ if (tag === 'option') return 'option';
3927
+ if (tag === 'summary') return 'button';
3928
+
3929
+ if (tag === 'input') {
3930
+ var type = (el.type || 'text').toLowerCase();
3931
+ if (type === 'checkbox') return 'checkbox';
3932
+ if (type === 'radio') return 'radio';
3933
+ if (type === 'search') return 'searchbox';
3934
+ if (type === 'number') return 'spinbutton';
3935
+ if (type === 'button' || type === 'submit' || type === 'reset' || type === 'image') {
3936
+ return 'button';
3937
+ }
3938
+ return 'textbox';
3939
+ }
3940
+
3941
+ return '';
3942
+ }
3943
+
3944
+ function bpTextFromIdRefs(refs) {
3945
+ if (!refs) return '';
3946
+ var ids = refs.split(/\\s+/).filter(Boolean);
3947
+ var parts = [];
3948
+ for (var i = 0; i < ids.length; i++) {
3949
+ var node = document.getElementById(ids[i]);
3950
+ if (!node) continue;
3951
+ var text = bpNormalizeSpace(node.innerText || node.textContent || '');
3952
+ if (text) parts.push(text);
3953
+ }
3954
+ return bpNormalizeSpace(parts.join(' '));
3955
+ }
3956
+
3957
+ function bpAccessibleName(el) {
3958
+ if (!el) return '';
3959
+
3960
+ var labelledBy = bpTextFromIdRefs(el.getAttribute && el.getAttribute('aria-labelledby'));
3961
+ if (labelledBy) return labelledBy;
3962
+
3963
+ var ariaLabel = bpNormalizeSpace(el.getAttribute && el.getAttribute('aria-label'));
3964
+ if (ariaLabel) return ariaLabel;
3965
+
3966
+ if (el.labels && el.labels.length) {
3967
+ var labels = [];
3968
+ for (var i = 0; i < el.labels.length; i++) {
3969
+ var labelText = bpNormalizeSpace(el.labels[i].innerText || el.labels[i].textContent || '');
3970
+ if (labelText) labels.push(labelText);
3971
+ }
3972
+ if (labels.length) return bpNormalizeSpace(labels.join(' '));
3973
+ }
3974
+
3975
+ if (el.id) {
3976
+ var fallbackLabel = document.querySelector('label[for="' + el.id.replace(/"/g, '\\\\"') + '"]');
3977
+ if (fallbackLabel) {
3978
+ var fallbackText = bpNormalizeSpace(
3979
+ fallbackLabel.innerText || fallbackLabel.textContent || ''
3980
+ );
3981
+ if (fallbackText) return fallbackText;
3982
+ }
3983
+ }
3984
+
3985
+ var type = (el.type || '').toLowerCase();
3986
+ if (
3987
+ el.tagName === 'INPUT' &&
3988
+ (type === 'submit' || type === 'button' || type === 'reset' || type === 'image')
3989
+ ) {
3990
+ var inputValue = bpNormalizeSpace(el.value || el.getAttribute('value'));
3991
+ if (inputValue) return inputValue;
3992
+ }
3993
+
3994
+ var alt = bpNormalizeSpace(el.getAttribute && el.getAttribute('alt'));
3995
+ if (alt) return alt;
3996
+
3997
+ var text = bpNormalizeSpace(el.innerText || el.textContent || '');
3998
+ if (text) return text;
3999
+
4000
+ var placeholder = bpNormalizeSpace(el.getAttribute && el.getAttribute('placeholder'));
4001
+ if (placeholder) return placeholder;
4002
+
4003
+ var title = bpNormalizeSpace(el.getAttribute && el.getAttribute('title'));
4004
+ if (title) return title;
4005
+
4006
+ var value = bpNormalizeSpace(el.value);
4007
+ if (value) return value;
4008
+
4009
+ return bpNormalizeSpace(el.name || el.id || '');
4010
+ }
4011
+
4012
+ function bpIsInteractive(role, el) {
4013
+ if (
4014
+ role === 'button' ||
4015
+ role === 'link' ||
4016
+ role === 'textbox' ||
4017
+ role === 'checkbox' ||
4018
+ role === 'radio' ||
4019
+ role === 'combobox' ||
4020
+ role === 'listbox' ||
4021
+ role === 'option' ||
4022
+ role === 'searchbox' ||
4023
+ role === 'spinbutton' ||
4024
+ role === 'switch' ||
4025
+ role === 'tab'
4026
+ ) {
4027
+ return true;
4028
+ }
4029
+
4030
+ if (!el || !el.tagName) return false;
4031
+ var tag = el.tagName.toLowerCase();
4032
+ return tag === 'button' || tag === 'a' || tag === 'input' || tag === 'select' || tag === 'textarea';
4033
+ }
4034
+
4035
+ function bpFindByText(query, exact, includeHidden) {
4036
+ var needle = bpNormalizeSpace(query).toLowerCase();
4037
+ if (!needle) return null;
4038
+
4039
+ var best = null;
4040
+ var bestScore = -1;
4041
+ var elements = bpCollectElements(document);
4042
+
4043
+ for (var i = 0; i < elements.length; i++) {
4044
+ var el = elements[i];
4045
+ if (!includeHidden && !bpIsVisible(el)) continue;
4046
+
4047
+ var text = bpAccessibleName(el);
4048
+ if (!text) continue;
4049
+
4050
+ var haystack = text.toLowerCase();
4051
+ var matched = exact ? haystack === needle : haystack.includes(needle);
4052
+ if (!matched) continue;
4053
+
4054
+ var role = bpInferRole(el);
4055
+ var score = 0;
4056
+ if (bpIsInteractive(role, el)) score += 100;
4057
+ if (haystack === needle) score += 50;
4058
+ if (role === 'button' || role === 'link') score += 10;
4059
+
4060
+ if (score > bestScore) {
4061
+ best = el;
4062
+ bestScore = score;
4063
+ }
4064
+ }
4065
+
4066
+ return best;
4067
+ }
4068
+
4069
+ function bpFindByRole(role, name, includeHidden) {
4070
+ var targetRole = bpNormalizeSpace(role).toLowerCase();
4071
+ if (!targetRole) return null;
4072
+
4073
+ var nameNeedle = bpNormalizeSpace(name).toLowerCase();
4074
+ var best = null;
4075
+ var bestScore = -1;
4076
+ var elements = bpCollectElements(document);
4077
+
4078
+ for (var i = 0; i < elements.length; i++) {
4079
+ var el = elements[i];
4080
+ if (!includeHidden && !bpIsVisible(el)) continue;
4081
+
4082
+ var actualRole = bpInferRole(el);
4083
+ if (actualRole !== targetRole) continue;
4084
+
4085
+ var accessibleName = bpAccessibleName(el);
4086
+ if (nameNeedle) {
4087
+ var loweredName = accessibleName.toLowerCase();
4088
+ if (!loweredName.includes(nameNeedle)) continue;
4089
+ }
4090
+
4091
+ var score = 0;
4092
+ if (accessibleName) score += 10;
4093
+ if (nameNeedle && accessibleName.toLowerCase() === nameNeedle) score += 20;
4094
+
4095
+ if (score > bestScore) {
4096
+ best = el;
4097
+ bestScore = score;
4098
+ }
4099
+ }
4100
+
4101
+ return best;
4102
+ }
4103
+ `;
4104
+ function buildSpecialSelectorLookupExpression(selector, options = {}) {
4105
+ const includeHidden = options.includeHidden === true;
4106
+ const text = parseTextSelector(selector);
4107
+ if (text) {
4108
+ return `(() => {
4109
+ ${SPECIAL_SELECTOR_SCRIPT}
4110
+ return bpFindByText(${JSON.stringify(text.query)}, ${text.exact}, ${includeHidden});
4111
+ })()`;
4112
+ }
4113
+ const role = parseRoleSelector(selector);
4114
+ if (role) {
4115
+ return `(() => {
4116
+ ${SPECIAL_SELECTOR_SCRIPT}
4117
+ return bpFindByRole(${JSON.stringify(role.role)}, ${JSON.stringify(role.name ?? "")}, ${includeHidden});
4118
+ })()`;
4119
+ }
4120
+ return null;
4121
+ }
4122
+ function buildSpecialSelectorPredicateExpression(selector, options = {}) {
4123
+ const lookup = buildSpecialSelectorLookupExpression(selector, options);
4124
+ if (!lookup) return null;
4125
+ return `(() => !!(${lookup}))()`;
4126
+ }
4127
+
3796
4128
  // src/wait/strategies.ts
3797
4129
  var DEEP_QUERY_SCRIPT = `
3798
4130
  function deepQuery(selector, root = document) {
@@ -3826,18 +4158,19 @@ function deepQuery(selector, root = document) {
3826
4158
  }
3827
4159
  `;
3828
4160
  async function isElementVisible(cdp, selector, contextId) {
4161
+ const specialExpression = buildSpecialSelectorPredicateExpression(selector);
3829
4162
  const params = {
3830
- expression: `(() => {
3831
- ${DEEP_QUERY_SCRIPT}
3832
- const el = deepQuery(${JSON.stringify(selector)});
3833
- if (!el) return false;
3834
- const style = getComputedStyle(el);
3835
- if (style.display === 'none') return false;
3836
- if (style.visibility === 'hidden') return false;
3837
- if (parseFloat(style.opacity) === 0) return false;
3838
- const rect = el.getBoundingClientRect();
3839
- return rect.width > 0 && rect.height > 0;
3840
- })()`,
4163
+ expression: specialExpression ?? `(() => {
4164
+ ${DEEP_QUERY_SCRIPT}
4165
+ const el = deepQuery(${JSON.stringify(selector)});
4166
+ if (!el) return false;
4167
+ const style = getComputedStyle(el);
4168
+ if (style.display === 'none') return false;
4169
+ if (style.visibility === 'hidden') return false;
4170
+ if (parseFloat(style.opacity) === 0) return false;
4171
+ const rect = el.getBoundingClientRect();
4172
+ return rect.width > 0 && rect.height > 0;
4173
+ })()`,
3841
4174
  returnByValue: true
3842
4175
  };
3843
4176
  if (contextId !== void 0) {
@@ -3847,11 +4180,14 @@ async function isElementVisible(cdp, selector, contextId) {
3847
4180
  return result.result.value === true;
3848
4181
  }
3849
4182
  async function isElementAttached(cdp, selector, contextId) {
4183
+ const specialExpression = buildSpecialSelectorPredicateExpression(selector, {
4184
+ includeHidden: true
4185
+ });
3850
4186
  const params = {
3851
- expression: `(() => {
3852
- ${DEEP_QUERY_SCRIPT}
3853
- return deepQuery(${JSON.stringify(selector)}) !== null;
3854
- })()`,
4187
+ expression: specialExpression ?? `(() => {
4188
+ ${DEEP_QUERY_SCRIPT}
4189
+ return deepQuery(${JSON.stringify(selector)}) !== null;
4190
+ })()`,
3855
4191
  returnByValue: true
3856
4192
  };
3857
4193
  if (contextId !== void 0) {
@@ -4512,6 +4848,9 @@ var Page = class {
4512
4848
  timeout: options.timeout ?? DEFAULT_TIMEOUT2
4513
4849
  });
4514
4850
  } catch (e) {
4851
+ if (e instanceof ActionabilityError && e.failureType === "hitTarget" && await this.tryClickAssociatedLabel(objectId)) {
4852
+ return true;
4853
+ }
4515
4854
  if (options.optional) return false;
4516
4855
  throw e;
4517
4856
  }
@@ -4900,7 +5239,12 @@ var Page = class {
4900
5239
  returnByValue: true
4901
5240
  });
4902
5241
  if (!after.result.value) {
4903
- throw new Error("Clicking the checkbox did not change its state");
5242
+ if (await this.tryToggleViaLabel(object.objectId, true)) {
5243
+ return true;
5244
+ }
5245
+ throw new Error(
5246
+ "Clicking the checkbox did not change its state. Tried the associated label too."
5247
+ );
4904
5248
  }
4905
5249
  return true;
4906
5250
  });
@@ -4952,7 +5296,12 @@ var Page = class {
4952
5296
  returnByValue: true
4953
5297
  });
4954
5298
  if (after.result.value) {
4955
- throw new Error("Clicking the checkbox did not change its state");
5299
+ if (await this.tryToggleViaLabel(object.objectId, false)) {
5300
+ return true;
5301
+ }
5302
+ throw new Error(
5303
+ "Clicking the checkbox did not change its state. Tried the associated label too."
5304
+ );
4956
5305
  }
4957
5306
  return true;
4958
5307
  });
@@ -5276,7 +5625,7 @@ var Page = class {
5276
5625
  }
5277
5626
  const result = await this.cdp.send("Runtime.evaluate", params);
5278
5627
  if (result.exceptionDetails) {
5279
- throw new Error(`Evaluation failed: ${result.exceptionDetails.text}`);
5628
+ throw new Error(this.formatEvaluationError(result.exceptionDetails));
5280
5629
  }
5281
5630
  return result.result.value;
5282
5631
  }
@@ -5328,6 +5677,75 @@ var Page = class {
5328
5677
  return result.result.value ?? "";
5329
5678
  });
5330
5679
  }
5680
+ /**
5681
+ * Enumerate form controls on the page with labels and current state.
5682
+ */
5683
+ async forms() {
5684
+ const result = await this.evaluateInFrame(
5685
+ `(() => {
5686
+ function normalize(value) {
5687
+ return String(value == null ? '' : value).replace(/\\s+/g, ' ').trim();
5688
+ }
5689
+
5690
+ function labelFor(el) {
5691
+ if (!el) return '';
5692
+ if (el.labels && el.labels.length) {
5693
+ return normalize(
5694
+ Array.from(el.labels)
5695
+ .map((label) => label.innerText || label.textContent || '')
5696
+ .join(' ')
5697
+ );
5698
+ }
5699
+ var ariaLabel = normalize(el.getAttribute && el.getAttribute('aria-label'));
5700
+ if (ariaLabel) return ariaLabel;
5701
+ if (el.id) {
5702
+ var byFor = document.querySelector('label[for="' + el.id.replace(/"/g, '\\\\"') + '"]');
5703
+ if (byFor) return normalize(byFor.innerText || byFor.textContent || '');
5704
+ }
5705
+ var closest = el.closest && el.closest('label');
5706
+ if (closest) return normalize(closest.innerText || closest.textContent || '');
5707
+ return '';
5708
+ }
5709
+
5710
+ return Array.from(document.querySelectorAll('input, select, textarea')).map((el) => {
5711
+ var tag = el.tagName.toLowerCase();
5712
+ var type = tag === 'input' ? (el.type || 'text').toLowerCase() : tag;
5713
+ var value = null;
5714
+
5715
+ if (tag === 'select') {
5716
+ value = el.multiple
5717
+ ? Array.from(el.selectedOptions).map((opt) => opt.value)
5718
+ : el.value || null;
5719
+ } else if (tag === 'textarea' || tag === 'input') {
5720
+ value = typeof el.value === 'string' ? el.value : null;
5721
+ }
5722
+
5723
+ return {
5724
+ tag: tag,
5725
+ type: type,
5726
+ id: el.id || undefined,
5727
+ name: el.getAttribute('name') || undefined,
5728
+ value: value,
5729
+ checked: 'checked' in el ? !!el.checked : undefined,
5730
+ required: !!el.required,
5731
+ disabled: !!el.disabled,
5732
+ label: labelFor(el) || undefined,
5733
+ placeholder: normalize(el.getAttribute && el.getAttribute('placeholder')) || undefined,
5734
+ options:
5735
+ tag === 'select'
5736
+ ? Array.from(el.options).map((opt) => ({
5737
+ value: opt.value || '',
5738
+ text: normalize(opt.text || opt.label || ''),
5739
+ selected: !!opt.selected,
5740
+ disabled: !!opt.disabled,
5741
+ }))
5742
+ : undefined,
5743
+ };
5744
+ });
5745
+ })()`
5746
+ );
5747
+ return result.result.value ?? [];
5748
+ }
5331
5749
  // ============ File Handling ============
5332
5750
  /**
5333
5751
  * Set files on a file input
@@ -5800,7 +6218,8 @@ var Page = class {
5800
6218
  /**
5801
6219
  * Get an accessibility tree snapshot of the page
5802
6220
  */
5803
- async snapshot() {
6221
+ async snapshot(options = {}) {
6222
+ const roleFilter = new Set((options.roles ?? []).map((role) => role.trim().toLowerCase()));
5804
6223
  const [url, title, axTree] = await Promise.all([
5805
6224
  this.url(),
5806
6225
  this.title(),
@@ -5821,7 +6240,7 @@ var Page = class {
5821
6240
  const buildNode = (nodeId) => {
5822
6241
  const node = nodeMap.get(nodeId);
5823
6242
  if (!node) return null;
5824
- const role = node.role?.value ?? "generic";
6243
+ const role = (node.role?.value ?? "generic").toLowerCase();
5825
6244
  const name = node.name?.value;
5826
6245
  const value = node.value?.value;
5827
6246
  const ref = nodeRefs.get(nodeId);
@@ -5837,7 +6256,7 @@ var Page = class {
5837
6256
  return {
5838
6257
  role,
5839
6258
  name,
5840
- value,
6259
+ value: value !== void 0 ? String(value) : void 0,
5841
6260
  ref,
5842
6261
  children: children.length > 0 ? children : void 0,
5843
6262
  disabled,
@@ -5845,7 +6264,24 @@ var Page = class {
5845
6264
  };
5846
6265
  };
5847
6266
  const rootNodes = nodes.filter((n) => !n.parentId || !nodeMap.has(n.parentId));
5848
- const accessibilityTree = rootNodes.map((n) => buildNode(n.nodeId)).filter((n) => n !== null);
6267
+ let accessibilityTree = rootNodes.map((n) => buildNode(n.nodeId)).filter((n) => n !== null);
6268
+ if (roleFilter.size > 0) {
6269
+ const filteredAccessibilityTree = [];
6270
+ for (const node of nodes) {
6271
+ if (!roleFilter.has((node.role?.value ?? "generic").toLowerCase())) {
6272
+ continue;
6273
+ }
6274
+ const snapshotNode = buildNode(node.nodeId);
6275
+ if (!snapshotNode) {
6276
+ continue;
6277
+ }
6278
+ filteredAccessibilityTree.push({
6279
+ ...snapshotNode,
6280
+ children: void 0
6281
+ });
6282
+ }
6283
+ accessibilityTree = filteredAccessibilityTree;
6284
+ }
5849
6285
  const interactiveRoles = /* @__PURE__ */ new Set([
5850
6286
  "button",
5851
6287
  "link",
@@ -5867,37 +6303,44 @@ var Page = class {
5867
6303
  ]);
5868
6304
  const interactiveElements = [];
5869
6305
  for (const node of nodes) {
5870
- const role = node.role?.value;
5871
- if (role && interactiveRoles.has(role)) {
6306
+ const role = (node.role?.value ?? "").toLowerCase();
6307
+ if (role && interactiveRoles.has(role) && (roleFilter.size === 0 || roleFilter.has(role))) {
5872
6308
  const ref = nodeRefs.get(node.nodeId);
5873
6309
  const name = node.name?.value ?? "";
5874
6310
  const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
6311
+ const checked = node.properties?.find((p) => p.name === "checked")?.value.value;
6312
+ const value = node.value?.value;
5875
6313
  const selector = node.backendDOMNodeId ? `[data-backend-node-id="${node.backendDOMNodeId}"]` : `[aria-label="${name}"]`;
5876
6314
  interactiveElements.push({
5877
6315
  ref,
5878
6316
  role,
5879
6317
  name,
5880
6318
  selector,
5881
- disabled
6319
+ disabled,
6320
+ checked,
6321
+ value: value !== void 0 ? String(value) : void 0
5882
6322
  });
5883
6323
  }
5884
6324
  }
6325
+ const formatNode = (node, depth = 0) => {
6326
+ let line = `${" ".repeat(depth)}- ${node.role}`;
6327
+ if (node.name) line += ` "${node.name}"`;
6328
+ line += ` ref:${node.ref}`;
6329
+ if (node.disabled) line += " (disabled)";
6330
+ if (node.checked !== void 0) line += node.checked ? " (checked)" : " (unchecked)";
6331
+ return line;
6332
+ };
5885
6333
  const formatTree = (nodes2, depth = 0) => {
5886
6334
  const lines = [];
5887
6335
  for (const node of nodes2) {
5888
- let line = `${" ".repeat(depth)}- ${node.role}`;
5889
- if (node.name) line += ` "${node.name}"`;
5890
- line += ` [ref=${node.ref}]`;
5891
- if (node.disabled) line += " (disabled)";
5892
- if (node.checked !== void 0) line += node.checked ? " (checked)" : " (unchecked)";
5893
- lines.push(line);
6336
+ lines.push(formatNode(node, depth));
5894
6337
  if (node.children) {
5895
6338
  lines.push(formatTree(node.children, depth + 1));
5896
6339
  }
5897
6340
  }
5898
6341
  return lines.join("\n");
5899
6342
  };
5900
- const text = formatTree(accessibilityTree);
6343
+ const text = roleFilter.size > 0 ? accessibilityTree.map((node) => formatNode(node)).join("\n") : formatTree(accessibilityTree);
5901
6344
  const result = {
5902
6345
  url,
5903
6346
  title,
@@ -5906,7 +6349,9 @@ var Page = class {
5906
6349
  interactiveElements,
5907
6350
  text
5908
6351
  };
5909
- this.lastSnapshot = result;
6352
+ if (roleFilter.size === 0) {
6353
+ this.lastSnapshot = result;
6354
+ }
5910
6355
  return result;
5911
6356
  }
5912
6357
  /**
@@ -6461,7 +6906,7 @@ var Page = class {
6461
6906
  }
6462
6907
  /**
6463
6908
  * Find an element using single or multiple selectors
6464
- * Supports ref: prefix for ref-based selectors (e.g., "ref:e4")
6909
+ * Supports ref:, text:, and role: selectors.
6465
6910
  */
6466
6911
  async findElement(selectors, options = {}) {
6467
6912
  const { timeout = DEFAULT_TIMEOUT2 } = options;
@@ -6528,11 +6973,11 @@ var Page = class {
6528
6973
  }
6529
6974
  }
6530
6975
  }
6531
- const cssSelectors = selectorList.filter((s) => !s.startsWith("ref:"));
6532
- if (cssSelectors.length === 0) {
6976
+ const runtimeSelectors = selectorList.filter((s) => !s.startsWith("ref:"));
6977
+ if (runtimeSelectors.length === 0) {
6533
6978
  return null;
6534
6979
  }
6535
- const result = await waitForAnyElement(this.cdp, cssSelectors, {
6980
+ const result = await waitForAnyElement(this.cdp, runtimeSelectors, {
6536
6981
  state: "visible",
6537
6982
  timeout,
6538
6983
  contextId: this.currentFrameContextId ?? void 0
@@ -6540,6 +6985,14 @@ var Page = class {
6540
6985
  if (!result.success || !result.selector) {
6541
6986
  return null;
6542
6987
  }
6988
+ const specialSelectorMatch = await this.resolveSpecialSelector(result.selector);
6989
+ if (specialSelectorMatch) {
6990
+ this._lastMatchedSelector = result.selector;
6991
+ return {
6992
+ ...specialSelectorMatch,
6993
+ waitedMs: result.waitedMs
6994
+ };
6995
+ }
6543
6996
  await this.ensureRootNode();
6544
6997
  const queryResult = await this.cdp.send("DOM.querySelector", {
6545
6998
  nodeId: this.rootNodeId,
@@ -6586,6 +7039,122 @@ var Page = class {
6586
7039
  waitedMs: result.waitedMs
6587
7040
  };
6588
7041
  }
7042
+ formatEvaluationError(details) {
7043
+ const description = typeof details.exception?.description === "string" && details.exception.description || typeof details.exception?.value === "string" && details.exception.value || details.text || "Uncaught";
7044
+ return `Evaluation failed: ${description}`;
7045
+ }
7046
+ async resolveSpecialSelector(selector, options = {}) {
7047
+ const expression = buildSpecialSelectorLookupExpression(selector, options);
7048
+ if (!expression) return null;
7049
+ const result = await this.evaluateInFrame(expression, {
7050
+ returnByValue: false
7051
+ });
7052
+ if (!result.result.objectId) {
7053
+ return null;
7054
+ }
7055
+ const resolved = await this.objectIdToNode(result.result.objectId);
7056
+ if (!resolved) {
7057
+ return null;
7058
+ }
7059
+ return {
7060
+ nodeId: resolved.nodeId,
7061
+ backendNodeId: resolved.backendNodeId,
7062
+ selector,
7063
+ waitedMs: 0
7064
+ };
7065
+ }
7066
+ async readCheckedState(objectId) {
7067
+ const result = await this.cdp.send("Runtime.callFunctionOn", {
7068
+ objectId,
7069
+ functionDeclaration: "function() { return !!this.checked; }",
7070
+ returnByValue: true
7071
+ });
7072
+ return result.result.value === true;
7073
+ }
7074
+ async readInputType(objectId) {
7075
+ const result = await this.cdp.send(
7076
+ "Runtime.callFunctionOn",
7077
+ {
7078
+ objectId,
7079
+ functionDeclaration: 'function() { return this instanceof HTMLInputElement ? String(this.type || "").toLowerCase() : null; }',
7080
+ returnByValue: true
7081
+ }
7082
+ );
7083
+ return result.result.value ?? null;
7084
+ }
7085
+ async getAssociatedLabelNodeId(objectId) {
7086
+ const result = await this.cdp.send("Runtime.callFunctionOn", {
7087
+ objectId,
7088
+ functionDeclaration: `function() {
7089
+ if (!(this instanceof HTMLInputElement)) return null;
7090
+
7091
+ if (this.id) {
7092
+ var labels = Array.from(document.querySelectorAll('label'));
7093
+ for (var i = 0; i < labels.length; i++) {
7094
+ if (labels[i].htmlFor === this.id) return labels[i];
7095
+ }
7096
+ }
7097
+
7098
+ return this.closest('label');
7099
+ }`,
7100
+ returnByValue: false
7101
+ });
7102
+ if (!result.result.objectId) {
7103
+ return null;
7104
+ }
7105
+ return (await this.objectIdToNode(result.result.objectId))?.nodeId ?? null;
7106
+ }
7107
+ async objectIdToNode(objectId) {
7108
+ const describeResult = await this.cdp.send("DOM.describeNode", {
7109
+ objectId,
7110
+ depth: 0
7111
+ });
7112
+ const backendNodeId = describeResult.node.backendNodeId;
7113
+ if (!backendNodeId) {
7114
+ return null;
7115
+ }
7116
+ if (describeResult.node.nodeId) {
7117
+ return {
7118
+ nodeId: describeResult.node.nodeId,
7119
+ backendNodeId
7120
+ };
7121
+ }
7122
+ await this.ensureRootNode();
7123
+ const pushResult = await this.cdp.send(
7124
+ "DOM.pushNodesByBackendIdsToFrontend",
7125
+ {
7126
+ backendNodeIds: [backendNodeId]
7127
+ }
7128
+ );
7129
+ const nodeId = pushResult.nodeIds?.[0];
7130
+ if (!nodeId) {
7131
+ return null;
7132
+ }
7133
+ return { nodeId, backendNodeId };
7134
+ }
7135
+ async tryClickAssociatedLabel(objectId) {
7136
+ const inputType = await this.readInputType(objectId);
7137
+ if (inputType !== "checkbox" && inputType !== "radio") {
7138
+ return false;
7139
+ }
7140
+ const labelNodeId = await this.getAssociatedLabelNodeId(objectId);
7141
+ if (!labelNodeId) {
7142
+ return false;
7143
+ }
7144
+ try {
7145
+ await this.scrollIntoView(labelNodeId);
7146
+ await this.clickElement(labelNodeId);
7147
+ return true;
7148
+ } catch {
7149
+ return false;
7150
+ }
7151
+ }
7152
+ async tryToggleViaLabel(objectId, desiredChecked) {
7153
+ if (!await this.tryClickAssociatedLabel(objectId)) {
7154
+ return false;
7155
+ }
7156
+ return await this.readCheckedState(objectId) === desiredChecked;
7157
+ }
6589
7158
  /**
6590
7159
  * Ensure we have a valid root node ID
6591
7160
  */
@@ -7003,6 +7572,7 @@ var Browser = class _Browser {
7003
7572
  cdp;
7004
7573
  providerSession;
7005
7574
  pages = /* @__PURE__ */ new Map();
7575
+ pageCounter = 0;
7006
7576
  constructor(cdp, _provider, providerSession, _options) {
7007
7577
  this.cdp = cdp;
7008
7578
  this.providerSession = providerSession;
@@ -7032,7 +7602,11 @@ var Browser = class _Browser {
7032
7602
  const pageName = name ?? "default";
7033
7603
  const cached = this.pages.get(pageName);
7034
7604
  if (cached) return cached;
7035
- const targets = await this.cdp.send("Target.getTargets");
7605
+ const targets = await this.cdp.send(
7606
+ "Target.getTargets",
7607
+ void 0,
7608
+ null
7609
+ );
7036
7610
  let pageTargets = targets.targetInfos.filter((t) => t.type === "page");
7037
7611
  if (options?.targetUrl) {
7038
7612
  const urlFilter = options.targetUrl;
@@ -7054,16 +7628,24 @@ var Browser = class _Browser {
7054
7628
  targetId = options.targetId;
7055
7629
  } else {
7056
7630
  console.warn(`[browser-pilot] Target ${options.targetId} no longer exists, falling back`);
7057
- targetId = pickBestTarget(pageTargets) ?? (await this.cdp.send("Target.createTarget", {
7058
- url: "about:blank"
7059
- })).targetId;
7631
+ targetId = pickBestTarget(pageTargets) ?? (await this.cdp.send(
7632
+ "Target.createTarget",
7633
+ {
7634
+ url: "about:blank"
7635
+ },
7636
+ null
7637
+ )).targetId;
7060
7638
  }
7061
7639
  } else if (pageTargets.length > 0) {
7062
7640
  targetId = pickBestTarget(pageTargets);
7063
7641
  } else {
7064
- const result = await this.cdp.send("Target.createTarget", {
7065
- url: "about:blank"
7066
- });
7642
+ const result = await this.cdp.send(
7643
+ "Target.createTarget",
7644
+ {
7645
+ url: "about:blank"
7646
+ },
7647
+ null
7648
+ );
7067
7649
  targetId = result.targetId;
7068
7650
  }
7069
7651
  await this.cdp.attachToTarget(targetId);
@@ -7091,13 +7673,17 @@ var Browser = class _Browser {
7091
7673
  * Create a new page (tab)
7092
7674
  */
7093
7675
  async newPage(url = "about:blank") {
7094
- const result = await this.cdp.send("Target.createTarget", {
7095
- url
7096
- });
7676
+ const result = await this.cdp.send(
7677
+ "Target.createTarget",
7678
+ {
7679
+ url
7680
+ },
7681
+ null
7682
+ );
7097
7683
  await this.cdp.attachToTarget(result.targetId);
7098
7684
  const page = new Page(this.cdp, result.targetId);
7099
7685
  await page.init();
7100
- const name = `page-${this.pages.size + 1}`;
7686
+ const name = `page-${++this.pageCounter}`;
7101
7687
  this.pages.set(name, page);
7102
7688
  return page;
7103
7689
  }
@@ -7107,14 +7693,30 @@ var Browser = class _Browser {
7107
7693
  async closePage(name) {
7108
7694
  const page = this.pages.get(name);
7109
7695
  if (!page) return;
7110
- const targets = await this.cdp.send("Target.getTargets");
7111
- const pageTargets = targets.targetInfos.filter((t) => t.type === "page");
7112
- if (pageTargets.length > 0) {
7113
- await this.cdp.send("Target.closeTarget", {
7114
- targetId: pageTargets[0].targetId
7115
- });
7116
- }
7696
+ const targetId = page.targetId;
7697
+ await this.cdp.send("Target.closeTarget", { targetId }, null);
7117
7698
  this.pages.delete(name);
7699
+ const deadline = Date.now() + 5e3;
7700
+ while (Date.now() < deadline) {
7701
+ const { targetInfos } = await this.cdp.send(
7702
+ "Target.getTargets",
7703
+ void 0,
7704
+ null
7705
+ );
7706
+ if (!targetInfos.some((t) => t.targetId === targetId)) return;
7707
+ await new Promise((r) => setTimeout(r, 50));
7708
+ }
7709
+ }
7710
+ /**
7711
+ * List all page targets in the connected browser.
7712
+ */
7713
+ async listTargets() {
7714
+ const { targetInfos } = await this.cdp.send(
7715
+ "Target.getTargets",
7716
+ void 0,
7717
+ null
7718
+ );
7719
+ return targetInfos.filter((target) => target.type === "page");
7118
7720
  }
7119
7721
  /**
7120
7722
  * Get the WebSocket URL for this browser connection