lighthouse 9.5.0-dev.20221018 → 9.5.0-dev.20221020

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 (83) hide show
  1. package/core/api.js +14 -10
  2. package/core/audits/accessibility/axe-audit.js +1 -1
  3. package/core/audits/accessibility/bypass.js +1 -0
  4. package/core/audits/accessibility/th-has-data-cells.js +1 -0
  5. package/core/audits/accessibility/video-caption.js +1 -0
  6. package/core/audits/autocomplete.js +3 -3
  7. package/core/audits/bootup-time.js +5 -5
  8. package/core/audits/byte-efficiency/duplicated-javascript.js +1 -1
  9. package/core/audits/byte-efficiency/legacy-javascript.js +1 -1
  10. package/core/audits/byte-efficiency/total-byte-weight.js +2 -2
  11. package/core/audits/byte-efficiency/uses-long-cache-ttl.js +3 -3
  12. package/core/audits/byte-efficiency/uses-responsive-images-snapshot.js +4 -4
  13. package/core/audits/csp-xss.js +3 -3
  14. package/core/audits/deprecations.js +2 -2
  15. package/core/audits/dobetterweb/dom-size.js +3 -3
  16. package/core/audits/dobetterweb/geolocation-on-start.js +1 -1
  17. package/core/audits/dobetterweb/inspector-issues.js +1 -1
  18. package/core/audits/dobetterweb/js-libraries.js +2 -2
  19. package/core/audits/dobetterweb/no-document-write.js +1 -1
  20. package/core/audits/dobetterweb/notification-on-start.js +1 -1
  21. package/core/audits/dobetterweb/password-inputs-can-be-pasted-into.js +1 -1
  22. package/core/audits/dobetterweb/uses-http2.js +2 -2
  23. package/core/audits/dobetterweb/uses-passive-event-listeners.js +1 -1
  24. package/core/audits/errors-in-console.js +4 -2
  25. package/core/audits/font-display.js +2 -2
  26. package/core/audits/image-aspect-ratio.js +4 -4
  27. package/core/audits/image-size-responsive.js +5 -5
  28. package/core/audits/installable-manifest.js +1 -1
  29. package/core/audits/is-on-https.js +2 -2
  30. package/core/audits/largest-contentful-paint-element.js +1 -1
  31. package/core/audits/layout-shift-elements.js +3 -3
  32. package/core/audits/lcp-lazy-loaded.js +1 -1
  33. package/core/audits/long-tasks.js +3 -3
  34. package/core/audits/main-thread-tasks.js +2 -2
  35. package/core/audits/mainthread-work-breakdown.js +4 -2
  36. package/core/audits/network-requests.js +11 -11
  37. package/core/audits/network-rtt.js +2 -2
  38. package/core/audits/network-server-latency.js +3 -3
  39. package/core/audits/no-unload-listeners.js +1 -1
  40. package/core/audits/non-composited-animations.js +2 -2
  41. package/core/audits/performance-budget.js +5 -5
  42. package/core/audits/preload-fonts.js +1 -1
  43. package/core/audits/resource-summary.js +3 -3
  44. package/core/audits/seo/crawlable-anchors.js +2 -2
  45. package/core/audits/seo/font-size.js +4 -4
  46. package/core/audits/seo/hreflang.js +3 -3
  47. package/core/audits/seo/is-crawlable.js +1 -1
  48. package/core/audits/seo/link-text.js +2 -2
  49. package/core/audits/seo/plugins.js +1 -1
  50. package/core/audits/seo/robots-txt.js +3 -3
  51. package/core/audits/seo/tap-targets.js +3 -3
  52. package/core/audits/themed-omnibox.js +6 -14
  53. package/core/audits/third-party-facades.js +3 -3
  54. package/core/audits/third-party-summary.js +3 -3
  55. package/core/audits/timing-budget.js +3 -3
  56. package/core/audits/unsized-images.js +2 -2
  57. package/core/audits/user-timings.js +6 -6
  58. package/core/audits/valid-source-maps.js +3 -3
  59. package/core/audits/work-during-interaction.js +6 -6
  60. package/core/gather/navigation-runner.js +3 -3
  61. package/core/gather/snapshot-runner.js +4 -3
  62. package/core/gather/timespan-runner.js +4 -3
  63. package/core/index.js +1 -1
  64. package/core/legacy/config/config.js +0 -26
  65. package/core/lib/bfcache-strings.js +655 -0
  66. package/core/lib/deprecations-strings.js +3 -2
  67. package/core/lib/manifest-parser.js +0 -16
  68. package/core/user-flow.js +135 -77
  69. package/core/util.cjs +27 -0
  70. package/dist/report/bundle.esm.js +38 -73
  71. package/dist/report/flow.js +2 -2
  72. package/dist/report/standalone.js +2 -2
  73. package/package.json +2 -3
  74. package/report/renderer/details-renderer.js +12 -73
  75. package/report/renderer/util.js +27 -0
  76. package/report/test/generator/report-generator-test.js +1 -1
  77. package/report/test/renderer/category-renderer-test.js +1 -1
  78. package/report/test/renderer/details-renderer-test.js +38 -38
  79. package/report/test/renderer/util-test.js +25 -0
  80. package/shared/localization/locales/en-US.json +352 -1
  81. package/shared/localization/locales/en-XL.json +352 -1
  82. package/types/lhr/audit-details.d.ts +4 -34
  83. package/types/user-flow.d.ts +10 -3
@@ -4,8 +4,6 @@
4
4
  * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
5
5
  */
6
6
 
7
- import cssParsers from 'cssstyle/lib/parsers.js';
8
-
9
7
  const ALLOWED_DISPLAY_VALUES = [
10
8
  'fullscreen',
11
9
  'standalone',
@@ -29,14 +27,6 @@ const ALLOWED_ORIENTATION_VALUES = [
29
27
  'landscape-secondary',
30
28
  ];
31
29
 
32
- /**
33
- * @param {string} color
34
- * @return {boolean}
35
- */
36
- function isValidColor(color) {
37
- return cssParsers.valueType(color) === cssParsers.TYPES.COLOR;
38
- }
39
-
40
30
  /**
41
31
  * @param {*} raw
42
32
  * @param {boolean=} trim
@@ -72,12 +62,6 @@ function parseColor(raw) {
72
62
  return color;
73
63
  }
74
64
 
75
- // Use color parser to check CSS3 Color parsing.
76
- if (!isValidColor(color.raw)) {
77
- color.value = undefined;
78
- color.warning = 'ERROR: color parsing failed.';
79
- }
80
-
81
65
  return color;
82
66
  }
83
67
 
package/core/user-flow.js CHANGED
@@ -10,22 +10,56 @@ import {startTimespanGather} from './gather/timespan-runner.js';
10
10
  import {navigationGather} from './gather/navigation-runner.js';
11
11
  import {Runner} from './runner.js';
12
12
  import {initializeConfig} from './config/config.js';
13
+ import {getFormatted} from '../shared/localization/format.js';
14
+ import * as i18n from './lib/i18n/i18n.js';
13
15
 
14
- /** @typedef {Parameters<snapshotGather>[0]} FrOptions */
15
- /** @typedef {Omit<FrOptions, 'page'> & {name?: string}} UserFlowOptions */
16
- /** @typedef {Omit<FrOptions, 'page'> & {stepName?: string}} StepOptions */
17
16
  /** @typedef {WeakMap<LH.UserFlow.GatherStep, LH.Gatherer.FRGatherResult['runnerOptions']>} GatherStepRunnerOptions */
18
17
 
18
+ const UIStrings = {
19
+ /**
20
+ * @description Default name for a user flow on the given url. "User flow" refers to the series of page navigations and user interactions being tested on the page.
21
+ * @example {https://example.com} url
22
+ */
23
+ defaultFlowName: 'User flow ({url})',
24
+ /**
25
+ * @description Default name for a user flow step that analyzes a page navigation.
26
+ * @example {https://example.com} url
27
+ */
28
+ defaultNavigationName: 'Navigation report ({url})',
29
+ /**
30
+ * @description Default name for a user flow step that analyzes user interactions over a period of time.
31
+ * @example {https://example.com} url
32
+ */
33
+ defaultTimespanName: 'Timespan report ({url})',
34
+ /**
35
+ * @description Default name for a user flow step that analyzes the page state at a point in time.
36
+ * @example {https://example.com} url
37
+ */
38
+ defaultSnapshotName: 'Snapshot report ({url})',
39
+ };
40
+
41
+ const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
42
+
43
+ /**
44
+ * @param {string} message
45
+ * @param {Record<string, string | number>} values
46
+ * @param {LH.Locale} locale
47
+ */
48
+ function translate(message, values, locale) {
49
+ const icuMessage = str_(message, values);
50
+ return getFormatted(icuMessage, locale);
51
+ }
52
+
19
53
  class UserFlow {
20
54
  /**
21
- * @param {FrOptions['page']} page
22
- * @param {UserFlowOptions=} options
55
+ * @param {LH.Puppeteer.Page} page
56
+ * @param {LH.UserFlow.Options} [options]
23
57
  */
24
58
  constructor(page, options) {
25
- /** @type {FrOptions} */
26
- this.options = {page, ...options};
27
- /** @type {string|undefined} */
28
- this.name = options?.name;
59
+ /** @type {LH.Puppeteer.Page} */
60
+ this._page = page;
61
+ /** @type {LH.UserFlow.Options|undefined} */
62
+ this._options = options;
29
63
  /** @type {LH.UserFlow.GatherStep[]} */
30
64
  this._gatherSteps = [];
31
65
  /** @type {GatherStepRunnerOptions} */
@@ -33,67 +67,37 @@ class UserFlow {
33
67
  }
34
68
 
35
69
  /**
36
- * @param {string} longUrl
37
- * @returns {string}
38
- */
39
- _shortenUrl(longUrl) {
40
- const url = new URL(longUrl);
41
- return `${url.hostname}${url.pathname}`;
42
- }
43
-
44
- /**
45
- * @param {LH.Artifacts} artifacts
46
- * @return {string}
47
- */
48
- _getDefaultStepName(artifacts) {
49
- const shortUrl = this._shortenUrl(artifacts.URL.finalDisplayedUrl);
50
- switch (artifacts.GatherContext.gatherMode) {
51
- case 'navigation':
52
- return `Navigation report (${shortUrl})`;
53
- case 'timespan':
54
- return `Timespan report (${shortUrl})`;
55
- case 'snapshot':
56
- return `Snapshot report (${shortUrl})`;
57
- }
58
- }
59
-
60
- /**
61
- * @param {StepOptions=} stepOptions
70
+ * @param {LH.UserFlow.StepFlags} [flags]
71
+ * @return {LH.UserFlow.StepFlags}
62
72
  */
63
- _getNextNavigationOptions(stepOptions) {
64
- const options = {...this.options, ...stepOptions};
65
- const flags = {...options.flags};
73
+ _getNextNavigationFlags(flags) {
74
+ const newStepFlags = {...flags};
66
75
 
67
- if (flags.skipAboutBlank === undefined) {
68
- flags.skipAboutBlank = true;
76
+ if (newStepFlags.skipAboutBlank === undefined) {
77
+ newStepFlags.skipAboutBlank = true;
69
78
  }
70
79
 
71
80
  // On repeat navigations, we want to disable storage reset by default (i.e. it's not a cold load).
72
81
  const isSubsequentNavigation = this._gatherSteps
73
82
  .some(step => step.artifacts.GatherContext.gatherMode === 'navigation');
74
83
  if (isSubsequentNavigation) {
75
- if (flags.disableStorageReset === undefined) {
76
- flags.disableStorageReset = true;
84
+ if (newStepFlags.disableStorageReset === undefined) {
85
+ newStepFlags.disableStorageReset = true;
77
86
  }
78
87
  }
79
88
 
80
- options.flags = flags;
81
-
82
- return options;
89
+ return newStepFlags;
83
90
  }
84
91
 
85
92
  /**
86
93
  *
87
94
  * @param {LH.Gatherer.FRGatherResult} gatherResult
88
- * @param {StepOptions} options
95
+ * @param {LH.UserFlow.StepFlags} [flags]
89
96
  */
90
- _addGatherStep(gatherResult, options) {
91
- const providedName = options?.stepName;
97
+ _addGatherStep(gatherResult, flags) {
92
98
  const gatherStep = {
93
99
  artifacts: gatherResult.artifacts,
94
- name: providedName || this._getDefaultStepName(gatherResult.artifacts),
95
- config: options.config,
96
- flags: options.flags,
100
+ flags,
97
101
  };
98
102
  this._gatherSteps.push(gatherStep);
99
103
  this._gatherStepRunnerOptions.set(gatherStep, gatherResult.runnerOptions);
@@ -101,23 +105,26 @@ class UserFlow {
101
105
 
102
106
  /**
103
107
  * @param {LH.NavigationRequestor} requestor
104
- * @param {StepOptions=} stepOptions
108
+ * @param {LH.UserFlow.StepFlags} [flags]
105
109
  */
106
- async navigate(requestor, stepOptions) {
110
+ async navigate(requestor, flags) {
107
111
  if (this.currentTimespan) throw new Error('Timespan already in progress');
108
112
  if (this.currentNavigation) throw new Error('Navigation already in progress');
109
113
 
110
- const options = this._getNextNavigationOptions(stepOptions);
111
- const gatherResult = await navigationGather(requestor, options);
114
+ const newStepFlags = this._getNextNavigationFlags(flags);
115
+ const gatherResult = await navigationGather(this._page, requestor, {
116
+ config: this._options?.config,
117
+ flags: newStepFlags,
118
+ });
112
119
 
113
- this._addGatherStep(gatherResult, options);
120
+ this._addGatherStep(gatherResult, newStepFlags);
114
121
  }
115
122
 
116
123
  /**
117
124
  * This is an alternative to `navigate()` that can be used to analyze a navigation triggered by user interaction.
118
125
  * For more on user triggered navigations, see https://github.com/GoogleChrome/lighthouse/blob/main/docs/user-flows.md#triggering-a-navigation-via-user-interactions.
119
126
  *
120
- * @param {StepOptions=} stepOptions
127
+ * @param {LH.UserFlow.StepFlags} [stepOptions]
121
128
  */
122
129
  async startNavigation(stepOptions) {
123
130
  /** @type {(value: () => void) => void} */
@@ -166,39 +173,43 @@ class UserFlow {
166
173
  }
167
174
 
168
175
  /**
169
- * @param {StepOptions=} stepOptions
176
+ * @param {LH.UserFlow.StepFlags} [flags]
170
177
  */
171
- async startTimespan(stepOptions) {
178
+ async startTimespan(flags) {
172
179
  if (this.currentTimespan) throw new Error('Timespan already in progress');
173
180
  if (this.currentNavigation) throw new Error('Navigation already in progress');
174
181
 
175
- const options = {...this.options, ...stepOptions};
176
- const timespan = await startTimespanGather(options);
177
- this.currentTimespan = {timespan, options};
182
+ const timespan = await startTimespanGather(this._page, {
183
+ config: this._options?.config,
184
+ flags: flags,
185
+ });
186
+ this.currentTimespan = {timespan, flags};
178
187
  }
179
188
 
180
189
  async endTimespan() {
181
190
  if (!this.currentTimespan) throw new Error('No timespan in progress');
182
191
  if (this.currentNavigation) throw new Error('Navigation already in progress');
183
192
 
184
- const {timespan, options} = this.currentTimespan;
193
+ const {timespan, flags} = this.currentTimespan;
185
194
  const gatherResult = await timespan.endTimespanGather();
186
195
  this.currentTimespan = undefined;
187
196
 
188
- this._addGatherStep(gatherResult, options);
197
+ this._addGatherStep(gatherResult, flags);
189
198
  }
190
199
 
191
200
  /**
192
- * @param {StepOptions=} stepOptions
201
+ * @param {LH.UserFlow.StepFlags} [flags]
193
202
  */
194
- async snapshot(stepOptions) {
203
+ async snapshot(flags) {
195
204
  if (this.currentTimespan) throw new Error('Timespan already in progress');
196
205
  if (this.currentNavigation) throw new Error('Navigation already in progress');
197
206
 
198
- const options = {...this.options, ...stepOptions};
199
- const gatherResult = await snapshotGather(options);
207
+ const gatherResult = await snapshotGather(this._page, {
208
+ config: this._options?.config,
209
+ flags: flags,
210
+ });
200
211
 
201
- this._addGatherStep(gatherResult, options);
212
+ this._addGatherStep(gatherResult, flags);
202
213
  }
203
214
 
204
215
  /**
@@ -206,8 +217,8 @@ class UserFlow {
206
217
  */
207
218
  async createFlowResult() {
208
219
  return auditGatherSteps(this._gatherSteps, {
209
- name: this.name,
210
- config: this.options.config,
220
+ name: this._options?.name,
221
+ config: this._options?.config,
211
222
  gatherStepRunnerOptions: this._gatherStepRunnerOptions,
212
223
  });
213
224
  }
@@ -226,11 +237,56 @@ class UserFlow {
226
237
  createArtifactsJson() {
227
238
  return {
228
239
  gatherSteps: this._gatherSteps,
229
- name: this.name,
240
+ name: this._options?.name,
230
241
  };
231
242
  }
232
243
  }
233
244
 
245
+ /**
246
+ * @param {string} longUrl
247
+ * @returns {string}
248
+ */
249
+ function shortenUrl(longUrl) {
250
+ const url = new URL(longUrl);
251
+ return `${url.hostname}${url.pathname}`;
252
+ }
253
+
254
+ /**
255
+ * @param {LH.UserFlow.StepFlags|undefined} flags
256
+ * @param {LH.Artifacts} artifacts
257
+ * @return {string}
258
+ */
259
+ function getStepName(flags, artifacts) {
260
+ if (flags?.name) return flags.name;
261
+
262
+ const {locale} = artifacts.settings;
263
+ const shortUrl = shortenUrl(artifacts.URL.finalDisplayedUrl);
264
+ switch (artifacts.GatherContext.gatherMode) {
265
+ case 'navigation':
266
+ return translate(UIStrings.defaultNavigationName, {url: shortUrl}, locale);
267
+ case 'timespan':
268
+ return translate(UIStrings.defaultTimespanName, {url: shortUrl}, locale);
269
+ case 'snapshot':
270
+ return translate(UIStrings.defaultSnapshotName, {url: shortUrl}, locale);
271
+ default:
272
+ throw new Error('Unsupported gather mode');
273
+ }
274
+ }
275
+
276
+ /**
277
+ * @param {string|undefined} name
278
+ * @param {LH.UserFlow.GatherStep[]} gatherSteps
279
+ * @return {string}
280
+ */
281
+ function getFlowName(name, gatherSteps) {
282
+ if (name) return name;
283
+
284
+ const firstArtifacts = gatherSteps[0].artifacts;
285
+ const {locale} = firstArtifacts.settings;
286
+ const url = new URL(firstArtifacts.URL.finalDisplayedUrl).hostname;
287
+ return translate(UIStrings.defaultFlowName, {url}, locale);
288
+ }
289
+
234
290
  /**
235
291
  * @param {Array<LH.UserFlow.GatherStep>} gatherSteps
236
292
  * @param {{name?: string, config?: LH.Config.Json, gatherStepRunnerOptions?: GatherStepRunnerOptions}} options
@@ -243,14 +299,15 @@ async function auditGatherSteps(gatherSteps, options) {
243
299
  /** @type {LH.FlowResult['steps']} */
244
300
  const steps = [];
245
301
  for (const gatherStep of gatherSteps) {
246
- const {artifacts, name, flags} = gatherStep;
302
+ const {artifacts, flags} = gatherStep;
303
+ const name = getStepName(flags, artifacts);
247
304
 
248
305
  let runnerOptions = options.gatherStepRunnerOptions?.get(gatherStep);
249
306
 
250
307
  // If the gather step is not active, we must recreate the runner options.
251
308
  if (!runnerOptions) {
252
309
  // Step specific configs take precedence over a config for the entire flow.
253
- const configJson = gatherStep.config || options.config;
310
+ const configJson = options.config;
254
311
  const {gatherMode} = artifacts.GatherContext;
255
312
  const {config} = await initializeConfig(gatherMode, configJson, flags);
256
313
  runnerOptions = {
@@ -264,13 +321,14 @@ async function auditGatherSteps(gatherSteps, options) {
264
321
  steps.push({lhr: result.lhr, name});
265
322
  }
266
323
 
267
- const url = new URL(gatherSteps[0].artifacts.URL.finalDisplayedUrl);
268
- const flowName = options.name || `User flow (${url.hostname})`;
269
- return {steps, name: flowName};
324
+ return {steps, name: getFlowName(options.name, gatherSteps)};
270
325
  }
271
326
 
272
327
 
273
328
  export {
274
329
  UserFlow,
275
330
  auditGatherSteps,
331
+ getStepName,
332
+ getFlowName,
333
+ UIStrings,
276
334
  };
package/core/util.cjs CHANGED
@@ -127,6 +127,33 @@ class Util {
127
127
  }
128
128
  }
129
129
  }
130
+
131
+ // Circa 10.0, table items were refactored.
132
+ if (audit.details.type === 'table') {
133
+ for (const heading of audit.details.headings) {
134
+ /** @type {{itemType: LH.Audit.Details.ItemValueType|undefined, text: string|undefined}} */
135
+ // @ts-expect-error
136
+ const {itemType, text} = heading;
137
+ if (itemType !== undefined) {
138
+ heading.valueType = itemType;
139
+ // @ts-expect-error
140
+ delete heading.itemType;
141
+ }
142
+ if (text !== undefined) {
143
+ heading.label = text;
144
+ // @ts-expect-error
145
+ delete heading.text;
146
+ }
147
+
148
+ // @ts-expect-error
149
+ const subItemsItemType = heading.subItemsHeading?.itemType;
150
+ if (heading.subItemsHeading && subItemsItemType !== undefined) {
151
+ heading.subItemsHeading.valueType = subItemsItemType;
152
+ // @ts-expect-error
153
+ delete heading.subItemsHeading.itemType;
154
+ }
155
+ }
156
+ }
130
157
  }
131
158
  }
132
159
 
@@ -123,6 +123,33 @@ class Util {
123
123
  }
124
124
  }
125
125
  }
126
+
127
+ // Circa 10.0, table items were refactored.
128
+ if (audit.details.type === 'table') {
129
+ for (const heading of audit.details.headings) {
130
+ /** @type {{itemType: LH.Audit.Details.ItemValueType|undefined, text: string|undefined}} */
131
+ // @ts-expect-error
132
+ const {itemType, text} = heading;
133
+ if (itemType !== undefined) {
134
+ heading.valueType = itemType;
135
+ // @ts-expect-error
136
+ delete heading.itemType;
137
+ }
138
+ if (text !== undefined) {
139
+ heading.label = text;
140
+ // @ts-expect-error
141
+ delete heading.text;
142
+ }
143
+
144
+ // @ts-expect-error
145
+ const subItemsItemType = heading.subItemsHeading?.itemType;
146
+ if (heading.subItemsHeading && subItemsItemType !== undefined) {
147
+ heading.subItemsHeading.valueType = subItemsItemType;
148
+ // @ts-expect-error
149
+ delete heading.subItemsHeading.itemType;
150
+ }
151
+ }
152
+ }
126
153
  }
127
154
  }
128
155
 
@@ -2893,11 +2920,10 @@ class DetailsRenderer {
2893
2920
  case 'list':
2894
2921
  return this._renderList(details);
2895
2922
  case 'table':
2923
+ case 'opportunity':
2896
2924
  return this._renderTable(details);
2897
2925
  case 'criticalrequestchain':
2898
2926
  return CriticalRequestChainRenderer.render(this._dom, details, this);
2899
- case 'opportunity':
2900
- return this._renderTable(details);
2901
2927
 
2902
2928
  // Internal-only details, not for rendering.
2903
2929
  case 'screenshot':
@@ -3055,7 +3081,7 @@ class DetailsRenderer {
3055
3081
  * based on the heading's valueType, unless the value itself has a `type`
3056
3082
  * property to override it.
3057
3083
  * @param {TableItemValue} value
3058
- * @param {LH.Audit.Details.OpportunityColumnHeading} heading
3084
+ * @param {LH.Audit.Details.TableColumnHeading} heading
3059
3085
  * @return {Element|null}
3060
3086
  */
3061
3087
  _renderTableValue(value, heading) {
@@ -3140,73 +3166,14 @@ class DetailsRenderer {
3140
3166
  }
3141
3167
  }
3142
3168
 
3143
- /**
3144
- * Get the headings of a table-like details object, converted into the
3145
- * OpportunityColumnHeading type until we have all details use the same
3146
- * heading format.
3147
- * @param {Table|OpportunityTable} tableLike
3148
- * @return {OpportunityTable['headings']}
3149
- */
3150
- _getCanonicalizedHeadingsFromTable(tableLike) {
3151
- if (tableLike.type === 'opportunity') {
3152
- return tableLike.headings;
3153
- }
3154
-
3155
- return tableLike.headings.map(heading => this._getCanonicalizedHeading(heading));
3156
- }
3157
-
3158
- /**
3159
- * Get the headings of a table-like details object, converted into the
3160
- * OpportunityColumnHeading type until we have all details use the same
3161
- * heading format.
3162
- * @param {Table['headings'][number]} heading
3163
- * @return {OpportunityTable['headings'][number]}
3164
- */
3165
- _getCanonicalizedHeading(heading) {
3166
- let subItemsHeading;
3167
- if (heading.subItemsHeading) {
3168
- subItemsHeading = this._getCanonicalizedsubItemsHeading(heading.subItemsHeading, heading);
3169
- }
3170
-
3171
- return {
3172
- key: heading.key,
3173
- valueType: heading.itemType,
3174
- subItemsHeading,
3175
- label: heading.text,
3176
- displayUnit: heading.displayUnit,
3177
- granularity: heading.granularity,
3178
- };
3179
- }
3180
-
3181
- /**
3182
- * @param {Exclude<LH.Audit.Details.TableColumnHeading['subItemsHeading'], undefined>} subItemsHeading
3183
- * @param {LH.Audit.Details.TableColumnHeading} parentHeading
3184
- * @return {LH.Audit.Details.OpportunityColumnHeading['subItemsHeading']}
3185
- */
3186
- _getCanonicalizedsubItemsHeading(subItemsHeading, parentHeading) {
3187
- // Low-friction way to prevent committing a falsy key (which is never allowed for
3188
- // a subItemsHeading) from passing in CI.
3189
- if (!subItemsHeading.key) {
3190
- // eslint-disable-next-line no-console
3191
- console.warn('key should not be null');
3192
- }
3193
-
3194
- return {
3195
- key: subItemsHeading.key || '',
3196
- valueType: subItemsHeading.itemType || parentHeading.itemType,
3197
- granularity: subItemsHeading.granularity || parentHeading.granularity,
3198
- displayUnit: subItemsHeading.displayUnit || parentHeading.displayUnit,
3199
- };
3200
- }
3201
-
3202
3169
  /**
3203
3170
  * Returns a new heading where the values are defined first by `heading.subItemsHeading`,
3204
3171
  * and secondly by `heading`. If there is no subItemsHeading, returns null, which will
3205
3172
  * be rendered as an empty column.
3206
- * @param {LH.Audit.Details.OpportunityColumnHeading} heading
3207
- * @return {LH.Audit.Details.OpportunityColumnHeading | null}
3173
+ * @param {LH.Audit.Details.TableColumnHeading} heading
3174
+ * @return {LH.Audit.Details.TableColumnHeading | null}
3208
3175
  */
3209
- _getDerivedsubItemsHeading(heading) {
3176
+ _getDerivedSubItemsHeading(heading) {
3210
3177
  if (!heading.subItemsHeading) return null;
3211
3178
  return {
3212
3179
  key: heading.subItemsHeading.key || '',
@@ -3219,7 +3186,7 @@ class DetailsRenderer {
3219
3186
 
3220
3187
  /**
3221
3188
  * @param {TableItem} item
3222
- * @param {(LH.Audit.Details.OpportunityColumnHeading | null)[]} headings
3189
+ * @param {(LH.Audit.Details.TableColumnHeading | null)[]} headings
3223
3190
  */
3224
3191
  _renderTableRow(item, headings) {
3225
3192
  const rowElem = this._dom.createElement('tr');
@@ -3256,7 +3223,7 @@ class DetailsRenderer {
3256
3223
  * Renders one or more rows from a details table item. A single table item can
3257
3224
  * expand into multiple rows, if there is a subItemsHeading.
3258
3225
  * @param {TableItem} item
3259
- * @param {LH.Audit.Details.OpportunityColumnHeading[]} headings
3226
+ * @param {LH.Audit.Details.TableColumnHeading[]} headings
3260
3227
  */
3261
3228
  _renderTableRowsFromItem(item, headings) {
3262
3229
  const fragment = this._dom.createFragment();
@@ -3264,7 +3231,7 @@ class DetailsRenderer {
3264
3231
 
3265
3232
  if (!item.subItems) return fragment;
3266
3233
 
3267
- const subItemsHeadings = headings.map(this._getDerivedsubItemsHeading);
3234
+ const subItemsHeadings = headings.map(this._getDerivedSubItemsHeading);
3268
3235
  if (!subItemsHeadings.some(Boolean)) return fragment;
3269
3236
 
3270
3237
  for (const subItem of item.subItems.items) {
@@ -3277,7 +3244,7 @@ class DetailsRenderer {
3277
3244
  }
3278
3245
 
3279
3246
  /**
3280
- * @param {OpportunityTable|Table} details
3247
+ * @param {{headings: TableColumnHeading[], items: TableItem[]}} details
3281
3248
  * @return {Element}
3282
3249
  */
3283
3250
  _renderTable(details) {
@@ -3287,9 +3254,7 @@ class DetailsRenderer {
3287
3254
  const theadElem = this._dom.createChildOf(tableElem, 'thead');
3288
3255
  const theadTrElem = this._dom.createChildOf(theadElem, 'tr');
3289
3256
 
3290
- const headings = this._getCanonicalizedHeadingsFromTable(details);
3291
-
3292
- for (const heading of headings) {
3257
+ for (const heading of details.headings) {
3293
3258
  const valueType = heading.valueType || 'text';
3294
3259
  const classes = `lh-table-column--${valueType}`;
3295
3260
  const labelEl = this._dom.createElement('div', 'lh-text');
@@ -3300,7 +3265,7 @@ class DetailsRenderer {
3300
3265
  const tbodyElem = this._dom.createChildOf(tableElem, 'tbody');
3301
3266
  let even = true;
3302
3267
  for (const item of details.items) {
3303
- const rowsFragment = this._renderTableRowsFromItem(item, headings);
3268
+ const rowsFragment = this._renderTableRowsFromItem(item, details.headings);
3304
3269
  for (const rowEl of this._dom.findAll('tr', rowsFragment)) {
3305
3270
  // For zebra styling.
3306
3271
  rowEl.classList.add(even ? 'lh-row--even' : 'lh-row--odd');