@reticlehq/browser 3.7.0 → 3.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/dist/actions/action-report.d.ts +10 -0
  2. package/dist/actions/action-report.js +59 -14
  3. package/dist/actions/actions-dom.d.ts +4 -0
  4. package/dist/actions/actions-dom.js +20 -4
  5. package/dist/actions/actions.d.ts +1 -0
  6. package/dist/actions/actions.js +94 -46
  7. package/dist/actions/appeared-text.js +2 -2
  8. package/dist/actions/click-target.d.ts +1 -0
  9. package/dist/actions/click-target.js +14 -0
  10. package/dist/actions/danger-context.d.ts +2 -1
  11. package/dist/actions/danger-context.js +43 -17
  12. package/dist/actions/scroll.d.ts +2 -0
  13. package/dist/actions/scroll.js +10 -2
  14. package/dist/activate.d.ts +14 -0
  15. package/dist/activate.js +43 -0
  16. package/dist/busy.d.ts +3 -0
  17. package/dist/busy.js +42 -0
  18. package/dist/commands/commands.js +45 -17
  19. package/dist/connect-options.d.ts +12 -1
  20. package/dist/connect-options.js +18 -1
  21. package/dist/dom/a11y.d.ts +0 -2
  22. package/dist/dom/a11y.js +103 -363
  23. package/dist/dom/chart.d.ts +3 -2
  24. package/dist/dom/chart.js +3 -2
  25. package/dist/dom/dom-ignore.d.ts +6 -0
  26. package/dist/dom/dom-ignore.js +60 -5
  27. package/dist/dom/layout-audit.d.ts +15 -0
  28. package/dist/dom/layout-audit.js +208 -0
  29. package/dist/dom/paint-context.d.ts +15 -0
  30. package/dist/dom/paint-context.js +78 -0
  31. package/dist/dom/query-finders.d.ts +6 -0
  32. package/dist/dom/query-finders.js +216 -0
  33. package/dist/dom/query.d.ts +1 -1
  34. package/dist/dom/query.js +154 -227
  35. package/dist/dom/shadow-registry.d.ts +1 -0
  36. package/dist/dom/shadow-registry.js +27 -0
  37. package/dist/dom/snapshot.d.ts +5 -0
  38. package/dist/dom/snapshot.js +87 -35
  39. package/dist/dom/stable-selector.d.ts +1 -0
  40. package/dist/dom/stable-selector.js +19 -0
  41. package/dist/dom/text-match.d.ts +12 -0
  42. package/dist/dom/text-match.js +115 -0
  43. package/dist/dom/theme.d.ts +1 -0
  44. package/dist/dom/theme.js +21 -9
  45. package/dist/dom/virtualized.js +5 -1
  46. package/dist/dom/visibility.d.ts +7 -0
  47. package/dist/dom/visibility.js +322 -0
  48. package/dist/index.d.ts +4 -2
  49. package/dist/index.js +7 -1
  50. package/dist/observers/blind-spots.js +3 -2
  51. package/dist/observers/canvas.d.ts +18 -0
  52. package/dist/observers/canvas.js +76 -0
  53. package/dist/observers/dom.js +68 -19
  54. package/dist/observers/field.d.ts +11 -0
  55. package/dist/observers/field.js +117 -13
  56. package/dist/observers/install-all.js +4 -0
  57. package/dist/observers/media.d.ts +2 -0
  58. package/dist/observers/media.js +32 -0
  59. package/dist/observers/net-detail/network-body.d.ts +2 -0
  60. package/dist/observers/net-detail/network-body.js +36 -9
  61. package/dist/observers/net-detail/network-event-stream.d.ts +5 -0
  62. package/dist/observers/net-detail/network-event-stream.js +55 -0
  63. package/dist/observers/net-detail/network-frames.d.ts +2 -0
  64. package/dist/observers/net-detail/network-frames.js +39 -0
  65. package/dist/observers/net-detail/network-response-body.d.ts +3 -0
  66. package/dist/observers/net-detail/network-response-body.js +97 -0
  67. package/dist/observers/network.js +111 -101
  68. package/dist/observers/route.d.ts +2 -0
  69. package/dist/observers/route.js +33 -4
  70. package/dist/presenter/chrome/presenter-report-copy.d.ts +0 -5
  71. package/dist/presenter/chrome/presenter-report-copy.js +0 -14
  72. package/dist/presenter/hud-telemetry.js +1 -1
  73. package/dist/presenter/presenter-chat-views.d.ts +3 -4
  74. package/dist/presenter/presenter-chat-views.js +47 -20
  75. package/dist/presenter/presenter-config.d.ts +1 -0
  76. package/dist/presenter/presenter-config.js +1 -0
  77. package/dist/presenter/presenter-frame.d.ts +3 -2
  78. package/dist/presenter/presenter-frame.js +24 -15
  79. package/dist/presenter/presenter-harness-row.d.ts +4 -15
  80. package/dist/presenter/presenter-harness-row.js +11 -50
  81. package/dist/presenter/presenter-report-styles.js +0 -17
  82. package/dist/presenter/presenter-report.d.ts +1 -2
  83. package/dist/presenter/presenter-report.js +4 -54
  84. package/dist/presenter/presenter-run-state.js +2 -1
  85. package/dist/presenter/presenter-settings.d.ts +2 -3
  86. package/dist/presenter/presenter-settings.js +14 -17
  87. package/dist/presenter/presenter-shell.d.ts +6 -4
  88. package/dist/presenter/presenter-shell.js +37 -7
  89. package/dist/presenter/presenter.js +32 -11
  90. package/dist/registry/store-adapters.d.ts +5 -2
  91. package/dist/registry/store-adapters.js +40 -10
  92. package/dist/reticle-inject.js +700 -645
  93. package/dist/reticle.d.ts +4 -2
  94. package/dist/reticle.js +36 -6
  95. package/dist/security/page-location.d.ts +2 -0
  96. package/dist/security/page-location.js +7 -0
  97. package/dist/security/serialization.js +1 -1
  98. package/dist/storage-keys.d.ts +1 -0
  99. package/dist/storage-keys.js +1 -0
  100. package/dist/timers/native/native-timers.d.ts +1 -0
  101. package/dist/timers/native/native-timers.js +5 -0
  102. package/dist/transport/transport.d.ts +1 -0
  103. package/dist/transport/transport.js +18 -0
  104. package/package.json +6 -2
@@ -1,10 +1,15 @@
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, isVisible } from './a11y.js';
4
+ import { getAccessibleName, getRole, getStates, getValue } from './a11y.js';
5
+ import { isVisible } from './visibility.js';
5
6
  import { refs } from './addressing/refs.js';
7
+ import { readTestId } from './addressing/testid-attr.js';
8
+ import { identifyComponent } from '../registry/stores/adapters.js';
9
+ import { auditLayout } from './layout-audit.js';
10
+ import { pageRoute } from '../security/page-location.js';
6
11
  import { isIgnored, isReticleOverlay } from './dom-ignore.js';
7
- const INTERACTIVE = new Set([
12
+ export const INTERACTIVE = new Set([
8
13
  'button',
9
14
  'link',
10
15
  'textbox',
@@ -23,8 +28,9 @@ const INTERACTIVE = new Set([
23
28
  const SKIP_TAGS = new Set(['script', 'style', 'noscript', 'template', 'head', 'meta', 'link']);
24
29
  const ANNOUNCE_ROLES = new Set(['alert', 'status']);
25
30
  function announces(el, role) {
26
- if (ANNOUNCE_ROLES.has(role))
31
+ if (ANNOUNCE_ROLES.has(role)) {
27
32
  return true;
33
+ }
28
34
  const live = el.getAttribute('aria-live');
29
35
  return live !== null && 'off' !== live;
30
36
  }
@@ -56,30 +62,36 @@ function readingParts(el) {
56
62
  function isInlineIn(el) {
57
63
  const view = el.ownerDocument.defaultView;
58
64
  const parent = el.parentElement;
59
- if (null === view || null === parent)
65
+ if (null === view || null === parent) {
60
66
  return false;
67
+ }
61
68
  const around = view.getComputedStyle(parent).display;
62
- if (around.includes('flex') || around.includes('grid'))
69
+ if (around.includes('flex') || around.includes('grid')) {
63
70
  return false;
71
+ }
64
72
  return 'inline' === view.getComputedStyle(el).display;
65
73
  }
66
74
  function foldsIntoParent(el) {
67
- if (skipEarly(el) || !isInlineIn(el))
75
+ if (skipEarly(el) || !isInlineIn(el)) {
68
76
  return false;
77
+ }
69
78
  const role = getRole(el);
70
79
  return !INTERACTIVE.has(role) && !announces(el, role) && 0 === getAccessibleName(el).length;
71
80
  }
72
81
  export const MAX_UNREAD_BRANCHES = 50;
73
82
  function hiddenByMarkup(el) {
74
- if ('true' === el.getAttribute('aria-hidden'))
83
+ if ('true' === el.getAttribute('aria-hidden')) {
75
84
  return true;
85
+ }
76
86
  return el instanceof HTMLElement && el.hidden;
77
87
  }
78
88
  function skipEarly(el) {
79
- if (SKIP_TAGS.has(el.tagName.toLowerCase()))
89
+ if (SKIP_TAGS.has(el.tagName.toLowerCase())) {
80
90
  return true;
81
- if (isIgnored(el))
91
+ }
92
+ if (isIgnored(el)) {
82
93
  return true;
94
+ }
83
95
  return hiddenByMarkup(el);
84
96
  }
85
97
  function stateSuffix(el) {
@@ -90,7 +102,7 @@ function stateSuffix(el) {
90
102
  s === ElementState.FOCUSED);
91
103
  return states.length > 0 ? ` [${states.join(',')}]` : '';
92
104
  }
93
- function formatLine(el, depth, role, name, layout, ownText = '') {
105
+ function formatLine(el, depth, role, name, layout, ownText = '', withTestId = false) {
94
106
  const indent = ' '.repeat(depth);
95
107
  const value = getValue(el);
96
108
  const label = name;
@@ -99,14 +111,27 @@ function formatLine(el, depth, role, name, layout, ownText = '') {
99
111
  const valuePart = value !== undefined && value.length > 0 ? ` [value="${value}"]` : '';
100
112
  const layoutPart = layout.length > 0 ? ` [${layout}]` : '';
101
113
  const textPart = ownText.length > 0 ? ` [text="${ownText}"]` : '';
102
- return `${indent}- ${role}${namePart}${refPart}${valuePart}${textPart}${layoutPart}${stateSuffix(el)}`;
114
+ const testid = withTestId ? readTestId(el) : null;
115
+ const info = withTestId ? identifyComponent(el) : null;
116
+ const component = null === testid ? info?.componentStack[0] : undefined;
117
+ const where = info?.source;
118
+ let who = '';
119
+ if (null !== testid) {
120
+ who = ` [testid=${testid}]`;
121
+ }
122
+ else if (component !== undefined) {
123
+ who = ` [component=${component}]`;
124
+ }
125
+ const idPart = `${who}${where === undefined ? '' : ` [src=${where.file}:${String(where.line)}]`}`;
126
+ return `${indent}- ${role}${namePart}${refPart}${valuePart}${textPart}${layoutPart}${stateSuffix(el)}${idPart}`;
103
127
  }
104
128
  function formatTextLine(depth, text) {
105
129
  return `${' '.repeat(depth)}- text "${text}"`;
106
130
  }
107
131
  function layoutSignature(style) {
108
- if (null === style)
132
+ if (null === style) {
109
133
  return '';
134
+ }
110
135
  const display = style.display;
111
136
  if ('grid' === display || 'inline-grid' === display) {
112
137
  const cols = style.gridTemplateColumns;
@@ -127,13 +152,15 @@ function recordUnread(branches, ctx) {
127
152
  function pierceChildren(parent) {
128
153
  const out = [...parent.children];
129
154
  const shadow = parent.shadowRoot ?? capturedRootOf(parent);
130
- if (shadow !== null)
155
+ if (shadow !== null) {
131
156
  out.push(...shadow.children);
157
+ }
132
158
  if (isFrame(parent)) {
133
159
  try {
134
160
  const body = parent.contentDocument?.body;
135
- if (body !== null && body !== undefined)
161
+ if (body !== null && body !== undefined) {
136
162
  out.push(...body.children);
163
+ }
137
164
  }
138
165
  catch {
139
166
  }
@@ -146,8 +173,9 @@ function visit(child, depth, ctx, inLive) {
146
173
  !isIgnored(child) &&
147
174
  !(child instanceof HTMLElement && child.hidden);
148
175
  if (!stampedOnly && skipEarly(child)) {
149
- if (hiddenByMarkup(child))
176
+ if (hiddenByMarkup(child)) {
150
177
  ctx.hiddenSkipped += 1;
178
+ }
151
179
  return;
152
180
  }
153
181
  const view = child.ownerDocument.defaultView;
@@ -170,13 +198,14 @@ function visit(child, depth, ctx, inLive) {
170
198
  const actionable = interactive;
171
199
  const meaningful = actionable || role !== 'generic' || name.length > 0 || text.length > 0 || layout.length > 0;
172
200
  const include = lean ? actionable : meaningful;
173
- if (lean && !include && meaningful)
201
+ if (lean && !include && meaningful) {
174
202
  ctx.leanSkipped += 1;
203
+ }
175
204
  if (include) {
176
205
  ctx.nodes += 1;
177
206
  ctx.lines.push('generic' === role && text.length > 0 && 0 === layout.length
178
207
  ? formatTextLine(depth, text)
179
- : formatLine(child, depth, role, name, layout, text));
208
+ : formatLine(child, depth, role, name, layout, text, true === ctx.ids));
180
209
  walk(child, depth + 1, ctx, announce);
181
210
  }
182
211
  else {
@@ -199,31 +228,36 @@ function walk(parent, depth, ctx, inLive = false) {
199
228
  return;
200
229
  }
201
230
  const child = children[index];
202
- if (child !== undefined)
231
+ if (child !== undefined) {
203
232
  visit(child, depth, ctx, inLive);
233
+ }
204
234
  }
205
235
  }
206
236
  function collectDialogs(root) {
207
237
  const nodes = root.querySelectorAll('[role="dialog"], dialog[open], [aria-modal="true"]');
208
238
  const names = [];
209
239
  for (const node of nodes) {
210
- if (isReticleOverlay(node))
240
+ if (isReticleOverlay(node)) {
211
241
  continue;
212
- if (isVisible(node))
242
+ }
243
+ if (isVisible(node)) {
213
244
  names.push(getAccessibleName(node) || '(unnamed dialog)');
245
+ }
214
246
  }
215
247
  return names;
216
248
  }
217
249
  const MODAL_SELECTOR = '[role="dialog"], [role="alertdialog"], dialog[open], [aria-modal="true"]';
218
250
  function overlayHidingPage(root) {
219
- if (root !== document.body)
251
+ if (root !== document.body) {
220
252
  return undefined;
253
+ }
221
254
  const dialogs = document.body.querySelectorAll('[role="dialog"], dialog[open], [aria-modal="true"]');
222
255
  let modal;
223
256
  let hiddenCandidate;
224
257
  for (const d of dialogs) {
225
- if (isReticleOverlay(d))
258
+ if (isReticleOverlay(d)) {
226
259
  continue;
260
+ }
227
261
  if (isVisible(d)) {
228
262
  modal = d;
229
263
  break;
@@ -231,14 +265,17 @@ function overlayHidingPage(root) {
231
265
  hiddenCandidate ??= d;
232
266
  }
233
267
  modal ??= hiddenCandidate;
234
- if (modal === undefined)
268
+ if (modal === undefined) {
235
269
  return undefined;
270
+ }
236
271
  const modalEl = modal;
237
272
  const outside = Array.from(document.body.children).filter((c) => !c.contains(modalEl));
238
- if (0 === outside.length)
273
+ if (0 === outside.length) {
239
274
  return undefined;
240
- if (!outside.every((c) => 'true' === c.getAttribute('aria-hidden')))
275
+ }
276
+ if (!outside.every((c) => 'true' === c.getAttribute('aria-hidden'))) {
241
277
  return undefined;
278
+ }
242
279
  return ('the rest of the page is aria-hidden behind an open overlay (a focus-trap modal), so this ' +
243
280
  'snapshot shows only the overlay. If it will not close its cleanup may be stuck on a throttled ' +
244
281
  'tab: dismiss the overlay, or snapshot with { scope } inside it.');
@@ -247,7 +284,7 @@ function buildStatus(root) {
247
284
  const visibleDialogs = collectDialogs(root);
248
285
  const overlay = overlayHidingPage(root);
249
286
  return {
250
- route: `${location.pathname}${location.search}${location.hash}`,
287
+ route: pageRoute(),
251
288
  title: document.title,
252
289
  ...(visibleDialogs.length > 0 ? { visibleDialogs } : {}),
253
290
  ...(overlay !== undefined ? { overlayHidingPage: overlay } : {}),
@@ -255,11 +292,20 @@ function buildStatus(root) {
255
292
  }
256
293
  export function buildSnapshot(options = {}) {
257
294
  const mode = options.mode ?? SnapshotMode.FULL;
258
- const scopeEl = options.scope !== undefined
259
- ? (refs.resolve(options.scope) ?? document.querySelector(options.scope))
260
- : document.body;
261
- const scopeMissing = options.scope !== undefined && !(scopeEl instanceof Element);
262
- const root = scopeEl instanceof Element ? scopeEl : document.body;
295
+ const scope = options.scope?.trim();
296
+ let scopeEl = document.body;
297
+ if (undefined !== scope && '' !== scope) {
298
+ scopeEl = refs.resolve(scope);
299
+ if (null === scopeEl) {
300
+ try {
301
+ scopeEl = document.querySelector(scope);
302
+ }
303
+ catch {
304
+ }
305
+ }
306
+ }
307
+ const scopeMissing = null === scopeEl;
308
+ const root = scopeEl ?? document.body;
263
309
  const status = buildStatus(root);
264
310
  if (scopeMissing) {
265
311
  return { tree: '', status, nodes: 0, truncated: false, scopeMissing: true };
@@ -278,16 +324,21 @@ export function buildSnapshot(options = {}) {
278
324
  maxDepth: options.maxDepth ?? 20,
279
325
  unread: [],
280
326
  unreadOverflow: false,
327
+ ...(true === options.ids ? { ids: true } : {}),
281
328
  };
282
- if (true === options.includeRoot)
329
+ if (true === options.includeRoot) {
283
330
  visit(root, 0, ctx, false);
284
- else
331
+ }
332
+ else {
285
333
  walk(root, 0, ctx);
334
+ }
286
335
  if (0 === ctx.nodes && root === document.body) {
287
336
  ctx.throughOverlay = true;
288
- for (const child of Array.from(root.children))
289
- if (overlayStamped(child) && null !== child.querySelector(MODAL_SELECTOR))
337
+ for (const child of Array.from(root.children)) {
338
+ if (overlayStamped(child) && null !== child.querySelector(MODAL_SELECTOR)) {
290
339
  visit(child, 0, ctx, false);
340
+ }
341
+ }
291
342
  ctx.throughOverlay = false;
292
343
  }
293
344
  return {
@@ -300,5 +351,6 @@ export function buildSnapshot(options = {}) {
300
351
  ...(ctx.hiddenSkipped > 0 ? { hiddenSkipped: ctx.hiddenSkipped } : {}),
301
352
  ...(ctx.unread.length > 0 ? { unread: ctx.unread } : {}),
302
353
  ...(ctx.unreadOverflow ? { unreadOverflow: true } : {}),
354
+ ...(true === options.audit ? { audit: auditLayout(root) } : {}),
303
355
  };
304
356
  }
@@ -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
+ }
@@ -1,3 +1,4 @@
1
+ export declare function tokenNames(): Set<string>;
1
2
  interface ThemeReport {
2
3
  colorToken: string | null;
3
4
  colorTokens: string[];
package/dist/dom/theme.js CHANGED
@@ -1,5 +1,24 @@
1
1
  const THEME_ATTRIBUTE = 'data-theme';
2
- function tokenNames() {
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
- for (const rule of Array.from(rules)) {
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
  }
@@ -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.offsetTop;
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;
@@ -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;