@reticlehq/browser 2.13.1 → 3.1.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/actions/actions-dom.d.ts +8 -20
- package/dist/actions/actions-dom.js +70 -69
- package/dist/actions/actions-press.d.ts +14 -0
- package/dist/actions/actions-press.js +123 -0
- package/dist/actions/actions.d.ts +1 -89
- package/dist/actions/actions.js +64 -365
- package/dist/actions/appeared-text.d.ts +0 -20
- package/dist/actions/appeared-text.js +6 -36
- package/dist/actions/danger-context.d.ts +3 -0
- package/dist/actions/danger-context.js +39 -0
- package/dist/actions/scroll.d.ts +8 -10
- package/dist/actions/scroll.js +28 -17
- package/dist/actions/synthetic/synthetic-input.d.ts +2 -0
- package/dist/actions/synthetic/synthetic-input.js +13 -0
- package/dist/actions/value-input.d.ts +0 -30
- package/dist/actions/value-input.js +0 -33
- package/dist/commands/commands.d.ts +0 -2
- package/dist/commands/commands.js +10 -70
- package/dist/connect-options.d.ts +2 -96
- package/dist/dom/a11y.d.ts +0 -29
- package/dist/dom/a11y.js +3 -90
- package/dist/dom/addressing/auto-anchor.d.ts +25 -0
- package/dist/dom/addressing/auto-anchor.js +41 -0
- package/dist/dom/addressing/refs.d.ts +14 -0
- package/dist/dom/addressing/refs.js +114 -0
- package/dist/dom/addressing/source.d.ts +9 -0
- package/dist/dom/addressing/source.js +29 -0
- package/dist/dom/chart.d.ts +0 -61
- package/dist/dom/chart.js +0 -60
- package/dist/dom/desktop-capture.d.ts +0 -30
- package/dist/dom/desktop-capture.js +1 -73
- package/dist/dom/dom-ignore.d.ts +0 -30
- package/dist/dom/dom-ignore.js +1 -48
- package/dist/dom/occlusion.d.ts +0 -7
- package/dist/dom/occlusion.js +31 -12
- package/dist/dom/paint-context.d.ts +6 -0
- package/dist/dom/paint-context.js +38 -0
- package/dist/dom/query.d.ts +0 -14
- package/dist/dom/query.js +50 -216
- package/dist/dom/realm.d.ts +0 -20
- package/dist/dom/realm.js +0 -28
- package/dist/dom/shadow-registry.d.ts +0 -33
- package/dist/dom/shadow-registry.js +2 -72
- package/dist/dom/snapshot.d.ts +2 -43
- package/dist/dom/snapshot.js +27 -152
- package/dist/dom/theme.d.ts +0 -37
- package/dist/dom/theme.js +2 -52
- package/dist/dom/virtualized.d.ts +0 -17
- package/dist/dom/virtualized.js +0 -27
- package/dist/edit-epoch.d.ts +1 -23
- package/dist/edit-epoch.js +1 -53
- package/dist/index.d.ts +4 -5
- package/dist/index.js +4 -28
- package/dist/observers/animation.d.ts +0 -1
- package/dist/observers/animation.js +1 -4
- package/dist/observers/blind-spots.d.ts +0 -26
- package/dist/observers/blind-spots.js +3 -62
- package/dist/observers/console.d.ts +1 -2
- package/dist/observers/console.js +41 -30
- package/dist/observers/context-open.d.ts +1 -11
- package/dist/observers/context-open.js +10 -14
- package/dist/observers/departure.d.ts +3 -0
- package/dist/observers/departure.js +52 -0
- package/dist/observers/dialogs.d.ts +2 -0
- package/dist/observers/dialogs.js +39 -0
- package/dist/observers/dom.js +28 -52
- package/dist/observers/download.d.ts +1 -2
- package/dist/observers/download.js +39 -56
- package/dist/observers/focus.d.ts +0 -6
- package/dist/observers/focus.js +0 -7
- package/dist/observers/frames.d.ts +0 -9
- package/dist/observers/frames.js +1 -28
- package/dist/observers/health.d.ts +0 -5
- package/dist/observers/health.js +2 -34
- package/dist/observers/install-all.d.ts +2 -0
- package/dist/observers/install-all.js +26 -27
- package/dist/observers/ipc.d.ts +0 -25
- package/dist/observers/ipc.js +0 -65
- package/dist/observers/navigation.d.ts +2 -0
- package/dist/observers/navigation.js +33 -0
- package/dist/observers/net-detail/network-body.d.ts +12 -0
- package/dist/observers/net-detail/network-body.js +71 -0
- package/dist/observers/net-detail/network-redact.d.ts +1 -0
- package/dist/observers/net-detail/network-redact.js +1 -0
- package/dist/observers/network-stream.d.ts +0 -21
- package/dist/observers/network-stream.js +3 -46
- package/dist/observers/network.d.ts +3 -27
- package/dist/observers/network.js +178 -295
- package/dist/observers/perf.d.ts +0 -10
- package/dist/observers/perf.js +0 -16
- package/dist/observers/route.d.ts +1 -2
- package/dist/observers/route.js +13 -19
- package/dist/observers/scroll.d.ts +0 -1
- package/dist/observers/scroll.js +2 -11
- package/dist/observers/sdk-failure.d.ts +1 -9
- package/dist/observers/sdk-failure.js +1 -30
- package/dist/observers/state.d.ts +1 -11
- package/dist/observers/state.js +13 -41
- package/dist/observers/storage.d.ts +0 -14
- package/dist/observers/storage.js +10 -34
- package/dist/observers/types.d.ts +0 -15
- package/dist/observers/types.js +0 -14
- package/dist/patching/capture-method.d.ts +0 -24
- package/dist/patching/capture-method.js +0 -26
- package/dist/presenter/chrome/layers.d.ts +2 -0
- package/dist/presenter/chrome/layers.js +2 -0
- package/dist/presenter/chrome/log-kinds.d.ts +12 -0
- package/dist/presenter/chrome/log-kinds.js +7 -0
- package/dist/presenter/{overlay.d.ts → chrome/overlay.d.ts} +0 -1
- package/dist/presenter/{overlay.js → chrome/overlay.js} +0 -1
- package/dist/presenter/chrome/presenter-brand.d.ts +3 -0
- package/dist/presenter/{presenter-brand.js → chrome/presenter-brand.js} +0 -21
- package/dist/presenter/chrome/presenter-effects.d.ts +4 -0
- package/dist/presenter/{presenter-effects.js → chrome/presenter-effects.js} +1 -10
- package/dist/presenter/chrome/presenter-hud-chrome.d.ts +7 -0
- package/dist/presenter/{presenter-hud-chrome.js → chrome/presenter-hud-chrome.js} +0 -10
- package/dist/presenter/chrome/presenter-log.d.ts +17 -0
- package/dist/presenter/{presenter-log.js → chrome/presenter-log.js} +5 -54
- package/dist/presenter/chrome/presenter-report-copy.d.ts +51 -0
- package/dist/presenter/chrome/presenter-report-copy.js +95 -0
- package/dist/presenter/chrome/presenter-safe-html.d.ts +2 -0
- package/dist/presenter/chrome/presenter-safe-html.js +16 -0
- package/dist/presenter/{presenter-tally.d.ts → chrome/presenter-tally.d.ts} +0 -5
- package/dist/presenter/{presenter-tally.js → chrome/presenter-tally.js} +1 -5
- package/dist/presenter/{presenter-verbs.js → chrome/presenter-verbs.js} +0 -7
- package/dist/presenter/{presenter-heroicons-data.d.ts → icons/presenter-heroicons-data.d.ts} +1 -8
- package/dist/presenter/{presenter-heroicons-data.js → icons/presenter-heroicons-data.js} +1 -8
- package/dist/presenter/{presenter-icons.d.ts → icons/presenter-icons.d.ts} +1 -6
- package/dist/presenter/{presenter-icons.js → icons/presenter-icons.js} +1 -6
- package/dist/presenter/presenter-account-styles.d.ts +1 -0
- package/dist/presenter/presenter-account-styles.js +99 -0
- package/dist/presenter/presenter-account.d.ts +40 -0
- package/dist/presenter/presenter-account.js +178 -0
- package/dist/presenter/presenter-config.d.ts +10 -107
- package/dist/presenter/presenter-config.js +5 -92
- package/dist/presenter/presenter-controls.d.ts +1 -35
- package/dist/presenter/presenter-controls.js +10 -133
- package/dist/presenter/presenter-dock-layout.d.ts +0 -3
- package/dist/presenter/presenter-dock-layout.js +1 -5
- package/dist/presenter/presenter-drag.d.ts +2 -42
- package/dist/presenter/presenter-drag.js +3 -76
- package/dist/presenter/presenter-glow.d.ts +0 -21
- package/dist/presenter/presenter-glow.js +2 -28
- package/dist/presenter/presenter-hud-position.d.ts +18 -0
- package/dist/presenter/presenter-hud-position.js +50 -0
- package/dist/presenter/presenter-report-styles.d.ts +0 -6
- package/dist/presenter/presenter-report-styles.js +22 -7
- package/dist/presenter/presenter-report.d.ts +5 -7
- package/dist/presenter/presenter-report.js +44 -95
- package/dist/presenter/presenter-run-state.d.ts +1 -8
- package/dist/presenter/presenter-run-state.js +1 -6
- package/dist/presenter/presenter-settings-styles.d.ts +0 -1
- package/dist/presenter/presenter-settings-styles.js +2 -2
- package/dist/presenter/presenter-settings.d.ts +5 -23
- package/dist/presenter/presenter-settings.js +21 -33
- package/dist/presenter/presenter-shell-styles.js +23 -20
- package/dist/presenter/presenter-shell.d.ts +6 -18
- package/dist/presenter/presenter-shell.js +26 -77
- package/dist/presenter/presenter-styles.d.ts +2 -10
- package/dist/presenter/presenter-styles.js +6 -12
- package/dist/presenter/presenter-workspace.d.ts +4 -4
- package/dist/presenter/presenter-workspace.js +30 -10
- package/dist/presenter/presenter.d.ts +3 -63
- package/dist/presenter/presenter.js +29 -132
- package/dist/presenter/tour/tour-view.d.ts +33 -0
- package/dist/presenter/tour/tour-view.js +188 -0
- package/dist/presenter/tour/tour.d.ts +25 -0
- package/dist/presenter/tour/tour.js +245 -0
- package/dist/recorder/recorder-styles.d.ts +0 -6
- package/dist/recorder/recorder-styles.js +0 -6
- package/dist/recorder/recorder.d.ts +0 -34
- package/dist/recorder/recorder.js +6 -49
- package/dist/registry/auto-testids.d.ts +0 -19
- package/dist/registry/auto-testids.js +1 -21
- package/dist/registry/capabilities.d.ts +0 -31
- package/dist/registry/capabilities.js +1 -46
- package/dist/registry/commit-and-signal.d.ts +0 -6
- package/dist/registry/commit-and-signal.js +1 -7
- package/dist/registry/declared-channels.d.ts +2 -0
- package/dist/registry/declared-channels.js +14 -0
- package/dist/registry/declared-platform.d.ts +4 -0
- package/dist/registry/declared-platform.js +34 -0
- package/dist/registry/domains.d.ts +0 -8
- package/dist/registry/domains.js +0 -7
- package/dist/registry/emitter.d.ts +0 -14
- package/dist/registry/emitter.js +1 -11
- package/dist/registry/store-adapters.d.ts +0 -186
- package/dist/registry/store-adapters.js +1 -164
- package/dist/registry/{adapters.d.ts → stores/adapters.d.ts} +0 -10
- package/dist/registry/{adapters.js → stores/adapters.js} +0 -10
- package/dist/registry/stores.d.ts +0 -33
- package/dist/registry/stores.js +1 -60
- package/dist/reticle-presenter-helpers.d.ts +0 -13
- package/dist/reticle-presenter-helpers.js +10 -20
- package/dist/reticle-singleton.d.ts +2 -0
- package/dist/reticle-singleton.js +3 -0
- package/dist/reticle.d.ts +1 -52
- package/dist/reticle.js +115 -180
- package/dist/review/annotator-styles.js +1 -8
- package/dist/review/annotator.d.ts +2 -13
- package/dist/review/annotator.js +2 -42
- package/dist/review/mark-anchor.d.ts +0 -11
- package/dist/review/mark-anchor.js +3 -11
- package/dist/security/serialization.d.ts +0 -20
- package/dist/security/serialization.js +1 -66
- package/dist/session-continuity.d.ts +0 -16
- package/dist/session-continuity.js +1 -20
- package/dist/timers/clock.d.ts +1 -14
- package/dist/timers/clock.js +59 -60
- package/dist/timers/console/native-console.js +5 -0
- package/dist/timers/native/native-timers.d.ts +11 -0
- package/dist/timers/native/native-timers.js +48 -0
- package/dist/transport/transport.d.ts +0 -51
- package/dist/transport/transport.js +5 -131
- package/dist/transport/unreachable-message.d.ts +0 -17
- package/dist/transport/unreachable-message.js +0 -17
- package/dist/util/captured-method.d.ts +0 -35
- package/dist/util/captured-method.js +0 -35
- package/package.json +5 -4
- package/dist/actions/synthetic-input.d.ts +0 -9
- package/dist/actions/synthetic-input.js +0 -40
- package/dist/dom/auto-anchor.d.ts +0 -65
- package/dist/dom/auto-anchor.js +0 -74
- package/dist/dom/refs.d.ts +0 -90
- package/dist/dom/refs.js +0 -245
- package/dist/dom/source.d.ts +0 -53
- package/dist/dom/source.js +0 -74
- package/dist/observers/network-body.d.ts +0 -35
- package/dist/observers/network-body.js +0 -168
- package/dist/observers/network-redact.d.ts +0 -20
- package/dist/observers/network-redact.js +0 -80
- package/dist/presenter/presenter-brand.d.ts +0 -22
- package/dist/presenter/presenter-effects.d.ts +0 -13
- package/dist/presenter/presenter-hud-chrome.d.ts +0 -17
- package/dist/presenter/presenter-log.d.ts +0 -64
- package/dist/presenter/presenter-report-copy.d.ts +0 -112
- package/dist/presenter/presenter-report-copy.js +0 -156
- package/dist/presenter/presenter-test-helpers.d.ts +0 -16
- package/dist/presenter/presenter-test-helpers.js +0 -41
- package/dist/timers/native-console.js +0 -9
- package/dist/timers/native-timers.d.ts +0 -36
- package/dist/timers/native-timers.js +0 -78
- /package/dist/presenter/{presenter-verbs.d.ts → chrome/presenter-verbs.d.ts} +0 -0
- /package/dist/timers/{native-console.d.ts → console/native-console.d.ts} +0 -0
package/dist/dom/query.js
CHANGED
|
@@ -2,39 +2,20 @@ import { DATA_RETICLE_SOURCE_ATTR, ElementState, QueryBy, REDACTED_VALUE, TRANSP
|
|
|
2
2
|
import { isFrame, isHtmlElement, isInput, isSelect, isTextArea } from './realm.js';
|
|
3
3
|
import { capturedRootOf } from './shadow-registry.js';
|
|
4
4
|
import { getAccessibleName, getRole, describe, getStates, isInViewport, isVisible, } from './a11y.js';
|
|
5
|
-
import { isIgnored } from './dom-ignore.js';
|
|
5
|
+
import { isIgnored, isReticleOverlay } from './dom-ignore.js';
|
|
6
6
|
import { isSensitiveKey } from '../security/serialization.js';
|
|
7
7
|
import { declaredTestids } from '../registry/capabilities.js';
|
|
8
|
-
import { identifyComponent } from '../registry/adapters.js';
|
|
9
|
-
import { refs } from './refs.js';
|
|
8
|
+
import { identifyComponent } from '../registry/stores/adapters.js';
|
|
9
|
+
import { refs } from './addressing/refs.js';
|
|
10
10
|
const TESTID_ATTR = 'data-testid';
|
|
11
11
|
const SOURCE_ATTR = DATA_RETICLE_SOURCE_ATTR;
|
|
12
12
|
const MAX_PRESENT_TESTIDS = 12;
|
|
13
|
-
/** Bound the fiber-walk fallback so a component-name query can't scan an unbounded DOM. */
|
|
14
13
|
const MAX_COMPONENT_CANDIDATES = 2000;
|
|
15
|
-
/** Likely-actionable elements considered when resolving a component anchor without a source stamp. */
|
|
16
14
|
const COMPONENT_CANDIDATE_SELECTOR = `[${SOURCE_ATTR}], [${TESTID_ATTR}], button, a, input, select, textarea, [role]`;
|
|
17
|
-
/**
|
|
18
|
-
* Every candidate a semantic locator may match: the container ITSELF first, then its descendants.
|
|
19
|
-
*
|
|
20
|
-
* The container's own text, role or name was matched by every engine this replaces - a scoped
|
|
21
|
-
* `{ text: "Saved" }` against `<div id="status">Saved</div>` found #status itself - so dropping
|
|
22
|
-
* the root here would turn exactly those scoped queries into silent zero-match answers: the worst
|
|
23
|
-
* failure mode, indistinguishable from the element being absent. `self: true` stays the way to
|
|
24
|
-
* reach an UNLABELLED root (it skips the predicate entirely); this keeps a root that DOES satisfy
|
|
25
|
-
* the predicate findable without a second spelling.
|
|
26
|
-
*/
|
|
27
15
|
function elementsUnder(container) {
|
|
28
|
-
// Embedded roots include ShadowRoots, which are DocumentFragments: they have no attributes or
|
|
29
|
-
// tag to match on, so only a true Element root joins the candidates.
|
|
30
16
|
const self = Node.ELEMENT_NODE === container.nodeType ? [container] : [];
|
|
31
17
|
return [...self, ...Array.from(container.querySelectorAll('*'))];
|
|
32
18
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Match user-visible prose the way the public query API has always promised: trim and collapse
|
|
35
|
-
* whitespace, compare canonically equivalent Unicode in NFC, and keep fuzzy text queries
|
|
36
|
-
* case-insensitive. Attribute identifiers such as testid stay exact and do not pass through here.
|
|
37
|
-
*/
|
|
38
19
|
function normaliseVisibleText(value) {
|
|
39
20
|
return value.replace(/\s+/g, ' ').trim().normalize('NFC');
|
|
40
21
|
}
|
|
@@ -60,15 +41,6 @@ function directText(el) {
|
|
|
60
41
|
.map((node) => node.textContent ?? '')
|
|
61
42
|
.join('');
|
|
62
43
|
}
|
|
63
|
-
/**
|
|
64
|
-
* Elements whose accessible name comes from author-supplied naming rather than free subtree
|
|
65
|
-
* content - the set `by: label` addresses.
|
|
66
|
-
*
|
|
67
|
-
* Form controls plus anything explicitly named with aria-label/aria-labelledby covers the common
|
|
68
|
-
* case. `button`, `meter`, `output` and `progress` are included because their names arrive on
|
|
69
|
-
* attributes (`value`) or from their caption, which is precisely what a label query is looking
|
|
70
|
-
* for; leaving them out would silently drop buttons from label searches.
|
|
71
|
-
*/
|
|
72
44
|
function semanticNameTarget(el) {
|
|
73
45
|
if (isInput(el) || isTextArea(el) || isSelect(el))
|
|
74
46
|
return true;
|
|
@@ -80,12 +52,6 @@ function semanticNameTarget(el) {
|
|
|
80
52
|
el.hasAttribute('aria-label') ||
|
|
81
53
|
el.hasAttribute('aria-labelledby'));
|
|
82
54
|
}
|
|
83
|
-
/**
|
|
84
|
-
* Resolve a scope to its container. A container of `null` means a scope was GIVEN but resolved to
|
|
85
|
-
* nothing (unmounted, or a selector that matches no element) - the caller must NOT fall back to the
|
|
86
|
-
* whole page, or a scoped query silently widens into a phantom match. `scope === undefined` (no scope)
|
|
87
|
-
* legitimately searches the body.
|
|
88
|
-
*/
|
|
89
55
|
function resolveContainer(scope) {
|
|
90
56
|
if (scope === undefined)
|
|
91
57
|
return { container: document.body, scopeMissing: false };
|
|
@@ -98,16 +64,9 @@ function resolveContainer(scope) {
|
|
|
98
64
|
return { container: found, scopeMissing: false };
|
|
99
65
|
}
|
|
100
66
|
catch {
|
|
101
|
-
// invalid selector - treat as a missing scope, never a whole-page search
|
|
102
67
|
}
|
|
103
68
|
return { container: null, scopeMissing: true };
|
|
104
69
|
}
|
|
105
|
-
/**
|
|
106
|
-
* Resolve an element by its SOURCE location - the precise, granular auto-anchor. The babel plugin
|
|
107
|
-
* stamps `data-reticle-source="file:line:column"` on host elements, so a line-level starts-with match
|
|
108
|
-
* pins the exact JSX element with a single fast attribute selector (no fiber walk). Column is
|
|
109
|
-
* ignored so a small column drift doesn't unbind the anchor.
|
|
110
|
-
*/
|
|
111
70
|
function findBySource(container, source) {
|
|
112
71
|
const prefix = `${source.file}:${source.line}:`.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
113
72
|
try {
|
|
@@ -117,12 +76,6 @@ function findBySource(container, source) {
|
|
|
117
76
|
return [];
|
|
118
77
|
}
|
|
119
78
|
}
|
|
120
|
-
/**
|
|
121
|
-
* Resolve by component display name when no source stamp is available: scan a bounded set of
|
|
122
|
-
* likely-actionable elements and keep those whose NEAREST enclosing component (via the registered
|
|
123
|
-
* framework adapter) matches. Coarser than source (one component renders many hosts) - used as a
|
|
124
|
-
* fallback / for frameworks without a source plugin.
|
|
125
|
-
*/
|
|
126
79
|
function findByComponentName(container, component) {
|
|
127
80
|
const out = [];
|
|
128
81
|
let scanned = 0;
|
|
@@ -136,7 +89,6 @@ function findByComponentName(container, component) {
|
|
|
136
89
|
}
|
|
137
90
|
return out;
|
|
138
91
|
}
|
|
139
|
-
/** Auto-anchor resolution: source (precise) first, then component name (coarse fallback). */
|
|
140
92
|
function findByComponent(container, query) {
|
|
141
93
|
if (query.source !== undefined) {
|
|
142
94
|
const bySource = findBySource(container, query.source);
|
|
@@ -148,13 +100,6 @@ function findByComponent(container, query) {
|
|
|
148
100
|
}
|
|
149
101
|
return [];
|
|
150
102
|
}
|
|
151
|
-
/**
|
|
152
|
-
* Role + name, matched with the same local accessibility engine used to describe results.
|
|
153
|
-
*
|
|
154
|
-
* This intentionally makes Reticle's reported role and name the source of truth. If an element is
|
|
155
|
-
* described as `textbox "Search User"`, the resolver must accept that exact pair, not a different
|
|
156
|
-
* role or name from a second library.
|
|
157
|
-
*/
|
|
158
103
|
function queryByRoleAndName(container, role, name) {
|
|
159
104
|
return elementsUnder(container).filter((el) => getRole(el) === role && (name === undefined || exactVisibleText(getAccessibleName(el), name)));
|
|
160
105
|
}
|
|
@@ -179,11 +124,9 @@ function queryByAlt(container, value) {
|
|
|
179
124
|
return alt !== null && fuzzyVisibleText(alt, value);
|
|
180
125
|
});
|
|
181
126
|
}
|
|
182
|
-
/** Run the appropriate local query against ONE root, light DOM or a shadow root. */
|
|
183
127
|
function findIn(container, query) {
|
|
184
128
|
const by = query.by;
|
|
185
129
|
const value = query.value;
|
|
186
|
-
// Explicit `by`+`value` form.
|
|
187
130
|
if (by !== undefined && value !== undefined) {
|
|
188
131
|
switch (by) {
|
|
189
132
|
case QueryBy.ROLE:
|
|
@@ -199,25 +142,14 @@ function findIn(container, query) {
|
|
|
199
142
|
case QueryBy.ALT:
|
|
200
143
|
return queryByAlt(container, value);
|
|
201
144
|
case QueryBy.COMPONENT:
|
|
202
|
-
// value is the component name;.source (if present) still takes precedence inside.
|
|
203
145
|
return findByComponent(container, { ...query, component: query.component ?? value });
|
|
204
146
|
default:
|
|
205
|
-
// THROW, never `return []`. An unsupported strategy answering "no matches" is
|
|
206
|
-
// indistinguishable from the element genuinely being absent, so `by:'css'` - the first thing
|
|
207
|
-
// anyone arriving from Playwright reaches for - reported a page with a <body> as empty. The
|
|
208
|
-
// server now rejects unknown strategies at the schema; this is the same guarantee for every
|
|
209
|
-
// other path in (replay, reticle_run, an internal caller), because a false negative invented
|
|
210
|
-
// by the tool is the exact failure this product exists to prevent.
|
|
211
147
|
throw new Error(`unsupported query strategy '${String(by)}' - use one of: ${Object.values(QueryBy).join(', ')}`);
|
|
212
148
|
}
|
|
213
149
|
}
|
|
214
|
-
// Auto-anchor (component / source) - checked before the role/text fields so a query carrying
|
|
215
|
-
// both a component anchor and an incidental role resolves by the more durable anchor.
|
|
216
150
|
if (query.component !== undefined || query.source !== undefined) {
|
|
217
151
|
return findByComponent(container, query);
|
|
218
152
|
}
|
|
219
|
-
// Structured form (role+name, or any single field). Same round-trip guarantee as the by+value
|
|
220
|
-
// spelling above - the two forms must not disagree about what is findable.
|
|
221
153
|
if (query.role !== undefined) {
|
|
222
154
|
return queryByRoleAndName(container, query.role, query.name);
|
|
223
155
|
}
|
|
@@ -233,19 +165,6 @@ function findIn(container, query) {
|
|
|
233
165
|
return queryByAlt(container, query.alt);
|
|
234
166
|
return [];
|
|
235
167
|
}
|
|
236
|
-
/**
|
|
237
|
-
* Every open shadow root at or beneath `root`, in document order.
|
|
238
|
-
*
|
|
239
|
-
* A closed root is deliberately unreachable - `element.shadowRoot` is null by design, and the SDK
|
|
240
|
-
* reports that as a blind spot rather than pretending to see through it.
|
|
241
|
-
*/
|
|
242
|
-
/**
|
|
243
|
-
* A same-origin `<iframe>`'s body, or null when the document is unreachable.
|
|
244
|
-
*
|
|
245
|
-
* Cross-origin access THROWS in some engines and yields null in others, so both are handled - and
|
|
246
|
-
* both mean the same thing here: not ours to read. That case is not a silent skip, it is reported
|
|
247
|
-
* separately as a declared blind spot.
|
|
248
|
-
*/
|
|
249
168
|
function readableFrameBody(frame) {
|
|
250
169
|
try {
|
|
251
170
|
return frame.contentDocument?.body ?? null;
|
|
@@ -254,27 +173,15 @@ function readableFrameBody(frame) {
|
|
|
254
173
|
return null;
|
|
255
174
|
}
|
|
256
175
|
}
|
|
257
|
-
/** How deep to follow frames-inside-frames before giving up. Real consoles nest one, maybe two. */
|
|
258
176
|
const FRAME_DEPTH_MAX = 3;
|
|
259
|
-
/**
|
|
260
|
-
* Every root beneath the scope that the page can read but `querySelectorAll` will not reach:
|
|
261
|
-
* open shadow roots, and the documents of SAME-ORIGIN iframes.
|
|
262
|
-
*
|
|
263
|
-
* The iframe half was measured missing: a same-origin frame whose text the page could read straight
|
|
264
|
-
* off `contentDocument` returned zero matches from `query`, and coverage said nothing about it - the
|
|
265
|
-
* worst combination, an absent-element answer for content that is right there. Cross-origin frames
|
|
266
|
-
* are a genuinely different case and stay unread; they are declared instead.
|
|
267
|
-
*/
|
|
268
177
|
function embeddedRootsUnder(root) {
|
|
269
178
|
const found = [];
|
|
270
179
|
const walk = (node, depth) => {
|
|
271
180
|
for (const el of node.querySelectorAll('*')) {
|
|
272
181
|
const shadow = el.shadowRoot ?? capturedRootOf(el);
|
|
273
182
|
if (shadow !== null) {
|
|
274
|
-
// A ShadowRoot is a DocumentFragment. The local query helpers only call querySelectorAll on
|
|
275
|
-
// the container, and every call site only reads from it.
|
|
276
183
|
found.push(shadow);
|
|
277
|
-
walk(shadow, depth);
|
|
184
|
+
walk(shadow, depth);
|
|
278
185
|
continue;
|
|
279
186
|
}
|
|
280
187
|
if (depth >= FRAME_DEPTH_MAX)
|
|
@@ -283,7 +190,7 @@ function embeddedRootsUnder(root) {
|
|
|
283
190
|
continue;
|
|
284
191
|
const body = readableFrameBody(el);
|
|
285
192
|
if (null === body)
|
|
286
|
-
continue;
|
|
193
|
+
continue;
|
|
287
194
|
found.push(body);
|
|
288
195
|
walk(body, depth + 1);
|
|
289
196
|
}
|
|
@@ -291,42 +198,16 @@ function embeddedRootsUnder(root) {
|
|
|
291
198
|
walk(root, 0);
|
|
292
199
|
return found;
|
|
293
200
|
}
|
|
294
|
-
/**
|
|
295
|
-
* NOTE ON COST. The walk above is a `querySelectorAll('*')` over the container, and `reticle_query` is
|
|
296
|
-
* a hot-path tool, so every query on every app pays it - including the overwhelming majority that hold
|
|
297
|
-
* no web components.
|
|
298
|
-
*
|
|
299
|
-
* A cache invalidated by a MutationObserver was written and then REMOVED, because it is not sound:
|
|
300
|
-
* `attachShadow` on an element that is already in the document mutates only the shadow tree, which an
|
|
301
|
-
* observer watching documentElement's subtree never sees. The cached "no shadow roots here" would
|
|
302
|
-
* persist and the content would be silently missed - a false negative indistinguishable from a genuinely
|
|
303
|
-
* absent element, which is the exact bug the piercing was added to fix.
|
|
304
|
-
*
|
|
305
|
-
* Correctness wins until there is a measurement saying the walk actually matters. The allocation is
|
|
306
|
-
* avoided (iterate the live NodeList rather than copying it); the traversal itself stays.
|
|
307
|
-
*/
|
|
308
|
-
/**
|
|
309
|
-
* Run the query against the light DOM AND every open shadow root beneath the scope.
|
|
310
|
-
*
|
|
311
|
-
* A single querySelectorAll call only walks the root it starts from, so a control inside a web
|
|
312
|
-
* component returned zero matches on a completely healthy page - a miss indistinguishable from a
|
|
313
|
-
* genuinely absent element. The snapshot has always pierced open roots; this makes `query` agree
|
|
314
|
-
* with it.
|
|
315
|
-
*/
|
|
316
201
|
function findCandidates(query) {
|
|
317
202
|
const { container, scopeMissing } = resolveContainer(query.scope);
|
|
318
|
-
// A given-but-missing scope searches NOTHING - never the whole page. The empty result plus the
|
|
319
|
-
// scopeMissing flag is what keeps "gone scope" distinct from "absent element".
|
|
320
203
|
if (null === container)
|
|
321
204
|
return { candidates: [], scopeMissing: true };
|
|
322
|
-
// `self: true` asks for the container ITSELF, which every other path excludes by construction.
|
|
323
|
-
// A layout element with no role, name, testid or own text is unreachable by any semantic locator,
|
|
324
|
-
// and it is routinely the element carrying the handler - "click the empty space in this row" is a
|
|
325
|
-
// real user action that was simply not expressible. Requires a scope: without one there is no root
|
|
326
|
-
// to return, and answering `document.body` would be a wrong answer wearing the shape of one.
|
|
327
205
|
if (true === query.self) {
|
|
328
206
|
if (query.scope === undefined)
|
|
329
207
|
return { candidates: [], scopeMissing };
|
|
208
|
+
if (query.text !== undefined && !fuzzyVisibleText(container.textContent ?? '', query.text)) {
|
|
209
|
+
return { candidates: [], scopeMissing };
|
|
210
|
+
}
|
|
330
211
|
return { candidates: isIgnored(container) ? [] : [container], scopeMissing };
|
|
331
212
|
}
|
|
332
213
|
const seen = new Set();
|
|
@@ -334,14 +215,7 @@ function findCandidates(query) {
|
|
|
334
215
|
const collect = (els) => {
|
|
335
216
|
for (const el of els) {
|
|
336
217
|
if (seen.has(el))
|
|
337
|
-
continue;
|
|
338
|
-
// Reticle's OWN UI is not part of the app under test. `snapshot` has always excluded it; query
|
|
339
|
-
// did not, so the presenter panel and the annotator answered `by: role` like app controls -
|
|
340
|
-
// measured on a real merchant dashboard, 7 of the 40 buttons an agent could see were ours
|
|
341
|
-
// ("Pause", "End", "Chat", "Export"). Two of those
|
|
342
|
-
// share a NAME with a real control on that page, so an agent resolving "Export" could drive the
|
|
343
|
-
// observer instead of the app and then reason about the result. Filtered here, at the one
|
|
344
|
-
// collection point, so every entry path (by role/text/testid, shadow roots, scoped) is covered.
|
|
218
|
+
continue;
|
|
345
219
|
if (isIgnored(el))
|
|
346
220
|
continue;
|
|
347
221
|
seen.add(el);
|
|
@@ -353,17 +227,8 @@ function findCandidates(query) {
|
|
|
353
227
|
collect(findIn(embedded, query));
|
|
354
228
|
return { candidates: out, scopeMissing };
|
|
355
229
|
}
|
|
356
|
-
/** Longest attribute value returned; one enormous href must not blow the response budget. */
|
|
357
230
|
export const ATTR_VALUE_MAX = 512;
|
|
358
|
-
/** Most attributes projected per element - a guard against a caller asking for everything. */
|
|
359
231
|
const ATTR_KEYS_MAX = 12;
|
|
360
|
-
/**
|
|
361
|
-
* Read the requested attributes off an element.
|
|
362
|
-
*
|
|
363
|
-
* Absent attributes are OMITTED rather than returned empty, so "not present" and "present but blank"
|
|
364
|
-
* stay distinguishable. Credential-bearing names are redacted with the same rule the network and
|
|
365
|
-
* storage paths use - a projection API must not become an exfiltration path.
|
|
366
|
-
*/
|
|
367
232
|
function projectAttrs(el, keys) {
|
|
368
233
|
const out = {};
|
|
369
234
|
for (const key of keys.slice(0, ATTR_KEYS_MAX)) {
|
|
@@ -375,50 +240,27 @@ function projectAttrs(el, keys) {
|
|
|
375
240
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
376
241
|
}
|
|
377
242
|
function inState(el, state, memo) {
|
|
378
|
-
// inViewport is computed on demand rather than listed in getStates, so it stays assertable without
|
|
379
|
-
// adding a state to every element in every snapshot (it would bloat the wire and churn every
|
|
380
|
-
// describe). The predicate path is the only consumer that needs it. (#398)
|
|
381
243
|
if (ElementState.IN_VIEWPORT === state)
|
|
382
244
|
return isInViewport(el, memo);
|
|
383
245
|
return getStates(el, isVisible(el, memo)).includes(state);
|
|
384
246
|
}
|
|
385
|
-
/**
|
|
386
|
-
* How many matches are DESCRIBED, however many are found.
|
|
387
|
-
*
|
|
388
|
-
* describe() is the expensive part of a query: it resolves the accessible name and forces a style
|
|
389
|
-
* computation for every ancestor to decide visibility. Describing every match on a page with
|
|
390
|
-
* thousands of them freezes the main thread for seconds - and then the wire discards all but the
|
|
391
|
-
* first `MAX_COLLECTION_ITEMS` anyway, so the work past that point was never observable by anyone.
|
|
392
|
-
* Matching one to the other means the cost of a broad query is bounded by the transport rather than
|
|
393
|
-
* by the size of the page.
|
|
394
|
-
*/
|
|
395
247
|
const MAX_DESCRIBED = TRANSPORT_LIMITS.MAX_COLLECTION_ITEMS;
|
|
396
|
-
/**
|
|
397
|
-
* Match an element predicate against the live DOM.
|
|
398
|
-
*
|
|
399
|
-
* `count` is every match; `elements` is the described prefix. Keeping those separate is what lets
|
|
400
|
-
* "how many?" stay exact while the answer stays affordable.
|
|
401
|
-
*/
|
|
402
248
|
export function matchQuery(query, state, limit = MAX_DESCRIBED) {
|
|
403
|
-
// NO blanket try/catch here. It used to turn ANY exception during candidate-finding into
|
|
404
|
-
// `elements = []`, which is the same lie as the `default` arm above: a query that could not run
|
|
405
|
-
// reported that the element is not on the page. The one failure it was plausibly guarding -
|
|
406
|
-
// a scope selector that matches nothing - is already handled explicitly and distinctly, as
|
|
407
|
-
// `scopeMissing`, so what remained was a net that could only convert real faults into false
|
|
408
|
-
// negatives.
|
|
409
249
|
const found = findCandidates(query);
|
|
410
250
|
const elements = found.candidates;
|
|
411
251
|
const scopeMissing = found.scopeMissing;
|
|
412
|
-
// One visibility cache for the whole (synchronous) query pass. isVisible is an O(depth) forced-style
|
|
413
|
-
// walk; the state filter runs it over EVERY candidate (the count must be exact) - on a match-heavy
|
|
414
|
-
// page (e.g. a 3k-row grid) that is tens of thousands of getComputedStyle calls on the host's main
|
|
415
|
-
// thread. The memo makes each element's ancestors resolve once, then short-circuit for every sibling.
|
|
416
252
|
const visMemo = new Map();
|
|
417
253
|
const filtered = state === undefined ? elements : elements.filter((el) => inState(el, state, visMemo));
|
|
418
254
|
const attrs = query.attrs;
|
|
419
255
|
const described = filtered.slice(0, Math.max(0, Math.min(limit, MAX_DESCRIBED)));
|
|
256
|
+
const stampViewport = described.length > 1;
|
|
420
257
|
const descriptors = described.map((el) => {
|
|
421
|
-
|
|
258
|
+
let base = describe(el, visMemo);
|
|
259
|
+
if (stampViewport &&
|
|
260
|
+
isInViewport(el, visMemo) &&
|
|
261
|
+
!base.states.includes(ElementState.IN_VIEWPORT)) {
|
|
262
|
+
base = { ...base, states: [...base.states, ElementState.IN_VIEWPORT] };
|
|
263
|
+
}
|
|
422
264
|
if (attrs === undefined || 0 === attrs.length)
|
|
423
265
|
return base;
|
|
424
266
|
const projected = projectAttrs(el, attrs);
|
|
@@ -429,15 +271,9 @@ export function matchQuery(query, state, limit = MAX_DESCRIBED) {
|
|
|
429
271
|
count: filtered.length,
|
|
430
272
|
elements: descriptors,
|
|
431
273
|
...(scopeMissing ? { scopeMissing: true } : {}),
|
|
432
|
-
// On a MISS, carry the same diagnosis `runQuery` has always returned. MATCH is the command every
|
|
433
|
-
// PREDICATE uses, so without this a failed assertion was a dead end ("no element matched") while
|
|
434
|
-
// the identical failure through reticle_query listed the testids that ARE present. Computed only
|
|
435
|
-
// when there is nothing to report, so the hot path pays nothing.
|
|
436
274
|
...(0 === filtered.length ? { hint: buildEmptyHint(query) } : {}),
|
|
437
275
|
};
|
|
438
276
|
}
|
|
439
|
-
/** Resolve `aria-labelledby` (one or more element IDs) to the referenced elements' text - a bare ID is
|
|
440
|
-
* not a human-readable name. Undefined when unset or nothing resolves. */
|
|
441
277
|
function resolveLabelledBy(el) {
|
|
442
278
|
const ids = el.getAttribute('aria-labelledby');
|
|
443
279
|
if (null === ids)
|
|
@@ -449,10 +285,7 @@ function resolveLabelledBy(el) {
|
|
|
449
285
|
.join(' ');
|
|
450
286
|
return text.length > 0 ? text : undefined;
|
|
451
287
|
}
|
|
452
|
-
/** Structural clusters of the page - the successor to the raw testid list in zero-match hints. */
|
|
453
288
|
function buildPresentRegions(query) {
|
|
454
|
-
// For the diagnostic hint we WANT the page's orientation even when the scope is gone, so fall back
|
|
455
|
-
// to the body here (the match path above already reported scopeMissing, so this can't mask it).
|
|
456
289
|
const container = resolveContainer(query.scope).container ?? document.body;
|
|
457
290
|
const regions = [];
|
|
458
291
|
const CONTAINER_ROLES = [
|
|
@@ -476,8 +309,10 @@ function buildPresentRegions(query) {
|
|
|
476
309
|
for (const role of CONTAINER_ROLES) {
|
|
477
310
|
const containers = queryByRoleAndName(container, role, undefined);
|
|
478
311
|
for (const el of containers) {
|
|
312
|
+
if (isReticleOverlay(el))
|
|
313
|
+
continue;
|
|
479
314
|
const name = el.getAttribute('aria-label') ??
|
|
480
|
-
resolveLabelledBy(el) ??
|
|
315
|
+
resolveLabelledBy(el) ??
|
|
481
316
|
el.getAttribute('data-testid') ??
|
|
482
317
|
undefined;
|
|
483
318
|
const children = el.querySelectorAll('[role]');
|
|
@@ -504,29 +339,12 @@ function buildPresentRegions(query) {
|
|
|
504
339
|
}
|
|
505
340
|
return regions;
|
|
506
341
|
}
|
|
507
|
-
/**
|
|
508
|
-
* The tightest single element whose SUBTREE text carries `wanted`, when no element's OWN text does.
|
|
509
|
-
*
|
|
510
|
-
* `by: text` matches an element's direct text nodes (`directText`), so a label rendered across
|
|
511
|
-
* several child nodes — `v-html`, an inline `<span>` per word, a highlighted substring — exists on
|
|
512
|
-
* the page and matches nothing. The two channels disagree by construction: `act_and_wait` reports
|
|
513
|
-
* `appeared` by concatenating an added subtree, so the string an agent is most likely to copy into
|
|
514
|
-
* its next predicate is exactly the shape this query cannot find.
|
|
515
|
-
*
|
|
516
|
-
* That failure is indistinguishable from the element being absent, which is the expensive half: the
|
|
517
|
-
* agent reads "no element matched", concludes the app did not render, and reports a bug against
|
|
518
|
-
* correct code.
|
|
519
|
-
*
|
|
520
|
-
* Returns the DEEPEST container rather than the first, because every ancestor up to `<body>` also
|
|
521
|
-
* contains the string and naming `<body>` is not a locator. The deepest one is the tightest scope
|
|
522
|
-
* that still holds the whole string, which is the one worth pasting back.
|
|
523
|
-
*/
|
|
524
342
|
function splitTextOwner(container, wanted) {
|
|
525
343
|
let best;
|
|
526
344
|
let bestDepth = -1;
|
|
527
345
|
for (const el of elementsUnder(container)) {
|
|
528
346
|
if (isIgnored(el))
|
|
529
|
-
continue;
|
|
347
|
+
continue;
|
|
530
348
|
if (!fuzzyVisibleText(el.textContent ?? '', wanted))
|
|
531
349
|
continue;
|
|
532
350
|
let depth = 0;
|
|
@@ -539,20 +357,44 @@ function splitTextOwner(container, wanted) {
|
|
|
539
357
|
}
|
|
540
358
|
return best;
|
|
541
359
|
}
|
|
542
|
-
/** The text this query searched for, in either spelling, or undefined when it searched by something else. */
|
|
543
360
|
function wantedTextOf(query) {
|
|
544
361
|
if (query.text !== undefined)
|
|
545
362
|
return query.text;
|
|
546
363
|
return QueryBy.TEXT === query.by ? query.value : undefined;
|
|
547
364
|
}
|
|
548
|
-
|
|
365
|
+
const MAX_NAME_NEAR_MISSES = 5;
|
|
366
|
+
function nameNearMisses(container, query) {
|
|
367
|
+
const role = QueryBy.ROLE === query.by ? query.value : undefined;
|
|
368
|
+
const wanted = query.name;
|
|
369
|
+
if (role === undefined || wanted === undefined || 0 === wanted.length)
|
|
370
|
+
return [];
|
|
371
|
+
const target = normaliseVisibleText(wanted).toLowerCase();
|
|
372
|
+
if (0 === target.length)
|
|
373
|
+
return [];
|
|
374
|
+
const out = [];
|
|
375
|
+
for (const el of elementsUnder(container)) {
|
|
376
|
+
if (isIgnored(el) || getRole(el) !== role)
|
|
377
|
+
continue;
|
|
378
|
+
const name = normaliseVisibleText(getAccessibleName(el));
|
|
379
|
+
if (0 === name.length)
|
|
380
|
+
continue;
|
|
381
|
+
const folded = name.toLowerCase();
|
|
382
|
+
if (!folded.includes(target) && !target.includes(folded))
|
|
383
|
+
continue;
|
|
384
|
+
if (!out.includes(name))
|
|
385
|
+
out.push(name);
|
|
386
|
+
if (out.length >= MAX_NAME_NEAR_MISSES)
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
return out;
|
|
390
|
+
}
|
|
549
391
|
function buildEmptyHint(query) {
|
|
550
392
|
const container = resolveContainer(query.scope).container ?? document.body;
|
|
551
393
|
const all = container.querySelectorAll(`[${TESTID_ATTR}]`);
|
|
552
394
|
const present = [];
|
|
553
395
|
for (const el of Array.from(all)) {
|
|
554
396
|
if (isIgnored(el))
|
|
555
|
-
continue;
|
|
397
|
+
continue;
|
|
556
398
|
const id = el.getAttribute(TESTID_ATTR);
|
|
557
399
|
if (id !== null && id.length > 0 && !present.includes(id)) {
|
|
558
400
|
present.push(id);
|
|
@@ -560,8 +402,6 @@ function buildEmptyHint(query) {
|
|
|
560
402
|
break;
|
|
561
403
|
}
|
|
562
404
|
}
|
|
563
|
-
// DECLARED, not observed: see declaredTestids. Every present testid is also an observed one, so
|
|
564
|
-
// the merged list would make this flag true for any page that has a testid at all.
|
|
565
405
|
const registered = declaredTestids();
|
|
566
406
|
const knownEmptyState = present.some((id) => registered.includes(id));
|
|
567
407
|
const route = `${location.pathname}${location.search}`;
|
|
@@ -571,23 +411,17 @@ function buildEmptyHint(query) {
|
|
|
571
411
|
presentRegions: buildPresentRegions(query),
|
|
572
412
|
knownEmptyState,
|
|
573
413
|
};
|
|
574
|
-
// Only for a text search, and only when the string IS here — otherwise this is an ordinary miss
|
|
575
|
-
// and the extra clause would lengthen every failure to say nothing.
|
|
576
414
|
const wanted = wantedTextOf(query);
|
|
577
415
|
if (wanted !== undefined) {
|
|
578
416
|
const owner = splitTextOwner(container, wanted);
|
|
579
417
|
if (owner !== undefined)
|
|
580
418
|
hint.splitText = describe(owner);
|
|
581
419
|
}
|
|
420
|
+
const near = nameNearMisses(container, query);
|
|
421
|
+
if (near.length > 0)
|
|
422
|
+
hint.nameNearMiss = near;
|
|
582
423
|
return hint;
|
|
583
424
|
}
|
|
584
|
-
/**
|
|
585
|
-
* Resolve a query to descriptors for the `query` MCP tool.
|
|
586
|
-
*
|
|
587
|
-
* `count` is carried through deliberately: the server reports match totals from it rather than from
|
|
588
|
-
* the array, because the array is capped in transit. Dropping it here - which this function used to
|
|
589
|
-
* do - silently put the server back to counting survivors and calling that the answer.
|
|
590
|
-
*/
|
|
591
425
|
export function runQuery(query, limit) {
|
|
592
426
|
const result = matchQuery(query, undefined, limit);
|
|
593
427
|
const scopeFields = true === result.scopeMissing ? { scopeMissing: true } : {};
|
package/dist/dom/realm.d.ts
CHANGED
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cross-realm type tests.
|
|
3
|
-
*
|
|
4
|
-
* `instanceof` compares against ONE realm's constructor. An element inside a same-origin `<iframe>`
|
|
5
|
-
* lives in that document's realm, so `frameButton instanceof HTMLElement` — evaluated with the top
|
|
6
|
-
* window's `HTMLElement` — is FALSE for a perfectly ordinary button.
|
|
7
|
-
*
|
|
8
|
-
* That is not theoretical. Once `query` learned to descend into same-origin frames, it returned the
|
|
9
|
-
* frame's button correctly and `act` then rejected the ref with "is not an HTMLElement": found, and
|
|
10
|
-
* unusable. A find you cannot act on is worse than a miss, because it reads as a product bug in the
|
|
11
|
-
* app rather than in the tool.
|
|
12
|
-
*
|
|
13
|
-
* These resolve the constructor from the NODE's own document instead, so an element is measured
|
|
14
|
-
* against its own realm. Same-realm nodes behave exactly as `instanceof` did.
|
|
15
|
-
*/
|
|
16
1
|
export declare const isElement: (n: unknown) => n is Element;
|
|
17
2
|
export declare const isImage: (n: unknown) => n is HTMLImageElement;
|
|
18
3
|
export declare const isHtmlElement: (n: unknown) => n is HTMLElement;
|
|
@@ -22,9 +7,4 @@ export declare const isSelect: (n: unknown) => n is HTMLSelectElement;
|
|
|
22
7
|
export declare const isButton: (n: unknown) => n is HTMLButtonElement;
|
|
23
8
|
export declare const isForm: (n: unknown) => n is HTMLFormElement;
|
|
24
9
|
export declare const isFrame: (n: unknown) => n is HTMLIFrameElement;
|
|
25
|
-
/**
|
|
26
|
-
* The prototype whose native value setter drives `el` — React and friends install their own accessor
|
|
27
|
-
* on the instance, so a value write has to go through the prototype's. It must come from the
|
|
28
|
-
* ELEMENT's realm for the same reason as above.
|
|
29
|
-
*/
|
|
30
10
|
export declare function valuePrototypeOf(el: Element): object | undefined;
|
package/dist/dom/realm.js
CHANGED
|
@@ -1,35 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cross-realm type tests.
|
|
3
|
-
*
|
|
4
|
-
* `instanceof` compares against ONE realm's constructor. An element inside a same-origin `<iframe>`
|
|
5
|
-
* lives in that document's realm, so `frameButton instanceof HTMLElement` — evaluated with the top
|
|
6
|
-
* window's `HTMLElement` — is FALSE for a perfectly ordinary button.
|
|
7
|
-
*
|
|
8
|
-
* That is not theoretical. Once `query` learned to descend into same-origin frames, it returned the
|
|
9
|
-
* frame's button correctly and `act` then rejected the ref with "is not an HTMLElement": found, and
|
|
10
|
-
* unusable. A find you cannot act on is worse than a miss, because it reads as a product bug in the
|
|
11
|
-
* app rather than in the tool.
|
|
12
|
-
*
|
|
13
|
-
* These resolve the constructor from the NODE's own document instead, so an element is measured
|
|
14
|
-
* against its own realm. Same-realm nodes behave exactly as `instanceof` did.
|
|
15
|
-
*/
|
|
16
|
-
/** The constructor named `name` in the realm that owns `node`, or undefined if there isn't one. */
|
|
17
1
|
function ctorIn(node, name) {
|
|
18
2
|
if (null === node || typeof node !== 'object')
|
|
19
3
|
return undefined;
|
|
20
|
-
// A Document owns itself; every other node reports an ownerDocument.
|
|
21
4
|
const doc = node.ownerDocument ?? node;
|
|
22
5
|
const view = doc.defaultView;
|
|
23
6
|
if (null === view || view === undefined)
|
|
24
7
|
return undefined;
|
|
25
8
|
return view[name];
|
|
26
9
|
}
|
|
27
|
-
/**
|
|
28
|
-
* `node instanceof <name>`, evaluated in the node's own realm.
|
|
29
|
-
*
|
|
30
|
-
* Falls back to the ambient constructor when the node has no view — a detached document, or a jsdom
|
|
31
|
-
* fragment — so behaviour outside a browser is unchanged.
|
|
32
|
-
*/
|
|
33
10
|
function isIn(node, name, ambient) {
|
|
34
11
|
const ctor = ctorIn(node, name) ?? ambient;
|
|
35
12
|
return 'function' === typeof ctor && node instanceof ctor;
|
|
@@ -43,11 +20,6 @@ export const isSelect = (n) => isIn(n, 'HTMLSelectElement', 'undefined' === type
|
|
|
43
20
|
export const isButton = (n) => isIn(n, 'HTMLButtonElement', 'undefined' === typeof HTMLButtonElement ? undefined : HTMLButtonElement);
|
|
44
21
|
export const isForm = (n) => isIn(n, 'HTMLFormElement', 'undefined' === typeof HTMLFormElement ? undefined : HTMLFormElement);
|
|
45
22
|
export const isFrame = (n) => isIn(n, 'HTMLIFrameElement', 'undefined' === typeof HTMLIFrameElement ? undefined : HTMLIFrameElement);
|
|
46
|
-
/**
|
|
47
|
-
* The prototype whose native value setter drives `el` — React and friends install their own accessor
|
|
48
|
-
* on the instance, so a value write has to go through the prototype's. It must come from the
|
|
49
|
-
* ELEMENT's realm for the same reason as above.
|
|
50
|
-
*/
|
|
51
23
|
export function valuePrototypeOf(el) {
|
|
52
24
|
const name = isTextArea(el) ? 'HTMLTextAreaElement' : 'HTMLInputElement';
|
|
53
25
|
const ctor = ctorIn(el, name);
|
|
@@ -1,40 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Every shadow root this SDK has seen created, open or closed.
|
|
3
|
-
*
|
|
4
|
-
* Two problems are solved by the same record.
|
|
5
|
-
*
|
|
6
|
-
* **Observation.** A `MutationObserver` on `documentElement` does NOT cross a shadow boundary, so a
|
|
7
|
-
* click that changes text inside a web component produced an entirely empty observe window. Measured
|
|
8
|
-
* on a console with a shadow-DOM alert badge: the click landed, the badge changed, and `observe`
|
|
9
|
-
* reported zero DOM events — indistinguishable from an app that ignored the click. The DOM observer
|
|
10
|
-
* needs a list of roots to attach to, and no API enumerates them.
|
|
11
|
-
*
|
|
12
|
-
* **Closed roots.** `element.shadowRoot` is null forever for `mode: 'closed'`, so a closed root is
|
|
13
|
-
* unreadable AFTER the fact — but `attachShadow` still RETURNS it to its caller. Patching that call
|
|
14
|
-
* captures the root at the one instant it is exposed. This only works when the SDK is installed
|
|
15
|
-
* before the component upgrades, which is why hosts are tracked individually rather than assumed:
|
|
16
|
-
* whatever was missed is still declared as a blind spot, and only what was genuinely captured stops
|
|
17
|
-
* being one.
|
|
18
|
-
*/
|
|
19
|
-
/**
|
|
20
|
-
* Bookkeeping ceiling. Weak references make the ROOTS collectable; this bounds the entries pointing
|
|
21
|
-
* at them, which are not. Sized far above any real page's live component count.
|
|
22
|
-
*/
|
|
23
1
|
export declare const MAX_TRACKED_ROOTS = 4000;
|
|
24
|
-
/** Dead-entry sweeps are O(retained), so they run once per this many roots rather than per root. */
|
|
25
2
|
export declare const SWEEP_EVERY = 250;
|
|
26
|
-
/** Shadow roots captured so far. Includes closed roots when the patch was installed in time. */
|
|
27
3
|
export declare function capturedRoots(): readonly ShadowRoot[];
|
|
28
|
-
/** Whether this element's shadow root was captured — i.e. whether its contents are reachable. */
|
|
29
4
|
export declare function isCaptured(host: Element): boolean;
|
|
30
|
-
/** The captured root for a host, or null. Non-null for closed roots the patch caught in time. */
|
|
31
5
|
export declare function capturedRootOf(host: Element): ShadowRoot | null;
|
|
32
|
-
/** Run `listener` for every root captured from now on. Returns an unsubscribe. */
|
|
33
6
|
export declare function onShadowRoot(listener: (root: ShadowRoot) => void): () => void;
|
|
34
|
-
/**
|
|
35
|
-
* Patch `attachShadow` to record each root as it is created, and sweep the roots that already exist.
|
|
36
|
-
*
|
|
37
|
-
* The sweep can only find OPEN roots (there is no other way in), so a closed root attached before
|
|
38
|
-
* install stays unreachable — correctly, and it is declared as such.
|
|
39
|
-
*/
|
|
40
7
|
export declare function installShadowRegistry(): () => void;
|