browser-debugger-cli 0.11.0 → 0.13.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 (222) hide show
  1. package/.claude/skills/bdg/SKILL.md +4 -4
  2. package/README.md +143 -79
  3. package/dist/commands/cdp.d.ts +22 -1
  4. package/dist/commands/cdp.js +100 -43
  5. package/dist/commands/console.d.ts +12 -0
  6. package/dist/commands/console.js +62 -12
  7. package/dist/commands/css.d.ts +13 -0
  8. package/dist/commands/css.js +53 -0
  9. package/dist/commands/dom/DomElementResolver.d.ts +3 -1
  10. package/dist/commands/dom/DomElementResolver.js +10 -3
  11. package/dist/commands/dom/a11y.js +3 -2
  12. package/dist/commands/dom/audit.d.ts +14 -0
  13. package/dist/commands/dom/audit.js +87 -0
  14. package/dist/commands/dom/eval.d.ts +3 -2
  15. package/dist/commands/dom/eval.js +11 -5
  16. package/dist/commands/dom/form.js +10 -9
  17. package/dist/commands/dom/formInteraction.js +42 -11
  18. package/dist/commands/dom/get.js +8 -8
  19. package/dist/commands/dom/helpers/index.d.ts +1 -1
  20. package/dist/commands/dom/helpers/index.js +1 -1
  21. package/dist/commands/dom/helpers/keyAttributes.d.ts +3 -2
  22. package/dist/commands/dom/helpers/keyAttributes.js +6 -4
  23. package/dist/commands/dom/helpers/query.d.ts +27 -3
  24. package/dist/commands/dom/helpers/query.js +152 -64
  25. package/dist/commands/dom/helpers/screenshot.d.ts +1 -0
  26. package/dist/commands/dom/helpers/screenshot.js +169 -49
  27. package/dist/commands/dom/index.js +7 -2
  28. package/dist/commands/dom/query.d.ts +19 -2
  29. package/dist/commands/dom/query.js +37 -6
  30. package/dist/commands/dom/screenshot.js +12 -7
  31. package/dist/commands/dom/semanticUtils.d.ts +3 -2
  32. package/dist/commands/dom/semanticUtils.js +40 -9
  33. package/dist/commands/dom/wait.js +5 -3
  34. package/dist/commands/helpJson.d.ts +82 -19
  35. package/dist/commands/helpJson.js +112 -41
  36. package/dist/commands/helpTopic.d.ts +16 -1
  37. package/dist/commands/helpTopic.js +59 -1
  38. package/dist/commands/installSkill.d.ts +15 -5
  39. package/dist/commands/installSkill.js +86 -16
  40. package/dist/commands/network/list.js +22 -12
  41. package/dist/commands/optionBehaviors.js +53 -16
  42. package/dist/commands/page.js +7 -4
  43. package/dist/commands/peek.d.ts +7 -0
  44. package/dist/commands/peek.js +65 -23
  45. package/dist/commands/shared/daemonErrorHandler.d.ts +5 -2
  46. package/dist/commands/shared/daemonErrorHandler.js +20 -9
  47. package/dist/commands/shared/dataFetcher.d.ts +12 -4
  48. package/dist/commands/shared/dataFetcher.js +12 -4
  49. package/dist/commands/shared/followMode.d.ts +9 -1
  50. package/dist/commands/shared/followMode.js +22 -4
  51. package/dist/commands/shared/optionTypes.d.ts +9 -2
  52. package/dist/commands/shared/outputFile.js +6 -1
  53. package/dist/commands/start.d.ts +20 -5
  54. package/dist/commands/start.js +84 -23
  55. package/dist/commands/stop.d.ts +11 -0
  56. package/dist/commands/stop.js +24 -1
  57. package/dist/commands/tail.d.ts +7 -1
  58. package/dist/commands/tail.js +13 -62
  59. package/dist/commands.js +2 -0
  60. package/dist/connection/cdp.d.ts +7 -0
  61. package/dist/connection/cdp.js +9 -0
  62. package/dist/connection/launcher.js +3 -2
  63. package/dist/daemon/SessionController.js +6 -1
  64. package/dist/daemon/launcher.d.ts +3 -2
  65. package/dist/daemon/launcher.js +47 -3
  66. package/dist/daemon/session/Session.d.ts +4 -1
  67. package/dist/daemon/session/Session.js +33 -2
  68. package/dist/daemon/session/TelemetryStore.d.ts +8 -1
  69. package/dist/daemon/session/TelemetryStore.js +13 -1
  70. package/dist/daemon/session/commandRegistry.js +36 -14
  71. package/dist/daemon/session/interactions.d.ts +2 -1
  72. package/dist/daemon/session/interactions.js +13 -1
  73. package/dist/daemon/session/plugins.js +19 -53
  74. package/dist/daemon/session/teardown.js +1 -1
  75. package/dist/daemon.js +9234 -7222
  76. package/dist/errors/messages.d.ts +88 -15
  77. package/dist/errors/messages.js +177 -27
  78. package/dist/index.js +19322 -13961
  79. package/dist/ipc/client.d.ts +22 -2
  80. package/dist/ipc/client.js +34 -5
  81. package/dist/ipc/protocol/auditTypes.d.ts +135 -0
  82. package/dist/ipc/protocol/auditTypes.js +6 -0
  83. package/dist/ipc/protocol/commands.d.ts +35 -0
  84. package/dist/ipc/protocol/commands.js +2 -0
  85. package/dist/ipc/protocol/domTypes.d.ts +16 -0
  86. package/dist/ipc/protocol/inspectTypes.d.ts +73 -8
  87. package/dist/ipc/session/types.d.ts +2 -0
  88. package/dist/runtime/css/search.d.ts +39 -0
  89. package/dist/runtime/css/search.js +122 -0
  90. package/dist/runtime/dom/actionEffects.d.ts +9 -2
  91. package/dist/runtime/dom/actionEffects.js +30 -14
  92. package/dist/runtime/dom/audit.d.ts +19 -0
  93. package/dist/runtime/dom/audit.js +37 -0
  94. package/dist/runtime/dom/auditModel.d.ts +45 -0
  95. package/dist/runtime/dom/auditModel.js +220 -0
  96. package/dist/runtime/dom/auditScripts.d.ts +113 -0
  97. package/dist/runtime/dom/auditScripts.js +148 -0
  98. package/dist/runtime/dom/elementGeometry.d.ts +16 -3
  99. package/dist/runtime/dom/elementGeometry.js +49 -10
  100. package/dist/runtime/dom/elementInfo.d.ts +74 -17
  101. package/dist/runtime/dom/elementInfo.js +187 -34
  102. package/dist/runtime/dom/evalHelpers.d.ts +12 -2
  103. package/dist/runtime/dom/evalHelpers.js +67 -7
  104. package/dist/runtime/dom/formDiscovery.d.ts +6 -2
  105. package/dist/runtime/dom/formDiscovery.js +20 -3
  106. package/dist/runtime/dom/formFillHelpers/fill.js +8 -12
  107. package/dist/runtime/dom/formFillHelpers/pressKey.js +2 -2
  108. package/dist/runtime/dom/formFillHelpers/shared.d.ts +16 -10
  109. package/dist/runtime/dom/formFillHelpers/shared.js +19 -52
  110. package/dist/runtime/dom/formSubmitHelpers.js +4 -3
  111. package/dist/runtime/dom/frameLayout.js +1 -0
  112. package/dist/runtime/dom/inspect.d.ts +7 -0
  113. package/dist/runtime/dom/inspect.js +92 -28
  114. package/dist/runtime/dom/inspectAllStyles.d.ts +16 -4
  115. package/dist/runtime/dom/inspectAllStyles.js +90 -7
  116. package/dist/runtime/dom/inspectCascade.d.ts +19 -2
  117. package/dist/runtime/dom/inspectCascade.js +214 -44
  118. package/dist/runtime/dom/inspectCascadeModel.d.ts +8 -0
  119. package/dist/runtime/dom/inspectCascadeModel.js +108 -34
  120. package/dist/runtime/dom/inspectHints.d.ts +26 -3
  121. package/dist/runtime/dom/inspectHints.js +125 -9
  122. package/dist/runtime/dom/inspectModel.d.ts +5 -1
  123. package/dist/runtime/dom/inspectModel.js +37 -10
  124. package/dist/runtime/dom/inspectPaintModel.d.ts +50 -22
  125. package/dist/runtime/dom/inspectPaintModel.js +182 -68
  126. package/dist/runtime/dom/inspectRules.d.ts +19 -0
  127. package/dist/runtime/dom/inspectRules.js +21 -5
  128. package/dist/runtime/dom/inspectScripts.d.ts +112 -12
  129. package/dist/runtime/dom/inspectScripts.js +357 -32
  130. package/dist/runtime/dom/inspectTree.js +10 -2
  131. package/dist/runtime/dom/inspectWhyModel.d.ts +2 -1
  132. package/dist/runtime/dom/inspectWhyModel.js +52 -10
  133. package/dist/runtime/dom/layout.js +40 -16
  134. package/dist/runtime/dom/reactEventHelpers.d.ts +21 -4
  135. package/dist/runtime/dom/reactEventHelpers.js +90 -36
  136. package/dist/runtime/dom/targetNode.d.ts +18 -5
  137. package/dist/runtime/dom/targetNode.js +268 -8
  138. package/dist/runtime/dom/wait.js +2 -1
  139. package/dist/runtime/page/bdgWorld.d.ts +57 -0
  140. package/dist/runtime/page/bdgWorld.js +180 -0
  141. package/dist/runtime/page/emulation.d.ts +13 -4
  142. package/dist/runtime/page/emulation.js +69 -4
  143. package/dist/runtime/page/replacedBuiltins.d.ts +28 -0
  144. package/dist/runtime/page/replacedBuiltins.js +136 -0
  145. package/dist/runtime/page/userAgent.d.ts +17 -0
  146. package/dist/runtime/page/userAgent.js +57 -0
  147. package/dist/session/QueryCacheManager.d.ts +4 -1
  148. package/dist/session/QueryCacheManager.js +5 -2
  149. package/dist/session/chrome.d.ts +4 -1
  150. package/dist/session/chrome.js +7 -1
  151. package/dist/session/cleanup/staleSession.d.ts +21 -4
  152. package/dist/session/cleanup/staleSession.js +79 -9
  153. package/dist/session/cleanup/userCommands.d.ts +4 -1
  154. package/dist/session/cleanup/userCommands.js +10 -5
  155. package/dist/session/daemonSocket.d.ts +10 -0
  156. package/dist/session/daemonSocket.js +22 -0
  157. package/dist/session/lastSession.d.ts +6 -3
  158. package/dist/session/lastSession.js +11 -5
  159. package/dist/session/paths.d.ts +3 -1
  160. package/dist/session/paths.js +5 -5
  161. package/dist/session/portClaims.js +4 -3
  162. package/dist/session/sessionList.d.ts +13 -5
  163. package/dist/session/sessionList.js +31 -7
  164. package/dist/telemetry/a11y.js +2 -2
  165. package/dist/telemetry/console.d.ts +2 -1
  166. package/dist/telemetry/console.js +30 -21
  167. package/dist/telemetry/pageCrash.d.ts +26 -0
  168. package/dist/telemetry/pageCrash.js +53 -0
  169. package/dist/types.d.ts +20 -0
  170. package/dist/ui/formatters/audit.d.ts +19 -0
  171. package/dist/ui/formatters/audit.js +115 -0
  172. package/dist/ui/formatters/cdp.d.ts +138 -0
  173. package/dist/ui/formatters/cdp.js +131 -0
  174. package/dist/ui/formatters/console/chronological.js +3 -1
  175. package/dist/ui/formatters/console/follow.d.ts +2 -1
  176. package/dist/ui/formatters/console/follow.js +2 -2
  177. package/dist/ui/formatters/console/json.d.ts +2 -2
  178. package/dist/ui/formatters/console/json.js +11 -5
  179. package/dist/ui/formatters/console/shared.d.ts +30 -0
  180. package/dist/ui/formatters/console/shared.js +16 -0
  181. package/dist/ui/formatters/console/summarize.d.ts +9 -2
  182. package/dist/ui/formatters/console/summarize.js +40 -9
  183. package/dist/ui/formatters/console.d.ts +2 -1
  184. package/dist/ui/formatters/console.js +7 -5
  185. package/dist/ui/formatters/details.js +3 -1
  186. package/dist/ui/formatters/dom.d.ts +2 -2
  187. package/dist/ui/formatters/dom.js +10 -8
  188. package/dist/ui/formatters/helpFormatters.js +1 -1
  189. package/dist/ui/formatters/inspect.js +50 -17
  190. package/dist/ui/formatters/installSkill.d.ts +9 -1
  191. package/dist/ui/formatters/installSkill.js +32 -6
  192. package/dist/ui/formatters/layout.js +2 -1
  193. package/dist/ui/formatters/networkList.d.ts +1 -1
  194. package/dist/ui/formatters/networkList.js +1 -2
  195. package/dist/ui/formatters/preview.d.ts +2 -0
  196. package/dist/ui/formatters/preview.js +17 -7
  197. package/dist/ui/formatters/sessions.d.ts +2 -2
  198. package/dist/ui/formatters/sessions.js +9 -2
  199. package/dist/ui/formatters/status.js +1 -1
  200. package/dist/ui/logging/logger.d.ts +1 -1
  201. package/dist/ui/messages/commands.d.ts +168 -11
  202. package/dist/ui/messages/commands.js +245 -18
  203. package/dist/ui/messages/consoleMessages.d.ts +24 -0
  204. package/dist/ui/messages/consoleMessages.js +32 -0
  205. package/dist/ui/messages/preview.d.ts +12 -0
  206. package/dist/ui/messages/preview.js +18 -2
  207. package/dist/ui/messages/session.d.ts +13 -2
  208. package/dist/ui/messages/session.js +22 -3
  209. package/dist/utils/cssValues.js +36 -4
  210. package/dist/utils/decisionTrees.js +0 -5
  211. package/dist/utils/directories.d.ts +34 -0
  212. package/dist/utils/directories.js +88 -0
  213. package/dist/utils/display.d.ts +16 -0
  214. package/dist/utils/display.js +42 -0
  215. package/dist/utils/exitCodes.d.ts +1 -0
  216. package/dist/utils/exitCodes.js +6 -0
  217. package/dist/utils/process.d.ts +12 -0
  218. package/dist/utils/process.js +25 -0
  219. package/dist/utils/suggestions.d.ts +4 -2
  220. package/dist/utils/suggestions.js +7 -5
  221. package/dist/utils/taskMappings.js +1 -1
  222. package/package.json +3 -2
@@ -4,9 +4,10 @@
4
4
  * property, sides set by one declaration merged into the shorthand) and
5
5
  * `--why` (every declaration of one property).
6
6
  */
7
- import { normalizeProperty } from './inspectAllStyles.js';
8
- import { physicalName, ownProperties, resolveCascade, shorthandLonghands, } from './inspectCascade.js';
7
+ import { collapsedValue, normalizeProperty } from './inspectAllStyles.js';
8
+ import { physicalName, ownProperties, ruleField, resolveCascade, shorthandLonghands, } from './inspectCascade.js';
9
9
  import { formControlFontHints, inactiveHints, undefinedVariableHints, } from './inspectHints.js';
10
+ import { substituteVariables } from './inspectVariables.js';
10
11
  import { buildWhy } from './inspectWhyModel.js';
11
12
  const SIDES = ['top', 'right', 'bottom', 'left'];
12
13
  const sides = (pattern) => SIDES.map((side) => pattern.replace('{side}', side));
@@ -33,6 +34,11 @@ const RULE_GROUPS = [
33
34
  },
34
35
  { longhands: ['background-color'] },
35
36
  { longhands: ['background-image'] },
37
+ { longhands: ['background-size'] },
38
+ {
39
+ shorthand: 'background-position',
40
+ longhands: ['background-position-x', 'background-position-y'],
41
+ },
36
42
  { longhands: ['color'] },
37
43
  { longhands: ['font-family'] },
38
44
  { longhands: ['font-size'] },
@@ -63,8 +69,10 @@ const RULE_GROUPS = [
63
69
  * @returns `hints` (empty when checked and nothing found), `rules` and `why`
64
70
  */
65
71
  export function buildCascadeFields(input) {
66
- const ruleGroups = input.rules ? (input.props?.map(propertyGroup) ?? RULE_GROUPS) : [];
67
- const whyGroup = input.why ? propertyGroup(input.why) : undefined;
72
+ const ruleGroups = input.rules
73
+ ? (input.props?.map((name) => propertyGroup(name)) ?? RULE_GROUPS)
74
+ : [];
75
+ const whyGroup = input.why ? propertyGroup(input.why, input.whyLonghands) : undefined;
68
76
  const wanted = [
69
77
  ...new Set([
70
78
  ...ownProperties(input.matched),
@@ -73,40 +81,59 @@ export function buildCascadeFields(input) {
73
81
  ]),
74
82
  ];
75
83
  const cascade = resolveCascade(input.matched, wanted);
76
- const hints = input.hints === false ? undefined : buildHints(cascade, input);
84
+ const found = collectHints(cascade, input);
85
+ const hints = input.hints === false ? undefined : found.map((hint) => toInspectHint(hint, input));
77
86
  const rules = buildRules(ruleGroups, cascade, input);
78
87
  return {
79
88
  ...(hints && { hints }),
80
89
  ...(rules.length > 0 && { rules }),
81
- ...(whyGroup && { why: buildWhy(whyGroup, cascade, input) }),
90
+ ...(whyGroup && { why: withInactive(buildWhy(whyGroup, cascade, input, found), input) }),
82
91
  };
83
92
  }
84
93
  /**
85
94
  * The longhands a property name stands for.
86
95
  *
87
96
  * @param name - Shorthand, longhand, logical or custom property
97
+ * @param expanded - Its longhands as the browser expands it, for a
98
+ * shorthand bdg does not list (`transition`, `grid-template`)
88
99
  * @returns Group (a shorthand with its longhands, or one physical longhand)
89
100
  */
90
- function propertyGroup(name) {
91
- const longhands = shorthandLonghands(name);
101
+ function propertyGroup(name, expanded) {
102
+ const longhands = shorthandLonghands(name) ?? (expanded && [...new Set(expanded.map(physicalName))]);
92
103
  return longhands
93
104
  ? { shorthand: name, longhands: [...longhands] }
94
105
  : { longhands: [physicalName(name)] };
95
106
  }
107
+ /**
108
+ * The `--why` answer with the rules that would set the property under a
109
+ * condition that does not apply now (on its first entry).
110
+ *
111
+ * @param whys - `--why` entries
112
+ * @param input - Page-side findings
113
+ * @returns Entries
114
+ */
115
+ function withInactive(whys, input) {
116
+ const [first, ...rest] = whys;
117
+ if (!first || !input.whyInactive?.length)
118
+ return whys;
119
+ return [{ ...first, inactive: input.whyInactive }, ...rest];
120
+ }
96
121
  /**
97
122
  * Hints: declarations with no effect, and var() of unset custom properties;
98
123
  * none without the computed style to check them against.
99
124
  *
100
125
  * @param cascade - Resolved properties
101
- * @param input - Styles and the label function
102
- * @returns Hints
126
+ * @param input - Styles
127
+ * @returns Hints with their declarations
103
128
  */
104
- function buildHints(cascade, input) {
129
+ function collectHints(cascade, input) {
105
130
  if (input.style['display'] === undefined)
106
131
  return [];
132
+ const declaredDisplay = cascade.get('display')?.winner?.value;
107
133
  const ctx = {
108
134
  style: input.style,
109
135
  parentStyle: input.parentStyle,
136
+ declaredDisplay: declaredDisplay === input.style['display'] ? undefined : declaredDisplay,
110
137
  replaced: input.replaced,
111
138
  formControl: input.formControl === true,
112
139
  };
@@ -114,14 +141,26 @@ function buildHints(cascade, input) {
114
141
  ...inactiveHints(cascade, ctx),
115
142
  ...undefinedVariableHints(cascade, input.style),
116
143
  ...formControlFontHints(cascade, ctx),
117
- ].map((hint) => ({
144
+ ];
145
+ }
146
+ /**
147
+ * A hint as the result shows it, with where its declaration is.
148
+ *
149
+ * @param hint - Hint
150
+ * @param input - The label function
151
+ * @returns Result hint
152
+ */
153
+ function toInspectHint(hint, input) {
154
+ return {
118
155
  kind: hint.kind,
119
156
  property: hint.property,
120
157
  value: hint.value,
121
158
  reason: hint.reason,
122
159
  fix: hint.fix,
160
+ ...(hint.only && { only: hint.only }),
161
+ ...(hint.variables && { variables: hint.variables }),
123
162
  source: input.label(hint.declaration),
124
- }));
163
+ };
125
164
  }
126
165
  /**
127
166
  * `--rules`: one row per property set by an author declaration; one row for
@@ -139,16 +178,22 @@ function buildRules(groups, cascade, input) {
139
178
  const rows = groups.flatMap((group) => {
140
179
  const resolutions = group.longhands.map((longhand) => cascade.get(longhand));
141
180
  const winners = resolutions.map((resolution) => resolution?.winner);
142
- const grouped = group.shorthand !== undefined ? groupedRow(group.shorthand, winners) : undefined;
143
- if (grouped)
144
- return [toRule(grouped.property, grouped, resolutions[0], input)];
181
+ const grouped = group.shorthand !== undefined ? groupedRow(winners) : undefined;
182
+ if (grouped) {
183
+ const longhands = shorthandLonghands(grouped.property) ?? group.longhands;
184
+ const partly = longhands.some((longhand) => {
185
+ const owner = cascade.get(longhand)?.winner;
186
+ return (owner !== undefined && !(sameSource(owner, grouped) && owner.via === grouped.property));
187
+ });
188
+ return [toRule(grouped.property, grouped, resolutions[0], input, longhands, partly)];
189
+ }
145
190
  return group.longhands.flatMap((longhand, i) => {
146
191
  const winner = winners[i];
147
- if (!winner || winner.source.origin === 'user-agent')
192
+ if (!winner || (winner.source.origin === 'user-agent' && !input.props))
148
193
  return [];
149
194
  const unexpanded = winner.via !== undefined && winner.value === winner.written;
150
195
  return unexpanded && winner.via
151
- ? [toRule(winner.via, winner, resolutions[i], input)]
196
+ ? [toRule(winner.via, winner, resolutions[i], input, [longhand])]
152
197
  : [toRule(longhand, winner, resolutions[i], input)];
153
198
  });
154
199
  });
@@ -163,28 +208,20 @@ function buildRules(groups, cascade, input) {
163
208
  }
164
209
  /**
165
210
  * One row for a group of longhands that a single author declaration sets:
166
- * the shorthand as written, or (set by a wider shorthand, like `border`
167
- * for `border-width`) the values it gives the sides; a wider shorthand
168
- * Chrome could not expand (`var()`) as written.
211
+ * that declaration as written (`padding: 4px 8px`; a wider shorthand like
212
+ * `border: none` once, not once per `border-width`/`-style`/`-color` group).
169
213
  *
170
- * @param shorthand - The group's shorthand
171
214
  * @param winners - Winning declaration of each longhand
172
215
  * @returns The row's declaration, or undefined when the sides differ in source
173
216
  */
174
- function groupedRow(shorthand, winners) {
217
+ function groupedRow(winners) {
175
218
  const [first] = winners;
176
219
  if (!first?.via || first.source.origin === 'user-agent')
177
220
  return undefined;
178
221
  const same = winners.every((w) => w && w.via === first.via && sameSource(w, first));
179
222
  if (!same)
180
223
  return undefined;
181
- const written = first.written ?? first.value;
182
- if (first.via === shorthand || first.value === written) {
183
- return { ...first, property: first.via, value: written };
184
- }
185
- const values = winners.map((w) => w?.value ?? '');
186
- const value = values.every((v) => v === first.value) ? first.value : values.join(' ');
187
- return { ...first, property: shorthand, value };
224
+ return { ...first, property: first.via, value: first.written ?? first.value };
188
225
  }
189
226
  /**
190
227
  * Whether two declarations come from the same place.
@@ -200,6 +237,26 @@ function sameSource(a, b) {
200
237
  a.source.line === b.source.line &&
201
238
  a.source.column === b.source.column);
202
239
  }
240
+ /**
241
+ * The computed value of a row shown as a shorthand or logical property,
242
+ * from its longhands: as `--all` writes it (`padding 4 8`), else the
243
+ * longhands' values with runs of equal ones written once (`border` →
244
+ * `2px solid red`).
245
+ *
246
+ * @param property - Shorthand or logical property
247
+ * @param longhands - Its longhands
248
+ * @param style - Computed longhands
249
+ * @returns Value, or undefined when no longhand has one
250
+ */
251
+ function shorthandValue(property, longhands, style) {
252
+ const collapsed = collapsedValue(property, style);
253
+ if (collapsed !== undefined)
254
+ return collapsed;
255
+ const values = longhands.map((longhand) => style[longhand]);
256
+ if (values.some((value) => value === undefined))
257
+ return undefined;
258
+ return values.filter((value, i) => i === 0 || value !== values[i - 1]).join(' ');
259
+ }
203
260
  /**
204
261
  * A `--rules` row.
205
262
  *
@@ -207,21 +264,38 @@ function sameSource(a, b) {
207
264
  * @param winner - Winning declaration
208
265
  * @param resolution - Its resolution (for what it overrides)
209
266
  * @param input - Computed style and the label function
267
+ * @param longhands - The longhands the row stands for (a shorthand's value comes from them)
268
+ * @param partly - Other declarations win some of its longhands: the value shown is
269
+ * the declaration's own, `var()` substituted, not the computed one
210
270
  * @returns Rule
211
271
  */
212
- function toRule(property, winner, resolution, input) {
213
- const computed = input.style[property];
272
+ function toRule(property, winner, resolution, input, longhands = [property], partly = false) {
273
+ const substituted = substituteVariables(winner.value, input.style);
274
+ const computed = partly
275
+ ? substituted.includes('var(')
276
+ ? undefined
277
+ : substituted
278
+ : (input.style[property] ?? shorthandValue(property, longhands, input.style));
214
279
  const overrides = [
215
280
  ...new Set((resolution?.overridden ?? [])
216
281
  .filter((d) => d.source.origin !== 'user-agent')
217
- .map((d) => d.source.selector ?? d.source.kind)),
282
+ .map((d) => d.source.selector !== undefined && d.source.selector === winner.source.selector
283
+ ? input.label(d)
284
+ : (d.source.selector ?? d.source.kind))),
218
285
  ];
219
286
  return {
220
287
  property,
221
288
  value: winner.value,
222
289
  ...(winner.value.includes('var(') &&
223
- computed !== undefined && { computed: normalizeProperty(property, computed) }),
290
+ computed !== undefined && {
291
+ computed: `${normalizeProperty(property, computed)}${partly ? ' (partly overridden)' : ''}`,
292
+ }),
293
+ ...(property === 'display' &&
294
+ computed !== undefined &&
295
+ computed !== winner.value &&
296
+ /^(inline|table-|ruby)/.test(winner.value) && { computed: `${computed} (blockified)` }),
224
297
  source: input.label(winner),
298
+ ...ruleField(winner),
225
299
  ...(overrides.length > 0 && { overrides }),
226
300
  ...(winner.ancestor !== undefined && { inherited: winner.ancestor }),
227
301
  ...(winner.important && { important: true }),
@@ -4,8 +4,10 @@
4
4
  * checked against authored declarations only (never the browser's own
5
5
  * styles), and `var()` references to custom properties that are not set.
6
6
  */
7
+ import type { InspectHint } from '../../ipc/protocol/inspectTypes.js';
7
8
  import type { Declaration, Resolution } from './inspectCascade.js';
8
9
  import type { StyleMap } from './inspectLayoutModel.js';
10
+ import type { VariableSetter } from './inspectScripts.js';
9
11
  /** A declaration that has no effect */
10
12
  export interface CssHint {
11
13
  kind: 'inactive' | 'unset-variable' | 'not-inherited';
@@ -17,6 +19,10 @@ export interface CssHint {
17
19
  reason: string;
18
20
  /** What would make it work */
19
21
  fix: string;
22
+ /** The longhands of a shorthand that have no effect, when the others do (`margin-top`, `margin-bottom`) */
23
+ only?: string[];
24
+ /** The custom properties that are not set (`unset-variable`) */
25
+ variables?: string[];
20
26
  /** The declaration */
21
27
  declaration: Declaration;
22
28
  }
@@ -24,15 +30,19 @@ export interface CssHint {
24
30
  interface HintContext {
25
31
  style: StyleMap;
26
32
  parentStyle: StyleMap | undefined;
33
+ /** `display` as the winning declaration wrote it, when it differs from the computed one (blockified) */
34
+ declaredDisplay?: string | undefined;
27
35
  /** The element is replaced (img, input, video…) */
28
36
  replaced: boolean;
29
37
  /** The element is a form control (input, textarea, select, button) */
30
38
  formControl?: boolean;
31
39
  }
32
40
  /**
33
- * Declarations of the element that have no effect: every longhand a
34
- * declaration wins is inactive (`margin: 0 4px` on an inline element still
35
- * moves it sideways, `gap` on a multi-column block still spaces the columns).
41
+ * Declarations of the element that have no effect: the longhands a
42
+ * declaration wins that are inactive, unless they only restate a default.
43
+ * When only some of a `margin` shorthand's longhands are inactive
44
+ * (`margin: 8px 12px` on an inline element: the sides still move it), the
45
+ * hint names those; other partly inactive shorthands are not hinted.
36
46
  *
37
47
  * @param cascade - Resolved properties (winning declarations are checked)
38
48
  * @param ctx - Computed styles of the element and its parent
@@ -49,6 +59,19 @@ export declare function inactiveHints(cascade: Map<string, Resolution>, ctx: Hin
49
59
  * @returns Hints
50
60
  */
51
61
  export declare function undefinedVariableHints(cascade: Map<string, Resolution>, style: StyleMap): CssHint[];
62
+ /**
63
+ * Unset-variable hints told where the page does set the variable, when it
64
+ * does: only in a rule that does not match now (`.btn:hover`: expected in
65
+ * the other state), only in `@keyframes` (while the animation runs), or to
66
+ * `inherit`/`initial`/an empty value in a rule that matches (nothing above
67
+ * gives it a value). A set variable is not a typo, so the "did you mean"
68
+ * suggestion goes.
69
+ *
70
+ * @param hints - Hints of the result
71
+ * @param setters - Where each variable is set, by name ({@link VARIABLE_SETTERS_JS})
72
+ * @returns Hints
73
+ */
74
+ export declare function explainUnsetVariables(hints: readonly InspectHint[], setters: Readonly<Record<string, VariableSetter>>): InspectHint[];
52
75
  /**
53
76
  * A form control drawn in the browser's font while its parent uses another:
54
77
  * controls do not inherit the font unless told to (a common oversight).
@@ -4,12 +4,14 @@
4
4
  * checked against authored declarations only (never the browser's own
5
5
  * styles), and `var()` references to custom properties that are not set.
6
6
  */
7
+ import { isDefaultValue } from './inspectAllStyles.js';
7
8
  import { unsetVariables } from './inspectVariables.js';
8
9
  import { findSimilarNames } from '../../utils/suggestions.js';
9
10
  const display = (style) => style?.['display'] ?? 'inline';
10
11
  const isFlex = (value) => /(^|-)flex$/.test(value);
11
12
  const isGrid = (value) => /(^|-)grid$/.test(value);
12
13
  const isFlexOrGrid = (value) => isFlex(value) || isGrid(value);
14
+ const horizontal = (style) => (style['writing-mode'] ?? 'horizontal-tb') === 'horizontal-tb';
13
15
  const isMulticol = (style) => (style['column-count'] ?? 'auto') !== 'auto' || (style['column-width'] ?? 'auto') !== 'auto';
14
16
  /** The checks, in DevTools' wording */
15
17
  const RULES = [
@@ -120,7 +122,7 @@ const RULES = [
120
122
  },
121
123
  {
122
124
  properties: ['margin-top', 'margin-bottom'],
123
- inactive: ({ style, replaced }) => display(style) === 'inline' && !replaced
125
+ inactive: ({ style, replaced }) => display(style) === 'inline' && !replaced && horizontal(style)
124
126
  ? {
125
127
  reason: 'display is inline (vertical margins do not move it)',
126
128
  fix: 'use display: inline-block or block',
@@ -129,10 +131,10 @@ const RULES = [
129
131
  },
130
132
  {
131
133
  properties: ['vertical-align'],
132
- inactive: ({ style }) => /^(inline|inline-block|inline-flex|inline-grid|table-cell)$/.test(display(style))
134
+ inactive: ({ style, parentStyle, declaredDisplay }) => /^(inline|table-cell|ruby)/.test(display(style))
133
135
  ? undefined
134
136
  : {
135
- reason: `display is ${display(style)}`,
137
+ reason: `display is ${display(style)}${blockified(declaredDisplay, style, parentStyle)}`,
136
138
  fix: 'vertical-align works on inline and table-cell boxes; use flex alignment instead',
137
139
  },
138
140
  },
@@ -165,9 +167,58 @@ const RULES = [
165
167
  },
166
168
  ];
167
169
  /**
168
- * Declarations of the element that have no effect: every longhand a
169
- * declaration wins is inactive (`margin: 0 4px` on an inline element still
170
- * moves it sideways, `gap` on a multi-column block still spaces the columns).
170
+ * Why a declared inline-level display computes to a block-level one: the
171
+ * element is a flex or grid item, floated or positioned (CSS blockification).
172
+ *
173
+ * @param declared - `display` as written, when it differs from the computed one
174
+ * @param style - Computed styles
175
+ * @param parentStyle - Computed styles of the layout parent
176
+ * @returns ` (inline-flex blockified: a flex item)`, or empty
177
+ */
178
+ function blockified(declared, style, parentStyle) {
179
+ if (!declared || declared === display(style) || !/^(inline|table-|ruby)/.test(declared))
180
+ return '';
181
+ const parent = display(parentStyle);
182
+ const cause = isFlex(parent)
183
+ ? 'a flex item'
184
+ : isGrid(parent)
185
+ ? 'a grid item'
186
+ : (style['float'] ?? 'none') !== 'none'
187
+ ? 'floated'
188
+ : /^(absolute|fixed)$/.test(style['position'] ?? '')
189
+ ? 'positioned'
190
+ : 'by its context';
191
+ return ` (${declared} blockified: ${cause})`;
192
+ }
193
+ /**
194
+ * Shorthands whose inactive longhands are worth a hint while the others
195
+ * work: vertical margins of an inline element are a common mistake, while
196
+ * `gap` in a multi-column block or `grid-template-areas: none` are not
197
+ */
198
+ const PARTIAL_SHORTHANDS = new Set(['margin', 'margin-block']);
199
+ /** Values that change nothing wherever they are written */
200
+ const NO_OP_KEYWORDS = new Set(['initial', 'unset', 'revert', 'revert-layer']);
201
+ /**
202
+ * Whether a declared longhand value is its default, so writing it has no
203
+ * effect in any case (`vertical-align: baseline`, `margin-top: 0` from a
204
+ * reset): such declarations are not worth a hint.
205
+ *
206
+ * @param declaration - Declaration of a longhand
207
+ * @returns True for a default value
208
+ */
209
+ function isNoOp(declaration) {
210
+ const value = declaration.value.trim().toLowerCase();
211
+ if (NO_OP_KEYWORDS.has(value))
212
+ return true;
213
+ const zero = /^[+-]?0*\.?0+([a-z]+|%)?$/.test(value) ? '0px' : value;
214
+ return isDefaultValue(declaration.property, value) || isDefaultValue(declaration.property, zero);
215
+ }
216
+ /**
217
+ * Declarations of the element that have no effect: the longhands a
218
+ * declaration wins that are inactive, unless they only restate a default.
219
+ * When only some of a `margin` shorthand's longhands are inactive
220
+ * (`margin: 8px 12px` on an inline element: the sides still move it), the
221
+ * hint names those; other partly inactive shorthands are not hinted.
171
222
  *
172
223
  * @param cascade - Resolved properties (winning declarations are checked)
173
224
  * @param ctx - Computed styles of the element and its parent
@@ -176,16 +227,21 @@ const RULES = [
176
227
  export function inactiveHints(cascade, ctx) {
177
228
  return [...ownWinnersByDeclaration(cascade).values()].flatMap((winners) => {
178
229
  const checks = winners.map((declaration) => RULES.find((rule) => rule.properties.includes(declaration.property))?.inactive(ctx));
230
+ const inactive = winners.filter((declaration, i) => checks[i] && !isNoOp(declaration));
179
231
  const [first] = winners;
180
- const [inactive] = checks;
181
- if (!first || !inactive || checks.some((check) => !check))
232
+ const reason = checks.find(Boolean);
233
+ const partial = checks.some((check) => !check);
234
+ if (!first || !reason || inactive.length === 0)
235
+ return [];
236
+ if (partial && !PARTIAL_SHORTHANDS.has(first.via ?? ''))
182
237
  return [];
183
238
  return [
184
239
  {
185
240
  kind: 'inactive',
186
241
  property: first.via ?? first.property,
187
242
  value: first.written ?? first.value,
188
- ...inactive,
243
+ ...reason,
244
+ ...(partial && { only: inactive.map((declaration) => declaration.property) }),
189
245
  declaration: first,
190
246
  },
191
247
  ];
@@ -243,11 +299,71 @@ export function undefinedVariableHints(cascade, style) {
243
299
  value,
244
300
  reason: `${missing.join(', ')} is not set`,
245
301
  fix: variableFix(missing[0] ?? '', style),
302
+ variables: missing,
246
303
  declaration,
247
304
  });
248
305
  }
249
306
  return hints;
250
307
  }
308
+ /**
309
+ * Unset-variable hints told where the page does set the variable, when it
310
+ * does: only in a rule that does not match now (`.btn:hover`: expected in
311
+ * the other state), only in `@keyframes` (while the animation runs), or to
312
+ * `inherit`/`initial`/an empty value in a rule that matches (nothing above
313
+ * gives it a value). A set variable is not a typo, so the "did you mean"
314
+ * suggestion goes.
315
+ *
316
+ * @param hints - Hints of the result
317
+ * @param setters - Where each variable is set, by name ({@link VARIABLE_SETTERS_JS})
318
+ * @returns Hints
319
+ */
320
+ export function explainUnsetVariables(hints, setters) {
321
+ return hints.map((hint) => {
322
+ const variables = hint.variables ?? [];
323
+ const name = variables.find((variable) => setters[variable]);
324
+ const setter = name ? setters[name] : undefined;
325
+ if (!name || !setter)
326
+ return hint;
327
+ const explained = setterExplanation(name, setter);
328
+ if (variables.every((variable) => setters[variable]))
329
+ return { ...hint, ...explained };
330
+ return { ...hint, reason: `${hint.reason}; ${explained.reason}` };
331
+ });
332
+ }
333
+ /**
334
+ * Why a variable that the page sets is unset here, and the fix.
335
+ *
336
+ * @param name - Variable
337
+ * @param setter - Where it is set
338
+ * @returns Reason and fix
339
+ */
340
+ function setterExplanation(name, setter) {
341
+ const fallback = 'give var() a fallback';
342
+ if (setter.keyframes) {
343
+ return {
344
+ reason: `${name} is set only in @keyframes ${setter.keyframes} (while it runs)`,
345
+ fix: `${fallback} for when the animation is not running`,
346
+ };
347
+ }
348
+ if (setter.condition) {
349
+ return {
350
+ reason: `${name} is set only by ${setter.selector} under ${setter.condition}, which does not apply now`,
351
+ fix: `expected under other conditions; otherwise ${fallback}`,
352
+ };
353
+ }
354
+ if (setter.matches) {
355
+ return {
356
+ reason: `${name} is set to ${setter.value === '' ? 'an empty value' : setter.value} by ${setter.selector}, and nothing above gives it a value`,
357
+ fix: `set ${name} on an ancestor, or ${fallback}`,
358
+ };
359
+ }
360
+ return {
361
+ reason: setter.matches === null
362
+ ? `${name} is set only by ${setter.selector} (whether it applies here is not known)`
363
+ : `${name} is set only by ${setter.selector}, which does not match now`,
364
+ fix: `expected in that state; otherwise ${fallback}`,
365
+ };
366
+ }
251
367
  /**
252
368
  * How to fix a `var()` of an unset custom property, naming a similar one
253
369
  * that is set (a typo or a renamed token).
@@ -16,7 +16,10 @@ export interface InspectSources {
16
16
  raw: RawInspect;
17
17
  style: StyleMap;
18
18
  parentStyle?: StyleMap;
19
+ /** Computed styles of the descendant that draws the text, when it is not the element */
20
+ holderStyle?: StyleMap;
19
21
  pseudo: PseudoSource[];
22
+ /** Platform fonts of the text (of the descendant that draws it, when there is one) */
20
23
  fonts: PlatformFont[];
21
24
  /** Border box size from `DOM.getBoxModel`; absent when the element has no box */
22
25
  size?: {
@@ -49,7 +52,8 @@ export interface InspectRequest {
49
52
  propValues?: InspectResult['props'];
50
53
  }
51
54
  /**
52
- * The element's label: tag, id and the first classes, with a count of the rest.
55
+ * The element's label: tag, id and the first classes ({@link isLabelClass}),
56
+ * with a count of the rest.
53
57
  *
54
58
  * @param raw - Tag, id and classes
55
59
  * @returns e.g. `a.z-1.max-sm:hidden(+11)`, `input#user-name`
@@ -5,26 +5,29 @@
5
5
  * tree. Pure: every input is plain data, so the rules are tested without a
6
6
  * browser.
7
7
  */
8
+ import { isLabelClass } from './elementInfo.js';
8
9
  import { allStyles } from './inspectAllStyles.js';
9
10
  import { buildBox, buildLayout } from './inspectLayoutModel.js';
10
- import { buildEffects, buildFills, buildFx, buildOutline, buildPseudo, buildRadius, buildState, buildStrokes, buildText, effectiveBackground, } from './inspectPaintModel.js';
11
+ import { buildEffects, buildFills, buildSvgPaint, buildFx, buildOutline, buildPseudo, buildRadius, buildState, buildStrokes, buildText, effectiveBackground, } from './inspectPaintModel.js';
11
12
  import { buildTree, rowText } from './inspectTree.js';
12
13
  import { hexColor, relativeLuminance } from '../../utils/color.js';
13
14
  import { normalizeCssValue, round1 } from '../../utils/cssValues.js';
14
15
  /** Classes shown in the label (the rest are counted) */
15
16
  const LABEL_CLASSES = 2;
16
17
  /**
17
- * The element's label: tag, id and the first classes, with a count of the rest.
18
+ * The element's label: tag, id and the first classes ({@link isLabelClass}),
19
+ * with a count of the rest.
18
20
  *
19
21
  * @param raw - Tag, id and classes
20
22
  * @returns e.g. `a.z-1.max-sm:hidden(+11)`, `input#user-name`
21
23
  */
22
24
  export function elementLabel(raw) {
23
- const shown = raw.classes
25
+ const classes = raw.classes.filter(isLabelClass);
26
+ const shown = classes
24
27
  .slice(0, LABEL_CLASSES)
25
28
  .map((name) => `.${name}`)
26
29
  .join('');
27
- const more = raw.classes.length - LABEL_CLASSES;
30
+ const more = classes.length - LABEL_CLASSES;
28
31
  return `${raw.tag}${raw.id ? `#${raw.id}` : ''}${shown}${more > 0 ? `(+${more})` : ''}`;
29
32
  }
30
33
  /**
@@ -48,8 +51,33 @@ export function visibilityOf(layout, rendered) {
48
51
  ...(offscreen && { offscreen }),
49
52
  ...(layout.coveredBy && { coveredBy: layout.coveredBy }),
50
53
  ...(layout.coverTransparent && { coverTransparent: true }),
54
+ ...(layout.masked && { masked: layout.masked }),
51
55
  };
52
56
  }
57
+ /**
58
+ * The header rectangle: border box size (and the box it covers on screen
59
+ * when a transform turns it), and the page position, or the
60
+ * viewport position of an element fixed to the viewport (it or a container
61
+ * is `position: fixed`: its page position changes with the scroll).
62
+ *
63
+ * @param layout - Layout measurements
64
+ * @param size - Border box size
65
+ * @param style - Computed styles (transforms)
66
+ * @returns Rectangle
67
+ */
68
+ function headerRect(layout, size, style) {
69
+ const { width, height } = layout.bounds;
70
+ const transformed = ['transform', 'rotate', 'scale'].some((name) => (style[name] ?? 'none') !== 'none');
71
+ const turned = transformed && (Math.abs(width - size.w) > 1 || Math.abs(height - size.h) > 1);
72
+ const box = {
73
+ w: round1(size.w),
74
+ h: round1(size.h),
75
+ ...(turned && { screen: { w: width, h: height } }),
76
+ };
77
+ return layout.fixed
78
+ ? { x: layout.viewport.x, y: layout.viewport.y, ...box, in: 'viewport' }
79
+ : { x: layout.bounds.x, y: layout.bounds.y, ...box };
80
+ }
53
81
  /** Below this relative luminance a page background counts as dark */
54
82
  const DARK_LUMINANCE = 0.18;
55
83
  /**
@@ -72,10 +100,7 @@ function header(sources, request) {
72
100
  ...(content && { content }),
73
101
  ...(raw.placeholder && { placeholder: rowText(raw.placeholder) }),
74
102
  ...(raw.context && { context: raw.context }),
75
- ...(size &&
76
- layout && {
77
- rect: { x: layout.bounds.x, y: layout.bounds.y, w: round1(size.w), h: round1(size.h) },
78
- }),
103
+ ...(size && layout && { rect: headerRect(layout, size, sources.style) }),
79
104
  visibility: visibilityOf(layout, size !== undefined),
80
105
  ...(sources.colorScheme && { colorScheme: sources.colorScheme }),
81
106
  ...(sources.colorScheme === 'dark' && pageLooksDark(raw) && { theme: 'dark' }),
@@ -104,8 +129,9 @@ function pageLooksDark(raw) {
104
129
  */
105
130
  function groups(sources) {
106
131
  const { style, parentStyle, raw } = sources;
107
- const text = buildText(style, parentStyle, raw, sources.fonts);
132
+ const text = buildText({ style, parentStyle, holderStyle: sources.holderStyle }, raw, sources.fonts);
108
133
  const fills = buildFills(style);
134
+ const paint = buildSvgPaint(style, raw);
109
135
  const strokes = buildStrokes(style);
110
136
  const radius = buildRadius(style);
111
137
  const outline = buildOutline(style);
@@ -120,6 +146,7 @@ function groups(sources) {
120
146
  layout: buildLayout(style, parentStyle, raw),
121
147
  ...(text && { text }),
122
148
  ...(fills.length > 0 && { fills }),
149
+ ...(paint && { paint }),
123
150
  ...(opacity !== 1 && { opacity }),
124
151
  ...(blend && blend !== 'normal' && { blend }),
125
152
  ...(strokes.length > 0 && { strokes }),
@@ -156,7 +183,7 @@ function treeFields(raw, limit) {
156
183
  * @returns Properties and values
157
184
  */
158
185
  function allFields(sources) {
159
- const all = allStyles(sources.style, sources.raw.svg === true);
186
+ const all = allStyles(sources.style, sources.raw.svg === true, sources.raw.formControl);
160
187
  for (const pseudo of sources.pseudo) {
161
188
  const content = pseudo.style['content'];
162
189
  if (content && content !== 'none' && content !== 'normal') {