@reticlehq/browser 3.9.0 → 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 +16 -6
- package/dist/actions/actions-dom.js +94 -46
- package/dist/actions/actions-press.d.ts +10 -0
- package/dist/actions/actions-press.js +146 -2
- package/dist/actions/actions.js +52 -22
- 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 +82 -6
- package/dist/connect-options.d.ts +11 -1
- package/dist/connect-options.js +27 -1
- package/dist/dom/a11y.js +4 -3
- 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 +1 -0
- package/dist/dom/query.js +78 -51
- 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 +4 -3
- package/dist/dom/theme.js +6 -7
- package/dist/dom/visibility.d.ts +2 -0
- package/dist/dom/visibility.js +40 -8
- 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 +12 -45
- 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 +597 -597
- package/dist/reticle.d.ts +3 -19
- package/dist/reticle.js +74 -108
- package/dist/security/serialization.js +35 -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
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export const CHART_SHAPE_CAP = 120;
|
|
2
|
+
const MIN_CHART_WIDTH = 120;
|
|
3
|
+
const MIN_CHART_HEIGHT = 60;
|
|
4
|
+
const SHAPE_TAGS = ['rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'path'];
|
|
5
|
+
const NOT_DRAWN = 'defs, clipPath, mask, pattern, marker, symbol';
|
|
6
|
+
const pct = (v, of) => {
|
|
7
|
+
const n = Math.round((v / of) * 1000) / 10;
|
|
8
|
+
return Object.is(n, -0) ? '0' : String(n);
|
|
9
|
+
};
|
|
10
|
+
export function readCharts(root) {
|
|
11
|
+
const out = [];
|
|
12
|
+
const svgs = Array.from(root.querySelectorAll('svg')).filter((s) => null === s.parentElement?.closest('svg'));
|
|
13
|
+
for (const svg of svgs) {
|
|
14
|
+
const frame = svg.getBoundingClientRect();
|
|
15
|
+
if (frame.width < MIN_CHART_WIDTH || frame.height < MIN_CHART_HEIGHT) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const marks = Array.from(svg.querySelectorAll(SHAPE_TAGS.join(','))).filter((el) => null === el.closest(NOT_DRAWN));
|
|
19
|
+
const shapes = [];
|
|
20
|
+
for (const el of marks) {
|
|
21
|
+
const b = el.getBoundingClientRect();
|
|
22
|
+
if (0 === b.width && 0 === b.height) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const x = pct(b.left - frame.left, frame.width);
|
|
26
|
+
const y = pct(b.top - frame.top, frame.height);
|
|
27
|
+
shapes.push(`${el.tagName.toLowerCase()} ${x} ${y} ${pct(b.width, frame.width)} ${pct(b.height, frame.height)}`);
|
|
28
|
+
}
|
|
29
|
+
if (0 === shapes.length) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const label = svg.getAttribute('aria-label') ?? `chart ${String(out.length + 1)}`;
|
|
33
|
+
const kept = shapes.slice(0, CHART_SHAPE_CAP);
|
|
34
|
+
out.push(shapes.length > CHART_SHAPE_CAP
|
|
35
|
+
? { label, shapes: kept, cut: shapes.length - CHART_SHAPE_CAP }
|
|
36
|
+
: { label, shapes: kept });
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
package/dist/dom/occlusion.js
CHANGED
|
@@ -13,6 +13,10 @@ export function hitTestOccluder(el, rect) {
|
|
|
13
13
|
const doc = el.ownerDocument;
|
|
14
14
|
if (typeof doc.elementFromPoint !== 'function')
|
|
15
15
|
return null;
|
|
16
|
+
const passThrough = pointerPassesThrough(el, rect);
|
|
17
|
+
if (passThrough !== undefined) {
|
|
18
|
+
return passThrough;
|
|
19
|
+
}
|
|
16
20
|
const withStack = doc;
|
|
17
21
|
const isOursButNotTheTarget = (candidate) => isReticleUi(candidate) &&
|
|
18
22
|
candidate !== el &&
|
|
@@ -41,3 +45,20 @@ export function hitTestOccluder(el, rect) {
|
|
|
41
45
|
}
|
|
42
46
|
return worstCount >= BLOCKED_MAJORITY ? worst : null;
|
|
43
47
|
}
|
|
48
|
+
function pointerPassesThrough(el, rect) {
|
|
49
|
+
const view = el.ownerDocument.defaultView;
|
|
50
|
+
if (null === view || 'none' !== view.getComputedStyle(el).pointerEvents) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const top = el.ownerDocument.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2);
|
|
54
|
+
if (null === top || el.contains(top) || isReticleUi(top)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const labels = 'labels' in el ? el.labels : null;
|
|
58
|
+
for (const label of labels ?? []) {
|
|
59
|
+
if (label === top || label.contains(top)) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return top;
|
|
64
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ElementQuery } from '@reticlehq/core';
|
|
2
2
|
export declare function elementsUnder(container: HTMLElement): HTMLElement[];
|
|
3
|
-
export declare function
|
|
3
|
+
export declare function ownWords(el: Element): string;
|
|
4
4
|
export declare function isHiddenNativeOption(el: HTMLElement): boolean;
|
|
5
5
|
export declare function queryByRoleAndName(container: HTMLElement, role: string, name: string | undefined): HTMLElement[];
|
|
6
6
|
export declare function findIn(container: HTMLElement, query: ElementQuery): HTMLElement[];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DATA_RETICLE_SOURCE_ATTR, QueryBy } from '@reticlehq/core';
|
|
2
2
|
import { isInput, isSelect, isTextArea } from './realm.js';
|
|
3
3
|
import { getAccessibleName, getRole } from './a11y.js';
|
|
4
|
-
import { isVisible } from './visibility.js';
|
|
4
|
+
import { isNonRendered, isVisible } from './visibility.js';
|
|
5
5
|
import { identifyComponent } from '../registry/stores/adapters.js';
|
|
6
6
|
import { readTestId, testIdSelector } from './addressing/testid-attr.js';
|
|
7
7
|
import { exactVisibleText, fuzzyVisibleText, comparableText, paintedCase, textMatches, } from './text-match.js';
|
|
@@ -12,10 +12,6 @@ export function elementsUnder(container) {
|
|
|
12
12
|
const self = Node.ELEMENT_NODE === container.nodeType ? [container] : [];
|
|
13
13
|
return [...self, ...Array.from(container.querySelectorAll('*'))];
|
|
14
14
|
}
|
|
15
|
-
const NON_RENDERED_TAGS = ['script', 'style', 'template', 'noscript'];
|
|
16
|
-
export function isNonRendered(el) {
|
|
17
|
-
return NON_RENDERED_TAGS.includes(el.tagName.toLowerCase());
|
|
18
|
-
}
|
|
19
15
|
function directText(el) {
|
|
20
16
|
if (isInput(el)) {
|
|
21
17
|
const type = el.type.toLowerCase();
|
|
@@ -67,15 +63,22 @@ function findByComponentName(container, component) {
|
|
|
67
63
|
}
|
|
68
64
|
return out;
|
|
69
65
|
}
|
|
66
|
+
export function ownWords(el) {
|
|
67
|
+
return (el.textContent ?? '').replace(/\s+/g, ' ').trim();
|
|
68
|
+
}
|
|
70
69
|
function findByComponent(container, query) {
|
|
70
|
+
const words = query.text === undefined ? undefined : query.text.replace(/\s+/g, ' ').trim();
|
|
71
|
+
const role = query.role;
|
|
72
|
+
const keep = (els) => els.filter((el) => (words === undefined || ownWords(el) === words) &&
|
|
73
|
+
(role === undefined || matchesRole(getRole(el), role)));
|
|
71
74
|
if (query.source !== undefined) {
|
|
72
|
-
const bySource = findBySource(container, query.source);
|
|
75
|
+
const bySource = keep(findBySource(container, query.source));
|
|
73
76
|
if (bySource.length > 0) {
|
|
74
77
|
return bySource;
|
|
75
78
|
}
|
|
76
79
|
}
|
|
77
80
|
if (query.component !== undefined && query.component.length > 0) {
|
|
78
|
-
return findByComponentName(container, query.component);
|
|
81
|
+
return keep(findByComponentName(container, query.component));
|
|
79
82
|
}
|
|
80
83
|
return [];
|
|
81
84
|
}
|
package/dist/dom/query.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ElementState, type ElementQuery, type MatchResult, type NearestTarget, type QueryResult } from '@reticlehq/core';
|
|
2
|
+
export declare function embeddedRootsUnder(root: HTMLElement): HTMLElement[];
|
|
2
3
|
export declare const ATTR_VALUE_MAX = 512;
|
|
3
4
|
export declare function matchQuery(query: ElementQuery, state?: ElementState, limit?: number, wholePhrase?: boolean, diagnose?: boolean, fullText?: boolean): MatchResult;
|
|
4
5
|
export declare const MAX_NEAREST_NAME = 60;
|
package/dist/dom/query.js
CHANGED
|
@@ -1,21 +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';
|
|
15
17
|
import { INTERACTIVE } from './snapshot.js';
|
|
16
18
|
const MAX_PRESENT_TESTIDS = 12;
|
|
17
19
|
function visibleTextOf(el, memo) {
|
|
18
|
-
if (isNonRendered(el) || isWordsIgnored(el)) {
|
|
20
|
+
if (isHiddenSubtree(el, memo) || isNonRendered(el) || isWordsIgnored(el)) {
|
|
19
21
|
return '';
|
|
20
22
|
}
|
|
21
23
|
const visible = isVisible(el, memo);
|
|
@@ -26,6 +28,9 @@ function visibleTextOf(el, memo) {
|
|
|
26
28
|
text += node.textContent ?? '';
|
|
27
29
|
}
|
|
28
30
|
else if (isElement(node)) {
|
|
31
|
+
if (isHiddenSubtree(node, memo)) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
29
34
|
const own = visibleTextOf(node, memo);
|
|
30
35
|
text += apart || ownBox(node) ? ` ${own} ` : own;
|
|
31
36
|
}
|
|
@@ -40,24 +45,6 @@ function ownBox(el) {
|
|
|
40
45
|
const display = el.ownerDocument.defaultView?.getComputedStyle(el).display ?? 'inline';
|
|
41
46
|
return !display.startsWith('inline') && 'contents' !== display;
|
|
42
47
|
}
|
|
43
|
-
function resolveContainer(scope) {
|
|
44
|
-
if (scope === undefined) {
|
|
45
|
-
return { container: document.body, scopeMissing: false };
|
|
46
|
-
}
|
|
47
|
-
const byRef = refs.resolve(scope);
|
|
48
|
-
if (isActionTarget(byRef)) {
|
|
49
|
-
return { container: byRef, scopeMissing: false };
|
|
50
|
-
}
|
|
51
|
-
try {
|
|
52
|
-
const found = document.querySelector(scope);
|
|
53
|
-
if (isActionTarget(found)) {
|
|
54
|
-
return { container: found, scopeMissing: false };
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
}
|
|
59
|
-
return { container: null, scopeMissing: true };
|
|
60
|
-
}
|
|
61
48
|
function readableFrameBody(frame) {
|
|
62
49
|
try {
|
|
63
50
|
return frame.contentDocument?.body ?? null;
|
|
@@ -67,7 +54,7 @@ function readableFrameBody(frame) {
|
|
|
67
54
|
}
|
|
68
55
|
}
|
|
69
56
|
const FRAME_DEPTH_MAX = 3;
|
|
70
|
-
function embeddedRootsUnder(root) {
|
|
57
|
+
export function embeddedRootsUnder(root) {
|
|
71
58
|
const found = [];
|
|
72
59
|
const walk = (node, depth) => {
|
|
73
60
|
for (const el of node.querySelectorAll('*')) {
|
|
@@ -95,19 +82,20 @@ function embeddedRootsUnder(root) {
|
|
|
95
82
|
return found;
|
|
96
83
|
}
|
|
97
84
|
function findCandidates(query) {
|
|
98
|
-
const {
|
|
99
|
-
|
|
100
|
-
|
|
85
|
+
const { containers, scopeMissing } = resolveContainers(query.scope);
|
|
86
|
+
const scopeMatches = containers.length;
|
|
87
|
+
if (0 === scopeMatches) {
|
|
88
|
+
return { candidates: [], scopeMissing: true, scopeMatches };
|
|
101
89
|
}
|
|
102
90
|
if (true === query.self) {
|
|
103
91
|
if (query.scope === undefined) {
|
|
104
|
-
return { candidates: [], scopeMissing };
|
|
105
|
-
}
|
|
106
|
-
if (query.text !== undefined &&
|
|
107
|
-
!textMatches(visibleTextOf(container, new Map()), query, query.text)) {
|
|
108
|
-
return { candidates: [], scopeMissing };
|
|
92
|
+
return { candidates: [], scopeMissing, scopeMatches };
|
|
109
93
|
}
|
|
110
|
-
|
|
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 };
|
|
111
99
|
}
|
|
112
100
|
const seen = new Set();
|
|
113
101
|
const out = [];
|
|
@@ -117,6 +105,9 @@ function findCandidates(query) {
|
|
|
117
105
|
if (seen.has(el)) {
|
|
118
106
|
continue;
|
|
119
107
|
}
|
|
108
|
+
if (!pageContent(el, query.scope)) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
120
111
|
if (ignored(el)) {
|
|
121
112
|
continue;
|
|
122
113
|
}
|
|
@@ -124,11 +115,13 @@ function findCandidates(query) {
|
|
|
124
115
|
out.push(el);
|
|
125
116
|
}
|
|
126
117
|
};
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
118
|
+
for (const container of containers) {
|
|
119
|
+
collect(findIn(container, query));
|
|
120
|
+
for (const embedded of embeddedRootsUnder(container)) {
|
|
121
|
+
collect(findIn(embedded, query));
|
|
122
|
+
}
|
|
130
123
|
}
|
|
131
|
-
return { candidates: out, scopeMissing };
|
|
124
|
+
return { candidates: out, scopeMissing, scopeMatches };
|
|
132
125
|
}
|
|
133
126
|
export const ATTR_VALUE_MAX = 512;
|
|
134
127
|
const ATTR_KEYS_MAX = 12;
|
|
@@ -143,10 +136,29 @@ function projectAttrs(el, keys) {
|
|
|
143
136
|
}
|
|
144
137
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
145
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
|
+
}
|
|
146
149
|
function inState(el, state, memo) {
|
|
147
150
|
if (ElementState.IN_VIEWPORT === state) {
|
|
148
151
|
return isInViewport(el, memo);
|
|
149
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
|
+
}
|
|
150
162
|
return getStates(el, isVisible(el, memo)).includes(state);
|
|
151
163
|
}
|
|
152
164
|
const MAX_DESCRIBED = TRANSPORT_LIMITS.MAX_COLLECTION_ITEMS;
|
|
@@ -154,8 +166,10 @@ export function matchQuery(query, state, limit = MAX_DESCRIBED, wholePhrase = tr
|
|
|
154
166
|
const found = findCandidates(query);
|
|
155
167
|
const scopeMissing = found.scopeMissing;
|
|
156
168
|
const wanted = wholePhrase && textOnly(query) ? wantedTextOf(query) : undefined;
|
|
157
|
-
const
|
|
158
|
-
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));
|
|
159
173
|
const elements = owner === undefined ? found.candidates : [owner];
|
|
160
174
|
const visMemo = new Map();
|
|
161
175
|
const filtered = state === undefined ? elements : elements.filter((el) => inState(el, state, visMemo));
|
|
@@ -186,6 +200,7 @@ export function matchQuery(query, state, limit = MAX_DESCRIBED, wholePhrase = tr
|
|
|
186
200
|
elements: descriptors,
|
|
187
201
|
...(fullText ? { fullText: true } : {}),
|
|
188
202
|
...(scopeMissing ? { scopeMissing: true } : {}),
|
|
203
|
+
...(found.scopeMatches > 1 ? { scopeMatches: found.scopeMatches } : {}),
|
|
189
204
|
...(covered === undefined ? {} : { coveredBy: covered }),
|
|
190
205
|
...(unreadable === undefined ? {} : { unreadable }),
|
|
191
206
|
...(0 === filtered.length ? { hint: buildEmptyHint(query, state, diagnose) } : {}),
|
|
@@ -204,7 +219,7 @@ function resolveLabelledBy(el) {
|
|
|
204
219
|
return text.length > 0 ? text : undefined;
|
|
205
220
|
}
|
|
206
221
|
function buildPresentRegions(query) {
|
|
207
|
-
const
|
|
222
|
+
const roots = hintRoots(query.scope);
|
|
208
223
|
const regions = [];
|
|
209
224
|
const CONTAINER_ROLES = [
|
|
210
225
|
'list',
|
|
@@ -225,7 +240,7 @@ function buildPresentRegions(query) {
|
|
|
225
240
|
'tablist',
|
|
226
241
|
];
|
|
227
242
|
for (const role of CONTAINER_ROLES) {
|
|
228
|
-
const containers = queryByRoleAndName(
|
|
243
|
+
const containers = roots.flatMap((root) => queryByRoleAndName(root, role, undefined));
|
|
229
244
|
for (const el of containers) {
|
|
230
245
|
if (isReticleOverlay(el)) {
|
|
231
246
|
continue;
|
|
@@ -269,6 +284,9 @@ function splitTextOwner(container, wanted, query) {
|
|
|
269
284
|
if (isIgnored(el)) {
|
|
270
285
|
continue;
|
|
271
286
|
}
|
|
287
|
+
if (!pageContent(el, query.scope)) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
272
290
|
if (!textMatches(visibleTextOf(el, memo), query, wanted)) {
|
|
273
291
|
continue;
|
|
274
292
|
}
|
|
@@ -283,7 +301,7 @@ function splitTextOwner(container, wanted, query) {
|
|
|
283
301
|
}
|
|
284
302
|
return best;
|
|
285
303
|
}
|
|
286
|
-
function caseOnlyMatch(container, wanted) {
|
|
304
|
+
function caseOnlyMatch(container, wanted, scope) {
|
|
287
305
|
const target = comparableText(wanted).toLowerCase();
|
|
288
306
|
if (0 === target.length) {
|
|
289
307
|
return undefined;
|
|
@@ -293,6 +311,9 @@ function caseOnlyMatch(container, wanted) {
|
|
|
293
311
|
if (isIgnored(el)) {
|
|
294
312
|
continue;
|
|
295
313
|
}
|
|
314
|
+
if (!pageContent(el, scope)) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
296
317
|
const text = normaliseVisibleText(visibleTextOf(el, memo));
|
|
297
318
|
const at = foldTypography(text).toLowerCase().indexOf(target);
|
|
298
319
|
if (-1 !== at) {
|
|
@@ -474,14 +495,17 @@ function textInAriaHidden(container, wanted) {
|
|
|
474
495
|
return true;
|
|
475
496
|
}
|
|
476
497
|
function buildEmptyHint(query, state, diagnose = true) {
|
|
477
|
-
const
|
|
478
|
-
const all =
|
|
498
|
+
const roots = hintRoots(query.scope);
|
|
499
|
+
const all = roots.flatMap((root) => Array.from(root.querySelectorAll(testIdSelector())));
|
|
479
500
|
const present = [];
|
|
480
501
|
const seen = new Set();
|
|
481
|
-
for (const el of
|
|
502
|
+
for (const el of all) {
|
|
482
503
|
if (isIgnored(el)) {
|
|
483
504
|
continue;
|
|
484
505
|
}
|
|
506
|
+
if (!pageContent(el, query.scope)) {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
485
509
|
const id = readTestId(el);
|
|
486
510
|
if (null === id || 0 === id.length || seen.has(id)) {
|
|
487
511
|
continue;
|
|
@@ -503,14 +527,14 @@ function buildEmptyHint(query, state, diagnose = true) {
|
|
|
503
527
|
};
|
|
504
528
|
const wanted = wantedTextOf(query);
|
|
505
529
|
if (wanted !== undefined) {
|
|
506
|
-
const owner = splitTextOwner(
|
|
530
|
+
const owner = firstIn(roots, (root) => splitTextOwner(root, wanted, query));
|
|
507
531
|
if (owner !== undefined) {
|
|
508
532
|
const described = describe(owner);
|
|
509
533
|
const selector = stableSelectorFor(owner);
|
|
510
534
|
hint.splitText = selector === undefined ? described : { ...described, selector };
|
|
511
535
|
}
|
|
512
536
|
else if (true === query.matchCase) {
|
|
513
|
-
const shown = caseOnlyMatch(
|
|
537
|
+
const shown = firstIn(roots, (root) => caseOnlyMatch(root, wanted, query.scope));
|
|
514
538
|
if (shown !== undefined) {
|
|
515
539
|
hint.caseMiss = shown;
|
|
516
540
|
}
|
|
@@ -520,19 +544,19 @@ function buildEmptyHint(query, state, diagnose = true) {
|
|
|
520
544
|
wanted !== undefined &&
|
|
521
545
|
hint.splitText === undefined &&
|
|
522
546
|
(state === undefined || ElementState.VISIBLE === state || ElementState.IN_VIEWPORT === state)) {
|
|
523
|
-
if (textInAriaHidden(
|
|
547
|
+
if (roots.some((root) => textInAriaHidden(root, wanted))) {
|
|
524
548
|
hint.ariaHiddenMatch = true;
|
|
525
549
|
}
|
|
526
550
|
}
|
|
527
|
-
const near = nameNearMisses(
|
|
551
|
+
const near = firstListIn(roots, (root) => nameNearMisses(root, query));
|
|
528
552
|
if (near.length > 0) {
|
|
529
553
|
hint.nameNearMiss = near;
|
|
530
554
|
}
|
|
531
|
-
const elsewhere = testidFoundUnder(
|
|
555
|
+
const elsewhere = firstIn(roots, (root) => testidFoundUnder(root, query));
|
|
532
556
|
if (elsewhere !== undefined) {
|
|
533
557
|
hint.testidFoundUnder = elsewhere;
|
|
534
558
|
}
|
|
535
|
-
const nearest = diagnose ? nearestTargets(
|
|
559
|
+
const nearest = diagnose ? firstListIn(roots, (root) => nearestTargets(root, query)) : [];
|
|
536
560
|
if (nearest.length > 0) {
|
|
537
561
|
hint.nearest = nearest;
|
|
538
562
|
}
|
|
@@ -543,7 +567,10 @@ export function elementsMatching(query) {
|
|
|
543
567
|
}
|
|
544
568
|
export function runQuery(query, limit) {
|
|
545
569
|
const result = matchQuery(query, undefined, limit, false);
|
|
546
|
-
const scopeFields =
|
|
570
|
+
const scopeFields = {
|
|
571
|
+
...(true === result.scopeMissing ? { scopeMissing: true } : {}),
|
|
572
|
+
...(result.scopeMatches === undefined ? {} : { scopeMatches: result.scopeMatches }),
|
|
573
|
+
};
|
|
547
574
|
if (0 === result.elements.length) {
|
|
548
575
|
return {
|
|
549
576
|
elements: result.elements,
|
|
@@ -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
|
@@ -3,14 +3,15 @@ import { stalledFade } from './visibility.js';
|
|
|
3
3
|
export function normaliseVisibleText(value) {
|
|
4
4
|
return value.replace(/\s+/g, ' ').trim().normalize('NFC');
|
|
5
5
|
}
|
|
6
|
-
const SINGLE_QUOTES = /[\u2018
|
|
7
|
-
const DOUBLE_QUOTES = /[\u201c
|
|
6
|
+
const SINGLE_QUOTES = /[\u2018-\u201b]/g;
|
|
7
|
+
const DOUBLE_QUOTES = /[\u201c-\u201f]/g;
|
|
8
8
|
const DASHES = /[\u2013\u2014]/g;
|
|
9
9
|
export function foldTypography(value) {
|
|
10
10
|
return value.replace(SINGLE_QUOTES, "'").replace(DOUBLE_QUOTES, '"').replace(DASHES, '-');
|
|
11
11
|
}
|
|
12
|
+
const INVISIBLE_FORMAT = /[\u00ad\u200b-\u200f\u202a-\u202e\u2060-\u2064\u2066-\u2069\ufeff]/g;
|
|
12
13
|
export function comparableText(value) {
|
|
13
|
-
return foldTypography(normaliseVisibleText(value));
|
|
14
|
+
return foldTypography(normaliseVisibleText(value.replace(INVISIBLE_FORMAT, '')));
|
|
14
15
|
}
|
|
15
16
|
export function fuzzyVisibleText(actual, expected) {
|
|
16
17
|
return comparableText(actual).toLowerCase().includes(comparableText(expected).toLowerCase());
|
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
|
@@ -2,7 +2,9 @@ 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
4
|
export declare function stalledFade(el: Element): boolean;
|
|
5
|
+
export declare function isNonRendered(el: Element): boolean;
|
|
5
6
|
export declare function isHiddenByAriaOnly(el: Element): boolean;
|
|
6
7
|
export declare function isRenderedOutsideAriaHidden(el: Element): boolean;
|
|
7
8
|
export declare function isInViewport(el: Element, memo?: Map<Element, boolean>): boolean;
|
|
9
|
+
export declare function isHiddenSubtree(el: Element, memo?: Map<Element, boolean>): boolean;
|
|
8
10
|
export declare function isVisible(el: Element, memo?: Map<Element, boolean>): boolean;
|