lighthouse 9.5.0-dev.20221019 → 9.5.0-dev.20221021

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 (81) hide show
  1. package/core/audits/accessibility/axe-audit.js +1 -1
  2. package/core/audits/autocomplete.js +3 -3
  3. package/core/audits/bootup-time.js +5 -5
  4. package/core/audits/byte-efficiency/duplicated-javascript.js +1 -1
  5. package/core/audits/byte-efficiency/legacy-javascript.js +1 -1
  6. package/core/audits/byte-efficiency/total-byte-weight.js +2 -2
  7. package/core/audits/byte-efficiency/uses-long-cache-ttl.js +3 -3
  8. package/core/audits/byte-efficiency/uses-responsive-images-snapshot.js +4 -4
  9. package/core/audits/csp-xss.js +3 -3
  10. package/core/audits/deprecations.js +2 -2
  11. package/core/audits/dobetterweb/dom-size.js +3 -3
  12. package/core/audits/dobetterweb/geolocation-on-start.js +1 -1
  13. package/core/audits/dobetterweb/inspector-issues.js +1 -1
  14. package/core/audits/dobetterweb/js-libraries.js +2 -2
  15. package/core/audits/dobetterweb/no-document-write.js +1 -1
  16. package/core/audits/dobetterweb/notification-on-start.js +1 -1
  17. package/core/audits/dobetterweb/password-inputs-can-be-pasted-into.js +1 -1
  18. package/core/audits/dobetterweb/uses-http2.js +2 -2
  19. package/core/audits/dobetterweb/uses-passive-event-listeners.js +1 -1
  20. package/core/audits/errors-in-console.js +4 -2
  21. package/core/audits/font-display.js +2 -2
  22. package/core/audits/image-aspect-ratio.js +4 -4
  23. package/core/audits/image-size-responsive.js +5 -5
  24. package/core/audits/installable-manifest.js +1 -1
  25. package/core/audits/is-on-https.js +2 -2
  26. package/core/audits/largest-contentful-paint-element.js +1 -1
  27. package/core/audits/layout-shift-elements.js +3 -3
  28. package/core/audits/lcp-lazy-loaded.js +1 -1
  29. package/core/audits/long-tasks.js +3 -3
  30. package/core/audits/main-thread-tasks.js +2 -2
  31. package/core/audits/mainthread-work-breakdown.js +4 -2
  32. package/core/audits/network-requests.js +11 -11
  33. package/core/audits/network-rtt.js +2 -2
  34. package/core/audits/network-server-latency.js +3 -3
  35. package/core/audits/no-unload-listeners.js +1 -1
  36. package/core/audits/non-composited-animations.js +2 -2
  37. package/core/audits/performance-budget.js +5 -5
  38. package/core/audits/preload-fonts.js +1 -1
  39. package/core/audits/resource-summary.js +3 -3
  40. package/core/audits/seo/crawlable-anchors.js +2 -2
  41. package/core/audits/seo/font-size.js +4 -4
  42. package/core/audits/seo/hreflang.js +3 -3
  43. package/core/audits/seo/is-crawlable.js +1 -1
  44. package/core/audits/seo/link-text.js +2 -2
  45. package/core/audits/seo/plugins.js +1 -1
  46. package/core/audits/seo/robots-txt.js +3 -3
  47. package/core/audits/seo/tap-targets.js +3 -3
  48. package/core/audits/themed-omnibox.js +6 -14
  49. package/core/audits/third-party-facades.js +3 -3
  50. package/core/audits/third-party-summary.js +3 -3
  51. package/core/audits/timing-budget.js +3 -3
  52. package/core/audits/unsized-images.js +2 -2
  53. package/core/audits/user-timings.js +6 -6
  54. package/core/audits/valid-source-maps.js +3 -3
  55. package/core/audits/work-during-interaction.js +6 -6
  56. package/core/lib/bfcache-strings.js +655 -0
  57. package/core/lib/deprecations-strings.js +3 -2
  58. package/core/lib/manifest-parser.js +0 -16
  59. package/core/user-flow.js +93 -23
  60. package/core/util.cjs +27 -0
  61. package/dist/report/bundle.esm.js +38 -73
  62. package/dist/report/flow.js +2 -2
  63. package/dist/report/standalone.js +2 -2
  64. package/flow-report/src/app.tsx +1 -1
  65. package/flow-report/src/header.tsx +1 -1
  66. package/flow-report/src/util.ts +1 -1
  67. package/flow-report/src/wrappers/report.tsx +1 -1
  68. package/flow-report/types/flow-report.d.ts +8 -1
  69. package/package.json +2 -3
  70. package/report/generator/report-generator.js +1 -1
  71. package/report/renderer/details-renderer.js +12 -73
  72. package/report/renderer/util.js +27 -0
  73. package/report/test/renderer/details-renderer-test.js +38 -38
  74. package/report/test/renderer/util-test.js +25 -0
  75. package/shared/localization/locales/en-US.json +352 -1
  76. package/shared/localization/locales/en-XL.json +352 -1
  77. package/shared/types/shared.d.ts +2 -2
  78. package/types/global-lh.d.ts +1 -1
  79. package/types/lhr/audit-details.d.ts +4 -34
  80. package/types/lhr/{flow.d.ts → flow-result.d.ts} +3 -7
  81. package/types/user-flow.d.ts +7 -2
@@ -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,9 +10,47 @@ 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 {mergeConfigFragment, deepClone} from './config/config-helpers.js';
15
+ import * as i18n from './lib/i18n/i18n.js';
13
16
 
14
17
  /** @typedef {WeakMap<LH.UserFlow.GatherStep, LH.Gatherer.FRGatherResult['runnerOptions']>} GatherStepRunnerOptions */
15
18
 
19
+ const UIStrings = {
20
+ /**
21
+ * @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.
22
+ * @example {https://example.com} url
23
+ */
24
+ defaultFlowName: 'User flow ({url})',
25
+ /**
26
+ * @description Default name for a user flow step that analyzes a page navigation.
27
+ * @example {https://example.com} url
28
+ */
29
+ defaultNavigationName: 'Navigation report ({url})',
30
+ /**
31
+ * @description Default name for a user flow step that analyzes user interactions over a period of time.
32
+ * @example {https://example.com} url
33
+ */
34
+ defaultTimespanName: 'Timespan report ({url})',
35
+ /**
36
+ * @description Default name for a user flow step that analyzes the page state at a point in time.
37
+ * @example {https://example.com} url
38
+ */
39
+ defaultSnapshotName: 'Snapshot report ({url})',
40
+ };
41
+
42
+ const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
43
+
44
+ /**
45
+ * @param {string} message
46
+ * @param {Record<string, string | number>} values
47
+ * @param {LH.Locale} locale
48
+ */
49
+ function translate(message, values, locale) {
50
+ const icuMessage = str_(message, values);
51
+ return getFormatted(icuMessage, locale);
52
+ }
53
+
16
54
  class UserFlow {
17
55
  /**
18
56
  * @param {LH.Puppeteer.Page} page
@@ -30,30 +68,39 @@ class UserFlow {
30
68
  }
31
69
 
32
70
  /**
33
- * @param {LH.UserFlow.StepFlags} [flags]
71
+ * @param {LH.UserFlow.StepFlags|undefined} flags
72
+ * @return {LH.UserFlow.StepFlags|undefined}
73
+ */
74
+ _getNextFlags(flags) {
75
+ const clonedFlowFlags = this._options?.flags && deepClone(this._options?.flags);
76
+ if (!flags) return clonedFlowFlags;
77
+ return mergeConfigFragment(clonedFlowFlags || {}, flags, true);
78
+ }
79
+
80
+ /**
81
+ * @param {LH.UserFlow.StepFlags|undefined} flags
34
82
  * @return {LH.UserFlow.StepFlags}
35
83
  */
36
84
  _getNextNavigationFlags(flags) {
37
- const newStepFlags = {...flags};
85
+ const nextFlags = this._getNextFlags(flags) || {};
38
86
 
39
- if (newStepFlags.skipAboutBlank === undefined) {
40
- newStepFlags.skipAboutBlank = true;
87
+ if (nextFlags.skipAboutBlank === undefined) {
88
+ nextFlags.skipAboutBlank = true;
41
89
  }
42
90
 
43
91
  // On repeat navigations, we want to disable storage reset by default (i.e. it's not a cold load).
44
92
  const isSubsequentNavigation = this._gatherSteps
45
93
  .some(step => step.artifacts.GatherContext.gatherMode === 'navigation');
46
94
  if (isSubsequentNavigation) {
47
- if (newStepFlags.disableStorageReset === undefined) {
48
- newStepFlags.disableStorageReset = true;
95
+ if (nextFlags.disableStorageReset === undefined) {
96
+ nextFlags.disableStorageReset = true;
49
97
  }
50
98
  }
51
99
 
52
- return newStepFlags;
100
+ return nextFlags;
53
101
  }
54
102
 
55
103
  /**
56
- *
57
104
  * @param {LH.Gatherer.FRGatherResult} gatherResult
58
105
  * @param {LH.UserFlow.StepFlags} [flags]
59
106
  */
@@ -74,13 +121,13 @@ class UserFlow {
74
121
  if (this.currentTimespan) throw new Error('Timespan already in progress');
75
122
  if (this.currentNavigation) throw new Error('Navigation already in progress');
76
123
 
77
- const newStepFlags = this._getNextNavigationFlags(flags);
124
+ const nextFlags = this._getNextNavigationFlags(flags);
78
125
  const gatherResult = await navigationGather(this._page, requestor, {
79
126
  config: this._options?.config,
80
- flags: newStepFlags,
127
+ flags: nextFlags,
81
128
  });
82
129
 
83
- this._addGatherStep(gatherResult, newStepFlags);
130
+ this._addGatherStep(gatherResult, nextFlags);
84
131
  }
85
132
 
86
133
  /**
@@ -142,11 +189,13 @@ class UserFlow {
142
189
  if (this.currentTimespan) throw new Error('Timespan already in progress');
143
190
  if (this.currentNavigation) throw new Error('Navigation already in progress');
144
191
 
192
+ const nextFlags = this._getNextFlags(flags);
193
+
145
194
  const timespan = await startTimespanGather(this._page, {
146
195
  config: this._options?.config,
147
- flags: flags,
196
+ flags: nextFlags,
148
197
  });
149
- this.currentTimespan = {timespan, flags};
198
+ this.currentTimespan = {timespan, flags: nextFlags};
150
199
  }
151
200
 
152
201
  async endTimespan() {
@@ -167,12 +216,14 @@ class UserFlow {
167
216
  if (this.currentTimespan) throw new Error('Timespan already in progress');
168
217
  if (this.currentNavigation) throw new Error('Navigation already in progress');
169
218
 
219
+ const nextFlags = this._getNextFlags(flags);
220
+
170
221
  const gatherResult = await snapshotGather(this._page, {
171
222
  config: this._options?.config,
172
- flags: flags,
223
+ flags: nextFlags,
173
224
  });
174
225
 
175
- this._addGatherStep(gatherResult, flags);
226
+ this._addGatherStep(gatherResult, nextFlags);
176
227
  }
177
228
 
178
229
  /**
@@ -215,23 +266,41 @@ function shortenUrl(longUrl) {
215
266
  }
216
267
 
217
268
  /**
269
+ * @param {LH.UserFlow.StepFlags|undefined} flags
218
270
  * @param {LH.Artifacts} artifacts
219
271
  * @return {string}
220
272
  */
221
- function getDefaultStepName(artifacts) {
273
+ function getStepName(flags, artifacts) {
274
+ if (flags?.name) return flags.name;
275
+
276
+ const {locale} = artifacts.settings;
222
277
  const shortUrl = shortenUrl(artifacts.URL.finalDisplayedUrl);
223
278
  switch (artifacts.GatherContext.gatherMode) {
224
279
  case 'navigation':
225
- return `Navigation report (${shortUrl})`;
280
+ return translate(UIStrings.defaultNavigationName, {url: shortUrl}, locale);
226
281
  case 'timespan':
227
- return `Timespan report (${shortUrl})`;
282
+ return translate(UIStrings.defaultTimespanName, {url: shortUrl}, locale);
228
283
  case 'snapshot':
229
- return `Snapshot report (${shortUrl})`;
284
+ return translate(UIStrings.defaultSnapshotName, {url: shortUrl}, locale);
230
285
  default:
231
286
  throw new Error('Unsupported gather mode');
232
287
  }
233
288
  }
234
289
 
290
+ /**
291
+ * @param {string|undefined} name
292
+ * @param {LH.UserFlow.GatherStep[]} gatherSteps
293
+ * @return {string}
294
+ */
295
+ function getFlowName(name, gatherSteps) {
296
+ if (name) return name;
297
+
298
+ const firstArtifacts = gatherSteps[0].artifacts;
299
+ const {locale} = firstArtifacts.settings;
300
+ const url = new URL(firstArtifacts.URL.finalDisplayedUrl).hostname;
301
+ return translate(UIStrings.defaultFlowName, {url}, locale);
302
+ }
303
+
235
304
  /**
236
305
  * @param {Array<LH.UserFlow.GatherStep>} gatherSteps
237
306
  * @param {{name?: string, config?: LH.Config.Json, gatherStepRunnerOptions?: GatherStepRunnerOptions}} options
@@ -245,7 +314,7 @@ async function auditGatherSteps(gatherSteps, options) {
245
314
  const steps = [];
246
315
  for (const gatherStep of gatherSteps) {
247
316
  const {artifacts, flags} = gatherStep;
248
- const name = flags?.name || getDefaultStepName(artifacts);
317
+ const name = getStepName(flags, artifacts);
249
318
 
250
319
  let runnerOptions = options.gatherStepRunnerOptions?.get(gatherStep);
251
320
 
@@ -266,13 +335,14 @@ async function auditGatherSteps(gatherSteps, options) {
266
335
  steps.push({lhr: result.lhr, name});
267
336
  }
268
337
 
269
- const url = new URL(gatherSteps[0].artifacts.URL.finalDisplayedUrl);
270
- const flowName = options.name || `User flow (${url.hostname})`;
271
- return {steps, name: flowName};
338
+ return {steps, name: getFlowName(options.name, gatherSteps)};
272
339
  }
273
340
 
274
341
 
275
342
  export {
276
343
  UserFlow,
277
344
  auditGatherSteps,
345
+ getStepName,
346
+ getFlowName,
347
+ UIStrings,
278
348
  };
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');