@reticlehq/browser 2.0.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/LICENSE +202 -0
- package/README.md +17 -0
- package/dist/actions/actions-dom.d.ts +29 -0
- package/dist/actions/actions-dom.d.ts.map +1 -0
- package/dist/actions/actions-dom.js +153 -0
- package/dist/actions/actions-dom.js.map +1 -0
- package/dist/actions/actions.d.ts +92 -0
- package/dist/actions/actions.d.ts.map +1 -0
- package/dist/actions/actions.js +348 -0
- package/dist/actions/actions.js.map +1 -0
- package/dist/actions/scroll.d.ts +19 -0
- package/dist/actions/scroll.d.ts.map +1 -0
- package/dist/actions/scroll.js +44 -0
- package/dist/actions/scroll.js.map +1 -0
- package/dist/commands/commands.d.ts +5 -0
- package/dist/commands/commands.d.ts.map +1 -0
- package/dist/commands/commands.js +246 -0
- package/dist/commands/commands.js.map +1 -0
- package/dist/dom/a11y.d.ts +20 -0
- package/dist/dom/a11y.d.ts.map +1 -0
- package/dist/dom/a11y.js +263 -0
- package/dist/dom/a11y.js.map +1 -0
- package/dist/dom/auto-anchor.d.ts +66 -0
- package/dist/dom/auto-anchor.d.ts.map +1 -0
- package/dist/dom/auto-anchor.js +75 -0
- package/dist/dom/auto-anchor.js.map +1 -0
- package/dist/dom/dom-ignore.d.ts +7 -0
- package/dist/dom/dom-ignore.d.ts.map +1 -0
- package/dist/dom/dom-ignore.js +21 -0
- package/dist/dom/dom-ignore.js.map +1 -0
- package/dist/dom/query.d.ts +6 -0
- package/dist/dom/query.d.ts.map +1 -0
- package/dist/dom/query.js +236 -0
- package/dist/dom/query.js.map +1 -0
- package/dist/dom/refs.d.ts +15 -0
- package/dist/dom/refs.d.ts.map +1 -0
- package/dist/dom/refs.js +36 -0
- package/dist/dom/refs.js.map +1 -0
- package/dist/dom/snapshot.d.ts +22 -0
- package/dist/dom/snapshot.d.ts.map +1 -0
- package/dist/dom/snapshot.js +171 -0
- package/dist/dom/snapshot.js.map +1 -0
- package/dist/dom/theme.d.ts +26 -0
- package/dist/dom/theme.d.ts.map +1 -0
- package/dist/dom/theme.js +90 -0
- package/dist/dom/theme.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/observers/animation.d.ts +4 -0
- package/dist/observers/animation.d.ts.map +1 -0
- package/dist/observers/animation.js +35 -0
- package/dist/observers/animation.js.map +1 -0
- package/dist/observers/console.d.ts +4 -0
- package/dist/observers/console.d.ts.map +1 -0
- package/dist/observers/console.js +58 -0
- package/dist/observers/console.js.map +1 -0
- package/dist/observers/dom.d.ts +4 -0
- package/dist/observers/dom.d.ts.map +1 -0
- package/dist/observers/dom.js +96 -0
- package/dist/observers/dom.js.map +1 -0
- package/dist/observers/health.d.ts +8 -0
- package/dist/observers/health.d.ts.map +1 -0
- package/dist/observers/health.js +33 -0
- package/dist/observers/health.js.map +1 -0
- package/dist/observers/network.d.ts +11 -0
- package/dist/observers/network.d.ts.map +1 -0
- package/dist/observers/network.js +128 -0
- package/dist/observers/network.js.map +1 -0
- package/dist/observers/route.d.ts +4 -0
- package/dist/observers/route.d.ts.map +1 -0
- package/dist/observers/route.js +56 -0
- package/dist/observers/route.js.map +1 -0
- package/dist/observers/scroll.d.ts +4 -0
- package/dist/observers/scroll.d.ts.map +1 -0
- package/dist/observers/scroll.js +42 -0
- package/dist/observers/scroll.js.map +1 -0
- package/dist/observers/types.d.ts +6 -0
- package/dist/observers/types.d.ts.map +1 -0
- package/dist/observers/types.js +2 -0
- package/dist/observers/types.js.map +1 -0
- package/dist/presenter/overlay.d.ts +12 -0
- package/dist/presenter/overlay.d.ts.map +1 -0
- package/dist/presenter/overlay.js +31 -0
- package/dist/presenter/overlay.js.map +1 -0
- package/dist/presenter/presenter-config.d.ts +86 -0
- package/dist/presenter/presenter-config.d.ts.map +1 -0
- package/dist/presenter/presenter-config.js +51 -0
- package/dist/presenter/presenter-config.js.map +1 -0
- package/dist/presenter/presenter-controls.d.ts +74 -0
- package/dist/presenter/presenter-controls.d.ts.map +1 -0
- package/dist/presenter/presenter-controls.js +360 -0
- package/dist/presenter/presenter-controls.js.map +1 -0
- package/dist/presenter/presenter-effects.d.ts +14 -0
- package/dist/presenter/presenter-effects.d.ts.map +1 -0
- package/dist/presenter/presenter-effects.js +40 -0
- package/dist/presenter/presenter-effects.js.map +1 -0
- package/dist/presenter/presenter-glow.d.ts +43 -0
- package/dist/presenter/presenter-glow.d.ts.map +1 -0
- package/dist/presenter/presenter-glow.js +126 -0
- package/dist/presenter/presenter-glow.js.map +1 -0
- package/dist/presenter/presenter-log.d.ts +43 -0
- package/dist/presenter/presenter-log.d.ts.map +1 -0
- package/dist/presenter/presenter-log.js +118 -0
- package/dist/presenter/presenter-log.js.map +1 -0
- package/dist/presenter/presenter-run-state.d.ts +48 -0
- package/dist/presenter/presenter-run-state.d.ts.map +1 -0
- package/dist/presenter/presenter-run-state.js +37 -0
- package/dist/presenter/presenter-run-state.js.map +1 -0
- package/dist/presenter/presenter-styles.d.ts +7 -0
- package/dist/presenter/presenter-styles.d.ts.map +1 -0
- package/dist/presenter/presenter-styles.js +118 -0
- package/dist/presenter/presenter-styles.js.map +1 -0
- package/dist/presenter/presenter-tally.d.ts +20 -0
- package/dist/presenter/presenter-tally.d.ts.map +1 -0
- package/dist/presenter/presenter-tally.js +38 -0
- package/dist/presenter/presenter-tally.js.map +1 -0
- package/dist/presenter/presenter-verbs.d.ts +2 -0
- package/dist/presenter/presenter-verbs.d.ts.map +1 -0
- package/dist/presenter/presenter-verbs.js +29 -0
- package/dist/presenter/presenter-verbs.js.map +1 -0
- package/dist/presenter/presenter.d.ts +89 -0
- package/dist/presenter/presenter.d.ts.map +1 -0
- package/dist/presenter/presenter.js +428 -0
- package/dist/presenter/presenter.js.map +1 -0
- package/dist/recorder/recorder-styles.d.ts +12 -0
- package/dist/recorder/recorder-styles.d.ts.map +1 -0
- package/dist/recorder/recorder-styles.js +45 -0
- package/dist/recorder/recorder-styles.js.map +1 -0
- package/dist/recorder/recorder.d.ts +60 -0
- package/dist/recorder/recorder.d.ts.map +1 -0
- package/dist/recorder/recorder.js +436 -0
- package/dist/recorder/recorder.js.map +1 -0
- package/dist/registry/adapters.d.ts +31 -0
- package/dist/registry/adapters.d.ts.map +1 -0
- package/dist/registry/adapters.js +47 -0
- package/dist/registry/adapters.js.map +1 -0
- package/dist/registry/capabilities.d.ts +25 -0
- package/dist/registry/capabilities.d.ts.map +1 -0
- package/dist/registry/capabilities.js +48 -0
- package/dist/registry/capabilities.js.map +1 -0
- package/dist/registry/commit-and-signal.d.ts +9 -0
- package/dist/registry/commit-and-signal.d.ts.map +1 -0
- package/dist/registry/commit-and-signal.js +12 -0
- package/dist/registry/commit-and-signal.js.map +1 -0
- package/dist/registry/domains.d.ts +15 -0
- package/dist/registry/domains.d.ts.map +1 -0
- package/dist/registry/domains.js +19 -0
- package/dist/registry/domains.js.map +1 -0
- package/dist/registry/emitter.d.ts +28 -0
- package/dist/registry/emitter.d.ts.map +1 -0
- package/dist/registry/emitter.js +27 -0
- package/dist/registry/emitter.js.map +1 -0
- package/dist/registry/stores.d.ts +9 -0
- package/dist/registry/stores.d.ts.map +1 -0
- package/dist/registry/stores.js +30 -0
- package/dist/registry/stores.js.map +1 -0
- package/dist/reticle-presenter-helpers.d.ts +19 -0
- package/dist/reticle-presenter-helpers.d.ts.map +1 -0
- package/dist/reticle-presenter-helpers.js +81 -0
- package/dist/reticle-presenter-helpers.js.map +1 -0
- package/dist/reticle.d.ts +113 -0
- package/dist/reticle.d.ts.map +1 -0
- package/dist/reticle.js +379 -0
- package/dist/reticle.js.map +1 -0
- package/dist/review/annotator.d.ts +36 -0
- package/dist/review/annotator.d.ts.map +1 -0
- package/dist/review/annotator.js +321 -0
- package/dist/review/annotator.js.map +1 -0
- package/dist/review/mark-anchor.d.ts +23 -0
- package/dist/review/mark-anchor.d.ts.map +1 -0
- package/dist/review/mark-anchor.js +74 -0
- package/dist/review/mark-anchor.js.map +1 -0
- package/dist/security/serialization.d.ts +6 -0
- package/dist/security/serialization.d.ts.map +1 -0
- package/dist/security/serialization.js +103 -0
- package/dist/security/serialization.js.map +1 -0
- package/dist/timers/clock.d.ts +6 -0
- package/dist/timers/clock.d.ts.map +1 -0
- package/dist/timers/clock.js +80 -0
- package/dist/timers/clock.js.map +1 -0
- package/dist/timers/native-timers.d.ts +37 -0
- package/dist/timers/native-timers.d.ts.map +1 -0
- package/dist/timers/native-timers.js +79 -0
- package/dist/timers/native-timers.js.map +1 -0
- package/dist/transport/transport.d.ts +54 -0
- package/dist/transport/transport.d.ts.map +1 -0
- package/dist/transport/transport.js +182 -0
- package/dist/transport/transport.js.map +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-anchor synthesis — derive a STABLE way to address any element WITHOUT a hand-added
|
|
3
|
+
* data-testid. This is the brain of zero-config integration: given what the framework already knows
|
|
4
|
+
* about an element (its component name + source location from the fiber, its role + accessible name,
|
|
5
|
+
* its position), pick the most durable anchor so a flow can re-find the element across refactors.
|
|
6
|
+
*
|
|
7
|
+
* Pure and framework-agnostic: it takes a plain descriptor (the React adapter, or a future Vue/Svelte
|
|
8
|
+
* adapter, fills it in) and returns the chosen anchor + whether it is stable. No DOM, no fiber here.
|
|
9
|
+
*
|
|
10
|
+
* Priority (most → least durable). Each tier is only used when the one above is unavailable:
|
|
11
|
+
* 1. testid — explicit, author-chosen, the gold standard. Stable.
|
|
12
|
+
* 2. component@source — component display name + source file:line. Survives text/markup/style
|
|
13
|
+
* changes; uniquely identifies WHERE in the source the element comes from.
|
|
14
|
+
* Stable (a move that changes the source line is a real change worth re-pinning).
|
|
15
|
+
* 3. component+role/name — component name plus role/accessible-name when no source is available.
|
|
16
|
+
* Stable enough: the component identity anchors it, role/name disambiguate.
|
|
17
|
+
* 4. role+name — semantic but content-dependent: an accessible name can change with copy.
|
|
18
|
+
* NOT marked stable (a healed locator could drift to a renamed element).
|
|
19
|
+
* 5. role / position — last resort. NOT stable; surfaced so the element is still addressable, but
|
|
20
|
+
* a flow built on it should be flagged (the recorder marks the step degraded).
|
|
21
|
+
*
|
|
22
|
+
* The point: tiers 1–3 are "stable" (a flow anchored on them can be trusted to re-resolve to the
|
|
23
|
+
* same element); tiers 4–5 are best-effort. Today most elements without a testid fall to role/text
|
|
24
|
+
* (degraded). With component+source, the SAME elements become tier-2 stable anchors — that is how
|
|
25
|
+
* integration becomes universal without the app author adding a single testid.
|
|
26
|
+
*/
|
|
27
|
+
/** Which signal produced the anchor — also its durability tier. */
|
|
28
|
+
export const AnchorStrategy = {
|
|
29
|
+
TESTID: 'testid',
|
|
30
|
+
COMPONENT: 'component',
|
|
31
|
+
ROLE: 'role',
|
|
32
|
+
POSITION: 'position',
|
|
33
|
+
};
|
|
34
|
+
function nonEmpty(s) {
|
|
35
|
+
return typeof s === 'string' && s.length > 0;
|
|
36
|
+
}
|
|
37
|
+
/** Source as a compact `file:line` suffix; basename only to stay terse and path-independent. */
|
|
38
|
+
function sourceTag(source) {
|
|
39
|
+
const base = source.file.split('/').pop() ?? source.file;
|
|
40
|
+
return `${base}:${source.line}`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Choose the most durable anchor for an element from whatever the adapter could observe. Always
|
|
44
|
+
* returns something addressable (falls through to position), with `stable` marking tiers 1–3.
|
|
45
|
+
*/
|
|
46
|
+
export function synthesizeAnchor(input) {
|
|
47
|
+
if (nonEmpty(input.testid)) {
|
|
48
|
+
return { strategy: AnchorStrategy.TESTID, value: input.testid, stable: true };
|
|
49
|
+
}
|
|
50
|
+
if (nonEmpty(input.component) && input.source !== undefined) {
|
|
51
|
+
return {
|
|
52
|
+
strategy: AnchorStrategy.COMPONENT,
|
|
53
|
+
value: `${input.component}@${sourceTag(input.source)}`,
|
|
54
|
+
stable: true,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (nonEmpty(input.component) && (nonEmpty(input.role) || nonEmpty(input.name))) {
|
|
58
|
+
const qualifier = nonEmpty(input.name) ? input.name : input.role;
|
|
59
|
+
return {
|
|
60
|
+
strategy: AnchorStrategy.COMPONENT,
|
|
61
|
+
value: `${input.component}[${qualifier ?? ''}]`,
|
|
62
|
+
stable: true,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (nonEmpty(input.role) && nonEmpty(input.name)) {
|
|
66
|
+
return { strategy: AnchorStrategy.ROLE, value: `${input.role}:${input.name}`, stable: false };
|
|
67
|
+
}
|
|
68
|
+
if (nonEmpty(input.role)) {
|
|
69
|
+
const suffix = input.nth !== undefined ? `#${input.nth}` : '';
|
|
70
|
+
return { strategy: AnchorStrategy.ROLE, value: `${input.role}${suffix}`, stable: false };
|
|
71
|
+
}
|
|
72
|
+
// Last resort: position only — addressable but never trusted.
|
|
73
|
+
return { strategy: AnchorStrategy.POSITION, value: `el#${input.nth ?? 0}`, stable: false };
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=auto-anchor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-anchor.js","sourceRoot":"","sources":["../../src/dom/auto-anchor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,mEAAmE;AACnE,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,MAAM,EAAE,QAAQ;IAChB,SAAS,EAAE,WAAW;IACtB,IAAI,EAAE,MAAM;IACZ,QAAQ,EAAE,UAAU;CACZ,CAAC;AA2BX,SAAS,QAAQ,CAAC,CAAqB;IACrC,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;AAC/C,CAAC;AAED,gGAAgG;AAChG,SAAS,SAAS,CAAC,MAAsC;IACvD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,MAAM,CAAC,IAAI,CAAC;IACzD,OAAO,GAAG,IAAI,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;AAClC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAkB;IACjD,IAAI,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3B,OAAO,EAAE,QAAQ,EAAE,cAAc,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAChF,CAAC;IACD,IAAI,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC5D,OAAO;YACL,QAAQ,EAAE,cAAc,CAAC,SAAS;YAClC,KAAK,EAAE,GAAG,KAAK,CAAC,SAAS,IAAI,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE;YACtD,MAAM,EAAE,IAAI;SACb,CAAC;IACJ,CAAC;IACD,IAAI,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QAChF,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;QACjE,OAAO;YACL,QAAQ,EAAE,cAAc,CAAC,SAAS;YAClC,KAAK,EAAE,GAAG,KAAK,CAAC,SAAS,IAAI,SAAS,IAAI,EAAE,GAAG;YAC/C,MAAM,EAAE,IAAI;SACb,CAAC;IACJ,CAAC;IACD,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACjD,OAAO,EAAE,QAAQ,EAAE,cAAc,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAChG,CAAC;IACD,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9D,OAAO,EAAE,QAAQ,EAAE,cAAc,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC,IAAI,GAAG,MAAM,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAC3F,CAAC;IACD,8DAA8D;IAC9D,OAAO,EAAE,QAAQ,EAAE,cAAc,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;AAC7F,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Let the host app add selectors to exclude from snapshots (e.g. its own dev widgets). */
|
|
2
|
+
export declare function setIgnoreSelectors(selectors: string[]): void;
|
|
3
|
+
/** True if the element is part of Reticle's own presenter overlay. */
|
|
4
|
+
export declare function isReticleOverlay(el: Element): boolean;
|
|
5
|
+
/** True if the element should be excluded from snapshots/queries (Reticle overlay or dev overlay). */
|
|
6
|
+
export declare function isIgnored(el: Element): boolean;
|
|
7
|
+
//# sourceMappingURL=dom-ignore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dom-ignore.d.ts","sourceRoot":"","sources":["../../src/dom/dom-ignore.ts"],"names":[],"mappings":"AAUA,2FAA2F;AAC3F,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,CAE5D;AAED,sEAAsE;AACtE,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,OAAO,GAAG,OAAO,CAErD;AAED,sGAAsG;AACtG,wBAAgB,SAAS,CAAC,EAAE,EAAE,OAAO,GAAG,OAAO,CAM9C"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Selectors for Reticle's own presenter overlay (cursor, HUD, glow) — never observed/snapshotted. */
|
|
2
|
+
const RETICLE_OVERLAY = '[data-reticle-overlay],[data-reticle-cursor],[data-reticle-hud],[data-reticle-glow]';
|
|
3
|
+
/** Known third-party dev overlays to keep out of snapshots (Agentation, Next dev UI). */
|
|
4
|
+
const DEV_OVERLAYS = '[data-agentation],#__next-build-watcher,nextjs-portal,[data-nextjs-dialog],[data-nextjs-toast]';
|
|
5
|
+
let extraIgnore = '';
|
|
6
|
+
/** Let the host app add selectors to exclude from snapshots (e.g. its own dev widgets). */
|
|
7
|
+
export function setIgnoreSelectors(selectors) {
|
|
8
|
+
extraIgnore = selectors.join(',');
|
|
9
|
+
}
|
|
10
|
+
/** True if the element is part of Reticle's own presenter overlay. */
|
|
11
|
+
export function isReticleOverlay(el) {
|
|
12
|
+
return el.closest(RETICLE_OVERLAY) !== null;
|
|
13
|
+
}
|
|
14
|
+
/** True if the element should be excluded from snapshots/queries (Reticle overlay or dev overlay). */
|
|
15
|
+
export function isIgnored(el) {
|
|
16
|
+
const sel = extraIgnore.length > 0
|
|
17
|
+
? `${RETICLE_OVERLAY},${DEV_OVERLAYS},${extraIgnore}`
|
|
18
|
+
: `${RETICLE_OVERLAY},${DEV_OVERLAYS}`;
|
|
19
|
+
return el.closest(sel) !== null;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=dom-ignore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dom-ignore.js","sourceRoot":"","sources":["../../src/dom/dom-ignore.ts"],"names":[],"mappings":"AAAA,sGAAsG;AACtG,MAAM,eAAe,GACnB,qFAAqF,CAAC;AAExF,yFAAyF;AACzF,MAAM,YAAY,GAChB,gGAAgG,CAAC;AAEnG,IAAI,WAAW,GAAG,EAAE,CAAC;AAErB,2FAA2F;AAC3F,MAAM,UAAU,kBAAkB,CAAC,SAAmB;IACpD,WAAW,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACpC,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,gBAAgB,CAAC,EAAW;IAC1C,OAAO,EAAE,CAAC,OAAO,CAAC,eAAe,CAAC,KAAK,IAAI,CAAC;AAC9C,CAAC;AAED,sGAAsG;AACtG,MAAM,UAAU,SAAS,CAAC,EAAW;IACnC,MAAM,GAAG,GACP,WAAW,CAAC,MAAM,GAAG,CAAC;QACpB,CAAC,CAAC,GAAG,eAAe,IAAI,YAAY,IAAI,WAAW,EAAE;QACrD,CAAC,CAAC,GAAG,eAAe,IAAI,YAAY,EAAE,CAAC;IAC3C,OAAO,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC;AAClC,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ElementState, type ElementQuery, type MatchResult, type QueryResult } from '@reticlehq/core';
|
|
2
|
+
/** Match an element predicate against the live DOM (plan/06). */
|
|
3
|
+
export declare function matchQuery(query: ElementQuery, state?: ElementState): MatchResult;
|
|
4
|
+
/** Resolve a query to descriptors for the `query` MCP tool. */
|
|
5
|
+
export declare function runQuery(query: ElementQuery): QueryResult;
|
|
6
|
+
//# sourceMappingURL=query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/dom/query.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,YAAY,EAGZ,KAAK,YAAY,EACjB,KAAK,WAAW,EAGhB,KAAK,WAAW,EACjB,MAAM,iBAAiB,CAAC;AA8IzB,iEAAiE;AACjE,wBAAgB,UAAU,CAAC,KAAK,EAAE,YAAY,EAAE,KAAK,CAAC,EAAE,YAAY,GAAG,WAAW,CAUjF;AAmFD,+DAA+D;AAC/D,wBAAgB,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,WAAW,CAMzD"}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { queryAllByRole, queryAllByText, queryAllByLabelText, queryAllByPlaceholderText, queryAllByTestId, queryAllByAltText, } from '@testing-library/dom';
|
|
2
|
+
import { ElementState, QueryBy, } from '@reticlehq/core';
|
|
3
|
+
import { describe, getStates } from './a11y.js';
|
|
4
|
+
import { getCapabilities } from '../registry/capabilities.js';
|
|
5
|
+
import { identifyComponent } from '../registry/adapters.js';
|
|
6
|
+
import { refs } from './refs.js';
|
|
7
|
+
const TESTID_ATTR = 'data-testid';
|
|
8
|
+
const SOURCE_ATTR = 'data-reticle-source';
|
|
9
|
+
const MAX_PRESENT_TESTIDS = 12;
|
|
10
|
+
/** Bound the fiber-walk fallback so a component-name query can't scan an unbounded DOM. */
|
|
11
|
+
const MAX_COMPONENT_CANDIDATES = 2000;
|
|
12
|
+
/** Likely-actionable elements considered when resolving a component anchor without a source stamp. */
|
|
13
|
+
const COMPONENT_CANDIDATE_SELECTOR = '[data-reticle-source], [data-testid], button, a, input, select, textarea, [role]';
|
|
14
|
+
function resolveContainer(scope) {
|
|
15
|
+
const body = document.body;
|
|
16
|
+
if (scope === undefined)
|
|
17
|
+
return body;
|
|
18
|
+
const byRef = refs.resolve(scope);
|
|
19
|
+
if (byRef instanceof HTMLElement)
|
|
20
|
+
return byRef;
|
|
21
|
+
try {
|
|
22
|
+
const found = document.querySelector(scope);
|
|
23
|
+
if (found instanceof HTMLElement)
|
|
24
|
+
return found;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// invalid selector — fall through to body
|
|
28
|
+
}
|
|
29
|
+
return body;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Resolve an element by its SOURCE location — the precise, granular auto-anchor. The babel plugin
|
|
33
|
+
* stamps `data-reticle-source="file:line:column"` on host elements, so a line-level starts-with match
|
|
34
|
+
* pins the exact JSX element with a single fast attribute selector (no fiber walk). Column is
|
|
35
|
+
* ignored so a small column drift doesn't unbind the anchor.
|
|
36
|
+
*/
|
|
37
|
+
function findBySource(container, source) {
|
|
38
|
+
const prefix = `${source.file}:${source.line}:`.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
39
|
+
try {
|
|
40
|
+
return Array.from(container.querySelectorAll(`[${SOURCE_ATTR}^="${prefix}"]`));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolve by component display name when no source stamp is available: scan a bounded set of
|
|
48
|
+
* likely-actionable elements and keep those whose NEAREST enclosing component (via the registered
|
|
49
|
+
* framework adapter) matches. Coarser than source (one component renders many hosts) — used as a
|
|
50
|
+
* fallback / for frameworks without a source plugin.
|
|
51
|
+
*/
|
|
52
|
+
function findByComponentName(container, component) {
|
|
53
|
+
const out = [];
|
|
54
|
+
let scanned = 0;
|
|
55
|
+
for (const el of Array.from(container.querySelectorAll(COMPONENT_CANDIDATE_SELECTOR))) {
|
|
56
|
+
if (scanned >= MAX_COMPONENT_CANDIDATES)
|
|
57
|
+
break;
|
|
58
|
+
scanned += 1;
|
|
59
|
+
const info = identifyComponent(el);
|
|
60
|
+
if (info !== null && info.componentStack[0] === component)
|
|
61
|
+
out.push(el);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
/** Auto-anchor resolution: source (precise) first, then component name (coarse fallback). */
|
|
66
|
+
function findByComponent(container, query) {
|
|
67
|
+
if (query.source !== undefined) {
|
|
68
|
+
const bySource = findBySource(container, query.source);
|
|
69
|
+
if (bySource.length > 0)
|
|
70
|
+
return bySource;
|
|
71
|
+
}
|
|
72
|
+
if (query.component !== undefined && query.component.length > 0) {
|
|
73
|
+
return findByComponentName(container, query.component);
|
|
74
|
+
}
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
/** Run the appropriate Testing-Library query for the given ElementQuery. */
|
|
78
|
+
function findCandidates(query) {
|
|
79
|
+
const container = resolveContainer(query.scope);
|
|
80
|
+
const by = query.by;
|
|
81
|
+
const value = query.value;
|
|
82
|
+
// Explicit `by`+`value` form.
|
|
83
|
+
if (by !== undefined && value !== undefined) {
|
|
84
|
+
switch (by) {
|
|
85
|
+
case QueryBy.ROLE:
|
|
86
|
+
return queryAllByRole(container, value, query.name !== undefined ? { hidden: true, name: query.name } : { hidden: true });
|
|
87
|
+
case QueryBy.TEXT:
|
|
88
|
+
return queryAllByText(container, value, { exact: false });
|
|
89
|
+
case QueryBy.LABEL:
|
|
90
|
+
return queryAllByLabelText(container, value, { exact: false });
|
|
91
|
+
case QueryBy.PLACEHOLDER:
|
|
92
|
+
return queryAllByPlaceholderText(container, value, { exact: false });
|
|
93
|
+
case QueryBy.TESTID:
|
|
94
|
+
return queryAllByTestId(container, value, { exact: true });
|
|
95
|
+
case QueryBy.ALT:
|
|
96
|
+
return queryAllByAltText(container, value, { exact: false });
|
|
97
|
+
case QueryBy.COMPONENT:
|
|
98
|
+
// value is the component name; .source (if present) still takes precedence inside.
|
|
99
|
+
return findByComponent(container, { ...query, component: query.component ?? value });
|
|
100
|
+
default:
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Auto-anchor (component / source) — checked before the role/text fields so a query carrying
|
|
105
|
+
// both a component anchor and an incidental role resolves by the more durable anchor.
|
|
106
|
+
if (query.component !== undefined || query.source !== undefined) {
|
|
107
|
+
return findByComponent(container, query);
|
|
108
|
+
}
|
|
109
|
+
// Structured form (role+name, or any single field).
|
|
110
|
+
if (query.role !== undefined) {
|
|
111
|
+
const options = query.name !== undefined
|
|
112
|
+
? { hidden: true, name: query.name }
|
|
113
|
+
: { hidden: true };
|
|
114
|
+
return queryAllByRole(container, query.role, options);
|
|
115
|
+
}
|
|
116
|
+
if (query.text !== undefined)
|
|
117
|
+
return queryAllByText(container, query.text, { exact: false });
|
|
118
|
+
if (query.label !== undefined) {
|
|
119
|
+
return queryAllByLabelText(container, query.label, { exact: false });
|
|
120
|
+
}
|
|
121
|
+
if (query.placeholder !== undefined) {
|
|
122
|
+
return queryAllByPlaceholderText(container, query.placeholder, { exact: false });
|
|
123
|
+
}
|
|
124
|
+
if (query.testid !== undefined)
|
|
125
|
+
return queryAllByTestId(container, query.testid, { exact: true });
|
|
126
|
+
if (query.alt !== undefined)
|
|
127
|
+
return queryAllByAltText(container, query.alt, { exact: false });
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
function inState(el, state) {
|
|
131
|
+
return getStates(el).includes(state);
|
|
132
|
+
}
|
|
133
|
+
/** Match an element predicate against the live DOM (plan/06). */
|
|
134
|
+
export function matchQuery(query, state) {
|
|
135
|
+
let elements;
|
|
136
|
+
try {
|
|
137
|
+
elements = findCandidates(query);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
elements = [];
|
|
141
|
+
}
|
|
142
|
+
const filtered = state === undefined ? elements : elements.filter((el) => inState(el, state));
|
|
143
|
+
const descriptors = filtered.map((el) => describe(el));
|
|
144
|
+
return { matched: descriptors.length > 0, count: descriptors.length, elements: descriptors };
|
|
145
|
+
}
|
|
146
|
+
/** Structural clusters of the page — the successor to the raw testid list in zero-match hints. */
|
|
147
|
+
function buildPresentRegions(query) {
|
|
148
|
+
const container = resolveContainer(query.scope);
|
|
149
|
+
const regions = [];
|
|
150
|
+
const CONTAINER_ROLES = [
|
|
151
|
+
'list',
|
|
152
|
+
'listbox',
|
|
153
|
+
'grid',
|
|
154
|
+
'table',
|
|
155
|
+
'tree',
|
|
156
|
+
'treegrid',
|
|
157
|
+
'dialog',
|
|
158
|
+
'alertdialog',
|
|
159
|
+
'navigation',
|
|
160
|
+
'main',
|
|
161
|
+
'banner',
|
|
162
|
+
'form',
|
|
163
|
+
'search',
|
|
164
|
+
'menu',
|
|
165
|
+
'menubar',
|
|
166
|
+
'tablist',
|
|
167
|
+
];
|
|
168
|
+
for (const role of CONTAINER_ROLES) {
|
|
169
|
+
let containers;
|
|
170
|
+
try {
|
|
171
|
+
containers = queryAllByRole(container, role, { hidden: true });
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
for (const el of containers) {
|
|
177
|
+
const name = el.getAttribute('aria-label') ??
|
|
178
|
+
el.getAttribute('aria-labelledby') ??
|
|
179
|
+
el.getAttribute('data-testid') ??
|
|
180
|
+
undefined;
|
|
181
|
+
const children = el.querySelectorAll('[role]');
|
|
182
|
+
const sample = [];
|
|
183
|
+
for (const child of Array.from(children)) {
|
|
184
|
+
if (sample.length >= 3)
|
|
185
|
+
break;
|
|
186
|
+
const childRole = child.getAttribute('role');
|
|
187
|
+
const childName = child.getAttribute('aria-label') ??
|
|
188
|
+
child.getAttribute('data-testid') ??
|
|
189
|
+
child.textContent?.trim().slice(0, 40) ??
|
|
190
|
+
'';
|
|
191
|
+
if (childRole !== null && childName.length > 0) {
|
|
192
|
+
sample.push(`${childRole}[${childName}]`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const region = { role, childCount: children.length, sample };
|
|
196
|
+
if (name !== undefined && name.length > 0)
|
|
197
|
+
region.name = name;
|
|
198
|
+
regions.push(region);
|
|
199
|
+
if (regions.length >= 10)
|
|
200
|
+
return regions;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return regions;
|
|
204
|
+
}
|
|
205
|
+
/** Diagnostic hint for a zero-match query: what testids ARE present in the searched scope. */
|
|
206
|
+
function buildEmptyHint(query) {
|
|
207
|
+
const container = resolveContainer(query.scope);
|
|
208
|
+
const all = container.querySelectorAll(`[${TESTID_ATTR}]`);
|
|
209
|
+
const present = [];
|
|
210
|
+
for (const el of Array.from(all)) {
|
|
211
|
+
const id = el.getAttribute(TESTID_ATTR);
|
|
212
|
+
if (id !== null && id.length > 0 && !present.includes(id)) {
|
|
213
|
+
present.push(id);
|
|
214
|
+
if (present.length >= MAX_PRESENT_TESTIDS)
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const registered = getCapabilities().testids;
|
|
219
|
+
const knownEmptyState = present.some((id) => registered.includes(id));
|
|
220
|
+
const route = `${location.pathname}${location.search}`;
|
|
221
|
+
return {
|
|
222
|
+
route,
|
|
223
|
+
presentTestids: present,
|
|
224
|
+
presentRegions: buildPresentRegions(query),
|
|
225
|
+
knownEmptyState,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/** Resolve a query to descriptors for the `query` MCP tool. */
|
|
229
|
+
export function runQuery(query) {
|
|
230
|
+
const result = matchQuery(query);
|
|
231
|
+
if (result.elements.length === 0) {
|
|
232
|
+
return { elements: result.elements, hint: buildEmptyHint(query) };
|
|
233
|
+
}
|
|
234
|
+
return { elements: result.elements };
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.js","sourceRoot":"","sources":["../../src/dom/query.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable, human-meaningful element handles. Each element gets a ref like `e7`; the same
|
|
3
|
+
* element keeps its ref across snapshots, and a ref re-resolves to its element as long as
|
|
4
|
+
* the element is still in the document. See plan/03 (Refs).
|
|
5
|
+
*/
|
|
6
|
+
export declare class RefRegistry {
|
|
7
|
+
#private;
|
|
8
|
+
/** Get the existing ref for an element, or mint a new one. */
|
|
9
|
+
refFor(el: Element): string;
|
|
10
|
+
/** Resolve a ref back to its element, or null if it's gone/detached. */
|
|
11
|
+
resolve(ref: string): Element | null;
|
|
12
|
+
}
|
|
13
|
+
/** Process-wide registry shared by snapshot, query, and the action executor. */
|
|
14
|
+
export declare const refs: RefRegistry;
|
|
15
|
+
//# sourceMappingURL=refs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refs.d.ts","sourceRoot":"","sources":["../../src/dom/refs.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,qBAAa,WAAW;;IAKtB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,EAAE,OAAO,GAAG,MAAM;IAU3B,wEAAwE;IACxE,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI;CAUrC;AAED,gFAAgF;AAChF,eAAO,MAAM,IAAI,aAAoB,CAAC"}
|
package/dist/dom/refs.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable, human-meaningful element handles. Each element gets a ref like `e7`; the same
|
|
3
|
+
* element keeps its ref across snapshots, and a ref re-resolves to its element as long as
|
|
4
|
+
* the element is still in the document. See plan/03 (Refs).
|
|
5
|
+
*/
|
|
6
|
+
export class RefRegistry {
|
|
7
|
+
#toRef = new WeakMap();
|
|
8
|
+
#fromRef = new Map();
|
|
9
|
+
#seq = 0;
|
|
10
|
+
/** Get the existing ref for an element, or mint a new one. */
|
|
11
|
+
refFor(el) {
|
|
12
|
+
const existing = this.#toRef.get(el);
|
|
13
|
+
if (existing !== undefined)
|
|
14
|
+
return existing;
|
|
15
|
+
this.#seq += 1;
|
|
16
|
+
const ref = `e${String(this.#seq)}`;
|
|
17
|
+
this.#toRef.set(el, ref);
|
|
18
|
+
this.#fromRef.set(ref, new WeakRef(el));
|
|
19
|
+
return ref;
|
|
20
|
+
}
|
|
21
|
+
/** Resolve a ref back to its element, or null if it's gone/detached. */
|
|
22
|
+
resolve(ref) {
|
|
23
|
+
const weak = this.#fromRef.get(ref);
|
|
24
|
+
if (weak === undefined)
|
|
25
|
+
return null;
|
|
26
|
+
const el = weak.deref();
|
|
27
|
+
if (el === undefined || !el.isConnected) {
|
|
28
|
+
this.#fromRef.delete(ref);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return el;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Process-wide registry shared by snapshot, query, and the action executor. */
|
|
35
|
+
export const refs = new RefRegistry();
|
|
36
|
+
//# sourceMappingURL=refs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refs.js","sourceRoot":"","sources":["../../src/dom/refs.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,OAAO,WAAW;IACb,MAAM,GAAG,IAAI,OAAO,EAAmB,CAAC;IACxC,QAAQ,GAAG,IAAI,GAAG,EAA4B,CAAC;IACxD,IAAI,GAAG,CAAC,CAAC;IAET,8DAA8D;IAC9D,MAAM,CAAC,EAAW;QAChB,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACrC,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO,QAAQ,CAAC;QAC5C,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC;QACf,MAAM,GAAG,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;QACzB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;QACxC,OAAO,GAAG,CAAC;IACb,CAAC;IAED,wEAAwE;IACxE,OAAO,CAAC,GAAW;QACjB,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACpC,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC;QACpC,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;QACxB,IAAI,EAAE,KAAK,SAAS,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC;YACxC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC1B,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,EAAE,CAAC;IACZ,CAAC;CACF;AAED,gFAAgF;AAChF,MAAM,CAAC,MAAM,IAAI,GAAG,IAAI,WAAW,EAAE,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { SnapshotMode } from '@reticlehq/core';
|
|
2
|
+
interface SnapshotStatus {
|
|
3
|
+
route: string;
|
|
4
|
+
title: string;
|
|
5
|
+
visibleDialogs: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface SnapshotResult {
|
|
8
|
+
tree: string;
|
|
9
|
+
status: SnapshotStatus;
|
|
10
|
+
nodes: number;
|
|
11
|
+
truncated: boolean;
|
|
12
|
+
}
|
|
13
|
+
interface SnapshotOptions {
|
|
14
|
+
scope?: string | undefined;
|
|
15
|
+
mode?: SnapshotMode | undefined;
|
|
16
|
+
maxNodes?: number | undefined;
|
|
17
|
+
maxDepth?: number | undefined;
|
|
18
|
+
}
|
|
19
|
+
/** Build the semantic accessibility snapshot of the page or a subtree (plan/04). */
|
|
20
|
+
export declare function buildSnapshot(options?: SnapshotOptions): SnapshotResult;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=snapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../../src/dom/snapshot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAyC7D,UAAU,cAAc;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,UAAU,eAAe;IACvB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,IAAI,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AA+HD,oFAAoF;AACpF,wBAAgB,aAAa,CAAC,OAAO,GAAE,eAAoB,GAAG,cAAc,CA4B3E"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { ElementState, SnapshotMode } from '@reticlehq/core';
|
|
2
|
+
import { getAccessibleName, getRole, getStates, getValue, isVisible } from './a11y.js';
|
|
3
|
+
import { refs } from './refs.js';
|
|
4
|
+
import { isIgnored } from './dom-ignore.js';
|
|
5
|
+
const INTERACTIVE = new Set([
|
|
6
|
+
'button',
|
|
7
|
+
'link',
|
|
8
|
+
'textbox',
|
|
9
|
+
'checkbox',
|
|
10
|
+
'radio',
|
|
11
|
+
'combobox',
|
|
12
|
+
'listbox',
|
|
13
|
+
'slider',
|
|
14
|
+
'spinbutton',
|
|
15
|
+
'switch',
|
|
16
|
+
'tab',
|
|
17
|
+
'menuitem',
|
|
18
|
+
'option',
|
|
19
|
+
]);
|
|
20
|
+
const SKIP_TAGS = new Set(['script', 'style', 'noscript', 'template', 'head', 'meta', 'link']);
|
|
21
|
+
/** Cap on inlined text content so a verbose node can't blow up the snapshot. */
|
|
22
|
+
const TEXT_MAX = 80;
|
|
23
|
+
/**
|
|
24
|
+
* Concatenated DIRECT text of an element (its own text nodes, not descendants' — those are
|
|
25
|
+
* captured when their own element is walked, so no duplication). Collapsed + truncated.
|
|
26
|
+
* This is what makes a silent removal of non-interactive content (e.g. a KPI card) visible:
|
|
27
|
+
* the accessibility role tree alone omits generic containers' text.
|
|
28
|
+
*/
|
|
29
|
+
function directText(el) {
|
|
30
|
+
let out = '';
|
|
31
|
+
for (const node of el.childNodes) {
|
|
32
|
+
if (node.nodeType === 3 /* Node.TEXT_NODE */)
|
|
33
|
+
out += node.textContent ?? '';
|
|
34
|
+
}
|
|
35
|
+
const collapsed = out.replace(/\s+/g, ' ').trim();
|
|
36
|
+
return collapsed.length > TEXT_MAX ? `${collapsed.slice(0, TEXT_MAX)}…` : collapsed;
|
|
37
|
+
}
|
|
38
|
+
function skip(el) {
|
|
39
|
+
if (SKIP_TAGS.has(el.tagName.toLowerCase()))
|
|
40
|
+
return true;
|
|
41
|
+
if (isIgnored(el))
|
|
42
|
+
return true; // Reticle overlay + known dev overlays
|
|
43
|
+
if (el.getAttribute('aria-hidden') === 'true')
|
|
44
|
+
return true;
|
|
45
|
+
if (el instanceof HTMLElement && el.hidden)
|
|
46
|
+
return true;
|
|
47
|
+
const view = el.ownerDocument.defaultView;
|
|
48
|
+
if (view !== null) {
|
|
49
|
+
const style = view.getComputedStyle(el);
|
|
50
|
+
if (style.display === 'none')
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
function stateSuffix(el) {
|
|
56
|
+
const states = getStates(el).filter((s) => s === ElementState.DISABLED ||
|
|
57
|
+
s === ElementState.CHECKED ||
|
|
58
|
+
s === ElementState.EXPANDED ||
|
|
59
|
+
s === ElementState.FOCUSED);
|
|
60
|
+
return states.length > 0 ? ` [${states.join(',')}]` : '';
|
|
61
|
+
}
|
|
62
|
+
function formatLine(el, depth, role, name, layout) {
|
|
63
|
+
const indent = ' '.repeat(depth);
|
|
64
|
+
const value = getValue(el);
|
|
65
|
+
const namePart = name.length > 0 ? ` "${name}"` : '';
|
|
66
|
+
const refPart = INTERACTIVE.has(role) || name.length > 0 ? ` (ref=${refs.refFor(el)})` : '';
|
|
67
|
+
const valuePart = value !== undefined && value.length > 0 ? ` [value="${value}"]` : '';
|
|
68
|
+
const layoutPart = layout.length > 0 ? ` [${layout}]` : '';
|
|
69
|
+
return `${indent}- ${role}${namePart}${refPart}${valuePart}${layoutPart}${stateSuffix(el)}`;
|
|
70
|
+
}
|
|
71
|
+
/** A generic container's own text content, with no ref (kept lean — text isn't actionable). */
|
|
72
|
+
function formatTextLine(depth, text) {
|
|
73
|
+
return `${' '.repeat(depth)}- text "${text}"`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Compact signature of an element's own layout when it is a grid/flex container. A layout
|
|
77
|
+
* regression (e.g. grid columns 2 -> 3) leaves the role+text tree identical, so a role-only
|
|
78
|
+
* snapshot is blind to it; this line makes it visible. Empty for non-container elements.
|
|
79
|
+
*/
|
|
80
|
+
function layoutSignature(el) {
|
|
81
|
+
const view = el.ownerDocument.defaultView;
|
|
82
|
+
if (view === null)
|
|
83
|
+
return '';
|
|
84
|
+
const style = view.getComputedStyle(el);
|
|
85
|
+
const display = style.display;
|
|
86
|
+
// Grid track templates are the high-signal CLS case (column/row count + sizing) and there are
|
|
87
|
+
// few grid containers per page. Flex is intentionally excluded: nearly every row is a flex
|
|
88
|
+
// box, so signing them all floods the snapshot for little regression value.
|
|
89
|
+
if (display === 'grid' || display === 'inline-grid') {
|
|
90
|
+
const cols = style.gridTemplateColumns;
|
|
91
|
+
return cols !== '' && cols !== 'none' ? `grid-cols:${cols}` : 'grid';
|
|
92
|
+
}
|
|
93
|
+
return '';
|
|
94
|
+
}
|
|
95
|
+
function walk(parent, depth, ctx) {
|
|
96
|
+
if (depth > ctx.maxDepth)
|
|
97
|
+
return;
|
|
98
|
+
for (const child of parent.children) {
|
|
99
|
+
if (ctx.nodes >= ctx.maxNodes) {
|
|
100
|
+
ctx.truncated = true;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (skip(child))
|
|
104
|
+
continue;
|
|
105
|
+
const role = getRole(child);
|
|
106
|
+
const name = getAccessibleName(child);
|
|
107
|
+
const interactive = INTERACTIVE.has(role);
|
|
108
|
+
// A generic, unnamed container's own text content — only consulted outside INTERACTIVE mode,
|
|
109
|
+
// so the actionable-only view stays lean while FULL/meaningful views see content regressions.
|
|
110
|
+
const lean = ctx.mode === SnapshotMode.INTERACTIVE;
|
|
111
|
+
const text = !lean && role === 'generic' && name.length === 0 ? directText(child) : '';
|
|
112
|
+
// Layout signature for grid/flex containers — makes CLS/layout regressions visible.
|
|
113
|
+
const layout = lean ? '' : layoutSignature(child);
|
|
114
|
+
const meaningful = interactive || role !== 'generic' || name.length > 0 || text.length > 0 || layout.length > 0;
|
|
115
|
+
const include = lean ? interactive : meaningful;
|
|
116
|
+
if (include) {
|
|
117
|
+
ctx.nodes += 1;
|
|
118
|
+
ctx.lines.push(text.length > 0 && name.length === 0 && layout.length === 0
|
|
119
|
+
? formatTextLine(depth, text)
|
|
120
|
+
: formatLine(child, depth, role, name, layout));
|
|
121
|
+
walk(child, depth + 1, ctx);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
walk(child, depth, ctx);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function collectDialogs(root) {
|
|
129
|
+
const nodes = root.querySelectorAll('[role="dialog"], dialog[open], [aria-modal="true"]');
|
|
130
|
+
const names = [];
|
|
131
|
+
for (const node of nodes) {
|
|
132
|
+
if (isVisible(node))
|
|
133
|
+
names.push(getAccessibleName(node) || '(unnamed dialog)');
|
|
134
|
+
}
|
|
135
|
+
return names;
|
|
136
|
+
}
|
|
137
|
+
function buildStatus(root) {
|
|
138
|
+
return {
|
|
139
|
+
route: `${location.pathname}${location.search}${location.hash}`,
|
|
140
|
+
title: document.title,
|
|
141
|
+
visibleDialogs: collectDialogs(root),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/** Build the semantic accessibility snapshot of the page or a subtree (plan/04). */
|
|
145
|
+
export function buildSnapshot(options = {}) {
|
|
146
|
+
const mode = options.mode ?? SnapshotMode.FULL;
|
|
147
|
+
const scopeEl = options.scope !== undefined
|
|
148
|
+
? (refs.resolve(options.scope) ?? document.querySelector(options.scope))
|
|
149
|
+
: document.body;
|
|
150
|
+
const root = scopeEl ?? document.body;
|
|
151
|
+
const status = buildStatus(root);
|
|
152
|
+
if (mode === SnapshotMode.STATUS) {
|
|
153
|
+
return { tree: '', status, nodes: 0, truncated: false };
|
|
154
|
+
}
|
|
155
|
+
const ctx = {
|
|
156
|
+
lines: [],
|
|
157
|
+
nodes: 0,
|
|
158
|
+
truncated: false,
|
|
159
|
+
mode,
|
|
160
|
+
maxNodes: options.maxNodes ?? 400,
|
|
161
|
+
maxDepth: options.maxDepth ?? 20,
|
|
162
|
+
};
|
|
163
|
+
walk(root, 0, ctx);
|
|
164
|
+
return {
|
|
165
|
+
tree: ctx.lines.join('\n'),
|
|
166
|
+
status,
|
|
167
|
+
nodes: ctx.nodes,
|
|
168
|
+
truncated: ctx.truncated,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=snapshot.js.map
|