@mercuryo-ai/agentbrowse 0.2.61 → 0.2.63

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/README.md +102 -9
  3. package/dist/browser-session-state.d.ts +2 -11
  4. package/dist/browser-session-state.d.ts.map +1 -1
  5. package/dist/browser-session-state.js +0 -4
  6. package/dist/commands/act.d.ts.map +1 -1
  7. package/dist/commands/act.js +14 -5
  8. package/dist/commands/attach.d.ts +1 -3
  9. package/dist/commands/attach.d.ts.map +1 -1
  10. package/dist/commands/attach.js +0 -2
  11. package/dist/commands/browser-status.d.ts +0 -2
  12. package/dist/commands/browser-status.d.ts.map +1 -1
  13. package/dist/commands/browser-status.js +1 -7
  14. package/dist/commands/interaction-kernel.d.ts +1 -1
  15. package/dist/commands/interaction-kernel.d.ts.map +1 -1
  16. package/dist/commands/interaction-kernel.js +1 -1
  17. package/dist/commands/launch.d.ts +0 -1
  18. package/dist/commands/launch.d.ts.map +1 -1
  19. package/dist/commands/launch.js +0 -4
  20. package/dist/commands/observe-accessibility.d.ts.map +1 -1
  21. package/dist/commands/observe-accessibility.js +36 -2
  22. package/dist/commands/observe-inventory.d.ts +49 -7
  23. package/dist/commands/observe-inventory.d.ts.map +1 -1
  24. package/dist/commands/observe-inventory.js +807 -96
  25. package/dist/commands/observe-persistence.d.ts.map +1 -1
  26. package/dist/commands/observe-persistence.js +49 -6
  27. package/dist/commands/observe-projection.d.ts +6 -2
  28. package/dist/commands/observe-projection.d.ts.map +1 -1
  29. package/dist/commands/observe-projection.js +251 -27
  30. package/dist/commands/observe-semantics.d.ts +1 -0
  31. package/dist/commands/observe-semantics.d.ts.map +1 -1
  32. package/dist/commands/observe-semantics.js +541 -135
  33. package/dist/commands/observe-signals.d.ts +4 -4
  34. package/dist/commands/observe-signals.d.ts.map +1 -1
  35. package/dist/commands/observe-signals.js +2 -2
  36. package/dist/commands/observe-surfaces.d.ts +2 -1
  37. package/dist/commands/observe-surfaces.d.ts.map +1 -1
  38. package/dist/commands/observe-surfaces.js +143 -45
  39. package/dist/commands/observe.d.ts +5 -1
  40. package/dist/commands/observe.d.ts.map +1 -1
  41. package/dist/commands/observe.js +15 -11
  42. package/dist/commands/semantic-observe.d.ts.map +1 -1
  43. package/dist/commands/semantic-observe.js +43 -0
  44. package/dist/library.d.ts +2 -1
  45. package/dist/library.d.ts.map +1 -1
  46. package/dist/library.js +2 -1
  47. package/dist/match-resolve-fill.d.ts +196 -0
  48. package/dist/match-resolve-fill.d.ts.map +1 -0
  49. package/dist/match-resolve-fill.js +700 -0
  50. package/dist/match-resolve-fill.test-support.d.ts +34 -0
  51. package/dist/match-resolve-fill.test-support.d.ts.map +1 -0
  52. package/dist/match-resolve-fill.test-support.js +81 -0
  53. package/dist/runtime-protected-state.d.ts.map +1 -1
  54. package/dist/runtime-protected-state.js +12 -0
  55. package/dist/runtime-state.d.ts +6 -0
  56. package/dist/runtime-state.d.ts.map +1 -1
  57. package/dist/runtime-state.js +6 -0
  58. package/dist/secrets/form-matcher.d.ts.map +1 -1
  59. package/dist/secrets/form-matcher.js +76 -27
  60. package/dist/secrets/protected-exact-value-redaction.d.ts.map +1 -1
  61. package/dist/secrets/protected-exact-value-redaction.js +6 -0
  62. package/dist/secrets/protected-fill.js +3 -3
  63. package/dist/session.d.ts +3 -3
  64. package/dist/session.d.ts.map +1 -1
  65. package/dist/session.js +2 -2
  66. package/dist/solver/browser-launcher.d.ts.map +1 -1
  67. package/dist/solver/browser-launcher.js +2 -1
  68. package/dist/testing.d.ts +1 -0
  69. package/dist/testing.d.ts.map +1 -1
  70. package/dist/testing.js +1 -0
  71. package/docs/README.md +28 -11
  72. package/docs/api-reference.md +311 -19
  73. package/docs/assistive-runtime.md +41 -16
  74. package/docs/getting-started.md +45 -1
  75. package/docs/integration-checklist.md +32 -3
  76. package/docs/match-resolve-fill.md +699 -0
  77. package/docs/protected-fill.md +373 -91
  78. package/docs/testing.md +147 -15
  79. package/docs/troubleshooting.md +5 -0
  80. package/examples/README.md +7 -0
  81. package/examples/match-resolve-fill.ts +107 -0
  82. package/package.json +4 -2
@@ -14,10 +14,13 @@ export function inferStructuredCellVariantFromEvidence(evidence) {
14
14
  const hasSeatMetadata = Boolean(evidence.hasSeatAttribute || evidence.hasSeatRowAttribute || evidence.hasSeatColumnAttribute);
15
15
  const seatIdentityLike = /(?:\bseat\s*\d{1,3}[a-z]?\b|место\s*\d{1,3}[a-z]?\b|\b\d{1,3}[a-z]\b|\b[a-z]\d{1,3}\b)/i.test(normalizedLabel);
16
16
  const seatClassLike = /seat|cabin|fare|row/.test(className);
17
- const compactDateCellLabel = /^(?:\d{1,2}|january|february|march|april|may|june|july|august|september|october|november|december|январ|феврал|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр)$/i.test(normalizedLabel) || /^(?:\d{1,2}[./-]\d{1,2}(?:[./-]\d{2,4})?)$/.test(normalizedLabel);
17
+ const compactDateCellLabel = /^\d{1,2}$/.test(normalizedLabel) ||
18
+ /^(?:\d{1,2}[./-]\d{1,2}(?:[./-]\d{2,4})?)$/.test(normalizedLabel);
19
+ const leadingNumericDayToken = /^\d{1,2}\b/.test(normalizedLabel);
18
20
  const dateLike = surfaceKind === 'datepicker' ||
19
21
  (hasDateMetadata && role === 'gridcell') ||
20
- ((role === 'gridcell' || structuredSurface || hasDateMetadata) && compactDateCellLabel);
22
+ ((role === 'gridcell' || structuredSurface || hasDateMetadata) &&
23
+ (compactDateCellLabel || (hasDateMetadata && leadingNumericDayToken)));
21
24
  if (dateLike) {
22
25
  return 'date-cell';
23
26
  }
@@ -415,14 +418,16 @@ export function normalizeStagehandSelector(selector) {
415
418
  }
416
419
  : { selector };
417
420
  }
418
- async function collectDomTargetsFromDocument(context, options) {
421
+ async function collectDomTargetsFromDocumentRaw(context, options) {
419
422
  const includeActivationAffordances = options?.includeActivationAffordances === true;
423
+ const debugStructuralProfileStats = options?.debugStructuralProfileStats === true;
420
424
  const inheritedFramePath = JSON.stringify(options?.framePath ?? []);
421
425
  const inheritedFrameUrl = JSON.stringify(options?.frameUrl ?? '');
422
426
  const inheritedPageSignature = JSON.stringify(options?.pageSignature ?? '');
423
427
  const inheritedPageFormSelector = JSON.stringify(options?.pageFormSelector ?? '');
424
- const observedTargets = await context.evaluate(String.raw `(() => {
428
+ const observedPayload = await context.evaluate(String.raw `(() => {
425
429
  const includeActivationAffordances = ${includeActivationAffordances ? 'true' : 'false'};
430
+ const debugStructuralProfileStats = ${debugStructuralProfileStats ? 'true' : 'false'};
426
431
  const inheritedFramePath = ${inheritedFramePath};
427
432
  const inheritedFrameUrl = ${inheritedFrameUrl};
428
433
  const inheritedPageSignature = ${inheritedPageSignature};
@@ -1108,7 +1113,26 @@ async function collectDomTargetsFromDocument(context, options) {
1108
1113
  return inferRole(element) || tag;
1109
1114
  };
1110
1115
 
1116
+ const selectorCache = new WeakMap();
1117
+
1111
1118
  const buildSelector = (element) => {
1119
+ if (!isHTMLElementNode(element)) {
1120
+ return undefined;
1121
+ }
1122
+
1123
+ const cachedSelector = selectorCache.get(element);
1124
+ if (typeof cachedSelector === 'string') {
1125
+ return cachedSelector;
1126
+ }
1127
+ if (cachedSelector === null) {
1128
+ return undefined;
1129
+ }
1130
+
1131
+ const finalizeSelector = (value) => {
1132
+ selectorCache.set(element, value ?? null);
1133
+ return value;
1134
+ };
1135
+
1112
1136
  const testIdAttributeOf = (candidate) => {
1113
1137
  if (candidate.hasAttribute('data-testid')) return 'data-testid';
1114
1138
  if (candidate.hasAttribute('data-test-id')) return 'data-test-id';
@@ -1135,7 +1159,7 @@ async function collectDomTargetsFromDocument(context, options) {
1135
1159
  };
1136
1160
 
1137
1161
  if (element.id && isSelectorUniqueFor(element, '#' + cssEscape(element.id))) {
1138
- return '#' + cssEscape(element.id);
1162
+ return finalizeSelector('#' + cssEscape(element.id));
1139
1163
  }
1140
1164
 
1141
1165
  const testId =
@@ -1143,7 +1167,7 @@ async function collectDomTargetsFromDocument(context, options) {
1143
1167
  if (testId) {
1144
1168
  const selectorValue = testIdSelectorOf(element, testId);
1145
1169
  if (isSelectorUniqueFor(element, selectorValue)) {
1146
- return selectorValue;
1170
+ return finalizeSelector(selectorValue);
1147
1171
  }
1148
1172
  }
1149
1173
 
@@ -1152,7 +1176,7 @@ async function collectDomTargetsFromDocument(context, options) {
1152
1176
  if (name) {
1153
1177
  const selectorValue = tag + '[name="' + cssEscape(name) + '"]';
1154
1178
  if (isSelectorUniqueFor(element, selectorValue)) {
1155
- return selectorValue;
1179
+ return finalizeSelector(selectorValue);
1156
1180
  }
1157
1181
  }
1158
1182
 
@@ -1201,10 +1225,14 @@ async function collectDomTargetsFromDocument(context, options) {
1201
1225
  }
1202
1226
  current = current.parentElement;
1203
1227
  }
1204
- if (path.length === 0) return undefined;
1228
+ if (path.length === 0) return finalizeSelector(undefined);
1205
1229
 
1206
1230
  const structuralSelector = path.join(' > ');
1207
- return isSelectorUniqueFor(element, structuralSelector) ? structuralSelector : undefined;
1231
+ return finalizeSelector(
1232
+ isSelectorUniqueFor(element, structuralSelector)
1233
+ ? structuralSelector
1234
+ : undefined
1235
+ );
1208
1236
  };
1209
1237
 
1210
1238
  const selectorFromRelation = (element, attribute) => {
@@ -1353,10 +1381,44 @@ async function collectDomTargetsFromDocument(context, options) {
1353
1381
  return false;
1354
1382
  };
1355
1383
 
1384
+ const visibleInteractiveDescendantCountCache = new WeakMap();
1385
+
1356
1386
  const visibleInteractiveDescendantCountOf = (element) => {
1357
- return Array.from(element.querySelectorAll(selector)).filter((candidate) => {
1387
+ if (!isHTMLElementNode(element)) {
1388
+ return 0;
1389
+ }
1390
+
1391
+ const cachedCount = visibleInteractiveDescendantCountCache.get(element);
1392
+ if (typeof cachedCount === 'number') {
1393
+ return cachedCount;
1394
+ }
1395
+
1396
+ const count = Array.from(element.querySelectorAll(selector)).filter((candidate) => {
1358
1397
  return isHTMLElementNode(candidate) && isVisible(candidate);
1359
1398
  }).length;
1399
+ visibleInteractiveDescendantCountCache.set(element, count);
1400
+ return count;
1401
+ };
1402
+
1403
+ const descendantEditableCountCache = new WeakMap();
1404
+
1405
+ const descendantEditableCountOf = (element) => {
1406
+ if (!isHTMLElementNode(element)) {
1407
+ return 0;
1408
+ }
1409
+
1410
+ const cachedCount = descendantEditableCountCache.get(element);
1411
+ if (typeof cachedCount === 'number') {
1412
+ return cachedCount;
1413
+ }
1414
+
1415
+ const count = Array.from(
1416
+ element.querySelectorAll(
1417
+ 'input:not([type="hidden"]), textarea, select, [contenteditable="true"]'
1418
+ )
1419
+ ).filter((candidate) => isHTMLElementNode(candidate) && isVisible(candidate)).length;
1420
+ descendantEditableCountCache.set(element, count);
1421
+ return count;
1360
1422
  };
1361
1423
 
1362
1424
  const clickableSemanticBlobOf = (element) => {
@@ -2230,6 +2292,87 @@ async function collectDomTargetsFromDocument(context, options) {
2230
2292
  return Object.keys(states).length > 0 ? states : undefined;
2231
2293
  };
2232
2294
 
2295
+ const canonicalDateIdentityFromParts = (yearValue, monthValue, dayValue) => {
2296
+ const year = Number.parseInt(String(yearValue || '').trim(), 10);
2297
+ const month = Number.parseInt(String(monthValue || '').trim(), 10);
2298
+ const day = Number.parseInt(String(dayValue || '').trim(), 10);
2299
+ if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) {
2300
+ return undefined;
2301
+ }
2302
+ if (month < 1 || month > 12 || day < 1 || day > 31) {
2303
+ return undefined;
2304
+ }
2305
+
2306
+ const candidate = new Date(Date.UTC(year, month - 1, day));
2307
+ if (
2308
+ candidate.getUTCFullYear() !== year ||
2309
+ candidate.getUTCMonth() !== month - 1 ||
2310
+ candidate.getUTCDate() !== day
2311
+ ) {
2312
+ return undefined;
2313
+ }
2314
+
2315
+ return {
2316
+ dateIso:
2317
+ year.toString().padStart(4, '0') +
2318
+ '-' +
2319
+ month.toString().padStart(2, '0') +
2320
+ '-' +
2321
+ day.toString().padStart(2, '0'),
2322
+ dateYear: year,
2323
+ dateMonth: month,
2324
+ dateDay: day,
2325
+ };
2326
+ };
2327
+
2328
+ const canonicalDateIdentityFromValue = (value) => {
2329
+ const normalized = normalizeDescriptorText(value || '');
2330
+ const match = normalized?.match(/\b(\d{4})-(\d{1,2})-(\d{1,2})\b/);
2331
+ if (!match) {
2332
+ return undefined;
2333
+ }
2334
+
2335
+ return canonicalDateIdentityFromParts(match[1], match[2], match[3]);
2336
+ };
2337
+
2338
+ const canonicalDateIdentityOf = (element, normalizedLabel) => {
2339
+ const metadataValues = [
2340
+ element.getAttribute('data-iso'),
2341
+ element.getAttribute('data-date'),
2342
+ element.getAttribute('data-day'),
2343
+ element.getAttribute('data-testid'),
2344
+ element.getAttribute('data-test-id'),
2345
+ element.getAttribute('id'),
2346
+ element.getAttribute('class'),
2347
+ element.getAttribute('title'),
2348
+ element.getAttribute('aria-label'),
2349
+ ];
2350
+
2351
+ for (const candidateValue of metadataValues) {
2352
+ const directIdentity = canonicalDateIdentityFromValue(candidateValue);
2353
+ if (directIdentity) {
2354
+ return directIdentity;
2355
+ }
2356
+ }
2357
+
2358
+ const leadingDayToken = normalizedLabel.match(/^(\d{1,2})\b/)?.[1];
2359
+ const dayValue = element.getAttribute('data-day')?.trim() || leadingDayToken;
2360
+ const monthValue =
2361
+ element.getAttribute('data-month')?.trim() ||
2362
+ element.getAttribute('data-date-month')?.trim() ||
2363
+ composedClosest(element, '[data-month], [data-date-month]')?.getAttribute?.('data-month')?.trim() ||
2364
+ composedClosest(element, '[data-month], [data-date-month]')?.getAttribute?.('data-date-month')?.trim() ||
2365
+ undefined;
2366
+ const yearValue =
2367
+ element.getAttribute('data-year')?.trim() ||
2368
+ element.getAttribute('data-date-year')?.trim() ||
2369
+ composedClosest(element, '[data-year], [data-date-year]')?.getAttribute?.('data-year')?.trim() ||
2370
+ composedClosest(element, '[data-year], [data-date-year]')?.getAttribute?.('data-date-year')?.trim() ||
2371
+ undefined;
2372
+
2373
+ return canonicalDateIdentityFromParts(yearValue, monthValue, dayValue);
2374
+ };
2375
+
2233
2376
  const inferStructuredCell = (element, surface) => {
2234
2377
  if (!isHTMLElementNode(element)) return undefined;
2235
2378
 
@@ -2243,10 +2386,22 @@ async function collectDomTargetsFromDocument(context, options) {
2243
2386
  const label =
2244
2387
  explicitLabelOf(element) || textOf(element) || seatValueLabel || syntheticLabelOf(element) || '';
2245
2388
  const normalizedLabel = label.replace(/\s+/g, ' ').trim();
2389
+ const dateIdentity = canonicalDateIdentityOf(element, normalizedLabel);
2246
2390
  const explicitDateCellMetadata =
2391
+ Boolean(dateIdentity) ||
2247
2392
  element.hasAttribute('data-day') ||
2248
2393
  element.hasAttribute('data-date') ||
2249
2394
  element.hasAttribute('data-iso') ||
2395
+ /\bdate[-_ ]?cell\b|\bcalendar[-_ ]?day\b|\b\d{4}-\d{2}-\d{2}\b/i.test(
2396
+ [
2397
+ element.getAttribute('data-testid') || '',
2398
+ element.getAttribute('data-test-id') || '',
2399
+ element.getAttribute('id') || '',
2400
+ element.getAttribute('class') || '',
2401
+ ]
2402
+ .join(' ')
2403
+ .trim()
2404
+ ) ||
2250
2405
  element.hasAttribute('aria-rowindex') ||
2251
2406
  element.hasAttribute('aria-colindex') ||
2252
2407
  Boolean(
@@ -2291,6 +2446,7 @@ async function collectDomTargetsFromDocument(context, options) {
2291
2446
  return {
2292
2447
  family: 'structured-grid',
2293
2448
  variant: structuredCellVariant,
2449
+ ...(dateIdentity ?? {}),
2294
2450
  row,
2295
2451
  column,
2296
2452
  zone,
@@ -2369,9 +2525,19 @@ async function collectDomTargetsFromDocument(context, options) {
2369
2525
  return undefined;
2370
2526
  };
2371
2527
 
2528
+ const surfacePositionTraitsCache = new WeakMap();
2529
+
2372
2530
  const surfacePositionTraitsOf = (surface) => {
2373
2531
  if (!isHTMLElementNode(surface) || !isVisible(surface)) return undefined;
2374
2532
 
2533
+ const cachedTraits = surfacePositionTraitsCache.get(surface);
2534
+ if (cachedTraits) {
2535
+ return cachedTraits;
2536
+ }
2537
+ if (cachedTraits === null) {
2538
+ return undefined;
2539
+ }
2540
+
2375
2541
  const style = window.getComputedStyle(surface);
2376
2542
  const position = (style.position || '').toLowerCase();
2377
2543
  const rect = surface.getBoundingClientRect();
@@ -2390,19 +2556,26 @@ async function collectDomTargetsFromDocument(context, options) {
2390
2556
  style.backgroundColor !== 'rgba(0, 0, 0, 0)');
2391
2557
 
2392
2558
  if (rect.width < 180 || rect.height < 72 || interactiveCount < 1 || coverage > 0.45) {
2559
+ surfacePositionTraitsCache.set(surface, null);
2393
2560
  return undefined;
2394
2561
  }
2395
2562
 
2396
2563
  if (position === 'fixed') {
2397
- return { kind: 'floating-panel', priority: 92 };
2564
+ const traits = { kind: 'floating-panel', priority: 92 };
2565
+ surfacePositionTraitsCache.set(surface, traits);
2566
+ return traits;
2398
2567
  }
2399
2568
 
2400
2569
  if (position === 'sticky') {
2401
- return { kind: 'sticky-panel', priority: 88 };
2570
+ const traits = { kind: 'sticky-panel', priority: 88 };
2571
+ surfacePositionTraitsCache.set(surface, traits);
2572
+ return traits;
2402
2573
  }
2403
2574
 
2404
2575
  if (position === 'absolute' && hasCardChrome && zIndexValue > 0) {
2405
- return { kind: 'floating-panel', priority: 82 };
2576
+ const traits = { kind: 'floating-panel', priority: 82 };
2577
+ surfacePositionTraitsCache.set(surface, traits);
2578
+ return traits;
2406
2579
  }
2407
2580
 
2408
2581
  if (
@@ -2411,33 +2584,67 @@ async function collectDomTargetsFromDocument(context, options) {
2411
2584
  interactiveCount >= 1 &&
2412
2585
  (modalBackdropAncestorOf(surface) || siblingModalBackdropOf(surface))
2413
2586
  ) {
2414
- return { kind: 'floating-panel', priority: 90 };
2587
+ const traits = { kind: 'floating-panel', priority: 90 };
2588
+ surfacePositionTraitsCache.set(surface, traits);
2589
+ return traits;
2415
2590
  }
2416
2591
 
2592
+ surfacePositionTraitsCache.set(surface, null);
2417
2593
  return undefined;
2418
2594
  };
2419
2595
 
2596
+ const inferredSurfaceKindCache = new WeakMap();
2597
+
2420
2598
  const inferSurfaceKind = (surface) => {
2421
2599
  if (!isHTMLElementNode(surface)) return undefined;
2600
+
2601
+ const cachedSurfaceKind = inferredSurfaceKindCache.get(surface);
2602
+ if (typeof cachedSurfaceKind === 'string') {
2603
+ return cachedSurfaceKind;
2604
+ }
2605
+ if (cachedSurfaceKind === null) {
2606
+ return undefined;
2607
+ }
2608
+
2422
2609
  const positionedSurface = surfacePositionTraitsOf(surface);
2423
- if (positionedSurface?.kind) return positionedSurface.kind;
2610
+ if (positionedSurface?.kind) {
2611
+ inferredSurfaceKindCache.set(surface, positionedSurface.kind);
2612
+ return positionedSurface.kind;
2613
+ }
2424
2614
  const role = surface.getAttribute('role')?.trim();
2425
- if (role === 'dialog') return 'dialog';
2426
- if (role === 'listbox') return 'listbox';
2427
- if (role === 'menu') return 'menu';
2428
- if (role === 'grid') return 'grid';
2429
- if (role === 'tabpanel') return 'tabpanel';
2430
- if (isGenericClickableElement(surface) && isStructuredContainer(surface)) return 'card';
2615
+ if (role === 'dialog') return inferredSurfaceKindCache.set(surface, 'dialog'), 'dialog';
2616
+ if (role === 'listbox') return inferredSurfaceKindCache.set(surface, 'listbox'), 'listbox';
2617
+ if (role === 'menu') return inferredSurfaceKindCache.set(surface, 'menu'), 'menu';
2618
+ if (role === 'grid') return inferredSurfaceKindCache.set(surface, 'grid'), 'grid';
2619
+ if (role === 'tabpanel') return inferredSurfaceKindCache.set(surface, 'tabpanel'), 'tabpanel';
2620
+ if (isGenericClickableElement(surface) && isStructuredContainer(surface)) {
2621
+ inferredSurfaceKindCache.set(surface, 'card');
2622
+ return 'card';
2623
+ }
2431
2624
  const className = (surface.getAttribute('class') || '').toLowerCase();
2432
- if (className.includes('calendar') || className.includes('datepicker')) return 'datepicker';
2433
- if (className.includes('popover')) return 'popover';
2434
- if (className.includes('dropdown')) return 'dropdown';
2625
+ if (className.includes('calendar') || className.includes('datepicker')) {
2626
+ inferredSurfaceKindCache.set(surface, 'datepicker');
2627
+ return 'datepicker';
2628
+ }
2629
+ if (className.includes('popover')) {
2630
+ inferredSurfaceKindCache.set(surface, 'popover');
2631
+ return 'popover';
2632
+ }
2633
+ if (className.includes('dropdown')) {
2634
+ inferredSurfaceKindCache.set(surface, 'dropdown');
2635
+ return 'dropdown';
2636
+ }
2435
2637
  const tag = surface.tagName.toLowerCase();
2436
- if (tag === 'article') return 'card';
2437
- if (tag === 'fieldset') return 'group';
2438
- if (tag === 'li') return 'listitem';
2439
- if (tag === 'section' || tag === 'form' || tag === 'aside') return tag;
2440
- return role || surface.tagName.toLowerCase();
2638
+ if (tag === 'article') return inferredSurfaceKindCache.set(surface, 'card'), 'card';
2639
+ if (tag === 'fieldset') return inferredSurfaceKindCache.set(surface, 'group'), 'group';
2640
+ if (tag === 'li') return inferredSurfaceKindCache.set(surface, 'listitem'), 'listitem';
2641
+ if (tag === 'section' || tag === 'form' || tag === 'aside') {
2642
+ inferredSurfaceKindCache.set(surface, tag);
2643
+ return tag;
2644
+ }
2645
+ const resolvedSurfaceKind = role || surface.tagName.toLowerCase();
2646
+ inferredSurfaceKindCache.set(surface, resolvedSurfaceKind || null);
2647
+ return resolvedSurfaceKind;
2441
2648
  };
2442
2649
 
2443
2650
  const labelledByTextOf = (element) => {
@@ -2611,15 +2818,29 @@ async function collectDomTargetsFromDocument(context, options) {
2611
2818
  return text.length <= 140 ? text : text.slice(0, 140);
2612
2819
  };
2613
2820
 
2821
+ const contextNodeCache = new WeakMap();
2822
+
2614
2823
  const contextNodeOf = (element) => {
2615
- if (!element) return undefined;
2824
+ if (!isHTMLElementNode(element)) return undefined;
2616
2825
 
2617
- const kind = element.getAttribute?.('role')?.trim() || element.tagName?.toLowerCase?.();
2826
+ const cachedNode = contextNodeCache.get(element);
2827
+ if (cachedNode) {
2828
+ return cachedNode;
2829
+ }
2830
+ if (cachedNode === null) {
2831
+ return undefined;
2832
+ }
2833
+
2834
+ const kind = element.getAttribute('role')?.trim() || element.tagName?.toLowerCase?.();
2618
2835
  const fallbackLabel = contextLabelOf(element);
2619
2836
  const text = contextTextOf(element);
2620
2837
  const selector = buildSelector(element);
2621
- if (!kind && !fallbackLabel && !text && !selector) return undefined;
2622
- return {
2838
+ if (!kind && !fallbackLabel && !text && !selector) {
2839
+ contextNodeCache.set(element, null);
2840
+ return undefined;
2841
+ }
2842
+
2843
+ const contextNode = {
2623
2844
  kind: kind || undefined,
2624
2845
  label: fallbackLabel,
2625
2846
  text:
@@ -2629,6 +2850,8 @@ async function collectDomTargetsFromDocument(context, options) {
2629
2850
  selector,
2630
2851
  fallbackLabel,
2631
2852
  };
2853
+ contextNodeCache.set(element, contextNode);
2854
+ return contextNode;
2632
2855
  };
2633
2856
 
2634
2857
  const pageSignature =
@@ -2646,6 +2869,23 @@ async function collectDomTargetsFromDocument(context, options) {
2646
2869
  const seenElements = new Set();
2647
2870
  const overlaySurfaceSelector =
2648
2871
  '[role="dialog"], [aria-modal="true"], [role="listbox"], [role="menu"], [role="grid"], [role="tabpanel"], [class*="popover"], [class*="dropdown"], [class*="listbox"], [class*="calendar"], [class*="datepicker"]';
2872
+ // Keep structural scans bounded without assuming shallow wrapper trees.
2873
+ const structuralAncestorSearchDepth = 16;
2874
+ const structuralProfileDebugStats = debugStructuralProfileStats
2875
+ ? {
2876
+ structuralProfileBuilds: 0,
2877
+ ownerCandidateAncestorVisits: 0,
2878
+ explicitSurfaceAncestorVisits: 0,
2879
+ surfaceSelectorAncestorVisits: 0,
2880
+ }
2881
+ : undefined;
2882
+ const structuralProfileCache = new WeakMap();
2883
+
2884
+ const incrementStructuralProfileDebugStat = (key) => {
2885
+ if (structuralProfileDebugStats) {
2886
+ structuralProfileDebugStats[key] += 1;
2887
+ }
2888
+ };
2649
2889
 
2650
2890
  const compactText = (value) => (value || '').replace(/\s+/g, ' ').trim();
2651
2891
 
@@ -2867,7 +3107,7 @@ async function collectDomTargetsFromDocument(context, options) {
2867
3107
  let preferredFallback = undefined;
2868
3108
  let current = surface;
2869
3109
  let depth = 0;
2870
- while (current && depth < 8) {
3110
+ while (current && depth < structuralAncestorSearchDepth) {
2871
3111
  if (!isHTMLElementNode(current) || !isVisible(current)) {
2872
3112
  current = current?.parentElement ?? null;
2873
3113
  depth += 1;
@@ -3138,7 +3378,7 @@ async function collectDomTargetsFromDocument(context, options) {
3138
3378
  for (const tokenNode of tokenNodes) {
3139
3379
  let current = composedParentElement(tokenNode);
3140
3380
  let depth = 0;
3141
- while (current && depth < 8) {
3381
+ while (current && depth < structuralAncestorSearchDepth) {
3142
3382
  if (isPotentialVisualGridSurface(current)) {
3143
3383
  candidateSurfaces.set(
3144
3384
  current,
@@ -3190,66 +3430,67 @@ async function collectDomTargetsFromDocument(context, options) {
3190
3430
  .filter((element) => isHTMLElementNode(element))
3191
3431
  .filter((element) => isVisible(element));
3192
3432
 
3193
- const localSurfaceCandidateOf = (element) => {
3194
- const ranked = [];
3195
- const pushCandidate = (candidate, depth) => {
3196
- if (
3197
- !isHTMLElementNode(candidate) ||
3198
- candidate === element ||
3199
- !isVisible(candidate)
3200
- ) {
3201
- return;
3202
- }
3433
+ const explicitSurfaceKindOf = (surface) => {
3434
+ if (!isHTMLElementNode(surface)) {
3435
+ return undefined;
3436
+ }
3203
3437
 
3204
- const priority = surfacePriorityOf(candidate);
3205
- if (priority <= 0) {
3206
- return;
3207
- }
3438
+ const positionedSurface = surfacePositionTraitsOf(surface);
3439
+ if (positionedSurface?.kind) {
3440
+ return positionedSurface.kind;
3441
+ }
3208
3442
 
3209
- if (ranked.some((entry) => entry.element === candidate)) {
3210
- return;
3211
- }
3443
+ const role = surface.getAttribute('role')?.trim() || '';
3444
+ if (role === 'dialog') return 'dialog';
3445
+ if (role === 'listbox') return 'listbox';
3446
+ if (role === 'menu') return 'menu';
3447
+ if (role === 'grid') return 'grid';
3448
+ if (role === 'tabpanel') return 'tabpanel';
3212
3449
 
3213
- ranked.push({ element: candidate, priority, depth });
3214
- };
3450
+ const className = (surface.getAttribute('class') || '').toLowerCase();
3451
+ if (className.includes('calendar') || className.includes('datepicker')) return 'datepicker';
3452
+ if (className.includes('popover')) return 'popover';
3453
+ if (className.includes('dropdown')) return 'dropdown';
3215
3454
 
3216
- pushCandidate(itemOf(element), 0);
3217
- pushCandidate(containerOf(element), 1);
3455
+ const tag = surface.tagName.toLowerCase();
3456
+ if (tag === 'form') return 'form';
3457
+ return undefined;
3458
+ };
3218
3459
 
3460
+ const explicitSurfaceOf = (element) => {
3219
3461
  let current = composedParentElement(element);
3220
3462
  let depth = 0;
3221
- while (current && depth < 16) {
3222
- pushCandidate(current, depth + 2);
3463
+
3464
+ while (current && depth < structuralAncestorSearchDepth) {
3465
+ incrementStructuralProfileDebugStat('explicitSurfaceAncestorVisits');
3466
+ if (structuralProfileOf(current)?.explicitSurfaceKind) {
3467
+ return current;
3468
+ }
3223
3469
  current = composedParentElement(current);
3224
3470
  depth += 1;
3225
3471
  }
3226
3472
 
3227
- ranked.sort((left, right) => {
3228
- if (left.priority !== right.priority) {
3229
- return right.priority - left.priority;
3230
- }
3231
- return left.depth - right.depth;
3232
- });
3233
-
3234
- return ranked[0]?.element;
3473
+ return undefined;
3235
3474
  };
3236
3475
 
3237
- const surfaceSelectorsOf = (element, localSurface) => {
3476
+ const surfaceSelectorsOf = (element, explicitSurface) => {
3238
3477
  const selectors = [];
3239
- let current = element.parentElement;
3478
+ let current = composedParentElement(element);
3240
3479
 
3241
3480
  while (current) {
3242
- if (current.matches?.(overlaySurfaceSelector)) {
3243
- const selector = buildSelector(current);
3481
+ incrementStructuralProfileDebugStat('surfaceSelectorAncestorVisits');
3482
+ const currentProfile = structuralProfileOf(current);
3483
+ if (currentProfile?.explicitSurfaceKind) {
3484
+ const selector = currentProfile.selector;
3244
3485
  if (selector && !selectors.includes(selector)) {
3245
3486
  selectors.push(selector);
3246
3487
  }
3247
3488
  }
3248
- current = current.parentElement;
3489
+ current = composedParentElement(current);
3249
3490
  }
3250
3491
 
3251
- if (localSurface) {
3252
- const selector = buildSelector(localSurface);
3492
+ if (explicitSurface) {
3493
+ const selector = structuralProfileOf(explicitSurface)?.selector;
3253
3494
  if (selector && !selectors.includes(selector)) {
3254
3495
  selectors.push(selector);
3255
3496
  }
@@ -3258,6 +3499,413 @@ async function collectDomTargetsFromDocument(context, options) {
3258
3499
  return selectors.length > 0 ? selectors : undefined;
3259
3500
  };
3260
3501
 
3502
+ const directContextTextProfileCache = new WeakMap();
3503
+
3504
+ const directContextTextProfileOf = (candidate) => {
3505
+ if (!isHTMLElementNode(candidate)) {
3506
+ return undefined;
3507
+ }
3508
+
3509
+ const cachedProfile = directContextTextProfileCache.get(candidate);
3510
+ if (cachedProfile) {
3511
+ return cachedProfile;
3512
+ }
3513
+ if (cachedProfile === null) {
3514
+ return undefined;
3515
+ }
3516
+
3517
+ const children = [];
3518
+ for (const child of Array.from(candidate.children).slice(0, 12)) {
3519
+ if (!isHTMLElementNode(child) || !isVisible(child)) {
3520
+ continue;
3521
+ }
3522
+
3523
+ const tag = child.tagName.toLowerCase();
3524
+ if (
3525
+ ['button', 'input', 'textarea', 'select', 'a', 'svg', 'img'].includes(tag) ||
3526
+ child.matches?.(selector)
3527
+ ) {
3528
+ continue;
3529
+ }
3530
+
3531
+ const normalized = normalizeDescriptorText(textOf(child, { container: true }));
3532
+ if (!normalized) {
3533
+ continue;
3534
+ }
3535
+
3536
+ const semanticText = normalized
3537
+ .replace(observedLooseActionTextRe, ' ')
3538
+ .replace(/\s+/g, ' ')
3539
+ .trim();
3540
+ if (!semanticText || semanticText.length < 2) {
3541
+ continue;
3542
+ }
3543
+
3544
+ children.push(child);
3545
+ }
3546
+
3547
+ const profile = {
3548
+ count: children.length,
3549
+ children,
3550
+ };
3551
+ directContextTextProfileCache.set(candidate, profile);
3552
+ return profile;
3553
+ };
3554
+
3555
+ const directContextTextBlockCountOf = (candidate, excludedTarget) => {
3556
+ const profile = directContextTextProfileOf(candidate);
3557
+ if (!profile) {
3558
+ return 0;
3559
+ }
3560
+
3561
+ if (
3562
+ isHTMLElementNode(excludedTarget) &&
3563
+ profile.children.includes(excludedTarget)
3564
+ ) {
3565
+ return Math.max(0, profile.count - 1);
3566
+ }
3567
+
3568
+ return profile.count;
3569
+ };
3570
+
3571
+ const ownerCandidateShapeKeyCache = new WeakMap();
3572
+
3573
+ const ownerCandidateShapeKeyOf = (candidate) => {
3574
+ const cachedShapeKey = ownerCandidateShapeKeyCache.get(candidate);
3575
+ if (typeof cachedShapeKey === 'string') {
3576
+ return cachedShapeKey;
3577
+ }
3578
+
3579
+ const tag = candidate.tagName.toLowerCase();
3580
+ const role = candidate.getAttribute('role')?.trim().toLowerCase() || '';
3581
+ const className = (candidate.getAttribute('class') || '')
3582
+ .trim()
3583
+ .toLowerCase()
3584
+ .split(/\s+/)
3585
+ .filter(Boolean)
3586
+ .sort()
3587
+ .join('.');
3588
+ const shapeKey = tag + '|' + role + '|' + className;
3589
+ ownerCandidateShapeKeyCache.set(candidate, shapeKey);
3590
+ return shapeKey;
3591
+ };
3592
+
3593
+ const ownerCandidatePeerCountOf = (
3594
+ candidate,
3595
+ directTextBlockCount,
3596
+ descendantInteractiveCount
3597
+ ) => {
3598
+ const parent = composedParentElement(candidate);
3599
+ if (!isHTMLElementNode(parent)) {
3600
+ return 0;
3601
+ }
3602
+
3603
+ const candidateShapeKey = ownerCandidateShapeKeyOf(candidate);
3604
+ const candidateChildCount = candidate.children.length;
3605
+ let peerCount = 0;
3606
+
3607
+ for (const sibling of Array.from(parent.children)) {
3608
+ if (!isHTMLElementNode(sibling) || sibling === candidate || !isVisible(sibling)) {
3609
+ continue;
3610
+ }
3611
+ if (ownerCandidateShapeKeyOf(sibling) !== candidateShapeKey) {
3612
+ continue;
3613
+ }
3614
+
3615
+ const siblingInteractiveCount = visibleInteractiveDescendantCountOf(sibling);
3616
+ const siblingTextBlockCount = directContextTextBlockCountOf(sibling);
3617
+ if (Math.abs(siblingInteractiveCount - descendantInteractiveCount) > 1) {
3618
+ continue;
3619
+ }
3620
+ if (Math.abs(siblingTextBlockCount - directTextBlockCount) > 1) {
3621
+ continue;
3622
+ }
3623
+ if (Math.abs(sibling.children.length - candidateChildCount) > 1) {
3624
+ continue;
3625
+ }
3626
+
3627
+ peerCount += 1;
3628
+ }
3629
+
3630
+ return peerCount;
3631
+ };
3632
+
3633
+ const ownerBoundaryMetaOf = (candidate, coverage, descendantInteractiveCount) => {
3634
+ if (!isHTMLElementNode(candidate)) {
3635
+ return undefined;
3636
+ }
3637
+
3638
+ const positionedSurface = surfacePositionTraitsOf(candidate);
3639
+ if (positionedSurface?.kind) {
3640
+ return {
3641
+ kind: positionedSurface.kind,
3642
+ stop: 'hard',
3643
+ };
3644
+ }
3645
+
3646
+ const role = candidate.getAttribute('role')?.trim().toLowerCase() || '';
3647
+ if (role === 'dialog' || candidate.getAttribute('aria-modal') === 'true') {
3648
+ return {
3649
+ kind: 'dialog',
3650
+ stop: 'hard',
3651
+ };
3652
+ }
3653
+ if (['listbox', 'menu', 'grid', 'tabpanel'].includes(role)) {
3654
+ return {
3655
+ kind: role,
3656
+ stop: 'soft',
3657
+ };
3658
+ }
3659
+
3660
+ const tag = candidate.tagName.toLowerCase();
3661
+ if (tag === 'form') {
3662
+ return {
3663
+ kind: 'form',
3664
+ stop: 'hard',
3665
+ };
3666
+ }
3667
+
3668
+ const className = (candidate.getAttribute('class') || '').toLowerCase();
3669
+ if (className.includes('calendar') || className.includes('datepicker')) {
3670
+ return {
3671
+ kind: 'datepicker',
3672
+ stop: 'hard',
3673
+ };
3674
+ }
3675
+ if (className.includes('popover')) {
3676
+ return {
3677
+ kind: 'popover',
3678
+ stop: 'hard',
3679
+ };
3680
+ }
3681
+ if (className.includes('dropdown')) {
3682
+ return {
3683
+ kind: 'dropdown',
3684
+ stop: 'hard',
3685
+ };
3686
+ }
3687
+
3688
+ if (
3689
+ ['main', 'aside', 'section'].includes(tag) &&
3690
+ (coverage >= 0.24 || descendantInteractiveCount > 4)
3691
+ ) {
3692
+ return {
3693
+ kind: tag,
3694
+ stop: 'hard',
3695
+ };
3696
+ }
3697
+
3698
+ return undefined;
3699
+ };
3700
+
3701
+ const structuralProfileOf = (element) => {
3702
+ if (!isHTMLElementNode(element) || !isVisible(element)) {
3703
+ return undefined;
3704
+ }
3705
+
3706
+ const cachedProfile = structuralProfileCache.get(element);
3707
+ if (cachedProfile) {
3708
+ return cachedProfile;
3709
+ }
3710
+ if (cachedProfile === null) {
3711
+ return undefined;
3712
+ }
3713
+
3714
+ const rect = element.getBoundingClientRect();
3715
+ const area = Math.max(rect.width * rect.height, 1);
3716
+ const coverage = area / viewportArea();
3717
+ const descendantInteractiveCount = visibleInteractiveDescendantCountOf(element);
3718
+ const descendantEditableCount = descendantEditableCountOf(element);
3719
+ const boundaryMeta = ownerBoundaryMetaOf(
3720
+ element,
3721
+ coverage,
3722
+ descendantInteractiveCount
3723
+ );
3724
+ const profile = {
3725
+ selector: buildSelector(element),
3726
+ area,
3727
+ coverage,
3728
+ descendantInteractiveCount,
3729
+ descendantEditableCount,
3730
+ directTextBlockCount: directContextTextBlockCountOf(element),
3731
+ boundaryMeta,
3732
+ explicitSurfaceKind: explicitSurfaceKindOf(element),
3733
+ inferredSurfaceKind: inferSurfaceKind(element),
3734
+ contextNode: contextNodeOf(element),
3735
+ surfacePriority: surfacePriorityOf(element),
3736
+ tag: element.tagName.toLowerCase(),
3737
+ role: element.getAttribute('role')?.trim().toLowerCase() || '',
3738
+ };
3739
+ structuralProfileCache.set(element, profile);
3740
+ incrementStructuralProfileDebugStat('structuralProfileBuilds');
3741
+ return profile;
3742
+ };
3743
+
3744
+ const ownerCandidateSurfacePriorityOf = (surfaceKind, fallbackPriority) => {
3745
+ if (typeof fallbackPriority === 'number' && fallbackPriority > 0) {
3746
+ return fallbackPriority;
3747
+ }
3748
+
3749
+ switch ((surfaceKind || '').toLowerCase()) {
3750
+ case 'dialog':
3751
+ return 100;
3752
+ case 'listbox':
3753
+ case 'menu':
3754
+ return 95;
3755
+ case 'floating-panel':
3756
+ return 92;
3757
+ case 'datepicker':
3758
+ return 90;
3759
+ case 'sticky-panel':
3760
+ return 88;
3761
+ case 'popover':
3762
+ case 'dropdown':
3763
+ return 85;
3764
+ case 'grid':
3765
+ case 'tabpanel':
3766
+ return 80;
3767
+ case 'form':
3768
+ return 70;
3769
+ case 'card':
3770
+ return 58;
3771
+ case 'group':
3772
+ return 55;
3773
+ case 'section':
3774
+ case 'aside':
3775
+ return 52;
3776
+ case 'listitem':
3777
+ case 'row':
3778
+ return 48;
3779
+ default:
3780
+ return undefined;
3781
+ }
3782
+ };
3783
+
3784
+ const ownerCandidateSurfaceKindOf = (candidateProfile, metrics) => {
3785
+ if (!candidateProfile) {
3786
+ return undefined;
3787
+ }
3788
+
3789
+ if (metrics.boundaryKind) {
3790
+ return metrics.boundaryKind;
3791
+ }
3792
+
3793
+ const inferredSurfaceKind = candidateProfile.inferredSurfaceKind;
3794
+ if (inferredSurfaceKind && inferredSurfaceKind !== 'div') {
3795
+ return inferredSurfaceKind;
3796
+ }
3797
+
3798
+ const tag = candidateProfile.tag;
3799
+ const role = candidateProfile.role;
3800
+
3801
+ if (tag === 'article') {
3802
+ return 'card';
3803
+ }
3804
+ if (tag === 'fieldset') {
3805
+ return 'group';
3806
+ }
3807
+ if (tag === 'li' || role === 'listitem') {
3808
+ return 'listitem';
3809
+ }
3810
+ if (role === 'row') {
3811
+ return 'row';
3812
+ }
3813
+
3814
+ const itemLikeCard =
3815
+ metrics.directTextBlockCount >= 2 &&
3816
+ metrics.descendantInteractiveCount >= 1 &&
3817
+ metrics.descendantInteractiveCount <= 4 &&
3818
+ metrics.coverage <= 0.45 &&
3819
+ metrics.relativeAreaToTarget >= 2 &&
3820
+ (metrics.peerCount >= 1 || metrics.descendantInteractiveCount === 1);
3821
+ if (itemLikeCard) {
3822
+ return 'card';
3823
+ }
3824
+
3825
+ return inferredSurfaceKind;
3826
+ };
3827
+
3828
+ const ownerCandidatesOf = (target) => {
3829
+ const targetRect = target.getBoundingClientRect();
3830
+ const targetArea = Math.max(targetRect.width * targetRect.height, 1);
3831
+ const candidates = [];
3832
+ let current = composedParentElement(target);
3833
+ let depth = 0;
3834
+
3835
+ while (current && depth < structuralAncestorSearchDepth) {
3836
+ incrementStructuralProfileDebugStat('ownerCandidateAncestorVisits');
3837
+ const candidateProfile = structuralProfileOf(current);
3838
+ if (!candidateProfile) {
3839
+ current = composedParentElement(current);
3840
+ depth += 1;
3841
+ continue;
3842
+ }
3843
+
3844
+ const selectorValue = candidateProfile.selector;
3845
+ if (!selectorValue) {
3846
+ current = composedParentElement(current);
3847
+ depth += 1;
3848
+ continue;
3849
+ }
3850
+
3851
+ const candidateArea = candidateProfile.area;
3852
+ const coverage = candidateProfile.coverage;
3853
+ const descendantInteractiveCount = candidateProfile.descendantInteractiveCount;
3854
+ const descendantEditableCount = candidateProfile.descendantEditableCount;
3855
+ const directTextBlockCount = directContextTextBlockCountOf(current, target);
3856
+ const peerCount = ownerCandidatePeerCountOf(
3857
+ current,
3858
+ directTextBlockCount,
3859
+ descendantInteractiveCount
3860
+ );
3861
+ const boundaryMeta = candidateProfile.boundaryMeta;
3862
+ const boundaryKind = boundaryMeta?.kind;
3863
+ const surfaceKind = ownerCandidateSurfaceKindOf(candidateProfile, {
3864
+ boundaryKind,
3865
+ coverage,
3866
+ directTextBlockCount,
3867
+ descendantInteractiveCount,
3868
+ peerCount,
3869
+ relativeAreaToTarget: candidateArea / targetArea,
3870
+ });
3871
+ const shell =
3872
+ surfaceKind === 'sticky-panel' ||
3873
+ surfaceKind === 'floating-panel' ||
3874
+ (['main', 'aside', 'section'].includes(candidateProfile.tag) &&
3875
+ (coverage >= 0.24 || descendantInteractiveCount > 4));
3876
+ const boundary = Boolean(boundaryKind) || shell;
3877
+ const stopSearch = shell || boundaryMeta?.stop === 'hard';
3878
+ const contextNode = candidateProfile.contextNode;
3879
+ candidates.push({
3880
+ ...contextNode,
3881
+ selector: selectorValue,
3882
+ depth,
3883
+ surfaceKind,
3884
+ surfacePriority: ownerCandidateSurfacePriorityOf(
3885
+ surfaceKind,
3886
+ candidateProfile.surfacePriority
3887
+ ),
3888
+ directTextBlockCount,
3889
+ peerCount,
3890
+ descendantInteractiveCount,
3891
+ descendantEditableCount,
3892
+ viewportCoverage: coverage,
3893
+ relativeAreaToTarget: candidateArea / targetArea,
3894
+ boundary,
3895
+ shell,
3896
+ });
3897
+
3898
+ if (stopSearch) {
3899
+ break;
3900
+ }
3901
+
3902
+ current = composedParentElement(current);
3903
+ depth += 1;
3904
+ }
3905
+
3906
+ return candidates.length > 0 ? candidates : undefined;
3907
+ };
3908
+
3261
3909
  const targets = elements.map((element, ordinal) => {
3262
3910
  const visualSeatGrid = visualSeatGridMeta.get(element);
3263
3911
  const associatedChoiceControl = associatedChoiceControlOf(element);
@@ -3289,16 +3937,9 @@ async function collectDomTargetsFromDocument(context, options) {
3289
3937
  const item =
3290
3938
  (compactClickableAffordance ? compactClickableItemOf(element) : undefined) ||
3291
3939
  itemOf(element);
3292
- const overlaySurface = composedClosest(element, overlaySurfaceSelector);
3293
- const localSurface = localSurfaceCandidateOf(element);
3294
- const selfSurface =
3295
- genericClickable && !associatedChoiceControl && isStructuredContainer(element)
3296
- ? element
3297
- : undefined;
3298
- const surface =
3299
- visualSeatGrid?.surface ||
3300
- (isHTMLElementNode(overlaySurface) ? overlaySurface : localSurface || selfSurface);
3301
- const surfaceSelectors = surfaceSelectorsOf(element, localSurface || selfSurface);
3940
+ const explicitSurface = explicitSurfaceOf(element);
3941
+ const surface = visualSeatGrid?.surface || explicitSurface;
3942
+ const surfaceSelectors = surfaceSelectorsOf(element, explicitSurface);
3302
3943
  const structure = visualSeatGrid?.structure || inferStructuredCell(element, surface);
3303
3944
  const directFallbackLabel = explicitLabelOf(element) || looseFieldLabelOf(element);
3304
3945
  const directionalFallbackLabel = directionalControlFallbackLabelOf(
@@ -3309,10 +3950,23 @@ async function collectDomTargetsFromDocument(context, options) {
3309
3950
  const currentValue = popupCurrentValueOf(element);
3310
3951
  const selection = selectionOf(element);
3311
3952
  const role = inferRole(element) || inferRole(associatedChoiceControl);
3312
- const surfaceKind = visualSeatGrid?.surfaceKind || surfaceKindOf(surface);
3953
+ const elementProfile = structuralProfileOf(element);
3954
+ const surfaceProfile = surface ? structuralProfileOf(surface) : undefined;
3955
+ const explicitSurfaceProfile = explicitSurface
3956
+ ? structuralProfileOf(explicitSurface)
3957
+ : undefined;
3958
+ const surfaceKind =
3959
+ visualSeatGrid?.surfaceKind ||
3960
+ surfaceProfile?.inferredSurfaceKind ||
3961
+ surfaceKindOf(surface);
3313
3962
  const fallbackSurfaceLabel =
3314
3963
  (visualSeatGrid?.hintText ? 'Seat map' : undefined) ||
3315
3964
  surfaceFallbackLabelOf(surface, surfaceKind);
3965
+ const explicitSurfaceKind = explicitSurfaceProfile?.explicitSurfaceKind;
3966
+ const explicitFallbackSurfaceLabel =
3967
+ explicitSurface && explicitSurfaceKind
3968
+ ? surfaceFallbackLabelOf(explicitSurface, explicitSurfaceKind)
3969
+ : undefined;
3316
3970
  const form = composedClosest(element, 'form');
3317
3971
  const testIdAttribute = element.hasAttribute('data-testid')
3318
3972
  ? 'data-testid'
@@ -3362,17 +4016,28 @@ async function collectDomTargetsFromDocument(context, options) {
3362
4016
  surfaceKind,
3363
4017
  surfaceLabel: fallbackSurfaceLabel,
3364
4018
  fallbackSurfaceLabel,
3365
- surfaceSelector: surface ? buildSelector(surface) : undefined,
4019
+ surfaceSelector: surfaceProfile?.selector,
3366
4020
  surfaceSelectors,
3367
- surfacePriority: surfacePriorityOf(surface),
4021
+ surfacePriority: surfaceProfile?.surfacePriority ?? surfacePriorityOf(surface),
4022
+ explicitSurfaceKind,
4023
+ explicitSurfaceLabel: explicitFallbackSurfaceLabel,
4024
+ explicitFallbackSurfaceLabel,
4025
+ explicitSurfaceSelector: explicitSurfaceProfile?.selector,
4026
+ explicitSurfaceSelectors: surfaceSelectors,
4027
+ explicitSurfacePriority:
4028
+ explicitSurfaceProfile?.surfacePriority ??
4029
+ (explicitSurface ? surfacePriorityOf(explicitSurface) : undefined),
3368
4030
  controlsSurfaceSelector:
3369
4031
  selectorFromRelation(element, 'aria-controls') || selectorFromRelation(element, 'aria-owns'),
3370
4032
  formSelector: form ? buildSelector(form) : undefined,
3371
4033
  pageFormSelector: form ? buildSelector(form) : inheritedPageFormSelector || undefined,
3372
- descendantInteractiveCount: element.querySelectorAll(selector).length,
3373
- descendantEditableCount: element.querySelectorAll(
3374
- 'input:not([type="hidden"]), textarea, select, [contenteditable="true"]'
3375
- ).length,
4034
+ descendantInteractiveCount:
4035
+ elementProfile?.descendantInteractiveCount ?? element.querySelectorAll(selector).length,
4036
+ descendantEditableCount:
4037
+ elementProfile?.descendantEditableCount ??
4038
+ element.querySelectorAll(
4039
+ 'input:not([type="hidden"]), textarea, select, [contenteditable="true"]'
4040
+ ).length,
3376
4041
  structure,
3377
4042
  ordinal,
3378
4043
  context: {
@@ -3387,11 +4052,15 @@ async function collectDomTargetsFromDocument(context, options) {
3387
4052
  hintText: describedByTextOf(element),
3388
4053
  fallbackHintText: visualSeatGrid?.hintText,
3389
4054
  visual: visualOf(element),
4055
+ ownerCandidates: ownerCandidatesOf(element),
3390
4056
  },
3391
4057
  };
3392
4058
  });
3393
4059
 
3394
- return targets;
4060
+ return {
4061
+ targets,
4062
+ debugStats: structuralProfileDebugStats,
4063
+ };
3395
4064
  };
3396
4065
 
3397
4066
  const enrichDisplayLabels = (targets) => {
@@ -3494,8 +4163,9 @@ async function collectDomTargetsFromDocument(context, options) {
3494
4163
  return score;
3495
4164
  };
3496
4165
 
4166
+ const collectionResult = collectTargets(document);
3497
4167
  const seen = new Set();
3498
- return enrichDisplayLabels(collectTargets(document))
4168
+ const finalTargets = enrichDisplayLabels(collectionResult.targets)
3499
4169
  .filter((candidate) => {
3500
4170
  const frameKey = candidate.framePath ? candidate.framePath.join(' -> ') : 'top';
3501
4171
  const key = frameKey + '|' + (candidate.selector || candidate.domSignature || '');
@@ -3524,18 +4194,55 @@ async function collectDomTargetsFromDocument(context, options) {
3524
4194
  return left.index - right.index;
3525
4195
  })
3526
4196
  .map(({ candidate }) => candidate);
4197
+
4198
+ return debugStructuralProfileStats
4199
+ ? {
4200
+ targets: finalTargets,
4201
+ debugStats: collectionResult.debugStats,
4202
+ }
4203
+ : finalTargets;
3527
4204
  })()`);
3528
- if (!Array.isArray(observedTargets)) {
3529
- return [];
4205
+ let observedTargets = [];
4206
+ let debugStats;
4207
+ if (Array.isArray(observedPayload)) {
4208
+ observedTargets = observedPayload;
4209
+ }
4210
+ else if (observedPayload &&
4211
+ typeof observedPayload === 'object' &&
4212
+ Array.isArray(observedPayload.targets)) {
4213
+ observedTargets = observedPayload.targets;
4214
+ const candidateDebugStats = observedPayload.debugStats;
4215
+ if (candidateDebugStats &&
4216
+ typeof candidateDebugStats === 'object' &&
4217
+ typeof candidateDebugStats
4218
+ .structuralProfileBuilds === 'number' &&
4219
+ typeof candidateDebugStats
4220
+ .ownerCandidateAncestorVisits === 'number' &&
4221
+ typeof candidateDebugStats
4222
+ .explicitSurfaceAncestorVisits === 'number' &&
4223
+ typeof candidateDebugStats
4224
+ .surfaceSelectorAncestorVisits === 'number') {
4225
+ debugStats = candidateDebugStats;
4226
+ }
3530
4227
  }
3531
- return observedTargets
3532
- .filter((target) => Boolean(target && typeof target === 'object' && typeof target.kind === 'string'))
4228
+ const targets = observedTargets
4229
+ .filter((target) => Boolean(target &&
4230
+ typeof target === 'object' &&
4231
+ typeof target.kind === 'string'))
3533
4232
  .map((target) => enrichObservedTargetSemantics(applyInheritedDomTargetMetadata(target, {
3534
4233
  framePath: options?.framePath,
3535
4234
  frameUrl: options?.frameUrl,
3536
4235
  pageSignature: options?.pageSignature,
3537
4236
  pageFormSelector: options?.pageFormSelector,
3538
4237
  })));
4238
+ return {
4239
+ targets,
4240
+ debugStats,
4241
+ };
4242
+ }
4243
+ async function collectDomTargetsFromDocument(context, options) {
4244
+ const { targets } = await collectDomTargetsFromDocumentRaw(context, options);
4245
+ return targets;
3539
4246
  }
3540
4247
  const FRAME_HOST_DESCRIPTOR_SCRIPT = String.raw `
3541
4248
  const ownerWindowOf = (node) => node?.ownerDocument?.defaultView || window;
@@ -3856,6 +4563,10 @@ export async function collectDomTargets(page, options) {
3856
4563
  }
3857
4564
  export const __testDomTargetCollection = {
3858
4565
  collectDomTargetsFromDocument,
4566
+ collectDomTargetsDebugFromDocument: (context, options) => collectDomTargetsFromDocumentRaw(context, {
4567
+ ...options,
4568
+ debugStructuralProfileStats: true,
4569
+ }),
3859
4570
  inferStructuredCellVariantFromEvidence,
3860
4571
  inferDirectionalControlFallbackFromEvidence,
3861
4572
  locatorDomSignatureScript: LOCATOR_DOM_SIGNATURE_SCRIPT,