@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/theme.js
CHANGED
|
@@ -1,36 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Design-token (theme) awareness — the in-source answer to "is this color on-theme?". A DOM/a11y
|
|
3
|
-
* tool sees a color rendered; it does not know the app's INTENDED palette. Reticle reads the design
|
|
4
|
-
* tokens the app declares as CSS custom properties, resolves them AS THE PAGE CURRENTLY RENDERS
|
|
5
|
-
* THEM, and reports, per element, which tokens its color/background map to or whether the value is
|
|
6
|
-
* an off-palette hardcoded one. That is a theme-compliance signal no outside-the-page tool has
|
|
7
|
-
* without re-deriving the palette itself.
|
|
8
|
-
*
|
|
9
|
-
* Conservative by design: only a SET, opaque color with no matching token is flagged off-theme, and
|
|
10
|
-
* every matching token name is returned — so the agent can judge, not just trust a flag.
|
|
11
|
-
*
|
|
12
|
-
* Two defects made this field untrustworthy in BOTH directions before it was rewritten:
|
|
13
|
-
* - the reverse map was color → ONE name, first writer wins, so a button styled with a brand token
|
|
14
|
-
* was told its background was a focus-border token because the two resolve to the same hex. The
|
|
15
|
-
* name is the part an agent reasons about, and it was arbitrary among the candidates.
|
|
16
|
-
* - the palette was built from the RAW `:root` declaration text and cached for the page lifetime,
|
|
17
|
-
* on the reasoning that a theme rarely changes at runtime. A human toggling the theme between
|
|
18
|
-
* two inspect calls left every lookup keyed on the previous theme's colors, so a `<summary>` that
|
|
19
|
-
* had just been changed to a text token reported a surface token. The reporter read that as their
|
|
20
|
-
* edit not applying and went hunting for a theme-scope bug that did not exist.
|
|
21
|
-
* Hence: no cache, values resolved through `getComputedStyle` so the active theme wins, and the
|
|
22
|
-
* scope that was active at capture time reported alongside, so two results minutes apart are
|
|
23
|
-
* comparable at all.
|
|
24
|
-
*/
|
|
25
|
-
/** Attribute an app flips to switch themes, reported as part of the scope so results are comparable. */
|
|
26
1
|
const THEME_ATTRIBUTE = 'data-theme';
|
|
27
|
-
/** Collect every custom-property NAME the app declares, in any rule. */
|
|
28
2
|
function tokenNames() {
|
|
29
3
|
const names = new Set();
|
|
30
4
|
for (const sheet of Array.from(document.styleSheets)) {
|
|
31
5
|
let rules = null;
|
|
32
6
|
try {
|
|
33
|
-
rules = sheet.cssRules;
|
|
7
|
+
rules = sheet.cssRules;
|
|
34
8
|
}
|
|
35
9
|
catch {
|
|
36
10
|
continue;
|
|
@@ -40,10 +14,6 @@ function tokenNames() {
|
|
|
40
14
|
for (const rule of Array.from(rules)) {
|
|
41
15
|
if (!(rule instanceof CSSStyleRule))
|
|
42
16
|
continue;
|
|
43
|
-
// Deliberately NOT filtered to `:root`/`html`: a theme block is usually `.dark` or
|
|
44
|
-
// `[data-theme="dark"]`, and an app that declares its palette only there had no palette at all
|
|
45
|
-
// under the old filter. Names are cheap; a name that is not in scope resolves to nothing below
|
|
46
|
-
// and is dropped, so widening collection cannot invent a token.
|
|
47
17
|
for (const prop of Array.from(rule.style)) {
|
|
48
18
|
if (prop.startsWith('--'))
|
|
49
19
|
names.add(prop);
|
|
@@ -52,21 +22,12 @@ function tokenNames() {
|
|
|
52
22
|
}
|
|
53
23
|
return names;
|
|
54
24
|
}
|
|
55
|
-
/**
|
|
56
|
-
* The palette as the page renders it right now: canonical `rgb(...)` → every token with that color.
|
|
57
|
-
*
|
|
58
|
-
* Resolved against `document.body` rather than the raw declaration text, because custom properties
|
|
59
|
-
* inherit: this picks up a theme scoped to `<html>` or to `<body>`, and picks up the value the
|
|
60
|
-
* ACTIVE theme block won with rather than whichever block the stylesheet happened to list first.
|
|
61
|
-
*/
|
|
62
25
|
function palette() {
|
|
63
26
|
const byColor = new Map();
|
|
64
27
|
const root = document.body ?? document.documentElement;
|
|
65
28
|
if (null === root)
|
|
66
29
|
return byColor;
|
|
67
30
|
const rootStyle = getComputedStyle(root);
|
|
68
|
-
// One shared probe for the whole build. There is no cache any more, so this runs per inspect, and
|
|
69
|
-
// a span per token would be a few hundred appends on a real design system.
|
|
70
31
|
const probe = document.createElement('span');
|
|
71
32
|
probe.style.position = 'absolute';
|
|
72
33
|
probe.style.pointerEvents = 'none';
|
|
@@ -86,30 +47,22 @@ function palette() {
|
|
|
86
47
|
finally {
|
|
87
48
|
probe.remove();
|
|
88
49
|
}
|
|
89
|
-
// Sorted so the reported order is a property of the app's tokens, not of stylesheet iteration
|
|
90
|
-
// order — the same collision must read the same way on two consecutive inspects.
|
|
91
50
|
for (const named of byColor.values())
|
|
92
51
|
named.sort();
|
|
93
52
|
return byColor;
|
|
94
53
|
}
|
|
95
|
-
/** Resolve any CSS color string to canonical computed `rgb...)`/`rgba...)`, or null if not a color. */
|
|
96
54
|
function toRgb(probe, value) {
|
|
97
55
|
if (0 === value.length)
|
|
98
56
|
return null;
|
|
99
57
|
probe.style.color = '';
|
|
100
|
-
probe.style.color = value;
|
|
58
|
+
probe.style.color = value;
|
|
101
59
|
if ('' === probe.style.color)
|
|
102
60
|
return null;
|
|
103
61
|
return getComputedStyle(probe).color;
|
|
104
62
|
}
|
|
105
|
-
/** True for a color that carries no visual weight (fully transparent) — never flagged off-theme. */
|
|
106
63
|
function isTransparent(rgb) {
|
|
107
64
|
return 'rgba(0, 0, 0, 0)' === rgb || 'transparent' === rgb;
|
|
108
65
|
}
|
|
109
|
-
/**
|
|
110
|
-
* The theme scope in force at capture time, as a selector-shaped string (`.dark[data-theme="dark"]`),
|
|
111
|
-
* or null when the root carries no theme marker at all.
|
|
112
|
-
*/
|
|
113
66
|
function themeScope() {
|
|
114
67
|
const el = document.documentElement;
|
|
115
68
|
if (null === el)
|
|
@@ -121,11 +74,9 @@ function themeScope() {
|
|
|
121
74
|
const scope = `${classes}${null === attr ? '' : `[${THEME_ATTRIBUTE}="${attr}"]`}`;
|
|
122
75
|
return 0 === scope.length ? null : scope;
|
|
123
76
|
}
|
|
124
|
-
/** The unambiguous match, or null when the palette cannot tell the candidates apart. */
|
|
125
77
|
function sole(names) {
|
|
126
78
|
return 1 === names.length ? (names[0] ?? null) : null;
|
|
127
79
|
}
|
|
128
|
-
/** Report an element's theme compliance from its computed style. */
|
|
129
80
|
export function themeReport(cs) {
|
|
130
81
|
const byColor = palette();
|
|
131
82
|
const colorTokens = byColor.get(cs.color) ?? [];
|
|
@@ -140,7 +91,6 @@ export function themeReport(cs) {
|
|
|
140
91
|
colorTokens,
|
|
141
92
|
backgroundToken: sole(backgroundTokens),
|
|
142
93
|
backgroundTokens,
|
|
143
|
-
// Only meaningful when a palette exists; an app with no tokens can't violate one.
|
|
144
94
|
offTheme: tokenCount > 0 && (colorOff || bgOff),
|
|
145
95
|
tokenCount,
|
|
146
96
|
themeScope: themeScope(),
|
|
@@ -1,19 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rows a scroll container has reserved space for but has NOT rendered.
|
|
3
|
-
*
|
|
4
|
-
* `BlindSpotKind.VIRTUALIZED_UNMOUNTED` has had a label on the server since it was defined and
|
|
5
|
-
* nothing ever emitted it — a declared blind spot that could not fire, shaped exactly like the most
|
|
6
|
-
* common data-heavy UI there is. Measured on a 10,000-row console: 29 rows in the DOM, and a verdict
|
|
7
|
-
* that reported full coverage. An assertion like "no shipment is held" was a claim about 29 rows,
|
|
8
|
-
* delivered as a claim about all of them.
|
|
9
|
-
*
|
|
10
|
-
* The detector measures EMPTY RESERVED SPACE rather than guessing from child counts. A virtualizer
|
|
11
|
-
* sizes its scroll area for the whole list and renders only a window, so the area above the first
|
|
12
|
-
* child and below the last is space the container has promised and not filled. An ordinary list —
|
|
13
|
-
* even one with large gaps or margins between rows — spans its own scroll height, so it measures
|
|
14
|
-
* zero and never reports.
|
|
15
|
-
*/
|
|
16
|
-
/** How many rows a container is holding space for but has not rendered. 0 when it is not virtualized. */
|
|
17
1
|
export declare function unmountedRowsIn(container: HTMLElement): number;
|
|
18
|
-
/** Total unmounted rows across every scroll container on the page. */
|
|
19
2
|
export declare function countUnmountedRows(): number;
|
package/dist/dom/virtualized.js
CHANGED
|
@@ -1,27 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rows a scroll container has reserved space for but has NOT rendered.
|
|
3
|
-
*
|
|
4
|
-
* `BlindSpotKind.VIRTUALIZED_UNMOUNTED` has had a label on the server since it was defined and
|
|
5
|
-
* nothing ever emitted it — a declared blind spot that could not fire, shaped exactly like the most
|
|
6
|
-
* common data-heavy UI there is. Measured on a 10,000-row console: 29 rows in the DOM, and a verdict
|
|
7
|
-
* that reported full coverage. An assertion like "no shipment is held" was a claim about 29 rows,
|
|
8
|
-
* delivered as a claim about all of them.
|
|
9
|
-
*
|
|
10
|
-
* The detector measures EMPTY RESERVED SPACE rather than guessing from child counts. A virtualizer
|
|
11
|
-
* sizes its scroll area for the whole list and renders only a window, so the area above the first
|
|
12
|
-
* child and below the last is space the container has promised and not filled. An ordinary list —
|
|
13
|
-
* even one with large gaps or margins between rows — spans its own scroll height, so it measures
|
|
14
|
-
* zero and never reports.
|
|
15
|
-
*/
|
|
16
|
-
/** Below this, the empty space is padding or a sticky header rather than a virtualized remainder. */
|
|
17
1
|
const UNMOUNTED_RATIO = 0.25;
|
|
18
|
-
/** A container has to be meaningfully scrollable before "what is not rendered" is even a question. */
|
|
19
2
|
const MIN_SCROLL_RATIO = 1.2;
|
|
20
|
-
/**
|
|
21
|
-
* Virtualizers usually put one full-height SPACER inside the scroller and position rows absolutely
|
|
22
|
-
* inside it, so the scroller's own children are just that spacer. Descend through it to reach the
|
|
23
|
-
* rows — without this the spacer measures as filling the container and nothing is ever detected.
|
|
24
|
-
*/
|
|
25
3
|
function rowHost(container) {
|
|
26
4
|
let host = container;
|
|
27
5
|
for (let depth = 0; depth < 3; depth += 1) {
|
|
@@ -29,14 +7,12 @@ function rowHost(container) {
|
|
|
29
7
|
const only = 1 === children.length ? children[0] : undefined;
|
|
30
8
|
if (only === undefined)
|
|
31
9
|
return host;
|
|
32
|
-
// A spacer is a single child that fills (or over-fills) the scroll area.
|
|
33
10
|
if (only.offsetHeight < host.scrollHeight * 0.9)
|
|
34
11
|
return host;
|
|
35
12
|
host = only;
|
|
36
13
|
}
|
|
37
14
|
return host;
|
|
38
15
|
}
|
|
39
|
-
/** How many rows a container is holding space for but has not rendered. 0 when it is not virtualized. */
|
|
40
16
|
export function unmountedRowsIn(container) {
|
|
41
17
|
const scrollHeight = container.scrollHeight;
|
|
42
18
|
const clientHeight = container.clientHeight;
|
|
@@ -58,7 +34,6 @@ export function unmountedRowsIn(container) {
|
|
|
58
34
|
bottom = rowBottom;
|
|
59
35
|
totalHeight += row.offsetHeight;
|
|
60
36
|
}
|
|
61
|
-
// Space the container reserved and left empty: above the first row plus below the last.
|
|
62
37
|
const empty = Math.max(0, top) + Math.max(0, scrollHeight - bottom);
|
|
63
38
|
if (empty < scrollHeight * UNMOUNTED_RATIO)
|
|
64
39
|
return 0;
|
|
@@ -67,11 +42,9 @@ export function unmountedRowsIn(container) {
|
|
|
67
42
|
return 0;
|
|
68
43
|
return Math.round(empty / averageRow);
|
|
69
44
|
}
|
|
70
|
-
/** Total unmounted rows across every scroll container on the page. */
|
|
71
45
|
export function countUnmountedRows() {
|
|
72
46
|
let total = 0;
|
|
73
47
|
for (const element of document.querySelectorAll('*')) {
|
|
74
|
-
// Cheap gate first: only elements that actually scroll are worth measuring.
|
|
75
48
|
if (element.scrollHeight <= element.clientHeight)
|
|
76
49
|
continue;
|
|
77
50
|
total += unmountedRowsIn(element);
|
package/dist/edit-epoch.d.ts
CHANGED
|
@@ -1,32 +1,10 @@
|
|
|
1
|
-
import { type RefRegistry } from './dom/refs.js';
|
|
1
|
+
import { type RefRegistry } from './dom/addressing/refs.js';
|
|
2
2
|
export declare class EditEpoch {
|
|
3
3
|
#private;
|
|
4
|
-
/** The registry is injected for the same reason the clock is: the interesting property is the
|
|
5
|
-
* RELATION between when a ref was minted and when an edit landed, and a global has no seam. */
|
|
6
4
|
constructor(registry: RefRegistry);
|
|
7
|
-
/**
|
|
8
|
-
* How many hot updates this document has applied.
|
|
9
|
-
*
|
|
10
|
-
* `NO_EDITS_OBSERVED` means exactly that and never "none happened": most pages Reticle instruments
|
|
11
|
-
* have no channel that could report one, so absence here is UNKNOWN.
|
|
12
|
-
*/
|
|
13
5
|
get current(): number;
|
|
14
|
-
/** Subscribe to a hot-update channel if one was handed in. Anything else is silently ignored —
|
|
15
|
-
* no channel is the normal case, not an error. */
|
|
16
6
|
observe(hot: unknown): void;
|
|
17
|
-
/** Record that an update was applied, naming the files it changed (which may be none). */
|
|
18
7
|
applied(files: readonly string[]): void;
|
|
19
|
-
/**
|
|
20
|
-
* The refusal for a ref that no longer resolves — saying WHY when the page can tell us.
|
|
21
|
-
*
|
|
22
|
-
* The generic wording is untouched and stays at the front: it is already tuned, and the server's
|
|
23
|
-
* recovery table matches on it. When the ref predates the last hot update the diagnosis is
|
|
24
|
-
* appended, because "you edited TripCard.tsx and it re-rendered" is a next step and "that ref is
|
|
25
|
-
* stale" is a dead end. A ref minted AFTER the last update gets the generic message unchanged —
|
|
26
|
-
* blaming an edit for an ordinary post-click stale ref would be a confident wrong answer, which is
|
|
27
|
-
* worse than the vague right one.
|
|
28
|
-
*/
|
|
29
8
|
staleRefMessage(ref: string): string;
|
|
30
9
|
}
|
|
31
|
-
/** The epoch of the page, paired with the process-wide ref registry it has to reason against. */
|
|
32
10
|
export declare const editEpoch: EditEpoch;
|
package/dist/edit-epoch.js
CHANGED
|
@@ -1,38 +1,9 @@
|
|
|
1
1
|
import { NO_EDITS_OBSERVED } from '@reticlehq/core';
|
|
2
|
-
import { echoRef, refs } from './dom/refs.js';
|
|
3
|
-
/**
|
|
4
|
-
* What the page can tell us about its own source being edited underneath it.
|
|
5
|
-
*
|
|
6
|
-
* The agent driving Reticle edits source and re-verifies in a loop. When an edit lands, the dev
|
|
7
|
-
* server hot-updates the module and the framework re-renders: DOM nodes are REPLACED, every ref the
|
|
8
|
-
* agent holds points at a detached node — and there was no navigation, so the document is the same
|
|
9
|
-
* one and `documentId` never moves. The agent got a generic "that ref is stale" and could not tell
|
|
10
|
-
* its own edit landing from a bug in the app it was verifying.
|
|
11
|
-
*
|
|
12
|
-
* DELIBERATELY NOT A BUILD INTEGRATION. Nothing here watches files, reads the user's repo or knows
|
|
13
|
-
* what a module graph is. The page is told by its own dev server that it hot-updated; this records
|
|
14
|
-
* that it happened and what it named, and that is the whole feature.
|
|
15
|
-
*/
|
|
16
|
-
/**
|
|
17
|
-
* Vite's "the update has been applied" event, on a hot context.
|
|
18
|
-
*
|
|
19
|
-
* `vite:afterUpdate` rather than `vite:beforeUpdate`: the point of the epoch is that the DOM has
|
|
20
|
-
* been replaced, and before the update it has not been yet.
|
|
21
|
-
*
|
|
22
|
-
* The SDK does NOT depend on Vite — it ships to Next, Electron, Tauri and plain pages, and it cannot
|
|
23
|
-
* read `import.meta.hot` of its own module in any of them (a dependency is pre-bundled by Vite's
|
|
24
|
-
* optimizer and gets no hot context at all). The channel is handed IN by whoever has one, and this
|
|
25
|
-
* name is the only Vite-shaped thing in the SDK.
|
|
26
|
-
*/
|
|
2
|
+
import { echoRef, refs } from './dom/addressing/refs.js';
|
|
27
3
|
const HOT_UPDATE_APPLIED = 'vite:afterUpdate';
|
|
28
|
-
/** How many changed files a refusal names before it stops being a diagnosis and starts being a list. */
|
|
29
4
|
const MAX_NAMED_FILES = 3;
|
|
30
|
-
/** The exact substring the server's recovery table keys off. Changing it silently unclassifies. */
|
|
31
5
|
const STALE_REF_REFUSAL = 'no longer resolves to an element';
|
|
32
|
-
/** The clause that turns a dead end into a diagnosis, and the one the server's table matches on. */
|
|
33
6
|
const CODE_CHANGED = 'the code changed underneath it';
|
|
34
|
-
/** Anything object-shaped, as a bag of unknowns. The payload comes from the page, so nothing about
|
|
35
|
-
* its shape is guaranteed and every field has to be checked before it is read. */
|
|
36
7
|
function fields(value) {
|
|
37
8
|
return null !== value && 'object' === typeof value
|
|
38
9
|
? value
|
|
@@ -41,7 +12,6 @@ function fields(value) {
|
|
|
41
12
|
function isHotLike(value) {
|
|
42
13
|
return 'function' === typeof fields(value)?.['on'];
|
|
43
14
|
}
|
|
44
|
-
/** The file paths a hot-update payload named, narrowed from `unknown`. */
|
|
45
15
|
function updatedPaths(payload) {
|
|
46
16
|
const updates = fields(payload)?.['updates'];
|
|
47
17
|
if (!Array.isArray(updates))
|
|
@@ -58,43 +28,22 @@ export class EditEpoch {
|
|
|
58
28
|
#epoch = NO_EDITS_OBSERVED;
|
|
59
29
|
#files = [];
|
|
60
30
|
#refs;
|
|
61
|
-
/** The registry is injected for the same reason the clock is: the interesting property is the
|
|
62
|
-
* RELATION between when a ref was minted and when an edit landed, and a global has no seam. */
|
|
63
31
|
constructor(registry) {
|
|
64
32
|
this.#refs = registry;
|
|
65
33
|
}
|
|
66
|
-
/**
|
|
67
|
-
* How many hot updates this document has applied.
|
|
68
|
-
*
|
|
69
|
-
* `NO_EDITS_OBSERVED` means exactly that and never "none happened": most pages Reticle instruments
|
|
70
|
-
* have no channel that could report one, so absence here is UNKNOWN.
|
|
71
|
-
*/
|
|
72
34
|
get current() {
|
|
73
35
|
return this.#epoch;
|
|
74
36
|
}
|
|
75
|
-
/** Subscribe to a hot-update channel if one was handed in. Anything else is silently ignored —
|
|
76
|
-
* no channel is the normal case, not an error. */
|
|
77
37
|
observe(hot) {
|
|
78
38
|
if (!isHotLike(hot))
|
|
79
39
|
return;
|
|
80
40
|
hot.on(HOT_UPDATE_APPLIED, (payload) => this.applied(updatedPaths(payload)));
|
|
81
41
|
}
|
|
82
|
-
/** Record that an update was applied, naming the files it changed (which may be none). */
|
|
83
42
|
applied(files) {
|
|
84
43
|
this.#epoch += 1;
|
|
85
44
|
this.#files = files.slice(0, MAX_NAMED_FILES);
|
|
86
45
|
this.#refs.markEdited();
|
|
87
46
|
}
|
|
88
|
-
/**
|
|
89
|
-
* The refusal for a ref that no longer resolves — saying WHY when the page can tell us.
|
|
90
|
-
*
|
|
91
|
-
* The generic wording is untouched and stays at the front: it is already tuned, and the server's
|
|
92
|
-
* recovery table matches on it. When the ref predates the last hot update the diagnosis is
|
|
93
|
-
* appended, because "you edited TripCard.tsx and it re-rendered" is a next step and "that ref is
|
|
94
|
-
* stale" is a dead end. A ref minted AFTER the last update gets the generic message unchanged —
|
|
95
|
-
* blaming an edit for an ordinary post-click stale ref would be a confident wrong answer, which is
|
|
96
|
-
* worse than the vague right one.
|
|
97
|
-
*/
|
|
98
47
|
staleRefMessage(ref) {
|
|
99
48
|
const generic = `ref '${echoRef(ref)}' ${STALE_REF_REFUSAL}`;
|
|
100
49
|
if (!this.#refs.mintedBeforeLastEdit(ref))
|
|
@@ -104,5 +53,4 @@ export class EditEpoch {
|
|
|
104
53
|
'replaced the node. Query again for a fresh ref: this is your own edit landing, not the app failing.');
|
|
105
54
|
}
|
|
106
55
|
}
|
|
107
|
-
/** The epoch of the page, paired with the process-wide ref registry it has to reason against. */
|
|
108
56
|
export const editEpoch = new EditEpoch(refs);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const reticle: Reticle;
|
|
1
|
+
export { reticle } from './reticle-singleton.js';
|
|
3
2
|
export { Reticle } from './reticle.js';
|
|
4
3
|
export type { ReticleConnectOptions } from './reticle.js';
|
|
5
4
|
export { SESSION_AUTO } from '@reticlehq/core';
|
|
6
5
|
export { setIgnoreSelectors } from './dom/dom-ignore.js';
|
|
7
|
-
export { registerAdapter, identifyComponent, readComponentState, elementHasHoverHandlers, adapterNames, } from './registry/adapters.js';
|
|
8
|
-
export type { ReticleAdapter, ComponentInfo, ComponentSource } from './registry/adapters.js';
|
|
6
|
+
export { registerAdapter, identifyComponent, readComponentState, elementHasHoverHandlers, adapterNames, } from './registry/stores/adapters.js';
|
|
7
|
+
export type { ReticleAdapter, ComponentInfo, ComponentSource } from './registry/stores/adapters.js';
|
|
9
8
|
export { registerStore, unregisterStore, storeNames, readStores, readStoresWithTruncation, sourceOwner, markAdapterSource, } from './registry/stores.js';
|
|
10
9
|
export type { StoreGetter, StoreLike, StoreSubscribe } from './registry/stores.js';
|
|
11
10
|
export { tanstackQueryStore, jotaiStore, xstateStore, valtioStore, mobxStore, recoilStore, svelteStore, piniaStore, pushStore, } from './registry/store-adapters.js';
|
|
@@ -23,6 +22,6 @@ export type { ChartReport, ChartFinding, CanvasChartData } from './dom/chart.js'
|
|
|
23
22
|
export { matchQuery, runQuery } from './dom/query.js';
|
|
24
23
|
export { executeAction, executeSequence } from './actions/actions.js';
|
|
25
24
|
export { describe, getRole, getAccessibleName, getStates, isVisible } from './dom/a11y.js';
|
|
26
|
-
export { refs, RefRegistry } from './dom/refs.js';
|
|
25
|
+
export { refs, RefRegistry } from './dom/addressing/refs.js';
|
|
27
26
|
export { Annotator, installAnnotator, type AnnotatorDeps } from './review/annotator.js';
|
|
28
27
|
export { resolveMarkAnchor, type MarkAnchor } from './review/mark-anchor.js';
|
package/dist/index.js
CHANGED
|
@@ -1,43 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* The singleton embedded in the host app: `import { reticle } from '@reticlehq/browser'`.
|
|
4
|
-
* Persisted on a global so HMR module re-evaluation reuses the same (already-connected)
|
|
5
|
-
* instance instead of creating a second bridge connection (feedback #7).
|
|
6
|
-
*/
|
|
7
|
-
const globalStore = globalThis;
|
|
8
|
-
export const reticle = (globalStore.__reticleInstance ??= new Reticle());
|
|
1
|
+
export { reticle } from './reticle-singleton.js';
|
|
9
2
|
export { Reticle } from './reticle.js';
|
|
10
|
-
// Session label sentinel: pass as `session` (or omit it) to get a unique per-tab id (no collisions).
|
|
11
3
|
export { SESSION_AUTO } from '@reticlehq/core';
|
|
12
|
-
// Exclude your own dev widgets from snapshots/observers.
|
|
13
4
|
export { setIgnoreSelectors } from './dom/dom-ignore.js';
|
|
14
|
-
|
|
15
|
-
export {
|
|
16
|
-
// Store registry: pull live framework/store state on demand.
|
|
17
|
-
export { registerStore, unregisterStore, storeNames, readStores, readStoresWithTruncation,
|
|
18
|
-
// Which name a store SOURCE is registered under. The React adapter discovers stores from the fiber
|
|
19
|
-
// tree and needs this to tell "the app already wired this exact store" from "this is another one" —
|
|
20
|
-
// a question neither the name nor the read value can answer.
|
|
21
|
-
sourceOwner, markAdapterSource, } from './registry/stores.js';
|
|
22
|
-
// Adapters for state libraries that do not natively expose `{getState, subscribe}`. zustand and Redux
|
|
23
|
-
// need none of these; TanStack Query is the one that matters most, because a stale-cache bug fires no
|
|
24
|
-
// request and renders a plausible value, so the cache is the only witness.
|
|
5
|
+
export { registerAdapter, identifyComponent, readComponentState, elementHasHoverHandlers, adapterNames, } from './registry/stores/adapters.js';
|
|
6
|
+
export { registerStore, unregisterStore, storeNames, readStores, readStoresWithTruncation, sourceOwner, markAdapterSource, } from './registry/stores.js';
|
|
25
7
|
export { tanstackQueryStore, jotaiStore, xstateStore, valtioStore, mobxStore, recoilStore, svelteStore, piniaStore, pushStore, } from './registry/store-adapters.js';
|
|
26
|
-
// Capability registry: the app self-describes its testable surface via reticle.describe.
|
|
27
8
|
export { registerCapabilities, getCapabilities, hasCapabilities, setCapabilitiesListener, } from './registry/capabilities.js';
|
|
28
|
-
// SDK helpers: adopt the recommended integration patterns without boilerplate.
|
|
29
9
|
export { createReticleEmitter } from './registry/emitter.js';
|
|
30
10
|
export { commitAndSignal } from './registry/commit-and-signal.js';
|
|
31
11
|
export { registerReticleDomain } from './registry/domains.js';
|
|
32
|
-
// Lower-level building blocks (useful for tests and advanced embedding).
|
|
33
12
|
export { buildSnapshot } from './dom/snapshot.js';
|
|
34
|
-
// Chart inspection. Surfaced automatically on any element descriptor that contains faulty plot
|
|
35
|
-
// geometry (see ElementDescriptor.chart), and exported for direct use — canvasChartData is the only
|
|
36
|
-
// route to a canvas chart's data, since its pixels are not DOM.
|
|
37
13
|
export { inspectChart, canvasChartData } from './dom/chart.js';
|
|
38
14
|
export { matchQuery, runQuery } from './dom/query.js';
|
|
39
15
|
export { executeAction, executeSequence } from './actions/actions.js';
|
|
40
16
|
export { describe, getRole, getAccessibleName, getStates, isVisible } from './dom/a11y.js';
|
|
41
|
-
export { refs, RefRegistry } from './dom/refs.js';
|
|
17
|
+
export { refs, RefRegistry } from './dom/addressing/refs.js';
|
|
42
18
|
export { Annotator, installAnnotator } from './review/annotator.js';
|
|
43
19
|
export { resolveMarkAnchor } from './review/mark-anchor.js';
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { EventType } from '@reticlehq/core';
|
|
2
|
-
import { refs } from '../dom/refs.js';
|
|
2
|
+
import { refs } from '../dom/addressing/refs.js';
|
|
3
3
|
import { isReticleOverlay } from '../dom/dom-ignore.js';
|
|
4
|
-
/** Observe CSS animations + transitions and emit anim.start / anim.end. */
|
|
5
4
|
export function installAnimation(emit) {
|
|
6
5
|
const ac = new AbortController();
|
|
7
6
|
const { signal } = ac;
|
|
8
7
|
const onStart = (event) => {
|
|
9
8
|
const target = event.target;
|
|
10
|
-
// Skip Reticle's own HUD keyframes (reticle-pulse/reticle-shimmer/…) so observe/record never
|
|
11
|
-
// self-pollute the agent's view of the app (matches the DOM observer's overlay filter).
|
|
12
9
|
if (target instanceof Element && !isReticleOverlay(target)) {
|
|
13
10
|
emit(EventType.ANIM_START, { name: event.animationName }, refs.refFor(target));
|
|
14
11
|
}
|
|
@@ -1,31 +1,5 @@
|
|
|
1
1
|
import type { Emit, Teardown } from './types.js';
|
|
2
|
-
/**
|
|
3
|
-
* Blind-spot sensor. The SDK instruments the DOM, but a cross-origin iframe is a wall it cannot
|
|
4
|
-
* see through — its contents, network, and console are invisible. Left unstated, a green verdict would
|
|
5
|
-
* imply the whole page was verified. This detects cross-origin frames and emits a BLIND_SPOT event so the
|
|
6
|
-
* server can mark results `coverage: partial` instead of lying by omission. This sensor owns the three
|
|
7
|
-
* kinds cheaply and reliably detectable from the page: cross-origin frames, closed shadow roots, and
|
|
8
|
-
* virtualized-unmounted rows.
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* Elements rendering content nobody outside them can read.
|
|
12
|
-
*
|
|
13
|
-
* `attachShadow({mode:'closed'})` makes `el.shadowRoot` null FOREVER, for every caller — Reticle,
|
|
14
|
-
* Playwright's accessibility snapshot, a screenshot-reading model, all of them. There is no API that
|
|
15
|
-
* recovers it, so the only honest behaviour is to say the region exists and is unreadable.
|
|
16
|
-
*
|
|
17
|
-
* It is not directly detectable either, so this infers it: a custom element (a dash in the tag name is
|
|
18
|
-
* the spec's own rule) that has NO light-DOM children and no text, yet occupies layout space, is
|
|
19
|
-
* painting from somewhere. Nothing else produces that combination — an empty custom element that
|
|
20
|
-
* renders nothing measures zero and is skipped.
|
|
21
|
-
*/
|
|
22
2
|
export declare function countClosedShadowRoots(): number;
|
|
23
|
-
/**
|
|
24
|
-
* Whether a frame is cross-origin (unobservable). A same-origin frame exposes its `contentDocument`; a
|
|
25
|
-
* cross-origin one returns null (or THROWS a SecurityError on access). A src-less / about:blank frame is
|
|
26
|
-
* same-origin and simply not navigated — not a blind spot.
|
|
27
|
-
*/
|
|
28
3
|
export declare function isCrossOriginFrame(frame: HTMLIFrameElement): boolean;
|
|
29
|
-
/** How many of the given frames are cross-origin (unobservable). */
|
|
30
4
|
export declare function countCrossOriginFrames(frames: readonly HTMLIFrameElement[]): number;
|
|
31
5
|
export declare function installBlindSpots(emit: Emit): Teardown;
|
|
@@ -2,36 +2,14 @@ import { EventType, BlindSpotKind } from '@reticlehq/core';
|
|
|
2
2
|
import { countUnmountedRows } from '../dom/virtualized.js';
|
|
3
3
|
import { isCaptured } from '../dom/shadow-registry.js';
|
|
4
4
|
import { sameOriginFrameBodies } from './frames.js';
|
|
5
|
-
import { nativeClearTimeout, nativeSetTimeout } from '../timers/native-timers.js';
|
|
6
|
-
/**
|
|
7
|
-
* Blind-spot sensor. The SDK instruments the DOM, but a cross-origin iframe is a wall it cannot
|
|
8
|
-
* see through — its contents, network, and console are invisible. Left unstated, a green verdict would
|
|
9
|
-
* imply the whole page was verified. This detects cross-origin frames and emits a BLIND_SPOT event so the
|
|
10
|
-
* server can mark results `coverage: partial` instead of lying by omission. This sensor owns the three
|
|
11
|
-
* kinds cheaply and reliably detectable from the page: cross-origin frames, closed shadow roots, and
|
|
12
|
-
* virtualized-unmounted rows.
|
|
13
|
-
*/
|
|
14
|
-
/**
|
|
15
|
-
* Elements rendering content nobody outside them can read.
|
|
16
|
-
*
|
|
17
|
-
* `attachShadow({mode:'closed'})` makes `el.shadowRoot` null FOREVER, for every caller — Reticle,
|
|
18
|
-
* Playwright's accessibility snapshot, a screenshot-reading model, all of them. There is no API that
|
|
19
|
-
* recovers it, so the only honest behaviour is to say the region exists and is unreadable.
|
|
20
|
-
*
|
|
21
|
-
* It is not directly detectable either, so this infers it: a custom element (a dash in the tag name is
|
|
22
|
-
* the spec's own rule) that has NO light-DOM children and no text, yet occupies layout space, is
|
|
23
|
-
* painting from somewhere. Nothing else produces that combination — an empty custom element that
|
|
24
|
-
* renders nothing measures zero and is skipped.
|
|
25
|
-
*/
|
|
5
|
+
import { nativeClearTimeout, nativeSetTimeout } from '../timers/native/native-timers.js';
|
|
26
6
|
export function countClosedShadowRoots() {
|
|
27
7
|
let count = 0;
|
|
28
8
|
for (const el of document.querySelectorAll('*')) {
|
|
29
9
|
if (!el.tagName.includes('-'))
|
|
30
10
|
continue;
|
|
31
11
|
if (el.shadowRoot !== null)
|
|
32
|
-
continue;
|
|
33
|
-
// A closed root attached AFTER the SDK installed was captured from attachShadow's return value,
|
|
34
|
-
// so it is readable too. Only what was genuinely missed is declared.
|
|
12
|
+
continue;
|
|
35
13
|
if (isCaptured(el))
|
|
36
14
|
continue;
|
|
37
15
|
if (el.children.length > 0)
|
|
@@ -44,11 +22,6 @@ export function countClosedShadowRoots() {
|
|
|
44
22
|
}
|
|
45
23
|
return count;
|
|
46
24
|
}
|
|
47
|
-
/**
|
|
48
|
-
* Whether a frame is cross-origin (unobservable). A same-origin frame exposes its `contentDocument`; a
|
|
49
|
-
* cross-origin one returns null (or THROWS a SecurityError on access). A src-less / about:blank frame is
|
|
50
|
-
* same-origin and simply not navigated — not a blind spot.
|
|
51
|
-
*/
|
|
52
25
|
export function isCrossOriginFrame(frame) {
|
|
53
26
|
const src = frame.getAttribute('src');
|
|
54
27
|
if (null === src || 0 === src.length)
|
|
@@ -57,10 +30,9 @@ export function isCrossOriginFrame(frame) {
|
|
|
57
30
|
return null === frame.contentDocument;
|
|
58
31
|
}
|
|
59
32
|
catch {
|
|
60
|
-
return true;
|
|
33
|
+
return true;
|
|
61
34
|
}
|
|
62
35
|
}
|
|
63
|
-
/** How many of the given frames are cross-origin (unobservable). */
|
|
64
36
|
export function countCrossOriginFrames(frames) {
|
|
65
37
|
let count = 0;
|
|
66
38
|
for (const frame of frames)
|
|
@@ -71,31 +43,11 @@ export function countCrossOriginFrames(frames) {
|
|
|
71
43
|
function currentCount() {
|
|
72
44
|
return countCrossOriginFrames(Array.from(document.querySelectorAll('iframe')));
|
|
73
45
|
}
|
|
74
|
-
/**
|
|
75
|
-
* Same-origin frames — observed for DOM, NOT for network.
|
|
76
|
-
*
|
|
77
|
-
* Their `fetch` lives in the frame's own realm, which the top realm's patch never sees. Left
|
|
78
|
-
* undeclared, `settled` can read true while a frame request is still in flight.
|
|
79
|
-
*/
|
|
80
46
|
function uninstrumentedFrameCount() {
|
|
81
47
|
return sameOriginFrameBodies(document).length;
|
|
82
48
|
}
|
|
83
|
-
/**
|
|
84
|
-
* Watch the page for cross-origin iframes and emit BLIND_SPOT whenever the count CHANGES (install +
|
|
85
|
-
* on any added/removed frame). Only the delta is emitted — never a per-mutation flood — so a static
|
|
86
|
-
* embed reports once. Reversible: disconnects the observer on teardown.
|
|
87
|
-
*/
|
|
88
|
-
/**
|
|
89
|
-
* How long added/removed nodes are allowed to pile up before the frame count is re-checked.
|
|
90
|
-
*
|
|
91
|
-
* A blind spot is a coverage caveat on a verdict, not a live signal — noticing an embed a fraction of
|
|
92
|
-
* a second later costs nothing, while checking on every mutation costs a DOM scan per render.
|
|
93
|
-
*/
|
|
94
49
|
const RECHECK_DEBOUNCE_MS = 250;
|
|
95
50
|
export function installBlindSpots(emit) {
|
|
96
|
-
// Every kind is counted the same way and emitted on CHANGE only, so a static embed or a steady list
|
|
97
|
-
// reports once instead of flooding. Virtualized rows matter most: without them an assertion over a
|
|
98
|
-
// 10,000-row grid is a claim about the ~29 on screen, delivered as full coverage.
|
|
99
51
|
const sensors = [
|
|
100
52
|
{ kind: BlindSpotKind.CROSS_ORIGIN_IFRAME, count: currentCount },
|
|
101
53
|
{ kind: BlindSpotKind.UNINSTRUMENTED_FRAME, count: uninstrumentedFrameCount },
|
|
@@ -114,17 +66,6 @@ export function installBlindSpots(emit) {
|
|
|
114
66
|
}
|
|
115
67
|
};
|
|
116
68
|
report();
|
|
117
|
-
// Coalesce rather than gate.
|
|
118
|
-
//
|
|
119
|
-
// This used to try to skip unrelated churn by testing each added/removed node with
|
|
120
|
-
// `n.querySelector('iframe')` — but that is a full subtree scan per mutated node, so a React commit
|
|
121
|
-
// mounting a 500-node panel paid a 500-node scan to decide whether to do a document scan. The gate
|
|
122
|
-
// cost more than the work, and it scaled with exactly the thing a large app does most.
|
|
123
|
-
//
|
|
124
|
-
// Since report() emits only when the count CHANGES, running it late and rarely is free. One
|
|
125
|
-
// debounced document scan per quarter-second replaces per-node scanning, and it is also more
|
|
126
|
-
// correct: the old gate's `querySelector` missed an iframe added inside a subtree whose own root
|
|
127
|
-
// was a text or comment node.
|
|
128
69
|
let pending;
|
|
129
70
|
const observer = new MutationObserver(() => {
|
|
130
71
|
if (pending !== undefined)
|