browser-debugger-cli 0.9.0 → 0.10.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 (133) hide show
  1. package/README.md +4 -1
  2. package/dist/commands/dom/a11y.js +2 -1
  3. package/dist/commands/dom/formInteraction.js +56 -25
  4. package/dist/commands/dom/helpers/keyAttributes.d.ts +20 -0
  5. package/dist/commands/dom/helpers/keyAttributes.js +54 -0
  6. package/dist/commands/dom/helpers/query.d.ts +1 -1
  7. package/dist/commands/dom/helpers/query.js +66 -19
  8. package/dist/commands/dom/helpers/runElementCommand.js +4 -3
  9. package/dist/commands/dom/helpers/screenshot.js +85 -12
  10. package/dist/commands/dom/index.d.ts +1 -0
  11. package/dist/commands/dom/index.js +8 -3
  12. package/dist/commands/dom/inspect.d.ts +15 -0
  13. package/dist/commands/dom/inspect.js +82 -0
  14. package/dist/commands/dom/layout.js +2 -2
  15. package/dist/commands/dom/listeners.js +2 -2
  16. package/dist/commands/dom/semanticUtils.d.ts +14 -1
  17. package/dist/commands/dom/semanticUtils.js +44 -3
  18. package/dist/commands/network/list.js +13 -2
  19. package/dist/commands/optionBehaviors.js +48 -6
  20. package/dist/commands/page.d.ts +1 -1
  21. package/dist/commands/page.js +62 -3
  22. package/dist/commands/shared/commonOptions.d.ts +4 -0
  23. package/dist/commands/shared/commonOptions.js +9 -0
  24. package/dist/commands/shared/optionTypes.d.ts +21 -0
  25. package/dist/commands/shared/startHelpers.d.ts +66 -0
  26. package/dist/commands/shared/startHelpers.js +91 -10
  27. package/dist/commands/shared/validation.d.ts +11 -0
  28. package/dist/commands/shared/validation.js +16 -0
  29. package/dist/daemon/launcher.d.ts +8 -1
  30. package/dist/daemon/launcher.js +3 -1
  31. package/dist/daemon/session/Session.d.ts +7 -0
  32. package/dist/daemon/session/Session.js +23 -1
  33. package/dist/daemon/session/commandRegistry.d.ts +14 -1
  34. package/dist/daemon/session/commandRegistry.js +65 -9
  35. package/dist/daemon/session/interactions.d.ts +18 -5
  36. package/dist/daemon/session/interactions.js +22 -12
  37. package/dist/daemon.js +3565 -329
  38. package/dist/errors/messages.d.ts +69 -0
  39. package/dist/errors/messages.js +102 -1
  40. package/dist/index.js +2416 -1320
  41. package/dist/ipc/client.d.ts +9 -0
  42. package/dist/ipc/client.js +13 -0
  43. package/dist/ipc/protocol/commands.d.ts +56 -1
  44. package/dist/ipc/protocol/commands.js +2 -0
  45. package/dist/ipc/protocol/domTypes.d.ts +35 -2
  46. package/dist/ipc/protocol/inspectTypes.d.ts +388 -0
  47. package/dist/ipc/protocol/inspectTypes.js +10 -0
  48. package/dist/runtime/dom/actionEffects.d.ts +94 -15
  49. package/dist/runtime/dom/actionEffects.js +173 -27
  50. package/dist/runtime/dom/actionEffectsScripts.d.ts +52 -14
  51. package/dist/runtime/dom/actionEffectsScripts.js +224 -32
  52. package/dist/runtime/dom/elementInfo.d.ts +26 -0
  53. package/dist/runtime/dom/elementInfo.js +65 -0
  54. package/dist/runtime/dom/eventListeners.js +14 -4
  55. package/dist/runtime/dom/formFillHelpers/fill.d.ts +3 -4
  56. package/dist/runtime/dom/formFillHelpers/fill.js +77 -28
  57. package/dist/runtime/dom/frameSelection.d.ts +11 -0
  58. package/dist/runtime/dom/frameSelection.js +20 -1
  59. package/dist/runtime/dom/frames.d.ts +38 -5
  60. package/dist/runtime/dom/frames.js +136 -21
  61. package/dist/runtime/dom/inspect.d.ts +28 -0
  62. package/dist/runtime/dom/inspect.js +557 -0
  63. package/dist/runtime/dom/inspectAllStyles.d.ts +62 -0
  64. package/dist/runtime/dom/inspectAllStyles.js +385 -0
  65. package/dist/runtime/dom/inspectCascade.d.ts +94 -0
  66. package/dist/runtime/dom/inspectCascade.js +371 -0
  67. package/dist/runtime/dom/inspectCascadeModel.d.ts +39 -0
  68. package/dist/runtime/dom/inspectCascadeModel.js +232 -0
  69. package/dist/runtime/dom/inspectHints.d.ts +62 -0
  70. package/dist/runtime/dom/inspectHints.js +305 -0
  71. package/dist/runtime/dom/inspectLayoutModel.d.ts +87 -0
  72. package/dist/runtime/dom/inspectLayoutModel.js +346 -0
  73. package/dist/runtime/dom/inspectModel.d.ts +74 -0
  74. package/dist/runtime/dom/inspectModel.js +184 -0
  75. package/dist/runtime/dom/inspectPaintModel.d.ts +157 -0
  76. package/dist/runtime/dom/inspectPaintModel.js +461 -0
  77. package/dist/runtime/dom/inspectRules.d.ts +37 -0
  78. package/dist/runtime/dom/inspectRules.js +101 -0
  79. package/dist/runtime/dom/inspectScripts.d.ts +132 -0
  80. package/dist/runtime/dom/inspectScripts.js +263 -0
  81. package/dist/runtime/dom/inspectTree.d.ts +40 -0
  82. package/dist/runtime/dom/inspectTree.js +134 -0
  83. package/dist/runtime/dom/inspectVariables.d.ts +33 -0
  84. package/dist/runtime/dom/inspectVariables.js +94 -0
  85. package/dist/runtime/dom/inspectWhyModel.d.ts +20 -0
  86. package/dist/runtime/dom/inspectWhyModel.js +134 -0
  87. package/dist/runtime/dom/layout.d.ts +5 -1
  88. package/dist/runtime/dom/layout.js +10 -3
  89. package/dist/runtime/dom/listenerPageScripts.d.ts +11 -5
  90. package/dist/runtime/dom/listenerPageScripts.js +95 -9
  91. package/dist/runtime/dom/listenerSummary.d.ts +4 -0
  92. package/dist/runtime/dom/listenerSummary.js +26 -9
  93. package/dist/runtime/dom/reactEventHelpers.d.ts +5 -0
  94. package/dist/runtime/dom/reactEventHelpers.js +12 -4
  95. package/dist/runtime/page/emulation.d.ts +20 -0
  96. package/dist/runtime/page/emulation.js +37 -0
  97. package/dist/telemetry/a11y.d.ts +10 -0
  98. package/dist/telemetry/a11y.js +78 -1
  99. package/dist/telemetry/console.d.ts +1 -0
  100. package/dist/telemetry/console.js +100 -5
  101. package/dist/telemetry/network.js +3 -1
  102. package/dist/types.d.ts +32 -0
  103. package/dist/ui/formatters/details.d.ts +8 -0
  104. package/dist/ui/formatters/details.js +59 -3
  105. package/dist/ui/formatters/dom.d.ts +2 -1
  106. package/dist/ui/formatters/dom.js +25 -9
  107. package/dist/ui/formatters/inspect.d.ts +39 -0
  108. package/dist/ui/formatters/inspect.js +596 -0
  109. package/dist/ui/formatters/keyAttributes.d.ts +19 -0
  110. package/dist/ui/formatters/keyAttributes.js +84 -0
  111. package/dist/ui/formatters/layout.js +2 -2
  112. package/dist/ui/formatters/networkHeaders.d.ts +13 -0
  113. package/dist/ui/formatters/networkHeaders.js +23 -3
  114. package/dist/ui/formatters/networkList.d.ts +29 -1
  115. package/dist/ui/formatters/networkList.js +86 -20
  116. package/dist/ui/formatters/status.js +1 -1
  117. package/dist/ui/formatting.d.ts +9 -0
  118. package/dist/ui/formatting.js +6 -3
  119. package/dist/ui/messages/commands.d.ts +123 -7
  120. package/dist/ui/messages/commands.js +181 -10
  121. package/dist/ui/messages/networkMessages.d.ts +14 -0
  122. package/dist/ui/messages/networkMessages.js +18 -0
  123. package/dist/ui/messages/session.d.ts +14 -0
  124. package/dist/ui/messages/session.js +20 -0
  125. package/dist/utils/async.d.ts +9 -0
  126. package/dist/utils/async.js +17 -0
  127. package/dist/utils/color.d.ts +84 -0
  128. package/dist/utils/color.js +376 -0
  129. package/dist/utils/cssValues.d.ts +109 -0
  130. package/dist/utils/cssValues.js +236 -0
  131. package/dist/utils/selectorFilters.d.ts +12 -0
  132. package/dist/utils/selectorFilters.js +29 -0
  133. package/package.json +1 -1
@@ -0,0 +1,557 @@
1
+ /**
2
+ * `bdg dom inspect`: what one element looks like, read in the daemon.
3
+ *
4
+ * The element is found like the other element commands find it (selector
5
+ * with filters through open shadow roots and same-origin iframes, or the
6
+ * exact cached node). Then, in parallel: one page-side walk on the element
7
+ * ({@link INSPECT_PAGE_JS}: text, placement in the parent, backgrounds, child
8
+ * tree), `dom layout`'s measurement (page position, hidden, covered,
9
+ * offscreen) and, once the nodes are pushed to CDP, `CSS.getComputedStyleForNode`
10
+ * for the element, its layout parent and its `::before`/`::after`,
11
+ * `CSS.getPlatformFontsForNode` for its text and `DOM.getBoxModel`. DOM and
12
+ * CSS are enabled on the first inspect and kept on. Matched rules are not
13
+ * read (no cascade).
14
+ */
15
+ import { CommandError } from '../../errors/index.js';
16
+ import { operationFailedError, unknownCssPropertyError } from '../../errors/messages.js';
17
+ import { throwIfInvalidSelector } from './formFillHelpers/shared.js';
18
+ import { selectedProps } from './inspectAllStyles.js';
19
+ import { buildCascadeFields } from './inspectCascadeModel.js';
20
+ import { buildInspectResult } from './inspectModel.js';
21
+ import { matchedStyles, sourceLabel, trackStyleSheets } from './inspectRules.js';
22
+ import { INSPECT_PAGE_JS, RELATED_NODE_JS } from './inspectScripts.js';
23
+ import { DEFAULT_TREE_DEPTH, DEFAULT_TREE_LIMIT } from './inspectTree.js';
24
+ import { inspectLayout } from './layout.js';
25
+ import { DEEP_QUERY_JS, missingElementError, selectorArgsJS } from './targetNode.js';
26
+ import { createLogger } from '../../ui/logging/index.js';
27
+ import { getErrorMessage } from '../../utils/errors.js';
28
+ import { EXIT_CODES } from '../../utils/exitCodes.js';
29
+ import { findSimilar } from '../../utils/suggestions.js';
30
+ const log = createLogger('dom');
31
+ /** Connections DOM and CSS were enabled on */
32
+ const stylesEnabled = new WeakSet();
33
+ /** Time allowed for the matched rules behind the default hints (large stylesheets take longer) */
34
+ const HINTS_BUDGET_MS = 1000;
35
+ /** Time allowed for them with --rules or --why */
36
+ const RULES_BUDGET_MS = 5000;
37
+ /** Distinguishes the object groups of concurrent calls */
38
+ let groupCounter = 0;
39
+ /** Page-side choice of a match: the index asked for, else the first rendered one (the first when none is) */
40
+ const PICK_MATCH_JS = `function (i) {
41
+ if (i !== null) return i;
42
+ const shown = (el) => (el.checkVisibility ? el.checkVisibility() : el.getClientRects().length > 0);
43
+ const first = Array.prototype.findIndex.call(this, shown);
44
+ return first < 0 ? 0 : first;
45
+ }`;
46
+ /**
47
+ * Inspect one element.
48
+ *
49
+ * @param cdp - CDP connection
50
+ * @param params - Selector (and index) or backend node id, and options
51
+ * @returns Inspect result
52
+ * @throws CommandError (83) no match, (81) index out of range, invalid
53
+ * selector or unknown property, (87) the cached element left the page
54
+ */
55
+ export async function inspectElement(cdp, params) {
56
+ const started = Date.now();
57
+ const objectGroup = `bdg-inspect-${++groupCounter}`;
58
+ try {
59
+ const found = await findElement(cdp, params, objectGroup);
60
+ const sources = await readSources(cdp, found.objectId, params, objectGroup);
61
+ const propValues = sources.props ? checkedProps(sources.props, sources) : undefined;
62
+ const built = buildInspectResult(sources, {
63
+ selector: params.selector,
64
+ index: found.index,
65
+ count: found.count,
66
+ treeLimit: params.treeLimit ?? DEFAULT_TREE_LIMIT,
67
+ ...(params.all && { all: true }),
68
+ ...(propValues && { propValues }),
69
+ });
70
+ const withCascade = { ...built, ...cascadeFields(cdp, sources) };
71
+ const result = found.picked ? { ...withCascade, picked: found.picked } : withCascade;
72
+ log.debug(`Inspected ${result.element} in ${Date.now() - started} ms`);
73
+ return result;
74
+ }
75
+ finally {
76
+ void cdp
77
+ .send('Runtime.releaseObjectGroup', { objectGroup })
78
+ .catch((error) => log.debug(`Object group not released: ${getErrorMessage(error)}`));
79
+ }
80
+ }
81
+ /**
82
+ * The properties asked for with `--props`.
83
+ *
84
+ * @param names - Property names (custom property patterns expanded)
85
+ * @param sources - What was read
86
+ * @returns Values by name
87
+ * @throws CommandError (81) for a name no value was found for
88
+ */
89
+ function checkedProps(names, sources) {
90
+ const { props, unknown } = selectedProps(names, sources.style, sources.raw.props, sources.raw.unknownProps);
91
+ if (unknown.length === 0)
92
+ return props;
93
+ const suggestions = unknown.flatMap((name) => findSimilar(name, Object.keys(sources.style), { maxSuggestions: 1 }));
94
+ const err = unknownCssPropertyError(unknown, suggestions);
95
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
96
+ }
97
+ /**
98
+ * `--props` names with custom property patterns expanded: `--*` gives every
99
+ * custom property the element has (its own and inherited), `--bs-btn-*`
100
+ * those with that prefix, sorted.
101
+ *
102
+ * @param names - Names asked for
103
+ * @param style - Computed styles (custom properties included)
104
+ * @returns Names
105
+ */
106
+ function expandCustomPropertyPatterns(names, style) {
107
+ return names.flatMap((name) => {
108
+ if (!name.startsWith('--') || !name.endsWith('*'))
109
+ return [name];
110
+ const prefix = name.slice(0, -1);
111
+ return Object.keys(style)
112
+ .filter((key) => key.startsWith('--') && key.startsWith(prefix))
113
+ .sort();
114
+ });
115
+ }
116
+ /**
117
+ * Hints, `--rules` and `--why` from the matched rules.
118
+ *
119
+ * @param cdp - CDP connection (stylesheet headers for the source labels)
120
+ * @param sources - What was read
121
+ * @returns Cascade fields, or `cascade: 'timeout' | 'failed'` when the rules were not read
122
+ */
123
+ function cascadeFields(cdp, sources) {
124
+ if (!sources.matched)
125
+ return {};
126
+ if (typeof sources.matched === 'string')
127
+ return { cascade: sources.matched };
128
+ return buildCascadeFields({
129
+ matched: sources.matched,
130
+ style: sources.style,
131
+ parentStyle: sources.parentStyle,
132
+ replaced: sources.raw.replaced === true,
133
+ formControl: sources.raw.formControl,
134
+ ...(sources.hints === false && { hints: false }),
135
+ label: (declaration) => sourceLabel(declaration, cdp),
136
+ ...(sources.rules && { rules: true }),
137
+ ...(sources.why && { why: sources.why }),
138
+ ...(sources.props && { props: sources.props }),
139
+ });
140
+ }
141
+ /**
142
+ * Find the element: the cached node, or the match at the index (default 0)
143
+ * among the selector's matches.
144
+ *
145
+ * @param cdp - CDP connection
146
+ * @param params - Selector (and index) or backend node id
147
+ * @param objectGroup - Object group for the handles
148
+ * @returns The element's remote object and the match count
149
+ * @throws CommandError when there is no such element
150
+ */
151
+ async function findElement(cdp, params, objectGroup) {
152
+ if (params.backendNodeId !== undefined) {
153
+ const objectId = await resolveCachedNode(cdp, params.backendNodeId, objectGroup);
154
+ if (!objectId)
155
+ throw missingElementError(params, 0);
156
+ return { objectId, count: 1, index: 0 };
157
+ }
158
+ const matches = await querySelector(cdp, params.selector, objectGroup);
159
+ const [count, index] = await Promise.all([
160
+ callOn(cdp, matches, 'function () { return this.length; }', []),
161
+ callOn(cdp, matches, PICK_MATCH_JS, [params.index ?? null]),
162
+ ]);
163
+ const element = (await cdp.send('Runtime.callFunctionOn', {
164
+ objectId: matches,
165
+ functionDeclaration: 'function (i) { return this[i] || null; }',
166
+ arguments: [{ value: index ?? 0 }],
167
+ objectGroup,
168
+ }));
169
+ const objectId = element.result.objectId;
170
+ if (!objectId)
171
+ throw missingElementError(params, count ?? 0);
172
+ return { objectId, count: count ?? 0, index: index ?? 0, ...pickedHow(params, count, index) };
173
+ }
174
+ /**
175
+ * How a match was chosen, for the note on several matches.
176
+ *
177
+ * @param params - Command parameters (an explicit --index needs no note)
178
+ * @param count - Number of matches
179
+ * @param index - Index chosen
180
+ * @returns `picked` when no index was given and several elements matched
181
+ */
182
+ function pickedHow(params, count, index) {
183
+ if (params.index !== undefined || (count ?? 0) < 2)
184
+ return {};
185
+ return { picked: (index ?? 0) > 0 ? 'first-visible' : 'first' };
186
+ }
187
+ /**
188
+ * Resolve a cached node that is still in the page.
189
+ *
190
+ * @param cdp - CDP connection
191
+ * @param backendNodeId - Backend node id from the query cache
192
+ * @param objectGroup - Object group for the handle
193
+ * @returns Remote object id, or undefined when the node left the page
194
+ */
195
+ async function resolveCachedNode(cdp, backendNodeId, objectGroup) {
196
+ const resolved = (await cdp
197
+ .send('DOM.resolveNode', { backendNodeId, objectGroup })
198
+ .catch((error) => {
199
+ log.debug(`Node ${backendNodeId} not resolved: ${getErrorMessage(error)}`);
200
+ return {};
201
+ }));
202
+ const objectId = resolved.object?.objectId;
203
+ if (!objectId)
204
+ return undefined;
205
+ const connected = await callOn(cdp, objectId, 'function () { return this.isConnected; }', []);
206
+ return connected ? objectId : undefined;
207
+ }
208
+ /**
209
+ * Run the selector search ({@link DEEP_QUERY_JS}) and keep the array of matches.
210
+ *
211
+ * @param cdp - CDP connection
212
+ * @param selector - Selector (filters allowed)
213
+ * @param objectGroup - Object group for the array
214
+ * @returns Remote object id of the array
215
+ * @throws CommandError (81) invalid selector, (91) page script failure
216
+ */
217
+ async function querySelector(cdp, selector, objectGroup) {
218
+ const response = (await cdp.send('Runtime.evaluate', {
219
+ expression: `(${DEEP_QUERY_JS})(${selectorArgsJS(selector)})`,
220
+ objectGroup,
221
+ }));
222
+ if (response.exceptionDetails || !response.result.objectId) {
223
+ if (response.exceptionDetails)
224
+ throwIfInvalidSelector(response.exceptionDetails, selector);
225
+ const err = operationFailedError('find the element', response.exceptionDetails?.text ?? 'no result');
226
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.SCRIPT_ERROR);
227
+ }
228
+ return response.result.objectId;
229
+ }
230
+ /**
231
+ * Call a function on a remote object and return its value.
232
+ *
233
+ * @param cdp - CDP connection
234
+ * @param objectId - Remote object (`this`)
235
+ * @param functionDeclaration - Function source
236
+ * @param args - Arguments (JSON values)
237
+ * @returns The value, or undefined when the call threw
238
+ */
239
+ async function callOn(cdp, objectId, functionDeclaration, args) {
240
+ const response = (await cdp.send('Runtime.callFunctionOn', {
241
+ objectId,
242
+ functionDeclaration,
243
+ arguments: args.map((value) => ({ value })),
244
+ returnByValue: true,
245
+ }));
246
+ if (response.exceptionDetails) {
247
+ log.debug(`Page function failed: ${response.exceptionDetails.exception?.description ?? response.exceptionDetails.text}`);
248
+ return undefined;
249
+ }
250
+ return response.result.value;
251
+ }
252
+ /**
253
+ * Read everything about the element: the page-side walk, `dom layout`'s
254
+ * measurement and the CDP styles, fonts and box.
255
+ *
256
+ * @param cdp - CDP connection
257
+ * @param objectId - The element
258
+ * @param params - Request
259
+ * @param objectGroup - Object group for handles
260
+ * @returns Inputs of {@link buildInspectResult}
261
+ */
262
+ async function readSources(cdp, objectId, params, objectGroup) {
263
+ const related = await relatedNodes(cdp, objectId, objectGroup);
264
+ const [raw, measured, styles] = await Promise.all([
265
+ readPage(cdp, objectId, params),
266
+ measure(cdp, params.selector, related.node),
267
+ readStyles(cdp, related, params),
268
+ ]);
269
+ return {
270
+ raw,
271
+ ...styles,
272
+ fonts: raw.ownText || raw.formControl ? styles.fonts.node : styles.fonts.textHolder,
273
+ ...measured,
274
+ ...(params.rules && { rules: true }),
275
+ ...(params.why && { why: params.why }),
276
+ ...(params.props && { props: expandCustomPropertyPatterns(params.props, styles.style) }),
277
+ ...(params.hints === false && { hints: false }),
278
+ };
279
+ }
280
+ /**
281
+ * The page-side walk on the element.
282
+ *
283
+ * @param cdp - CDP connection
284
+ * @param objectId - The element
285
+ * @param params - Tree depth and `--props`
286
+ * @returns Page-side measurements
287
+ * @throws CommandError (91) when the walk fails
288
+ */
289
+ async function readPage(cdp, objectId, params) {
290
+ const raw = await callOn(cdp, objectId, INSPECT_PAGE_JS, [
291
+ params.props || params.all ? 0 : (params.tree ?? DEFAULT_TREE_DEPTH),
292
+ params.props ?? null,
293
+ ]);
294
+ if (raw)
295
+ return raw;
296
+ const err = operationFailedError('inspect the element', 'the page script failed');
297
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.SCRIPT_ERROR);
298
+ }
299
+ /**
300
+ * The backend node id of the element's layout parent or first text holder.
301
+ *
302
+ * @param cdp - CDP connection
303
+ * @param objectId - The element
304
+ * @param which - `parent` or `textHolder`
305
+ * @param objectGroup - Object group for handles
306
+ * @returns Backend node id, or undefined when there is none
307
+ */
308
+ async function relatedNode(cdp, objectId, which, objectGroup) {
309
+ const response = (await cdp.send('Runtime.callFunctionOn', {
310
+ objectId,
311
+ functionDeclaration: RELATED_NODE_JS,
312
+ arguments: [{ value: which }],
313
+ objectGroup,
314
+ }));
315
+ const id = response.result.objectId;
316
+ return id ? (await describe(cdp, id)).backendNodeId : undefined;
317
+ }
318
+ /**
319
+ * The backend node ids of the element, its layout parent, its first text
320
+ * holder and its generated pseudo-elements.
321
+ *
322
+ * @param cdp - CDP connection
323
+ * @param objectId - The element
324
+ * @param objectGroup - Object group for handles
325
+ * @returns Backend node ids
326
+ */
327
+ async function relatedNodes(cdp, objectId, objectGroup) {
328
+ const [node, parent, textHolder] = await Promise.all([
329
+ describe(cdp, objectId),
330
+ relatedNode(cdp, objectId, 'parent', objectGroup),
331
+ relatedNode(cdp, objectId, 'textHolder', objectGroup),
332
+ ]);
333
+ const pseudo = (node.pseudoElements ?? [])
334
+ .filter((p) => p.pseudoType === 'before' || p.pseudoType === 'after')
335
+ .map((p) => ({
336
+ type: `::${p.pseudoType}`,
337
+ backendNodeId: p.backendNodeId,
338
+ }));
339
+ return {
340
+ node: node.backendNodeId,
341
+ ...(parent && { parent }),
342
+ ...(textHolder && { textHolder }),
343
+ pseudo,
344
+ };
345
+ }
346
+ /**
347
+ * Describe a node (no tracking needed).
348
+ *
349
+ * @param cdp - CDP connection
350
+ * @param objectId - Remote object of the node
351
+ * @returns CDP node description
352
+ */
353
+ async function describe(cdp, objectId) {
354
+ const { node } = (await cdp.send('DOM.describeNode', {
355
+ objectId,
356
+ }));
357
+ return node;
358
+ }
359
+ /**
360
+ * The element as `dom layout` measures it (page position, visibility, cover)
361
+ * and the color scheme the page sees. Not fatal: without it the header has
362
+ * no position.
363
+ *
364
+ * @param cdp - CDP connection
365
+ * @param selector - Selector of the request
366
+ * @param backendNodeId - The element
367
+ * @returns Layout and color scheme
368
+ */
369
+ async function measure(cdp, selector, backendNodeId) {
370
+ try {
371
+ const result = await inspectLayout(cdp, { selector, backendNodeId });
372
+ const [layout] = result.elements;
373
+ return {
374
+ ...(layout && { layout }),
375
+ ...(result.page.colorScheme && { colorScheme: result.page.colorScheme }),
376
+ };
377
+ }
378
+ catch (error) {
379
+ log.debug(`Layout not measured: ${getErrorMessage(error)}`);
380
+ return {};
381
+ }
382
+ }
383
+ /**
384
+ * Node ids of the related nodes, for the CSS methods.
385
+ *
386
+ * @param cdp - CDP connection
387
+ * @param related - Backend node ids
388
+ * @returns Node id for a backend node id (undefined when it cannot be tracked)
389
+ */
390
+ async function nodeIdLookup(cdp, related) {
391
+ const order = [
392
+ related.node,
393
+ related.parent,
394
+ related.textHolder,
395
+ ...related.pseudo.map((p) => p.backendNodeId),
396
+ ];
397
+ const ids = await pushNodes(cdp, order.filter((id) => id !== undefined));
398
+ return (backendNodeId) => (backendNodeId === undefined ? undefined : ids.get(backendNodeId));
399
+ }
400
+ /**
401
+ * Computed styles of the element, its parent and pseudo-elements, the
402
+ * platform fonts of its text and its border box size.
403
+ *
404
+ * @param cdp - CDP connection
405
+ * @param related - Backend node ids
406
+ * @returns CDP styles
407
+ */
408
+ async function readStyles(cdp, related, params) {
409
+ await enableStyleDomains(cdp);
410
+ const nodeIdOf = await nodeIdLookup(cdp, related);
411
+ const [matched, style, parentStyle, nodeFonts, holderFonts, size, pseudo] = await Promise.all([
412
+ readMatched(cdp, nodeIdOf(related.node), params),
413
+ computedStyle(cdp, nodeIdOf(related.node)),
414
+ related.parent === undefined ? undefined : computedStyle(cdp, nodeIdOf(related.parent)),
415
+ platformFonts(cdp, nodeIdOf(related.node)),
416
+ platformFonts(cdp, nodeIdOf(related.textHolder)),
417
+ borderBoxSize(cdp, related.node),
418
+ Promise.all(related.pseudo.map((p) => pseudoSource(cdp, p.type, p.backendNodeId, nodeIdOf(p.backendNodeId)))),
419
+ ]);
420
+ return {
421
+ style,
422
+ ...(parentStyle && { parentStyle }),
423
+ pseudo,
424
+ fonts: { node: nodeFonts, textHolder: holderFonts },
425
+ ...(size && { size }),
426
+ ...(matched && { matched }),
427
+ };
428
+ }
429
+ /**
430
+ * The element's matched rules, when hints, `--rules` or `--why` need them:
431
+ * within {@link HINTS_BUDGET_MS} for the default hints (skipped on very
432
+ * large stylesheets), {@link RULES_BUDGET_MS} when asked for explicitly.
433
+ *
434
+ * @param cdp - CDP connection
435
+ * @param nodeId - Node id of the element
436
+ * @param params - Request
437
+ * @returns Matched styles, `timeout`, or undefined when not needed (or no node id)
438
+ */
439
+ async function readMatched(cdp, nodeId, params) {
440
+ const explicit = params.rules === true || params.why !== undefined;
441
+ const skipped = !explicit && (params.hints === false || params.props !== undefined || params.all === true);
442
+ if (nodeId === undefined || skipped) {
443
+ return undefined;
444
+ }
445
+ return matchedStyles(cdp, nodeId, explicit ? RULES_BUDGET_MS : HINTS_BUDGET_MS);
446
+ }
447
+ /**
448
+ * A pseudo-element's computed styles and size.
449
+ *
450
+ * @param cdp - CDP connection
451
+ * @param type - `::before` or `::after`
452
+ * @param backendNodeId - Its backend node id
453
+ * @param nodeId - Its node id
454
+ * @returns Pseudo source
455
+ */
456
+ async function pseudoSource(cdp, type, backendNodeId, nodeId) {
457
+ const [style, size] = await Promise.all([
458
+ computedStyle(cdp, nodeId),
459
+ borderBoxSize(cdp, backendNodeId),
460
+ ]);
461
+ return { type, style, ...(size && { size: { w: Math.round(size.w), h: Math.round(size.h) } }) };
462
+ }
463
+ /**
464
+ * Enable DOM and CSS once per connection (kept on: CSS.enable replays every
465
+ * stylesheet, which costs up to a few hundred ms on large sites the first time).
466
+ *
467
+ * @param cdp - CDP connection
468
+ */
469
+ async function enableStyleDomains(cdp) {
470
+ if (stylesEnabled.has(cdp))
471
+ return;
472
+ trackStyleSheets(cdp);
473
+ await cdp.send('DOM.enable', {});
474
+ await cdp.send('CSS.enable', {});
475
+ stylesEnabled.add(cdp);
476
+ }
477
+ /**
478
+ * Node ids for backend node ids (CSS methods take node ids). When none can
479
+ * be tracked, the document was never requested on this connection (or was
480
+ * replaced by a navigation): it is requested, shallowly, and the push tried
481
+ * again. A node that left the page meanwhile is simply missing.
482
+ *
483
+ * @param cdp - CDP connection
484
+ * @param backendNodeIds - Backend node ids
485
+ * @returns Node id per backend node id (missing when CDP cannot track it)
486
+ */
487
+ async function pushNodes(cdp, backendNodeIds) {
488
+ const push = async () => {
489
+ const response = (await cdp.send('DOM.pushNodesByBackendIdsToFrontend', {
490
+ backendNodeIds,
491
+ }));
492
+ return response.nodeIds;
493
+ };
494
+ let nodeIds = await push().catch(() => []);
495
+ if (nodeIds.every((id) => id === 0)) {
496
+ await cdp.send('DOM.getDocument', { depth: 0 });
497
+ nodeIds = await push().catch(() => []);
498
+ }
499
+ return new Map(backendNodeIds.flatMap((backendNodeId, i) => nodeIds[i] ? [[backendNodeId, nodeIds[i]]] : []));
500
+ }
501
+ /**
502
+ * Computed styles of a node.
503
+ *
504
+ * @param cdp - CDP connection
505
+ * @param nodeId - Node id (none: empty)
506
+ * @returns Styles by property name
507
+ */
508
+ async function computedStyle(cdp, nodeId) {
509
+ if (!nodeId)
510
+ return {};
511
+ const response = (await cdp
512
+ .send('CSS.getComputedStyleForNode', { nodeId })
513
+ .catch((error) => {
514
+ log.debug(`Computed style not read: ${getErrorMessage(error)}`);
515
+ return { computedStyle: [] };
516
+ }));
517
+ return Object.fromEntries(response.computedStyle.map((entry) => [entry.name, entry.value]));
518
+ }
519
+ /**
520
+ * Fonts Chrome rendered a node's own text with.
521
+ *
522
+ * @param cdp - CDP connection
523
+ * @param nodeId - Node id (none: no fonts)
524
+ * @returns Platform fonts
525
+ */
526
+ async function platformFonts(cdp, nodeId) {
527
+ if (!nodeId)
528
+ return [];
529
+ const response = (await cdp
530
+ .send('CSS.getPlatformFontsForNode', { nodeId })
531
+ .catch((error) => {
532
+ log.debug(`Platform fonts not read: ${getErrorMessage(error)}`);
533
+ return { fonts: [] };
534
+ }));
535
+ return response.fonts;
536
+ }
537
+ /**
538
+ * Border box size of a node.
539
+ *
540
+ * @param cdp - CDP connection
541
+ * @param backendNodeId - Backend node id
542
+ * @returns Width and height, or undefined when it has no box (not rendered)
543
+ */
544
+ async function borderBoxSize(cdp, backendNodeId) {
545
+ try {
546
+ const { model } = (await cdp.send('DOM.getBoxModel', {
547
+ backendNodeId,
548
+ }));
549
+ const [x1 = 0, y1 = 0, x2 = 0, y2 = 0, , , x4 = 0, y4 = 0] = model.border;
550
+ return { w: Math.hypot(x2 - x1, y2 - y1), h: Math.hypot(x4 - x1, y4 - y1) };
551
+ }
552
+ catch (error) {
553
+ log.debug(`No box model: ${getErrorMessage(error)}`);
554
+ return undefined;
555
+ }
556
+ }
557
+ //# sourceMappingURL=inspect.js.map
@@ -0,0 +1,62 @@
1
+ /**
2
+ * `bdg dom inspect --all` and `--props`.
3
+ *
4
+ * `--all` lists every computed longhand that is not noise and not its
5
+ * default, collapsed into shorthands (margin, padding, inset, border,
6
+ * radius, overflow, gap, flex, grid area, outline, animation). Defaults are
7
+ * the CSS initial values as Chrome computes them; the UA stylesheet's
8
+ * per-tag values (a `<p>`'s margins, a button's padding) are real values and
9
+ * are listed. Display, font, size, line height and color are always listed.
10
+ *
11
+ * Noise: custom properties, logical duplicates of physical properties,
12
+ * width/height (in the header), transitions, origins without a transform,
13
+ * colors that only repeat `color` (currentColor), properties of borders,
14
+ * outlines and rules that are not drawn, SVG paint properties on HTML
15
+ * elements, and vendor-prefixed internals.
16
+ */
17
+ import type { InspectProp } from '../../ipc/protocol/inspectTypes.js';
18
+ import type { StyleMap } from './inspectLayoutModel.js';
19
+ /**
20
+ * Whether a computed value is its property's default.
21
+ *
22
+ * @param name - Property name
23
+ * @param value - Computed value
24
+ * @returns True for the initial value (or an equivalent), an empty value, and
25
+ * a generic default keyword for a property the table does not know
26
+ */
27
+ export declare function isDefaultValue(name: string, value: string): boolean;
28
+ /**
29
+ * A computed value normalized: px as numbers, colors as hex, URLs as file
30
+ * names, matrices as transforms.
31
+ *
32
+ * @param name - Property name
33
+ * @param value - Computed value
34
+ * @returns Normalized value
35
+ */
36
+ export declare function normalizeProperty(name: string, value: string): string;
37
+ /**
38
+ * Every computed longhand that is not noise and not a default, normalized
39
+ * and collapsed into shorthands.
40
+ *
41
+ * @param style - Computed styles
42
+ * @param svg - The element is an SVG element (SVG properties are listed)
43
+ * @returns Properties and values
44
+ */
45
+ export declare function allStyles(style: StyleMap, svg: boolean): Record<string, string>;
46
+ /**
47
+ * The properties asked for with `--props`, raw and normalized. CDP's
48
+ * computed longhands come first; shorthands and custom properties come from
49
+ * the page (`getComputedStyle`).
50
+ *
51
+ * @param names - Property names, lowercase
52
+ * @param style - Computed longhands (CDP)
53
+ * @param pageValues - Values the page read
54
+ * @param pageUnknown - Names the page does not know as CSS properties
55
+ * @returns Values by name (an unset custom property is empty), and the
56
+ * names that are not properties
57
+ */
58
+ export declare function selectedProps(names: readonly string[], style: StyleMap, pageValues: Record<string, string> | undefined, pageUnknown?: readonly string[]): {
59
+ props: Record<string, InspectProp>;
60
+ unknown: string[];
61
+ };
62
+ //# sourceMappingURL=inspectAllStyles.d.ts.map