agent-web-interface 4.3.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/browser/connection-utils.d.ts +48 -0
- package/dist/src/browser/connection-utils.d.ts.map +1 -0
- package/dist/src/browser/connection-utils.js +129 -0
- package/dist/src/browser/connection-utils.js.map +1 -0
- package/dist/src/browser/index.d.ts +3 -1
- package/dist/src/browser/index.d.ts.map +1 -1
- package/dist/src/browser/index.js +2 -1
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/browser/session-manager.d.ts +1 -89
- package/dist/src/browser/session-manager.d.ts.map +1 -1
- package/dist/src/browser/session-manager.js +1 -116
- package/dist/src/browser/session-manager.js.map +1 -1
- package/dist/src/browser/session-manager.types.d.ts +90 -0
- package/dist/src/browser/session-manager.types.d.ts.map +1 -0
- package/dist/src/browser/session-manager.types.js +7 -0
- package/dist/src/browser/session-manager.types.js.map +1 -0
- package/dist/src/form/constraint-extraction.d.ts +31 -0
- package/dist/src/form/constraint-extraction.d.ts.map +1 -0
- package/dist/src/form/constraint-extraction.js +110 -0
- package/dist/src/form/constraint-extraction.js.map +1 -0
- package/dist/src/form/field-extractor.d.ts.map +1 -1
- package/dist/src/form/field-extractor.js +3 -444
- package/dist/src/form/field-extractor.js.map +1 -1
- package/dist/src/form/field-state-extractor.d.ts +22 -0
- package/dist/src/form/field-state-extractor.d.ts.map +1 -0
- package/dist/src/form/field-state-extractor.js +55 -0
- package/dist/src/form/field-state-extractor.js.map +1 -0
- package/dist/src/form/form-actions.d.ts +45 -0
- package/dist/src/form/form-actions.d.ts.map +1 -0
- package/dist/src/form/form-actions.js +108 -0
- package/dist/src/form/form-actions.js.map +1 -0
- package/dist/src/form/form-detector.d.ts +0 -36
- package/dist/src/form/form-detector.d.ts.map +1 -1
- package/dist/src/form/form-detector.js +11 -376
- package/dist/src/form/form-detector.js.map +1 -1
- package/dist/src/form/input-clustering.d.ts +15 -0
- package/dist/src/form/input-clustering.d.ts.map +1 -0
- package/dist/src/form/input-clustering.js +61 -0
- package/dist/src/form/input-clustering.js.map +1 -0
- package/dist/src/form/intent-inference.d.ts +28 -0
- package/dist/src/form/intent-inference.d.ts.map +1 -0
- package/dist/src/form/intent-inference.js +137 -0
- package/dist/src/form/intent-inference.js.map +1 -0
- package/dist/src/form/purpose-inference.d.ts +50 -0
- package/dist/src/form/purpose-inference.d.ts.map +1 -0
- package/dist/src/form/purpose-inference.js +313 -0
- package/dist/src/form/purpose-inference.js.map +1 -0
- package/dist/src/form/submit-detection.d.ts +36 -0
- package/dist/src/form/submit-detection.d.ts.map +1 -0
- package/dist/src/form/submit-detection.js +101 -0
- package/dist/src/form/submit-detection.js.map +1 -0
- package/dist/src/form/types.d.ts +2 -2
- package/dist/src/index.js +52 -47
- package/dist/src/index.js.map +1 -1
- package/dist/src/observation/observation-accumulator.d.ts +1 -1
- package/dist/src/observation/observation-accumulator.js +1 -1
- package/dist/src/observation/observer-script.d.ts +1 -1
- package/dist/src/observation/observer-script.d.ts.map +1 -1
- package/dist/src/observation/observer-script.js +129 -7
- package/dist/src/observation/observer-script.js.map +1 -1
- package/dist/src/query/disambiguation.d.ts +18 -0
- package/dist/src/query/disambiguation.d.ts.map +1 -0
- package/dist/src/query/disambiguation.js +123 -0
- package/dist/src/query/disambiguation.js.map +1 -0
- package/dist/src/query/fuzzy-match.d.ts +17 -0
- package/dist/src/query/fuzzy-match.d.ts.map +1 -0
- package/dist/src/query/fuzzy-match.js +34 -0
- package/dist/src/query/fuzzy-match.js.map +1 -0
- package/dist/src/query/index.d.ts +3 -0
- package/dist/src/query/index.d.ts.map +1 -1
- package/dist/src/query/index.js +6 -0
- package/dist/src/query/index.js.map +1 -1
- package/dist/src/query/query-engine.d.ts +0 -35
- package/dist/src/query/query-engine.d.ts.map +1 -1
- package/dist/src/query/query-engine.js +9 -309
- package/dist/src/query/query-engine.js.map +1 -1
- package/dist/src/query/scoring.d.ts +52 -0
- package/dist/src/query/scoring.d.ts.map +1 -0
- package/dist/src/query/scoring.js +162 -0
- package/dist/src/query/scoring.js.map +1 -0
- package/dist/src/snapshot/element-resolver.d.ts +24 -13
- package/dist/src/snapshot/element-resolver.d.ts.map +1 -1
- package/dist/src/snapshot/element-resolver.js +117 -87
- package/dist/src/snapshot/element-resolver.js.map +1 -1
- package/dist/src/snapshot/extractors/ax-extractor.d.ts +1 -1
- package/dist/src/snapshot/extractors/ax-extractor.d.ts.map +1 -1
- package/dist/src/snapshot/extractors/ax-extractor.js +4 -1
- package/dist/src/snapshot/extractors/ax-extractor.js.map +1 -1
- package/dist/src/snapshot/extractors/index.d.ts +1 -1
- package/dist/src/snapshot/extractors/index.d.ts.map +1 -1
- package/dist/src/snapshot/extractors/index.js +1 -1
- package/dist/src/snapshot/extractors/index.js.map +1 -1
- package/dist/src/snapshot/extractors/region-resolver.d.ts.map +1 -1
- package/dist/src/snapshot/extractors/region-resolver.js +8 -0
- package/dist/src/snapshot/extractors/region-resolver.js.map +1 -1
- package/dist/src/snapshot/extractors/types.d.ts +8 -0
- package/dist/src/snapshot/extractors/types.d.ts.map +1 -1
- package/dist/src/snapshot/extractors/types.js +16 -0
- package/dist/src/snapshot/extractors/types.js.map +1 -1
- package/dist/src/snapshot/frame-context.d.ts +68 -0
- package/dist/src/snapshot/frame-context.d.ts.map +1 -0
- package/dist/src/snapshot/frame-context.js +131 -0
- package/dist/src/snapshot/frame-context.js.map +1 -0
- package/dist/src/snapshot/heading-index.d.ts +28 -0
- package/dist/src/snapshot/heading-index.d.ts.map +1 -0
- package/dist/src/snapshot/heading-index.js +108 -0
- package/dist/src/snapshot/heading-index.js.map +1 -0
- package/dist/src/snapshot/index.d.ts +5 -3
- package/dist/src/snapshot/index.d.ts.map +1 -1
- package/dist/src/snapshot/index.js +3 -2
- package/dist/src/snapshot/index.js.map +1 -1
- package/dist/src/snapshot/kind-mapping.d.ts +30 -0
- package/dist/src/snapshot/kind-mapping.d.ts.map +1 -0
- package/dist/src/snapshot/kind-mapping.js +114 -0
- package/dist/src/snapshot/kind-mapping.js.map +1 -0
- package/dist/src/snapshot/node-filter.d.ts +31 -0
- package/dist/src/snapshot/node-filter.d.ts.map +1 -0
- package/dist/src/snapshot/node-filter.js +137 -0
- package/dist/src/snapshot/node-filter.js.map +1 -0
- package/dist/src/snapshot/node-synthesizer.d.ts +62 -0
- package/dist/src/snapshot/node-synthesizer.d.ts.map +1 -0
- package/dist/src/snapshot/node-synthesizer.js +185 -0
- package/dist/src/snapshot/node-synthesizer.js.map +1 -0
- package/dist/src/snapshot/snapshot-compiler.d.ts +2 -36
- package/dist/src/snapshot/snapshot-compiler.d.ts.map +1 -1
- package/dist/src/snapshot/snapshot-compiler.js +25 -547
- package/dist/src/snapshot/snapshot-compiler.js.map +1 -1
- package/dist/src/snapshot/snapshot.types.d.ts +7 -2
- package/dist/src/snapshot/snapshot.types.d.ts.map +1 -1
- package/dist/src/snapshot/snapshot.types.js +8 -0
- package/dist/src/snapshot/snapshot.types.js.map +1 -1
- package/dist/src/state/actionables-filter.d.ts +5 -0
- package/dist/src/state/actionables-filter.d.ts.map +1 -1
- package/dist/src/state/actionables-filter.js +21 -3
- package/dist/src/state/actionables-filter.js.map +1 -1
- package/dist/src/state/diff-engine.js +3 -3
- package/dist/src/state/diff-engine.js.map +1 -1
- package/dist/src/state/element-registry.d.ts.map +1 -1
- package/dist/src/state/element-registry.js +6 -4
- package/dist/src/state/element-registry.js.map +1 -1
- package/dist/src/state/hash-utils.d.ts +24 -0
- package/dist/src/state/hash-utils.d.ts.map +1 -0
- package/dist/src/state/hash-utils.js +41 -0
- package/dist/src/state/hash-utils.js.map +1 -0
- package/dist/src/state/layer-detector.d.ts.map +1 -1
- package/dist/src/state/layer-detector.js +15 -286
- package/dist/src/state/layer-detector.js.map +1 -1
- package/dist/src/state/layer-detectors/drawer-detector.d.ts +32 -0
- package/dist/src/state/layer-detectors/drawer-detector.d.ts.map +1 -0
- package/dist/src/state/layer-detectors/drawer-detector.js +96 -0
- package/dist/src/state/layer-detectors/drawer-detector.js.map +1 -0
- package/dist/src/state/layer-detectors/index.d.ts +10 -0
- package/dist/src/state/layer-detectors/index.d.ts.map +1 -0
- package/dist/src/state/layer-detectors/index.js +10 -0
- package/dist/src/state/layer-detectors/index.js.map +1 -0
- package/dist/src/state/layer-detectors/modal-detector.d.ts +30 -0
- package/dist/src/state/layer-detectors/modal-detector.d.ts.map +1 -0
- package/dist/src/state/layer-detectors/modal-detector.js +127 -0
- package/dist/src/state/layer-detectors/modal-detector.js.map +1 -0
- package/dist/src/state/layer-detectors/popover-detector.d.ts +20 -0
- package/dist/src/state/layer-detectors/popover-detector.d.ts.map +1 -0
- package/dist/src/state/layer-detectors/popover-detector.js +76 -0
- package/dist/src/state/layer-detectors/popover-detector.js.map +1 -0
- package/dist/src/state/layer-detectors/toast-detector.d.ts +24 -0
- package/dist/src/state/layer-detectors/toast-detector.d.ts.map +1 -0
- package/dist/src/state/layer-detectors/toast-detector.js +48 -0
- package/dist/src/state/layer-detectors/toast-detector.js.map +1 -0
- package/dist/src/state/region-mapping.d.ts +13 -0
- package/dist/src/state/region-mapping.d.ts.map +1 -0
- package/dist/src/state/region-mapping.js +25 -0
- package/dist/src/state/region-mapping.js.map +1 -0
- package/dist/src/state/state-manager.d.ts.map +1 -1
- package/dist/src/state/state-manager.js +8 -192
- package/dist/src/state/state-manager.js.map +1 -1
- package/dist/src/state/state-renderer.d.ts.map +1 -1
- package/dist/src/state/state-renderer.js +14 -2
- package/dist/src/state/state-renderer.js.map +1 -1
- package/dist/src/state/types.d.ts +8 -4
- package/dist/src/state/types.d.ts.map +1 -1
- package/dist/src/state/url-sanitization.d.ts +22 -0
- package/dist/src/state/url-sanitization.d.ts.map +1 -0
- package/dist/src/state/url-sanitization.js +60 -0
- package/dist/src/state/url-sanitization.js.map +1 -0
- package/dist/src/state/value-masking.d.ts +36 -0
- package/dist/src/state/value-masking.d.ts.map +1 -0
- package/dist/src/state/value-masking.js +86 -0
- package/dist/src/state/value-masking.js.map +1 -0
- package/dist/src/tools/action-context.d.ts +60 -0
- package/dist/src/tools/action-context.d.ts.map +1 -0
- package/dist/src/tools/action-context.js +78 -0
- package/dist/src/tools/action-context.js.map +1 -0
- package/dist/src/tools/action-stabilization.d.ts +48 -0
- package/dist/src/tools/action-stabilization.d.ts.map +1 -0
- package/dist/src/tools/action-stabilization.js +87 -0
- package/dist/src/tools/action-stabilization.js.map +1 -0
- package/dist/src/tools/browser-tools.d.ts +8 -167
- package/dist/src/tools/browser-tools.d.ts.map +1 -1
- package/dist/src/tools/browser-tools.js +13 -651
- package/dist/src/tools/browser-tools.js.map +1 -1
- package/dist/src/tools/effect-tracker.d.ts +25 -0
- package/dist/src/tools/effect-tracker.d.ts.map +1 -0
- package/dist/src/tools/effect-tracker.js +69 -0
- package/dist/src/tools/effect-tracker.js.map +1 -0
- package/dist/src/tools/execute-action.d.ts +1 -31
- package/dist/src/tools/execute-action.d.ts.map +1 -1
- package/dist/src/tools/execute-action.js +7 -276
- package/dist/src/tools/execute-action.js.map +1 -1
- package/dist/src/tools/form-tools.d.ts +2 -2
- package/dist/src/tools/form-tools.js +4 -4
- package/dist/src/tools/form-tools.js.map +1 -1
- package/dist/src/tools/index.d.ts +2 -2
- package/dist/src/tools/index.d.ts.map +1 -1
- package/dist/src/tools/index.js +10 -8
- package/dist/src/tools/index.js.map +1 -1
- package/dist/src/tools/interaction-tools.d.ts +46 -0
- package/dist/src/tools/interaction-tools.d.ts.map +1 -0
- package/dist/src/tools/interaction-tools.js +138 -0
- package/dist/src/tools/interaction-tools.js.map +1 -0
- package/dist/src/tools/navigation-detection.d.ts +31 -0
- package/dist/src/tools/navigation-detection.d.ts.map +1 -0
- package/dist/src/tools/navigation-detection.js +46 -0
- package/dist/src/tools/navigation-detection.js.map +1 -0
- package/dist/src/tools/navigation-tools.d.ts +57 -0
- package/dist/src/tools/navigation-tools.d.ts.map +1 -0
- package/dist/src/tools/navigation-tools.js +178 -0
- package/dist/src/tools/navigation-tools.js.map +1 -0
- package/dist/src/tools/observation-tools.d.ts +53 -0
- package/dist/src/tools/observation-tools.d.ts.map +1 -0
- package/dist/src/tools/observation-tools.js +247 -0
- package/dist/src/tools/observation-tools.js.map +1 -0
- package/dist/src/tools/response-builder.js +2 -2
- package/dist/src/tools/response-builder.js.map +1 -1
- package/dist/src/tools/stale-element-retry.d.ts +37 -0
- package/dist/src/tools/stale-element-retry.d.ts.map +1 -0
- package/dist/src/tools/stale-element-retry.js +68 -0
- package/dist/src/tools/stale-element-retry.js.map +1 -0
- package/dist/src/tools/state-manager-registry.d.ts +26 -0
- package/dist/src/tools/state-manager-registry.d.ts.map +1 -0
- package/dist/src/tools/state-manager-registry.js +39 -0
- package/dist/src/tools/state-manager-registry.js.map +1 -0
- package/dist/src/tools/tool-context.js +1 -1
- package/dist/src/tools/tool-context.js.map +1 -1
- package/dist/src/tools/tool-schemas.d.ts +106 -21
- package/dist/src/tools/tool-schemas.d.ts.map +1 -1
- package/dist/src/tools/tool-schemas.js +59 -18
- package/dist/src/tools/tool-schemas.js.map +1 -1
- package/dist/src/tools/viewport-tools.d.ts +36 -0
- package/dist/src/tools/viewport-tools.d.ts.map +1 -0
- package/dist/src/tools/viewport-tools.js +105 -0
- package/dist/src/tools/viewport-tools.js.map +1 -0
- package/package.json +1 -1
|
@@ -12,17 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { Page } from 'puppeteer-core';
|
|
14
14
|
import type { CdpClient } from '../cdp/cdp-client.interface.js';
|
|
15
|
-
import type { BaseSnapshot
|
|
16
|
-
import { type
|
|
17
|
-
/**
|
|
18
|
-
* Snapshot compiler options
|
|
19
|
-
*/
|
|
20
|
-
export interface CompileOptions extends Partial<SnapshotOptions> {
|
|
21
|
-
/** Include readable content nodes (headings, paragraphs). Default: true */
|
|
22
|
-
includeReadable?: boolean;
|
|
23
|
-
/** Extract bounding boxes and layout info. Default: true */
|
|
24
|
-
includeLayout?: boolean;
|
|
25
|
-
}
|
|
15
|
+
import type { BaseSnapshot } from './snapshot.types.js';
|
|
16
|
+
import { type CompileOptions } from './kind-mapping.js';
|
|
26
17
|
/**
|
|
27
18
|
* SnapshotCompiler class
|
|
28
19
|
*
|
|
@@ -54,32 +45,7 @@ export declare class SnapshotCompiler {
|
|
|
54
45
|
* Transform raw node data to ReadableNode.
|
|
55
46
|
*/
|
|
56
47
|
private transformNode;
|
|
57
|
-
/**
|
|
58
|
-
* Get NodeKind from HTML tag name.
|
|
59
|
-
*/
|
|
60
|
-
private getKindFromTag;
|
|
61
|
-
/**
|
|
62
|
-
* Filter out noise nodes to reduce snapshot size.
|
|
63
|
-
*
|
|
64
|
-
* Filters:
|
|
65
|
-
* 1. Empty list/listitem containers with no semantic name AND no interactive descendants
|
|
66
|
-
* 2. StaticText/text nodes that mirror their parent's label exactly
|
|
67
|
-
*/
|
|
68
|
-
private filterNoiseNodes;
|
|
69
48
|
}
|
|
70
|
-
/**
|
|
71
|
-
* Slice nodes to max_nodes budget while preserving high z-index overlay content.
|
|
72
|
-
*
|
|
73
|
-
* Portal-rendered content (dropdowns, popovers, modals) appears at the end of
|
|
74
|
-
* DOM order. On heavy pages, a naive slice truncates it.
|
|
75
|
-
*
|
|
76
|
-
* Strategy:
|
|
77
|
-
* 1. Partition nodes into overlay (z-index > threshold) and main
|
|
78
|
-
* 2. Take all overlay nodes (up to 30% of budget)
|
|
79
|
-
* 3. Fill remaining budget with main nodes (DOM order)
|
|
80
|
-
* 4. Re-sort by original DOM order
|
|
81
|
-
*/
|
|
82
|
-
export declare function sliceWithOverlayPriority(nodes: RawNodeData[], maxNodes: number): RawNodeData[];
|
|
83
49
|
/**
|
|
84
50
|
* Export a compile function for simpler usage.
|
|
85
51
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snapshot-compiler.d.ts","sourceRoot":"","sources":["../../../src/snapshot/snapshot-compiler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAChE,OAAO,KAAK,EACV,YAAY,
|
|
1
|
+
{"version":3,"file":"snapshot-compiler.d.ts","sourceRoot":"","sources":["../../../src/snapshot/snapshot-compiler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAChE,OAAO,KAAK,EACV,YAAY,EAQb,MAAM,qBAAqB,CAAC;AAyC7B,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,mBAAmB,CAAC;AAE3B;;;;GAIG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2B;IAEnD,iDAAiD;IACjD,OAAO,CAAC,eAAe,CAAK;gBAEhB,OAAO,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC;IAI7C;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAK1B;;;;;;;OAOG;IACG,OAAO,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IA6WjF;;OAEG;YACW,iBAAiB;IAe/B;;OAEG;IACH,OAAO,CAAC,aAAa;CAgItB;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,GAChC,OAAO,CAAC,YAAY,CAAC,CAGvB"}
|
|
@@ -10,294 +10,14 @@
|
|
|
10
10
|
* - Accessibility: Semantic information
|
|
11
11
|
* - CSS: Computed styles (optional, for layout)
|
|
12
12
|
*/
|
|
13
|
-
import { createExtractorContext, extractDom, extractAx, extractLayout, extractState, resolveLabel, resolveRegion, buildLocators, resolveGrouping, classifyAxRole, extractAttributes, } from './extractors/index.js';
|
|
13
|
+
import { createExtractorContext, extractDom, extractAx, extractLayout, extractState, resolveLabel, resolveRegion, buildLocators, resolveGrouping, classifyAxRole, extractAttributes, LIVE_REGION_AX_ROLES, } from './extractors/index.js';
|
|
14
14
|
import { detectInteractivity } from './extractors/interactivity-detector.js';
|
|
15
15
|
import { getTextContent } from '../lib/text-utils.js';
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
function buildContextKey(node) {
|
|
22
|
-
const frameKey = node.framePath?.length ? node.framePath.join('/') : ROOT_CONTEXT;
|
|
23
|
-
const shadowKey = node.shadowPath?.length ? node.shadowPath.join('/') : LIGHT_DOM_CONTEXT;
|
|
24
|
-
return `${frameKey}|${shadowKey}`;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Build context-scoped ID maps to avoid cross-frame/shadow collisions.
|
|
28
|
-
*/
|
|
29
|
-
function buildIdMapsByContext(domResult) {
|
|
30
|
-
const idMaps = new Map();
|
|
31
|
-
for (const node of domResult.nodes.values()) {
|
|
32
|
-
const id = node.attributes?.id;
|
|
33
|
-
if (!id)
|
|
34
|
-
continue;
|
|
35
|
-
const contextKey = buildContextKey(node);
|
|
36
|
-
let map = idMaps.get(contextKey);
|
|
37
|
-
if (!map) {
|
|
38
|
-
map = new Map();
|
|
39
|
-
idMaps.set(contextKey, map);
|
|
40
|
-
}
|
|
41
|
-
map.set(id, node);
|
|
42
|
-
}
|
|
43
|
-
return idMaps;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Get the ID map scoped to a node's frame/shadow context.
|
|
47
|
-
*/
|
|
48
|
-
function getIdMapForNode(node, idMapsByContext) {
|
|
49
|
-
if (!node)
|
|
50
|
-
return undefined;
|
|
51
|
-
return idMapsByContext.get(buildContextKey(node));
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Build adjacency maps for shadow roots and iframe content documents.
|
|
55
|
-
* Single O(n) pass through all nodes.
|
|
56
|
-
*
|
|
57
|
-
* @param domResult - DOM extraction result
|
|
58
|
-
* @returns Adjacency maps for shadow roots and content documents
|
|
59
|
-
*/
|
|
60
|
-
function buildAdjacencyMaps(domResult) {
|
|
61
|
-
const shadowRootsByHost = new Map();
|
|
62
|
-
const contentDocsByFrame = new Map();
|
|
63
|
-
for (const [nodeId, node] of domResult.nodes) {
|
|
64
|
-
if (node.parentId === undefined)
|
|
65
|
-
continue;
|
|
66
|
-
if (node.nodeName === '#document-fragment') {
|
|
67
|
-
// This is a shadow root - add to shadow host's children
|
|
68
|
-
const existing = shadowRootsByHost.get(node.parentId) ?? [];
|
|
69
|
-
existing.push(nodeId);
|
|
70
|
-
shadowRootsByHost.set(node.parentId, existing);
|
|
71
|
-
}
|
|
72
|
-
else if (node.nodeName === '#document') {
|
|
73
|
-
// This is a content document - add to iframe's children
|
|
74
|
-
const existing = contentDocsByFrame.get(node.parentId) ?? [];
|
|
75
|
-
existing.push(nodeId);
|
|
76
|
-
contentDocsByFrame.set(node.parentId, existing);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return { shadowRootsByHost, contentDocsByFrame };
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Build DOM pre-order index by traversing the DOM tree.
|
|
83
|
-
* Also traverses into shadow roots and iframe content documents.
|
|
84
|
-
*
|
|
85
|
-
* @param domResult - DOM extraction result with nodes and rootId
|
|
86
|
-
* @param adjacencyMaps - Precomputed maps for shadow roots and content documents
|
|
87
|
-
* @returns Map of backendNodeId -> DOM order index
|
|
88
|
-
*/
|
|
89
|
-
function buildDomOrderIndex(domResult, adjacencyMaps) {
|
|
90
|
-
const orderIndex = new Map();
|
|
91
|
-
const shadowHostSet = new Set(domResult.shadowRoots);
|
|
92
|
-
let index = 0;
|
|
93
|
-
function traverse(nodeId) {
|
|
94
|
-
const node = domResult.nodes.get(nodeId);
|
|
95
|
-
if (!node)
|
|
96
|
-
return;
|
|
97
|
-
orderIndex.set(nodeId, index++);
|
|
98
|
-
// 1. Process light DOM children first (pre-order DFS)
|
|
99
|
-
if (node.childNodeIds) {
|
|
100
|
-
for (const childId of node.childNodeIds) {
|
|
101
|
-
traverse(childId);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
// 2. If this node hosts a shadow root, traverse shadow content (O(1) lookup)
|
|
105
|
-
if (shadowHostSet.has(nodeId)) {
|
|
106
|
-
const shadowRoots = adjacencyMaps.shadowRootsByHost.get(nodeId) ?? [];
|
|
107
|
-
for (const shadowRootId of shadowRoots) {
|
|
108
|
-
traverse(shadowRootId);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
// 3. If this node is an iframe, traverse content document (O(1) lookup)
|
|
112
|
-
if (node.frameId || node.nodeName.toUpperCase() === 'IFRAME') {
|
|
113
|
-
const contentDocs = adjacencyMaps.contentDocsByFrame.get(nodeId) ?? [];
|
|
114
|
-
for (const contentDocId of contentDocs) {
|
|
115
|
-
traverse(contentDocId);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
traverse(domResult.rootId);
|
|
120
|
-
return orderIndex;
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Build heading index mapping each backendNodeId to its heading context.
|
|
124
|
-
* Uses DOM order to determine the most recent preceding heading.
|
|
125
|
-
* Also traverses into shadow roots and iframe content documents.
|
|
126
|
-
*
|
|
127
|
-
* Heading context is isolated at iframe boundaries:
|
|
128
|
-
* - Heading from parent document does NOT propagate into iframe
|
|
129
|
-
* - Heading from iframe does NOT propagate back to parent document
|
|
130
|
-
* - Shadow DOM shares heading context with its host document
|
|
131
|
-
*
|
|
132
|
-
* @param domResult - DOM extraction result
|
|
133
|
-
* @param axResult - AX extraction result for heading names
|
|
134
|
-
* @param idMap - Map of DOM ID to RawDomNode for aria-labelledby resolution
|
|
135
|
-
* @param adjacencyMaps - Precomputed maps for shadow roots and content documents
|
|
136
|
-
* @returns Map of backendNodeId -> heading context string
|
|
137
|
-
*/
|
|
138
|
-
function buildHeadingIndex(domResult, axResult, idMapsByContext, adjacencyMaps) {
|
|
139
|
-
const headingIndex = new Map();
|
|
140
|
-
const shadowHostSet = new Set(domResult.shadowRoots);
|
|
141
|
-
// Helper to check if a node is a heading and resolve its name
|
|
142
|
-
function isHeading(backendNodeId) {
|
|
143
|
-
const domNode = domResult.nodes.get(backendNodeId);
|
|
144
|
-
const axNode = axResult?.nodes.get(backendNodeId);
|
|
145
|
-
// Check AX role first
|
|
146
|
-
const scopedIdMap = domNode ? getIdMapForNode(domNode, idMapsByContext) : undefined;
|
|
147
|
-
if (axNode?.role === 'heading') {
|
|
148
|
-
// Priority: AX name -> resolveLabel -> DOM text content
|
|
149
|
-
let name = axNode.name;
|
|
150
|
-
if (!name && domNode) {
|
|
151
|
-
const labelResult = resolveLabel(domNode, axNode, scopedIdMap);
|
|
152
|
-
if (labelResult.source !== 'none') {
|
|
153
|
-
name = labelResult.label;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
name ??= getTextContent(backendNodeId, domResult.nodes);
|
|
157
|
-
return { isHeading: true, name };
|
|
158
|
-
}
|
|
159
|
-
// Check DOM tag (H1-H6)
|
|
160
|
-
if (domNode?.nodeName?.match(/^H[1-6]$/i)) {
|
|
161
|
-
// Priority: AX name -> resolveLabel -> DOM text content
|
|
162
|
-
let name = axNode?.name;
|
|
163
|
-
if (!name) {
|
|
164
|
-
const labelResult = resolveLabel(domNode, axNode, scopedIdMap);
|
|
165
|
-
if (labelResult.source !== 'none') {
|
|
166
|
-
name = labelResult.label;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
name ??= getTextContent(backendNodeId, domResult.nodes);
|
|
170
|
-
return { isHeading: true, name };
|
|
171
|
-
}
|
|
172
|
-
return { isHeading: false };
|
|
173
|
-
}
|
|
174
|
-
// Traverse DOM in pre-order, passing and returning heading context
|
|
175
|
-
function traverse(nodeId, currentHeading) {
|
|
176
|
-
const node = domResult.nodes.get(nodeId);
|
|
177
|
-
if (!node)
|
|
178
|
-
return currentHeading;
|
|
179
|
-
// Check if this node is a heading
|
|
180
|
-
const headingInfo = isHeading(nodeId);
|
|
181
|
-
if (headingInfo.isHeading && headingInfo.name) {
|
|
182
|
-
currentHeading = headingInfo.name;
|
|
183
|
-
}
|
|
184
|
-
// Record the current heading context for this node
|
|
185
|
-
if (currentHeading) {
|
|
186
|
-
headingIndex.set(nodeId, currentHeading);
|
|
187
|
-
}
|
|
188
|
-
// 1. Process light DOM children first (pre-order DFS)
|
|
189
|
-
// Heading context propagates and updates through light DOM
|
|
190
|
-
if (node.childNodeIds) {
|
|
191
|
-
for (const childId of node.childNodeIds) {
|
|
192
|
-
currentHeading = traverse(childId, currentHeading) ?? currentHeading;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
// 2. If this node hosts a shadow root, traverse shadow content (O(1) lookup)
|
|
196
|
-
// Shadow DOM shares heading context with host document (same logical document)
|
|
197
|
-
if (shadowHostSet.has(nodeId)) {
|
|
198
|
-
const shadowRoots = adjacencyMaps.shadowRootsByHost.get(nodeId) ?? [];
|
|
199
|
-
for (const shadowRootId of shadowRoots) {
|
|
200
|
-
currentHeading = traverse(shadowRootId, currentHeading) ?? currentHeading;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
// 3. If this node is an iframe, traverse content document (O(1) lookup)
|
|
204
|
-
// IMPORTANT: Heading context resets at iframe boundary (separate document)
|
|
205
|
-
// - Pass undefined to reset context inside iframe
|
|
206
|
-
// - Discard returned heading (iframe headings don't affect parent)
|
|
207
|
-
if (node.frameId || node.nodeName.toUpperCase() === 'IFRAME') {
|
|
208
|
-
const contentDocs = adjacencyMaps.contentDocsByFrame.get(nodeId) ?? [];
|
|
209
|
-
for (const contentDocId of contentDocs) {
|
|
210
|
-
traverse(contentDocId, undefined);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
return currentHeading;
|
|
214
|
-
}
|
|
215
|
-
traverse(domResult.rootId, undefined);
|
|
216
|
-
return headingIndex;
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Recursively collect frame loaderIds from frame tree.
|
|
220
|
-
*/
|
|
221
|
-
function collectFrameLoaderIds(frameTree, frameLoaderIds, _isMainFrame = true) {
|
|
222
|
-
const frame = frameTree.frame;
|
|
223
|
-
frameLoaderIds.set(frame.id, {
|
|
224
|
-
frameId: frame.id,
|
|
225
|
-
loaderId: frame.loaderId,
|
|
226
|
-
isMainFrame: !frame.parentId,
|
|
227
|
-
});
|
|
228
|
-
if (frameTree.childFrames) {
|
|
229
|
-
for (const child of frameTree.childFrames) {
|
|
230
|
-
collectFrameLoaderIds(child, frameLoaderIds, false);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Default compile options
|
|
236
|
-
*/
|
|
237
|
-
const DEFAULT_OPTIONS = {
|
|
238
|
-
include_hidden: false,
|
|
239
|
-
max_nodes: 2000,
|
|
240
|
-
timeout: 30000,
|
|
241
|
-
redact_sensitive: true,
|
|
242
|
-
include_values: true, // Enable value extraction with password redaction
|
|
243
|
-
includeReadable: true,
|
|
244
|
-
includeLayout: true,
|
|
245
|
-
};
|
|
246
|
-
/**
|
|
247
|
-
* Map AX role to NodeKind.
|
|
248
|
-
*/
|
|
249
|
-
function mapRoleToKind(role) {
|
|
250
|
-
if (!role)
|
|
251
|
-
return undefined;
|
|
252
|
-
const normalized = role.toLowerCase();
|
|
253
|
-
const kindMap = {
|
|
254
|
-
// Interactive
|
|
255
|
-
button: 'button',
|
|
256
|
-
link: 'link',
|
|
257
|
-
textbox: 'input',
|
|
258
|
-
searchbox: 'input',
|
|
259
|
-
combobox: 'combobox',
|
|
260
|
-
listbox: 'select',
|
|
261
|
-
checkbox: 'checkbox',
|
|
262
|
-
radio: 'radio',
|
|
263
|
-
switch: 'switch',
|
|
264
|
-
slider: 'slider',
|
|
265
|
-
spinbutton: 'slider',
|
|
266
|
-
tab: 'tab',
|
|
267
|
-
menuitem: 'menuitem',
|
|
268
|
-
menuitemcheckbox: 'menuitem',
|
|
269
|
-
menuitemradio: 'menuitem',
|
|
270
|
-
option: 'menuitem',
|
|
271
|
-
// Readable
|
|
272
|
-
heading: 'heading',
|
|
273
|
-
paragraph: 'paragraph',
|
|
274
|
-
text: 'text',
|
|
275
|
-
statictext: 'text',
|
|
276
|
-
list: 'list',
|
|
277
|
-
listitem: 'listitem',
|
|
278
|
-
tree: 'list',
|
|
279
|
-
treeitem: 'listitem',
|
|
280
|
-
image: 'image',
|
|
281
|
-
img: 'image',
|
|
282
|
-
figure: 'image',
|
|
283
|
-
canvas: 'canvas',
|
|
284
|
-
table: 'table',
|
|
285
|
-
grid: 'table',
|
|
286
|
-
treegrid: 'table',
|
|
287
|
-
// Structural
|
|
288
|
-
form: 'form',
|
|
289
|
-
dialog: 'dialog',
|
|
290
|
-
alertdialog: 'dialog',
|
|
291
|
-
navigation: 'navigation',
|
|
292
|
-
region: 'section',
|
|
293
|
-
article: 'section',
|
|
294
|
-
main: 'section',
|
|
295
|
-
banner: 'section',
|
|
296
|
-
complementary: 'section',
|
|
297
|
-
contentinfo: 'section',
|
|
298
|
-
};
|
|
299
|
-
return kindMap[normalized];
|
|
300
|
-
}
|
|
16
|
+
import { buildIdMapsByContext, getIdMapForNode, buildAdjacencyMaps, buildDomOrderIndex, collectFrameLoaderIds, } from './frame-context.js';
|
|
17
|
+
import { buildHeadingIndex } from './heading-index.js';
|
|
18
|
+
import { synthesizeOptionNodes, synthesizeCanvasNodes, promoteToastNodes, } from './node-synthesizer.js';
|
|
19
|
+
import { filterNoiseNodes, sliceWithOverlayPriority } from './node-filter.js';
|
|
20
|
+
import { mapRoleToKind, getKindFromTag, DEFAULT_OPTIONS, } from './kind-mapping.js';
|
|
301
21
|
/**
|
|
302
22
|
* SnapshotCompiler class
|
|
303
23
|
*
|
|
@@ -427,7 +147,10 @@ export class SnapshotCompiler {
|
|
|
427
147
|
const isReadable = classification === 'readable' && this.options.includeReadable;
|
|
428
148
|
const isEssentialStructural = classification === 'structural' &&
|
|
429
149
|
essentialStructuralRoles.has(axNode.role?.toLowerCase() ?? '');
|
|
430
|
-
|
|
150
|
+
// Live region roles (alert, status, log, tooltip, progressbar, timer)
|
|
151
|
+
// are always included — they carry critical action feedback
|
|
152
|
+
const isLiveRegion = classification === 'live';
|
|
153
|
+
if (isInteractive || isReadable || isEssentialStructural || isLiveRegion) {
|
|
431
154
|
const domNode = domResult?.nodes.get(backendNodeId);
|
|
432
155
|
nodesToProcess.push({
|
|
433
156
|
backendNodeId,
|
|
@@ -449,90 +172,11 @@ export class SnapshotCompiler {
|
|
|
449
172
|
}
|
|
450
173
|
}
|
|
451
174
|
}
|
|
452
|
-
// Phase 2.1: Synthesize
|
|
453
|
-
// Chrome's AX tree often marks <option> nodes as ignored when the select
|
|
454
|
-
// is collapsed, and their bounding boxes are zero (OS-rendered).
|
|
455
|
-
// We inject them from the DOM so AI agents can discover available options.
|
|
175
|
+
// Phase 2.1-2.3: Synthesize missing nodes from DOM
|
|
456
176
|
if (domResult) {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
if (domNode?.nodeName.toUpperCase() !== 'SELECT')
|
|
461
|
-
continue;
|
|
462
|
-
const collectOptions = (parentId) => {
|
|
463
|
-
const parent = domResult.nodes.get(parentId);
|
|
464
|
-
if (!parent?.childNodeIds)
|
|
465
|
-
return;
|
|
466
|
-
for (const childId of parent.childNodeIds) {
|
|
467
|
-
const child = domResult.nodes.get(childId);
|
|
468
|
-
if (!child)
|
|
469
|
-
continue;
|
|
470
|
-
const childTag = child.nodeName.toUpperCase();
|
|
471
|
-
if (childTag === 'OPTGROUP') {
|
|
472
|
-
// Recurse into optgroup to find nested options
|
|
473
|
-
collectOptions(childId);
|
|
474
|
-
}
|
|
475
|
-
else if (childTag === 'OPTION' && !alreadyInSet.has(childId)) {
|
|
476
|
-
// Extract text content from option's child text nodes
|
|
477
|
-
const optionText = getTextContent(childId, domResult.nodes);
|
|
478
|
-
// Build synthetic AX node so label resolution and state extraction work
|
|
479
|
-
const syntheticAx = {
|
|
480
|
-
nodeId: `synthetic-opt-${childId}`,
|
|
481
|
-
backendDOMNodeId: childId,
|
|
482
|
-
role: 'option',
|
|
483
|
-
name: optionText ?? '',
|
|
484
|
-
properties: [],
|
|
485
|
-
};
|
|
486
|
-
// Transfer selected attribute to AX property
|
|
487
|
-
if (child.attributes?.selected !== undefined) {
|
|
488
|
-
syntheticAx.properties.push({
|
|
489
|
-
name: 'selected',
|
|
490
|
-
value: { type: 'boolean', value: true },
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
|
-
// Transfer disabled attribute to AX property
|
|
494
|
-
if (child.attributes?.disabled !== undefined) {
|
|
495
|
-
syntheticAx.properties.push({
|
|
496
|
-
name: 'disabled',
|
|
497
|
-
value: { type: 'boolean', value: true },
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
nodesToProcess.push({
|
|
501
|
-
backendNodeId: childId,
|
|
502
|
-
domNode: child,
|
|
503
|
-
axNode: syntheticAx,
|
|
504
|
-
});
|
|
505
|
-
alreadyInSet.add(childId);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
};
|
|
509
|
-
collectOptions(domNode.backendNodeId);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
// Phase 2.2: Synthesize canvas nodes from DOM.
|
|
513
|
-
// Canvas elements are classified as generic/ignored by the AX tree,
|
|
514
|
-
// so we inject them from the DOM so AI agents can discover and interact with them.
|
|
515
|
-
if (domResult) {
|
|
516
|
-
const alreadyInCanvas = new Set(nodesToProcess.map((n) => n.backendNodeId));
|
|
517
|
-
for (const [backendNodeId, domNode] of domResult.nodes) {
|
|
518
|
-
if (domNode.nodeName.toUpperCase() !== 'CANVAS')
|
|
519
|
-
continue;
|
|
520
|
-
if (alreadyInCanvas.has(backendNodeId))
|
|
521
|
-
continue;
|
|
522
|
-
const syntheticAx = {
|
|
523
|
-
nodeId: `synthetic-canvas-${backendNodeId}`,
|
|
524
|
-
backendDOMNodeId: backendNodeId,
|
|
525
|
-
role: 'canvas',
|
|
526
|
-
name: domNode.attributes?.['aria-label'] ?? '',
|
|
527
|
-
properties: [],
|
|
528
|
-
};
|
|
529
|
-
nodesToProcess.push({
|
|
530
|
-
backendNodeId,
|
|
531
|
-
domNode,
|
|
532
|
-
axNode: syntheticAx,
|
|
533
|
-
});
|
|
534
|
-
alreadyInCanvas.add(backendNodeId);
|
|
535
|
-
}
|
|
177
|
+
synthesizeOptionNodes(nodesToProcess, domResult);
|
|
178
|
+
synthesizeCanvasNodes(nodesToProcess, domResult);
|
|
179
|
+
promoteToastNodes(nodesToProcess, domResult);
|
|
536
180
|
}
|
|
537
181
|
// Sort by DOM order if available (before max_nodes slicing)
|
|
538
182
|
if (domOrderAvailable && domOrderIndex) {
|
|
@@ -678,7 +322,7 @@ export class SnapshotCompiler {
|
|
|
678
322
|
}
|
|
679
323
|
}
|
|
680
324
|
// Phase 4.5: Filter noise nodes (empty containers, duplicate text)
|
|
681
|
-
const nodes =
|
|
325
|
+
const nodes = filterNoiseNodes(transformedNodes, domResult?.nodes ?? new Map(), axResult?.nodes ?? new Map());
|
|
682
326
|
// Phase 5: Build BaseSnapshot
|
|
683
327
|
const duration = Date.now() - startTime;
|
|
684
328
|
const interactiveCount = nodes.filter((n) => [
|
|
@@ -750,14 +394,22 @@ export class SnapshotCompiler {
|
|
|
750
394
|
kind = mapRoleToKind(axNode.role) ?? 'generic';
|
|
751
395
|
}
|
|
752
396
|
else if (domNode) {
|
|
753
|
-
const tagKind =
|
|
397
|
+
const tagKind = getKindFromTag(domNode.nodeName);
|
|
754
398
|
if (tagKind)
|
|
755
399
|
kind = tagKind;
|
|
756
400
|
}
|
|
757
401
|
// Resolve label
|
|
758
402
|
const scopedIdMap = getIdMapForNode(domNode, idMapsByContext);
|
|
759
403
|
const labelResult = resolveLabel(domNode, axNode, scopedIdMap);
|
|
760
|
-
|
|
404
|
+
let label = labelResult.label;
|
|
405
|
+
// Fallback for live region containers (alert, status, etc.) with empty labels:
|
|
406
|
+
// Their text typically lives in child StaticText/text nodes, not in the AX name.
|
|
407
|
+
if (!label &&
|
|
408
|
+
axNode?.role &&
|
|
409
|
+
LIVE_REGION_AX_ROLES.has(axNode.role.toLowerCase()) &&
|
|
410
|
+
domTree.size > 0) {
|
|
411
|
+
label = getTextContent(backendNodeId, domTree, 3) ?? '';
|
|
412
|
+
}
|
|
761
413
|
// Resolve region (pass axTree for ancestor AX role lookup)
|
|
762
414
|
const region = resolveRegion(domNode, axNode, domTree, axTree);
|
|
763
415
|
// Resolve grouping (for group_id and group_path only)
|
|
@@ -825,180 +477,6 @@ export class SnapshotCompiler {
|
|
|
825
477
|
}
|
|
826
478
|
return node;
|
|
827
479
|
}
|
|
828
|
-
/**
|
|
829
|
-
* Get NodeKind from HTML tag name.
|
|
830
|
-
*/
|
|
831
|
-
getKindFromTag(tagName) {
|
|
832
|
-
const tag = tagName.toUpperCase();
|
|
833
|
-
const tagMap = {
|
|
834
|
-
BUTTON: 'button',
|
|
835
|
-
A: 'link',
|
|
836
|
-
INPUT: 'input',
|
|
837
|
-
TEXTAREA: 'textarea',
|
|
838
|
-
SELECT: 'select',
|
|
839
|
-
H1: 'heading',
|
|
840
|
-
H2: 'heading',
|
|
841
|
-
H3: 'heading',
|
|
842
|
-
H4: 'heading',
|
|
843
|
-
H5: 'heading',
|
|
844
|
-
H6: 'heading',
|
|
845
|
-
P: 'paragraph',
|
|
846
|
-
IMG: 'image',
|
|
847
|
-
TABLE: 'table',
|
|
848
|
-
UL: 'list',
|
|
849
|
-
OL: 'list',
|
|
850
|
-
LI: 'listitem',
|
|
851
|
-
FORM: 'form',
|
|
852
|
-
DIALOG: 'dialog',
|
|
853
|
-
NAV: 'navigation',
|
|
854
|
-
OPTION: 'menuitem',
|
|
855
|
-
CANVAS: 'canvas',
|
|
856
|
-
};
|
|
857
|
-
return tagMap[tag];
|
|
858
|
-
}
|
|
859
|
-
/**
|
|
860
|
-
* Filter out noise nodes to reduce snapshot size.
|
|
861
|
-
*
|
|
862
|
-
* Filters:
|
|
863
|
-
* 1. Empty list/listitem containers with no semantic name AND no interactive descendants
|
|
864
|
-
* 2. StaticText/text nodes that mirror their parent's label exactly
|
|
865
|
-
*/
|
|
866
|
-
filterNoiseNodes(nodes, domTree, axTree) {
|
|
867
|
-
// Build set of interactive node backend IDs for descendant checking
|
|
868
|
-
const interactiveKinds = new Set([
|
|
869
|
-
'button',
|
|
870
|
-
'link',
|
|
871
|
-
'input',
|
|
872
|
-
'textarea',
|
|
873
|
-
'select',
|
|
874
|
-
'combobox',
|
|
875
|
-
'checkbox',
|
|
876
|
-
'radio',
|
|
877
|
-
'switch',
|
|
878
|
-
'slider',
|
|
879
|
-
'tab',
|
|
880
|
-
'menuitem',
|
|
881
|
-
'canvas',
|
|
882
|
-
]);
|
|
883
|
-
const interactiveBackendIds = new Set(nodes.filter((n) => interactiveKinds.has(n.kind)).map((n) => n.backend_node_id));
|
|
884
|
-
// Build parent-child relationship from DOM tree
|
|
885
|
-
const childToParent = new Map();
|
|
886
|
-
for (const [nodeId, domNode] of domTree) {
|
|
887
|
-
if (domNode.parentId !== undefined) {
|
|
888
|
-
childToParent.set(nodeId, domNode.parentId);
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
// Check if a node has any interactive descendants in the DOM tree
|
|
892
|
-
const hasInteractiveDescendant = (nodeId) => {
|
|
893
|
-
const domNode = domTree.get(nodeId);
|
|
894
|
-
if (!domNode)
|
|
895
|
-
return false;
|
|
896
|
-
// Check direct children
|
|
897
|
-
if (domNode.childNodeIds) {
|
|
898
|
-
for (const childId of domNode.childNodeIds) {
|
|
899
|
-
if (interactiveBackendIds.has(childId)) {
|
|
900
|
-
return true;
|
|
901
|
-
}
|
|
902
|
-
if (hasInteractiveDescendant(childId)) {
|
|
903
|
-
return true;
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
return false;
|
|
908
|
-
};
|
|
909
|
-
// Get label of parent node in the node list
|
|
910
|
-
const getParentLabel = (nodeId) => {
|
|
911
|
-
const parentId = childToParent.get(nodeId);
|
|
912
|
-
if (parentId === undefined)
|
|
913
|
-
return undefined;
|
|
914
|
-
// Look up parent in our node list
|
|
915
|
-
const parentNode = nodes.find((n) => n.backend_node_id === parentId);
|
|
916
|
-
if (parentNode) {
|
|
917
|
-
return parentNode.label;
|
|
918
|
-
}
|
|
919
|
-
// Parent might be further up - check AX tree for parent's name
|
|
920
|
-
const parentAx = axTree.get(parentId);
|
|
921
|
-
return parentAx?.name;
|
|
922
|
-
};
|
|
923
|
-
// Container kinds that can be noisy when empty
|
|
924
|
-
const containerKinds = new Set(['list', 'listitem']);
|
|
925
|
-
// Text kinds that can duplicate parent labels
|
|
926
|
-
const textKinds = new Set(['text']);
|
|
927
|
-
return nodes.filter((node) => {
|
|
928
|
-
// Rule 1: Filter empty container nodes without interactive descendants
|
|
929
|
-
if (containerKinds.has(node.kind)) {
|
|
930
|
-
const hasSemanticName = node.label && node.label.trim().length > 0;
|
|
931
|
-
if (!hasSemanticName) {
|
|
932
|
-
const hasInteractive = hasInteractiveDescendant(node.backend_node_id);
|
|
933
|
-
if (!hasInteractive) {
|
|
934
|
-
return false; // Filter out empty container without interactive content
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
// Rule 2: Filter text nodes that mirror parent's label
|
|
939
|
-
if (textKinds.has(node.kind)) {
|
|
940
|
-
const parentLabel = getParentLabel(node.backend_node_id);
|
|
941
|
-
if (parentLabel && node.label) {
|
|
942
|
-
// Normalize and compare
|
|
943
|
-
const normalizedParent = parentLabel.trim().toLowerCase();
|
|
944
|
-
const normalizedNode = node.label.trim().toLowerCase();
|
|
945
|
-
if (normalizedNode === normalizedParent) {
|
|
946
|
-
return false; // Filter out duplicate text
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
return true; // Keep the node
|
|
951
|
-
});
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
/**
|
|
955
|
-
* Slice nodes to max_nodes budget while preserving high z-index overlay content.
|
|
956
|
-
*
|
|
957
|
-
* Portal-rendered content (dropdowns, popovers, modals) appears at the end of
|
|
958
|
-
* DOM order. On heavy pages, a naive slice truncates it.
|
|
959
|
-
*
|
|
960
|
-
* Strategy:
|
|
961
|
-
* 1. Partition nodes into overlay (z-index > threshold) and main
|
|
962
|
-
* 2. Take all overlay nodes (up to 30% of budget)
|
|
963
|
-
* 3. Fill remaining budget with main nodes (DOM order)
|
|
964
|
-
* 4. Re-sort by original DOM order
|
|
965
|
-
*/
|
|
966
|
-
export function sliceWithOverlayPriority(nodes, maxNodes) {
|
|
967
|
-
if (nodes.length <= maxNodes) {
|
|
968
|
-
return nodes;
|
|
969
|
-
}
|
|
970
|
-
const OVERLAY_Z_THRESHOLD = 100;
|
|
971
|
-
const MAX_OVERLAY_RATIO = 0.3;
|
|
972
|
-
const overlayNodes = [];
|
|
973
|
-
const mainNodes = [];
|
|
974
|
-
for (const node of nodes) {
|
|
975
|
-
const zIndex = node.layout?.zIndex;
|
|
976
|
-
if (zIndex !== undefined && zIndex > OVERLAY_Z_THRESHOLD) {
|
|
977
|
-
overlayNodes.push(node);
|
|
978
|
-
}
|
|
979
|
-
else {
|
|
980
|
-
mainNodes.push(node);
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
// No overlay content → simple slice
|
|
984
|
-
if (overlayNodes.length === 0) {
|
|
985
|
-
return nodes.slice(0, maxNodes);
|
|
986
|
-
}
|
|
987
|
-
// Reserve budget for overlay (capped at 30% of total)
|
|
988
|
-
const maxOverlay = Math.min(overlayNodes.length, Math.floor(maxNodes * MAX_OVERLAY_RATIO));
|
|
989
|
-
const overlaySlice = overlayNodes.slice(0, maxOverlay);
|
|
990
|
-
// Fill remaining budget with main content
|
|
991
|
-
const mainBudget = maxNodes - overlaySlice.length;
|
|
992
|
-
const mainSlice = mainNodes.slice(0, mainBudget);
|
|
993
|
-
// Merge and re-sort by original DOM order
|
|
994
|
-
const merged = [...mainSlice, ...overlaySlice];
|
|
995
|
-
const indexMap = new Map(nodes.map((n, i) => [n.backendNodeId, i]));
|
|
996
|
-
merged.sort((a, b) => {
|
|
997
|
-
const ia = indexMap.get(a.backendNodeId) ?? Infinity;
|
|
998
|
-
const ib = indexMap.get(b.backendNodeId) ?? Infinity;
|
|
999
|
-
return ia - ib;
|
|
1000
|
-
});
|
|
1001
|
-
return merged;
|
|
1002
480
|
}
|
|
1003
481
|
/**
|
|
1004
482
|
* Export a compile function for simpler usage.
|