@reticlehq/browser 3.6.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 +64 -0
- package/dist/actions/action-report.js +87 -0
- package/dist/actions/actions-dom.d.ts +5 -0
- package/dist/actions/actions-dom.js +32 -4
- package/dist/actions/actions-press.d.ts +1 -0
- package/dist/actions/actions-press.js +25 -1
- package/dist/actions/actions.d.ts +2 -37
- package/dist/actions/actions.js +56 -80
- package/dist/actions/appeared-text.js +5 -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 +4 -1
- package/dist/actions/danger-context.js +45 -4
- package/dist/actions/value-input.d.ts +1 -0
- package/dist/actions/value-input.js +11 -0
- 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 +49 -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 +353 -80
- package/dist/dom/addressing/coverage-markers.d.ts +6 -0
- package/dist/dom/addressing/coverage-markers.js +13 -0
- package/dist/dom/chart.d.ts +3 -2
- package/dist/dom/chart.js +3 -2
- package/dist/dom/dom-ignore.d.ts +8 -0
- package/dist/dom/dom-ignore.js +57 -2
- 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 +163 -38
- package/dist/dom/render-page.d.ts +12 -0
- package/dist/dom/render-page.js +135 -0
- 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 +135 -36
- 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 +96 -32
- package/dist/observers/field.d.ts +11 -0
- package/dist/observers/field.js +116 -30
- 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 +82 -13
- package/dist/observers/perf.js +53 -1
- package/dist/observers/route.d.ts +2 -0
- package/dist/observers/route.js +39 -9
- package/dist/presenter/carousel/carousel.js +2 -7
- package/dist/presenter/carousel/panel-slides.d.ts +2 -4
- package/dist/presenter/carousel/panel-slides.js +17 -15
- package/dist/presenter/chrome/log-memory.d.ts +1 -0
- package/dist/presenter/chrome/presenter-log.d.ts +7 -0
- package/dist/presenter/chrome/presenter-log.js +48 -8
- package/dist/presenter/chrome/presenter-morph.d.ts +1 -0
- package/dist/presenter/chrome/presenter-morph.js +34 -0
- package/dist/presenter/chrome/presenter-report-copy.d.ts +12 -4
- package/dist/presenter/chrome/presenter-report-copy.js +12 -4
- package/dist/presenter/hud-telemetry.js +55 -4
- package/dist/presenter/presenter-account-styles.js +3 -3
- package/dist/presenter/presenter-account.d.ts +27 -5
- package/dist/presenter/presenter-account.js +83 -6
- package/dist/presenter/presenter-agent-link.d.ts +9 -0
- package/dist/presenter/presenter-agent-link.js +62 -0
- package/dist/presenter/presenter-chat-views.d.ts +25 -4
- package/dist/presenter/presenter-chat-views.js +339 -72
- package/dist/presenter/presenter-config.d.ts +3 -0
- package/dist/presenter/presenter-config.js +2 -0
- package/dist/presenter/presenter-controls.d.ts +0 -1
- package/dist/presenter/presenter-controls.js +13 -91
- package/dist/presenter/presenter-design-tokens.js +1 -1
- package/dist/presenter/presenter-frame.d.ts +3 -2
- package/dist/presenter/presenter-frame.js +31 -24
- package/dist/presenter/presenter-harness-row.d.ts +56 -0
- package/dist/presenter/presenter-harness-row.js +170 -0
- package/dist/presenter/presenter-personas.d.ts +35 -0
- package/dist/presenter/presenter-personas.js +95 -0
- package/dist/presenter/presenter-plan.js +25 -24
- package/dist/presenter/presenter-report-styles.js +23 -22
- package/dist/presenter/presenter-report.js +29 -13
- package/dist/presenter/presenter-run-state.js +2 -1
- package/dist/presenter/presenter-settings-styles.js +8 -4
- package/dist/presenter/presenter-settings.d.ts +4 -4
- package/dist/presenter/presenter-settings.js +66 -28
- package/dist/presenter/presenter-shell-styles.js +12 -3
- package/dist/presenter/presenter-shell.d.ts +11 -3
- package/dist/presenter/presenter-shell.js +60 -22
- package/dist/presenter/presenter-styles.js +0 -2
- package/dist/presenter/presenter-workspace.js +10 -3
- package/dist/presenter/presenter.d.ts +3 -2
- package/dist/presenter/presenter.js +107 -25
- package/dist/registry/declared-platform.js +2 -0
- package/dist/registry/store-adapters.d.ts +5 -2
- package/dist/registry/store-adapters.js +40 -10
- package/dist/reticle-inject.js +771 -703
- package/dist/reticle.d.ts +4 -2
- package/dist/reticle.js +72 -8
- 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 +4 -2
- package/dist/storage-keys.js +8 -3
- 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 +23 -3
- package/dist/transport/unreachable-message.d.ts +1 -1
- package/dist/transport/unreachable-message.js +3 -5
- package/package.json +6 -2
- package/dist/presenter/carousel/offer-card.d.ts +0 -21
- package/dist/presenter/carousel/offer-card.js +0 -56
package/dist/dom/snapshot.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { ElementState, SnapshotMode } from '@reticlehq/core';
|
|
2
2
|
import { capturedRootOf } from './shadow-registry.js';
|
|
3
|
-
import { isFrame } from './realm.js';
|
|
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,32 +27,70 @@ 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
|
}
|
|
31
36
|
const TEXT_MAX = 80;
|
|
32
37
|
function directText(el) {
|
|
33
|
-
|
|
38
|
+
const parts = readingParts(el);
|
|
39
|
+
const collapsed = (parts.own ? parts.text : '').replace(/\s+/g, ' ').trim();
|
|
40
|
+
return collapsed.length > TEXT_MAX ? `${collapsed.slice(0, TEXT_MAX)}…` : collapsed;
|
|
41
|
+
}
|
|
42
|
+
function readingParts(el) {
|
|
43
|
+
let text = '';
|
|
44
|
+
let own = false;
|
|
34
45
|
for (const node of el.childNodes) {
|
|
35
|
-
if (3 === node.nodeType)
|
|
36
|
-
|
|
46
|
+
if (3 === node.nodeType) {
|
|
47
|
+
text += node.textContent ?? '';
|
|
48
|
+
own ||= 0 < (node.textContent ?? '').trim().length;
|
|
49
|
+
}
|
|
50
|
+
else if (isElement(node) && foldsIntoParent(node)) {
|
|
51
|
+
const inner = readingParts(node);
|
|
52
|
+
text += inner.text;
|
|
53
|
+
own ||= inner.own;
|
|
54
|
+
}
|
|
55
|
+
else if (isElement(node) && isInlineIn(node) && INTERACTIVE.has(getRole(node))) {
|
|
56
|
+
text += getAccessibleName(node);
|
|
57
|
+
}
|
|
37
58
|
}
|
|
38
|
-
|
|
39
|
-
|
|
59
|
+
return { text, own };
|
|
60
|
+
}
|
|
61
|
+
function isInlineIn(el) {
|
|
62
|
+
const view = el.ownerDocument.defaultView;
|
|
63
|
+
const parent = el.parentElement;
|
|
64
|
+
if (null === view || null === parent) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
const around = view.getComputedStyle(parent).display;
|
|
68
|
+
if (around.includes('flex') || around.includes('grid')) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return 'inline' === view.getComputedStyle(el).display;
|
|
72
|
+
}
|
|
73
|
+
function foldsIntoParent(el) {
|
|
74
|
+
if (skipEarly(el) || !isInlineIn(el)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const role = getRole(el);
|
|
78
|
+
return !INTERACTIVE.has(role) && !announces(el, role) && 0 === getAccessibleName(el).length;
|
|
40
79
|
}
|
|
41
80
|
export const MAX_UNREAD_BRANCHES = 50;
|
|
42
81
|
function hiddenByMarkup(el) {
|
|
43
|
-
if ('true' === el.getAttribute('aria-hidden'))
|
|
82
|
+
if ('true' === el.getAttribute('aria-hidden')) {
|
|
44
83
|
return true;
|
|
84
|
+
}
|
|
45
85
|
return el instanceof HTMLElement && el.hidden;
|
|
46
86
|
}
|
|
47
87
|
function skipEarly(el) {
|
|
48
|
-
if (SKIP_TAGS.has(el.tagName.toLowerCase()))
|
|
88
|
+
if (SKIP_TAGS.has(el.tagName.toLowerCase())) {
|
|
49
89
|
return true;
|
|
50
|
-
|
|
90
|
+
}
|
|
91
|
+
if (isIgnored(el)) {
|
|
51
92
|
return true;
|
|
93
|
+
}
|
|
52
94
|
return hiddenByMarkup(el);
|
|
53
95
|
}
|
|
54
96
|
function stateSuffix(el) {
|
|
@@ -59,7 +101,7 @@ function stateSuffix(el) {
|
|
|
59
101
|
s === ElementState.FOCUSED);
|
|
60
102
|
return states.length > 0 ? ` [${states.join(',')}]` : '';
|
|
61
103
|
}
|
|
62
|
-
function formatLine(el, depth, role, name, layout, ownText = '') {
|
|
104
|
+
function formatLine(el, depth, role, name, layout, ownText = '', withTestId = false) {
|
|
63
105
|
const indent = ' '.repeat(depth);
|
|
64
106
|
const value = getValue(el);
|
|
65
107
|
const label = name;
|
|
@@ -68,14 +110,27 @@ function formatLine(el, depth, role, name, layout, ownText = '') {
|
|
|
68
110
|
const valuePart = value !== undefined && value.length > 0 ? ` [value="${value}"]` : '';
|
|
69
111
|
const layoutPart = layout.length > 0 ? ` [${layout}]` : '';
|
|
70
112
|
const textPart = ownText.length > 0 ? ` [text="${ownText}"]` : '';
|
|
71
|
-
|
|
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}`;
|
|
72
126
|
}
|
|
73
127
|
function formatTextLine(depth, text) {
|
|
74
128
|
return `${' '.repeat(depth)}- text "${text}"`;
|
|
75
129
|
}
|
|
76
130
|
function layoutSignature(style) {
|
|
77
|
-
if (null === style)
|
|
131
|
+
if (null === style) {
|
|
78
132
|
return '';
|
|
133
|
+
}
|
|
79
134
|
const display = style.display;
|
|
80
135
|
if ('grid' === display || 'inline-grid' === display) {
|
|
81
136
|
const cols = style.gridTemplateColumns;
|
|
@@ -83,6 +138,7 @@ function layoutSignature(style) {
|
|
|
83
138
|
}
|
|
84
139
|
return '';
|
|
85
140
|
}
|
|
141
|
+
const overlayStamped = (el) => el.hasAttribute('data-aria-hidden') && 'true' === el.getAttribute('aria-hidden');
|
|
86
142
|
function recordUnread(branches, ctx) {
|
|
87
143
|
for (const branch of branches) {
|
|
88
144
|
if (ctx.unread.length >= MAX_UNREAD_BRANCHES) {
|
|
@@ -95,13 +151,15 @@ function recordUnread(branches, ctx) {
|
|
|
95
151
|
function pierceChildren(parent) {
|
|
96
152
|
const out = [...parent.children];
|
|
97
153
|
const shadow = parent.shadowRoot ?? capturedRootOf(parent);
|
|
98
|
-
if (shadow !== null)
|
|
154
|
+
if (shadow !== null) {
|
|
99
155
|
out.push(...shadow.children);
|
|
156
|
+
}
|
|
100
157
|
if (isFrame(parent)) {
|
|
101
158
|
try {
|
|
102
159
|
const body = parent.contentDocument?.body;
|
|
103
|
-
if (body !== null && body !== undefined)
|
|
160
|
+
if (body !== null && body !== undefined) {
|
|
104
161
|
out.push(...body.children);
|
|
162
|
+
}
|
|
105
163
|
}
|
|
106
164
|
catch {
|
|
107
165
|
}
|
|
@@ -109,9 +167,14 @@ function pierceChildren(parent) {
|
|
|
109
167
|
return out;
|
|
110
168
|
}
|
|
111
169
|
function visit(child, depth, ctx, inLive) {
|
|
112
|
-
|
|
113
|
-
|
|
170
|
+
const stampedOnly = true === ctx.throughOverlay &&
|
|
171
|
+
overlayStamped(child) &&
|
|
172
|
+
!isIgnored(child) &&
|
|
173
|
+
!(child instanceof HTMLElement && child.hidden);
|
|
174
|
+
if (!stampedOnly && skipEarly(child)) {
|
|
175
|
+
if (hiddenByMarkup(child)) {
|
|
114
176
|
ctx.hiddenSkipped += 1;
|
|
177
|
+
}
|
|
115
178
|
return;
|
|
116
179
|
}
|
|
117
180
|
const view = child.ownerDocument.defaultView;
|
|
@@ -120,6 +183,10 @@ function visit(child, depth, ctx, inLive) {
|
|
|
120
183
|
ctx.hiddenSkipped += 1;
|
|
121
184
|
return;
|
|
122
185
|
}
|
|
186
|
+
if (style !== null && 'inline' === style.display && !inLive && foldsIntoParent(child)) {
|
|
187
|
+
walk(child, depth, ctx, inLive);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
123
190
|
const role = getRole(child);
|
|
124
191
|
const name = getAccessibleName(child);
|
|
125
192
|
const interactive = INTERACTIVE.has(role);
|
|
@@ -130,13 +197,14 @@ function visit(child, depth, ctx, inLive) {
|
|
|
130
197
|
const actionable = interactive;
|
|
131
198
|
const meaningful = actionable || role !== 'generic' || name.length > 0 || text.length > 0 || layout.length > 0;
|
|
132
199
|
const include = lean ? actionable : meaningful;
|
|
133
|
-
if (lean && !include && meaningful)
|
|
200
|
+
if (lean && !include && meaningful) {
|
|
134
201
|
ctx.leanSkipped += 1;
|
|
202
|
+
}
|
|
135
203
|
if (include) {
|
|
136
204
|
ctx.nodes += 1;
|
|
137
205
|
ctx.lines.push('generic' === role && text.length > 0 && 0 === layout.length
|
|
138
206
|
? formatTextLine(depth, text)
|
|
139
|
-
: formatLine(child, depth, role, name, layout, text));
|
|
207
|
+
: formatLine(child, depth, role, name, layout, text, true === ctx.ids));
|
|
140
208
|
walk(child, depth + 1, ctx, announce);
|
|
141
209
|
}
|
|
142
210
|
else {
|
|
@@ -159,30 +227,36 @@ function walk(parent, depth, ctx, inLive = false) {
|
|
|
159
227
|
return;
|
|
160
228
|
}
|
|
161
229
|
const child = children[index];
|
|
162
|
-
if (child !== undefined)
|
|
230
|
+
if (child !== undefined) {
|
|
163
231
|
visit(child, depth, ctx, inLive);
|
|
232
|
+
}
|
|
164
233
|
}
|
|
165
234
|
}
|
|
166
235
|
function collectDialogs(root) {
|
|
167
236
|
const nodes = root.querySelectorAll('[role="dialog"], dialog[open], [aria-modal="true"]');
|
|
168
237
|
const names = [];
|
|
169
238
|
for (const node of nodes) {
|
|
170
|
-
if (isReticleOverlay(node))
|
|
239
|
+
if (isReticleOverlay(node)) {
|
|
171
240
|
continue;
|
|
172
|
-
|
|
241
|
+
}
|
|
242
|
+
if (isVisible(node)) {
|
|
173
243
|
names.push(getAccessibleName(node) || '(unnamed dialog)');
|
|
244
|
+
}
|
|
174
245
|
}
|
|
175
246
|
return names;
|
|
176
247
|
}
|
|
248
|
+
const MODAL_SELECTOR = '[role="dialog"], [role="alertdialog"], dialog[open], [aria-modal="true"]';
|
|
177
249
|
function overlayHidingPage(root) {
|
|
178
|
-
if (root !== document.body)
|
|
250
|
+
if (root !== document.body) {
|
|
179
251
|
return undefined;
|
|
252
|
+
}
|
|
180
253
|
const dialogs = document.body.querySelectorAll('[role="dialog"], dialog[open], [aria-modal="true"]');
|
|
181
254
|
let modal;
|
|
182
255
|
let hiddenCandidate;
|
|
183
256
|
for (const d of dialogs) {
|
|
184
|
-
if (isReticleOverlay(d))
|
|
257
|
+
if (isReticleOverlay(d)) {
|
|
185
258
|
continue;
|
|
259
|
+
}
|
|
186
260
|
if (isVisible(d)) {
|
|
187
261
|
modal = d;
|
|
188
262
|
break;
|
|
@@ -190,14 +264,17 @@ function overlayHidingPage(root) {
|
|
|
190
264
|
hiddenCandidate ??= d;
|
|
191
265
|
}
|
|
192
266
|
modal ??= hiddenCandidate;
|
|
193
|
-
if (modal === undefined)
|
|
267
|
+
if (modal === undefined) {
|
|
194
268
|
return undefined;
|
|
269
|
+
}
|
|
195
270
|
const modalEl = modal;
|
|
196
271
|
const outside = Array.from(document.body.children).filter((c) => !c.contains(modalEl));
|
|
197
|
-
if (0 === outside.length)
|
|
272
|
+
if (0 === outside.length) {
|
|
198
273
|
return undefined;
|
|
199
|
-
|
|
274
|
+
}
|
|
275
|
+
if (!outside.every((c) => 'true' === c.getAttribute('aria-hidden'))) {
|
|
200
276
|
return undefined;
|
|
277
|
+
}
|
|
201
278
|
return ('the rest of the page is aria-hidden behind an open overlay (a focus-trap modal), so this ' +
|
|
202
279
|
'snapshot shows only the overlay. If it will not close its cleanup may be stuck on a throttled ' +
|
|
203
280
|
'tab: dismiss the overlay, or snapshot with { scope } inside it.');
|
|
@@ -206,7 +283,7 @@ function buildStatus(root) {
|
|
|
206
283
|
const visibleDialogs = collectDialogs(root);
|
|
207
284
|
const overlay = overlayHidingPage(root);
|
|
208
285
|
return {
|
|
209
|
-
route:
|
|
286
|
+
route: pageRoute(),
|
|
210
287
|
title: document.title,
|
|
211
288
|
...(visibleDialogs.length > 0 ? { visibleDialogs } : {}),
|
|
212
289
|
...(overlay !== undefined ? { overlayHidingPage: overlay } : {}),
|
|
@@ -214,11 +291,20 @@ function buildStatus(root) {
|
|
|
214
291
|
}
|
|
215
292
|
export function buildSnapshot(options = {}) {
|
|
216
293
|
const mode = options.mode ?? SnapshotMode.FULL;
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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;
|
|
222
308
|
const status = buildStatus(root);
|
|
223
309
|
if (scopeMissing) {
|
|
224
310
|
return { tree: '', status, nodes: 0, truncated: false, scopeMissing: true };
|
|
@@ -237,11 +323,23 @@ export function buildSnapshot(options = {}) {
|
|
|
237
323
|
maxDepth: options.maxDepth ?? 20,
|
|
238
324
|
unread: [],
|
|
239
325
|
unreadOverflow: false,
|
|
326
|
+
...(true === options.ids ? { ids: true } : {}),
|
|
240
327
|
};
|
|
241
|
-
if (true === options.includeRoot)
|
|
328
|
+
if (true === options.includeRoot) {
|
|
242
329
|
visit(root, 0, ctx, false);
|
|
243
|
-
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
244
332
|
walk(root, 0, ctx);
|
|
333
|
+
}
|
|
334
|
+
if (0 === ctx.nodes && root === document.body) {
|
|
335
|
+
ctx.throughOverlay = true;
|
|
336
|
+
for (const child of Array.from(root.children)) {
|
|
337
|
+
if (overlayStamped(child) && null !== child.querySelector(MODAL_SELECTOR)) {
|
|
338
|
+
visit(child, 0, ctx, false);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
ctx.throughOverlay = false;
|
|
342
|
+
}
|
|
245
343
|
return {
|
|
246
344
|
tree: ctx.lines.join('\n'),
|
|
247
345
|
status,
|
|
@@ -252,5 +350,6 @@ export function buildSnapshot(options = {}) {
|
|
|
252
350
|
...(ctx.hiddenSkipped > 0 ? { hiddenSkipped: ctx.hiddenSkipped } : {}),
|
|
253
351
|
...(ctx.unread.length > 0 ? { unread: ctx.unread } : {}),
|
|
254
352
|
...(ctx.unreadOverflow ? { unreadOverflow: true } : {}),
|
|
353
|
+
...(true === options.audit ? { audit: auditLayout(root) } : {}),
|
|
255
354
|
};
|
|
256
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;
|