@reticlehq/browser 2.2.0 → 2.3.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 +0 -1
- package/dist/actions/actions-dom.js +0 -1
- package/dist/actions/actions.d.ts +3 -1
- package/dist/actions/actions.js +55 -36
- package/dist/actions/scroll.d.ts +0 -1
- package/dist/actions/scroll.js +0 -1
- package/dist/actions/value-input.d.ts +33 -0
- package/dist/actions/value-input.js +63 -0
- package/dist/commands/commands.d.ts +0 -1
- package/dist/commands/commands.js +15 -7
- package/dist/dom/a11y.d.ts +0 -2
- package/dist/dom/a11y.js +54 -24
- package/dist/dom/auto-anchor.d.ts +0 -1
- package/dist/dom/auto-anchor.js +0 -1
- package/dist/dom/blind-spots.d.ts +0 -1
- package/dist/dom/blind-spots.js +1 -2
- package/dist/dom/chart.d.ts +0 -1
- package/dist/dom/chart.js +0 -1
- package/dist/dom/desktop-capture.d.ts +36 -0
- package/dist/dom/desktop-capture.js +113 -0
- package/dist/dom/dom-ignore.d.ts +7 -1
- package/dist/dom/dom-ignore.js +1 -2
- package/dist/dom/occlusion.d.ts +0 -1
- package/dist/dom/occlusion.js +0 -1
- package/dist/dom/query.d.ts +0 -1
- package/dist/dom/query.js +61 -13
- package/dist/dom/realm.d.ts +30 -0
- package/dist/dom/realm.js +58 -0
- package/dist/dom/refs.d.ts +0 -1
- package/dist/dom/refs.js +0 -1
- package/dist/dom/shadow-registry.d.ts +40 -0
- package/dist/dom/shadow-registry.js +165 -0
- package/dist/dom/snapshot.d.ts +2 -2
- package/dist/dom/snapshot.js +21 -4
- package/dist/dom/source.d.ts +0 -1
- package/dist/dom/source.js +0 -1
- package/dist/dom/theme.d.ts +0 -1
- package/dist/dom/theme.js +0 -1
- package/dist/dom/virtualized.d.ts +19 -0
- package/dist/dom/virtualized.js +80 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/observers/animation.d.ts +0 -1
- package/dist/observers/animation.js +0 -1
- package/dist/observers/blind-spots.d.ts +16 -4
- package/dist/observers/blind-spots.js +65 -11
- package/dist/observers/console.d.ts +0 -1
- package/dist/observers/console.js +0 -1
- package/dist/observers/dom.d.ts +0 -2
- package/dist/observers/dom.js +67 -9
- package/dist/observers/download.d.ts +6 -0
- package/dist/observers/download.js +103 -0
- package/dist/observers/focus.d.ts +0 -1
- package/dist/observers/focus.js +0 -1
- package/dist/observers/frames.d.ts +12 -0
- package/dist/observers/frames.js +67 -0
- package/dist/observers/health.d.ts +0 -1
- package/dist/observers/health.js +30 -2
- package/dist/observers/install-all.d.ts +5 -0
- package/dist/observers/install-all.js +63 -0
- package/dist/observers/ipc.d.ts +22 -0
- package/dist/observers/ipc.js +121 -0
- package/dist/observers/network-body.d.ts +17 -1
- package/dist/observers/network-body.js +26 -1
- package/dist/observers/network-redact.d.ts +0 -1
- package/dist/observers/network-redact.js +6 -3
- package/dist/observers/network-stream.d.ts +23 -0
- package/dist/observers/network-stream.js +102 -0
- package/dist/observers/network.d.ts +11 -1
- package/dist/observers/network.js +7 -1
- package/dist/observers/perf.d.ts +0 -1
- package/dist/observers/perf.js +0 -1
- package/dist/observers/route.d.ts +0 -1
- package/dist/observers/route.js +0 -1
- package/dist/observers/scroll.d.ts +0 -1
- package/dist/observers/scroll.js +0 -1
- package/dist/observers/sdk-failure.d.ts +28 -0
- package/dist/observers/sdk-failure.js +60 -0
- package/dist/observers/state.d.ts +0 -1
- package/dist/observers/state.js +0 -1
- package/dist/observers/storage.d.ts +0 -1
- package/dist/observers/storage.js +0 -1
- package/dist/observers/types.d.ts +0 -1
- package/dist/observers/types.js +0 -1
- package/dist/patching/capture-method.d.ts +0 -1
- package/dist/patching/capture-method.js +0 -1
- package/dist/presenter/overlay.d.ts +0 -1
- package/dist/presenter/overlay.js +0 -1
- package/dist/presenter/presenter-config.d.ts +0 -1
- package/dist/presenter/presenter-config.js +0 -1
- package/dist/presenter/presenter-controls.d.ts +0 -1
- package/dist/presenter/presenter-controls.js +0 -1
- package/dist/presenter/presenter-effects.d.ts +0 -1
- package/dist/presenter/presenter-effects.js +0 -1
- package/dist/presenter/presenter-glow.d.ts +0 -1
- package/dist/presenter/presenter-glow.js +0 -1
- package/dist/presenter/presenter-log.d.ts +0 -1
- package/dist/presenter/presenter-log.js +0 -1
- package/dist/presenter/presenter-run-state.d.ts +0 -1
- package/dist/presenter/presenter-run-state.js +0 -1
- package/dist/presenter/presenter-styles.d.ts +0 -1
- package/dist/presenter/presenter-styles.js +0 -1
- package/dist/presenter/presenter-tally.d.ts +0 -1
- package/dist/presenter/presenter-tally.js +0 -1
- package/dist/presenter/presenter-test-helpers.d.ts +0 -1
- package/dist/presenter/presenter-test-helpers.js +0 -1
- package/dist/presenter/presenter-verbs.d.ts +0 -1
- package/dist/presenter/presenter-verbs.js +0 -1
- package/dist/presenter/presenter.d.ts +0 -1
- package/dist/presenter/presenter.js +0 -1
- package/dist/recorder/recorder-styles.d.ts +0 -1
- package/dist/recorder/recorder-styles.js +0 -1
- package/dist/recorder/recorder.d.ts +0 -1
- package/dist/recorder/recorder.js +0 -1
- package/dist/registry/adapters.d.ts +0 -1
- package/dist/registry/adapters.js +0 -1
- package/dist/registry/capabilities.d.ts +0 -1
- package/dist/registry/capabilities.js +0 -1
- package/dist/registry/commit-and-signal.d.ts +0 -1
- package/dist/registry/commit-and-signal.js +0 -1
- package/dist/registry/domains.d.ts +0 -1
- package/dist/registry/domains.js +0 -1
- package/dist/registry/emitter.d.ts +0 -1
- package/dist/registry/emitter.js +0 -1
- package/dist/registry/store-adapters.d.ts +14 -6
- package/dist/registry/store-adapters.js +0 -1
- package/dist/registry/stores.d.ts +0 -1
- package/dist/registry/stores.js +10 -2
- package/dist/reticle-presenter-helpers.d.ts +0 -1
- package/dist/reticle-presenter-helpers.js +0 -1
- package/dist/reticle.d.ts +3 -2
- package/dist/reticle.js +12 -32
- package/dist/review/annotator.d.ts +0 -1
- package/dist/review/annotator.js +0 -1
- package/dist/review/mark-anchor.d.ts +0 -1
- package/dist/review/mark-anchor.js +0 -1
- package/dist/security/serialization.d.ts +0 -1
- package/dist/security/serialization.js +0 -1
- package/dist/timers/clock.d.ts +0 -1
- package/dist/timers/clock.js +0 -1
- package/dist/timers/native-console.d.ts +1 -0
- package/dist/timers/native-console.js +9 -0
- package/dist/timers/native-timers.d.ts +0 -1
- package/dist/timers/native-timers.js +0 -1
- package/dist/transport/transport.d.ts +2 -6
- package/dist/transport/transport.js +95 -18
- package/package.json +8 -3
- package/dist/actions/actions-dom.d.ts.map +0 -1
- package/dist/actions/actions-dom.js.map +0 -1
- package/dist/actions/actions.d.ts.map +0 -1
- package/dist/actions/actions.js.map +0 -1
- package/dist/actions/scroll.d.ts.map +0 -1
- package/dist/actions/scroll.js.map +0 -1
- package/dist/commands/commands.d.ts.map +0 -1
- package/dist/commands/commands.js.map +0 -1
- package/dist/dom/a11y.d.ts.map +0 -1
- package/dist/dom/a11y.js.map +0 -1
- package/dist/dom/auto-anchor.d.ts.map +0 -1
- package/dist/dom/auto-anchor.js.map +0 -1
- package/dist/dom/blind-spots.d.ts.map +0 -1
- package/dist/dom/blind-spots.js.map +0 -1
- package/dist/dom/chart.d.ts.map +0 -1
- package/dist/dom/chart.js.map +0 -1
- package/dist/dom/dom-ignore.d.ts.map +0 -1
- package/dist/dom/dom-ignore.js.map +0 -1
- package/dist/dom/occlusion.d.ts.map +0 -1
- package/dist/dom/occlusion.js.map +0 -1
- package/dist/dom/query.d.ts.map +0 -1
- package/dist/dom/query.js.map +0 -1
- package/dist/dom/refs.d.ts.map +0 -1
- package/dist/dom/refs.js.map +0 -1
- package/dist/dom/snapshot.d.ts.map +0 -1
- package/dist/dom/snapshot.js.map +0 -1
- package/dist/dom/source.d.ts.map +0 -1
- package/dist/dom/source.js.map +0 -1
- package/dist/dom/theme.d.ts.map +0 -1
- package/dist/dom/theme.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/observers/animation.d.ts.map +0 -1
- package/dist/observers/animation.js.map +0 -1
- package/dist/observers/blind-spots.d.ts.map +0 -1
- package/dist/observers/blind-spots.js.map +0 -1
- package/dist/observers/console.d.ts.map +0 -1
- package/dist/observers/console.js.map +0 -1
- package/dist/observers/dom.d.ts.map +0 -1
- package/dist/observers/dom.js.map +0 -1
- package/dist/observers/focus.d.ts.map +0 -1
- package/dist/observers/focus.js.map +0 -1
- package/dist/observers/health.d.ts.map +0 -1
- package/dist/observers/health.js.map +0 -1
- package/dist/observers/network-body.d.ts.map +0 -1
- package/dist/observers/network-body.js.map +0 -1
- package/dist/observers/network-redact.d.ts.map +0 -1
- package/dist/observers/network-redact.js.map +0 -1
- package/dist/observers/network.d.ts.map +0 -1
- package/dist/observers/network.js.map +0 -1
- package/dist/observers/perf.d.ts.map +0 -1
- package/dist/observers/perf.js.map +0 -1
- package/dist/observers/route.d.ts.map +0 -1
- package/dist/observers/route.js.map +0 -1
- package/dist/observers/scroll.d.ts.map +0 -1
- package/dist/observers/scroll.js.map +0 -1
- package/dist/observers/state.d.ts.map +0 -1
- package/dist/observers/state.js.map +0 -1
- package/dist/observers/storage.d.ts.map +0 -1
- package/dist/observers/storage.js.map +0 -1
- package/dist/observers/types.d.ts.map +0 -1
- package/dist/observers/types.js.map +0 -1
- package/dist/patching/capture-method.d.ts.map +0 -1
- package/dist/patching/capture-method.js.map +0 -1
- package/dist/presenter/overlay.d.ts.map +0 -1
- package/dist/presenter/overlay.js.map +0 -1
- package/dist/presenter/presenter-config.d.ts.map +0 -1
- package/dist/presenter/presenter-config.js.map +0 -1
- package/dist/presenter/presenter-controls.d.ts.map +0 -1
- package/dist/presenter/presenter-controls.js.map +0 -1
- package/dist/presenter/presenter-effects.d.ts.map +0 -1
- package/dist/presenter/presenter-effects.js.map +0 -1
- package/dist/presenter/presenter-glow.d.ts.map +0 -1
- package/dist/presenter/presenter-glow.js.map +0 -1
- package/dist/presenter/presenter-log.d.ts.map +0 -1
- package/dist/presenter/presenter-log.js.map +0 -1
- package/dist/presenter/presenter-run-state.d.ts.map +0 -1
- package/dist/presenter/presenter-run-state.js.map +0 -1
- package/dist/presenter/presenter-styles.d.ts.map +0 -1
- package/dist/presenter/presenter-styles.js.map +0 -1
- package/dist/presenter/presenter-tally.d.ts.map +0 -1
- package/dist/presenter/presenter-tally.js.map +0 -1
- package/dist/presenter/presenter-test-helpers.d.ts.map +0 -1
- package/dist/presenter/presenter-test-helpers.js.map +0 -1
- package/dist/presenter/presenter-verbs.d.ts.map +0 -1
- package/dist/presenter/presenter-verbs.js.map +0 -1
- package/dist/presenter/presenter.d.ts.map +0 -1
- package/dist/presenter/presenter.js.map +0 -1
- package/dist/recorder/recorder-styles.d.ts.map +0 -1
- package/dist/recorder/recorder-styles.js.map +0 -1
- package/dist/recorder/recorder.d.ts.map +0 -1
- package/dist/recorder/recorder.js.map +0 -1
- package/dist/registry/adapters.d.ts.map +0 -1
- package/dist/registry/adapters.js.map +0 -1
- package/dist/registry/capabilities.d.ts.map +0 -1
- package/dist/registry/capabilities.js.map +0 -1
- package/dist/registry/commit-and-signal.d.ts.map +0 -1
- package/dist/registry/commit-and-signal.js.map +0 -1
- package/dist/registry/domains.d.ts.map +0 -1
- package/dist/registry/domains.js.map +0 -1
- package/dist/registry/emitter.d.ts.map +0 -1
- package/dist/registry/emitter.js.map +0 -1
- package/dist/registry/store-adapters.d.ts.map +0 -1
- package/dist/registry/store-adapters.js.map +0 -1
- package/dist/registry/stores.d.ts.map +0 -1
- package/dist/registry/stores.js.map +0 -1
- package/dist/reticle-presenter-helpers.d.ts.map +0 -1
- package/dist/reticle-presenter-helpers.js.map +0 -1
- package/dist/reticle.d.ts.map +0 -1
- package/dist/reticle.js.map +0 -1
- package/dist/review/annotator.d.ts.map +0 -1
- package/dist/review/annotator.js.map +0 -1
- package/dist/review/mark-anchor.d.ts.map +0 -1
- package/dist/review/mark-anchor.js.map +0 -1
- package/dist/security/serialization.d.ts.map +0 -1
- package/dist/security/serialization.js.map +0 -1
- package/dist/timers/clock.d.ts.map +0 -1
- package/dist/timers/clock.js.map +0 -1
- package/dist/timers/native-timers.d.ts.map +0 -1
- package/dist/timers/native-timers.js.map +0 -1
- package/dist/transport/transport.d.ts.map +0 -1
- package/dist/transport/transport.js.map +0 -1
package/dist/dom/snapshot.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { ElementState, SnapshotMode } from '@reticlehq/core';
|
|
2
|
+
import { capturedRootOf } from './shadow-registry.js';
|
|
3
|
+
import { isFrame } from './realm.js';
|
|
2
4
|
import { getAccessibleName, getRole, getStates, getValue, isVisible } from './a11y.js';
|
|
3
5
|
import { refs } from './refs.js';
|
|
4
6
|
import { isIgnored } from './dom-ignore.js';
|
|
@@ -93,10 +95,16 @@ function layoutSignature(style) {
|
|
|
93
95
|
*/
|
|
94
96
|
function pierceChildren(parent) {
|
|
95
97
|
const out = [...parent.children];
|
|
96
|
-
|
|
98
|
+
// A CLOSED root reports `shadowRoot === null` forever, but the registry captured it at the moment
|
|
99
|
+
// `attachShadow` returned it. `query` has been able to reach that content since; the snapshot could
|
|
100
|
+
// not, so the two tools disagreed about what is on the page — and the snapshot is the one an agent
|
|
101
|
+
// reads to decide what to query for, which makes its omission the one that actually costs.
|
|
102
|
+
const shadow = parent.shadowRoot ?? capturedRootOf(parent);
|
|
97
103
|
if (shadow !== null)
|
|
98
104
|
out.push(...shadow.children);
|
|
99
|
-
|
|
105
|
+
// Realm-aware: an iframe nested INSIDE another frame's body belongs to that frame's realm, so the
|
|
106
|
+
// top window's constructor never matches it and nested embeds were silently never pierced.
|
|
107
|
+
if (isFrame(parent)) {
|
|
100
108
|
try {
|
|
101
109
|
const body = parent.contentDocument?.body;
|
|
102
110
|
if (body !== null && body !== undefined)
|
|
@@ -156,11 +164,21 @@ function collectDialogs(root) {
|
|
|
156
164
|
}
|
|
157
165
|
return names;
|
|
158
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Status for a snapshot, with uninformative defaults OMITTED.
|
|
169
|
+
*
|
|
170
|
+
* `reticle_act` already follows this rule — "fields at their uninformative default are omitted so a
|
|
171
|
+
* clean action collapses to its consequence" — and snapshot did not, so every response carried
|
|
172
|
+
* `visibleDialogs: []` whether or not a dialog existed. An empty array is the overwhelmingly common
|
|
173
|
+
* case, so the field was almost pure repetition; a reader learns nothing from its presence and
|
|
174
|
+
* everything from it.
|
|
175
|
+
*/
|
|
159
176
|
function buildStatus(root) {
|
|
177
|
+
const visibleDialogs = collectDialogs(root);
|
|
160
178
|
return {
|
|
161
179
|
route: `${location.pathname}${location.search}${location.hash}`,
|
|
162
180
|
title: document.title,
|
|
163
|
-
visibleDialogs:
|
|
181
|
+
...(visibleDialogs.length > 0 ? { visibleDialogs } : {}),
|
|
164
182
|
};
|
|
165
183
|
}
|
|
166
184
|
/** Build the semantic accessibility snapshot of the page or a subtree. */
|
|
@@ -195,4 +213,3 @@ export function buildSnapshot(options = {}) {
|
|
|
195
213
|
truncated: ctx.truncated,
|
|
196
214
|
};
|
|
197
215
|
}
|
|
198
|
-
//# sourceMappingURL=snapshot.js.map
|
package/dist/dom/source.d.ts
CHANGED
package/dist/dom/source.js
CHANGED
package/dist/dom/theme.d.ts
CHANGED
package/dist/dom/theme.js
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
export declare function unmountedRowsIn(container: HTMLElement): number;
|
|
18
|
+
/** Total unmounted rows across every scroll container on the page. */
|
|
19
|
+
export declare function countUnmountedRows(): number;
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
const UNMOUNTED_RATIO = 0.25;
|
|
18
|
+
/** A container has to be meaningfully scrollable before "what is not rendered" is even a question. */
|
|
19
|
+
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
|
+
function rowHost(container) {
|
|
26
|
+
let host = container;
|
|
27
|
+
for (let depth = 0; depth < 3; depth += 1) {
|
|
28
|
+
const children = Array.from(host.children).filter((c) => c instanceof HTMLElement);
|
|
29
|
+
const only = children.length === 1 ? children[0] : undefined;
|
|
30
|
+
if (only === undefined)
|
|
31
|
+
return host;
|
|
32
|
+
// A spacer is a single child that fills (or over-fills) the scroll area.
|
|
33
|
+
if (only.offsetHeight < host.scrollHeight * 0.9)
|
|
34
|
+
return host;
|
|
35
|
+
host = only;
|
|
36
|
+
}
|
|
37
|
+
return host;
|
|
38
|
+
}
|
|
39
|
+
/** How many rows a container is holding space for but has not rendered. 0 when it is not virtualized. */
|
|
40
|
+
export function unmountedRowsIn(container) {
|
|
41
|
+
const scrollHeight = container.scrollHeight;
|
|
42
|
+
const clientHeight = container.clientHeight;
|
|
43
|
+
if (clientHeight <= 0 || scrollHeight < clientHeight * MIN_SCROLL_RATIO)
|
|
44
|
+
return 0;
|
|
45
|
+
const host = rowHost(container);
|
|
46
|
+
const rows = Array.from(host.children).filter((c) => c instanceof HTMLElement);
|
|
47
|
+
if (rows.length < 2)
|
|
48
|
+
return 0;
|
|
49
|
+
let top = Infinity;
|
|
50
|
+
let bottom = 0;
|
|
51
|
+
let totalHeight = 0;
|
|
52
|
+
for (const row of rows) {
|
|
53
|
+
const rowTop = row.offsetTop;
|
|
54
|
+
const rowBottom = rowTop + row.offsetHeight;
|
|
55
|
+
if (rowTop < top)
|
|
56
|
+
top = rowTop;
|
|
57
|
+
if (rowBottom > bottom)
|
|
58
|
+
bottom = rowBottom;
|
|
59
|
+
totalHeight += row.offsetHeight;
|
|
60
|
+
}
|
|
61
|
+
// Space the container reserved and left empty: above the first row plus below the last.
|
|
62
|
+
const empty = Math.max(0, top) + Math.max(0, scrollHeight - bottom);
|
|
63
|
+
if (empty < scrollHeight * UNMOUNTED_RATIO)
|
|
64
|
+
return 0;
|
|
65
|
+
const averageRow = totalHeight / rows.length;
|
|
66
|
+
if (averageRow <= 0)
|
|
67
|
+
return 0;
|
|
68
|
+
return Math.round(empty / averageRow);
|
|
69
|
+
}
|
|
70
|
+
/** Total unmounted rows across every scroll container on the page. */
|
|
71
|
+
export function countUnmountedRows() {
|
|
72
|
+
let total = 0;
|
|
73
|
+
for (const element of document.querySelectorAll('*')) {
|
|
74
|
+
// Cheap gate first: only elements that actually scroll are worth measuring.
|
|
75
|
+
if (element.scrollHeight <= element.clientHeight)
|
|
76
|
+
continue;
|
|
77
|
+
total += unmountedRowsIn(element);
|
|
78
|
+
}
|
|
79
|
+
return total;
|
|
80
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -26,4 +26,3 @@ export { describe, getRole, getAccessibleName, getStates, isVisible } from './do
|
|
|
26
26
|
export { refs, RefRegistry } from './dom/refs.js';
|
|
27
27
|
export { Annotator, installAnnotator, type AnnotatorDeps } from './review/annotator.js';
|
|
28
28
|
export { resolveMarkAnchor, type MarkAnchor } from './review/mark-anchor.js';
|
|
29
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -37,4 +37,3 @@ export { describe, getRole, getAccessibleName, getStates, isVisible } from './do
|
|
|
37
37
|
export { refs, RefRegistry } from './dom/refs.js';
|
|
38
38
|
export { Annotator, installAnnotator } from './review/annotator.js';
|
|
39
39
|
export { resolveMarkAnchor } from './review/mark-anchor.js';
|
|
40
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -3,10 +3,23 @@ import type { Emit, Teardown } from './types.js';
|
|
|
3
3
|
* Blind-spot sensor. The SDK instruments the DOM, but a cross-origin iframe is a wall it cannot
|
|
4
4
|
* see through — its contents, network, and console are invisible. Left unstated, a green verdict would
|
|
5
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.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
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
|
+
export declare function countClosedShadowRoots(): number;
|
|
10
23
|
/**
|
|
11
24
|
* Whether a frame is cross-origin (unobservable). A same-origin frame exposes its `contentDocument`; a
|
|
12
25
|
* cross-origin one returns null (or THROWS a SecurityError on access). A src-less / about:blank frame is
|
|
@@ -16,4 +29,3 @@ export declare function isCrossOriginFrame(frame: HTMLIFrameElement): boolean;
|
|
|
16
29
|
/** How many of the given frames are cross-origin (unobservable). */
|
|
17
30
|
export declare function countCrossOriginFrames(frames: readonly HTMLIFrameElement[]): number;
|
|
18
31
|
export declare function installBlindSpots(emit: Emit): Teardown;
|
|
19
|
-
//# sourceMappingURL=blind-spots.d.ts.map
|
|
@@ -1,13 +1,49 @@
|
|
|
1
1
|
import { EventType, BlindSpotKind } from '@reticlehq/core';
|
|
2
|
+
import { countUnmountedRows } from '../dom/virtualized.js';
|
|
3
|
+
import { isCaptured } from '../dom/shadow-registry.js';
|
|
4
|
+
import { sameOriginFrameBodies } from './frames.js';
|
|
2
5
|
import { nativeClearTimeout, nativeSetTimeout } from '../timers/native-timers.js';
|
|
3
6
|
/**
|
|
4
7
|
* Blind-spot sensor. The SDK instruments the DOM, but a cross-origin iframe is a wall it cannot
|
|
5
8
|
* see through — its contents, network, and console are invisible. Left unstated, a green verdict would
|
|
6
9
|
* imply the whole page was verified. This detects cross-origin frames and emits a BLIND_SPOT event so the
|
|
7
|
-
* server can mark results `coverage: partial` instead of lying by omission.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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.
|
|
10
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
|
+
*/
|
|
26
|
+
export function countClosedShadowRoots() {
|
|
27
|
+
let count = 0;
|
|
28
|
+
for (const el of document.querySelectorAll('*')) {
|
|
29
|
+
if (!el.tagName.includes('-'))
|
|
30
|
+
continue;
|
|
31
|
+
if (el.shadowRoot !== null)
|
|
32
|
+
continue; // open — readable, not a blind spot
|
|
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.
|
|
35
|
+
if (isCaptured(el))
|
|
36
|
+
continue;
|
|
37
|
+
if (el.children.length > 0)
|
|
38
|
+
continue;
|
|
39
|
+
if ((el.textContent ?? '').trim().length > 0)
|
|
40
|
+
continue;
|
|
41
|
+
if (el.offsetHeight <= 0 && el.offsetWidth <= 0)
|
|
42
|
+
continue;
|
|
43
|
+
count += 1;
|
|
44
|
+
}
|
|
45
|
+
return count;
|
|
46
|
+
}
|
|
11
47
|
/**
|
|
12
48
|
* Whether a frame is cross-origin (unobservable). A same-origin frame exposes its `contentDocument`; a
|
|
13
49
|
* cross-origin one returns null (or THROWS a SecurityError on access). A src-less / about:blank frame is
|
|
@@ -35,6 +71,15 @@ export function countCrossOriginFrames(frames) {
|
|
|
35
71
|
function currentCount() {
|
|
36
72
|
return countCrossOriginFrames(Array.from(document.querySelectorAll('iframe')));
|
|
37
73
|
}
|
|
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
|
+
function uninstrumentedFrameCount() {
|
|
81
|
+
return sameOriginFrameBodies(document).length;
|
|
82
|
+
}
|
|
38
83
|
/**
|
|
39
84
|
* Watch the page for cross-origin iframes and emit BLIND_SPOT whenever the count CHANGES (install +
|
|
40
85
|
* on any added/removed frame). Only the delta is emitted — never a per-mutation flood — so a static
|
|
@@ -48,14 +93,24 @@ function currentCount() {
|
|
|
48
93
|
*/
|
|
49
94
|
const RECHECK_DEBOUNCE_MS = 250;
|
|
50
95
|
export function installBlindSpots(emit) {
|
|
51
|
-
|
|
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
|
+
const sensors = [
|
|
100
|
+
{ kind: BlindSpotKind.CROSS_ORIGIN_IFRAME, count: currentCount },
|
|
101
|
+
{ kind: BlindSpotKind.UNINSTRUMENTED_FRAME, count: uninstrumentedFrameCount },
|
|
102
|
+
{ kind: BlindSpotKind.CLOSED_SHADOW_ROOT, count: countClosedShadowRoots },
|
|
103
|
+
{ kind: BlindSpotKind.VIRTUALIZED_UNMOUNTED, count: countUnmountedRows },
|
|
104
|
+
];
|
|
105
|
+
const last = new Map();
|
|
52
106
|
const report = () => {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
107
|
+
for (const sensor of sensors) {
|
|
108
|
+
const count = sensor.count();
|
|
109
|
+
if (count === last.get(sensor.kind))
|
|
110
|
+
continue;
|
|
111
|
+
last.set(sensor.kind, count);
|
|
112
|
+
if (count > 0)
|
|
113
|
+
emit(EventType.BLIND_SPOT, { kind: sensor.kind, count });
|
|
59
114
|
}
|
|
60
115
|
};
|
|
61
116
|
report();
|
|
@@ -86,4 +141,3 @@ export function installBlindSpots(emit) {
|
|
|
86
141
|
observer.disconnect();
|
|
87
142
|
};
|
|
88
143
|
}
|
|
89
|
-
//# sourceMappingURL=blind-spots.js.map
|
package/dist/observers/dom.d.ts
CHANGED
package/dist/observers/dom.js
CHANGED
|
@@ -8,13 +8,16 @@ import { refs } from '../dom/refs.js';
|
|
|
8
8
|
* container persists across ticks, so its testid (or ref) is the identity that does accumulate.
|
|
9
9
|
*/
|
|
10
10
|
function regionKeyOf(target) {
|
|
11
|
-
const el = target
|
|
11
|
+
const el = isElement(target) ? target : null;
|
|
12
12
|
if (el === null)
|
|
13
13
|
return undefined;
|
|
14
14
|
const labelled = el.closest('[data-testid]');
|
|
15
15
|
return labelled?.getAttribute('data-testid') ?? refs.refFor(el);
|
|
16
16
|
}
|
|
17
17
|
import { isReticleOverlay } from '../dom/dom-ignore.js';
|
|
18
|
+
import { capturedRoots, installShadowRegistry, onShadowRoot } from '../dom/shadow-registry.js';
|
|
19
|
+
import { isElement } from '../dom/realm.js';
|
|
20
|
+
import { observeSameOriginFrames } from './frames.js';
|
|
18
21
|
const WATCHED_ATTRS = [
|
|
19
22
|
'class',
|
|
20
23
|
'hidden',
|
|
@@ -46,7 +49,30 @@ function isMeaningful(role, name) {
|
|
|
46
49
|
return role !== 'generic' || name.length > 0;
|
|
47
50
|
}
|
|
48
51
|
/** Observe DOM mutations and emit semantic (not raw) events. */
|
|
52
|
+
/**
|
|
53
|
+
* The trimmed text carried by a node list, or undefined when it carries none.
|
|
54
|
+
*
|
|
55
|
+
* Whitespace-only nodes are ignored deliberately: a re-render that reshuffles indentation would
|
|
56
|
+
* otherwise emit a text change on every layout node, which is noise the timeline cannot afford.
|
|
57
|
+
*/
|
|
58
|
+
const TEXT_NODE = 3;
|
|
59
|
+
function textOf(nodes) {
|
|
60
|
+
let out = '';
|
|
61
|
+
for (const node of nodes) {
|
|
62
|
+
// The numeric nodeType, not `Node.TEXT_NODE`: the global `Node` constructor is not guaranteed to
|
|
63
|
+
// exist wherever this runs, and referencing it threw a ReferenceError rather than degrading.
|
|
64
|
+
if (node.nodeType !== TEXT_NODE)
|
|
65
|
+
continue;
|
|
66
|
+
out += node.nodeValue ?? '';
|
|
67
|
+
}
|
|
68
|
+
const trimmed = out.trim();
|
|
69
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
70
|
+
}
|
|
49
71
|
export function installDom(emit) {
|
|
72
|
+
// Installed here rather than in the bootstrap list because this observer is the registry's reason
|
|
73
|
+
// for existing, and the ordering matters: a root attached between the two would be missed by both.
|
|
74
|
+
// It is also the only chance to capture a CLOSED root, which attachShadow returns exactly once.
|
|
75
|
+
const stopRegistry = installShadowRegistry();
|
|
50
76
|
const observer = new MutationObserver((records) => {
|
|
51
77
|
let added = 0;
|
|
52
78
|
let removed = 0;
|
|
@@ -58,9 +84,7 @@ export function installDom(emit) {
|
|
|
58
84
|
for (const record of records) {
|
|
59
85
|
if (record.type === 'attributes') {
|
|
60
86
|
const target = record.target;
|
|
61
|
-
if (target
|
|
62
|
-
record.attributeName !== null &&
|
|
63
|
-
!isReticleOverlay(target)) {
|
|
87
|
+
if (isElement(target) && record.attributeName !== null && !isReticleOverlay(target)) {
|
|
64
88
|
if (changed >= MAX_PER_BATCH) {
|
|
65
89
|
dropped += 1;
|
|
66
90
|
continue;
|
|
@@ -93,8 +117,31 @@ export function installDom(emit) {
|
|
|
93
117
|
}
|
|
94
118
|
continue;
|
|
95
119
|
}
|
|
120
|
+
// A TEXT node arriving or leaving is a text change, and it is the ordinary one. `characterData`
|
|
121
|
+
// fires only when an EXISTING text node's `data` is edited in place; `el.textContent = x`,
|
|
122
|
+
// `innerText`, and React replacing a child all REPLACE the node instead — a childList mutation
|
|
123
|
+
// carrying a Text node, which the Element-only loops below skip. Measured on a live page:
|
|
124
|
+
// `firstChild.data = x` emitted `dom.text`, while `textContent`, `innerText` and
|
|
125
|
+
// `appendChild(createTextNode())` emitted NOTHING. So the most common visible change an app
|
|
126
|
+
// makes — updating text — produced no event at all, `reticle_observe` reported "nothing
|
|
127
|
+
// happened" over a screen that had visibly changed, and `settled` went quiet immediately.
|
|
128
|
+
if (textOf(record.addedNodes) !== undefined || textOf(record.removedNodes) !== undefined) {
|
|
129
|
+
const parent = isElement(record.target) ? record.target : null;
|
|
130
|
+
if (parent !== null && !isReticleOverlay(parent)) {
|
|
131
|
+
if (changed >= MAX_PER_BATCH)
|
|
132
|
+
dropped += 1;
|
|
133
|
+
else {
|
|
134
|
+
changed += 1;
|
|
135
|
+
// The element's text AFTER the mutation, so a cleared node reports '' rather than the
|
|
136
|
+
// text that just left — the reader wants what is on screen now.
|
|
137
|
+
const text = (parent.textContent ?? '').trim().slice(0, 80);
|
|
138
|
+
const old = capValue(textOf(record.removedNodes) ?? null);
|
|
139
|
+
emit(EventType.DOM_TEXT, { text, ...(old === undefined ? {} : { old }) }, refs.refFor(parent));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
96
143
|
for (const node of record.addedNodes) {
|
|
97
|
-
if (!(node
|
|
144
|
+
if (!isElement(node))
|
|
98
145
|
continue;
|
|
99
146
|
if (added >= MAX_PER_BATCH) {
|
|
100
147
|
dropped += 1;
|
|
@@ -117,7 +164,7 @@ export function installDom(emit) {
|
|
|
117
164
|
}
|
|
118
165
|
}
|
|
119
166
|
for (const node of record.removedNodes) {
|
|
120
|
-
if (!(node
|
|
167
|
+
if (!isElement(node))
|
|
121
168
|
continue;
|
|
122
169
|
if (removed >= MAX_PER_BATCH) {
|
|
123
170
|
dropped += 1;
|
|
@@ -139,7 +186,7 @@ export function installDom(emit) {
|
|
|
139
186
|
if (dropped > 0)
|
|
140
187
|
emit(EventType.TRUNCATED, { channel: TruncationChannel.DOM, dropped });
|
|
141
188
|
});
|
|
142
|
-
|
|
189
|
+
const options = {
|
|
143
190
|
subtree: true,
|
|
144
191
|
childList: true,
|
|
145
192
|
attributes: true,
|
|
@@ -147,9 +194,20 @@ export function installDom(emit) {
|
|
|
147
194
|
attributeOldValue: true,
|
|
148
195
|
characterData: true,
|
|
149
196
|
characterDataOldValue: true,
|
|
150
|
-
}
|
|
197
|
+
};
|
|
198
|
+
observer.observe(document.documentElement, options);
|
|
199
|
+
// A MutationObserver does NOT cross a shadow boundary, so every root needs its own observation.
|
|
200
|
+
// Without this, a click that changed text inside a web component produced an empty observe window —
|
|
201
|
+
// the same shape as an app that ignored the click entirely.
|
|
202
|
+
for (const root of capturedRoots())
|
|
203
|
+
observer.observe(root, options);
|
|
204
|
+
const unsubscribe = onShadowRoot((root) => observer.observe(root, options));
|
|
205
|
+
// Same-origin frame documents are separate node trees for the same reason, with the same symptom.
|
|
206
|
+
const stopFrames = observeSameOriginFrames((body) => observer.observe(body, options));
|
|
151
207
|
return () => {
|
|
208
|
+
stopFrames();
|
|
209
|
+
unsubscribe();
|
|
210
|
+
stopRegistry();
|
|
152
211
|
observer.disconnect();
|
|
153
212
|
};
|
|
154
213
|
}
|
|
155
|
-
//# sourceMappingURL=dom.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Emit, Teardown } from './types.js';
|
|
2
|
+
export interface DownloadOptions {
|
|
3
|
+
/** Include a content preview. Gated for the same reason request bodies are: exports hold PII. */
|
|
4
|
+
capturePreview?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function installDownload(emit: Emit, opts?: DownloadOptions): Teardown;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { EventType } from '@reticlehq/core';
|
|
2
|
+
import { captureMethod } from '../patching/capture-method.js';
|
|
3
|
+
import { projectBody } from './network-body.js';
|
|
4
|
+
/**
|
|
5
|
+
* Files the app PRODUCES — the one artifact class an outside-the-browser tool cannot inspect.
|
|
6
|
+
*
|
|
7
|
+
* A generated export never crosses the network: the app builds a Blob, hands it to
|
|
8
|
+
* `URL.createObjectURL`, and clicks an anchor. There is no request to intercept, and Playwright MCP
|
|
9
|
+
* has no tool that returns a downloaded file at all. Reticle runs inside the page, where the Blob is
|
|
10
|
+
* constructed, so the export is observable before it reaches the disk.
|
|
11
|
+
*
|
|
12
|
+
* Measured on a real merchant dashboard: one Export click produced a CSV wrong three ways at once —
|
|
13
|
+
* 25 data rows in a file named `transactions-128-rows.csv`, an unquoted `3 Aug, 10:00 pm` that
|
|
14
|
+
* shifts every column after it, and an `amount_inr` header over rows denominated in USD — while the
|
|
15
|
+
* click itself reported a clean success and every other channel agreed. The benchmark's ground truth
|
|
16
|
+
* records those three as missed by BOTH toolchains, for the same reason: nobody opened the file.
|
|
17
|
+
*/
|
|
18
|
+
/** Text-like types worth counting lines in and previewing. Anything else reports type + size only. */
|
|
19
|
+
const TEXTUAL = /text\/|json|csv|xml|ndjson|javascript/i;
|
|
20
|
+
/** Lines are reported for text files; the preview is capped and redacted by projectBody. */
|
|
21
|
+
const PREVIEW_CHARS = 2000;
|
|
22
|
+
export function installDownload(emit, opts = {}) {
|
|
23
|
+
// jsdom does not implement `createObjectURL`, and plenty of users run this SDK under jsdom in their
|
|
24
|
+
// own test suite. Reaching for `.bind` on an absent function threw during `connect()` — an
|
|
25
|
+
// observability layer taking the host app down with it, which is the one failure mode it may never
|
|
26
|
+
// have. No object URLs means no generated files to observe, so this simply stands down.
|
|
27
|
+
if (typeof URL.createObjectURL !== 'function')
|
|
28
|
+
return () => undefined;
|
|
29
|
+
const objectUrl = URL.createObjectURL.bind(URL);
|
|
30
|
+
const known = new Map();
|
|
31
|
+
/**
|
|
32
|
+
* `createObjectURL` is where the file becomes real. The filename is NOT here — it lives on the
|
|
33
|
+
* anchor that gets clicked — so the blob is remembered against its URL and reported when either
|
|
34
|
+
* the anchor fires or, failing that, on a microtask so a download that is never clicked (a
|
|
35
|
+
* generated preview, an <img> src) is still visible.
|
|
36
|
+
*/
|
|
37
|
+
const originalCreate = captureMethod(URL, 'createObjectURL');
|
|
38
|
+
URL.createObjectURL = function reticleObservedObjectUrl(obj) {
|
|
39
|
+
const url = objectUrl(obj);
|
|
40
|
+
if (typeof Blob !== 'undefined' && obj instanceof Blob) {
|
|
41
|
+
// Started here, awaited at report time — never blocking the app's own download path.
|
|
42
|
+
const text = TEXTUAL.test(obj.type)
|
|
43
|
+
? obj.text().catch(() => undefined)
|
|
44
|
+
: Promise.resolve(undefined);
|
|
45
|
+
known.set(url, { type: obj.type, size: obj.size, text });
|
|
46
|
+
}
|
|
47
|
+
return url;
|
|
48
|
+
};
|
|
49
|
+
const report = (url, filename) => {
|
|
50
|
+
const record = known.get(url);
|
|
51
|
+
if (record === undefined)
|
|
52
|
+
return;
|
|
53
|
+
known.delete(url);
|
|
54
|
+
void record.text.then((text) => {
|
|
55
|
+
emitDownload(record, filename, text);
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
const emitDownload = (record, filename, text) => {
|
|
59
|
+
const data = { mimeType: record.type, bytes: record.size };
|
|
60
|
+
if (filename !== undefined && filename.length > 0)
|
|
61
|
+
data['filename'] = filename;
|
|
62
|
+
if (text !== undefined) {
|
|
63
|
+
// The LINE COUNT is reported even without preview capture: it is a number, not content, and it
|
|
64
|
+
// is the whole tell for "25 rows in a file the toast calls 128".
|
|
65
|
+
data['lines'] = text.split('\n').filter((l) => l.length > 0).length;
|
|
66
|
+
if (opts.capturePreview === true) {
|
|
67
|
+
const { body, truncated } = projectBody(text.slice(0, PREVIEW_CHARS), record.type);
|
|
68
|
+
data['preview'] = body;
|
|
69
|
+
if (truncated)
|
|
70
|
+
data['previewTruncated'] = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
emit(EventType.DOWNLOAD, data);
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* An anchor click carries the NAME. Captured at the document level so it sees the click whether the
|
|
77
|
+
* app dispatches it programmatically or a human does, and whether or not the anchor is in the DOM.
|
|
78
|
+
*/
|
|
79
|
+
const onClick = (event) => {
|
|
80
|
+
const target = event.target;
|
|
81
|
+
const anchor = target instanceof Element ? target.closest('a[download]') : null;
|
|
82
|
+
if (anchor === null)
|
|
83
|
+
return;
|
|
84
|
+
report(anchor.getAttribute('href') ?? '', anchor.getAttribute('download') ?? undefined);
|
|
85
|
+
};
|
|
86
|
+
document.addEventListener('click', onClick, true);
|
|
87
|
+
// A programmatic `a.click()` on an anchor never inserted into the document dispatches no bubbling
|
|
88
|
+
// event this listener can see, so the anchor path is patched too.
|
|
89
|
+
const originalClick = captureMethod(HTMLAnchorElement.prototype, 'click');
|
|
90
|
+
HTMLAnchorElement.prototype.click = function reticleObservedAnchorClick() {
|
|
91
|
+
if (this.hasAttribute('download')) {
|
|
92
|
+
report(this.getAttribute('href') ?? '', this.getAttribute('download') ?? undefined);
|
|
93
|
+
}
|
|
94
|
+
originalClick.call(this);
|
|
95
|
+
};
|
|
96
|
+
return () => {
|
|
97
|
+
// A REAL restore, so teardown leaves the page exactly as it was found.
|
|
98
|
+
document.removeEventListener('click', onClick, true);
|
|
99
|
+
URL.createObjectURL = originalCreate;
|
|
100
|
+
HTMLAnchorElement.prototype.click = originalClick;
|
|
101
|
+
known.clear();
|
|
102
|
+
};
|
|
103
|
+
}
|
package/dist/observers/focus.js
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Teardown } from './types.js';
|
|
2
|
+
/** Every same-origin frame body at or beneath `root`, in document order. */
|
|
3
|
+
export declare function sameOriginFrameBodies(root: ParentNode): HTMLElement[];
|
|
4
|
+
/**
|
|
5
|
+
* Call `attach` for every same-origin frame body now and after each frame load.
|
|
6
|
+
*
|
|
7
|
+
* A frame that navigates gets a BRAND NEW document, so the old observation is dead and re-attaching on
|
|
8
|
+
* `load` is not optional — without it, observation silently stops the first time an embedded panel
|
|
9
|
+
* routes. Attaching twice to the same node is harmless (MutationObserver de-duplicates), so no
|
|
10
|
+
* bookkeeping is kept.
|
|
11
|
+
*/
|
|
12
|
+
export declare function observeSameOriginFrames(attach: (body: HTMLElement) => void): Teardown;
|