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