@reticlehq/browser 3.8.2 → 3.10.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/action-report.d.ts +14 -0
- package/dist/actions/action-report.js +95 -22
- package/dist/actions/actions-dom.d.ts +17 -6
- package/dist/actions/actions-dom.js +98 -47
- package/dist/actions/actions-press.d.ts +10 -0
- package/dist/actions/actions-press.js +146 -2
- package/dist/actions/actions.js +64 -24
- package/dist/actions/insert-text.d.ts +3 -0
- package/dist/actions/insert-text.js +90 -0
- package/dist/actions/scroll.d.ts +1 -1
- package/dist/actions/scroll.js +25 -1
- package/dist/actions/value-input.d.ts +2 -0
- package/dist/actions/value-input.js +30 -9
- package/dist/commands/commands.js +85 -9
- package/dist/connect-options.d.ts +11 -1
- package/dist/connect-options.js +27 -1
- package/dist/dom/a11y.d.ts +1 -1
- package/dist/dom/a11y.js +11 -10
- package/dist/dom/chart-geometry.d.ts +7 -0
- package/dist/dom/chart-geometry.js +39 -0
- package/dist/dom/occlusion.js +21 -0
- package/dist/dom/query-finders.d.ts +1 -1
- package/dist/dom/query-finders.js +10 -7
- package/dist/dom/query.d.ts +6 -2
- package/dist/dom/query.js +165 -54
- package/dist/dom/scope-roots.d.ts +8 -0
- package/dist/dom/scope-roots.js +44 -0
- package/dist/dom/snapshot.d.ts +3 -0
- package/dist/dom/snapshot.js +9 -2
- package/dist/dom/text-match.js +6 -4
- package/dist/dom/theme.js +6 -7
- package/dist/dom/visibility.d.ts +3 -0
- package/dist/dom/visibility.js +125 -10
- package/dist/observers/canvas.d.ts +1 -0
- package/dist/observers/canvas.js +10 -1
- package/dist/observers/console.js +46 -35
- package/dist/observers/dom.js +28 -4
- package/dist/observers/net-detail/resource-timing.d.ts +12 -0
- package/dist/observers/net-detail/resource-timing.js +69 -0
- package/dist/observers/network.js +13 -44
- package/dist/observers/perf.js +15 -1
- package/dist/presenter/chrome/presenter-verbs.js +1 -0
- package/dist/presenter/presenter-config.d.ts +1 -0
- package/dist/presenter/presenter-config.js +1 -0
- package/dist/presenter/presenter-shell.d.ts +1 -0
- package/dist/presenter/presenter-shell.js +6 -3
- package/dist/presenter/tour/tour.js +4 -0
- package/dist/recorder/recorder.js +82 -39
- package/dist/reticle-inject.js +618 -618
- package/dist/reticle.d.ts +3 -19
- package/dist/reticle.js +76 -110
- package/dist/review/annotator.js +4 -0
- package/dist/security/serialization.js +35 -0
- package/dist/session-continuity.d.ts +1 -0
- package/dist/session-continuity.js +18 -0
- package/dist/storage-keys.js +2 -1
- package/dist/timers/native/native-timers.js +21 -4
- package/dist/transport/bridge-url.d.ts +8 -0
- package/dist/transport/bridge-url.js +52 -0
- package/package.json +10 -10
package/dist/dom/query.js
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import { ALTERNATIVE_TESTID_ATTRS, DEFAULT_TESTID_ATTR, ElementState, QueryBy, REDACTED_VALUE, TRANSPORT_LIMITS, } from '@reticlehq/core';
|
|
2
|
-
import {
|
|
2
|
+
import { isElement, isFrame } from './realm.js';
|
|
3
3
|
import { capturedRootOf } from './shadow-registry.js';
|
|
4
4
|
import { getAccessibleName, getRole, describe, getStates } from './a11y.js';
|
|
5
|
-
import { isHiddenByAriaOnly, isInViewport, isRenderedOutsideAriaHidden, isVisible, } from './visibility.js';
|
|
5
|
+
import { isHiddenByAriaOnly, isHiddenSubtree, isInViewport, isRenderedOutsideAriaHidden, isVisible, isNonRendered, } from './visibility.js';
|
|
6
6
|
import { dialogCovering, isIgnored, isReticleOverlay, isWordsIgnored } from './dom-ignore.js';
|
|
7
|
+
import { hitTestOccluder } from './occlusion.js';
|
|
7
8
|
import { isSensitiveKey } from '../security/serialization.js';
|
|
8
9
|
import { declaredTestids } from '../registry/capabilities.js';
|
|
9
10
|
import { refs } from './addressing/refs.js';
|
|
11
|
+
import { firstIn, firstListIn, hintRoots, pageContent, resolveContainers } from './scope-roots.js';
|
|
10
12
|
import { getTestIdAttr, readTestId, testIdSelector } from './addressing/testid-attr.js';
|
|
11
13
|
import { stableSelectorFor } from './stable-selector.js';
|
|
12
14
|
import { fuzzyVisibleText, normaliseVisibleText, comparableText, foldTypography, textMatches, unreadableMatch, } from './text-match.js';
|
|
13
15
|
import { pageRoute } from '../security/page-location.js';
|
|
14
|
-
import { elementsUnder, findIn,
|
|
16
|
+
import { elementsUnder, findIn, queryByRoleAndName } from './query-finders.js';
|
|
17
|
+
import { INTERACTIVE } from './snapshot.js';
|
|
15
18
|
const MAX_PRESENT_TESTIDS = 12;
|
|
16
19
|
function visibleTextOf(el, memo) {
|
|
17
|
-
if (isNonRendered(el) || isWordsIgnored(el)) {
|
|
20
|
+
if (isHiddenSubtree(el, memo) || isNonRendered(el) || isWordsIgnored(el)) {
|
|
18
21
|
return '';
|
|
19
22
|
}
|
|
20
23
|
const visible = isVisible(el, memo);
|
|
@@ -25,6 +28,9 @@ function visibleTextOf(el, memo) {
|
|
|
25
28
|
text += node.textContent ?? '';
|
|
26
29
|
}
|
|
27
30
|
else if (isElement(node)) {
|
|
31
|
+
if (isHiddenSubtree(node, memo)) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
28
34
|
const own = visibleTextOf(node, memo);
|
|
29
35
|
text += apart || ownBox(node) ? ` ${own} ` : own;
|
|
30
36
|
}
|
|
@@ -39,24 +45,6 @@ function ownBox(el) {
|
|
|
39
45
|
const display = el.ownerDocument.defaultView?.getComputedStyle(el).display ?? 'inline';
|
|
40
46
|
return !display.startsWith('inline') && 'contents' !== display;
|
|
41
47
|
}
|
|
42
|
-
function resolveContainer(scope) {
|
|
43
|
-
if (scope === undefined) {
|
|
44
|
-
return { container: document.body, scopeMissing: false };
|
|
45
|
-
}
|
|
46
|
-
const byRef = refs.resolve(scope);
|
|
47
|
-
if (isActionTarget(byRef)) {
|
|
48
|
-
return { container: byRef, scopeMissing: false };
|
|
49
|
-
}
|
|
50
|
-
try {
|
|
51
|
-
const found = document.querySelector(scope);
|
|
52
|
-
if (isActionTarget(found)) {
|
|
53
|
-
return { container: found, scopeMissing: false };
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
}
|
|
58
|
-
return { container: null, scopeMissing: true };
|
|
59
|
-
}
|
|
60
48
|
function readableFrameBody(frame) {
|
|
61
49
|
try {
|
|
62
50
|
return frame.contentDocument?.body ?? null;
|
|
@@ -66,7 +54,7 @@ function readableFrameBody(frame) {
|
|
|
66
54
|
}
|
|
67
55
|
}
|
|
68
56
|
const FRAME_DEPTH_MAX = 3;
|
|
69
|
-
function embeddedRootsUnder(root) {
|
|
57
|
+
export function embeddedRootsUnder(root) {
|
|
70
58
|
const found = [];
|
|
71
59
|
const walk = (node, depth) => {
|
|
72
60
|
for (const el of node.querySelectorAll('*')) {
|
|
@@ -94,19 +82,20 @@ function embeddedRootsUnder(root) {
|
|
|
94
82
|
return found;
|
|
95
83
|
}
|
|
96
84
|
function findCandidates(query) {
|
|
97
|
-
const {
|
|
98
|
-
|
|
99
|
-
|
|
85
|
+
const { containers, scopeMissing } = resolveContainers(query.scope);
|
|
86
|
+
const scopeMatches = containers.length;
|
|
87
|
+
if (0 === scopeMatches) {
|
|
88
|
+
return { candidates: [], scopeMissing: true, scopeMatches };
|
|
100
89
|
}
|
|
101
90
|
if (true === query.self) {
|
|
102
91
|
if (query.scope === undefined) {
|
|
103
|
-
return { candidates: [], scopeMissing };
|
|
104
|
-
}
|
|
105
|
-
if (query.text !== undefined &&
|
|
106
|
-
!textMatches(visibleTextOf(container, new Map()), query, query.text)) {
|
|
107
|
-
return { candidates: [], scopeMissing };
|
|
92
|
+
return { candidates: [], scopeMissing, scopeMatches };
|
|
108
93
|
}
|
|
109
|
-
|
|
94
|
+
const visibleMemo = new Map();
|
|
95
|
+
const selves = containers.filter((container) => !isIgnored(container) &&
|
|
96
|
+
(query.text === undefined ||
|
|
97
|
+
textMatches(visibleTextOf(container, visibleMemo), query, query.text)));
|
|
98
|
+
return { candidates: selves, scopeMissing, scopeMatches };
|
|
110
99
|
}
|
|
111
100
|
const seen = new Set();
|
|
112
101
|
const out = [];
|
|
@@ -116,6 +105,9 @@ function findCandidates(query) {
|
|
|
116
105
|
if (seen.has(el)) {
|
|
117
106
|
continue;
|
|
118
107
|
}
|
|
108
|
+
if (!pageContent(el, query.scope)) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
119
111
|
if (ignored(el)) {
|
|
120
112
|
continue;
|
|
121
113
|
}
|
|
@@ -123,11 +115,13 @@ function findCandidates(query) {
|
|
|
123
115
|
out.push(el);
|
|
124
116
|
}
|
|
125
117
|
};
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
118
|
+
for (const container of containers) {
|
|
119
|
+
collect(findIn(container, query));
|
|
120
|
+
for (const embedded of embeddedRootsUnder(container)) {
|
|
121
|
+
collect(findIn(embedded, query));
|
|
122
|
+
}
|
|
129
123
|
}
|
|
130
|
-
return { candidates: out, scopeMissing };
|
|
124
|
+
return { candidates: out, scopeMissing, scopeMatches };
|
|
131
125
|
}
|
|
132
126
|
export const ATTR_VALUE_MAX = 512;
|
|
133
127
|
const ATTR_KEYS_MAX = 12;
|
|
@@ -142,27 +136,51 @@ function projectAttrs(el, keys) {
|
|
|
142
136
|
}
|
|
143
137
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
144
138
|
}
|
|
139
|
+
function receivesPointer(el, memo) {
|
|
140
|
+
if (!isVisible(el, memo)) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
const rect = el.getBoundingClientRect();
|
|
144
|
+
if (0 === rect.width || 0 === rect.height) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
return isInViewport(el, memo) && null === hitTestOccluder(el, rect);
|
|
148
|
+
}
|
|
145
149
|
function inState(el, state, memo) {
|
|
146
150
|
if (ElementState.IN_VIEWPORT === state) {
|
|
147
151
|
return isInViewport(el, memo);
|
|
148
152
|
}
|
|
153
|
+
if (ElementState.RECEIVES_POINTER === state) {
|
|
154
|
+
return receivesPointer(el, memo);
|
|
155
|
+
}
|
|
156
|
+
if (ElementState.VISIBLE === state) {
|
|
157
|
+
return isVisible(el, memo);
|
|
158
|
+
}
|
|
159
|
+
if (ElementState.HIDDEN === state) {
|
|
160
|
+
return !isVisible(el, memo);
|
|
161
|
+
}
|
|
149
162
|
return getStates(el, isVisible(el, memo)).includes(state);
|
|
150
163
|
}
|
|
151
164
|
const MAX_DESCRIBED = TRANSPORT_LIMITS.MAX_COLLECTION_ITEMS;
|
|
152
|
-
export function matchQuery(query, state, limit = MAX_DESCRIBED, wholePhrase = true, diagnose = true) {
|
|
165
|
+
export function matchQuery(query, state, limit = MAX_DESCRIBED, wholePhrase = true, diagnose = true, fullText = false) {
|
|
153
166
|
const found = findCandidates(query);
|
|
154
167
|
const scopeMissing = found.scopeMissing;
|
|
155
168
|
const wanted = wholePhrase && textOnly(query) ? wantedTextOf(query) : undefined;
|
|
156
|
-
const
|
|
157
|
-
const owner =
|
|
169
|
+
const scopes = 0 === found.candidates.length ? resolveContainers(query.scope).containers : [];
|
|
170
|
+
const owner = wanted === undefined
|
|
171
|
+
? undefined
|
|
172
|
+
: firstIn(scopes, (scope) => splitTextOwner(scope, wanted, query));
|
|
158
173
|
const elements = owner === undefined ? found.candidates : [owner];
|
|
159
174
|
const visMemo = new Map();
|
|
160
175
|
const filtered = state === undefined ? elements : elements.filter((el) => inState(el, state, visMemo));
|
|
161
176
|
const attrs = query.attrs;
|
|
162
|
-
const
|
|
177
|
+
const cap = fullText
|
|
178
|
+
? Math.min(MAX_DESCRIBED, TRANSPORT_LIMITS.MAX_FULL_TEXT_ELEMENTS)
|
|
179
|
+
: MAX_DESCRIBED;
|
|
180
|
+
const described = filtered.slice(0, Math.max(0, Math.min(limit, cap)));
|
|
163
181
|
const stampViewport = described.length > 1 || ElementState.IN_VIEWPORT === state;
|
|
164
182
|
const descriptors = described.map((el) => {
|
|
165
|
-
let base = describe(el, visMemo);
|
|
183
|
+
let base = describe(el, visMemo, fullText);
|
|
166
184
|
if (stampViewport &&
|
|
167
185
|
isInViewport(el, visMemo) &&
|
|
168
186
|
!base.states.includes(ElementState.IN_VIEWPORT)) {
|
|
@@ -180,7 +198,9 @@ export function matchQuery(query, state, limit = MAX_DESCRIBED, wholePhrase = tr
|
|
|
180
198
|
matched: filtered.length > 0,
|
|
181
199
|
count: filtered.length,
|
|
182
200
|
elements: descriptors,
|
|
201
|
+
...(fullText ? { fullText: true } : {}),
|
|
183
202
|
...(scopeMissing ? { scopeMissing: true } : {}),
|
|
203
|
+
...(found.scopeMatches > 1 ? { scopeMatches: found.scopeMatches } : {}),
|
|
184
204
|
...(covered === undefined ? {} : { coveredBy: covered }),
|
|
185
205
|
...(unreadable === undefined ? {} : { unreadable }),
|
|
186
206
|
...(0 === filtered.length ? { hint: buildEmptyHint(query, state, diagnose) } : {}),
|
|
@@ -199,7 +219,7 @@ function resolveLabelledBy(el) {
|
|
|
199
219
|
return text.length > 0 ? text : undefined;
|
|
200
220
|
}
|
|
201
221
|
function buildPresentRegions(query) {
|
|
202
|
-
const
|
|
222
|
+
const roots = hintRoots(query.scope);
|
|
203
223
|
const regions = [];
|
|
204
224
|
const CONTAINER_ROLES = [
|
|
205
225
|
'list',
|
|
@@ -220,7 +240,7 @@ function buildPresentRegions(query) {
|
|
|
220
240
|
'tablist',
|
|
221
241
|
];
|
|
222
242
|
for (const role of CONTAINER_ROLES) {
|
|
223
|
-
const containers = queryByRoleAndName(
|
|
243
|
+
const containers = roots.flatMap((root) => queryByRoleAndName(root, role, undefined));
|
|
224
244
|
for (const el of containers) {
|
|
225
245
|
if (isReticleOverlay(el)) {
|
|
226
246
|
continue;
|
|
@@ -264,6 +284,9 @@ function splitTextOwner(container, wanted, query) {
|
|
|
264
284
|
if (isIgnored(el)) {
|
|
265
285
|
continue;
|
|
266
286
|
}
|
|
287
|
+
if (!pageContent(el, query.scope)) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
267
290
|
if (!textMatches(visibleTextOf(el, memo), query, wanted)) {
|
|
268
291
|
continue;
|
|
269
292
|
}
|
|
@@ -278,7 +301,7 @@ function splitTextOwner(container, wanted, query) {
|
|
|
278
301
|
}
|
|
279
302
|
return best;
|
|
280
303
|
}
|
|
281
|
-
function caseOnlyMatch(container, wanted) {
|
|
304
|
+
function caseOnlyMatch(container, wanted, scope) {
|
|
282
305
|
const target = comparableText(wanted).toLowerCase();
|
|
283
306
|
if (0 === target.length) {
|
|
284
307
|
return undefined;
|
|
@@ -288,6 +311,9 @@ function caseOnlyMatch(container, wanted) {
|
|
|
288
311
|
if (isIgnored(el)) {
|
|
289
312
|
continue;
|
|
290
313
|
}
|
|
314
|
+
if (!pageContent(el, scope)) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
291
317
|
const text = normaliseVisibleText(visibleTextOf(el, memo));
|
|
292
318
|
const at = foldTypography(text).toLowerCase().indexOf(target);
|
|
293
319
|
if (-1 !== at) {
|
|
@@ -346,6 +372,81 @@ function nameNearMisses(container, query) {
|
|
|
346
372
|
}
|
|
347
373
|
return out;
|
|
348
374
|
}
|
|
375
|
+
const MAX_NEAREST_TARGETS = 3;
|
|
376
|
+
export const MAX_NEAREST_NAME = 60;
|
|
377
|
+
function bigrams(value) {
|
|
378
|
+
const padded = ` ${value} `;
|
|
379
|
+
const out = new Set();
|
|
380
|
+
for (let i = 0; i < padded.length - 1; i += 1) {
|
|
381
|
+
out.add(padded.slice(i, i + 2));
|
|
382
|
+
}
|
|
383
|
+
return out;
|
|
384
|
+
}
|
|
385
|
+
export function nameSimilarity(a, b) {
|
|
386
|
+
const left = bigrams(foldTypography(a).toLowerCase());
|
|
387
|
+
const right = bigrams(foldTypography(b).toLowerCase());
|
|
388
|
+
let shared = 0;
|
|
389
|
+
for (const gram of left) {
|
|
390
|
+
if (right.has(gram)) {
|
|
391
|
+
shared += 1;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return (2 * shared) / (left.size + right.size);
|
|
395
|
+
}
|
|
396
|
+
function askedRole(query) {
|
|
397
|
+
return QueryBy.ROLE === query.by ? query.value : query.role;
|
|
398
|
+
}
|
|
399
|
+
function askedName(query) {
|
|
400
|
+
const value = QueryBy.ROLE === query.by ? undefined : query.value;
|
|
401
|
+
const asked = query.name ??
|
|
402
|
+
value ??
|
|
403
|
+
query.text ??
|
|
404
|
+
query.label ??
|
|
405
|
+
query.placeholder ??
|
|
406
|
+
query.testid ??
|
|
407
|
+
query.alt;
|
|
408
|
+
return asked === undefined ? '' : normaliseVisibleText(asked);
|
|
409
|
+
}
|
|
410
|
+
export function nearestTargets(container, query) {
|
|
411
|
+
const role = askedRole(query);
|
|
412
|
+
const name = askedName(query);
|
|
413
|
+
if (role === undefined && 0 === name.length) {
|
|
414
|
+
return [];
|
|
415
|
+
}
|
|
416
|
+
const memo = new Map();
|
|
417
|
+
const scored = [];
|
|
418
|
+
for (const el of elementsUnder(container)) {
|
|
419
|
+
const elRole = getRole(el);
|
|
420
|
+
const roleMatch = role !== undefined && elRole === role;
|
|
421
|
+
if (!roleMatch && !INTERACTIVE.has(elRole)) {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (isIgnored(el) || !isVisible(el, memo)) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const elName = normaliseVisibleText(getAccessibleName(el));
|
|
428
|
+
if (0 === elName.length) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const score = 0 === name.length ? 0 : nameSimilarity(name, elName);
|
|
432
|
+
if (!roleMatch && 0 === score) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
scored.push({ el, role: elRole, name: elName, roleMatch, score });
|
|
436
|
+
}
|
|
437
|
+
scored.sort((a, b) => Number(b.roleMatch) - Number(a.roleMatch) || b.score - a.score);
|
|
438
|
+
return scored.slice(0, MAX_NEAREST_TARGETS).map((c) => {
|
|
439
|
+
const target = {
|
|
440
|
+
ref: refs.refFor(c.el),
|
|
441
|
+
role: c.role,
|
|
442
|
+
name: c.name.slice(0, MAX_NEAREST_NAME),
|
|
443
|
+
};
|
|
444
|
+
if (c.name.length > MAX_NEAREST_NAME) {
|
|
445
|
+
target.nameTruncated = true;
|
|
446
|
+
}
|
|
447
|
+
return target;
|
|
448
|
+
});
|
|
449
|
+
}
|
|
349
450
|
function testidFoundUnder(container, query) {
|
|
350
451
|
if (QueryBy.TESTID !== query.by || undefined === query.value) {
|
|
351
452
|
return undefined;
|
|
@@ -394,14 +495,17 @@ function textInAriaHidden(container, wanted) {
|
|
|
394
495
|
return true;
|
|
395
496
|
}
|
|
396
497
|
function buildEmptyHint(query, state, diagnose = true) {
|
|
397
|
-
const
|
|
398
|
-
const all =
|
|
498
|
+
const roots = hintRoots(query.scope);
|
|
499
|
+
const all = roots.flatMap((root) => Array.from(root.querySelectorAll(testIdSelector())));
|
|
399
500
|
const present = [];
|
|
400
501
|
const seen = new Set();
|
|
401
|
-
for (const el of
|
|
502
|
+
for (const el of all) {
|
|
402
503
|
if (isIgnored(el)) {
|
|
403
504
|
continue;
|
|
404
505
|
}
|
|
506
|
+
if (!pageContent(el, query.scope)) {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
405
509
|
const id = readTestId(el);
|
|
406
510
|
if (null === id || 0 === id.length || seen.has(id)) {
|
|
407
511
|
continue;
|
|
@@ -423,14 +527,14 @@ function buildEmptyHint(query, state, diagnose = true) {
|
|
|
423
527
|
};
|
|
424
528
|
const wanted = wantedTextOf(query);
|
|
425
529
|
if (wanted !== undefined) {
|
|
426
|
-
const owner = splitTextOwner(
|
|
530
|
+
const owner = firstIn(roots, (root) => splitTextOwner(root, wanted, query));
|
|
427
531
|
if (owner !== undefined) {
|
|
428
532
|
const described = describe(owner);
|
|
429
533
|
const selector = stableSelectorFor(owner);
|
|
430
534
|
hint.splitText = selector === undefined ? described : { ...described, selector };
|
|
431
535
|
}
|
|
432
536
|
else if (true === query.matchCase) {
|
|
433
|
-
const shown = caseOnlyMatch(
|
|
537
|
+
const shown = firstIn(roots, (root) => caseOnlyMatch(root, wanted, query.scope));
|
|
434
538
|
if (shown !== undefined) {
|
|
435
539
|
hint.caseMiss = shown;
|
|
436
540
|
}
|
|
@@ -440,18 +544,22 @@ function buildEmptyHint(query, state, diagnose = true) {
|
|
|
440
544
|
wanted !== undefined &&
|
|
441
545
|
hint.splitText === undefined &&
|
|
442
546
|
(state === undefined || ElementState.VISIBLE === state || ElementState.IN_VIEWPORT === state)) {
|
|
443
|
-
if (textInAriaHidden(
|
|
547
|
+
if (roots.some((root) => textInAriaHidden(root, wanted))) {
|
|
444
548
|
hint.ariaHiddenMatch = true;
|
|
445
549
|
}
|
|
446
550
|
}
|
|
447
|
-
const near = nameNearMisses(
|
|
551
|
+
const near = firstListIn(roots, (root) => nameNearMisses(root, query));
|
|
448
552
|
if (near.length > 0) {
|
|
449
553
|
hint.nameNearMiss = near;
|
|
450
554
|
}
|
|
451
|
-
const elsewhere = testidFoundUnder(
|
|
555
|
+
const elsewhere = firstIn(roots, (root) => testidFoundUnder(root, query));
|
|
452
556
|
if (elsewhere !== undefined) {
|
|
453
557
|
hint.testidFoundUnder = elsewhere;
|
|
454
558
|
}
|
|
559
|
+
const nearest = diagnose ? firstListIn(roots, (root) => nearestTargets(root, query)) : [];
|
|
560
|
+
if (nearest.length > 0) {
|
|
561
|
+
hint.nearest = nearest;
|
|
562
|
+
}
|
|
455
563
|
return hint;
|
|
456
564
|
}
|
|
457
565
|
export function elementsMatching(query) {
|
|
@@ -459,12 +567,15 @@ export function elementsMatching(query) {
|
|
|
459
567
|
}
|
|
460
568
|
export function runQuery(query, limit) {
|
|
461
569
|
const result = matchQuery(query, undefined, limit, false);
|
|
462
|
-
const scopeFields =
|
|
570
|
+
const scopeFields = {
|
|
571
|
+
...(true === result.scopeMissing ? { scopeMissing: true } : {}),
|
|
572
|
+
...(result.scopeMatches === undefined ? {} : { scopeMatches: result.scopeMatches }),
|
|
573
|
+
};
|
|
463
574
|
if (0 === result.elements.length) {
|
|
464
575
|
return {
|
|
465
576
|
elements: result.elements,
|
|
466
577
|
count: result.count,
|
|
467
|
-
hint: buildEmptyHint(query),
|
|
578
|
+
hint: result.hint ?? buildEmptyHint(query),
|
|
468
579
|
...scopeFields,
|
|
469
580
|
};
|
|
470
581
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function resolveContainers(scope: string | undefined): {
|
|
2
|
+
containers: HTMLElement[];
|
|
3
|
+
scopeMissing: boolean;
|
|
4
|
+
};
|
|
5
|
+
export declare function hintRoots(scope: string | undefined): HTMLElement[];
|
|
6
|
+
export declare function firstIn<T>(roots: HTMLElement[], find: (root: HTMLElement) => T | undefined): T | undefined;
|
|
7
|
+
export declare function firstListIn<T>(roots: HTMLElement[], find: (root: HTMLElement) => T[]): T[];
|
|
8
|
+
export declare function pageContent(el: Element, scope: string | undefined): boolean;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { isActionTarget } from './realm.js';
|
|
2
|
+
import { refs } from './addressing/refs.js';
|
|
3
|
+
export function resolveContainers(scope) {
|
|
4
|
+
let containers = [];
|
|
5
|
+
if (scope === undefined) {
|
|
6
|
+
containers = [document.documentElement];
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
const byRef = refs.resolve(scope);
|
|
10
|
+
if (isActionTarget(byRef)) {
|
|
11
|
+
containers = [byRef];
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
try {
|
|
15
|
+
containers = Array.from(document.querySelectorAll(scope)).filter(isActionTarget);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return { containers, scopeMissing: 0 === containers.length };
|
|
22
|
+
}
|
|
23
|
+
export function hintRoots(scope) {
|
|
24
|
+
const { containers } = resolveContainers(scope);
|
|
25
|
+
return containers.length > 0 ? containers : [document.body];
|
|
26
|
+
}
|
|
27
|
+
export function firstIn(roots, find) {
|
|
28
|
+
for (const root of roots) {
|
|
29
|
+
const found = find(root);
|
|
30
|
+
if (found !== undefined) {
|
|
31
|
+
return found;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
export function firstListIn(roots, find) {
|
|
37
|
+
return (firstIn(roots, (root) => {
|
|
38
|
+
const found = find(root);
|
|
39
|
+
return found.length > 0 ? found : undefined;
|
|
40
|
+
}) ?? []);
|
|
41
|
+
}
|
|
42
|
+
export function pageContent(el, scope) {
|
|
43
|
+
return scope !== undefined || !el.closest('head');
|
|
44
|
+
}
|
package/dist/dom/snapshot.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SnapshotMode } from '@reticlehq/core';
|
|
2
2
|
import { type LayoutFinding } from './layout-audit.js';
|
|
3
|
+
import { type ChartReading } from './chart-geometry.js';
|
|
3
4
|
export declare const INTERACTIVE: Set<string>;
|
|
4
5
|
interface SnapshotStatus {
|
|
5
6
|
route: string;
|
|
@@ -14,6 +15,7 @@ interface SnapshotResult {
|
|
|
14
15
|
truncated: boolean;
|
|
15
16
|
scopeMissing?: boolean;
|
|
16
17
|
audit?: LayoutFinding[];
|
|
18
|
+
charts?: ChartReading[];
|
|
17
19
|
leanSkipped?: number;
|
|
18
20
|
hiddenSkipped?: number;
|
|
19
21
|
domElements?: number;
|
|
@@ -28,6 +30,7 @@ interface SnapshotOptions {
|
|
|
28
30
|
includeRoot?: boolean | undefined;
|
|
29
31
|
ids?: boolean | undefined;
|
|
30
32
|
audit?: boolean | undefined;
|
|
33
|
+
charts?: boolean | undefined;
|
|
31
34
|
}
|
|
32
35
|
export declare const MAX_UNREAD_BRANCHES = 50;
|
|
33
36
|
export declare function buildSnapshot(options?: SnapshotOptions): SnapshotResult;
|
package/dist/dom/snapshot.js
CHANGED
|
@@ -7,6 +7,7 @@ import { refs } from './addressing/refs.js';
|
|
|
7
7
|
import { readTestId } from './addressing/testid-attr.js';
|
|
8
8
|
import { identifyComponent } from '../registry/stores/adapters.js';
|
|
9
9
|
import { auditLayout } from './layout-audit.js';
|
|
10
|
+
import { readCharts } from './chart-geometry.js';
|
|
10
11
|
import { pageRoute } from '../security/page-location.js';
|
|
11
12
|
import { isIgnored, isReticleOverlay } from './dom-ignore.js';
|
|
12
13
|
export const INTERACTIVE = new Set([
|
|
@@ -40,6 +41,10 @@ function directText(el) {
|
|
|
40
41
|
const collapsed = (parts.own ? parts.text : '').replace(/\s+/g, ' ').trim();
|
|
41
42
|
return collapsed.length > TEXT_MAX ? `${collapsed.slice(0, TEXT_MAX)}…` : collapsed;
|
|
42
43
|
}
|
|
44
|
+
const foldText = (s) => s.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
45
|
+
function textBesideName(own, name) {
|
|
46
|
+
return 0 < name.length && foldText(own) === foldText(name) ? '' : own;
|
|
47
|
+
}
|
|
43
48
|
function readingParts(el) {
|
|
44
49
|
let text = '';
|
|
45
50
|
let own = false;
|
|
@@ -193,7 +198,8 @@ function visit(child, depth, ctx, inLive) {
|
|
|
193
198
|
const interactive = INTERACTIVE.has(role);
|
|
194
199
|
const announce = inLive || announces(child, role);
|
|
195
200
|
const lean = ctx.mode === SnapshotMode.INTERACTIVE && !announce;
|
|
196
|
-
const
|
|
201
|
+
const ownText = lean || (interactive && 0 < name.length) ? '' : directText(child);
|
|
202
|
+
const text = textBesideName(ownText, name);
|
|
197
203
|
const layout = lean ? '' : layoutSignature(style);
|
|
198
204
|
const actionable = interactive;
|
|
199
205
|
const meaningful = actionable || role !== 'generic' || name.length > 0 || text.length > 0 || layout.length > 0;
|
|
@@ -203,7 +209,7 @@ function visit(child, depth, ctx, inLive) {
|
|
|
203
209
|
}
|
|
204
210
|
if (include) {
|
|
205
211
|
ctx.nodes += 1;
|
|
206
|
-
ctx.lines.push('generic' === role && text.length > 0 && 0 === layout.length
|
|
212
|
+
ctx.lines.push('generic' === role && 0 === name.length && text.length > 0 && 0 === layout.length
|
|
207
213
|
? formatTextLine(depth, text)
|
|
208
214
|
: formatLine(child, depth, role, name, layout, text, true === ctx.ids));
|
|
209
215
|
walk(child, depth + 1, ctx, announce);
|
|
@@ -352,5 +358,6 @@ export function buildSnapshot(options = {}) {
|
|
|
352
358
|
...(ctx.unread.length > 0 ? { unread: ctx.unread } : {}),
|
|
353
359
|
...(ctx.unreadOverflow ? { unreadOverflow: true } : {}),
|
|
354
360
|
...(true === options.audit ? { audit: auditLayout(root) } : {}),
|
|
361
|
+
...(true === options.charts ? { charts: readCharts(root) } : {}),
|
|
355
362
|
};
|
|
356
363
|
}
|
package/dist/dom/text-match.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { parentAcrossShadowBoundary } from './shadow-registry.js';
|
|
2
|
+
import { stalledFade } from './visibility.js';
|
|
2
3
|
export function normaliseVisibleText(value) {
|
|
3
4
|
return value.replace(/\s+/g, ' ').trim().normalize('NFC');
|
|
4
5
|
}
|
|
5
|
-
const SINGLE_QUOTES = /[\u2018
|
|
6
|
-
const DOUBLE_QUOTES = /[\u201c
|
|
6
|
+
const SINGLE_QUOTES = /[\u2018-\u201b]/g;
|
|
7
|
+
const DOUBLE_QUOTES = /[\u201c-\u201f]/g;
|
|
7
8
|
const DASHES = /[\u2013\u2014]/g;
|
|
8
9
|
export function foldTypography(value) {
|
|
9
10
|
return value.replace(SINGLE_QUOTES, "'").replace(DOUBLE_QUOTES, '"').replace(DASHES, '-');
|
|
10
11
|
}
|
|
12
|
+
const INVISIBLE_FORMAT = /[\u00ad\u200b-\u200f\u202a-\u202e\u2060-\u2064\u2066-\u2069\ufeff]/g;
|
|
11
13
|
export function comparableText(value) {
|
|
12
|
-
return foldTypography(normaliseVisibleText(value));
|
|
14
|
+
return foldTypography(normaliseVisibleText(value.replace(INVISIBLE_FORMAT, '')));
|
|
13
15
|
}
|
|
14
16
|
export function fuzzyVisibleText(actual, expected) {
|
|
15
17
|
return comparableText(actual).toLowerCase().includes(comparableText(expected).toLowerCase());
|
|
@@ -81,7 +83,7 @@ export function unreadableText(el) {
|
|
|
81
83
|
let opaque = false;
|
|
82
84
|
for (let node = el; null !== node; node = parentAcrossShadowBoundary(node)) {
|
|
83
85
|
const style = view.getComputedStyle(node);
|
|
84
|
-
const alpha = Number.parseFloat(style.opacity);
|
|
86
|
+
const alpha = stalledFade(node) ? 1 : Number.parseFloat(style.opacity);
|
|
85
87
|
opacity *= Number.isNaN(alpha) ? 1 : alpha;
|
|
86
88
|
if (opaque) {
|
|
87
89
|
continue;
|
package/dist/dom/theme.js
CHANGED
|
@@ -34,19 +34,18 @@ export function tokenNames() {
|
|
|
34
34
|
}
|
|
35
35
|
return names;
|
|
36
36
|
}
|
|
37
|
-
function palette() {
|
|
37
|
+
function palette(cs) {
|
|
38
38
|
const byColor = new Map();
|
|
39
|
-
const
|
|
40
|
-
if (null ===
|
|
39
|
+
const host = document.body ?? document.documentElement;
|
|
40
|
+
if (null === host)
|
|
41
41
|
return byColor;
|
|
42
|
-
const rootStyle = getComputedStyle(root);
|
|
43
42
|
const probe = document.createElement('span');
|
|
44
43
|
probe.style.position = 'absolute';
|
|
45
44
|
probe.style.pointerEvents = 'none';
|
|
46
|
-
|
|
45
|
+
host.appendChild(probe);
|
|
47
46
|
try {
|
|
48
47
|
for (const name of tokenNames()) {
|
|
49
|
-
const rgb = toRgb(probe,
|
|
48
|
+
const rgb = toRgb(probe, cs.getPropertyValue(name).trim());
|
|
50
49
|
if (null === rgb)
|
|
51
50
|
continue;
|
|
52
51
|
const named = byColor.get(rgb);
|
|
@@ -90,7 +89,7 @@ function sole(names) {
|
|
|
90
89
|
return 1 === names.length ? (names[0] ?? null) : null;
|
|
91
90
|
}
|
|
92
91
|
export function themeReport(cs) {
|
|
93
|
-
const byColor = palette();
|
|
92
|
+
const byColor = palette(cs);
|
|
94
93
|
const colorTokens = byColor.get(cs.color) ?? [];
|
|
95
94
|
const backgroundTokens = byColor.get(cs.backgroundColor) ?? [];
|
|
96
95
|
const colorOff = !isTransparent(cs.color) && 0 === colorTokens.length;
|
package/dist/dom/visibility.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
export declare const HTML_DETAILS_TAG = "details";
|
|
2
2
|
export declare const HTML_DETAILS_OPEN_ATTRIBUTE = "open";
|
|
3
3
|
export declare const HTML_SUMMARY_TAG = "summary";
|
|
4
|
+
export declare function stalledFade(el: Element): boolean;
|
|
5
|
+
export declare function isNonRendered(el: Element): boolean;
|
|
4
6
|
export declare function isHiddenByAriaOnly(el: Element): boolean;
|
|
5
7
|
export declare function isRenderedOutsideAriaHidden(el: Element): boolean;
|
|
6
8
|
export declare function isInViewport(el: Element, memo?: Map<Element, boolean>): boolean;
|
|
9
|
+
export declare function isHiddenSubtree(el: Element, memo?: Map<Element, boolean>): boolean;
|
|
7
10
|
export declare function isVisible(el: Element, memo?: Map<Element, boolean>): boolean;
|