@reticlehq/browser 3.7.0 → 3.8.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 +10 -0
- package/dist/actions/action-report.js +29 -2
- 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 +35 -17
- 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 +20 -5
- 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 +2 -0
- package/dist/dom/a11y.js +191 -159
- 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 +43 -1
- 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.d.ts +2 -1
- package/dist/dom/query.js +97 -34
- 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 +85 -34
- 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/index.d.ts +2 -1
- package/dist/index.js +5 -0
- 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 +66 -18
- 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 +8 -1
- package/dist/observers/network.js +26 -6
- 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 +602 -611
- 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
|
@@ -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;
|
package/dist/dom/snapshot.js
CHANGED
|
@@ -3,8 +3,12 @@ import { capturedRootOf } from './shadow-registry.js';
|
|
|
3
3
|
import { isElement, isFrame } from './realm.js';
|
|
4
4
|
import { getAccessibleName, getRole, getStates, getValue, isVisible } from './a11y.js';
|
|
5
5
|
import { refs } from './addressing/refs.js';
|
|
6
|
+
import { readTestId } from './addressing/testid-attr.js';
|
|
7
|
+
import { identifyComponent } from '../registry/stores/adapters.js';
|
|
8
|
+
import { auditLayout } from './layout-audit.js';
|
|
9
|
+
import { pageRoute } from '../security/page-location.js';
|
|
6
10
|
import { isIgnored, isReticleOverlay } from './dom-ignore.js';
|
|
7
|
-
const INTERACTIVE = new Set([
|
|
11
|
+
export const INTERACTIVE = new Set([
|
|
8
12
|
'button',
|
|
9
13
|
'link',
|
|
10
14
|
'textbox',
|
|
@@ -23,8 +27,9 @@ const INTERACTIVE = new Set([
|
|
|
23
27
|
const SKIP_TAGS = new Set(['script', 'style', 'noscript', 'template', 'head', 'meta', 'link']);
|
|
24
28
|
const ANNOUNCE_ROLES = new Set(['alert', 'status']);
|
|
25
29
|
function announces(el, role) {
|
|
26
|
-
if (ANNOUNCE_ROLES.has(role))
|
|
30
|
+
if (ANNOUNCE_ROLES.has(role)) {
|
|
27
31
|
return true;
|
|
32
|
+
}
|
|
28
33
|
const live = el.getAttribute('aria-live');
|
|
29
34
|
return live !== null && 'off' !== live;
|
|
30
35
|
}
|
|
@@ -56,30 +61,36 @@ function readingParts(el) {
|
|
|
56
61
|
function isInlineIn(el) {
|
|
57
62
|
const view = el.ownerDocument.defaultView;
|
|
58
63
|
const parent = el.parentElement;
|
|
59
|
-
if (null === view || null === parent)
|
|
64
|
+
if (null === view || null === parent) {
|
|
60
65
|
return false;
|
|
66
|
+
}
|
|
61
67
|
const around = view.getComputedStyle(parent).display;
|
|
62
|
-
if (around.includes('flex') || around.includes('grid'))
|
|
68
|
+
if (around.includes('flex') || around.includes('grid')) {
|
|
63
69
|
return false;
|
|
70
|
+
}
|
|
64
71
|
return 'inline' === view.getComputedStyle(el).display;
|
|
65
72
|
}
|
|
66
73
|
function foldsIntoParent(el) {
|
|
67
|
-
if (skipEarly(el) || !isInlineIn(el))
|
|
74
|
+
if (skipEarly(el) || !isInlineIn(el)) {
|
|
68
75
|
return false;
|
|
76
|
+
}
|
|
69
77
|
const role = getRole(el);
|
|
70
78
|
return !INTERACTIVE.has(role) && !announces(el, role) && 0 === getAccessibleName(el).length;
|
|
71
79
|
}
|
|
72
80
|
export const MAX_UNREAD_BRANCHES = 50;
|
|
73
81
|
function hiddenByMarkup(el) {
|
|
74
|
-
if ('true' === el.getAttribute('aria-hidden'))
|
|
82
|
+
if ('true' === el.getAttribute('aria-hidden')) {
|
|
75
83
|
return true;
|
|
84
|
+
}
|
|
76
85
|
return el instanceof HTMLElement && el.hidden;
|
|
77
86
|
}
|
|
78
87
|
function skipEarly(el) {
|
|
79
|
-
if (SKIP_TAGS.has(el.tagName.toLowerCase()))
|
|
88
|
+
if (SKIP_TAGS.has(el.tagName.toLowerCase())) {
|
|
80
89
|
return true;
|
|
81
|
-
|
|
90
|
+
}
|
|
91
|
+
if (isIgnored(el)) {
|
|
82
92
|
return true;
|
|
93
|
+
}
|
|
83
94
|
return hiddenByMarkup(el);
|
|
84
95
|
}
|
|
85
96
|
function stateSuffix(el) {
|
|
@@ -90,7 +101,7 @@ function stateSuffix(el) {
|
|
|
90
101
|
s === ElementState.FOCUSED);
|
|
91
102
|
return states.length > 0 ? ` [${states.join(',')}]` : '';
|
|
92
103
|
}
|
|
93
|
-
function formatLine(el, depth, role, name, layout, ownText = '') {
|
|
104
|
+
function formatLine(el, depth, role, name, layout, ownText = '', withTestId = false) {
|
|
94
105
|
const indent = ' '.repeat(depth);
|
|
95
106
|
const value = getValue(el);
|
|
96
107
|
const label = name;
|
|
@@ -99,14 +110,27 @@ function formatLine(el, depth, role, name, layout, ownText = '') {
|
|
|
99
110
|
const valuePart = value !== undefined && value.length > 0 ? ` [value="${value}"]` : '';
|
|
100
111
|
const layoutPart = layout.length > 0 ? ` [${layout}]` : '';
|
|
101
112
|
const textPart = ownText.length > 0 ? ` [text="${ownText}"]` : '';
|
|
102
|
-
|
|
113
|
+
const testid = withTestId ? readTestId(el) : null;
|
|
114
|
+
const info = withTestId ? identifyComponent(el) : null;
|
|
115
|
+
const component = null === testid ? info?.componentStack[0] : undefined;
|
|
116
|
+
const where = info?.source;
|
|
117
|
+
let who = '';
|
|
118
|
+
if (null !== testid) {
|
|
119
|
+
who = ` [testid=${testid}]`;
|
|
120
|
+
}
|
|
121
|
+
else if (component !== undefined) {
|
|
122
|
+
who = ` [component=${component}]`;
|
|
123
|
+
}
|
|
124
|
+
const idPart = `${who}${where === undefined ? '' : ` [src=${where.file}:${String(where.line)}]`}`;
|
|
125
|
+
return `${indent}- ${role}${namePart}${refPart}${valuePart}${textPart}${layoutPart}${stateSuffix(el)}${idPart}`;
|
|
103
126
|
}
|
|
104
127
|
function formatTextLine(depth, text) {
|
|
105
128
|
return `${' '.repeat(depth)}- text "${text}"`;
|
|
106
129
|
}
|
|
107
130
|
function layoutSignature(style) {
|
|
108
|
-
if (null === style)
|
|
131
|
+
if (null === style) {
|
|
109
132
|
return '';
|
|
133
|
+
}
|
|
110
134
|
const display = style.display;
|
|
111
135
|
if ('grid' === display || 'inline-grid' === display) {
|
|
112
136
|
const cols = style.gridTemplateColumns;
|
|
@@ -127,13 +151,15 @@ function recordUnread(branches, ctx) {
|
|
|
127
151
|
function pierceChildren(parent) {
|
|
128
152
|
const out = [...parent.children];
|
|
129
153
|
const shadow = parent.shadowRoot ?? capturedRootOf(parent);
|
|
130
|
-
if (shadow !== null)
|
|
154
|
+
if (shadow !== null) {
|
|
131
155
|
out.push(...shadow.children);
|
|
156
|
+
}
|
|
132
157
|
if (isFrame(parent)) {
|
|
133
158
|
try {
|
|
134
159
|
const body = parent.contentDocument?.body;
|
|
135
|
-
if (body !== null && body !== undefined)
|
|
160
|
+
if (body !== null && body !== undefined) {
|
|
136
161
|
out.push(...body.children);
|
|
162
|
+
}
|
|
137
163
|
}
|
|
138
164
|
catch {
|
|
139
165
|
}
|
|
@@ -146,8 +172,9 @@ function visit(child, depth, ctx, inLive) {
|
|
|
146
172
|
!isIgnored(child) &&
|
|
147
173
|
!(child instanceof HTMLElement && child.hidden);
|
|
148
174
|
if (!stampedOnly && skipEarly(child)) {
|
|
149
|
-
if (hiddenByMarkup(child))
|
|
175
|
+
if (hiddenByMarkup(child)) {
|
|
150
176
|
ctx.hiddenSkipped += 1;
|
|
177
|
+
}
|
|
151
178
|
return;
|
|
152
179
|
}
|
|
153
180
|
const view = child.ownerDocument.defaultView;
|
|
@@ -170,13 +197,14 @@ function visit(child, depth, ctx, inLive) {
|
|
|
170
197
|
const actionable = interactive;
|
|
171
198
|
const meaningful = actionable || role !== 'generic' || name.length > 0 || text.length > 0 || layout.length > 0;
|
|
172
199
|
const include = lean ? actionable : meaningful;
|
|
173
|
-
if (lean && !include && meaningful)
|
|
200
|
+
if (lean && !include && meaningful) {
|
|
174
201
|
ctx.leanSkipped += 1;
|
|
202
|
+
}
|
|
175
203
|
if (include) {
|
|
176
204
|
ctx.nodes += 1;
|
|
177
205
|
ctx.lines.push('generic' === role && text.length > 0 && 0 === layout.length
|
|
178
206
|
? formatTextLine(depth, text)
|
|
179
|
-
: formatLine(child, depth, role, name, layout, text));
|
|
207
|
+
: formatLine(child, depth, role, name, layout, text, true === ctx.ids));
|
|
180
208
|
walk(child, depth + 1, ctx, announce);
|
|
181
209
|
}
|
|
182
210
|
else {
|
|
@@ -199,31 +227,36 @@ function walk(parent, depth, ctx, inLive = false) {
|
|
|
199
227
|
return;
|
|
200
228
|
}
|
|
201
229
|
const child = children[index];
|
|
202
|
-
if (child !== undefined)
|
|
230
|
+
if (child !== undefined) {
|
|
203
231
|
visit(child, depth, ctx, inLive);
|
|
232
|
+
}
|
|
204
233
|
}
|
|
205
234
|
}
|
|
206
235
|
function collectDialogs(root) {
|
|
207
236
|
const nodes = root.querySelectorAll('[role="dialog"], dialog[open], [aria-modal="true"]');
|
|
208
237
|
const names = [];
|
|
209
238
|
for (const node of nodes) {
|
|
210
|
-
if (isReticleOverlay(node))
|
|
239
|
+
if (isReticleOverlay(node)) {
|
|
211
240
|
continue;
|
|
212
|
-
|
|
241
|
+
}
|
|
242
|
+
if (isVisible(node)) {
|
|
213
243
|
names.push(getAccessibleName(node) || '(unnamed dialog)');
|
|
244
|
+
}
|
|
214
245
|
}
|
|
215
246
|
return names;
|
|
216
247
|
}
|
|
217
248
|
const MODAL_SELECTOR = '[role="dialog"], [role="alertdialog"], dialog[open], [aria-modal="true"]';
|
|
218
249
|
function overlayHidingPage(root) {
|
|
219
|
-
if (root !== document.body)
|
|
250
|
+
if (root !== document.body) {
|
|
220
251
|
return undefined;
|
|
252
|
+
}
|
|
221
253
|
const dialogs = document.body.querySelectorAll('[role="dialog"], dialog[open], [aria-modal="true"]');
|
|
222
254
|
let modal;
|
|
223
255
|
let hiddenCandidate;
|
|
224
256
|
for (const d of dialogs) {
|
|
225
|
-
if (isReticleOverlay(d))
|
|
257
|
+
if (isReticleOverlay(d)) {
|
|
226
258
|
continue;
|
|
259
|
+
}
|
|
227
260
|
if (isVisible(d)) {
|
|
228
261
|
modal = d;
|
|
229
262
|
break;
|
|
@@ -231,14 +264,17 @@ function overlayHidingPage(root) {
|
|
|
231
264
|
hiddenCandidate ??= d;
|
|
232
265
|
}
|
|
233
266
|
modal ??= hiddenCandidate;
|
|
234
|
-
if (modal === undefined)
|
|
267
|
+
if (modal === undefined) {
|
|
235
268
|
return undefined;
|
|
269
|
+
}
|
|
236
270
|
const modalEl = modal;
|
|
237
271
|
const outside = Array.from(document.body.children).filter((c) => !c.contains(modalEl));
|
|
238
|
-
if (0 === outside.length)
|
|
272
|
+
if (0 === outside.length) {
|
|
239
273
|
return undefined;
|
|
240
|
-
|
|
274
|
+
}
|
|
275
|
+
if (!outside.every((c) => 'true' === c.getAttribute('aria-hidden'))) {
|
|
241
276
|
return undefined;
|
|
277
|
+
}
|
|
242
278
|
return ('the rest of the page is aria-hidden behind an open overlay (a focus-trap modal), so this ' +
|
|
243
279
|
'snapshot shows only the overlay. If it will not close its cleanup may be stuck on a throttled ' +
|
|
244
280
|
'tab: dismiss the overlay, or snapshot with { scope } inside it.');
|
|
@@ -247,7 +283,7 @@ function buildStatus(root) {
|
|
|
247
283
|
const visibleDialogs = collectDialogs(root);
|
|
248
284
|
const overlay = overlayHidingPage(root);
|
|
249
285
|
return {
|
|
250
|
-
route:
|
|
286
|
+
route: pageRoute(),
|
|
251
287
|
title: document.title,
|
|
252
288
|
...(visibleDialogs.length > 0 ? { visibleDialogs } : {}),
|
|
253
289
|
...(overlay !== undefined ? { overlayHidingPage: overlay } : {}),
|
|
@@ -255,11 +291,20 @@ function buildStatus(root) {
|
|
|
255
291
|
}
|
|
256
292
|
export function buildSnapshot(options = {}) {
|
|
257
293
|
const mode = options.mode ?? SnapshotMode.FULL;
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
294
|
+
const scope = options.scope?.trim();
|
|
295
|
+
let scopeEl = document.body;
|
|
296
|
+
if (undefined !== scope && '' !== scope) {
|
|
297
|
+
scopeEl = refs.resolve(scope);
|
|
298
|
+
if (null === scopeEl) {
|
|
299
|
+
try {
|
|
300
|
+
scopeEl = document.querySelector(scope);
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const scopeMissing = null === scopeEl;
|
|
307
|
+
const root = scopeEl ?? document.body;
|
|
263
308
|
const status = buildStatus(root);
|
|
264
309
|
if (scopeMissing) {
|
|
265
310
|
return { tree: '', status, nodes: 0, truncated: false, scopeMissing: true };
|
|
@@ -278,16 +323,21 @@ export function buildSnapshot(options = {}) {
|
|
|
278
323
|
maxDepth: options.maxDepth ?? 20,
|
|
279
324
|
unread: [],
|
|
280
325
|
unreadOverflow: false,
|
|
326
|
+
...(true === options.ids ? { ids: true } : {}),
|
|
281
327
|
};
|
|
282
|
-
if (true === options.includeRoot)
|
|
328
|
+
if (true === options.includeRoot) {
|
|
283
329
|
visit(root, 0, ctx, false);
|
|
284
|
-
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
285
332
|
walk(root, 0, ctx);
|
|
333
|
+
}
|
|
286
334
|
if (0 === ctx.nodes && root === document.body) {
|
|
287
335
|
ctx.throughOverlay = true;
|
|
288
|
-
for (const child of Array.from(root.children))
|
|
289
|
-
if (overlayStamped(child) && null !== child.querySelector(MODAL_SELECTOR))
|
|
336
|
+
for (const child of Array.from(root.children)) {
|
|
337
|
+
if (overlayStamped(child) && null !== child.querySelector(MODAL_SELECTOR)) {
|
|
290
338
|
visit(child, 0, ctx, false);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
291
341
|
ctx.throughOverlay = false;
|
|
292
342
|
}
|
|
293
343
|
return {
|
|
@@ -300,5 +350,6 @@ export function buildSnapshot(options = {}) {
|
|
|
300
350
|
...(ctx.hiddenSkipped > 0 ? { hiddenSkipped: ctx.hiddenSkipped } : {}),
|
|
301
351
|
...(ctx.unread.length > 0 ? { unread: ctx.unread } : {}),
|
|
302
352
|
...(ctx.unreadOverflow ? { unreadOverflow: true } : {}),
|
|
353
|
+
...(true === options.audit ? { audit: auditLayout(root) } : {}),
|
|
303
354
|
};
|
|
304
355
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function stableSelectorFor(el: HTMLElement): string | undefined;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { getTestIdAttr, readTestId } from './addressing/testid-attr.js';
|
|
2
|
+
export function stableSelectorFor(el) {
|
|
3
|
+
const candidate = candidateSelectorFor(el);
|
|
4
|
+
if (undefined === candidate)
|
|
5
|
+
return undefined;
|
|
6
|
+
const matches = el.ownerDocument.querySelectorAll(candidate);
|
|
7
|
+
return 1 === matches.length && matches[0] === el ? candidate : undefined;
|
|
8
|
+
}
|
|
9
|
+
function candidateSelectorFor(el) {
|
|
10
|
+
const testid = readTestId(el);
|
|
11
|
+
if (null !== testid && 0 < testid.length) {
|
|
12
|
+
const escaped = testid.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
13
|
+
return `[${getTestIdAttr()}="${escaped}"]`;
|
|
14
|
+
}
|
|
15
|
+
const id = el.getAttribute('id');
|
|
16
|
+
if (null !== id && /^[A-Za-z_][A-Za-z0-9_-]*$/.test(id))
|
|
17
|
+
return `#${id}`;
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ElementQuery } from '@reticlehq/core';
|
|
2
|
+
export declare function normaliseVisibleText(value: string): string;
|
|
3
|
+
export declare function foldTypography(value: string): string;
|
|
4
|
+
export declare function comparableText(value: string): string;
|
|
5
|
+
export declare function fuzzyVisibleText(actual: string, expected: string): boolean;
|
|
6
|
+
export declare function textMatches(actual: string, query: ElementQuery, expected: string): boolean;
|
|
7
|
+
export declare function exactVisibleText(actual: string, expected: string): boolean;
|
|
8
|
+
export declare function paintedCase(el: Element, text: string): string | undefined;
|
|
9
|
+
export declare const UNREADABLE_CONTRAST = 1.5;
|
|
10
|
+
export declare const UNREADABLE_OPACITY = 0.05;
|
|
11
|
+
export declare function unreadableText(el: Element): string | undefined;
|
|
12
|
+
export declare function unreadableMatch(matches: readonly Element[]): string | undefined;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { parentAcrossShadowBoundary } from './shadow-registry.js';
|
|
2
|
+
export function normaliseVisibleText(value) {
|
|
3
|
+
return value.replace(/\s+/g, ' ').trim().normalize('NFC');
|
|
4
|
+
}
|
|
5
|
+
const SINGLE_QUOTES = /[\u2018\u2019]/g;
|
|
6
|
+
const DOUBLE_QUOTES = /[\u201c\u201d]/g;
|
|
7
|
+
const DASHES = /[\u2013\u2014]/g;
|
|
8
|
+
export function foldTypography(value) {
|
|
9
|
+
return value.replace(SINGLE_QUOTES, "'").replace(DOUBLE_QUOTES, '"').replace(DASHES, '-');
|
|
10
|
+
}
|
|
11
|
+
export function comparableText(value) {
|
|
12
|
+
return foldTypography(normaliseVisibleText(value));
|
|
13
|
+
}
|
|
14
|
+
export function fuzzyVisibleText(actual, expected) {
|
|
15
|
+
return comparableText(actual).toLowerCase().includes(comparableText(expected).toLowerCase());
|
|
16
|
+
}
|
|
17
|
+
export function textMatches(actual, query, expected) {
|
|
18
|
+
if (true !== query.matchCase) {
|
|
19
|
+
return fuzzyVisibleText(actual, expected);
|
|
20
|
+
}
|
|
21
|
+
return comparableText(actual).includes(comparableText(expected));
|
|
22
|
+
}
|
|
23
|
+
export function exactVisibleText(actual, expected) {
|
|
24
|
+
return comparableText(actual) === comparableText(expected);
|
|
25
|
+
}
|
|
26
|
+
const CASE_TRANSFORMS = {
|
|
27
|
+
uppercase: (text) => text.toUpperCase(),
|
|
28
|
+
lowercase: (text) => text.toLowerCase(),
|
|
29
|
+
capitalize: (text) => text.replace(/(^|\s)(\p{L})/gu, (_, gap, c) => gap + c.toUpperCase()),
|
|
30
|
+
};
|
|
31
|
+
export function paintedCase(el, text) {
|
|
32
|
+
const transform = el.ownerDocument.defaultView?.getComputedStyle(el).textTransform ?? '';
|
|
33
|
+
return CASE_TRANSFORMS[transform]?.(text);
|
|
34
|
+
}
|
|
35
|
+
export const UNREADABLE_CONTRAST = 1.5;
|
|
36
|
+
export const UNREADABLE_OPACITY = 0.05;
|
|
37
|
+
const CANVAS = [255, 255, 255, 1];
|
|
38
|
+
function parseRgb(value) {
|
|
39
|
+
const m = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:[\s,/]+([\d.]+))?\s*\)$/.exec(value);
|
|
40
|
+
if (null === m) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
return [Number(m[1]), Number(m[2]), Number(m[3]), m[4] === undefined ? 1 : Number(m[4])];
|
|
44
|
+
}
|
|
45
|
+
function over(top, under) {
|
|
46
|
+
const a = top[3];
|
|
47
|
+
const mix = (t, u) => t * a + u * (1 - a);
|
|
48
|
+
return [mix(top[0], under[0]), mix(top[1], under[1]), mix(top[2], under[2]), 1];
|
|
49
|
+
}
|
|
50
|
+
function linear(v) {
|
|
51
|
+
const s = v / 255;
|
|
52
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
53
|
+
}
|
|
54
|
+
function luminance(c) {
|
|
55
|
+
return 0.2126 * linear(c[0]) + 0.7152 * linear(c[1]) + 0.0722 * linear(c[2]);
|
|
56
|
+
}
|
|
57
|
+
function contrast(a, b) {
|
|
58
|
+
const la = luminance(a);
|
|
59
|
+
const lb = luminance(b);
|
|
60
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
61
|
+
}
|
|
62
|
+
function show(c) {
|
|
63
|
+
return `rgb(${[c[0], c[1], c[2]].map((v) => String(Math.round(v))).join(', ')})`;
|
|
64
|
+
}
|
|
65
|
+
function outlined(style) {
|
|
66
|
+
const stroke = Number.parseFloat(style.getPropertyValue('-webkit-text-stroke-width'));
|
|
67
|
+
return /\dpx/.test(style.textShadow) || stroke > 0;
|
|
68
|
+
}
|
|
69
|
+
export function unreadableText(el) {
|
|
70
|
+
const view = el.ownerDocument.defaultView;
|
|
71
|
+
if (null === view) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
const own = view.getComputedStyle(el);
|
|
75
|
+
const ink = parseRgb(own.color);
|
|
76
|
+
if (ink === undefined || outlined(own)) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
let opacity = 1;
|
|
80
|
+
const layers = [];
|
|
81
|
+
let opaque = false;
|
|
82
|
+
for (let node = el; null !== node; node = parentAcrossShadowBoundary(node)) {
|
|
83
|
+
const style = view.getComputedStyle(node);
|
|
84
|
+
const alpha = Number.parseFloat(style.opacity);
|
|
85
|
+
opacity *= Number.isNaN(alpha) ? 1 : alpha;
|
|
86
|
+
if (opaque) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if ('' !== style.backgroundImage && 'none' !== style.backgroundImage) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const fill = parseRgb(style.backgroundColor);
|
|
93
|
+
if (fill === undefined) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
if (fill[3] > 0) {
|
|
97
|
+
layers.push(fill);
|
|
98
|
+
}
|
|
99
|
+
opaque = fill[3] >= 1;
|
|
100
|
+
}
|
|
101
|
+
if (opacity < UNREADABLE_OPACITY) {
|
|
102
|
+
return `the composed opacity is ${opacity.toFixed(2)}`;
|
|
103
|
+
}
|
|
104
|
+
const back = layers.reduceRight((under, top) => over(top, under), CANVAS);
|
|
105
|
+
const painted = over(ink, back);
|
|
106
|
+
const ratio = contrast(painted, back);
|
|
107
|
+
if (ratio >= UNREADABLE_CONTRAST) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
return `text ${show(painted)} on ${show(back)}: contrast ${ratio.toFixed(2)}:1`;
|
|
111
|
+
}
|
|
112
|
+
export function unreadableMatch(matches) {
|
|
113
|
+
const reasons = matches.map(unreadableText);
|
|
114
|
+
return reasons.length > 0 && reasons.every((r) => r !== undefined) ? reasons[0] : undefined;
|
|
115
|
+
}
|
package/dist/dom/theme.d.ts
CHANGED
package/dist/dom/theme.js
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
const THEME_ATTRIBUTE = 'data-theme';
|
|
2
|
-
|
|
2
|
+
const MAX_TOKEN_RULE_DEPTH = 8;
|
|
3
|
+
function isGroupingRule(rule) {
|
|
4
|
+
return 'cssRules' in rule && rule.cssRules instanceof CSSRuleList;
|
|
5
|
+
}
|
|
6
|
+
function collectTokenNames(rules, names, depth) {
|
|
7
|
+
if (depth <= 0)
|
|
8
|
+
return;
|
|
9
|
+
for (const rule of Array.from(rules)) {
|
|
10
|
+
if (rule instanceof CSSStyleRule) {
|
|
11
|
+
for (const prop of Array.from(rule.style)) {
|
|
12
|
+
if (prop.startsWith('--'))
|
|
13
|
+
names.add(prop);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
else if (isGroupingRule(rule)) {
|
|
17
|
+
collectTokenNames(rule.cssRules, names, depth - 1);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function tokenNames() {
|
|
3
22
|
const names = new Set();
|
|
4
23
|
for (const sheet of Array.from(document.styleSheets)) {
|
|
5
24
|
let rules = null;
|
|
@@ -11,14 +30,7 @@ function tokenNames() {
|
|
|
11
30
|
}
|
|
12
31
|
if (null === rules)
|
|
13
32
|
continue;
|
|
14
|
-
|
|
15
|
-
if (!(rule instanceof CSSStyleRule))
|
|
16
|
-
continue;
|
|
17
|
-
for (const prop of Array.from(rule.style)) {
|
|
18
|
-
if (prop.startsWith('--'))
|
|
19
|
-
names.add(prop);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
33
|
+
collectTokenNames(rules, names, MAX_TOKEN_RULE_DEPTH);
|
|
22
34
|
}
|
|
23
35
|
return names;
|
|
24
36
|
}
|
package/dist/dom/virtualized.js
CHANGED
|
@@ -25,8 +25,12 @@ export function unmountedRowsIn(container) {
|
|
|
25
25
|
let top = Infinity;
|
|
26
26
|
let bottom = 0;
|
|
27
27
|
let totalHeight = 0;
|
|
28
|
+
const bounds = container.getBoundingClientRect();
|
|
29
|
+
const scaleY = container.offsetHeight > 0 && bounds.height > 0 ? bounds.height / container.offsetHeight : 1;
|
|
28
30
|
for (const row of rows) {
|
|
29
|
-
const rowTop = row.
|
|
31
|
+
const rowTop = (row.getBoundingClientRect().top - bounds.top) / scaleY -
|
|
32
|
+
container.clientTop +
|
|
33
|
+
container.scrollTop;
|
|
30
34
|
const rowBottom = rowTop + row.offsetHeight;
|
|
31
35
|
if (rowTop < top)
|
|
32
36
|
top = rowTop;
|
package/dist/index.d.ts
CHANGED
|
@@ -18,10 +18,11 @@ export { registerReticleDomain } from './registry/domains.js';
|
|
|
18
18
|
export type { ReticleDomain } from './registry/domains.js';
|
|
19
19
|
export { buildSnapshot } from './dom/snapshot.js';
|
|
20
20
|
export { inspectChart, canvasChartData } from './dom/chart.js';
|
|
21
|
-
export type { ChartReport, ChartFinding, CanvasChartData } from './dom/chart.js';
|
|
21
|
+
export type { ChartReport, ChartFinding, CanvasChartData, ChartGlobals } from './dom/chart.js';
|
|
22
22
|
export { matchQuery, runQuery } from './dom/query.js';
|
|
23
23
|
export { executeAction, executeSequence } from './actions/actions.js';
|
|
24
24
|
export { describe, getRole, getAccessibleName, getStates, isVisible } from './dom/a11y.js';
|
|
25
25
|
export { refs, RefRegistry } from './dom/addressing/refs.js';
|
|
26
26
|
export { Annotator, installAnnotator, type AnnotatorDeps } from './review/annotator.js';
|
|
27
27
|
export { resolveMarkAnchor, type MarkAnchor } from './review/mark-anchor.js';
|
|
28
|
+
export declare const trackShortTimers: (maxMs?: number) => (() => void);
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { reticle } from './reticle-singleton.js';
|
|
2
|
+
import { reticle } from './reticle-singleton.js';
|
|
3
|
+
import { trackShortTimers as trackTimers } from './busy.js';
|
|
2
4
|
export { Reticle } from './reticle.js';
|
|
3
5
|
export { SESSION_AUTO } from '@reticlehq/core';
|
|
4
6
|
export { setIgnoreSelectors } from './dom/dom-ignore.js';
|
|
@@ -17,3 +19,6 @@ export { describe, getRole, getAccessibleName, getStates, isVisible } from './do
|
|
|
17
19
|
export { refs, RefRegistry } from './dom/addressing/refs.js';
|
|
18
20
|
export { Annotator, installAnnotator } from './review/annotator.js';
|
|
19
21
|
export { resolveMarkAnchor } from './review/mark-anchor.js';
|
|
22
|
+
export const trackShortTimers = (maxMs) => trackTimers((name, data) => {
|
|
23
|
+
reticle.signal(name, data);
|
|
24
|
+
}, maxMs);
|
|
@@ -58,10 +58,11 @@ export function installBlindSpots(emit) {
|
|
|
58
58
|
const report = () => {
|
|
59
59
|
for (const sensor of sensors) {
|
|
60
60
|
const count = sensor.count();
|
|
61
|
-
|
|
61
|
+
const previous = last.get(sensor.kind);
|
|
62
|
+
if (count === previous)
|
|
62
63
|
continue;
|
|
63
64
|
last.set(sensor.kind, count);
|
|
64
|
-
if (count > 0)
|
|
65
|
+
if (count > 0 || previous !== undefined)
|
|
65
66
|
emit(EventType.BLIND_SPOT, { kind: sensor.kind, count });
|
|
66
67
|
}
|
|
67
68
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { CanvasTrigger } from '@reticlehq/core';
|
|
2
|
+
export { CanvasTrigger };
|
|
3
|
+
import { type Emit, type Teardown } from './types.js';
|
|
4
|
+
export interface CanvasSample {
|
|
5
|
+
data: Uint8ClampedArray;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function digestPixels(data: Uint8ClampedArray): {
|
|
10
|
+
digest: string;
|
|
11
|
+
mean: [number, number, number];
|
|
12
|
+
};
|
|
13
|
+
interface CanvasOptions {
|
|
14
|
+
sample?: (canvas: HTMLCanvasElement) => CanvasSample | undefined;
|
|
15
|
+
every?: (run: () => void, ms: number) => () => void;
|
|
16
|
+
}
|
|
17
|
+
export declare function canvasAfterAct(): void;
|
|
18
|
+
export declare function installCanvas(emit: Emit, opts?: CanvasOptions): Teardown;
|