@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
package/dist/dom/query.js
CHANGED
|
@@ -1,63 +1,29 @@
|
|
|
1
|
-
import { ALTERNATIVE_TESTID_ATTRS, DEFAULT_TESTID_ATTR,
|
|
2
|
-
import { isActionTarget, isElement, isFrame
|
|
1
|
+
import { ALTERNATIVE_TESTID_ATTRS, DEFAULT_TESTID_ATTR, ElementState, QueryBy, REDACTED_VALUE, TRANSPORT_LIMITS, } from '@reticlehq/core';
|
|
2
|
+
import { isActionTarget, isElement, isFrame } from './realm.js';
|
|
3
3
|
import { capturedRootOf } from './shadow-registry.js';
|
|
4
|
-
import { getAccessibleName, getRole, describe, getStates
|
|
5
|
-
import {
|
|
4
|
+
import { getAccessibleName, getRole, describe, getStates } from './a11y.js';
|
|
5
|
+
import { isHiddenByAriaOnly, isInViewport, isRenderedOutsideAriaHidden, isVisible, } from './visibility.js';
|
|
6
|
+
import { dialogCovering, isIgnored, isReticleOverlay, isWordsIgnored } from './dom-ignore.js';
|
|
6
7
|
import { isSensitiveKey } from '../security/serialization.js';
|
|
7
8
|
import { declaredTestids } from '../registry/capabilities.js';
|
|
8
|
-
import { identifyComponent } from '../registry/stores/adapters.js';
|
|
9
9
|
import { refs } from './addressing/refs.js';
|
|
10
10
|
import { getTestIdAttr, readTestId, testIdSelector } from './addressing/testid-attr.js';
|
|
11
|
-
|
|
11
|
+
import { stableSelectorFor } from './stable-selector.js';
|
|
12
|
+
import { fuzzyVisibleText, normaliseVisibleText, comparableText, foldTypography, textMatches, unreadableMatch, } from './text-match.js';
|
|
13
|
+
import { pageRoute } from '../security/page-location.js';
|
|
14
|
+
import { elementsUnder, findIn, isNonRendered, queryByRoleAndName } from './query-finders.js';
|
|
12
15
|
const MAX_PRESENT_TESTIDS = 12;
|
|
13
|
-
const MAX_COMPONENT_CANDIDATES = 2000;
|
|
14
|
-
const componentCandidateSelector = () => `[${SOURCE_ATTR}], ${testIdSelector()}, button, a, input, select, textarea, [role]`;
|
|
15
|
-
function elementsUnder(container) {
|
|
16
|
-
const self = Node.ELEMENT_NODE === container.nodeType ? [container] : [];
|
|
17
|
-
return [...self, ...Array.from(container.querySelectorAll('*'))];
|
|
18
|
-
}
|
|
19
|
-
function normaliseVisibleText(value) {
|
|
20
|
-
return value.replace(/\s+/g, ' ').trim().normalize('NFC');
|
|
21
|
-
}
|
|
22
|
-
function fuzzyVisibleText(actual, expected) {
|
|
23
|
-
return normaliseVisibleText(actual)
|
|
24
|
-
.toLowerCase()
|
|
25
|
-
.includes(normaliseVisibleText(expected).toLowerCase());
|
|
26
|
-
}
|
|
27
|
-
function textMatches(actual, query, expected) {
|
|
28
|
-
if (true !== query.matchCase)
|
|
29
|
-
return fuzzyVisibleText(actual, expected);
|
|
30
|
-
return normaliseVisibleText(actual).includes(normaliseVisibleText(expected));
|
|
31
|
-
}
|
|
32
|
-
function exactVisibleText(actual, expected) {
|
|
33
|
-
return normaliseVisibleText(actual) === normaliseVisibleText(expected);
|
|
34
|
-
}
|
|
35
|
-
const NON_RENDERED_TAGS = ['script', 'style', 'template', 'noscript'];
|
|
36
|
-
function isNonRendered(el) {
|
|
37
|
-
return NON_RENDERED_TAGS.includes(el.tagName.toLowerCase());
|
|
38
|
-
}
|
|
39
|
-
function directText(el) {
|
|
40
|
-
if (isInput(el)) {
|
|
41
|
-
const type = el.type.toLowerCase();
|
|
42
|
-
if ('submit' === type || 'button' === type || 'reset' === type)
|
|
43
|
-
return el.value;
|
|
44
|
-
}
|
|
45
|
-
if (isNonRendered(el))
|
|
46
|
-
return '';
|
|
47
|
-
return Array.from(el.childNodes)
|
|
48
|
-
.filter((node) => Node.TEXT_NODE === node.nodeType)
|
|
49
|
-
.map((node) => node.textContent ?? '')
|
|
50
|
-
.join('');
|
|
51
|
-
}
|
|
52
16
|
function visibleTextOf(el, memo) {
|
|
53
|
-
if (isNonRendered(el) ||
|
|
17
|
+
if (isNonRendered(el) || isWordsIgnored(el)) {
|
|
54
18
|
return '';
|
|
19
|
+
}
|
|
55
20
|
const visible = isVisible(el, memo);
|
|
56
21
|
const apart = laysOutApart(el);
|
|
57
22
|
let text = '';
|
|
58
23
|
for (const node of Array.from(el.childNodes)) {
|
|
59
|
-
if (Node.TEXT_NODE === node.nodeType && visible)
|
|
24
|
+
if (Node.TEXT_NODE === node.nodeType && visible) {
|
|
60
25
|
text += node.textContent ?? '';
|
|
26
|
+
}
|
|
61
27
|
else if (isElement(node)) {
|
|
62
28
|
const own = visibleTextOf(node, memo);
|
|
63
29
|
text += apart || ownBox(node) ? ` ${own} ` : own;
|
|
@@ -73,149 +39,24 @@ function ownBox(el) {
|
|
|
73
39
|
const display = el.ownerDocument.defaultView?.getComputedStyle(el).display ?? 'inline';
|
|
74
40
|
return !display.startsWith('inline') && 'contents' !== display;
|
|
75
41
|
}
|
|
76
|
-
function semanticNameTarget(el) {
|
|
77
|
-
if (isInput(el) || isTextArea(el) || isSelect(el))
|
|
78
|
-
return true;
|
|
79
|
-
const tag = el.tagName.toLowerCase();
|
|
80
|
-
return ('button' === tag ||
|
|
81
|
-
'meter' === tag ||
|
|
82
|
-
'output' === tag ||
|
|
83
|
-
'progress' === tag ||
|
|
84
|
-
el.hasAttribute('aria-label') ||
|
|
85
|
-
el.hasAttribute('aria-labelledby'));
|
|
86
|
-
}
|
|
87
42
|
function resolveContainer(scope) {
|
|
88
|
-
if (scope === undefined)
|
|
43
|
+
if (scope === undefined) {
|
|
89
44
|
return { container: document.body, scopeMissing: false };
|
|
45
|
+
}
|
|
90
46
|
const byRef = refs.resolve(scope);
|
|
91
|
-
if (isActionTarget(byRef))
|
|
47
|
+
if (isActionTarget(byRef)) {
|
|
92
48
|
return { container: byRef, scopeMissing: false };
|
|
49
|
+
}
|
|
93
50
|
try {
|
|
94
51
|
const found = document.querySelector(scope);
|
|
95
|
-
if (isActionTarget(found))
|
|
52
|
+
if (isActionTarget(found)) {
|
|
96
53
|
return { container: found, scopeMissing: false };
|
|
54
|
+
}
|
|
97
55
|
}
|
|
98
56
|
catch {
|
|
99
57
|
}
|
|
100
58
|
return { container: null, scopeMissing: true };
|
|
101
59
|
}
|
|
102
|
-
function findBySource(container, source) {
|
|
103
|
-
const prefix = `${source.file}:${source.line}:`.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
104
|
-
try {
|
|
105
|
-
return Array.from(container.querySelectorAll(`[${SOURCE_ATTR}^="${prefix}"]`));
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
return [];
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
function findByComponentName(container, component) {
|
|
112
|
-
const out = [];
|
|
113
|
-
let scanned = 0;
|
|
114
|
-
for (const el of Array.from(container.querySelectorAll(componentCandidateSelector()))) {
|
|
115
|
-
if (scanned >= MAX_COMPONENT_CANDIDATES)
|
|
116
|
-
break;
|
|
117
|
-
scanned += 1;
|
|
118
|
-
const info = identifyComponent(el);
|
|
119
|
-
if (info !== null && info.componentStack[0] === component)
|
|
120
|
-
out.push(el);
|
|
121
|
-
}
|
|
122
|
-
return out;
|
|
123
|
-
}
|
|
124
|
-
function findByComponent(container, query) {
|
|
125
|
-
if (query.source !== undefined) {
|
|
126
|
-
const bySource = findBySource(container, query.source);
|
|
127
|
-
if (bySource.length > 0)
|
|
128
|
-
return bySource;
|
|
129
|
-
}
|
|
130
|
-
if (query.component !== undefined && query.component.length > 0) {
|
|
131
|
-
return findByComponentName(container, query.component);
|
|
132
|
-
}
|
|
133
|
-
return [];
|
|
134
|
-
}
|
|
135
|
-
function matchesRole(actual, queried) {
|
|
136
|
-
if (actual === queried)
|
|
137
|
-
return true;
|
|
138
|
-
if ('textbox' === queried && 'searchbox' === actual)
|
|
139
|
-
return true;
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
142
|
-
function queryByRoleAndName(container, role, name) {
|
|
143
|
-
return elementsUnder(container).filter((el) => matchesRole(getRole(el), role) &&
|
|
144
|
-
(name === undefined || exactVisibleText(getAccessibleName(el), name)));
|
|
145
|
-
}
|
|
146
|
-
function queryByText(container, value, query) {
|
|
147
|
-
return elementsUnder(container).filter((el) => textMatches(directText(el), query, value));
|
|
148
|
-
}
|
|
149
|
-
function queryByLabel(container, value) {
|
|
150
|
-
const expected = normaliseVisibleText(value).toLowerCase();
|
|
151
|
-
const exact = [];
|
|
152
|
-
const fuzzy = [];
|
|
153
|
-
for (const el of elementsUnder(container)) {
|
|
154
|
-
if (!semanticNameTarget(el))
|
|
155
|
-
continue;
|
|
156
|
-
const name = normaliseVisibleText(getAccessibleName(el)).toLowerCase();
|
|
157
|
-
if (!name.includes(expected))
|
|
158
|
-
continue;
|
|
159
|
-
(name === expected ? exact : fuzzy).push(el);
|
|
160
|
-
}
|
|
161
|
-
return exact.length > 0 ? exact : fuzzy;
|
|
162
|
-
}
|
|
163
|
-
function queryByPlaceholder(container, value) {
|
|
164
|
-
return elementsUnder(container).filter((el) => {
|
|
165
|
-
const placeholder = el.getAttribute('placeholder');
|
|
166
|
-
return placeholder !== null && fuzzyVisibleText(placeholder, value);
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
function queryByTestId(container, value) {
|
|
170
|
-
return elementsUnder(container).filter((el) => readTestId(el) === value);
|
|
171
|
-
}
|
|
172
|
-
function queryByAlt(container, value) {
|
|
173
|
-
return elementsUnder(container).filter((el) => {
|
|
174
|
-
const alt = el.getAttribute('alt');
|
|
175
|
-
return alt !== null && fuzzyVisibleText(alt, value);
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
function findIn(container, query) {
|
|
179
|
-
const by = query.by;
|
|
180
|
-
const value = query.value;
|
|
181
|
-
if (by !== undefined && value !== undefined) {
|
|
182
|
-
switch (by) {
|
|
183
|
-
case QueryBy.ROLE:
|
|
184
|
-
return queryByRoleAndName(container, value, query.name);
|
|
185
|
-
case QueryBy.TEXT:
|
|
186
|
-
return queryByText(container, value, query);
|
|
187
|
-
case QueryBy.LABEL:
|
|
188
|
-
return queryByLabel(container, value);
|
|
189
|
-
case QueryBy.PLACEHOLDER:
|
|
190
|
-
return queryByPlaceholder(container, value);
|
|
191
|
-
case QueryBy.TESTID:
|
|
192
|
-
return queryByTestId(container, value);
|
|
193
|
-
case QueryBy.ALT:
|
|
194
|
-
return queryByAlt(container, value);
|
|
195
|
-
case QueryBy.COMPONENT:
|
|
196
|
-
return findByComponent(container, { ...query, component: query.component ?? value });
|
|
197
|
-
default:
|
|
198
|
-
throw new Error(`unsupported query strategy '${String(by)}' - use one of: ${Object.values(QueryBy).join(', ')}`);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
if (query.component !== undefined || query.source !== undefined) {
|
|
202
|
-
return findByComponent(container, query);
|
|
203
|
-
}
|
|
204
|
-
if (query.role !== undefined) {
|
|
205
|
-
return queryByRoleAndName(container, query.role, query.name);
|
|
206
|
-
}
|
|
207
|
-
if (query.text !== undefined)
|
|
208
|
-
return queryByText(container, query.text, query);
|
|
209
|
-
if (query.label !== undefined)
|
|
210
|
-
return queryByLabel(container, query.label);
|
|
211
|
-
if (query.placeholder !== undefined)
|
|
212
|
-
return queryByPlaceholder(container, query.placeholder);
|
|
213
|
-
if (query.testid !== undefined)
|
|
214
|
-
return queryByTestId(container, query.testid);
|
|
215
|
-
if (query.alt !== undefined)
|
|
216
|
-
return queryByAlt(container, query.alt);
|
|
217
|
-
return [];
|
|
218
|
-
}
|
|
219
60
|
function readableFrameBody(frame) {
|
|
220
61
|
try {
|
|
221
62
|
return frame.contentDocument?.body ?? null;
|
|
@@ -235,13 +76,16 @@ function embeddedRootsUnder(root) {
|
|
|
235
76
|
walk(shadow, depth);
|
|
236
77
|
continue;
|
|
237
78
|
}
|
|
238
|
-
if (depth >= FRAME_DEPTH_MAX)
|
|
79
|
+
if (depth >= FRAME_DEPTH_MAX) {
|
|
239
80
|
continue;
|
|
240
|
-
|
|
81
|
+
}
|
|
82
|
+
if (!isFrame(el)) {
|
|
241
83
|
continue;
|
|
84
|
+
}
|
|
242
85
|
const body = readableFrameBody(el);
|
|
243
|
-
if (null === body)
|
|
86
|
+
if (null === body) {
|
|
244
87
|
continue;
|
|
88
|
+
}
|
|
245
89
|
found.push(body);
|
|
246
90
|
walk(body, depth + 1);
|
|
247
91
|
}
|
|
@@ -251,11 +95,13 @@ function embeddedRootsUnder(root) {
|
|
|
251
95
|
}
|
|
252
96
|
function findCandidates(query) {
|
|
253
97
|
const { container, scopeMissing } = resolveContainer(query.scope);
|
|
254
|
-
if (null === container)
|
|
98
|
+
if (null === container) {
|
|
255
99
|
return { candidates: [], scopeMissing: true };
|
|
100
|
+
}
|
|
256
101
|
if (true === query.self) {
|
|
257
|
-
if (query.scope === undefined)
|
|
102
|
+
if (query.scope === undefined) {
|
|
258
103
|
return { candidates: [], scopeMissing };
|
|
104
|
+
}
|
|
259
105
|
if (query.text !== undefined &&
|
|
260
106
|
!textMatches(visibleTextOf(container, new Map()), query, query.text)) {
|
|
261
107
|
return { candidates: [], scopeMissing };
|
|
@@ -264,19 +110,23 @@ function findCandidates(query) {
|
|
|
264
110
|
}
|
|
265
111
|
const seen = new Set();
|
|
266
112
|
const out = [];
|
|
113
|
+
const ignored = wantedTextOf(query) === undefined ? isIgnored : isWordsIgnored;
|
|
267
114
|
const collect = (els) => {
|
|
268
115
|
for (const el of els) {
|
|
269
|
-
if (seen.has(el))
|
|
116
|
+
if (seen.has(el)) {
|
|
270
117
|
continue;
|
|
271
|
-
|
|
118
|
+
}
|
|
119
|
+
if (ignored(el)) {
|
|
272
120
|
continue;
|
|
121
|
+
}
|
|
273
122
|
seen.add(el);
|
|
274
123
|
out.push(el);
|
|
275
124
|
}
|
|
276
125
|
};
|
|
277
126
|
collect(findIn(container, query));
|
|
278
|
-
for (const embedded of embeddedRootsUnder(container))
|
|
127
|
+
for (const embedded of embeddedRootsUnder(container)) {
|
|
279
128
|
collect(findIn(embedded, query));
|
|
129
|
+
}
|
|
280
130
|
return { candidates: out, scopeMissing };
|
|
281
131
|
}
|
|
282
132
|
export const ATTR_VALUE_MAX = 512;
|
|
@@ -285,19 +135,21 @@ function projectAttrs(el, keys) {
|
|
|
285
135
|
const out = {};
|
|
286
136
|
for (const key of keys.slice(0, ATTR_KEYS_MAX)) {
|
|
287
137
|
const raw = el.getAttribute(key);
|
|
288
|
-
if (null === raw)
|
|
138
|
+
if (null === raw) {
|
|
289
139
|
continue;
|
|
140
|
+
}
|
|
290
141
|
out[key] = isSensitiveKey(key) ? REDACTED_VALUE : raw.slice(0, ATTR_VALUE_MAX);
|
|
291
142
|
}
|
|
292
143
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
293
144
|
}
|
|
294
145
|
function inState(el, state, memo) {
|
|
295
|
-
if (ElementState.IN_VIEWPORT === state)
|
|
146
|
+
if (ElementState.IN_VIEWPORT === state) {
|
|
296
147
|
return isInViewport(el, memo);
|
|
148
|
+
}
|
|
297
149
|
return getStates(el, isVisible(el, memo)).includes(state);
|
|
298
150
|
}
|
|
299
151
|
const MAX_DESCRIBED = TRANSPORT_LIMITS.MAX_COLLECTION_ITEMS;
|
|
300
|
-
export function matchQuery(query, state, limit = MAX_DESCRIBED, wholePhrase = true) {
|
|
152
|
+
export function matchQuery(query, state, limit = MAX_DESCRIBED, wholePhrase = true, diagnose = true) {
|
|
301
153
|
const found = findCandidates(query);
|
|
302
154
|
const scopeMissing = found.scopeMissing;
|
|
303
155
|
const wanted = wholePhrase && textOnly(query) ? wantedTextOf(query) : undefined;
|
|
@@ -316,23 +168,29 @@ export function matchQuery(query, state, limit = MAX_DESCRIBED, wholePhrase = tr
|
|
|
316
168
|
!base.states.includes(ElementState.IN_VIEWPORT)) {
|
|
317
169
|
base = { ...base, states: [...base.states, ElementState.IN_VIEWPORT] };
|
|
318
170
|
}
|
|
319
|
-
if (attrs === undefined || 0 === attrs.length)
|
|
171
|
+
if (attrs === undefined || 0 === attrs.length) {
|
|
320
172
|
return base;
|
|
173
|
+
}
|
|
321
174
|
const projected = projectAttrs(el, attrs);
|
|
322
175
|
return projected === undefined ? base : { ...base, attrs: projected };
|
|
323
176
|
});
|
|
177
|
+
const covered = dialogCovering(filtered);
|
|
178
|
+
const unreadable = wantedTextOf(query) === undefined ? undefined : unreadableMatch(described);
|
|
324
179
|
return {
|
|
325
180
|
matched: filtered.length > 0,
|
|
326
181
|
count: filtered.length,
|
|
327
182
|
elements: descriptors,
|
|
328
183
|
...(scopeMissing ? { scopeMissing: true } : {}),
|
|
329
|
-
...(
|
|
184
|
+
...(covered === undefined ? {} : { coveredBy: covered }),
|
|
185
|
+
...(unreadable === undefined ? {} : { unreadable }),
|
|
186
|
+
...(0 === filtered.length ? { hint: buildEmptyHint(query, state, diagnose) } : {}),
|
|
330
187
|
};
|
|
331
188
|
}
|
|
332
189
|
function resolveLabelledBy(el) {
|
|
333
190
|
const ids = el.getAttribute('aria-labelledby');
|
|
334
|
-
if (null === ids)
|
|
191
|
+
if (null === ids) {
|
|
335
192
|
return undefined;
|
|
193
|
+
}
|
|
336
194
|
const text = ids
|
|
337
195
|
.split(/\s+/)
|
|
338
196
|
.map((id) => id.length > 0 ? (el.ownerDocument.getElementById(id)?.textContent?.trim() ?? '') : '')
|
|
@@ -364,8 +222,9 @@ function buildPresentRegions(query) {
|
|
|
364
222
|
for (const role of CONTAINER_ROLES) {
|
|
365
223
|
const containers = queryByRoleAndName(container, role, undefined);
|
|
366
224
|
for (const el of containers) {
|
|
367
|
-
if (isReticleOverlay(el))
|
|
225
|
+
if (isReticleOverlay(el)) {
|
|
368
226
|
continue;
|
|
227
|
+
}
|
|
369
228
|
const name = el.getAttribute('aria-label') ??
|
|
370
229
|
resolveLabelledBy(el) ??
|
|
371
230
|
readTestId(el) ??
|
|
@@ -373,8 +232,9 @@ function buildPresentRegions(query) {
|
|
|
373
232
|
const children = el.querySelectorAll('[role]');
|
|
374
233
|
const sample = [];
|
|
375
234
|
for (const child of Array.from(children)) {
|
|
376
|
-
if (sample.length >= 3)
|
|
235
|
+
if (sample.length >= 3) {
|
|
377
236
|
break;
|
|
237
|
+
}
|
|
378
238
|
const childRole = child.getAttribute('role');
|
|
379
239
|
const childName = child.getAttribute('aria-label') ??
|
|
380
240
|
readTestId(child) ??
|
|
@@ -385,11 +245,13 @@ function buildPresentRegions(query) {
|
|
|
385
245
|
}
|
|
386
246
|
}
|
|
387
247
|
const region = { role, childCount: children.length, sample };
|
|
388
|
-
if (name !== undefined && name.length > 0)
|
|
248
|
+
if (name !== undefined && name.length > 0) {
|
|
389
249
|
region.name = name;
|
|
250
|
+
}
|
|
390
251
|
regions.push(region);
|
|
391
|
-
if (regions.length >= 10)
|
|
252
|
+
if (regions.length >= 10) {
|
|
392
253
|
return regions;
|
|
254
|
+
}
|
|
393
255
|
}
|
|
394
256
|
}
|
|
395
257
|
return regions;
|
|
@@ -399,13 +261,16 @@ function splitTextOwner(container, wanted, query) {
|
|
|
399
261
|
let bestDepth = -1;
|
|
400
262
|
const memo = new Map();
|
|
401
263
|
for (const el of elementsUnder(container)) {
|
|
402
|
-
if (isIgnored(el))
|
|
264
|
+
if (isIgnored(el)) {
|
|
403
265
|
continue;
|
|
404
|
-
|
|
266
|
+
}
|
|
267
|
+
if (!textMatches(visibleTextOf(el, memo), query, wanted)) {
|
|
405
268
|
continue;
|
|
269
|
+
}
|
|
406
270
|
let depth = 0;
|
|
407
|
-
for (let parent = el.parentElement; parent !== null; parent = parent.parentElement)
|
|
271
|
+
for (let parent = el.parentElement; parent !== null; parent = parent.parentElement) {
|
|
408
272
|
depth++;
|
|
273
|
+
}
|
|
409
274
|
if (depth > bestDepth) {
|
|
410
275
|
best = el;
|
|
411
276
|
bestDepth = depth;
|
|
@@ -414,17 +279,20 @@ function splitTextOwner(container, wanted, query) {
|
|
|
414
279
|
return best;
|
|
415
280
|
}
|
|
416
281
|
function caseOnlyMatch(container, wanted) {
|
|
417
|
-
const target =
|
|
418
|
-
if (0 === target.length)
|
|
282
|
+
const target = comparableText(wanted).toLowerCase();
|
|
283
|
+
if (0 === target.length) {
|
|
419
284
|
return undefined;
|
|
285
|
+
}
|
|
420
286
|
const memo = new Map();
|
|
421
287
|
for (const el of elementsUnder(container)) {
|
|
422
|
-
if (isIgnored(el))
|
|
288
|
+
if (isIgnored(el)) {
|
|
423
289
|
continue;
|
|
290
|
+
}
|
|
424
291
|
const text = normaliseVisibleText(visibleTextOf(el, memo));
|
|
425
|
-
const at = text.toLowerCase().indexOf(target);
|
|
426
|
-
if (-1 !== at)
|
|
292
|
+
const at = foldTypography(text).toLowerCase().indexOf(target);
|
|
293
|
+
if (-1 !== at) {
|
|
427
294
|
return text.slice(at, at + target.length);
|
|
295
|
+
}
|
|
428
296
|
}
|
|
429
297
|
return undefined;
|
|
430
298
|
}
|
|
@@ -440,67 +308,112 @@ function textOnly(query) {
|
|
|
440
308
|
Object.keys(query).every((key) => TEXT_ONLY_FIELDS.has(key)));
|
|
441
309
|
}
|
|
442
310
|
function wantedTextOf(query) {
|
|
443
|
-
if (query.text !== undefined)
|
|
311
|
+
if (query.text !== undefined) {
|
|
444
312
|
return query.text;
|
|
313
|
+
}
|
|
445
314
|
return QueryBy.TEXT === query.by ? query.value : undefined;
|
|
446
315
|
}
|
|
447
316
|
const MAX_NAME_NEAR_MISSES = 5;
|
|
448
317
|
function nameNearMisses(container, query) {
|
|
449
318
|
const role = QueryBy.ROLE === query.by ? query.value : query.role;
|
|
450
319
|
const wanted = query.name;
|
|
451
|
-
if (role === undefined || wanted === undefined || 0 === wanted.length)
|
|
320
|
+
if (role === undefined || wanted === undefined || 0 === wanted.length) {
|
|
452
321
|
return [];
|
|
453
|
-
|
|
454
|
-
|
|
322
|
+
}
|
|
323
|
+
const target = comparableText(wanted).toLowerCase();
|
|
324
|
+
if (0 === target.length) {
|
|
455
325
|
return [];
|
|
326
|
+
}
|
|
456
327
|
const out = [];
|
|
457
328
|
for (const el of elementsUnder(container)) {
|
|
458
|
-
if (isIgnored(el) || getRole(el) !== role)
|
|
329
|
+
if (isIgnored(el) || getRole(el) !== role) {
|
|
459
330
|
continue;
|
|
331
|
+
}
|
|
460
332
|
const name = normaliseVisibleText(getAccessibleName(el));
|
|
461
|
-
if (0 === name.length)
|
|
333
|
+
if (0 === name.length) {
|
|
462
334
|
continue;
|
|
463
|
-
|
|
464
|
-
|
|
335
|
+
}
|
|
336
|
+
const folded = foldTypography(name).toLowerCase();
|
|
337
|
+
if (!folded.includes(target) && !target.includes(folded)) {
|
|
465
338
|
continue;
|
|
466
|
-
|
|
339
|
+
}
|
|
340
|
+
if (!out.includes(name)) {
|
|
467
341
|
out.push(name);
|
|
468
|
-
|
|
342
|
+
}
|
|
343
|
+
if (out.length >= MAX_NAME_NEAR_MISSES) {
|
|
469
344
|
break;
|
|
345
|
+
}
|
|
470
346
|
}
|
|
471
347
|
return out;
|
|
472
348
|
}
|
|
473
349
|
function testidFoundUnder(container, query) {
|
|
474
|
-
if (QueryBy.TESTID !== query.by || undefined === query.value)
|
|
350
|
+
if (QueryBy.TESTID !== query.by || undefined === query.value) {
|
|
475
351
|
return undefined;
|
|
352
|
+
}
|
|
476
353
|
const configured = getTestIdAttr();
|
|
477
354
|
for (const attr of [DEFAULT_TESTID_ATTR, ...ALTERNATIVE_TESTID_ATTRS]) {
|
|
478
|
-
if (attr === configured)
|
|
355
|
+
if (attr === configured) {
|
|
479
356
|
continue;
|
|
357
|
+
}
|
|
480
358
|
const found = elementsUnder(container).some((el) => el.getAttribute(attr) === query.value && !isIgnored(el));
|
|
481
|
-
if (found)
|
|
359
|
+
if (found) {
|
|
482
360
|
return attr;
|
|
361
|
+
}
|
|
483
362
|
}
|
|
484
363
|
return undefined;
|
|
485
364
|
}
|
|
486
|
-
function
|
|
365
|
+
function textInAriaHidden(container, wanted) {
|
|
366
|
+
let inAriaHidden = false;
|
|
367
|
+
for (const el of elementsUnder(container)) {
|
|
368
|
+
if (isIgnored(el)) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (!isHiddenByAriaOnly(el)) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (fuzzyVisibleText(el.textContent ?? '', wanted)) {
|
|
375
|
+
inAriaHidden = true;
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (!inAriaHidden) {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
const memo = new Map();
|
|
383
|
+
for (const el of elementsUnder(container)) {
|
|
384
|
+
if (isIgnored(el)) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (!isRenderedOutsideAriaHidden(el)) {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (fuzzyVisibleText(visibleTextOf(el, memo), wanted)) {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
function buildEmptyHint(query, state, diagnose = true) {
|
|
487
397
|
const container = resolveContainer(query.scope).container ?? document.body;
|
|
488
398
|
const all = container.querySelectorAll(testIdSelector());
|
|
489
399
|
const present = [];
|
|
490
400
|
const seen = new Set();
|
|
491
401
|
for (const el of Array.from(all)) {
|
|
492
|
-
if (isIgnored(el))
|
|
402
|
+
if (isIgnored(el)) {
|
|
493
403
|
continue;
|
|
404
|
+
}
|
|
494
405
|
const id = readTestId(el);
|
|
495
|
-
if (null === id || 0 === id.length || seen.has(id))
|
|
406
|
+
if (null === id || 0 === id.length || seen.has(id)) {
|
|
496
407
|
continue;
|
|
408
|
+
}
|
|
497
409
|
seen.add(id);
|
|
498
|
-
if (present.length < MAX_PRESENT_TESTIDS)
|
|
410
|
+
if (present.length < MAX_PRESENT_TESTIDS) {
|
|
499
411
|
present.push(id);
|
|
412
|
+
}
|
|
500
413
|
}
|
|
501
414
|
const registered = declaredTestids();
|
|
502
415
|
const knownEmptyState = present.some((id) => registered.includes(id));
|
|
503
|
-
const route =
|
|
416
|
+
const route = pageRoute(false);
|
|
504
417
|
const hint = {
|
|
505
418
|
route,
|
|
506
419
|
presentTestids: present,
|
|
@@ -511,20 +424,34 @@ function buildEmptyHint(query) {
|
|
|
511
424
|
const wanted = wantedTextOf(query);
|
|
512
425
|
if (wanted !== undefined) {
|
|
513
426
|
const owner = splitTextOwner(container, wanted, query);
|
|
514
|
-
if (owner !== undefined)
|
|
515
|
-
|
|
427
|
+
if (owner !== undefined) {
|
|
428
|
+
const described = describe(owner);
|
|
429
|
+
const selector = stableSelectorFor(owner);
|
|
430
|
+
hint.splitText = selector === undefined ? described : { ...described, selector };
|
|
431
|
+
}
|
|
516
432
|
else if (true === query.matchCase) {
|
|
517
433
|
const shown = caseOnlyMatch(container, wanted);
|
|
518
|
-
if (shown !== undefined)
|
|
434
|
+
if (shown !== undefined) {
|
|
519
435
|
hint.caseMiss = shown;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (diagnose &&
|
|
440
|
+
wanted !== undefined &&
|
|
441
|
+
hint.splitText === undefined &&
|
|
442
|
+
(state === undefined || ElementState.VISIBLE === state || ElementState.IN_VIEWPORT === state)) {
|
|
443
|
+
if (textInAriaHidden(container, wanted)) {
|
|
444
|
+
hint.ariaHiddenMatch = true;
|
|
520
445
|
}
|
|
521
446
|
}
|
|
522
447
|
const near = nameNearMisses(container, query);
|
|
523
|
-
if (near.length > 0)
|
|
448
|
+
if (near.length > 0) {
|
|
524
449
|
hint.nameNearMiss = near;
|
|
450
|
+
}
|
|
525
451
|
const elsewhere = testidFoundUnder(container, query);
|
|
526
|
-
if (elsewhere !== undefined)
|
|
452
|
+
if (elsewhere !== undefined) {
|
|
527
453
|
hint.testidFoundUnder = elsewhere;
|
|
454
|
+
}
|
|
528
455
|
return hint;
|
|
529
456
|
}
|
|
530
457
|
export function elementsMatching(query) {
|
|
@@ -5,3 +5,4 @@ export declare function isCaptured(host: Element): boolean;
|
|
|
5
5
|
export declare function capturedRootOf(host: Element): ShadowRoot | null;
|
|
6
6
|
export declare function onShadowRoot(listener: (root: ShadowRoot) => void): () => void;
|
|
7
7
|
export declare function installShadowRegistry(): () => void;
|
|
8
|
+
export declare function parentAcrossShadowBoundary(el: Element): Element | null;
|
|
@@ -93,3 +93,30 @@ export function installShadowRegistry() {
|
|
|
93
93
|
listeners.clear();
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
|
+
function slotInCapturedClosedRoot(el) {
|
|
97
|
+
const host = el.parentElement;
|
|
98
|
+
if (null === host || null !== host.shadowRoot) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const root = capturedRootOf(host);
|
|
102
|
+
if (null === root) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
for (const slot of Array.from(root.querySelectorAll('slot'))) {
|
|
106
|
+
if (slot.assignedElements().includes(el)) {
|
|
107
|
+
return slot;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
export function parentAcrossShadowBoundary(el) {
|
|
113
|
+
const slot = el.assignedSlot ?? slotInCapturedClosedRoot(el);
|
|
114
|
+
if (null !== slot) {
|
|
115
|
+
return slot;
|
|
116
|
+
}
|
|
117
|
+
if (null !== el.parentElement) {
|
|
118
|
+
return el.parentElement;
|
|
119
|
+
}
|
|
120
|
+
const host = el.getRootNode().host;
|
|
121
|
+
return host ?? null;
|
|
122
|
+
}
|
package/dist/dom/snapshot.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { SnapshotMode } from '@reticlehq/core';
|
|
2
|
+
import { type LayoutFinding } from './layout-audit.js';
|
|
3
|
+
export declare const INTERACTIVE: Set<string>;
|
|
2
4
|
interface SnapshotStatus {
|
|
3
5
|
route: string;
|
|
4
6
|
title: string;
|
|
@@ -11,6 +13,7 @@ interface SnapshotResult {
|
|
|
11
13
|
nodes: number;
|
|
12
14
|
truncated: boolean;
|
|
13
15
|
scopeMissing?: boolean;
|
|
16
|
+
audit?: LayoutFinding[];
|
|
14
17
|
leanSkipped?: number;
|
|
15
18
|
hiddenSkipped?: number;
|
|
16
19
|
domElements?: number;
|
|
@@ -23,6 +26,8 @@ interface SnapshotOptions {
|
|
|
23
26
|
maxNodes?: number | undefined;
|
|
24
27
|
maxDepth?: number | undefined;
|
|
25
28
|
includeRoot?: boolean | undefined;
|
|
29
|
+
ids?: boolean | undefined;
|
|
30
|
+
audit?: boolean | undefined;
|
|
26
31
|
}
|
|
27
32
|
export declare const MAX_UNREAD_BRANCHES = 50;
|
|
28
33
|
export declare function buildSnapshot(options?: SnapshotOptions): SnapshotResult;
|