browser-debugger-cli 0.11.0 → 0.12.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.
Files changed (98) hide show
  1. package/README.md +142 -79
  2. package/dist/commands/css.d.ts +13 -0
  3. package/dist/commands/css.js +53 -0
  4. package/dist/commands/dom/audit.d.ts +14 -0
  5. package/dist/commands/dom/audit.js +87 -0
  6. package/dist/commands/dom/formInteraction.js +36 -6
  7. package/dist/commands/dom/helpers/keyAttributes.d.ts +3 -2
  8. package/dist/commands/dom/helpers/keyAttributes.js +6 -4
  9. package/dist/commands/dom/helpers/screenshot.d.ts +1 -0
  10. package/dist/commands/dom/helpers/screenshot.js +158 -38
  11. package/dist/commands/dom/index.js +4 -1
  12. package/dist/commands/dom/screenshot.js +10 -6
  13. package/dist/commands/dom/wait.js +5 -3
  14. package/dist/commands/helpJson.js +1 -1
  15. package/dist/commands/optionBehaviors.js +21 -6
  16. package/dist/commands/page.js +7 -4
  17. package/dist/commands/peek.d.ts +7 -0
  18. package/dist/commands/peek.js +65 -23
  19. package/dist/commands/shared/optionTypes.d.ts +5 -1
  20. package/dist/commands/start.d.ts +13 -0
  21. package/dist/commands/start.js +19 -2
  22. package/dist/commands/tail.d.ts +7 -1
  23. package/dist/commands/tail.js +13 -62
  24. package/dist/commands.js +2 -0
  25. package/dist/daemon/session/commandRegistry.js +7 -1
  26. package/dist/daemon/session/plugins.js +4 -52
  27. package/dist/daemon.js +7986 -6848
  28. package/dist/errors/messages.d.ts +34 -4
  29. package/dist/errors/messages.js +68 -5
  30. package/dist/index.js +610 -186
  31. package/dist/ipc/client.d.ts +4 -0
  32. package/dist/ipc/client.js +8 -0
  33. package/dist/ipc/protocol/auditTypes.d.ts +129 -0
  34. package/dist/ipc/protocol/auditTypes.js +6 -0
  35. package/dist/ipc/protocol/commands.d.ts +23 -0
  36. package/dist/ipc/protocol/commands.js +2 -0
  37. package/dist/ipc/protocol/domTypes.d.ts +4 -0
  38. package/dist/ipc/protocol/inspectTypes.d.ts +71 -8
  39. package/dist/runtime/css/search.d.ts +39 -0
  40. package/dist/runtime/css/search.js +122 -0
  41. package/dist/runtime/dom/actionEffects.d.ts +4 -1
  42. package/dist/runtime/dom/actionEffects.js +8 -4
  43. package/dist/runtime/dom/audit.d.ts +19 -0
  44. package/dist/runtime/dom/audit.js +36 -0
  45. package/dist/runtime/dom/auditModel.d.ts +45 -0
  46. package/dist/runtime/dom/auditModel.js +215 -0
  47. package/dist/runtime/dom/auditScripts.d.ts +107 -0
  48. package/dist/runtime/dom/auditScripts.js +112 -0
  49. package/dist/runtime/dom/elementGeometry.d.ts +8 -2
  50. package/dist/runtime/dom/elementGeometry.js +24 -8
  51. package/dist/runtime/dom/elementInfo.d.ts +3 -2
  52. package/dist/runtime/dom/elementInfo.js +8 -2
  53. package/dist/runtime/dom/formFillHelpers/fill.js +2 -2
  54. package/dist/runtime/dom/inspect.d.ts +7 -0
  55. package/dist/runtime/dom/inspect.js +88 -23
  56. package/dist/runtime/dom/inspectAllStyles.d.ts +16 -4
  57. package/dist/runtime/dom/inspectAllStyles.js +89 -7
  58. package/dist/runtime/dom/inspectCascade.d.ts +19 -2
  59. package/dist/runtime/dom/inspectCascade.js +214 -44
  60. package/dist/runtime/dom/inspectCascadeModel.d.ts +8 -0
  61. package/dist/runtime/dom/inspectCascadeModel.js +108 -34
  62. package/dist/runtime/dom/inspectHints.d.ts +26 -3
  63. package/dist/runtime/dom/inspectHints.js +125 -9
  64. package/dist/runtime/dom/inspectModel.d.ts +3 -0
  65. package/dist/runtime/dom/inspectModel.js +30 -7
  66. package/dist/runtime/dom/inspectPaintModel.d.ts +48 -22
  67. package/dist/runtime/dom/inspectPaintModel.js +180 -68
  68. package/dist/runtime/dom/inspectRules.d.ts +19 -0
  69. package/dist/runtime/dom/inspectRules.js +21 -5
  70. package/dist/runtime/dom/inspectScripts.d.ts +85 -12
  71. package/dist/runtime/dom/inspectScripts.js +314 -28
  72. package/dist/runtime/dom/inspectTree.js +10 -2
  73. package/dist/runtime/dom/inspectWhyModel.d.ts +2 -1
  74. package/dist/runtime/dom/inspectWhyModel.js +52 -10
  75. package/dist/runtime/dom/layout.js +31 -9
  76. package/dist/runtime/dom/reactEventHelpers.d.ts +7 -0
  77. package/dist/runtime/dom/reactEventHelpers.js +27 -9
  78. package/dist/runtime/page/emulation.d.ts +13 -4
  79. package/dist/runtime/page/emulation.js +69 -4
  80. package/dist/runtime/page/userAgent.d.ts +17 -0
  81. package/dist/runtime/page/userAgent.js +57 -0
  82. package/dist/types.d.ts +4 -0
  83. package/dist/ui/formatters/audit.d.ts +19 -0
  84. package/dist/ui/formatters/audit.js +106 -0
  85. package/dist/ui/formatters/dom.d.ts +1 -1
  86. package/dist/ui/formatters/dom.js +6 -3
  87. package/dist/ui/formatters/inspect.js +42 -15
  88. package/dist/ui/formatters/status.js +1 -1
  89. package/dist/ui/messages/commands.d.ts +44 -7
  90. package/dist/ui/messages/commands.js +83 -11
  91. package/dist/ui/messages/preview.d.ts +6 -0
  92. package/dist/ui/messages/preview.js +9 -1
  93. package/dist/utils/cssValues.js +36 -4
  94. package/dist/utils/decisionTrees.js +0 -5
  95. package/dist/utils/suggestions.d.ts +4 -2
  96. package/dist/utils/suggestions.js +7 -5
  97. package/dist/utils/taskMappings.js +1 -1
  98. package/package.json +3 -2
@@ -29,26 +29,105 @@ const FLAT_TREE_JS = `(view) => {
29
29
  while (p && style(p).display === 'contents') p = flatParent(p);
30
30
  return p;
31
31
  };
32
+ const via = new WeakMap();
33
+ const label = (n) => n.localName + (n.id ? '#' + n.id : n.classList && n.classList.length ? '.' + n.classList[0] : '');
34
+ const slotText = (slot) => slot.assignedNodes({ flatten: true }).some((t) => t.nodeType === 3 && t.data.trim() !== '');
32
35
  const children = (n) => {
33
36
  const out = [];
34
- const add = (c) => {
37
+ const add = (c, through) => {
35
38
  if (skipped.test(c.localName)) return;
36
39
  if (c.localName === 'slot') {
37
40
  const assigned = c.assignedElements({ flatten: true });
38
- (assigned.length ? assigned : Array.from(c.children)).forEach(add);
41
+ if (assigned.length === 0 && slotText(c)) out.push(c);
42
+ else (assigned.length ? assigned : Array.from(c.children)).forEach((a) => add(a, label(c)));
39
43
  } else if (style(c).display === 'contents') {
40
- Array.from((c.shadowRoot || c).children).forEach(add);
44
+ Array.from((c.shadowRoot || c).children).forEach((a) => add(a, label(c) + ' (contents)'));
41
45
  } else {
46
+ if (through) via.set(c, through);
42
47
  out.push(c);
43
48
  }
44
49
  };
45
- Array.from((n.shadowRoot || n).children).forEach(add);
50
+ Array.from((n.shadowRoot || n).children).forEach((c) => add(c, null));
46
51
  return out;
47
52
  };
48
53
  const rendered = (n) => n.getClientRects().length > 0 &&
49
54
  (!n.checkVisibility || n.checkVisibility({ visibilityProperty: true }));
50
55
  const container = (n) => children(n).some((c) => rendered(c) && !/^inline/.test(style(c).display));
51
- return { style, flatParent, layoutParent, children, rendered, container };
56
+ return { style, skipped, flatParent, layoutParent, children, rendered, container, label, via, slotText };
57
+ }`;
58
+ /**
59
+ * Page-side holder of an element's text: the element whose styles draw most
60
+ * of the visible text in its flat tree (characters, whitespace aside), and
61
+ * the element whose child text nodes those are (for the rendered font).
62
+ * Text slotted into a shadow root is drawn with the slot's styles, and text
63
+ * of an element with a shadow root that is not slotted is not drawn. Null
64
+ * when there is no visible text.
65
+ */
66
+ const TEXT_HOLDER_JS = `(el, tree) => {
67
+ const counts = new Map();
68
+ const parents = new Map();
69
+ let budget = 2000;
70
+ const shown = (n) => tree.style(n).display === 'contents' || tree.rendered(n);
71
+ const nodesOf = (n) => {
72
+ if (n.localName !== 'slot') return Array.from((n.shadowRoot || n).childNodes);
73
+ const assigned = n.assignedNodes({ flatten: true });
74
+ return assigned.length ? assigned : Array.from(n.childNodes);
75
+ };
76
+ const visit = (n) => {
77
+ if (budget-- <= 0) return;
78
+ for (const c of nodesOf(n)) {
79
+ if (budget <= 0) return;
80
+ if (c.nodeType === 3) {
81
+ const length = c.data.replace(/\\s+/g, '').length;
82
+ if (length === 0) continue;
83
+ counts.set(n, (counts.get(n) || 0) + length);
84
+ if (!parents.has(n)) parents.set(n, c.parentElement || n);
85
+ } else if (c.nodeType === 1 && !tree.skipped.test(c.localName) && shown(c)) {
86
+ visit(c);
87
+ }
88
+ }
89
+ };
90
+ visit(el);
91
+ let best = null;
92
+ for (const [holder, length] of counts) if (!best || length > counts.get(best)) best = holder;
93
+ let total = 0;
94
+ for (const length of counts.values()) total += length;
95
+ return best && { style: best, font: parents.get(best), share: counts.get(best) / total };
96
+ }`;
97
+ /**
98
+ * Page-side: whether loaded `@font-face` (or `FontFace`) faces of the first
99
+ * family of an element's `font-family` cover the characters of its text
100
+ * (their `unicode-range`): then that web font draws the text, whatever
101
+ * name its file gives. A subset that lacks the text's characters (latin
102
+ * loaded, Cyrillic text) does not count.
103
+ */
104
+ const FAMILY_LOADED_JS = `(n, tree, text) => {
105
+ const unquote = (name) => name.trim().replace(/^["']|["']$/g, '').toLowerCase();
106
+ const match = /^\\s*("[^"]*"|'[^']*'|[^,]*)/.exec(tree.style(n).fontFamily);
107
+ const first = unquote(match ? match[1] : '');
108
+ if (!first || !n.ownerDocument.fonts) return false;
109
+ const ranges = [];
110
+ for (const face of n.ownerDocument.fonts) {
111
+ if (face.status !== 'loaded' || unquote(face.family) !== first) continue;
112
+ for (const part of face.unicodeRange.split(',')) {
113
+ const [from, to] = part.trim().replace(/^U\\+/i, '').split('-');
114
+ const low = parseInt(from.replace(/\\?/g, '0'), 16);
115
+ const high = parseInt((to || from).replace(/\\?/g, 'F'), 16);
116
+ if (!Number.isNaN(low) && !Number.isNaN(high)) ranges.push([low, high]);
117
+ }
118
+ }
119
+ if (ranges.length === 0) return false;
120
+ const covered = (code) => ranges.some(([low, high]) => code >= low && code <= high);
121
+ return Array.from(text.replace(/\\s+/g, '')).every((char) => covered(char.codePointAt(0)));
122
+ }`;
123
+ /**
124
+ * Page-side: whether a form control draws text of its own (a text field,
125
+ * a select, a button-like input); checkboxes, radios, ranges and color
126
+ * inputs do not. Buttons are not covered: their text is their content.
127
+ */
128
+ const TEXT_CONTROL_JS = `(el) => {
129
+ if (el.localName === 'textarea' || el.localName === 'select') return true;
130
+ return el.localName === 'input' && !/^(checkbox|radio|range|color|hidden|image)$/.test(el.type);
52
131
  }`;
53
132
  /**
54
133
  * Page-side text of an element: a form control's value or a select's chosen
@@ -100,14 +179,19 @@ const PLACEMENT_JS = `(el, tree) => {
100
179
  const r = el.getBoundingClientRect();
101
180
  const p = parent.getBoundingClientRect();
102
181
  const ps = tree.style(parent);
103
- const edge = (side) => parseFloat(ps['border-' + side + '-width']) + parseFloat(ps['padding-' + side]);
182
+ const zoom = parent.currentCSSZoom || 1;
183
+ const edge = (side) => (parseFloat(ps['border-' + side + '-width']) + parseFloat(ps['padding-' + side])) * zoom;
104
184
  const inParent = {
105
185
  left: r.left - (p.left + edge('left')),
106
186
  top: r.top - (p.top + edge('top')),
107
187
  right: (p.right - edge('right')) - r.right,
108
188
  bottom: (p.bottom - edge('bottom')) - r.bottom
109
189
  };
110
- const inFlow = (n) => n !== el && tree.rendered(n) && !/^(absolute|fixed)$/.test(tree.style(n).position);
190
+ const inFlow = (n) => {
191
+ if (n === el || !tree.rendered(n) || /^(absolute|fixed)$/.test(tree.style(n).position)) return false;
192
+ const box = n.getBoundingClientRect();
193
+ return box.width > 0 || box.height > 0;
194
+ };
111
195
  const siblings = tree.children(parent);
112
196
  const at = siblings.indexOf(el);
113
197
  const before = siblings.slice(0, Math.max(at, 0)).reverse().find(inFlow);
@@ -135,8 +219,10 @@ const BACKGROUNDS_JS = `(el, tree, view) => {
135
219
  let opacity = 1;
136
220
  for (let n = el; n && backgrounds.length < 60; n = tree.flatParent(n)) {
137
221
  const s = tree.style(n);
138
- backgrounds.push({ color: s.backgroundColor, image: s.backgroundImage !== 'none' });
139
- opacity *= Number(s.opacity) || 0;
222
+ const own = Number(s.opacity) || 0;
223
+ const background = { color: s.backgroundColor, image: s.backgroundImage !== 'none' };
224
+ backgrounds.push(own < 1 ? Object.assign(background, { opacity: own }) : background);
225
+ opacity *= own;
140
226
  }
141
227
  const doc = el.ownerDocument;
142
228
  const meta = doc.querySelector('meta[name="color-scheme"]');
@@ -146,6 +232,61 @@ const BACKGROUNDS_JS = `(el, tree, view) => {
146
232
  const canvasDark = /dark/.test(scheme) && (!/light/.test(scheme) || prefersDark);
147
233
  return { backgrounds: backgrounds, canvasDark: canvasDark, opacity: opacity };
148
234
  }`;
235
+ /**
236
+ * Page-side reasons the contrast of an element's text is approximate: a
237
+ * blend mode or filter on it or an ancestor, and, hit-testing the middle of
238
+ * its first line of text (else of its first box; only in the viewport), the
239
+ * nearest element below it that is not an ancestor, above the first opaque
240
+ * background of its own chain, and
241
+ * paints (a canvas, video, image, background) and the nearest one on top of
242
+ * it that paints (an overlay).
243
+ */
244
+ const PAINT_RISKS_JS = `(el, tree, textParent) => {
245
+ const risks = [];
246
+ const chain = [];
247
+ for (let n = el; n && chain.length < 60; n = tree.flatParent(n)) chain.push(n);
248
+ for (const n of chain) {
249
+ const s = tree.style(n);
250
+ if (s.mixBlendMode !== 'normal') risks.push('mix-blend-mode ' + s.mixBlendMode + ' on ' + tree.label(n));
251
+ if (s.filter !== 'none') risks.push('filter on ' + tree.label(n));
252
+ }
253
+ const textNode = Array.from(textParent.childNodes).find((c) => c.nodeType === 3 && c.data.trim() !== '');
254
+ const range = el.ownerDocument.createRange();
255
+ if (textNode) range.selectNodeContents(textNode);
256
+ const box = tree.style(el).display === 'contents' ? tree.layoutParent(el) : el;
257
+ const rect = (textNode && range.getClientRects()[0]) || (box && box.getClientRects()[0]);
258
+ const view = el.ownerDocument.defaultView;
259
+ if (!rect) return risks;
260
+ const x = rect.left + rect.width / 2;
261
+ const y = rect.top + rect.height / 2;
262
+ if (x < 0 || y < 0 || x >= view.innerWidth || y >= view.innerHeight) return risks;
263
+ const root = el.getRootNode();
264
+ const hits = (root.elementsFromPoint ? root : el.ownerDocument).elementsFromPoint(x, y);
265
+ const at = hits.findIndex((h) => chain.includes(h));
266
+ if (at < 0) return risks;
267
+ const clear = (color) => color === 'transparent' || /^rgba\\(.*,\\s*0\\)$/.test(color) || /\\/\\s*0\\)$/.test(color);
268
+ const opaque = (n) => {
269
+ const s = tree.style(n);
270
+ return /^rgb\\(/.test(s.backgroundColor) && s.backgroundImage === 'none' && Number(s.opacity) === 1;
271
+ };
272
+ const paints = (n) => {
273
+ const s = tree.style(n);
274
+ return /^(canvas|video|img|iframe|embed|object|svg)$/.test(n.localName) || s.backgroundImage !== 'none' || !clear(s.backgroundColor);
275
+ };
276
+ let behind = null;
277
+ for (const h of hits.slice(at)) {
278
+ if (chain.includes(h)) {
279
+ if (opaque(h)) break;
280
+ } else if (paints(h)) {
281
+ behind = h;
282
+ break;
283
+ }
284
+ }
285
+ if (behind) risks.push(tree.label(behind) + ' behind');
286
+ const onTop = hits.slice(0, at).find((h) => !el.contains(h) && paints(h));
287
+ if (onTop) risks.push(tree.label(onTop) + ' on top');
288
+ return risks;
289
+ }`;
149
290
  /**
150
291
  * Page-side child tree to `depth` levels: per element its label, size,
151
292
  * display, position relative to its parent's border box (like Figma's x/y
@@ -157,9 +298,8 @@ const BACKGROUNDS_JS = `(el, tree, view) => {
157
298
  const TREE_JS = `(el, tree, textOf, depth) => {
158
299
  let budget = ${TREE_NODE_CAP};
159
300
  let skipped = 0;
160
- const labelOf = (n) => n.localName + (n.id ? '#' + n.id : n.classList && n.classList.length ? '.' + n.classList[0] : '');
161
301
  const walk = (kids, level, origin) => {
162
- const shown = kids.filter(tree.rendered);
302
+ const shown = kids.filter((k) => tree.rendered(k) || k.localName === 'slot');
163
303
  const nodes = [];
164
304
  for (const k of shown) {
165
305
  if (budget <= 0) { skipped++; continue; }
@@ -170,7 +310,13 @@ const TREE_JS = `(el, tree, textOf, depth) => {
170
310
  };
171
311
  const nodeOf = (n, level, origin) => {
172
312
  const r = n.getBoundingClientRect();
173
- const node = { label: labelOf(n), x: r.left - origin.left, y: r.top - origin.top, w: r.width, h: r.height, display: tree.style(n).display };
313
+ const node = { label: tree.label(n), x: r.left - origin.left, y: r.top - origin.top, w: r.width, h: r.height, display: tree.style(n).display };
314
+ if (tree.via.has(n)) node.via = tree.via.get(n);
315
+ if (n.parentNode && n.parentNode.nodeType === 11 && n.parentNode.host) node.shadow = true;
316
+ if (n.localName === 'slot') {
317
+ node.text = n.assignedNodes({ flatten: true }).map((t) => t.textContent).join(' ').replace(/\\s+/g, ' ').trim().slice(0, 60);
318
+ return node;
319
+ }
174
320
  const kids = tree.children(n);
175
321
  if (!tree.container(n)) node.text = textOf(n).slice(0, 60);
176
322
  if (level >= depth) {
@@ -186,12 +332,49 @@ const TREE_JS = `(el, tree, textOf, depth) => {
186
332
  const top = walk(tree.children(el), 1, el.getBoundingClientRect());
187
333
  return { tree: top.nodes, hiddenChildren: top.hidden, treeSkipped: skipped };
188
334
  }`;
335
+ /**
336
+ * Page-side: rules for the element that set a property but sit under a
337
+ * `@media` or `@supports` condition that does not apply now (at most 5),
338
+ * from same-origin, `@import`ed, constructed and shadow-root stylesheets.
339
+ */
340
+ const INACTIVE_RULES_JS = `(el, names) => {
341
+ const view = el.ownerDocument.defaultView;
342
+ const found = [];
343
+ let budget = 20000;
344
+ const unmet = (rule) => {
345
+ if (rule.media && rule.media.mediaText && !view.matchMedia(rule.media.mediaText).matches) return '@media ' + rule.media.mediaText;
346
+ if (typeof CSSSupportsRule !== 'undefined' && rule instanceof CSSSupportsRule && !CSS.supports(rule.conditionText)) return '@supports ' + rule.conditionText;
347
+ return null;
348
+ };
349
+ const sets = (rule) => names.map((name) => rule.style.getPropertyValue(name).trim()).find((value) => value !== '');
350
+ const matches = (selector) => {
351
+ try { return el.matches(selector); } catch (e) { return false; }
352
+ };
353
+ const visit = (rules, condition) => {
354
+ for (const rule of Array.from(rules)) {
355
+ if (budget-- <= 0 || found.length >= 5) return;
356
+ if (typeof CSSImportRule !== 'undefined' && rule instanceof CSSImportRule) {
357
+ try { if (rule.styleSheet) visit(rule.styleSheet.cssRules, condition); } catch (e) { continue; }
358
+ continue;
359
+ }
360
+ const value = condition && rule.style && rule.selectorText ? sets(rule) : undefined;
361
+ if (value !== undefined && matches(rule.selectorText)) found.push({ selector: rule.selectorText, condition: condition, value: value });
362
+ if (rule.cssRules) visit(rule.cssRules, condition || unmet(rule));
363
+ }
364
+ };
365
+ for (const scope of new Set([el.getRootNode(), el.ownerDocument])) {
366
+ for (const sheet of [...Array.from(scope.styleSheets || []), ...Array.from(scope.adoptedStyleSheets || [])]) {
367
+ try { visit(sheet.cssRules, null); } catch (e) { continue; }
368
+ }
369
+ }
370
+ return found;
371
+ }`;
189
372
  /**
190
373
  * Page-side function run on the element (`this`), see {@link RawInspect}.
191
- * Arguments: tree depth, and the property names asked for with `--props`
192
- * (or null).
374
+ * Arguments: tree depth, the property names asked for with `--props` (or
375
+ * null) and the `--why` property (or null).
193
376
  */
194
- export const INSPECT_PAGE_JS = `function (depth, props) {
377
+ export const INSPECT_PAGE_JS = `function (depth, props, why) {
195
378
  const el = this;
196
379
  const view = el.ownerDocument.defaultView;
197
380
  const tree = (${FLAT_TREE_JS})(view);
@@ -199,6 +382,11 @@ export const INSPECT_PAGE_JS = `function (depth, props) {
199
382
  const describe = ${ELEMENT_DESCRIPTION_JS};
200
383
  const s = tree.style(el);
201
384
  const formControl = /^(input|textarea|select|button)$/.test(el.localName);
385
+ const textControl = (${TEXT_CONTROL_JS})(el);
386
+ const holder = textControl ? null : (${TEXT_HOLDER_JS})(el, tree);
387
+ const textChildren = tree.children(el).filter((c) => tree.rendered(c) && (c.textContent || '').trim() !== '').length;
388
+ const textual = textControl || Boolean(holder && (holder.style === el || (holder.share >= 0.9 && textChildren <= 1) || !tree.container(el)));
389
+ const textFrom = holder && holder.style !== el ? holder.style : el;
202
390
  const content = textOf(el);
203
391
  const parent = tree.layoutParent(el);
204
392
  const typedMap = el.computedStyleMap ? el.computedStyleMap() : null;
@@ -209,15 +397,26 @@ export const INSPECT_PAGE_JS = `function (depth, props) {
209
397
  classes: el.classList ? Array.from(el.classList) : [],
210
398
  context: (${ELEMENT_CONTEXT_JS})(el),
211
399
  content: content,
212
- textual: formControl || (content !== '' && !tree.container(el)),
400
+ textual: textual,
213
401
  formControl: formControl,
214
- replaced: el instanceof SVGElement || /^(img|video|canvas|iframe|embed|object|input|textarea|select|button|meter|progress)$/.test(el.localName),
402
+ rendered: textual && textFrom !== el
403
+ ? tree.style(textFrom).visibility !== 'hidden' && tree.rendered(tree.style(textFrom).display === 'contents' ? tree.layoutParent(textFrom) : textFrom)
404
+ : tree.rendered(el),
405
+ hasText: textControl || holder !== null,
406
+ replaced: el instanceof SVGElement || /^(img|video|audio|canvas|iframe|embed|object|input|textarea|select|button|meter|progress)$/.test(el.localName),
215
407
  svg: el.namespaceURI === 'http://www.w3.org/2000/svg',
216
- ownText: Array.from(el.childNodes).some((n) => n.nodeType === 3 && n.data.trim() !== ''),
217
408
  typed: { width: typed('width'), height: typed('height') },
218
409
  parent: parent ? describe(parent) : undefined,
219
- animating: el.getAnimations ? [...new Set(el.getAnimations().filter((a) => a.playState === 'running').map((a) => a.transitionProperty || a.animationName || 'animation'))] : []
410
+ animating: el.getAnimations ? [...new Set(el.getAnimations().filter((a) => a.playState === 'running' && (!a.timeline || a.timeline === el.ownerDocument.timeline)).map((a) => a.transitionProperty || a.animationName || 'animation'))] : []
220
411
  };
412
+ if (textFrom !== el) result.textHolder = tree.label(textFrom);
413
+ const clipped = (n) => {
414
+ const cs = tree.style(n);
415
+ if (cs.overflowX !== 'visible' && n.scrollWidth > n.clientWidth + 1) return true;
416
+ return cs.webkitLineClamp !== 'none' && cs.overflowY !== 'visible' && n.scrollHeight > n.clientHeight + 1;
417
+ };
418
+ if (textual && !textControl && (clipped(el) || (textFrom !== el && clipped(textFrom)))) result.truncated = true;
419
+ if ((${FAMILY_LOADED_JS})(textFrom, tree, textOf(textFrom))) result.familyLoaded = true;
221
420
  if (/^(input|textarea)$/.test(el.localName) && el.placeholder && !el.value) {
222
421
  result.placeholder = el.placeholder.replace(/\\s+/g, ' ').trim();
223
422
  const placeholderStyle = tree.style(el, '::placeholder');
@@ -236,28 +435,115 @@ export const INSPECT_PAGE_JS = `function (depth, props) {
236
435
  return sides.map((side) => side.slice(name.length + 1) + ' ' + s.getPropertyValue(side).trim()).join(' / ');
237
436
  };
238
437
  result.props = Object.fromEntries(props.filter(known).map((name) => [name, valueOf(name)]));
438
+ result.rootFontSize = parseFloat(tree.style(el.ownerDocument.documentElement).fontSize) || 16;
239
439
  result.unknownProps = props.filter((name) => !known(name));
240
440
  }
441
+ if (why && !why.startsWith('--')) {
442
+ if (!CSS.supports(why, 'inherit')) result.unknownWhy = true;
443
+ else {
444
+ const probe = el.ownerDocument.createElement('div').style;
445
+ probe.setProperty(why, 'inherit');
446
+ if (probe.length > 1) {
447
+ result.whyLonghands = Array.from(probe);
448
+ result.whyComputed = s.getPropertyValue(why).trim();
449
+ }
450
+ const inactive = (${INACTIVE_RULES_JS})(el, [why].concat(result.whyLonghands || []));
451
+ if (inactive.length > 0) result.whyInactive = inactive;
452
+ }
453
+ }
241
454
  return Object.assign(result,
242
455
  (${PLACEMENT_JS})(el, tree),
243
- (${BACKGROUNDS_JS})(el, tree, view),
456
+ (${BACKGROUNDS_JS})(textual ? textFrom : el, tree, view),
457
+ textual ? { paintRisks: (${PAINT_RISKS_JS})(textFrom, tree, holder ? holder.font : el) } : {},
244
458
  (${TREE_JS})(el, tree, textOf, depth));
245
459
  }`;
246
460
  /**
247
461
  * Page-side function run on the element (`this`) that returns another
248
462
  * element whose styles CDP reads: `"parent"`, the parent that lays it out
249
- * (`display: contents` skipped, through shadow roots and slots), or
250
- * `"textHolder"`, for an element whose text is in a descendant
251
- * (`<button><span>Buy</span></button>`), the first element holding text,
252
- * whose rendered font is the one shown. Null when there is none.
463
+ * (`display: contents` skipped, through shadow roots and slots),
464
+ * `"textHolder"`, the element whose styles draw most of its text
465
+ * ({@link TEXT_HOLDER_JS}; the element itself when that is its own text), or
466
+ * `"fontHolder"`, the parent of that text's nodes (for the rendered font: a
467
+ * slot's text nodes belong to the host's light DOM). Null when there is none.
253
468
  */
254
469
  export const RELATED_NODE_JS = `function (which) {
255
470
  const tree = (${FLAT_TREE_JS})(this.ownerDocument.defaultView);
256
471
  if (which === 'parent') return tree.layoutParent(this);
257
- const walker = this.ownerDocument.createTreeWalker(this, NodeFilter.SHOW_TEXT);
258
- while (walker.nextNode()) {
259
- if (walker.currentNode.data.trim() !== '') return walker.currentNode.parentElement;
472
+ const holder = (${TEXT_HOLDER_JS})(this, tree);
473
+ if (!holder) return null;
474
+ return which === 'textHolder' ? holder.style : holder.font;
475
+ }`;
476
+ /**
477
+ * Page-side function run on the element (`this`): for each custom property
478
+ * name, a rule in the page's stylesheets (same-origin, `@import`ed,
479
+ * constructed and the element's shadow root's) that sets it, keyframes
480
+ * included: the first one that matches the element or an ancestor now
481
+ * (across shadow roots, so `:root` matches inside a component), else the
482
+ * first that would match without its state (`.btn:hover` for this `.btn`),
483
+ * else the first one, with the `@media`/`@supports` condition it is under
484
+ * when that does not apply. At most 20000 rules are read; cross-origin sheets are
485
+ * skipped.
486
+ */
487
+ export const VARIABLE_SETTERS_JS = `function (names) {
488
+ const el = this;
489
+ const view = el.ownerDocument.defaultView;
490
+ const matching = {};
491
+ const related = {};
492
+ const other = {};
493
+ let budget = 20000;
494
+ const closestAcross = (selector) => {
495
+ for (let n = el; n; n = n.getRootNode().host || null) {
496
+ if (n.closest(selector)) return true;
497
+ }
498
+ return false;
499
+ };
500
+ const matches = (selector) => {
501
+ try { return closestAcross(selector); } catch (e) { return null; }
502
+ };
503
+ const stateless = (selector) => selector.replace(/:(focus-visible|focus-within|focus|hover|active|visited|checked|target|open)(?![\\w-])/g, '');
504
+ const applies = (rule) => {
505
+ if (rule.media && rule.media.mediaText && !view.matchMedia(rule.media.mediaText).matches) return '@media ' + rule.media.mediaText;
506
+ if (typeof CSSSupportsRule !== 'undefined' && rule instanceof CSSSupportsRule && !CSS.supports(rule.conditionText)) return '@supports ' + rule.conditionText;
507
+ return null;
508
+ };
509
+ const record = (name, rule, keyframes, condition) => {
510
+ const selector = keyframes ? '@keyframes ' + keyframes : rule.selectorText || '';
511
+ const match = keyframes || condition ? false : matches(selector);
512
+ const setter = Object.assign(
513
+ { selector: selector, value: rule.style.getPropertyValue(name).trim(), matches: match },
514
+ keyframes ? { keyframes: keyframes } : {},
515
+ condition ? { condition: condition } : {}
516
+ );
517
+ if (match && !matching[name]) matching[name] = setter;
518
+ else if (!match && !keyframes && !related[name] && matches(stateless(selector))) related[name] = setter;
519
+ else if (!match && !other[name]) other[name] = setter;
520
+ };
521
+ const visit = (rules, keyframes, condition) => {
522
+ for (const rule of Array.from(rules)) {
523
+ if (budget-- <= 0 || names.every((name) => matching[name])) return;
524
+ if (typeof CSSImportRule !== 'undefined' && rule instanceof CSSImportRule) {
525
+ try { if (rule.styleSheet) visit(rule.styleSheet.cssRules, keyframes, condition); } catch (e) { continue; }
526
+ continue;
527
+ }
528
+ if (typeof CSSKeyframesRule !== 'undefined' && rule instanceof CSSKeyframesRule) {
529
+ visit(rule.cssRules, rule.name, condition);
530
+ continue;
531
+ }
532
+ if (rule.style) {
533
+ for (const name of names) {
534
+ if (!matching[name] && Array.from(rule.style).includes(name)) record(name, rule, keyframes, condition);
535
+ }
536
+ }
537
+ if (rule.cssRules) visit(rule.cssRules, keyframes, condition || applies(rule));
538
+ }
539
+ };
540
+ const sheets = [];
541
+ for (const scope of new Set([el.getRootNode(), el.ownerDocument])) {
542
+ sheets.push(...Array.from(scope.styleSheets || []), ...Array.from(scope.adoptedStyleSheets || []));
543
+ }
544
+ for (const sheet of sheets) {
545
+ try { visit(sheet.cssRules, null, null); } catch (e) { continue; }
260
546
  }
261
- return null;
547
+ return Object.assign({}, other, related, matching);
262
548
  }`;
263
549
  //# sourceMappingURL=inspectScripts.js.map
@@ -40,6 +40,9 @@ function toNode(raw) {
40
40
  h: Math.round(raw.h),
41
41
  ...(layout && { layout }),
42
42
  ...(raw.text && { text: rowText(raw.text) }),
43
+ ...(raw.display === 'contents' && { contents: true }),
44
+ ...(raw.via && { via: raw.via }),
45
+ ...(raw.shadow && { shadow: true }),
43
46
  ...(children && children.length > 0 && { children }),
44
47
  ...(raw.childCount && { childCount: raw.childCount }),
45
48
  ...(raw.hidden && { hiddenChildren: raw.hidden }),
@@ -50,10 +53,15 @@ function toNode(raw) {
50
53
  *
51
54
  * @param a - Row
52
55
  * @param b - Row
53
- * @returns True for the same label, size and layout
56
+ * @returns True for the same label, size, layout and path (slot, shadow root)
54
57
  */
55
58
  function identical(a, b) {
56
- return a.element === b.element && a.w === b.w && a.h === b.h && a.layout === b.layout;
59
+ return (a.element === b.element &&
60
+ a.w === b.w &&
61
+ a.h === b.h &&
62
+ a.layout === b.layout &&
63
+ a.via === b.via &&
64
+ a.shadow === b.shadow);
57
65
  }
58
66
  /**
59
67
  * Group runs of identical siblings into one row with a count. A group keeps
@@ -8,6 +8,7 @@
8
8
  import type { InspectWhy } from '../../ipc/protocol/inspectTypes.js';
9
9
  import { type Resolution } from './inspectCascade.js';
10
10
  import type { CascadeInput, PropertyGroup } from './inspectCascadeModel.js';
11
+ import type { CssHint } from './inspectHints.js';
11
12
  /**
12
13
  * `--why` for a property.
13
14
  *
@@ -16,5 +17,5 @@ import type { CascadeInput, PropertyGroup } from './inspectCascadeModel.js';
16
17
  * @param input - Matched rules, computed style and the label function
17
18
  * @returns One entry, or one per longhand when they differ
18
19
  */
19
- export declare function buildWhy(group: PropertyGroup, cascade: Map<string, Resolution>, input: CascadeInput): InspectWhy[];
20
+ export declare function buildWhy(group: PropertyGroup, cascade: Map<string, Resolution>, input: CascadeInput, hints?: readonly CssHint[]): InspectWhy[];
20
21
  //# sourceMappingURL=inspectWhyModel.d.ts.map
@@ -5,8 +5,8 @@
5
5
  * declaration sets all its longhands (listing every declaration it beats on
6
6
  * any of them), else one per longhand.
7
7
  */
8
- import { normalizeProperty } from './inspectAllStyles.js';
9
- import { resolveCascade } from './inspectCascade.js';
8
+ import { collapsedValue, normalizeProperty } from './inspectAllStyles.js';
9
+ import { isInherited, resolveCascade, ruleField, } from './inspectCascade.js';
10
10
  import { substituteVariables, unsetVariables, usedVariables, } from './inspectVariables.js';
11
11
  /**
12
12
  * `--why` for a property.
@@ -16,8 +16,8 @@ import { substituteVariables, unsetVariables, usedVariables, } from './inspectVa
16
16
  * @param input - Matched rules, computed style and the label function
17
17
  * @returns One entry, or one per longhand when they differ
18
18
  */
19
- export function buildWhy(group, cascade, input) {
20
- const whys = group.longhands.map((longhand) => whyOf(longhand, cascade.get(longhand), input));
19
+ export function buildWhy(group, cascade, input, hints = []) {
20
+ const whys = group.longhands.map((longhand) => whyOf(longhand, cascade.get(longhand), input, hints));
21
21
  const [first] = whys;
22
22
  if (!first || group.shorthand === undefined)
23
23
  return whys;
@@ -25,9 +25,13 @@ export function buildWhy(group, cascade, input) {
25
25
  if (!whys.every((why) => JSON.stringify(why.chain[0]) === winner))
26
26
  return whys;
27
27
  const values = whys.map((why) => why.computed);
28
- const computed = values.every((value) => value === first.computed)
28
+ const joined = values.every((value) => value === first.computed)
29
29
  ? first.computed
30
30
  : values.join(' ');
31
+ const fromPage = input.whyComputed
32
+ ? normalizeProperty(group.shorthand, input.whyComputed)
33
+ : undefined;
34
+ const computed = collapsedValue(group.shorthand, input.style) ?? fromPage ?? joined;
31
35
  const beaten = whys.flatMap((why) => why.chain.slice(1));
32
36
  const overridden = [...new Map(beaten.map((entry) => [JSON.stringify(entry), entry])).values()];
33
37
  const chain = [...first.chain.slice(0, 1), ...overridden];
@@ -39,14 +43,14 @@ export function buildWhy(group, cascade, input) {
39
43
  * @param property - Longhand
40
44
  * @param resolution - Its resolution
41
45
  * @param input - Matched rules, computed style and the label function
46
+ * @param hints - Hints, for a winner that has no effect
42
47
  * @returns Why
43
48
  */
44
- function whyOf(property, resolution, input) {
49
+ function whyOf(property, resolution, input, hints) {
45
50
  const winner = resolution?.winner;
51
+ const status = winner?.ancestor !== undefined ? 'inherited' : 'applied';
46
52
  const chain = [
47
- ...(winner
48
- ? [whyEntry(winner, winner.ancestor !== undefined ? 'inherited' : 'applied', input)]
49
- : []),
53
+ ...(winner ? [withNote(whyEntry(winner, status, input), winner, property, hints)] : []),
50
54
  ...(resolution?.overridden ?? []).map((d) => whyEntry(d, 'overridden', input)),
51
55
  ];
52
56
  const variables = winner ? variableSources(winner, input) : [];
@@ -68,7 +72,10 @@ function whyOf(property, resolution, input) {
68
72
  * @returns Entry
69
73
  */
70
74
  function whyEntry(declaration, status, input) {
71
- const value = declaration.via ? (declaration.written ?? declaration.value) : declaration.value;
75
+ const written = declaration.via ? (declaration.written ?? declaration.value) : declaration.value;
76
+ const value = written === ''
77
+ ? `${input.style[declaration.property] ?? ''} (set by the browser)`.trim()
78
+ : written;
72
79
  const unset = value.includes('var(') ? unsetVariables(value, input.style) : [];
73
80
  const resolved = value.includes('var(') ? resolvedValue(declaration, value, input) : value;
74
81
  const { specificity, layer, condition } = declaration.source;
@@ -77,6 +84,7 @@ function whyEntry(declaration, status, input) {
77
84
  ...(declaration.via && { via: declaration.via }),
78
85
  ...(unset.length > 0 ? { unset } : resolved !== value && { resolved }),
79
86
  source: input.label(declaration),
87
+ ...ruleField(declaration),
80
88
  ...(specificity && { specificity }),
81
89
  status,
82
90
  ...(declaration.important && { important: true }),
@@ -84,6 +92,40 @@ function whyEntry(declaration, status, input) {
84
92
  ...(condition && { condition }),
85
93
  };
86
94
  }
95
+ /**
96
+ * The winner's entry with why it changes nothing: the hint that says it has
97
+ * no effect, or what applies instead of an invalid `var()`.
98
+ *
99
+ * @param entry - Winner's entry
100
+ * @param winner - Winning declaration
101
+ * @param property - Longhand
102
+ * @param hints - Hints
103
+ * @returns Entry, with `note` when there is one
104
+ */
105
+ function withNote(entry, winner, property, hints) {
106
+ if (entry.unset) {
107
+ const fallback = isInherited(property) ? 'the inherited value' : 'the initial value';
108
+ return { ...entry, note: `falls back to ${fallback}` };
109
+ }
110
+ const inactive = hints.find((hint) => hint.kind === 'inactive' &&
111
+ sameDeclaration(hint.declaration, winner) &&
112
+ (hint.only?.includes(property) ?? true));
113
+ return inactive ? { ...entry, note: `no effect: ${inactive.reason}` } : entry;
114
+ }
115
+ /**
116
+ * Whether two longhand declarations come from the same declaration as written.
117
+ *
118
+ * @param a - Declaration
119
+ * @param b - Declaration
120
+ * @returns True for the same place and property
121
+ */
122
+ function sameDeclaration(a, b) {
123
+ return (a.source.kind === b.source.kind &&
124
+ a.source.styleSheetId === b.source.styleSheetId &&
125
+ a.source.line === b.source.line &&
126
+ a.source.column === b.source.column &&
127
+ (a.via ?? a.property) === (b.via ?? b.property));
128
+ }
87
129
  /** How many levels of custom properties set from others are followed */
88
130
  const MAX_VARIABLE_DEPTH = 5;
89
131
  /**