@reticlehq/browser 3.7.0 → 3.8.1
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/action-report.d.ts +10 -0
- package/dist/actions/action-report.js +59 -14
- package/dist/actions/actions-dom.d.ts +4 -0
- package/dist/actions/actions-dom.js +20 -4
- package/dist/actions/actions.d.ts +1 -0
- package/dist/actions/actions.js +94 -46
- package/dist/actions/appeared-text.js +2 -2
- package/dist/actions/click-target.d.ts +1 -0
- package/dist/actions/click-target.js +14 -0
- package/dist/actions/danger-context.d.ts +2 -1
- package/dist/actions/danger-context.js +43 -17
- package/dist/actions/scroll.d.ts +2 -0
- package/dist/actions/scroll.js +10 -2
- package/dist/activate.d.ts +14 -0
- package/dist/activate.js +43 -0
- package/dist/busy.d.ts +3 -0
- package/dist/busy.js +42 -0
- package/dist/commands/commands.js +45 -17
- package/dist/connect-options.d.ts +12 -1
- package/dist/connect-options.js +18 -1
- package/dist/dom/a11y.d.ts +0 -2
- package/dist/dom/a11y.js +103 -363
- package/dist/dom/chart.d.ts +3 -2
- package/dist/dom/chart.js +3 -2
- package/dist/dom/dom-ignore.d.ts +6 -0
- package/dist/dom/dom-ignore.js +60 -5
- package/dist/dom/layout-audit.d.ts +15 -0
- package/dist/dom/layout-audit.js +208 -0
- package/dist/dom/paint-context.d.ts +15 -0
- package/dist/dom/paint-context.js +78 -0
- package/dist/dom/query-finders.d.ts +6 -0
- package/dist/dom/query-finders.js +216 -0
- package/dist/dom/query.d.ts +1 -1
- package/dist/dom/query.js +154 -227
- package/dist/dom/shadow-registry.d.ts +1 -0
- package/dist/dom/shadow-registry.js +27 -0
- package/dist/dom/snapshot.d.ts +5 -0
- package/dist/dom/snapshot.js +87 -35
- package/dist/dom/stable-selector.d.ts +1 -0
- package/dist/dom/stable-selector.js +19 -0
- package/dist/dom/text-match.d.ts +12 -0
- package/dist/dom/text-match.js +115 -0
- package/dist/dom/theme.d.ts +1 -0
- package/dist/dom/theme.js +21 -9
- package/dist/dom/virtualized.js +5 -1
- package/dist/dom/visibility.d.ts +7 -0
- package/dist/dom/visibility.js +322 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +7 -1
- package/dist/observers/blind-spots.js +3 -2
- package/dist/observers/canvas.d.ts +18 -0
- package/dist/observers/canvas.js +76 -0
- package/dist/observers/dom.js +68 -19
- package/dist/observers/field.d.ts +11 -0
- package/dist/observers/field.js +117 -13
- package/dist/observers/install-all.js +4 -0
- package/dist/observers/media.d.ts +2 -0
- package/dist/observers/media.js +32 -0
- package/dist/observers/net-detail/network-body.d.ts +2 -0
- package/dist/observers/net-detail/network-body.js +36 -9
- package/dist/observers/net-detail/network-event-stream.d.ts +5 -0
- package/dist/observers/net-detail/network-event-stream.js +55 -0
- package/dist/observers/net-detail/network-frames.d.ts +2 -0
- package/dist/observers/net-detail/network-frames.js +39 -0
- package/dist/observers/net-detail/network-response-body.d.ts +3 -0
- package/dist/observers/net-detail/network-response-body.js +97 -0
- package/dist/observers/network.js +111 -101
- package/dist/observers/route.d.ts +2 -0
- package/dist/observers/route.js +33 -4
- package/dist/presenter/chrome/presenter-report-copy.d.ts +0 -5
- package/dist/presenter/chrome/presenter-report-copy.js +0 -14
- package/dist/presenter/hud-telemetry.js +1 -1
- package/dist/presenter/presenter-chat-views.d.ts +3 -4
- package/dist/presenter/presenter-chat-views.js +47 -20
- package/dist/presenter/presenter-config.d.ts +1 -0
- package/dist/presenter/presenter-config.js +1 -0
- package/dist/presenter/presenter-frame.d.ts +3 -2
- package/dist/presenter/presenter-frame.js +24 -15
- package/dist/presenter/presenter-harness-row.d.ts +4 -15
- package/dist/presenter/presenter-harness-row.js +11 -50
- package/dist/presenter/presenter-report-styles.js +0 -17
- package/dist/presenter/presenter-report.d.ts +1 -2
- package/dist/presenter/presenter-report.js +4 -54
- package/dist/presenter/presenter-run-state.js +2 -1
- package/dist/presenter/presenter-settings.d.ts +2 -3
- package/dist/presenter/presenter-settings.js +14 -17
- package/dist/presenter/presenter-shell.d.ts +6 -4
- package/dist/presenter/presenter-shell.js +37 -7
- package/dist/presenter/presenter.js +32 -11
- package/dist/registry/store-adapters.d.ts +5 -2
- package/dist/registry/store-adapters.js +40 -10
- package/dist/reticle-inject.js +700 -645
- package/dist/reticle.d.ts +4 -2
- package/dist/reticle.js +36 -6
- package/dist/security/page-location.d.ts +2 -0
- package/dist/security/page-location.js +7 -0
- package/dist/security/serialization.js +1 -1
- package/dist/storage-keys.d.ts +1 -0
- package/dist/storage-keys.js +1 -0
- package/dist/timers/native/native-timers.d.ts +1 -0
- package/dist/timers/native/native-timers.js +5 -0
- package/dist/transport/transport.d.ts +1 -0
- package/dist/transport/transport.js +18 -0
- package/package.json +6 -2
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { isHtmlElement } from './realm.js';
|
|
2
|
+
import { parentAcrossShadowBoundary } from './shadow-registry.js';
|
|
3
|
+
import { clippingBox, overflowAxes, scrolledInto, } from './paint-context.js';
|
|
4
|
+
export const HTML_DETAILS_TAG = 'details';
|
|
5
|
+
export const HTML_DETAILS_OPEN_ATTRIBUTE = 'open';
|
|
6
|
+
export const HTML_SUMMARY_TAG = 'summary';
|
|
7
|
+
function hiddenInsideClosedDetails(el) {
|
|
8
|
+
const parent = el.parentElement;
|
|
9
|
+
if (null === parent) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
const details = parent.closest(HTML_DETAILS_TAG);
|
|
13
|
+
if (null === details || details.hasAttribute(HTML_DETAILS_OPEN_ATTRIBUTE)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const summary = Array.from(details.children).find((child) => HTML_SUMMARY_TAG === child.localName);
|
|
17
|
+
return summary === undefined || !summary.contains(el);
|
|
18
|
+
}
|
|
19
|
+
function stalledFade(el) {
|
|
20
|
+
if (!el.ownerDocument.hidden || !('getAnimations' in el)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return el.getAnimations().some((a) => 'running' === a.playState || a.pending);
|
|
24
|
+
}
|
|
25
|
+
const OVERLAY_ARIA_HIDDEN = 'data-aria-hidden';
|
|
26
|
+
function hiddenByAnOverlay(el) {
|
|
27
|
+
return el.hasAttribute(OVERLAY_ARIA_HIDDEN);
|
|
28
|
+
}
|
|
29
|
+
function selfCssHidden(el, style) {
|
|
30
|
+
if (isHtmlElement(el) && el.hidden) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
if (hiddenInsideClosedDetails(el)) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
if (style !== null) {
|
|
37
|
+
if ('none' === style.display ||
|
|
38
|
+
'hidden' === style.visibility ||
|
|
39
|
+
'collapse' === style.visibility) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
if (0 === Number.parseFloat(style.opacity || '1') && !stalledFade(el)) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
if (clippedAway(style)) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
function clippedAway(style) {
|
|
52
|
+
const rect = /^rect\((.*)\)$/.exec(style.getPropertyValue('clip').trim());
|
|
53
|
+
if (null !== rect && ('absolute' === style.position || 'fixed' === style.position)) {
|
|
54
|
+
const [top, right, bottom, left] = (rect[1] ?? '').split(/[\s,]+/).map(Number.parseFloat);
|
|
55
|
+
if (right !== undefined && left !== undefined && right <= left) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
if (bottom !== undefined && top !== undefined && bottom <= top) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const inset = /^inset\(([^)]*)\)$/.exec(style.getPropertyValue('clip-path').trim());
|
|
63
|
+
const edges = inset?.[1]
|
|
64
|
+
?.split(/\s+round\s/)[0]
|
|
65
|
+
?.trim()
|
|
66
|
+
.split(/\s+/) ?? [];
|
|
67
|
+
const known = (edge) => edge.endsWith('%') || 0 === Number.parseFloat(edge);
|
|
68
|
+
if (0 === edges.length || !edges.every(known)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const [top = 0, right = top, bottom = top, left = right] = edges.map(Number.parseFloat);
|
|
72
|
+
return top + bottom >= 100 || left + right >= 100;
|
|
73
|
+
}
|
|
74
|
+
function selfHidden(el, style) {
|
|
75
|
+
if ('true' === el.getAttribute('aria-hidden') && !hiddenByAnOverlay(el)) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return selfCssHidden(el, style);
|
|
79
|
+
}
|
|
80
|
+
export function isHiddenByAriaOnly(el) {
|
|
81
|
+
let foundAriaHidden = false;
|
|
82
|
+
let current = el;
|
|
83
|
+
while (null !== current) {
|
|
84
|
+
if ('true' === current.getAttribute('aria-hidden')) {
|
|
85
|
+
foundAriaHidden = true;
|
|
86
|
+
}
|
|
87
|
+
const style = current.ownerDocument.defaultView?.getComputedStyle(current) ?? null;
|
|
88
|
+
if (selfCssHidden(current, style)) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
current = parentAcrossShadowBoundary(current);
|
|
92
|
+
}
|
|
93
|
+
return foundAriaHidden;
|
|
94
|
+
}
|
|
95
|
+
export function isRenderedOutsideAriaHidden(el) {
|
|
96
|
+
let current = el;
|
|
97
|
+
while (null !== current) {
|
|
98
|
+
if ('true' === current.getAttribute('aria-hidden')) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
const style = current.ownerDocument.defaultView?.getComputedStyle(current) ?? null;
|
|
102
|
+
if (selfCssHidden(current, style)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
current = parentAcrossShadowBoundary(current);
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
const CLIPPING_OVERFLOW = new Set(['hidden', 'clip']);
|
|
110
|
+
const SCROLLING_OVERFLOW = new Set(['auto', 'scroll']);
|
|
111
|
+
const NON_CLIPPING_DISPLAY = new Set([
|
|
112
|
+
'contents',
|
|
113
|
+
'table-row',
|
|
114
|
+
'table-row-group',
|
|
115
|
+
'table-header-group',
|
|
116
|
+
'table-footer-group',
|
|
117
|
+
'table-column',
|
|
118
|
+
'table-column-group',
|
|
119
|
+
]);
|
|
120
|
+
const TRANSFORM_CONTAINING_BLOCK_PROPERTIES = new Set([
|
|
121
|
+
'transform',
|
|
122
|
+
'translate',
|
|
123
|
+
'rotate',
|
|
124
|
+
'scale',
|
|
125
|
+
'perspective',
|
|
126
|
+
]);
|
|
127
|
+
const CONTAINING_BLOCK_PROPERTIES = new Set([
|
|
128
|
+
...TRANSFORM_CONTAINING_BLOCK_PROPERTIES,
|
|
129
|
+
'filter',
|
|
130
|
+
'backdrop-filter',
|
|
131
|
+
]);
|
|
132
|
+
const CONTAINING_BLOCK_CONTAIN = new Set(['layout', 'paint', 'strict', 'content']);
|
|
133
|
+
const REPLACED_INLINE_TAGS = new Set([
|
|
134
|
+
'img',
|
|
135
|
+
'video',
|
|
136
|
+
'audio',
|
|
137
|
+
'canvas',
|
|
138
|
+
'iframe',
|
|
139
|
+
'embed',
|
|
140
|
+
'object',
|
|
141
|
+
'input',
|
|
142
|
+
'textarea',
|
|
143
|
+
'select',
|
|
144
|
+
]);
|
|
145
|
+
const visibilityInfoMemos = new WeakMap();
|
|
146
|
+
function visibilityInfoMemo(memo) {
|
|
147
|
+
if (memo === undefined) {
|
|
148
|
+
return new Map();
|
|
149
|
+
}
|
|
150
|
+
let info = visibilityInfoMemos.get(memo);
|
|
151
|
+
if (info === undefined) {
|
|
152
|
+
info = new Map();
|
|
153
|
+
visibilityInfoMemos.set(memo, info);
|
|
154
|
+
}
|
|
155
|
+
return info;
|
|
156
|
+
}
|
|
157
|
+
function ownVisibilityInfo(el, memo) {
|
|
158
|
+
let info = memo.get(el);
|
|
159
|
+
if (info === undefined) {
|
|
160
|
+
const style = el.ownerDocument.defaultView?.getComputedStyle(el) ?? null;
|
|
161
|
+
if (selfHidden(el, style)) {
|
|
162
|
+
info = {
|
|
163
|
+
hidden: true,
|
|
164
|
+
style,
|
|
165
|
+
parent: null,
|
|
166
|
+
clipX: false,
|
|
167
|
+
clipY: false,
|
|
168
|
+
scrollX: false,
|
|
169
|
+
scrollY: false,
|
|
170
|
+
hasClipping: false,
|
|
171
|
+
};
|
|
172
|
+
memo.set(el, info);
|
|
173
|
+
return info;
|
|
174
|
+
}
|
|
175
|
+
const parent = parentAcrossShadowBoundary(el);
|
|
176
|
+
const inherited = null === parent ? undefined : ownVisibilityInfo(parent, memo);
|
|
177
|
+
const [overflowX, overflowY] = overflowAxes(style);
|
|
178
|
+
const root = el.ownerDocument.documentElement;
|
|
179
|
+
const viewportOverflow = el === root ||
|
|
180
|
+
(el === el.ownerDocument.body &&
|
|
181
|
+
overflowAxes(ownVisibilityInfo(root, memo).style).every((axis) => 'visible' === axis));
|
|
182
|
+
const hasClipBox = !viewportOverflow &&
|
|
183
|
+
!NON_CLIPPING_DISPLAY.has(style?.display ?? '') &&
|
|
184
|
+
!(isHtmlElement(el) && 'inline' === style?.display);
|
|
185
|
+
const clipX = hasClipBox && CLIPPING_OVERFLOW.has(overflowX);
|
|
186
|
+
const clipY = hasClipBox && CLIPPING_OVERFLOW.has(overflowY);
|
|
187
|
+
info = {
|
|
188
|
+
hidden: true === inherited?.hidden,
|
|
189
|
+
style,
|
|
190
|
+
parent,
|
|
191
|
+
clipX,
|
|
192
|
+
clipY,
|
|
193
|
+
scrollX: hasClipBox && SCROLLING_OVERFLOW.has(overflowX),
|
|
194
|
+
scrollY: hasClipBox && SCROLLING_OVERFLOW.has(overflowY),
|
|
195
|
+
hasClipping: clipX || clipY || true === inherited?.hasClipping,
|
|
196
|
+
};
|
|
197
|
+
memo.set(el, info);
|
|
198
|
+
}
|
|
199
|
+
return info;
|
|
200
|
+
}
|
|
201
|
+
function positionedContainingBlock(el, fixed, memo) {
|
|
202
|
+
for (let parent = ownVisibilityInfo(el, memo).parent; parent !== null; parent = ownVisibilityInfo(parent, memo).parent) {
|
|
203
|
+
const style = ownVisibilityInfo(parent, memo).style;
|
|
204
|
+
if (null === style || 'contents' === style.display) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (!fixed && '' !== style.position && 'static' !== style.position) {
|
|
208
|
+
return parent;
|
|
209
|
+
}
|
|
210
|
+
const transformable = !('inline' === style.display &&
|
|
211
|
+
isHtmlElement(parent) &&
|
|
212
|
+
!REPLACED_INLINE_TAGS.has(parent.localName)) &&
|
|
213
|
+
'table-column' !== style.display &&
|
|
214
|
+
'table-column-group' !== style.display;
|
|
215
|
+
const applies = (property) => transformable || !TRANSFORM_CONTAINING_BLOCK_PROPERTIES.has(property);
|
|
216
|
+
if ([...CONTAINING_BLOCK_PROPERTIES].some((property) => {
|
|
217
|
+
const value = style.getPropertyValue(property);
|
|
218
|
+
return applies(property) && '' !== value && 'none' !== value;
|
|
219
|
+
}) ||
|
|
220
|
+
style.willChange
|
|
221
|
+
.split(',')
|
|
222
|
+
.some((property) => CONTAINING_BLOCK_PROPERTIES.has(property.trim()) && applies(property.trim())) ||
|
|
223
|
+
(transformable &&
|
|
224
|
+
!NON_CLIPPING_DISPLAY.has(style.display) &&
|
|
225
|
+
(style.contain.split(/\s+/).some((value) => CONTAINING_BLOCK_CONTAIN.has(value)) ||
|
|
226
|
+
'auto' === style.contentVisibility))) {
|
|
227
|
+
return parent;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
function visibleBounds(el, memo, initial, throughScrollers = false) {
|
|
233
|
+
let bounds = initial;
|
|
234
|
+
const target = ownVisibilityInfo(el, memo);
|
|
235
|
+
if (target.hidden) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
if (null === target.parent || !ownVisibilityInfo(target.parent, memo).hasClipping) {
|
|
239
|
+
return bounds;
|
|
240
|
+
}
|
|
241
|
+
let clipFrom;
|
|
242
|
+
for (let current = el; current !== null; current = ownVisibilityInfo(current, memo).parent) {
|
|
243
|
+
const info = ownVisibilityInfo(current, memo);
|
|
244
|
+
if (info.hidden) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
if (current === clipFrom) {
|
|
248
|
+
clipFrom = undefined;
|
|
249
|
+
}
|
|
250
|
+
const style = info.style;
|
|
251
|
+
if (throughScrollers &&
|
|
252
|
+
current !== el &&
|
|
253
|
+
undefined === clipFrom &&
|
|
254
|
+
(info.scrollX || info.scrollY)) {
|
|
255
|
+
bounds ??= el.getBoundingClientRect();
|
|
256
|
+
if (bounds.width > 0 && bounds.height > 0) {
|
|
257
|
+
bounds = scrolledInto(bounds, clippingBox(current, info), info.scrollX, info.scrollY);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (current !== el && undefined === clipFrom && (info.clipX || info.clipY)) {
|
|
261
|
+
bounds ??= el.getBoundingClientRect();
|
|
262
|
+
if (bounds.width > 0 && bounds.height > 0) {
|
|
263
|
+
const clip = clippingBox(current, info);
|
|
264
|
+
const left = info.clipX ? Math.max(bounds.left, clip.left) : bounds.left;
|
|
265
|
+
const right = info.clipX ? Math.min(bounds.right, clip.right) : bounds.right;
|
|
266
|
+
const top = info.clipY ? Math.max(bounds.top, clip.top) : bounds.top;
|
|
267
|
+
const bottom = info.clipY ? Math.min(bounds.bottom, clip.bottom) : bounds.bottom;
|
|
268
|
+
if (right <= left || bottom <= top) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
bounds = { left, right, top, bottom, width: right - left, height: bottom - top };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (clipFrom === undefined && ('absolute' === style?.position || 'fixed' === style?.position)) {
|
|
275
|
+
clipFrom = positionedContainingBlock(current, 'fixed' === style?.position, memo);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return bounds;
|
|
279
|
+
}
|
|
280
|
+
export function isInViewport(el, memo) {
|
|
281
|
+
if (!isVisible(el, memo)) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
const view = el.ownerDocument.defaultView;
|
|
285
|
+
if (null === view) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
const r = visibleBounds(el, visibilityInfoMemo(memo), el.getBoundingClientRect());
|
|
289
|
+
if (null === r || undefined === r) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
if (r.width <= 0 || r.height <= 0) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
return r.bottom > 0 && r.right > 0 && r.top < view.innerHeight && r.left < view.innerWidth;
|
|
296
|
+
}
|
|
297
|
+
function collapsed(el, memo) {
|
|
298
|
+
const own = ownVisibilityInfo(el, memo);
|
|
299
|
+
if (!own.clipX && !own.clipY) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
const rects = Array.from(el.getClientRects());
|
|
303
|
+
return (rects.length > 0 &&
|
|
304
|
+
rects.every((r) => (own.clipX && 0 === r.width) ||
|
|
305
|
+
(own.clipY && 0 === r.height) ||
|
|
306
|
+
(own.clipX && own.clipY && r.width <= 1 && r.height <= 1)));
|
|
307
|
+
}
|
|
308
|
+
export function isVisible(el, memo) {
|
|
309
|
+
if (!el.isConnected) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
const cached = memo?.get(el);
|
|
313
|
+
if (cached !== undefined) {
|
|
314
|
+
return cached;
|
|
315
|
+
}
|
|
316
|
+
const info = visibilityInfoMemo(memo);
|
|
317
|
+
const result = visibleBounds(el, info, undefined, true) !== null && !collapsed(el, info);
|
|
318
|
+
if (memo !== undefined) {
|
|
319
|
+
memo.set(el, result);
|
|
320
|
+
}
|
|
321
|
+
return result;
|
|
322
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -18,10 +18,12 @@ export { registerReticleDomain } from './registry/domains.js';
|
|
|
18
18
|
export type { ReticleDomain } from './registry/domains.js';
|
|
19
19
|
export { buildSnapshot } from './dom/snapshot.js';
|
|
20
20
|
export { inspectChart, canvasChartData } from './dom/chart.js';
|
|
21
|
-
export type { ChartReport, ChartFinding, CanvasChartData } from './dom/chart.js';
|
|
21
|
+
export type { ChartReport, ChartFinding, CanvasChartData, ChartGlobals } from './dom/chart.js';
|
|
22
22
|
export { matchQuery, runQuery } from './dom/query.js';
|
|
23
23
|
export { executeAction, executeSequence } from './actions/actions.js';
|
|
24
|
-
export { describe, getRole, getAccessibleName, getStates
|
|
24
|
+
export { describe, getRole, getAccessibleName, getStates } from './dom/a11y.js';
|
|
25
|
+
export { isVisible } from './dom/visibility.js';
|
|
25
26
|
export { refs, RefRegistry } from './dom/addressing/refs.js';
|
|
26
27
|
export { Annotator, installAnnotator, type AnnotatorDeps } from './review/annotator.js';
|
|
27
28
|
export { resolveMarkAnchor, type MarkAnchor } from './review/mark-anchor.js';
|
|
29
|
+
export declare const trackShortTimers: (maxMs?: number) => (() => void);
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { reticle } from './reticle-singleton.js';
|
|
2
|
+
import { reticle } from './reticle-singleton.js';
|
|
3
|
+
import { trackShortTimers as trackTimers } from './busy.js';
|
|
2
4
|
export { Reticle } from './reticle.js';
|
|
3
5
|
export { SESSION_AUTO } from '@reticlehq/core';
|
|
4
6
|
export { setIgnoreSelectors } from './dom/dom-ignore.js';
|
|
@@ -13,7 +15,11 @@ export { buildSnapshot } from './dom/snapshot.js';
|
|
|
13
15
|
export { inspectChart, canvasChartData } from './dom/chart.js';
|
|
14
16
|
export { matchQuery, runQuery } from './dom/query.js';
|
|
15
17
|
export { executeAction, executeSequence } from './actions/actions.js';
|
|
16
|
-
export { describe, getRole, getAccessibleName, getStates
|
|
18
|
+
export { describe, getRole, getAccessibleName, getStates } from './dom/a11y.js';
|
|
19
|
+
export { isVisible } from './dom/visibility.js';
|
|
17
20
|
export { refs, RefRegistry } from './dom/addressing/refs.js';
|
|
18
21
|
export { Annotator, installAnnotator } from './review/annotator.js';
|
|
19
22
|
export { resolveMarkAnchor } from './review/mark-anchor.js';
|
|
23
|
+
export const trackShortTimers = (maxMs) => trackTimers((name, data) => {
|
|
24
|
+
reticle.signal(name, data);
|
|
25
|
+
}, maxMs);
|
|
@@ -58,10 +58,11 @@ export function installBlindSpots(emit) {
|
|
|
58
58
|
const report = () => {
|
|
59
59
|
for (const sensor of sensors) {
|
|
60
60
|
const count = sensor.count();
|
|
61
|
-
|
|
61
|
+
const previous = last.get(sensor.kind);
|
|
62
|
+
if (count === previous)
|
|
62
63
|
continue;
|
|
63
64
|
last.set(sensor.kind, count);
|
|
64
|
-
if (count > 0)
|
|
65
|
+
if (count > 0 || previous !== undefined)
|
|
65
66
|
emit(EventType.BLIND_SPOT, { kind: sensor.kind, count });
|
|
66
67
|
}
|
|
67
68
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { CanvasTrigger } from '@reticlehq/core';
|
|
2
|
+
export { CanvasTrigger };
|
|
3
|
+
import { type Emit, type Teardown } from './types.js';
|
|
4
|
+
export interface CanvasSample {
|
|
5
|
+
data: Uint8ClampedArray;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function digestPixels(data: Uint8ClampedArray): {
|
|
10
|
+
digest: string;
|
|
11
|
+
mean: [number, number, number];
|
|
12
|
+
};
|
|
13
|
+
interface CanvasOptions {
|
|
14
|
+
sample?: (canvas: HTMLCanvasElement) => CanvasSample | undefined;
|
|
15
|
+
every?: (run: () => void, ms: number) => () => void;
|
|
16
|
+
}
|
|
17
|
+
export declare function canvasAfterAct(): void;
|
|
18
|
+
export declare function installCanvas(emit: Emit, opts?: CanvasOptions): Teardown;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { CanvasTrigger, EventType } from '@reticlehq/core';
|
|
2
|
+
export { CanvasTrigger };
|
|
3
|
+
import { refs } from '../dom/addressing/refs.js';
|
|
4
|
+
import { nativeSetInterval } from '../timers/native/native-timers.js';
|
|
5
|
+
import { observeSafely, observeValue } from './types.js';
|
|
6
|
+
const SIDE = 16;
|
|
7
|
+
const POLL_MS = 500;
|
|
8
|
+
const MAX_CANVASES = 4;
|
|
9
|
+
const FNV_OFFSET = 0x811c9dc5;
|
|
10
|
+
const FNV_PRIME = 0x01000193;
|
|
11
|
+
const QUANTUM = 8;
|
|
12
|
+
export function digestPixels(data) {
|
|
13
|
+
let hash = FNV_OFFSET;
|
|
14
|
+
const sum = [0, 0, 0];
|
|
15
|
+
const pixels = Math.max(1, data.length / 4);
|
|
16
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
17
|
+
for (let c = 0; c < 3; c += 1) {
|
|
18
|
+
const value = data[i + c] ?? 0;
|
|
19
|
+
sum[c] = (sum[c] ?? 0) + value;
|
|
20
|
+
hash = Math.imul(hash ^ Math.floor(value / QUANTUM), FNV_PRIME) >>> 0;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const mean = sum.map((s) => Math.round(s / pixels));
|
|
24
|
+
return { digest: hash.toString(16).padStart(8, '0'), mean };
|
|
25
|
+
}
|
|
26
|
+
function sampleCanvas(canvas) {
|
|
27
|
+
if (0 === canvas.width || 0 === canvas.height) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const scratch = document.createElement('canvas');
|
|
31
|
+
scratch.width = SIDE;
|
|
32
|
+
scratch.height = SIDE;
|
|
33
|
+
const ctx = scratch.getContext('2d');
|
|
34
|
+
if (null === ctx) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
const data = observeValue(() => {
|
|
38
|
+
ctx.drawImage(canvas, 0, 0, SIDE, SIDE);
|
|
39
|
+
return ctx.getImageData(0, 0, SIDE, SIDE).data;
|
|
40
|
+
});
|
|
41
|
+
return data === undefined ? undefined : { data, width: canvas.width, height: canvas.height };
|
|
42
|
+
}
|
|
43
|
+
let sampleAll;
|
|
44
|
+
export function canvasAfterAct() {
|
|
45
|
+
sampleAll?.(CanvasTrigger.ACT);
|
|
46
|
+
}
|
|
47
|
+
export function installCanvas(emit, opts = {}) {
|
|
48
|
+
const sample = opts.sample ?? sampleCanvas;
|
|
49
|
+
const every = opts.every ?? nativeSetInterval;
|
|
50
|
+
const last = new WeakMap();
|
|
51
|
+
const look = (trigger) => {
|
|
52
|
+
observeSafely(() => {
|
|
53
|
+
const canvases = Array.from(document.querySelectorAll('canvas')).slice(0, MAX_CANVASES);
|
|
54
|
+
for (const canvas of canvases) {
|
|
55
|
+
const read = sample(canvas);
|
|
56
|
+
if (read === undefined) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const { digest, mean } = digestPixels(read.data);
|
|
60
|
+
if (CanvasTrigger.POLL === trigger && last.get(canvas) === digest) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
last.set(canvas, digest);
|
|
64
|
+
emit(EventType.CANVAS_DIGEST, { digest, width: read.width, height: read.height, mean, trigger }, refs.refFor(canvas));
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
sampleAll = look;
|
|
69
|
+
const stop = every(() => look(CanvasTrigger.POLL), POLL_MS);
|
|
70
|
+
return () => {
|
|
71
|
+
stop();
|
|
72
|
+
if (sampleAll === look) {
|
|
73
|
+
sampleAll = undefined;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
package/dist/observers/dom.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { readTestId, testIdSelector } from '../dom/addressing/testid-attr.js';
|
|
2
2
|
import { EventType, REDACTED_VALUE, TruncationChannel } from '@reticlehq/core';
|
|
3
|
-
import { getAccessibleName, getRole, isSensitiveField
|
|
3
|
+
import { getAccessibleName, getRole, isSensitiveField } from '../dom/a11y.js';
|
|
4
|
+
import { isVisible } from '../dom/visibility.js';
|
|
4
5
|
import { refs } from '../dom/addressing/refs.js';
|
|
5
6
|
function regionKeyOf(target) {
|
|
6
7
|
const el = isElement(target) ? target : null;
|
|
7
|
-
if (null === el)
|
|
8
|
+
if (null === el) {
|
|
8
9
|
return undefined;
|
|
10
|
+
}
|
|
9
11
|
const labelled = el.closest(testIdSelector());
|
|
10
12
|
return (labelled ? readTestId(labelled) : null) ?? refs.refFor(el);
|
|
11
13
|
}
|
|
@@ -30,8 +32,9 @@ const WATCHED_ATTRS = [
|
|
|
30
32
|
];
|
|
31
33
|
const MAX_ATTR_VALUE_LEN = 120;
|
|
32
34
|
function capValue(value) {
|
|
33
|
-
if (null === value)
|
|
35
|
+
if (null === value) {
|
|
34
36
|
return undefined;
|
|
37
|
+
}
|
|
35
38
|
return value.length > MAX_ATTR_VALUE_LEN ? `${value.slice(0, MAX_ATTR_VALUE_LEN)}…` : value;
|
|
36
39
|
}
|
|
37
40
|
const DIALOG_ROLES = new Set(['dialog', 'alertdialog']);
|
|
@@ -44,19 +47,23 @@ const MOUNT_SCAN_LIMIT = 40;
|
|
|
44
47
|
function describeMount(node) {
|
|
45
48
|
const role = getRole(node);
|
|
46
49
|
const name = getAccessibleName(node);
|
|
47
|
-
if (isMeaningful(role, name))
|
|
50
|
+
if (isMeaningful(role, name)) {
|
|
48
51
|
return { role, name, el: node };
|
|
52
|
+
}
|
|
49
53
|
let scanned = 0;
|
|
50
54
|
for (const child of node.querySelectorAll('*')) {
|
|
51
|
-
if (scanned >= MOUNT_SCAN_LIMIT)
|
|
55
|
+
if (scanned >= MOUNT_SCAN_LIMIT) {
|
|
52
56
|
return undefined;
|
|
57
|
+
}
|
|
53
58
|
scanned += 1;
|
|
54
|
-
if (isUnobserved(child))
|
|
59
|
+
if (isUnobserved(child)) {
|
|
55
60
|
continue;
|
|
61
|
+
}
|
|
56
62
|
const childRole = getRole(child);
|
|
57
63
|
const childName = getAccessibleName(child);
|
|
58
|
-
if (isMeaningful(childRole, childName))
|
|
64
|
+
if (isMeaningful(childRole, childName)) {
|
|
59
65
|
return { role: childRole, name: childName, el: child };
|
|
66
|
+
}
|
|
60
67
|
}
|
|
61
68
|
return undefined;
|
|
62
69
|
}
|
|
@@ -64,8 +71,9 @@ const TEXT_NODE = 3;
|
|
|
64
71
|
function textOf(nodes) {
|
|
65
72
|
let out = '';
|
|
66
73
|
for (const node of nodes) {
|
|
67
|
-
if (node.nodeType !== TEXT_NODE)
|
|
74
|
+
if (node.nodeType !== TEXT_NODE) {
|
|
68
75
|
continue;
|
|
76
|
+
}
|
|
69
77
|
out += node.nodeValue ?? '';
|
|
70
78
|
}
|
|
71
79
|
const trimmed = out.trim();
|
|
@@ -83,12 +91,30 @@ const SAFE_ON_SECRET = new Set([
|
|
|
83
91
|
function secretAttr(el, attr) {
|
|
84
92
|
return !SAFE_ON_SECRET.has(attr) && !attr.startsWith('aria-') && isSensitiveField(el);
|
|
85
93
|
}
|
|
94
|
+
const TESTID_DEPTH = 3;
|
|
95
|
+
const MAX_INITIAL_TEXTS = 50;
|
|
96
|
+
function testidNear(el) {
|
|
97
|
+
let at = el;
|
|
98
|
+
for (let depth = 0; at !== null && depth <= TESTID_DEPTH; depth += 1) {
|
|
99
|
+
const id = readTestId(at);
|
|
100
|
+
if (id !== null && id.length > 0) {
|
|
101
|
+
return id;
|
|
102
|
+
}
|
|
103
|
+
at = at.parentElement;
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
const named = (el) => {
|
|
108
|
+
const testid = testidNear(el);
|
|
109
|
+
return testid === undefined ? {} : { testid };
|
|
110
|
+
};
|
|
86
111
|
export function installDom(emit) {
|
|
87
112
|
const stopRegistry = installShadowRegistry();
|
|
88
113
|
const observer = new MutationObserver((records) => {
|
|
89
114
|
let added = 0;
|
|
90
115
|
let removed = 0;
|
|
91
116
|
let changed = 0;
|
|
117
|
+
let initials = 0;
|
|
92
118
|
let dropped = 0;
|
|
93
119
|
for (const record of records) {
|
|
94
120
|
if ('attributes' === record.type) {
|
|
@@ -123,36 +149,54 @@ export function installDom(emit) {
|
|
|
123
149
|
? REDACTED_VALUE
|
|
124
150
|
: (record.target.textContent ?? '').trim().slice(0, 80);
|
|
125
151
|
const old = hide ? undefined : capValue(record.oldValue?.trim() ?? null);
|
|
126
|
-
emit(EventType.DOM_TEXT, { text, ...(old === undefined ? {} : { old }) }, refs.refFor(parent));
|
|
152
|
+
emit(EventType.DOM_TEXT, { text, ...(old === undefined ? {} : { old }), ...named(parent) }, refs.refFor(parent));
|
|
127
153
|
}
|
|
128
154
|
continue;
|
|
129
155
|
}
|
|
130
156
|
if (textOf(record.addedNodes) !== undefined || textOf(record.removedNodes) !== undefined) {
|
|
131
157
|
const parent = isElement(record.target) ? record.target : null;
|
|
132
158
|
if (parent !== null && !isUnobserved(parent)) {
|
|
133
|
-
if (changed >= MAX_PER_BATCH)
|
|
159
|
+
if (changed >= MAX_PER_BATCH) {
|
|
134
160
|
dropped += 1;
|
|
161
|
+
}
|
|
135
162
|
else {
|
|
136
163
|
changed += 1;
|
|
137
164
|
const hide = isSensitiveField(parent);
|
|
138
165
|
const text = hide ? REDACTED_VALUE : (parent.textContent ?? '').trim().slice(0, 80);
|
|
139
166
|
const old = hide ? undefined : capValue(textOf(record.removedNodes) ?? null);
|
|
140
|
-
emit(EventType.DOM_TEXT, { text, ...(old === undefined ? {} : { old }) }, refs.refFor(parent));
|
|
167
|
+
emit(EventType.DOM_TEXT, { text, ...(old === undefined ? {} : { old }), ...named(parent) }, refs.refFor(parent));
|
|
141
168
|
}
|
|
142
169
|
}
|
|
143
170
|
}
|
|
144
171
|
for (const node of record.addedNodes) {
|
|
145
|
-
if (!isElement(node))
|
|
172
|
+
if (!isElement(node)) {
|
|
146
173
|
continue;
|
|
174
|
+
}
|
|
147
175
|
if (added >= MAX_PER_BATCH) {
|
|
148
176
|
dropped += 1;
|
|
149
177
|
continue;
|
|
150
178
|
}
|
|
151
|
-
if (isUnobserved(node))
|
|
179
|
+
if (isUnobserved(node)) {
|
|
152
180
|
continue;
|
|
181
|
+
}
|
|
182
|
+
const testids = [node, ...Array.from(node.querySelectorAll(testIdSelector()))].filter((el) => readTestId(el) !== null);
|
|
183
|
+
for (const el of testids) {
|
|
184
|
+
const text = (el.textContent ?? '').trim();
|
|
185
|
+
if (0 === text.length) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (initials >= MAX_INITIAL_TEXTS) {
|
|
189
|
+
dropped += 1;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
initials += 1;
|
|
193
|
+
const shown = isSensitiveField(el) ? REDACTED_VALUE : text.slice(0, 80);
|
|
194
|
+
emit(EventType.DOM_TEXT, { text: shown, testid: readTestId(el) ?? '', initial: true }, refs.refFor(el));
|
|
195
|
+
}
|
|
153
196
|
const mounted = describeMount(node);
|
|
154
|
-
if (mounted === undefined)
|
|
197
|
+
if (mounted === undefined) {
|
|
155
198
|
continue;
|
|
199
|
+
}
|
|
156
200
|
const { role, name } = mounted;
|
|
157
201
|
added += 1;
|
|
158
202
|
const ref = refs.refFor(mounted.el);
|
|
@@ -160,13 +204,15 @@ export function installDom(emit) {
|
|
|
160
204
|
if (DIALOG_ROLES.has(role) ||
|
|
161
205
|
LIVE_ROLES.has(role) ||
|
|
162
206
|
'true' === node.getAttribute('aria-modal')) {
|
|
163
|
-
if (isVisible(node))
|
|
207
|
+
if (isVisible(node)) {
|
|
164
208
|
emit(EventType.VISIBLE_SHOWN, { role, name }, ref);
|
|
209
|
+
}
|
|
165
210
|
}
|
|
166
211
|
}
|
|
167
212
|
for (const node of record.removedNodes) {
|
|
168
|
-
if (!isElement(node))
|
|
213
|
+
if (!isElement(node)) {
|
|
169
214
|
continue;
|
|
215
|
+
}
|
|
170
216
|
if (removed >= MAX_PER_BATCH) {
|
|
171
217
|
dropped += 1;
|
|
172
218
|
continue;
|
|
@@ -175,15 +221,17 @@ export function installDom(emit) {
|
|
|
175
221
|
continue;
|
|
176
222
|
}
|
|
177
223
|
const gone = describeMount(node);
|
|
178
|
-
if (gone === undefined)
|
|
224
|
+
if (gone === undefined) {
|
|
179
225
|
continue;
|
|
226
|
+
}
|
|
180
227
|
const { role, name } = gone;
|
|
181
228
|
removed += 1;
|
|
182
229
|
emit(EventType.DOM_REMOVED, { role, name, region: regionKeyOf(record.target) });
|
|
183
230
|
}
|
|
184
231
|
}
|
|
185
|
-
if (dropped > 0)
|
|
232
|
+
if (dropped > 0) {
|
|
186
233
|
emit(EventType.TRUNCATED, { channel: TruncationChannel.DOM, dropped });
|
|
234
|
+
}
|
|
187
235
|
});
|
|
188
236
|
const options = {
|
|
189
237
|
subtree: true,
|
|
@@ -195,8 +243,9 @@ export function installDom(emit) {
|
|
|
195
243
|
characterDataOldValue: true,
|
|
196
244
|
};
|
|
197
245
|
observer.observe(document.documentElement, options);
|
|
198
|
-
for (const root of capturedRoots())
|
|
246
|
+
for (const root of capturedRoots()) {
|
|
199
247
|
observer.observe(root, options);
|
|
248
|
+
}
|
|
200
249
|
const unsubscribe = onShadowRoot((root) => observer.observe(root, options));
|
|
201
250
|
const stopFrames = observeSameOriginFrames((body) => observer.observe(body, options));
|
|
202
251
|
return () => {
|
|
@@ -1,2 +1,13 @@
|
|
|
1
1
|
import type { Emit, Teardown } from './types.js';
|
|
2
|
+
export interface FieldReading {
|
|
3
|
+
value: string;
|
|
4
|
+
checked?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface ActedField {
|
|
7
|
+
before: FieldReading;
|
|
8
|
+
stop: () => void;
|
|
9
|
+
written: () => boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function watchActedField(el: Element): ActedField | undefined;
|
|
12
|
+
export declare function publishSettledField(el: Element, acted: ActedField | undefined): void;
|
|
2
13
|
export declare function installField(emit: Emit): Teardown;
|