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.
- package/core/audits/accessibility/axe-audit.js +1 -1
- package/core/audits/autocomplete.js +3 -3
- package/core/audits/bootup-time.js +5 -5
- package/core/audits/byte-efficiency/duplicated-javascript.js +1 -1
- package/core/audits/byte-efficiency/legacy-javascript.js +1 -1
- package/core/audits/byte-efficiency/total-byte-weight.js +2 -2
- package/core/audits/byte-efficiency/uses-long-cache-ttl.js +3 -3
- package/core/audits/byte-efficiency/uses-responsive-images-snapshot.js +4 -4
- package/core/audits/csp-xss.js +3 -3
- package/core/audits/deprecations.js +2 -2
- package/core/audits/dobetterweb/dom-size.js +3 -3
- package/core/audits/dobetterweb/geolocation-on-start.js +1 -1
- package/core/audits/dobetterweb/inspector-issues.js +1 -1
- package/core/audits/dobetterweb/js-libraries.js +2 -2
- package/core/audits/dobetterweb/no-document-write.js +1 -1
- package/core/audits/dobetterweb/notification-on-start.js +1 -1
- package/core/audits/dobetterweb/password-inputs-can-be-pasted-into.js +1 -1
- package/core/audits/dobetterweb/uses-http2.js +2 -2
- package/core/audits/dobetterweb/uses-passive-event-listeners.js +1 -1
- package/core/audits/errors-in-console.js +4 -2
- package/core/audits/font-display.js +2 -2
- package/core/audits/image-aspect-ratio.js +4 -4
- package/core/audits/image-size-responsive.js +5 -5
- package/core/audits/installable-manifest.js +1 -1
- package/core/audits/is-on-https.js +2 -2
- package/core/audits/largest-contentful-paint-element.js +1 -1
- package/core/audits/layout-shift-elements.js +3 -3
- package/core/audits/lcp-lazy-loaded.js +1 -1
- package/core/audits/long-tasks.js +3 -3
- package/core/audits/main-thread-tasks.js +2 -2
- package/core/audits/mainthread-work-breakdown.js +4 -2
- package/core/audits/network-requests.js +11 -11
- package/core/audits/network-rtt.js +2 -2
- package/core/audits/network-server-latency.js +3 -3
- package/core/audits/no-unload-listeners.js +1 -1
- package/core/audits/non-composited-animations.js +2 -2
- package/core/audits/performance-budget.js +5 -5
- package/core/audits/preload-fonts.js +1 -1
- package/core/audits/resource-summary.js +3 -3
- package/core/audits/seo/crawlable-anchors.js +2 -2
- package/core/audits/seo/font-size.js +4 -4
- package/core/audits/seo/hreflang.js +3 -3
- package/core/audits/seo/is-crawlable.js +1 -1
- package/core/audits/seo/link-text.js +2 -2
- package/core/audits/seo/plugins.js +1 -1
- package/core/audits/seo/robots-txt.js +3 -3
- package/core/audits/seo/tap-targets.js +3 -3
- package/core/audits/themed-omnibox.js +6 -14
- package/core/audits/third-party-facades.js +3 -3
- package/core/audits/third-party-summary.js +3 -3
- package/core/audits/timing-budget.js +3 -3
- package/core/audits/unsized-images.js +2 -2
- package/core/audits/user-timings.js +6 -6
- package/core/audits/valid-source-maps.js +3 -3
- package/core/audits/work-during-interaction.js +6 -6
- package/core/lib/bfcache-strings.js +655 -0
- package/core/lib/deprecations-strings.js +3 -2
- package/core/lib/manifest-parser.js +0 -16
- package/core/user-flow.js +93 -23
- package/core/util.cjs +27 -0
- package/dist/report/bundle.esm.js +38 -73
- package/dist/report/flow.js +2 -2
- package/dist/report/standalone.js +2 -2
- package/flow-report/src/app.tsx +1 -1
- package/flow-report/src/header.tsx +1 -1
- package/flow-report/src/util.ts +1 -1
- package/flow-report/src/wrappers/report.tsx +1 -1
- package/flow-report/types/flow-report.d.ts +8 -1
- package/package.json +2 -3
- package/report/generator/report-generator.js +1 -1
- package/report/renderer/details-renderer.js +12 -73
- package/report/renderer/util.js +27 -0
- package/report/test/renderer/details-renderer-test.js +38 -38
- package/report/test/renderer/util-test.js +25 -0
- package/shared/localization/locales/en-US.json +352 -1
- package/shared/localization/locales/en-XL.json +352 -1
- package/shared/types/shared.d.ts +2 -2
- package/types/global-lh.d.ts +1 -1
- package/types/lhr/audit-details.d.ts +4 -34
- package/types/lhr/{flow.d.ts → flow-result.d.ts} +3 -7
- 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}
|
|
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
|
|
85
|
+
const nextFlags = this._getNextFlags(flags) || {};
|
|
38
86
|
|
|
39
|
-
if (
|
|
40
|
-
|
|
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 (
|
|
48
|
-
|
|
95
|
+
if (nextFlags.disableStorageReset === undefined) {
|
|
96
|
+
nextFlags.disableStorageReset = true;
|
|
49
97
|
}
|
|
50
98
|
}
|
|
51
99
|
|
|
52
|
-
return
|
|
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
|
|
124
|
+
const nextFlags = this._getNextNavigationFlags(flags);
|
|
78
125
|
const gatherResult = await navigationGather(this._page, requestor, {
|
|
79
126
|
config: this._options?.config,
|
|
80
|
-
flags:
|
|
127
|
+
flags: nextFlags,
|
|
81
128
|
});
|
|
82
129
|
|
|
83
|
-
this._addGatherStep(gatherResult,
|
|
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:
|
|
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:
|
|
223
|
+
flags: nextFlags,
|
|
173
224
|
});
|
|
174
225
|
|
|
175
|
-
this._addGatherStep(gatherResult,
|
|
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
|
|
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
|
|
280
|
+
return translate(UIStrings.defaultNavigationName, {url: shortUrl}, locale);
|
|
226
281
|
case 'timespan':
|
|
227
|
-
return
|
|
282
|
+
return translate(UIStrings.defaultTimespanName, {url: shortUrl}, locale);
|
|
228
283
|
case 'snapshot':
|
|
229
|
-
return
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
3207
|
-
* @return {LH.Audit.Details.
|
|
3173
|
+
* @param {LH.Audit.Details.TableColumnHeading} heading
|
|
3174
|
+
* @return {LH.Audit.Details.TableColumnHeading | null}
|
|
3208
3175
|
*/
|
|
3209
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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 {
|
|
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
|
|
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');
|