lighthouse 9.5.0-dev.20230123 → 9.5.0-dev.20230124
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/.codecov.yml +0 -5
- package/core/audits/audit.js +1 -1
- package/core/audits/byte-efficiency/legacy-javascript.d.ts +1 -0
- package/core/audits/byte-efficiency/legacy-javascript.js +7 -3
- package/core/audits/dobetterweb/uses-http2.d.ts +5 -2
- package/core/audits/dobetterweb/uses-http2.js +12 -7
- package/core/audits/third-party-facades.d.ts +3 -2
- package/core/audits/third-party-facades.js +12 -9
- package/core/audits/third-party-summary.d.ts +7 -6
- package/core/audits/third-party-summary.js +16 -14
- package/core/audits/valid-source-maps.d.ts +5 -3
- package/core/audits/valid-source-maps.js +11 -9
- package/core/computed/entity-classification.d.ts +29 -0
- package/core/computed/entity-classification.js +102 -0
- package/core/computed/resource-summary.d.ts +4 -2
- package/core/computed/resource-summary.js +8 -5
- package/core/lib/url-utils.js +1 -1
- package/core/runner.d.ts +10 -0
- package/core/runner.js +48 -0
- package/core/util.cjs +1 -1
- package/dist/report/bundle.esm.js +724 -687
- package/dist/report/flow.js +25 -15
- package/dist/report/standalone.js +25 -15
- package/flow-report/src/common.tsx +1 -1
- package/flow-report/src/i18n/i18n.tsx +17 -16
- package/flow-report/src/sidebar/sidebar.tsx +2 -2
- package/flow-report/src/summary/category.tsx +6 -6
- package/flow-report/src/summary/summary.tsx +2 -2
- package/package.json +4 -4
- package/report/renderer/category-renderer.js +19 -17
- package/report/renderer/crc-details-renderer.js +6 -6
- package/report/renderer/details-renderer.js +7 -6
- package/report/renderer/dom.js +1 -1
- package/report/renderer/element-screenshot-renderer.js +3 -3
- package/report/renderer/performance-category-renderer.js +17 -15
- package/report/renderer/pwa-category-renderer.js +5 -4
- package/report/renderer/report-globals.d.ts +21 -0
- package/report/renderer/report-globals.js +49 -0
- package/report/renderer/report-renderer.js +22 -20
- package/report/renderer/report-ui-features.js +9 -8
- package/report/renderer/report-utils.d.ts +116 -0
- package/report/renderer/{util.js → report-utils.js} +81 -429
- package/report/renderer/snippet-renderer.js +3 -2
- package/report/test/renderer/category-renderer-test.js +9 -4
- package/report/test/renderer/crc-details-renderer-test.js +7 -3
- package/report/test/renderer/details-renderer-test.js +7 -3
- package/report/test/renderer/dom-test.js +7 -3
- package/report/test/renderer/element-screenshot-renderer-test.js +8 -4
- package/report/test/renderer/performance-category-renderer-test.js +14 -9
- package/report/test/renderer/pwa-category-renderer-test.js +10 -5
- package/report/test/renderer/report-renderer-test.js +2 -2
- package/report/test/renderer/report-ui-features-test.js +3 -3
- package/report/test/renderer/{util-test.js → report-utils-test.js} +36 -234
- package/report/test/renderer/snippet-renderer-test.js +7 -3
- package/shared/localization/format.js +5 -2
- package/shared/localization/locales/ar-XB.json +50 -50
- package/shared/localization/locales/ar.json +50 -50
- package/shared/localization/locales/bg.json +50 -50
- package/shared/localization/locales/ca.json +50 -50
- package/shared/localization/locales/cs.json +50 -50
- package/shared/localization/locales/da.json +50 -50
- package/shared/localization/locales/de.json +50 -50
- package/shared/localization/locales/el.json +50 -50
- package/shared/localization/locales/en-GB.json +50 -50
- package/shared/localization/locales/en-US.json +54 -54
- package/shared/localization/locales/en-XA.json +50 -50
- package/shared/localization/locales/en-XL.json +54 -54
- package/shared/localization/locales/es-419.json +50 -50
- package/shared/localization/locales/es.json +50 -50
- package/shared/localization/locales/fi.json +50 -50
- package/shared/localization/locales/fil.json +50 -50
- package/shared/localization/locales/fr.json +50 -50
- package/shared/localization/locales/he.json +50 -50
- package/shared/localization/locales/hi.json +50 -50
- package/shared/localization/locales/hr.json +50 -50
- package/shared/localization/locales/hu.json +50 -50
- package/shared/localization/locales/id.json +50 -50
- package/shared/localization/locales/it.json +50 -50
- package/shared/localization/locales/ja.json +50 -50
- package/shared/localization/locales/ko.json +50 -50
- package/shared/localization/locales/lt.json +50 -50
- package/shared/localization/locales/lv.json +50 -50
- package/shared/localization/locales/nl.json +50 -50
- package/shared/localization/locales/no.json +50 -50
- package/shared/localization/locales/pl.json +50 -50
- package/shared/localization/locales/pt-PT.json +50 -50
- package/shared/localization/locales/pt.json +50 -50
- package/shared/localization/locales/ro.json +50 -50
- package/shared/localization/locales/ru.json +50 -50
- package/shared/localization/locales/sk.json +50 -50
- package/shared/localization/locales/sl.json +50 -50
- package/shared/localization/locales/sr-Latn.json +50 -50
- package/shared/localization/locales/sr.json +50 -50
- package/shared/localization/locales/sv.json +50 -50
- package/shared/localization/locales/ta.json +50 -50
- package/shared/localization/locales/te.json +50 -50
- package/shared/localization/locales/th.json +50 -50
- package/shared/localization/locales/tr.json +50 -50
- package/shared/localization/locales/uk.json +50 -50
- package/shared/localization/locales/vi.json +50 -50
- package/shared/localization/locales/zh-HK.json +50 -50
- package/shared/localization/locales/zh-TW.json +50 -50
- package/shared/localization/locales/zh.json +50 -50
- package/shared/test/util-test.js +214 -0
- package/shared/util.d.ts +122 -0
- package/shared/util.js +332 -0
- package/types/artifacts.d.ts +14 -0
- package/types/lhr/lhr.d.ts +33 -0
- package/report/renderer/util.d.ts +0 -342
|
@@ -15,14 +15,13 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
/** @typedef {import('
|
|
18
|
+
/** @typedef {import('../types/lhr/audit-details').default.SnippetValue} SnippetValue */
|
|
19
19
|
|
|
20
20
|
const ELLIPSIS = '\u2026';
|
|
21
21
|
const NBSP = '\xa0';
|
|
22
22
|
const PASS_THRESHOLD = 0.9;
|
|
23
|
-
const SCREENSHOT_PREFIX = 'data:image/jpeg;base64,';
|
|
24
23
|
|
|
25
|
-
const RATINGS = {
|
|
24
|
+
const RATINGS$1 = {
|
|
26
25
|
PASS: {label: 'pass', minScore: PASS_THRESHOLD},
|
|
27
26
|
AVERAGE: {label: 'average', minScore: 0.5},
|
|
28
27
|
FAIL: {label: 'fail'},
|
|
@@ -38,20 +37,8 @@ const listOfTlds = [
|
|
|
38
37
|
];
|
|
39
38
|
|
|
40
39
|
class Util {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
static i18n = null;
|
|
44
|
-
static strings = /** @type {typeof UIStrings} */ ({});
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* @param {Record<string, string>} providedStrings
|
|
48
|
-
*/
|
|
49
|
-
static applyStrings(providedStrings) {
|
|
50
|
-
this.strings = {
|
|
51
|
-
// Set missing renderer strings to default (english) values.
|
|
52
|
-
...UIStrings,
|
|
53
|
-
...providedStrings,
|
|
54
|
-
};
|
|
40
|
+
static get RATINGS() {
|
|
41
|
+
return RATINGS$1;
|
|
55
42
|
}
|
|
56
43
|
|
|
57
44
|
static get PASS_THRESHOLD() {
|
|
@@ -84,182 +71,6 @@ class Util {
|
|
|
84
71
|
return lhr.mainDocumentUrl || lhr.finalUrl;
|
|
85
72
|
}
|
|
86
73
|
|
|
87
|
-
/**
|
|
88
|
-
* Returns a new LHR that's reshaped for slightly better ergonomics within the report rendereer.
|
|
89
|
-
* Also, sets up the localized UI strings used within renderer and makes changes to old LHRs to be
|
|
90
|
-
* compatible with current renderer.
|
|
91
|
-
* The LHR passed in is not mutated.
|
|
92
|
-
* TODO(team): we all agree the LHR shape change is technical debt we should fix
|
|
93
|
-
* @param {LH.Result} result
|
|
94
|
-
* @return {LH.ReportResult}
|
|
95
|
-
*/
|
|
96
|
-
static prepareReportResult(result) {
|
|
97
|
-
// If any mutations happen to the report within the renderers, we want the original object untouched
|
|
98
|
-
const clone = /** @type {LH.ReportResult} */ (JSON.parse(JSON.stringify(result)));
|
|
99
|
-
|
|
100
|
-
// If LHR is older (≤3.0.3), it has no locale setting. Set default.
|
|
101
|
-
if (!clone.configSettings.locale) {
|
|
102
|
-
clone.configSettings.locale = 'en';
|
|
103
|
-
}
|
|
104
|
-
if (!clone.configSettings.formFactor) {
|
|
105
|
-
// @ts-expect-error fallback handling for emulatedFormFactor
|
|
106
|
-
clone.configSettings.formFactor = clone.configSettings.emulatedFormFactor;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
clone.finalDisplayedUrl = this.getFinalDisplayedUrl(clone);
|
|
110
|
-
clone.mainDocumentUrl = this.getMainDocumentUrl(clone);
|
|
111
|
-
|
|
112
|
-
for (const audit of Object.values(clone.audits)) {
|
|
113
|
-
// Turn 'not-applicable' (LHR <4.0) and 'not_applicable' (older proto versions)
|
|
114
|
-
// into 'notApplicable' (LHR ≥4.0).
|
|
115
|
-
// @ts-expect-error tsc rightly flags that these values shouldn't occur.
|
|
116
|
-
// eslint-disable-next-line max-len
|
|
117
|
-
if (audit.scoreDisplayMode === 'not_applicable' || audit.scoreDisplayMode === 'not-applicable') {
|
|
118
|
-
audit.scoreDisplayMode = 'notApplicable';
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (audit.details) {
|
|
122
|
-
// Turn `auditDetails.type` of undefined (LHR <4.2) and 'diagnostic' (LHR <5.0)
|
|
123
|
-
// into 'debugdata' (LHR ≥5.0).
|
|
124
|
-
// @ts-expect-error tsc rightly flags that these values shouldn't occur.
|
|
125
|
-
if (audit.details.type === undefined || audit.details.type === 'diagnostic') {
|
|
126
|
-
// @ts-expect-error details is of type never.
|
|
127
|
-
audit.details.type = 'debugdata';
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Add the jpg data URL prefix to filmstrip screenshots without them (LHR <5.0).
|
|
131
|
-
if (audit.details.type === 'filmstrip') {
|
|
132
|
-
for (const screenshot of audit.details.items) {
|
|
133
|
-
if (!screenshot.data.startsWith(SCREENSHOT_PREFIX)) {
|
|
134
|
-
screenshot.data = SCREENSHOT_PREFIX + screenshot.data;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Circa 10.0, table items were refactored.
|
|
140
|
-
if (audit.details.type === 'table') {
|
|
141
|
-
for (const heading of audit.details.headings) {
|
|
142
|
-
/** @type {{itemType: LH.Audit.Details.ItemValueType|undefined, text: string|undefined}} */
|
|
143
|
-
// @ts-expect-error
|
|
144
|
-
const {itemType, text} = heading;
|
|
145
|
-
if (itemType !== undefined) {
|
|
146
|
-
heading.valueType = itemType;
|
|
147
|
-
// @ts-expect-error
|
|
148
|
-
delete heading.itemType;
|
|
149
|
-
}
|
|
150
|
-
if (text !== undefined) {
|
|
151
|
-
heading.label = text;
|
|
152
|
-
// @ts-expect-error
|
|
153
|
-
delete heading.text;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// @ts-expect-error
|
|
157
|
-
const subItemsItemType = heading.subItemsHeading?.itemType;
|
|
158
|
-
if (heading.subItemsHeading && subItemsItemType !== undefined) {
|
|
159
|
-
heading.subItemsHeading.valueType = subItemsItemType;
|
|
160
|
-
// @ts-expect-error
|
|
161
|
-
delete heading.subItemsHeading.itemType;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// TODO: convert printf-style displayValue.
|
|
167
|
-
// Added: #5099, v3
|
|
168
|
-
// Removed: #6767, v4
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// For convenience, smoosh all AuditResults into their auditRef (which has just weight & group)
|
|
173
|
-
if (typeof clone.categories !== 'object') throw new Error('No categories provided.');
|
|
174
|
-
|
|
175
|
-
/** @type {Map<string, Array<LH.ReportResult.AuditRef>>} */
|
|
176
|
-
const relevantAuditToMetricsMap = new Map();
|
|
177
|
-
|
|
178
|
-
// This backcompat converts old LHRs (<9.0.0) to use the new "hidden" group.
|
|
179
|
-
// Old LHRs used "no group" to identify audits that should be hidden in performance instead of the "hidden" group.
|
|
180
|
-
// Newer LHRs use "no group" to identify opportunities and diagnostics whose groups are assigned by details type.
|
|
181
|
-
const [majorVersion] = clone.lighthouseVersion.split('.').map(Number);
|
|
182
|
-
const perfCategory = clone.categories['performance'];
|
|
183
|
-
if (majorVersion < 9 && perfCategory) {
|
|
184
|
-
if (!clone.categoryGroups) clone.categoryGroups = {};
|
|
185
|
-
clone.categoryGroups['hidden'] = {title: ''};
|
|
186
|
-
for (const auditRef of perfCategory.auditRefs) {
|
|
187
|
-
if (!auditRef.group) {
|
|
188
|
-
auditRef.group = 'hidden';
|
|
189
|
-
} else if (['load-opportunities', 'diagnostics'].includes(auditRef.group)) {
|
|
190
|
-
delete auditRef.group;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
for (const category of Object.values(clone.categories)) {
|
|
196
|
-
// Make basic lookup table for relevantAudits
|
|
197
|
-
category.auditRefs.forEach(metricRef => {
|
|
198
|
-
if (!metricRef.relevantAudits) return;
|
|
199
|
-
metricRef.relevantAudits.forEach(auditId => {
|
|
200
|
-
const arr = relevantAuditToMetricsMap.get(auditId) || [];
|
|
201
|
-
arr.push(metricRef);
|
|
202
|
-
relevantAuditToMetricsMap.set(auditId, arr);
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
category.auditRefs.forEach(auditRef => {
|
|
207
|
-
const result = clone.audits[auditRef.id];
|
|
208
|
-
auditRef.result = result;
|
|
209
|
-
|
|
210
|
-
// Attach any relevantMetric auditRefs
|
|
211
|
-
if (relevantAuditToMetricsMap.has(auditRef.id)) {
|
|
212
|
-
auditRef.relevantMetrics = relevantAuditToMetricsMap.get(auditRef.id);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// attach the stackpacks to the auditRef object
|
|
216
|
-
if (clone.stackPacks) {
|
|
217
|
-
clone.stackPacks.forEach(pack => {
|
|
218
|
-
if (pack.descriptions[auditRef.id]) {
|
|
219
|
-
auditRef.stackPacks = auditRef.stackPacks || [];
|
|
220
|
-
auditRef.stackPacks.push({
|
|
221
|
-
title: pack.title,
|
|
222
|
-
iconDataURL: pack.iconDataURL,
|
|
223
|
-
description: pack.descriptions[auditRef.id],
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Add some minimal stuff so older reports still work.
|
|
232
|
-
if (!clone.environment) {
|
|
233
|
-
// @ts-expect-error
|
|
234
|
-
clone.environment = {benchmarkIndex: 0};
|
|
235
|
-
}
|
|
236
|
-
if (!clone.configSettings.screenEmulation) {
|
|
237
|
-
// @ts-expect-error
|
|
238
|
-
clone.configSettings.screenEmulation = {};
|
|
239
|
-
}
|
|
240
|
-
if (!clone.i18n) {
|
|
241
|
-
// @ts-expect-error
|
|
242
|
-
clone.i18n = {};
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// In 10.0, full-page-screenshot became a top-level property on the LHR.
|
|
246
|
-
if (clone.audits['full-page-screenshot']) {
|
|
247
|
-
const details = /** @type {LH.Result.FullPageScreenshot=} */ (
|
|
248
|
-
clone.audits['full-page-screenshot'].details);
|
|
249
|
-
if (details) {
|
|
250
|
-
clone.fullPageScreenshot = {
|
|
251
|
-
screenshot: details.screenshot,
|
|
252
|
-
nodes: details.nodes,
|
|
253
|
-
};
|
|
254
|
-
} else {
|
|
255
|
-
clone.fullPageScreenshot = null;
|
|
256
|
-
}
|
|
257
|
-
delete clone.audits['full-page-screenshot'];
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return clone;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
74
|
/**
|
|
264
75
|
* @param {LH.Result} lhr
|
|
265
76
|
* @return {LH.Result.FullPageScreenshot=}
|
|
@@ -275,56 +86,6 @@ class Util {
|
|
|
275
86
|
return details;
|
|
276
87
|
}
|
|
277
88
|
|
|
278
|
-
/**
|
|
279
|
-
* Used to determine if the "passed" for the purposes of showing up in the "failed" or "passed"
|
|
280
|
-
* sections of the report.
|
|
281
|
-
*
|
|
282
|
-
* @param {{score: (number|null), scoreDisplayMode: string}} audit
|
|
283
|
-
* @return {boolean}
|
|
284
|
-
*/
|
|
285
|
-
static showAsPassed(audit) {
|
|
286
|
-
switch (audit.scoreDisplayMode) {
|
|
287
|
-
case 'manual':
|
|
288
|
-
case 'notApplicable':
|
|
289
|
-
return true;
|
|
290
|
-
case 'error':
|
|
291
|
-
case 'informative':
|
|
292
|
-
return false;
|
|
293
|
-
case 'numeric':
|
|
294
|
-
case 'binary':
|
|
295
|
-
default:
|
|
296
|
-
return Number(audit.score) >= RATINGS.PASS.minScore;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Convert a score to a rating label.
|
|
302
|
-
* TODO: Return `'error'` for `score === null && !scoreDisplayMode`.
|
|
303
|
-
*
|
|
304
|
-
* @param {number|null} score
|
|
305
|
-
* @param {string=} scoreDisplayMode
|
|
306
|
-
* @return {string}
|
|
307
|
-
*/
|
|
308
|
-
static calculateRating(score, scoreDisplayMode) {
|
|
309
|
-
// Handle edge cases first, manual and not applicable receive 'pass', errored audits receive 'error'
|
|
310
|
-
if (scoreDisplayMode === 'manual' || scoreDisplayMode === 'notApplicable') {
|
|
311
|
-
return RATINGS.PASS.label;
|
|
312
|
-
} else if (scoreDisplayMode === 'error') {
|
|
313
|
-
return RATINGS.ERROR.label;
|
|
314
|
-
} else if (score === null) {
|
|
315
|
-
return RATINGS.FAIL.label;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// At this point, we're rating a standard binary/numeric audit
|
|
319
|
-
let rating = RATINGS.FAIL.label;
|
|
320
|
-
if (score >= RATINGS.PASS.minScore) {
|
|
321
|
-
rating = RATINGS.PASS.label;
|
|
322
|
-
} else if (score >= RATINGS.AVERAGE.minScore) {
|
|
323
|
-
rating = RATINGS.AVERAGE.label;
|
|
324
|
-
}
|
|
325
|
-
return rating;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
89
|
/**
|
|
329
90
|
* Split a string by markdown code spans (enclosed in `backticks`), splitting
|
|
330
91
|
* into segments that were enclosed in backticks (marked as `isCode === true`)
|
|
@@ -523,101 +284,19 @@ class Util {
|
|
|
523
284
|
return hostname.split('.').slice(-splitTld.length).join('.');
|
|
524
285
|
}
|
|
525
286
|
|
|
526
|
-
|
|
527
287
|
/**
|
|
528
|
-
*
|
|
529
|
-
*
|
|
288
|
+
* Returns only lines that are near a message, or the first few lines if there are
|
|
289
|
+
* no line messages.
|
|
290
|
+
* @param {SnippetValue['lines']} lines
|
|
291
|
+
* @param {SnippetValue['lineMessages']} lineMessages
|
|
292
|
+
* @param {number} surroundingLineCount Number of lines to include before and after
|
|
293
|
+
* the message. If this is e.g. 2 this function might return 5 lines.
|
|
530
294
|
*/
|
|
531
|
-
static
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const throttling = settings.throttling;
|
|
537
|
-
|
|
538
|
-
switch (settings.throttlingMethod) {
|
|
539
|
-
case 'provided':
|
|
540
|
-
summary = networkThrottling = cpuThrottling = Util.strings.throttlingProvided;
|
|
541
|
-
break;
|
|
542
|
-
case 'devtools': {
|
|
543
|
-
const {cpuSlowdownMultiplier, requestLatencyMs} = throttling;
|
|
544
|
-
// eslint-disable-next-line max-len
|
|
545
|
-
cpuThrottling = `${Util.i18n.formatNumber(cpuSlowdownMultiplier)}x slowdown (DevTools)`;
|
|
546
|
-
networkThrottling = `${Util.i18n.formatMilliseconds(requestLatencyMs)} HTTP RTT, ` +
|
|
547
|
-
`${Util.i18n.formatKbps(throttling.downloadThroughputKbps)} down, ` +
|
|
548
|
-
`${Util.i18n.formatKbps(throttling.uploadThroughputKbps)} up (DevTools)`;
|
|
549
|
-
|
|
550
|
-
const isSlow4G = () => {
|
|
551
|
-
return requestLatencyMs === 150 * 3.75 &&
|
|
552
|
-
throttling.downloadThroughputKbps === 1.6 * 1024 * 0.9 &&
|
|
553
|
-
throttling.uploadThroughputKbps === 750 * 0.9;
|
|
554
|
-
};
|
|
555
|
-
summary = isSlow4G() ?
|
|
556
|
-
Util.strings.runtimeSlow4g : Util.strings.runtimeCustom;
|
|
557
|
-
break;
|
|
558
|
-
}
|
|
559
|
-
case 'simulate': {
|
|
560
|
-
const {cpuSlowdownMultiplier, rttMs, throughputKbps} = throttling;
|
|
561
|
-
// eslint-disable-next-line max-len
|
|
562
|
-
cpuThrottling = `${Util.i18n.formatNumber(cpuSlowdownMultiplier)}x slowdown (Simulated)`;
|
|
563
|
-
networkThrottling = `${Util.i18n.formatMilliseconds(rttMs)} TCP RTT, ` +
|
|
564
|
-
`${Util.i18n.formatKbps(throughputKbps)} throughput (Simulated)`;
|
|
565
|
-
|
|
566
|
-
const isSlow4G = () => {
|
|
567
|
-
return rttMs === 150 && throughputKbps === 1.6 * 1024;
|
|
568
|
-
};
|
|
569
|
-
summary = isSlow4G() ?
|
|
570
|
-
Util.strings.runtimeSlow4g : Util.strings.runtimeCustom;
|
|
571
|
-
break;
|
|
572
|
-
}
|
|
573
|
-
default:
|
|
574
|
-
summary = cpuThrottling = networkThrottling = Util.strings.runtimeUnknown;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// devtools-entry.js always sets `screenEmulation.disabled` when using mobile emulation,
|
|
578
|
-
// because we handle the emulation outside of Lighthouse. Since the screen truly is emulated
|
|
579
|
-
// as a mobile device, ignore `.disabled` in devtools and just check the form factor
|
|
580
|
-
const isScreenEmulationDisabled = settings.channel === 'devtools' ?
|
|
581
|
-
false :
|
|
582
|
-
settings.screenEmulation.disabled;
|
|
583
|
-
const isScreenEmulationMobile = settings.channel === 'devtools' ?
|
|
584
|
-
settings.formFactor === 'mobile' :
|
|
585
|
-
settings.screenEmulation.mobile;
|
|
586
|
-
|
|
587
|
-
let deviceEmulation = Util.strings.runtimeMobileEmulation;
|
|
588
|
-
if (isScreenEmulationDisabled) {
|
|
589
|
-
deviceEmulation = Util.strings.runtimeNoEmulation;
|
|
590
|
-
} else if (!isScreenEmulationMobile) {
|
|
591
|
-
deviceEmulation = Util.strings.runtimeDesktopEmulation;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
const screenEmulation = isScreenEmulationDisabled ?
|
|
595
|
-
undefined :
|
|
596
|
-
// eslint-disable-next-line max-len
|
|
597
|
-
`${settings.screenEmulation.width}x${settings.screenEmulation.height}, DPR ${settings.screenEmulation.deviceScaleFactor}`;
|
|
598
|
-
|
|
599
|
-
return {
|
|
600
|
-
deviceEmulation,
|
|
601
|
-
screenEmulation,
|
|
602
|
-
cpuThrottling,
|
|
603
|
-
networkThrottling,
|
|
604
|
-
summary,
|
|
605
|
-
};
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
/**
|
|
609
|
-
* Returns only lines that are near a message, or the first few lines if there are
|
|
610
|
-
* no line messages.
|
|
611
|
-
* @param {LH.Audit.Details.SnippetValue['lines']} lines
|
|
612
|
-
* @param {LH.Audit.Details.SnippetValue['lineMessages']} lineMessages
|
|
613
|
-
* @param {number} surroundingLineCount Number of lines to include before and after
|
|
614
|
-
* the message. If this is e.g. 2 this function might return 5 lines.
|
|
615
|
-
*/
|
|
616
|
-
static filterRelevantLines(lines, lineMessages, surroundingLineCount) {
|
|
617
|
-
if (lineMessages.length === 0) {
|
|
618
|
-
// no lines with messages, just return the first bunch of lines
|
|
619
|
-
return lines.slice(0, surroundingLineCount * 2 + 1);
|
|
620
|
-
}
|
|
295
|
+
static filterRelevantLines(lines, lineMessages, surroundingLineCount) {
|
|
296
|
+
if (lineMessages.length === 0) {
|
|
297
|
+
// no lines with messages, just return the first bunch of lines
|
|
298
|
+
return lines.slice(0, surroundingLineCount * 2 + 1);
|
|
299
|
+
}
|
|
621
300
|
|
|
622
301
|
const minGapSize = 3;
|
|
623
302
|
const lineNumbersToKeep = new Set();
|
|
@@ -646,195 +325,8 @@ class Util {
|
|
|
646
325
|
|
|
647
326
|
return lines.filter(line => lineNumbersToKeep.has(line.lineNumber));
|
|
648
327
|
}
|
|
649
|
-
|
|
650
|
-
/**
|
|
651
|
-
* @param {string} categoryId
|
|
652
|
-
*/
|
|
653
|
-
static isPluginCategory(categoryId) {
|
|
654
|
-
return categoryId.startsWith('lighthouse-plugin-');
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
/**
|
|
658
|
-
* @param {LH.Result.GatherMode} gatherMode
|
|
659
|
-
*/
|
|
660
|
-
static shouldDisplayAsFraction(gatherMode) {
|
|
661
|
-
return gatherMode === 'timespan' || gatherMode === 'snapshot';
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
/**
|
|
665
|
-
* @param {LH.ReportResult.Category} category
|
|
666
|
-
*/
|
|
667
|
-
static calculateCategoryFraction(category) {
|
|
668
|
-
let numPassableAudits = 0;
|
|
669
|
-
let numPassed = 0;
|
|
670
|
-
let numInformative = 0;
|
|
671
|
-
let totalWeight = 0;
|
|
672
|
-
for (const auditRef of category.auditRefs) {
|
|
673
|
-
const auditPassed = Util.showAsPassed(auditRef.result);
|
|
674
|
-
|
|
675
|
-
// Don't count the audit if it's manual, N/A, or isn't displayed.
|
|
676
|
-
if (auditRef.group === 'hidden' ||
|
|
677
|
-
auditRef.result.scoreDisplayMode === 'manual' ||
|
|
678
|
-
auditRef.result.scoreDisplayMode === 'notApplicable') {
|
|
679
|
-
continue;
|
|
680
|
-
} else if (auditRef.result.scoreDisplayMode === 'informative') {
|
|
681
|
-
if (!auditPassed) {
|
|
682
|
-
++numInformative;
|
|
683
|
-
}
|
|
684
|
-
continue;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
++numPassableAudits;
|
|
688
|
-
totalWeight += auditRef.weight;
|
|
689
|
-
if (auditPassed) numPassed++;
|
|
690
|
-
}
|
|
691
|
-
return {numPassed, numPassableAudits, numInformative, totalWeight};
|
|
692
|
-
}
|
|
693
328
|
}
|
|
694
329
|
|
|
695
|
-
/**
|
|
696
|
-
* Some parts of the report renderer require data found on the LHR. Instead of wiring it
|
|
697
|
-
* through, we have this global.
|
|
698
|
-
* @type {LH.ReportResult | null}
|
|
699
|
-
*/
|
|
700
|
-
Util.reportJson = null;
|
|
701
|
-
|
|
702
|
-
let svgSuffix = 0;
|
|
703
|
-
/**
|
|
704
|
-
* An always-increasing counter for making unique SVG ID suffixes.
|
|
705
|
-
*/
|
|
706
|
-
Util.getUniqueSuffix = () => {
|
|
707
|
-
return svgSuffix++;
|
|
708
|
-
};
|
|
709
|
-
Util.resetUniqueSuffix = () => {
|
|
710
|
-
svgSuffix = 0;
|
|
711
|
-
};
|
|
712
|
-
|
|
713
|
-
/**
|
|
714
|
-
* Report-renderer-specific strings.
|
|
715
|
-
*/
|
|
716
|
-
const UIStrings = {
|
|
717
|
-
/** Disclaimer shown to users below the metric values (First Contentful Paint, Time to Interactive, etc) to warn them that the numbers they see will likely change slightly the next time they run Lighthouse. */
|
|
718
|
-
varianceDisclaimer: 'Values are estimated and may vary. The [performance score is calculated](https://developer.chrome.com/docs/lighthouse/performance/performance-scoring/) directly from these metrics.',
|
|
719
|
-
/** Text link pointing to an interactive calculator that explains Lighthouse scoring. The link text should be fairly short. */
|
|
720
|
-
calculatorLink: 'See calculator.',
|
|
721
|
-
/** Label preceding a radio control for filtering the list of audits. The radio choices are various performance metrics (FCP, LCP, TBT), and if chosen, the audits in the report are hidden if they are not relevant to the selected metric. */
|
|
722
|
-
showRelevantAudits: 'Show audits relevant to:',
|
|
723
|
-
/** Column heading label for the listing of opportunity audits. Each audit title represents an opportunity. There are only 2 columns, so no strict character limit. */
|
|
724
|
-
opportunityResourceColumnLabel: 'Opportunity',
|
|
725
|
-
/** Column heading label for the estimated page load savings of opportunity audits. Estimated Savings is the total amount of time (in seconds) that Lighthouse computed could be reduced from the total page load time, if the suggested action is taken. There are only 2 columns, so no strict character limit. */
|
|
726
|
-
opportunitySavingsColumnLabel: 'Estimated Savings',
|
|
727
|
-
|
|
728
|
-
/** An error string displayed next to a particular audit when it has errored, but not provided any specific error message. */
|
|
729
|
-
errorMissingAuditInfo: 'Report error: no audit information',
|
|
730
|
-
/** A label, shown next to an audit title or metric title, indicating that there was an error computing it. The user can hover on the label to reveal a tooltip with the extended error message. Translation should be short (< 20 characters). */
|
|
731
|
-
errorLabel: 'Error!',
|
|
732
|
-
/** This label is shown above a bulleted list of warnings. It is shown directly below an audit that produced warnings. Warnings describe situations the user should be aware of, as Lighthouse was unable to complete all the work required on this audit. For example, The 'Unable to decode image (biglogo.jpg)' warning may show up below an image encoding audit. */
|
|
733
|
-
warningHeader: 'Warnings: ',
|
|
734
|
-
/** Section heading shown above a list of passed audits that contain warnings. Audits under this section do not negatively impact the score, but Lighthouse has generated some potentially actionable suggestions that should be reviewed. This section is expanded by default and displays after the failing audits. */
|
|
735
|
-
warningAuditsGroupTitle: 'Passed audits but with warnings',
|
|
736
|
-
/** Section heading shown above a list of audits that are passing. 'Passed' here refers to a passing grade. This section is collapsed by default, as the user should be focusing on the failed audits instead. Users can click this heading to reveal the list. */
|
|
737
|
-
passedAuditsGroupTitle: 'Passed audits',
|
|
738
|
-
/** Section heading shown above a list of audits that do not apply to the page. For example, if an audit is 'Are images optimized?', but the page has no images on it, the audit will be marked as not applicable. This is neither passing or failing. This section is collapsed by default, as the user should be focusing on the failed audits instead. Users can click this heading to reveal the list. */
|
|
739
|
-
notApplicableAuditsGroupTitle: 'Not applicable',
|
|
740
|
-
/** Section heading shown above a list of audits that were not computed by Lighthouse. They serve as a list of suggestions for the user to go and manually check. For example, Lighthouse can't automate testing cross-browser compatibility, so that is listed within this section, so the user is reminded to test it themselves. This section is collapsed by default, as the user should be focusing on the failed audits instead. Users can click this heading to reveal the list. */
|
|
741
|
-
manualAuditsGroupTitle: 'Additional items to manually check',
|
|
742
|
-
|
|
743
|
-
/** Label shown preceding any important warnings that may have invalidated the entire report. For example, if the user has Chrome extensions installed, they may add enough performance overhead that Lighthouse's performance metrics are unreliable. If shown, this will be displayed at the top of the report UI. */
|
|
744
|
-
toplevelWarningsMessage: 'There were issues affecting this run of Lighthouse:',
|
|
745
|
-
|
|
746
|
-
/** String of text shown in a graphical representation of the flow of network requests for the web page. This label represents the initial network request that fetches an HTML page. This navigation may be redirected (eg. Initial navigation to http://example.com redirects to https://www.example.com). */
|
|
747
|
-
crcInitialNavigation: 'Initial Navigation',
|
|
748
|
-
/** Label of value shown in the summary of critical request chains. Refers to the total amount of time (milliseconds) of the longest critical path chain/sequence of network requests. Example value: 2310 ms */
|
|
749
|
-
crcLongestDurationLabel: 'Maximum critical path latency:',
|
|
750
|
-
|
|
751
|
-
/** Label for button that shows all lines of the snippet when clicked */
|
|
752
|
-
snippetExpandButtonLabel: 'Expand snippet',
|
|
753
|
-
/** Label for button that only shows a few lines of the snippet when clicked */
|
|
754
|
-
snippetCollapseButtonLabel: 'Collapse snippet',
|
|
755
|
-
|
|
756
|
-
/** Explanation shown to users below performance results to inform them that the test was done with a 4G network connection and to warn them that the numbers they see will likely change slightly the next time they run Lighthouse. 'Lighthouse' becomes link text to additional documentation. */
|
|
757
|
-
lsPerformanceCategoryDescription: '[Lighthouse](https://developers.google.com/web/tools/lighthouse/) analysis of the current page on an emulated mobile network. Values are estimated and may vary.',
|
|
758
|
-
/** Title of the lab data section of the Performance category. Within this section are various speed metrics which quantify the pageload performance into values presented in seconds and milliseconds. "Lab" is an abbreviated form of "laboratory", and refers to the fact that the data is from a controlled test of a website, not measurements from real users visiting that site. */
|
|
759
|
-
labDataTitle: 'Lab Data',
|
|
760
|
-
|
|
761
|
-
/** This label is for a checkbox above a table of items loaded by a web page. The checkbox is used to show or hide third-party (or "3rd-party") resources in the table, where "third-party resources" refers to items loaded by a web page from URLs that aren't controlled by the owner of the web page. */
|
|
762
|
-
thirdPartyResourcesLabel: 'Show 3rd-party resources',
|
|
763
|
-
/** This label is for a button that opens a new tab to a webapp called "Treemap", which is a nested visual representation of a heierarchy of data related to the reports (script bytes and coverage, resource breakdown, etc.) */
|
|
764
|
-
viewTreemapLabel: 'View Treemap',
|
|
765
|
-
/** This label is for a button that will show the user a trace of the page. */
|
|
766
|
-
viewTraceLabel: 'View Trace',
|
|
767
|
-
/** This label is for a button that will show the user a trace of the page. */
|
|
768
|
-
viewOriginalTraceLabel: 'View Original Trace',
|
|
769
|
-
|
|
770
|
-
/** Option in a dropdown menu that opens a small, summary report in a print dialog. */
|
|
771
|
-
dropdownPrintSummary: 'Print Summary',
|
|
772
|
-
/** Option in a dropdown menu that opens a full Lighthouse report in a print dialog. */
|
|
773
|
-
dropdownPrintExpanded: 'Print Expanded',
|
|
774
|
-
/** Option in a dropdown menu that copies the Lighthouse JSON object to the system clipboard. */
|
|
775
|
-
dropdownCopyJSON: 'Copy JSON',
|
|
776
|
-
/** Option in a dropdown menu that saves the Lighthouse report HTML locally to the system as a '.html' file. */
|
|
777
|
-
dropdownSaveHTML: 'Save as HTML',
|
|
778
|
-
/** Option in a dropdown menu that saves the Lighthouse JSON object to the local system as a '.json' file. */
|
|
779
|
-
dropdownSaveJSON: 'Save as JSON',
|
|
780
|
-
/** Option in a dropdown menu that opens the current report in the Lighthouse Viewer Application. */
|
|
781
|
-
dropdownViewer: 'Open in Viewer',
|
|
782
|
-
/** Option in a dropdown menu that saves the current report as a new GitHub Gist. */
|
|
783
|
-
dropdownSaveGist: 'Save as Gist',
|
|
784
|
-
/** Option in a dropdown menu that toggles the themeing of the report between Light(default) and Dark themes. */
|
|
785
|
-
dropdownDarkTheme: 'Toggle Dark Theme',
|
|
786
|
-
|
|
787
|
-
/** Label for a row in a table that describes the kind of device that was emulated for the Lighthouse run. Example values for row elements: 'No Emulation', 'Emulated Desktop', etc. */
|
|
788
|
-
runtimeSettingsDevice: 'Device',
|
|
789
|
-
/** Label for a row in a table that describes the network throttling conditions that were used during a Lighthouse run, if any. */
|
|
790
|
-
runtimeSettingsNetworkThrottling: 'Network throttling',
|
|
791
|
-
/** Label for a row in a table that describes the CPU throttling conditions that were used during a Lighthouse run, if any.*/
|
|
792
|
-
runtimeSettingsCPUThrottling: 'CPU throttling',
|
|
793
|
-
/** Label for a row in a table that shows the User Agent that was used to send out all network requests during the Lighthouse run. */
|
|
794
|
-
runtimeSettingsUANetwork: 'User agent (network)',
|
|
795
|
-
/** Label for a row in a table that shows the estimated CPU power of the machine running Lighthouse. Example row values: 532, 1492, 783. */
|
|
796
|
-
runtimeSettingsBenchmark: 'CPU/Memory Power',
|
|
797
|
-
/** Label for a row in a table that shows the version of the Axe library used. Example row values: 2.1.0, 3.2.3 */
|
|
798
|
-
runtimeSettingsAxeVersion: 'Axe version',
|
|
799
|
-
/** Label for a row in a table that shows the screen resolution and DPR that was emulated for the Lighthouse run. Example values: '800x600, DPR: 3' */
|
|
800
|
-
runtimeSettingsScreenEmulation: 'Screen emulation',
|
|
801
|
-
|
|
802
|
-
/** Label for button to create an issue against the Lighthouse GitHub project. */
|
|
803
|
-
footerIssue: 'File an issue',
|
|
804
|
-
|
|
805
|
-
/** Descriptive explanation for emulation setting when no device emulation is set. */
|
|
806
|
-
runtimeNoEmulation: 'No emulation',
|
|
807
|
-
/** Descriptive explanation for emulation setting when emulating a Moto G4 mobile device. */
|
|
808
|
-
runtimeMobileEmulation: 'Emulated Moto G4',
|
|
809
|
-
/** Descriptive explanation for emulation setting when emulating a generic desktop form factor, as opposed to a mobile-device like form factor. */
|
|
810
|
-
runtimeDesktopEmulation: 'Emulated Desktop',
|
|
811
|
-
/** Descriptive explanation for a runtime setting that is set to an unknown value. */
|
|
812
|
-
runtimeUnknown: 'Unknown',
|
|
813
|
-
/** Descriptive label that this analysis run was from a single pageload of a browser (not a summary of hundreds of loads) */
|
|
814
|
-
runtimeSingleLoad: 'Single page load',
|
|
815
|
-
/** Descriptive label that this analysis only considers the initial load of the page, and no interaction beyond when the page had "fully loaded" */
|
|
816
|
-
runtimeAnalysisWindow: 'Initial page load',
|
|
817
|
-
/** Descriptive explanation that this analysis run was from a single pageload of a browser, whereas field data often summarizes hundreds+ of page loads */
|
|
818
|
-
runtimeSingleLoadTooltip: 'This data is taken from a single page load, as opposed to field data summarizing many sessions.', // eslint-disable-line max-len
|
|
819
|
-
|
|
820
|
-
/** Descriptive explanation for environment throttling that was provided by the runtime environment instead of provided by Lighthouse throttling. */
|
|
821
|
-
throttlingProvided: 'Provided by environment',
|
|
822
|
-
/** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Show' and 'Hide'. */
|
|
823
|
-
show: 'Show',
|
|
824
|
-
/** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Show' and 'Hide'. */
|
|
825
|
-
hide: 'Hide',
|
|
826
|
-
/** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Expand view' and 'Collapse view'. */
|
|
827
|
-
expandView: 'Expand view',
|
|
828
|
-
/** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Expand view' and 'Collapse view'. */
|
|
829
|
-
collapseView: 'Collapse view',
|
|
830
|
-
/** Label indicating that Lighthouse throttled the page to emulate a slow 4G network connection. */
|
|
831
|
-
runtimeSlow4g: 'Slow 4G throttling',
|
|
832
|
-
/** Label indicating that Lighthouse throttled the page using custom throttling settings. */
|
|
833
|
-
runtimeCustom: 'Custom throttling',
|
|
834
|
-
};
|
|
835
|
-
Util.UIStrings = UIStrings;
|
|
836
|
-
Util.strings = {...UIStrings};
|
|
837
|
-
|
|
838
330
|
// auto-generated by build/build-report-components.js
|
|
839
331
|
|
|
840
332
|
/** @typedef {import('./dom.js').DOM} DOM */
|
|
@@ -1729,125 +1221,655 @@ class DOM {
|
|
|
1729
1221
|
if (parsed && allowedProtocols.includes(parsed.protocol)) {
|
|
1730
1222
|
elem.href = parsed.href;
|
|
1731
1223
|
}
|
|
1732
|
-
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* Only create blob URLs for JSON & HTML
|
|
1228
|
+
* @param {HTMLAnchorElement} elem
|
|
1229
|
+
* @param {Blob} blob
|
|
1230
|
+
*/
|
|
1231
|
+
safelySetBlobHref(elem, blob) {
|
|
1232
|
+
if (blob.type !== 'text/html' && blob.type !== 'application/json') {
|
|
1233
|
+
throw new Error('Unsupported blob type');
|
|
1234
|
+
}
|
|
1235
|
+
const href = URL.createObjectURL(blob);
|
|
1236
|
+
elem.href = href;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* @param {string} markdownText
|
|
1241
|
+
* @return {Element}
|
|
1242
|
+
*/
|
|
1243
|
+
convertMarkdownCodeSnippets(markdownText) {
|
|
1244
|
+
const element = this.createElement('span');
|
|
1245
|
+
|
|
1246
|
+
for (const segment of Util.splitMarkdownCodeSpans(markdownText)) {
|
|
1247
|
+
if (segment.isCode) {
|
|
1248
|
+
const pre = this.createElement('code');
|
|
1249
|
+
pre.textContent = segment.text;
|
|
1250
|
+
element.append(pre);
|
|
1251
|
+
} else {
|
|
1252
|
+
element.append(this._document.createTextNode(segment.text));
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
return element;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/**
|
|
1260
|
+
* The channel to use for UTM data when rendering links to the documentation.
|
|
1261
|
+
* @param {string} lighthouseChannel
|
|
1262
|
+
*/
|
|
1263
|
+
setLighthouseChannel(lighthouseChannel) {
|
|
1264
|
+
this._lighthouseChannel = lighthouseChannel;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/**
|
|
1268
|
+
* ONLY use if `dom.rootEl` isn't sufficient for your needs. `dom.rootEl` is preferred
|
|
1269
|
+
* for all scoping, because a document can have multiple reports within it.
|
|
1270
|
+
* @return {Document}
|
|
1271
|
+
*/
|
|
1272
|
+
document() {
|
|
1273
|
+
return this._document;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
/**
|
|
1277
|
+
* TODO(paulirish): import and conditionally apply the DevTools frontend subclasses instead of this
|
|
1278
|
+
* @return {boolean}
|
|
1279
|
+
*/
|
|
1280
|
+
isDevTools() {
|
|
1281
|
+
return !!this._document.querySelector('.lh-devtools');
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* Guaranteed context.querySelector. Always returns an element or throws if
|
|
1286
|
+
* nothing matches query.
|
|
1287
|
+
* @template {string} T
|
|
1288
|
+
* @param {T} query
|
|
1289
|
+
* @param {ParentNode} context
|
|
1290
|
+
* @return {ParseSelector<T>}
|
|
1291
|
+
*/
|
|
1292
|
+
find(query, context) {
|
|
1293
|
+
const result = context.querySelector(query);
|
|
1294
|
+
if (result === null) {
|
|
1295
|
+
throw new Error(`query ${query} not found`);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// Because we control the report layout and templates, use the simpler
|
|
1299
|
+
// `typed-query-selector` types that don't require differentiating between
|
|
1300
|
+
// e.g. HTMLAnchorElement and SVGAElement. See https://github.com/GoogleChrome/lighthouse/issues/12011
|
|
1301
|
+
return /** @type {ParseSelector<T>} */ (result);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* Helper for context.querySelectorAll. Returns an Array instead of a NodeList.
|
|
1306
|
+
* @template {string} T
|
|
1307
|
+
* @param {T} query
|
|
1308
|
+
* @param {ParentNode} context
|
|
1309
|
+
*/
|
|
1310
|
+
findAll(query, context) {
|
|
1311
|
+
const elements = Array.from(context.querySelectorAll(query));
|
|
1312
|
+
return elements;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* Fires a custom DOM event on target.
|
|
1317
|
+
* @param {string} name Name of the event.
|
|
1318
|
+
* @param {Node=} target DOM node to fire the event on.
|
|
1319
|
+
* @param {*=} detail Custom data to include.
|
|
1320
|
+
*/
|
|
1321
|
+
fireEventOn(name, target = this._document, detail) {
|
|
1322
|
+
const event = new CustomEvent(name, detail ? {detail} : undefined);
|
|
1323
|
+
target.dispatchEvent(event);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
/**
|
|
1327
|
+
* Downloads a file (blob) using a[download].
|
|
1328
|
+
* @param {Blob|File} blob The file to save.
|
|
1329
|
+
* @param {string} filename
|
|
1330
|
+
*/
|
|
1331
|
+
saveFile(blob, filename) {
|
|
1332
|
+
const a = this.createElement('a');
|
|
1333
|
+
a.download = filename;
|
|
1334
|
+
this.safelySetBlobHref(a, blob);
|
|
1335
|
+
this._document.body.append(a); // Firefox requires anchor to be in the DOM.
|
|
1336
|
+
a.click();
|
|
1337
|
+
|
|
1338
|
+
// cleanup.
|
|
1339
|
+
this._document.body.removeChild(a);
|
|
1340
|
+
setTimeout(() => URL.revokeObjectURL(a.href), 500);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/**
|
|
1345
|
+
* @license Copyright 2023 The Lighthouse Authors. All Rights Reserved.
|
|
1346
|
+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
1347
|
+
* 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.
|
|
1348
|
+
*/
|
|
1349
|
+
|
|
1350
|
+
/** @typedef {import('./i18n-formatter').I18nFormatter} I18nFormatter */
|
|
1351
|
+
|
|
1352
|
+
let svgSuffix = 0;
|
|
1353
|
+
|
|
1354
|
+
class Globals {
|
|
1355
|
+
/** @type {I18nFormatter} */
|
|
1356
|
+
// @ts-expect-error: Set in report renderer.
|
|
1357
|
+
static i18n = null;
|
|
1358
|
+
|
|
1359
|
+
/** @type {typeof UIStrings} */
|
|
1360
|
+
// @ts-expect-error: Set in report renderer.
|
|
1361
|
+
static strings = {};
|
|
1362
|
+
|
|
1363
|
+
/** @type {LH.ReportResult | null} */
|
|
1364
|
+
static reportJson = null;
|
|
1365
|
+
|
|
1366
|
+
/**
|
|
1367
|
+
* @param {{providedStrings: Record<string, string>; i18n: I18nFormatter; reportJson: LH.ReportResult | null}} options
|
|
1368
|
+
*/
|
|
1369
|
+
static apply(options) {
|
|
1370
|
+
Globals.strings = {
|
|
1371
|
+
// Set missing renderer strings to default (english) values.
|
|
1372
|
+
...UIStrings,
|
|
1373
|
+
...options.providedStrings,
|
|
1374
|
+
};
|
|
1375
|
+
Globals.i18n = options.i18n;
|
|
1376
|
+
Globals.reportJson = options.reportJson;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
static getUniqueSuffix() {
|
|
1380
|
+
return svgSuffix++;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
static resetUniqueSuffix() {
|
|
1384
|
+
svgSuffix = 0;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
/**
|
|
1389
|
+
* @license Copyright 2023 The Lighthouse Authors. All Rights Reserved.
|
|
1390
|
+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
1391
|
+
* 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.
|
|
1392
|
+
*/
|
|
1393
|
+
|
|
1394
|
+
const SCREENSHOT_PREFIX = 'data:image/jpeg;base64,';
|
|
1395
|
+
const RATINGS = Util.RATINGS;
|
|
1396
|
+
|
|
1397
|
+
class ReportUtils {
|
|
1398
|
+
/**
|
|
1399
|
+
* Returns a new LHR that's reshaped for slightly better ergonomics within the report rendereer.
|
|
1400
|
+
* Also, sets up the localized UI strings used within renderer and makes changes to old LHRs to be
|
|
1401
|
+
* compatible with current renderer.
|
|
1402
|
+
* The LHR passed in is not mutated.
|
|
1403
|
+
* TODO(team): we all agree the LHR shape change is technical debt we should fix
|
|
1404
|
+
* @param {LH.Result} result
|
|
1405
|
+
* @return {LH.ReportResult}
|
|
1406
|
+
*/
|
|
1407
|
+
static prepareReportResult(result) {
|
|
1408
|
+
// If any mutations happen to the report within the renderers, we want the original object untouched
|
|
1409
|
+
const clone = /** @type {LH.ReportResult} */ (JSON.parse(JSON.stringify(result)));
|
|
1410
|
+
|
|
1411
|
+
// If LHR is older (≤3.0.3), it has no locale setting. Set default.
|
|
1412
|
+
if (!clone.configSettings.locale) {
|
|
1413
|
+
clone.configSettings.locale = 'en';
|
|
1414
|
+
}
|
|
1415
|
+
if (!clone.configSettings.formFactor) {
|
|
1416
|
+
// @ts-expect-error fallback handling for emulatedFormFactor
|
|
1417
|
+
clone.configSettings.formFactor = clone.configSettings.emulatedFormFactor;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
clone.finalDisplayedUrl = Util.getFinalDisplayedUrl(clone);
|
|
1421
|
+
clone.mainDocumentUrl = Util.getMainDocumentUrl(clone);
|
|
1422
|
+
|
|
1423
|
+
for (const audit of Object.values(clone.audits)) {
|
|
1424
|
+
// Turn 'not-applicable' (LHR <4.0) and 'not_applicable' (older proto versions)
|
|
1425
|
+
// into 'notApplicable' (LHR ≥4.0).
|
|
1426
|
+
// @ts-expect-error tsc rightly flags that these values shouldn't occur.
|
|
1427
|
+
// eslint-disable-next-line max-len
|
|
1428
|
+
if (audit.scoreDisplayMode === 'not_applicable' || audit.scoreDisplayMode === 'not-applicable') {
|
|
1429
|
+
audit.scoreDisplayMode = 'notApplicable';
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
if (audit.details) {
|
|
1433
|
+
// Turn `auditDetails.type` of undefined (LHR <4.2) and 'diagnostic' (LHR <5.0)
|
|
1434
|
+
// into 'debugdata' (LHR ≥5.0).
|
|
1435
|
+
// @ts-expect-error tsc rightly flags that these values shouldn't occur.
|
|
1436
|
+
if (audit.details.type === undefined || audit.details.type === 'diagnostic') {
|
|
1437
|
+
// @ts-expect-error details is of type never.
|
|
1438
|
+
audit.details.type = 'debugdata';
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// Add the jpg data URL prefix to filmstrip screenshots without them (LHR <5.0).
|
|
1442
|
+
if (audit.details.type === 'filmstrip') {
|
|
1443
|
+
for (const screenshot of audit.details.items) {
|
|
1444
|
+
if (!screenshot.data.startsWith(SCREENSHOT_PREFIX)) {
|
|
1445
|
+
screenshot.data = SCREENSHOT_PREFIX + screenshot.data;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// Circa 10.0, table items were refactored.
|
|
1451
|
+
if (audit.details.type === 'table') {
|
|
1452
|
+
for (const heading of audit.details.headings) {
|
|
1453
|
+
/** @type {{itemType: LH.Audit.Details.ItemValueType|undefined, text: string|undefined}} */
|
|
1454
|
+
// @ts-expect-error
|
|
1455
|
+
const {itemType, text} = heading;
|
|
1456
|
+
if (itemType !== undefined) {
|
|
1457
|
+
heading.valueType = itemType;
|
|
1458
|
+
// @ts-expect-error
|
|
1459
|
+
delete heading.itemType;
|
|
1460
|
+
}
|
|
1461
|
+
if (text !== undefined) {
|
|
1462
|
+
heading.label = text;
|
|
1463
|
+
// @ts-expect-error
|
|
1464
|
+
delete heading.text;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// @ts-expect-error
|
|
1468
|
+
const subItemsItemType = heading.subItemsHeading?.itemType;
|
|
1469
|
+
if (heading.subItemsHeading && subItemsItemType !== undefined) {
|
|
1470
|
+
heading.subItemsHeading.valueType = subItemsItemType;
|
|
1471
|
+
// @ts-expect-error
|
|
1472
|
+
delete heading.subItemsHeading.itemType;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// TODO: convert printf-style displayValue.
|
|
1478
|
+
// Added: #5099, v3
|
|
1479
|
+
// Removed: #6767, v4
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// For convenience, smoosh all AuditResults into their auditRef (which has just weight & group)
|
|
1484
|
+
if (typeof clone.categories !== 'object') throw new Error('No categories provided.');
|
|
1485
|
+
|
|
1486
|
+
/** @type {Map<string, Array<LH.ReportResult.AuditRef>>} */
|
|
1487
|
+
const relevantAuditToMetricsMap = new Map();
|
|
1488
|
+
|
|
1489
|
+
// This backcompat converts old LHRs (<9.0.0) to use the new "hidden" group.
|
|
1490
|
+
// Old LHRs used "no group" to identify audits that should be hidden in performance instead of the "hidden" group.
|
|
1491
|
+
// Newer LHRs use "no group" to identify opportunities and diagnostics whose groups are assigned by details type.
|
|
1492
|
+
const [majorVersion] = clone.lighthouseVersion.split('.').map(Number);
|
|
1493
|
+
const perfCategory = clone.categories['performance'];
|
|
1494
|
+
if (majorVersion < 9 && perfCategory) {
|
|
1495
|
+
if (!clone.categoryGroups) clone.categoryGroups = {};
|
|
1496
|
+
clone.categoryGroups['hidden'] = {title: ''};
|
|
1497
|
+
for (const auditRef of perfCategory.auditRefs) {
|
|
1498
|
+
if (!auditRef.group) {
|
|
1499
|
+
auditRef.group = 'hidden';
|
|
1500
|
+
} else if (['load-opportunities', 'diagnostics'].includes(auditRef.group)) {
|
|
1501
|
+
delete auditRef.group;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
for (const category of Object.values(clone.categories)) {
|
|
1507
|
+
// Make basic lookup table for relevantAudits
|
|
1508
|
+
category.auditRefs.forEach(metricRef => {
|
|
1509
|
+
if (!metricRef.relevantAudits) return;
|
|
1510
|
+
metricRef.relevantAudits.forEach(auditId => {
|
|
1511
|
+
const arr = relevantAuditToMetricsMap.get(auditId) || [];
|
|
1512
|
+
arr.push(metricRef);
|
|
1513
|
+
relevantAuditToMetricsMap.set(auditId, arr);
|
|
1514
|
+
});
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1517
|
+
category.auditRefs.forEach(auditRef => {
|
|
1518
|
+
const result = clone.audits[auditRef.id];
|
|
1519
|
+
auditRef.result = result;
|
|
1520
|
+
|
|
1521
|
+
// Attach any relevantMetric auditRefs
|
|
1522
|
+
if (relevantAuditToMetricsMap.has(auditRef.id)) {
|
|
1523
|
+
auditRef.relevantMetrics = relevantAuditToMetricsMap.get(auditRef.id);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// attach the stackpacks to the auditRef object
|
|
1527
|
+
if (clone.stackPacks) {
|
|
1528
|
+
clone.stackPacks.forEach(pack => {
|
|
1529
|
+
if (pack.descriptions[auditRef.id]) {
|
|
1530
|
+
auditRef.stackPacks = auditRef.stackPacks || [];
|
|
1531
|
+
auditRef.stackPacks.push({
|
|
1532
|
+
title: pack.title,
|
|
1533
|
+
iconDataURL: pack.iconDataURL,
|
|
1534
|
+
description: pack.descriptions[auditRef.id],
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// Add some minimal stuff so older reports still work.
|
|
1543
|
+
if (!clone.environment) {
|
|
1544
|
+
// @ts-expect-error
|
|
1545
|
+
clone.environment = {benchmarkIndex: 0};
|
|
1546
|
+
}
|
|
1547
|
+
if (!clone.configSettings.screenEmulation) {
|
|
1548
|
+
// @ts-expect-error
|
|
1549
|
+
clone.configSettings.screenEmulation = {};
|
|
1550
|
+
}
|
|
1551
|
+
if (!clone.i18n) {
|
|
1552
|
+
// @ts-expect-error
|
|
1553
|
+
clone.i18n = {};
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// In 10.0, full-page-screenshot became a top-level property on the LHR.
|
|
1557
|
+
if (clone.audits['full-page-screenshot']) {
|
|
1558
|
+
const details = /** @type {LH.Result.FullPageScreenshot=} */ (
|
|
1559
|
+
clone.audits['full-page-screenshot'].details);
|
|
1560
|
+
if (details) {
|
|
1561
|
+
clone.fullPageScreenshot = {
|
|
1562
|
+
screenshot: details.screenshot,
|
|
1563
|
+
nodes: details.nodes,
|
|
1564
|
+
};
|
|
1565
|
+
} else {
|
|
1566
|
+
clone.fullPageScreenshot = null;
|
|
1567
|
+
}
|
|
1568
|
+
delete clone.audits['full-page-screenshot'];
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
return clone;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
/**
|
|
1575
|
+
* @param {LH.Result['configSettings']} settings
|
|
1576
|
+
* @return {!{deviceEmulation: string, screenEmulation?: string, networkThrottling: string, cpuThrottling: string, summary: string}}
|
|
1577
|
+
*/
|
|
1578
|
+
static getEmulationDescriptions(settings) {
|
|
1579
|
+
let cpuThrottling;
|
|
1580
|
+
let networkThrottling;
|
|
1581
|
+
let summary;
|
|
1582
|
+
|
|
1583
|
+
const throttling = settings.throttling;
|
|
1584
|
+
const i18n = Globals.i18n;
|
|
1585
|
+
const strings = Globals.strings;
|
|
1586
|
+
|
|
1587
|
+
switch (settings.throttlingMethod) {
|
|
1588
|
+
case 'provided':
|
|
1589
|
+
summary = networkThrottling = cpuThrottling = strings.throttlingProvided;
|
|
1590
|
+
break;
|
|
1591
|
+
case 'devtools': {
|
|
1592
|
+
const {cpuSlowdownMultiplier, requestLatencyMs} = throttling;
|
|
1593
|
+
// eslint-disable-next-line max-len
|
|
1594
|
+
cpuThrottling = `${i18n.formatNumber(cpuSlowdownMultiplier)}x slowdown (DevTools)`;
|
|
1595
|
+
networkThrottling = `${i18n.formatMilliseconds(requestLatencyMs)} HTTP RTT, ` +
|
|
1596
|
+
`${i18n.formatKbps(throttling.downloadThroughputKbps)} down, ` +
|
|
1597
|
+
`${i18n.formatKbps(throttling.uploadThroughputKbps)} up (DevTools)`;
|
|
1598
|
+
|
|
1599
|
+
const isSlow4G = () => {
|
|
1600
|
+
return requestLatencyMs === 150 * 3.75 &&
|
|
1601
|
+
throttling.downloadThroughputKbps === 1.6 * 1024 * 0.9 &&
|
|
1602
|
+
throttling.uploadThroughputKbps === 750 * 0.9;
|
|
1603
|
+
};
|
|
1604
|
+
summary = isSlow4G() ? strings.runtimeSlow4g : strings.runtimeCustom;
|
|
1605
|
+
break;
|
|
1606
|
+
}
|
|
1607
|
+
case 'simulate': {
|
|
1608
|
+
const {cpuSlowdownMultiplier, rttMs, throughputKbps} = throttling;
|
|
1609
|
+
// eslint-disable-next-line max-len
|
|
1610
|
+
cpuThrottling = `${i18n.formatNumber(cpuSlowdownMultiplier)}x slowdown (Simulated)`;
|
|
1611
|
+
networkThrottling = `${i18n.formatMilliseconds(rttMs)} TCP RTT, ` +
|
|
1612
|
+
`${i18n.formatKbps(throughputKbps)} throughput (Simulated)`;
|
|
1613
|
+
|
|
1614
|
+
const isSlow4G = () => {
|
|
1615
|
+
return rttMs === 150 && throughputKbps === 1.6 * 1024;
|
|
1616
|
+
};
|
|
1617
|
+
summary = isSlow4G() ?
|
|
1618
|
+
strings.runtimeSlow4g : strings.runtimeCustom;
|
|
1619
|
+
break;
|
|
1620
|
+
}
|
|
1621
|
+
default:
|
|
1622
|
+
summary = cpuThrottling = networkThrottling = strings.runtimeUnknown;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// devtools-entry.js always sets `screenEmulation.disabled` when using mobile emulation,
|
|
1626
|
+
// because we handle the emulation outside of Lighthouse. Since the screen truly is emulated
|
|
1627
|
+
// as a mobile device, ignore `.disabled` in devtools and just check the form factor
|
|
1628
|
+
const isScreenEmulationDisabled = settings.channel === 'devtools' ?
|
|
1629
|
+
false :
|
|
1630
|
+
settings.screenEmulation.disabled;
|
|
1631
|
+
const isScreenEmulationMobile = settings.channel === 'devtools' ?
|
|
1632
|
+
settings.formFactor === 'mobile' :
|
|
1633
|
+
settings.screenEmulation.mobile;
|
|
1634
|
+
|
|
1635
|
+
let deviceEmulation = strings.runtimeMobileEmulation;
|
|
1636
|
+
if (isScreenEmulationDisabled) {
|
|
1637
|
+
deviceEmulation = strings.runtimeNoEmulation;
|
|
1638
|
+
} else if (!isScreenEmulationMobile) {
|
|
1639
|
+
deviceEmulation = strings.runtimeDesktopEmulation;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
const screenEmulation = isScreenEmulationDisabled ?
|
|
1643
|
+
undefined :
|
|
1644
|
+
// eslint-disable-next-line max-len
|
|
1645
|
+
`${settings.screenEmulation.width}x${settings.screenEmulation.height}, DPR ${settings.screenEmulation.deviceScaleFactor}`;
|
|
1646
|
+
|
|
1647
|
+
return {
|
|
1648
|
+
deviceEmulation,
|
|
1649
|
+
screenEmulation,
|
|
1650
|
+
cpuThrottling,
|
|
1651
|
+
networkThrottling,
|
|
1652
|
+
summary,
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
/**
|
|
1657
|
+
* Used to determine if the "passed" for the purposes of showing up in the "failed" or "passed"
|
|
1658
|
+
* sections of the report.
|
|
1659
|
+
*
|
|
1660
|
+
* @param {{score: (number|null), scoreDisplayMode: string}} audit
|
|
1661
|
+
* @return {boolean}
|
|
1662
|
+
*/
|
|
1663
|
+
static showAsPassed(audit) {
|
|
1664
|
+
switch (audit.scoreDisplayMode) {
|
|
1665
|
+
case 'manual':
|
|
1666
|
+
case 'notApplicable':
|
|
1667
|
+
return true;
|
|
1668
|
+
case 'error':
|
|
1669
|
+
case 'informative':
|
|
1670
|
+
return false;
|
|
1671
|
+
case 'numeric':
|
|
1672
|
+
case 'binary':
|
|
1673
|
+
default:
|
|
1674
|
+
return Number(audit.score) >= RATINGS.PASS.minScore;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
/**
|
|
1679
|
+
* Convert a score to a rating label.
|
|
1680
|
+
* TODO: Return `'error'` for `score === null && !scoreDisplayMode`.
|
|
1681
|
+
*
|
|
1682
|
+
* @param {number|null} score
|
|
1683
|
+
* @param {string=} scoreDisplayMode
|
|
1684
|
+
* @return {string}
|
|
1685
|
+
*/
|
|
1686
|
+
static calculateRating(score, scoreDisplayMode) {
|
|
1687
|
+
// Handle edge cases first, manual and not applicable receive 'pass', errored audits receive 'error'
|
|
1688
|
+
if (scoreDisplayMode === 'manual' || scoreDisplayMode === 'notApplicable') {
|
|
1689
|
+
return RATINGS.PASS.label;
|
|
1690
|
+
} else if (scoreDisplayMode === 'error') {
|
|
1691
|
+
return RATINGS.ERROR.label;
|
|
1692
|
+
} else if (score === null) {
|
|
1693
|
+
return RATINGS.FAIL.label;
|
|
1694
|
+
}
|
|
1733
1695
|
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
if (blob.type !== 'text/html' && blob.type !== 'application/json') {
|
|
1741
|
-
throw new Error('Unsupported blob type');
|
|
1696
|
+
// At this point, we're rating a standard binary/numeric audit
|
|
1697
|
+
let rating = RATINGS.FAIL.label;
|
|
1698
|
+
if (score >= RATINGS.PASS.minScore) {
|
|
1699
|
+
rating = RATINGS.PASS.label;
|
|
1700
|
+
} else if (score >= RATINGS.AVERAGE.minScore) {
|
|
1701
|
+
rating = RATINGS.AVERAGE.label;
|
|
1742
1702
|
}
|
|
1743
|
-
|
|
1744
|
-
elem.href = href;
|
|
1703
|
+
return rating;
|
|
1745
1704
|
}
|
|
1746
1705
|
|
|
1747
1706
|
/**
|
|
1748
|
-
* @param {
|
|
1749
|
-
* @return {Element}
|
|
1707
|
+
* @param {LH.ReportResult.Category} category
|
|
1750
1708
|
*/
|
|
1751
|
-
|
|
1752
|
-
|
|
1709
|
+
static calculateCategoryFraction(category) {
|
|
1710
|
+
let numPassableAudits = 0;
|
|
1711
|
+
let numPassed = 0;
|
|
1712
|
+
let numInformative = 0;
|
|
1713
|
+
let totalWeight = 0;
|
|
1714
|
+
for (const auditRef of category.auditRefs) {
|
|
1715
|
+
const auditPassed = ReportUtils.showAsPassed(auditRef.result);
|
|
1753
1716
|
|
|
1754
|
-
|
|
1755
|
-
if (
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
} else {
|
|
1760
|
-
|
|
1717
|
+
// Don't count the audit if it's manual, N/A, or isn't displayed.
|
|
1718
|
+
if (auditRef.group === 'hidden' ||
|
|
1719
|
+
auditRef.result.scoreDisplayMode === 'manual' ||
|
|
1720
|
+
auditRef.result.scoreDisplayMode === 'notApplicable') {
|
|
1721
|
+
continue;
|
|
1722
|
+
} else if (auditRef.result.scoreDisplayMode === 'informative') {
|
|
1723
|
+
if (!auditPassed) {
|
|
1724
|
+
++numInformative;
|
|
1725
|
+
}
|
|
1726
|
+
continue;
|
|
1761
1727
|
}
|
|
1762
|
-
}
|
|
1763
1728
|
|
|
1764
|
-
|
|
1729
|
+
++numPassableAudits;
|
|
1730
|
+
totalWeight += auditRef.weight;
|
|
1731
|
+
if (auditPassed) numPassed++;
|
|
1732
|
+
}
|
|
1733
|
+
return {numPassed, numPassableAudits, numInformative, totalWeight};
|
|
1765
1734
|
}
|
|
1766
1735
|
|
|
1767
1736
|
/**
|
|
1768
|
-
*
|
|
1769
|
-
* @param {string} lighthouseChannel
|
|
1737
|
+
* @param {string} categoryId
|
|
1770
1738
|
*/
|
|
1771
|
-
|
|
1772
|
-
|
|
1739
|
+
static isPluginCategory(categoryId) {
|
|
1740
|
+
return categoryId.startsWith('lighthouse-plugin-');
|
|
1773
1741
|
}
|
|
1774
1742
|
|
|
1775
1743
|
/**
|
|
1776
|
-
*
|
|
1777
|
-
* for all scoping, because a document can have multiple reports within it.
|
|
1778
|
-
* @return {Document}
|
|
1744
|
+
* @param {LH.Result.GatherMode} gatherMode
|
|
1779
1745
|
*/
|
|
1780
|
-
|
|
1781
|
-
return
|
|
1746
|
+
static shouldDisplayAsFraction(gatherMode) {
|
|
1747
|
+
return gatherMode === 'timespan' || gatherMode === 'snapshot';
|
|
1782
1748
|
}
|
|
1749
|
+
}
|
|
1783
1750
|
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1751
|
+
/**
|
|
1752
|
+
* Report-renderer-specific strings.
|
|
1753
|
+
*/
|
|
1754
|
+
const UIStrings = {
|
|
1755
|
+
/** Disclaimer shown to users below the metric values (First Contentful Paint, Time to Interactive, etc) to warn them that the numbers they see will likely change slightly the next time they run Lighthouse. */
|
|
1756
|
+
varianceDisclaimer: 'Values are estimated and may vary. The [performance score is calculated](https://developer.chrome.com/docs/lighthouse/performance/performance-scoring/) directly from these metrics.',
|
|
1757
|
+
/** Text link pointing to an interactive calculator that explains Lighthouse scoring. The link text should be fairly short. */
|
|
1758
|
+
calculatorLink: 'See calculator.',
|
|
1759
|
+
/** Label preceding a radio control for filtering the list of audits. The radio choices are various performance metrics (FCP, LCP, TBT), and if chosen, the audits in the report are hidden if they are not relevant to the selected metric. */
|
|
1760
|
+
showRelevantAudits: 'Show audits relevant to:',
|
|
1761
|
+
/** Column heading label for the listing of opportunity audits. Each audit title represents an opportunity. There are only 2 columns, so no strict character limit. */
|
|
1762
|
+
opportunityResourceColumnLabel: 'Opportunity',
|
|
1763
|
+
/** Column heading label for the estimated page load savings of opportunity audits. Estimated Savings is the total amount of time (in seconds) that Lighthouse computed could be reduced from the total page load time, if the suggested action is taken. There are only 2 columns, so no strict character limit. */
|
|
1764
|
+
opportunitySavingsColumnLabel: 'Estimated Savings',
|
|
1791
1765
|
|
|
1792
|
-
/**
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1766
|
+
/** An error string displayed next to a particular audit when it has errored, but not provided any specific error message. */
|
|
1767
|
+
errorMissingAuditInfo: 'Report error: no audit information',
|
|
1768
|
+
/** A label, shown next to an audit title or metric title, indicating that there was an error computing it. The user can hover on the label to reveal a tooltip with the extended error message. Translation should be short (< 20 characters). */
|
|
1769
|
+
errorLabel: 'Error!',
|
|
1770
|
+
/** This label is shown above a bulleted list of warnings. It is shown directly below an audit that produced warnings. Warnings describe situations the user should be aware of, as Lighthouse was unable to complete all the work required on this audit. For example, The 'Unable to decode image (biglogo.jpg)' warning may show up below an image encoding audit. */
|
|
1771
|
+
warningHeader: 'Warnings: ',
|
|
1772
|
+
/** Section heading shown above a list of passed audits that contain warnings. Audits under this section do not negatively impact the score, but Lighthouse has generated some potentially actionable suggestions that should be reviewed. This section is expanded by default and displays after the failing audits. */
|
|
1773
|
+
warningAuditsGroupTitle: 'Passed audits but with warnings',
|
|
1774
|
+
/** Section heading shown above a list of audits that are passing. 'Passed' here refers to a passing grade. This section is collapsed by default, as the user should be focusing on the failed audits instead. Users can click this heading to reveal the list. */
|
|
1775
|
+
passedAuditsGroupTitle: 'Passed audits',
|
|
1776
|
+
/** Section heading shown above a list of audits that do not apply to the page. For example, if an audit is 'Are images optimized?', but the page has no images on it, the audit will be marked as not applicable. This is neither passing or failing. This section is collapsed by default, as the user should be focusing on the failed audits instead. Users can click this heading to reveal the list. */
|
|
1777
|
+
notApplicableAuditsGroupTitle: 'Not applicable',
|
|
1778
|
+
/** Section heading shown above a list of audits that were not computed by Lighthouse. They serve as a list of suggestions for the user to go and manually check. For example, Lighthouse can't automate testing cross-browser compatibility, so that is listed within this section, so the user is reminded to test it themselves. This section is collapsed by default, as the user should be focusing on the failed audits instead. Users can click this heading to reveal the list. */
|
|
1779
|
+
manualAuditsGroupTitle: 'Additional items to manually check',
|
|
1805
1780
|
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
// e.g. HTMLAnchorElement and SVGAElement. See https://github.com/GoogleChrome/lighthouse/issues/12011
|
|
1809
|
-
return /** @type {ParseSelector<T>} */ (result);
|
|
1810
|
-
}
|
|
1781
|
+
/** Label shown preceding any important warnings that may have invalidated the entire report. For example, if the user has Chrome extensions installed, they may add enough performance overhead that Lighthouse's performance metrics are unreliable. If shown, this will be displayed at the top of the report UI. */
|
|
1782
|
+
toplevelWarningsMessage: 'There were issues affecting this run of Lighthouse:',
|
|
1811
1783
|
|
|
1812
|
-
/**
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
* @param {ParentNode} context
|
|
1817
|
-
*/
|
|
1818
|
-
findAll(query, context) {
|
|
1819
|
-
const elements = Array.from(context.querySelectorAll(query));
|
|
1820
|
-
return elements;
|
|
1821
|
-
}
|
|
1784
|
+
/** String of text shown in a graphical representation of the flow of network requests for the web page. This label represents the initial network request that fetches an HTML page. This navigation may be redirected (eg. Initial navigation to http://example.com redirects to https://www.example.com). */
|
|
1785
|
+
crcInitialNavigation: 'Initial Navigation',
|
|
1786
|
+
/** Label of value shown in the summary of critical request chains. Refers to the total amount of time (milliseconds) of the longest critical path chain/sequence of network requests. Example value: 2310 ms */
|
|
1787
|
+
crcLongestDurationLabel: 'Maximum critical path latency:',
|
|
1822
1788
|
|
|
1823
|
-
/**
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
* @param {*=} detail Custom data to include.
|
|
1828
|
-
*/
|
|
1829
|
-
fireEventOn(name, target = this._document, detail) {
|
|
1830
|
-
const event = new CustomEvent(name, detail ? {detail} : undefined);
|
|
1831
|
-
target.dispatchEvent(event);
|
|
1832
|
-
}
|
|
1789
|
+
/** Label for button that shows all lines of the snippet when clicked */
|
|
1790
|
+
snippetExpandButtonLabel: 'Expand snippet',
|
|
1791
|
+
/** Label for button that only shows a few lines of the snippet when clicked */
|
|
1792
|
+
snippetCollapseButtonLabel: 'Collapse snippet',
|
|
1833
1793
|
|
|
1834
|
-
/**
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
*/
|
|
1839
|
-
saveFile(blob, filename) {
|
|
1840
|
-
const a = this.createElement('a');
|
|
1841
|
-
a.download = filename;
|
|
1842
|
-
this.safelySetBlobHref(a, blob);
|
|
1843
|
-
this._document.body.append(a); // Firefox requires anchor to be in the DOM.
|
|
1844
|
-
a.click();
|
|
1794
|
+
/** Explanation shown to users below performance results to inform them that the test was done with a 4G network connection and to warn them that the numbers they see will likely change slightly the next time they run Lighthouse. 'Lighthouse' becomes link text to additional documentation. */
|
|
1795
|
+
lsPerformanceCategoryDescription: '[Lighthouse](https://developers.google.com/web/tools/lighthouse/) analysis of the current page on an emulated mobile network. Values are estimated and may vary.',
|
|
1796
|
+
/** Title of the lab data section of the Performance category. Within this section are various speed metrics which quantify the pageload performance into values presented in seconds and milliseconds. "Lab" is an abbreviated form of "laboratory", and refers to the fact that the data is from a controlled test of a website, not measurements from real users visiting that site. */
|
|
1797
|
+
labDataTitle: 'Lab Data',
|
|
1845
1798
|
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1799
|
+
/** This label is for a checkbox above a table of items loaded by a web page. The checkbox is used to show or hide third-party (or "3rd-party") resources in the table, where "third-party resources" refers to items loaded by a web page from URLs that aren't controlled by the owner of the web page. */
|
|
1800
|
+
thirdPartyResourcesLabel: 'Show 3rd-party resources',
|
|
1801
|
+
/** This label is for a button that opens a new tab to a webapp called "Treemap", which is a nested visual representation of a heierarchy of data related to the reports (script bytes and coverage, resource breakdown, etc.) */
|
|
1802
|
+
viewTreemapLabel: 'View Treemap',
|
|
1803
|
+
/** This label is for a button that will show the user a trace of the page. */
|
|
1804
|
+
viewTraceLabel: 'View Trace',
|
|
1805
|
+
/** This label is for a button that will show the user a trace of the page. */
|
|
1806
|
+
viewOriginalTraceLabel: 'View Original Trace',
|
|
1807
|
+
|
|
1808
|
+
/** Option in a dropdown menu that opens a small, summary report in a print dialog. */
|
|
1809
|
+
dropdownPrintSummary: 'Print Summary',
|
|
1810
|
+
/** Option in a dropdown menu that opens a full Lighthouse report in a print dialog. */
|
|
1811
|
+
dropdownPrintExpanded: 'Print Expanded',
|
|
1812
|
+
/** Option in a dropdown menu that copies the Lighthouse JSON object to the system clipboard. */
|
|
1813
|
+
dropdownCopyJSON: 'Copy JSON',
|
|
1814
|
+
/** Option in a dropdown menu that saves the Lighthouse report HTML locally to the system as a '.html' file. */
|
|
1815
|
+
dropdownSaveHTML: 'Save as HTML',
|
|
1816
|
+
/** Option in a dropdown menu that saves the Lighthouse JSON object to the local system as a '.json' file. */
|
|
1817
|
+
dropdownSaveJSON: 'Save as JSON',
|
|
1818
|
+
/** Option in a dropdown menu that opens the current report in the Lighthouse Viewer Application. */
|
|
1819
|
+
dropdownViewer: 'Open in Viewer',
|
|
1820
|
+
/** Option in a dropdown menu that saves the current report as a new GitHub Gist. */
|
|
1821
|
+
dropdownSaveGist: 'Save as Gist',
|
|
1822
|
+
/** Option in a dropdown menu that toggles the themeing of the report between Light(default) and Dark themes. */
|
|
1823
|
+
dropdownDarkTheme: 'Toggle Dark Theme',
|
|
1824
|
+
|
|
1825
|
+
/** Label for a row in a table that describes the kind of device that was emulated for the Lighthouse run. Example values for row elements: 'No Emulation', 'Emulated Desktop', etc. */
|
|
1826
|
+
runtimeSettingsDevice: 'Device',
|
|
1827
|
+
/** Label for a row in a table that describes the network throttling conditions that were used during a Lighthouse run, if any. */
|
|
1828
|
+
runtimeSettingsNetworkThrottling: 'Network throttling',
|
|
1829
|
+
/** Label for a row in a table that describes the CPU throttling conditions that were used during a Lighthouse run, if any.*/
|
|
1830
|
+
runtimeSettingsCPUThrottling: 'CPU throttling',
|
|
1831
|
+
/** Label for a row in a table that shows the User Agent that was used to send out all network requests during the Lighthouse run. */
|
|
1832
|
+
runtimeSettingsUANetwork: 'User agent (network)',
|
|
1833
|
+
/** Label for a row in a table that shows the estimated CPU power of the machine running Lighthouse. Example row values: 532, 1492, 783. */
|
|
1834
|
+
runtimeSettingsBenchmark: 'Unthrottled CPU/Memory Power',
|
|
1835
|
+
/** Label for a row in a table that shows the version of the Axe library used. Example row values: 2.1.0, 3.2.3 */
|
|
1836
|
+
runtimeSettingsAxeVersion: 'Axe version',
|
|
1837
|
+
/** Label for a row in a table that shows the screen resolution and DPR that was emulated for the Lighthouse run. Example values: '800x600, DPR: 3' */
|
|
1838
|
+
runtimeSettingsScreenEmulation: 'Screen emulation',
|
|
1839
|
+
|
|
1840
|
+
/** Label for button to create an issue against the Lighthouse GitHub project. */
|
|
1841
|
+
footerIssue: 'File an issue',
|
|
1842
|
+
|
|
1843
|
+
/** Descriptive explanation for emulation setting when no device emulation is set. */
|
|
1844
|
+
runtimeNoEmulation: 'No emulation',
|
|
1845
|
+
/** Descriptive explanation for emulation setting when emulating a Moto G4 mobile device. */
|
|
1846
|
+
runtimeMobileEmulation: 'Emulated Moto G4',
|
|
1847
|
+
/** Descriptive explanation for emulation setting when emulating a generic desktop form factor, as opposed to a mobile-device like form factor. */
|
|
1848
|
+
runtimeDesktopEmulation: 'Emulated Desktop',
|
|
1849
|
+
/** Descriptive explanation for a runtime setting that is set to an unknown value. */
|
|
1850
|
+
runtimeUnknown: 'Unknown',
|
|
1851
|
+
/** Descriptive label that this analysis run was from a single pageload of a browser (not a summary of hundreds of loads) */
|
|
1852
|
+
runtimeSingleLoad: 'Single page load',
|
|
1853
|
+
/** Descriptive label that this analysis only considers the initial load of the page, and no interaction beyond when the page had "fully loaded" */
|
|
1854
|
+
runtimeAnalysisWindow: 'Initial page load',
|
|
1855
|
+
/** Descriptive explanation that this analysis run was from a single pageload of a browser, whereas field data often summarizes hundreds+ of page loads */
|
|
1856
|
+
runtimeSingleLoadTooltip: 'This data is taken from a single page load, as opposed to field data summarizing many sessions.', // eslint-disable-line max-len
|
|
1857
|
+
|
|
1858
|
+
/** Descriptive explanation for environment throttling that was provided by the runtime environment instead of provided by Lighthouse throttling. */
|
|
1859
|
+
throttlingProvided: 'Provided by environment',
|
|
1860
|
+
/** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Show' and 'Hide'. */
|
|
1861
|
+
show: 'Show',
|
|
1862
|
+
/** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Show' and 'Hide'. */
|
|
1863
|
+
hide: 'Hide',
|
|
1864
|
+
/** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Expand view' and 'Collapse view'. */
|
|
1865
|
+
expandView: 'Expand view',
|
|
1866
|
+
/** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Expand view' and 'Collapse view'. */
|
|
1867
|
+
collapseView: 'Collapse view',
|
|
1868
|
+
/** Label indicating that Lighthouse throttled the page to emulate a slow 4G network connection. */
|
|
1869
|
+
runtimeSlow4g: 'Slow 4G throttling',
|
|
1870
|
+
/** Label indicating that Lighthouse throttled the page using custom throttling settings. */
|
|
1871
|
+
runtimeCustom: 'Custom throttling',
|
|
1872
|
+
};
|
|
1851
1873
|
|
|
1852
1874
|
/**
|
|
1853
1875
|
* @license
|
|
@@ -1883,10 +1905,10 @@ class CategoryRenderer {
|
|
|
1883
1905
|
*/
|
|
1884
1906
|
get _clumpTitles() {
|
|
1885
1907
|
return {
|
|
1886
|
-
warning:
|
|
1887
|
-
manual:
|
|
1888
|
-
passed:
|
|
1889
|
-
notApplicable:
|
|
1908
|
+
warning: Globals.strings.warningAuditsGroupTitle,
|
|
1909
|
+
manual: Globals.strings.manualAuditsGroupTitle,
|
|
1910
|
+
passed: Globals.strings.passedAuditsGroupTitle,
|
|
1911
|
+
notApplicable: Globals.strings.notApplicableAuditsGroupTitle,
|
|
1890
1912
|
};
|
|
1891
1913
|
}
|
|
1892
1914
|
|
|
@@ -1906,7 +1928,7 @@ class CategoryRenderer {
|
|
|
1906
1928
|
* @return {!Element}
|
|
1907
1929
|
*/
|
|
1908
1930
|
populateAuditValues(audit, component) {
|
|
1909
|
-
const strings =
|
|
1931
|
+
const strings = Globals.strings;
|
|
1910
1932
|
const auditEl = this.dom.find('.lh-audit', component);
|
|
1911
1933
|
auditEl.id = audit.result.id;
|
|
1912
1934
|
const scoreDisplayMode = audit.result.scoreDisplayMode;
|
|
@@ -2029,7 +2051,7 @@ class CategoryRenderer {
|
|
|
2029
2051
|
* @return {!Element}
|
|
2030
2052
|
*/
|
|
2031
2053
|
_setRatingClass(element, score, scoreDisplayMode) {
|
|
2032
|
-
const rating =
|
|
2054
|
+
const rating = ReportUtils.calculateRating(score, scoreDisplayMode);
|
|
2033
2055
|
element.classList.add(`lh-audit--${scoreDisplayMode.toLowerCase()}`);
|
|
2034
2056
|
if (scoreDisplayMode !== 'informative') {
|
|
2035
2057
|
element.classList.add(`lh-audit--${rating}`);
|
|
@@ -2178,8 +2200,8 @@ class CategoryRenderer {
|
|
|
2178
2200
|
el.append(descriptionEl);
|
|
2179
2201
|
}
|
|
2180
2202
|
|
|
2181
|
-
this.dom.find('.lh-clump-toggletext--show', el).textContent =
|
|
2182
|
-
this.dom.find('.lh-clump-toggletext--hide', el).textContent =
|
|
2203
|
+
this.dom.find('.lh-clump-toggletext--show', el).textContent = Globals.strings.show;
|
|
2204
|
+
this.dom.find('.lh-clump-toggletext--hide', el).textContent = Globals.strings.hide;
|
|
2183
2205
|
|
|
2184
2206
|
clumpElement.classList.add(`lh-clump--${clumpId.toLowerCase()}`);
|
|
2185
2207
|
return el;
|
|
@@ -2193,7 +2215,7 @@ class CategoryRenderer {
|
|
|
2193
2215
|
*/
|
|
2194
2216
|
renderCategoryScore(category, groupDefinitions, options) {
|
|
2195
2217
|
let categoryScore;
|
|
2196
|
-
if (options &&
|
|
2218
|
+
if (options && ReportUtils.shouldDisplayAsFraction(options.gatherMode)) {
|
|
2197
2219
|
categoryScore = this.renderCategoryFraction(category);
|
|
2198
2220
|
} else {
|
|
2199
2221
|
categoryScore = this.renderScoreGauge(category, groupDefinitions);
|
|
@@ -2221,7 +2243,7 @@ class CategoryRenderer {
|
|
|
2221
2243
|
const tmpl = this.dom.createComponent('gauge');
|
|
2222
2244
|
const wrapper = this.dom.find('a.lh-gauge__wrapper', tmpl);
|
|
2223
2245
|
|
|
2224
|
-
if (
|
|
2246
|
+
if (ReportUtils.isPluginCategory(category.id)) {
|
|
2225
2247
|
wrapper.classList.add('lh-gauge__wrapper--plugin');
|
|
2226
2248
|
}
|
|
2227
2249
|
|
|
@@ -2237,16 +2259,16 @@ class CategoryRenderer {
|
|
|
2237
2259
|
percentageEl.textContent = scoreOutOf100.toString();
|
|
2238
2260
|
if (category.score === null) {
|
|
2239
2261
|
percentageEl.textContent = '?';
|
|
2240
|
-
percentageEl.title =
|
|
2262
|
+
percentageEl.title = Globals.strings.errorLabel;
|
|
2241
2263
|
}
|
|
2242
2264
|
|
|
2243
2265
|
// Render a numerical score if the category has applicable audits, or no audits whatsoever.
|
|
2244
2266
|
if (category.auditRefs.length === 0 || this.hasApplicableAudits(category)) {
|
|
2245
|
-
wrapper.classList.add(`lh-gauge__wrapper--${
|
|
2267
|
+
wrapper.classList.add(`lh-gauge__wrapper--${ReportUtils.calculateRating(category.score)}`);
|
|
2246
2268
|
} else {
|
|
2247
2269
|
wrapper.classList.add(`lh-gauge__wrapper--not-applicable`);
|
|
2248
2270
|
percentageEl.textContent = '-';
|
|
2249
|
-
percentageEl.title =
|
|
2271
|
+
percentageEl.title = Globals.strings.notApplicableAuditsGroupTitle;
|
|
2250
2272
|
}
|
|
2251
2273
|
|
|
2252
2274
|
this.dom.find('.lh-gauge__label', tmpl).textContent = category.title;
|
|
@@ -2261,7 +2283,8 @@ class CategoryRenderer {
|
|
|
2261
2283
|
const tmpl = this.dom.createComponent('fraction');
|
|
2262
2284
|
const wrapper = this.dom.find('a.lh-fraction__wrapper', tmpl);
|
|
2263
2285
|
|
|
2264
|
-
const {numPassed, numPassableAudits, totalWeight} =
|
|
2286
|
+
const {numPassed, numPassableAudits, totalWeight} =
|
|
2287
|
+
ReportUtils.calculateCategoryFraction(category);
|
|
2265
2288
|
|
|
2266
2289
|
const fraction = numPassed / numPassableAudits;
|
|
2267
2290
|
const content = this.dom.find('.lh-fraction__content', tmpl);
|
|
@@ -2269,7 +2292,7 @@ class CategoryRenderer {
|
|
|
2269
2292
|
text.textContent = `${numPassed}/${numPassableAudits}`;
|
|
2270
2293
|
content.append(text);
|
|
2271
2294
|
|
|
2272
|
-
let rating =
|
|
2295
|
+
let rating = ReportUtils.calculateRating(fraction);
|
|
2273
2296
|
|
|
2274
2297
|
// If none of the available audits can affect the score, a rating isn't useful.
|
|
2275
2298
|
// The flow report should display the fraction with neutral icon and coloring in this case.
|
|
@@ -2334,7 +2357,7 @@ class CategoryRenderer {
|
|
|
2334
2357
|
return scoreDisplayMode;
|
|
2335
2358
|
}
|
|
2336
2359
|
|
|
2337
|
-
if (
|
|
2360
|
+
if (ReportUtils.showAsPassed(auditRef.result)) {
|
|
2338
2361
|
if (this._auditHasWarning(auditRef)) {
|
|
2339
2362
|
return 'warning';
|
|
2340
2363
|
} else {
|
|
@@ -2549,9 +2572,9 @@ class CriticalRequestChainRenderer {
|
|
|
2549
2572
|
const {startTime, endTime, transferSize} = segment.node.request;
|
|
2550
2573
|
const span = dom.createElement('span', 'lh-crc-node__chain-duration');
|
|
2551
2574
|
span.textContent =
|
|
2552
|
-
' - ' +
|
|
2575
|
+
' - ' + Globals.i18n.formatMilliseconds((endTime - startTime) * 1000) + ', ';
|
|
2553
2576
|
const span2 = dom.createElement('span', 'lh-crc-node__chain-duration');
|
|
2554
|
-
span2.textContent =
|
|
2577
|
+
span2.textContent = Globals.i18n.formatBytesToKiB(transferSize, 0.01);
|
|
2555
2578
|
|
|
2556
2579
|
treevalEl.append(span, span2);
|
|
2557
2580
|
}
|
|
@@ -2590,11 +2613,11 @@ class CriticalRequestChainRenderer {
|
|
|
2590
2613
|
const containerEl = dom.find('.lh-crc', tmpl);
|
|
2591
2614
|
|
|
2592
2615
|
// Fill in top summary.
|
|
2593
|
-
dom.find('.lh-crc-initial-nav', tmpl).textContent =
|
|
2616
|
+
dom.find('.lh-crc-initial-nav', tmpl).textContent = Globals.strings.crcInitialNavigation;
|
|
2594
2617
|
dom.find('.lh-crc__longest_duration_label', tmpl).textContent =
|
|
2595
|
-
|
|
2618
|
+
Globals.strings.crcLongestDurationLabel;
|
|
2596
2619
|
dom.find('.lh-crc__longest_duration', tmpl).textContent =
|
|
2597
|
-
|
|
2620
|
+
Globals.i18n.formatMilliseconds(details.longestChain.duration);
|
|
2598
2621
|
|
|
2599
2622
|
// Construct visual tree.
|
|
2600
2623
|
const root = CRCRenderer.initTree(details.chains);
|
|
@@ -2616,6 +2639,18 @@ const CRCRenderer = CriticalRequestChainRenderer;
|
|
|
2616
2639
|
* 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.
|
|
2617
2640
|
*/
|
|
2618
2641
|
|
|
2642
|
+
/** @typedef {import('./dom.js').DOM} DOM */
|
|
2643
|
+
/** @typedef {LH.Audit.Details.Rect} Rect */
|
|
2644
|
+
/** @typedef {{width: number, height: number}} Size */
|
|
2645
|
+
|
|
2646
|
+
/**
|
|
2647
|
+
* @typedef InstallOverlayFeatureParams
|
|
2648
|
+
* @property {DOM} dom
|
|
2649
|
+
* @property {Element} rootEl
|
|
2650
|
+
* @property {Element} overlayContainerEl
|
|
2651
|
+
* @property {LH.Result.FullPageScreenshot} fullPageScreenshot
|
|
2652
|
+
*/
|
|
2653
|
+
|
|
2619
2654
|
/**
|
|
2620
2655
|
* @param {LH.Result.FullPageScreenshot['screenshot']} screenshot
|
|
2621
2656
|
* @param {LH.Audit.Details.Rect} rect
|
|
@@ -2694,7 +2729,7 @@ class ElementScreenshotRenderer {
|
|
|
2694
2729
|
*/
|
|
2695
2730
|
static renderClipPathInScreenshot(dom, maskEl, positionClip, elementRect, elementPreviewSize) {
|
|
2696
2731
|
const clipPathEl = dom.find('clipPath', maskEl);
|
|
2697
|
-
const clipId = `clip-${
|
|
2732
|
+
const clipId = `clip-${Globals.getUniqueSuffix()}`;
|
|
2698
2733
|
clipPathEl.id = clipId;
|
|
2699
2734
|
maskEl.style.clipPath = `url(#${clipId})`;
|
|
2700
2735
|
|
|
@@ -2943,9 +2978,9 @@ class DetailsRenderer {
|
|
|
2943
2978
|
*/
|
|
2944
2979
|
_renderBytes(details) {
|
|
2945
2980
|
// TODO: handle displayUnit once we have something other than 'KiB'
|
|
2946
|
-
const value =
|
|
2981
|
+
const value = Globals.i18n.formatBytesToKiB(details.value, details.granularity || 0.1);
|
|
2947
2982
|
const textEl = this._renderText(value);
|
|
2948
|
-
textEl.title =
|
|
2983
|
+
textEl.title = Globals.i18n.formatBytes(details.value);
|
|
2949
2984
|
return textEl;
|
|
2950
2985
|
}
|
|
2951
2986
|
|
|
@@ -2956,9 +2991,9 @@ class DetailsRenderer {
|
|
|
2956
2991
|
_renderMilliseconds(details) {
|
|
2957
2992
|
let value;
|
|
2958
2993
|
if (details.displayUnit === 'duration') {
|
|
2959
|
-
value =
|
|
2994
|
+
value = Globals.i18n.formatDuration(details.value);
|
|
2960
2995
|
} else {
|
|
2961
|
-
value =
|
|
2996
|
+
value = Globals.i18n.formatMilliseconds(details.value, details.granularity || 10);
|
|
2962
2997
|
}
|
|
2963
2998
|
|
|
2964
2999
|
return this._renderText(value);
|
|
@@ -3037,7 +3072,7 @@ class DetailsRenderer {
|
|
|
3037
3072
|
* @return {Element}
|
|
3038
3073
|
*/
|
|
3039
3074
|
_renderNumeric(details) {
|
|
3040
|
-
const value =
|
|
3075
|
+
const value = Globals.i18n.formatNumber(details.value, details.granularity || 0.1);
|
|
3041
3076
|
const element = this._dom.createElement('div', 'lh-numeric');
|
|
3042
3077
|
element.textContent = value;
|
|
3043
3078
|
return element;
|
|
@@ -3695,7 +3730,7 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
3695
3730
|
const tmpl = this.dom.createComponent('metric');
|
|
3696
3731
|
const element = this.dom.find('.lh-metric', tmpl);
|
|
3697
3732
|
element.id = audit.result.id;
|
|
3698
|
-
const rating =
|
|
3733
|
+
const rating = ReportUtils.calculateRating(audit.result.score, audit.result.scoreDisplayMode);
|
|
3699
3734
|
element.classList.add(`lh-metric--${rating}`);
|
|
3700
3735
|
|
|
3701
3736
|
const titleEl = this.dom.find('.lh-metric__title', tmpl);
|
|
@@ -3743,7 +3778,7 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
3743
3778
|
this.dom.find('span.lh-audit__display-text, div.lh-audit__display-text', element);
|
|
3744
3779
|
const sparklineWidthPct = `${details.overallSavingsMs / scale * 100}%`;
|
|
3745
3780
|
this.dom.find('div.lh-sparkline__bar', element).style.width = sparklineWidthPct;
|
|
3746
|
-
displayEl.textContent =
|
|
3781
|
+
displayEl.textContent = Globals.i18n.formatSeconds(details.overallSavingsMs, 0.01);
|
|
3747
3782
|
|
|
3748
3783
|
// Set [title] tooltips
|
|
3749
3784
|
if (audit.result.displayValue) {
|
|
@@ -3810,9 +3845,9 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
3810
3845
|
});
|
|
3811
3846
|
const paramPairs = [...metricPairs];
|
|
3812
3847
|
|
|
3813
|
-
if (
|
|
3814
|
-
paramPairs.push(['device',
|
|
3815
|
-
paramPairs.push(['version',
|
|
3848
|
+
if (Globals.reportJson) {
|
|
3849
|
+
paramPairs.push(['device', Globals.reportJson.configSettings.formFactor]);
|
|
3850
|
+
paramPairs.push(['version', Globals.reportJson.lighthouseVersion]);
|
|
3816
3851
|
}
|
|
3817
3852
|
|
|
3818
3853
|
const params = new URLSearchParams(paramPairs);
|
|
@@ -3844,7 +3879,7 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
3844
3879
|
* @override
|
|
3845
3880
|
*/
|
|
3846
3881
|
render(category, groups, options) {
|
|
3847
|
-
const strings =
|
|
3882
|
+
const strings = Globals.strings;
|
|
3848
3883
|
const element = this.dom.createElement('div', 'lh-category');
|
|
3849
3884
|
element.id = category.id;
|
|
3850
3885
|
element.append(this.renderCategoryHeader(category, groups, options));
|
|
@@ -3856,7 +3891,7 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
3856
3891
|
|
|
3857
3892
|
// Metric descriptions toggle.
|
|
3858
3893
|
const checkboxEl = this.dom.createElement('input', 'lh-metrics-toggle__input');
|
|
3859
|
-
const checkboxId = `lh-metrics-toggle${
|
|
3894
|
+
const checkboxId = `lh-metrics-toggle${Globals.getUniqueSuffix()}`;
|
|
3860
3895
|
checkboxEl.setAttribute('aria-label', 'Toggle the display of metric descriptions');
|
|
3861
3896
|
checkboxEl.type = 'checkbox';
|
|
3862
3897
|
checkboxEl.id = checkboxId;
|
|
@@ -3866,8 +3901,8 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
3866
3901
|
labelEl.htmlFor = checkboxId;
|
|
3867
3902
|
const showEl = this.dom.createChildOf(labelEl, 'span', 'lh-metrics-toggle__labeltext--show');
|
|
3868
3903
|
const hideEl = this.dom.createChildOf(labelEl, 'span', 'lh-metrics-toggle__labeltext--hide');
|
|
3869
|
-
showEl.textContent =
|
|
3870
|
-
hideEl.textContent =
|
|
3904
|
+
showEl.textContent = Globals.strings.expandView;
|
|
3905
|
+
hideEl.textContent = Globals.strings.collapseView;
|
|
3871
3906
|
|
|
3872
3907
|
const metricsBoxesEl = this.dom.createElement('div', 'lh-metrics-container');
|
|
3873
3908
|
metricsGroupEl.insertBefore(metricsBoxesEl, metricsFooterEl);
|
|
@@ -3906,7 +3941,7 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
3906
3941
|
// Opportunities
|
|
3907
3942
|
const opportunityAudits = category.auditRefs
|
|
3908
3943
|
.filter(audit => this._classifyPerformanceAudit(audit) === 'load-opportunity')
|
|
3909
|
-
.filter(audit => !
|
|
3944
|
+
.filter(audit => !ReportUtils.showAsPassed(audit.result))
|
|
3910
3945
|
.sort((auditA, auditB) => this._getWastedMs(auditB) - this._getWastedMs(auditA));
|
|
3911
3946
|
|
|
3912
3947
|
const filterableMetrics = metricAudits.filter(a => !!a.relevantAudits);
|
|
@@ -3940,7 +3975,7 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
3940
3975
|
// Diagnostics
|
|
3941
3976
|
const diagnosticAudits = category.auditRefs
|
|
3942
3977
|
.filter(audit => this._classifyPerformanceAudit(audit) === 'diagnostic')
|
|
3943
|
-
.filter(audit => !
|
|
3978
|
+
.filter(audit => !ReportUtils.showAsPassed(audit.result))
|
|
3944
3979
|
.sort((a, b) => {
|
|
3945
3980
|
const scoreA = a.result.scoreDisplayMode === 'informative' ? 100 : Number(a.result.score);
|
|
3946
3981
|
const scoreB = b.result.scoreDisplayMode === 'informative' ? 100 : Number(b.result.score);
|
|
@@ -3956,7 +3991,8 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
3956
3991
|
|
|
3957
3992
|
// Passed audits
|
|
3958
3993
|
const passedAudits = category.auditRefs
|
|
3959
|
-
.filter(audit =>
|
|
3994
|
+
.filter(audit =>
|
|
3995
|
+
this._classifyPerformanceAudit(audit) && ReportUtils.showAsPassed(audit.result));
|
|
3960
3996
|
|
|
3961
3997
|
if (!passedAudits.length) return element;
|
|
3962
3998
|
|
|
@@ -3999,7 +4035,7 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
3999
4035
|
renderMetricAuditFilter(filterableMetrics, categoryEl) {
|
|
4000
4036
|
const metricFilterEl = this.dom.createElement('div', 'lh-metricfilter');
|
|
4001
4037
|
const textEl = this.dom.createChildOf(metricFilterEl, 'span', 'lh-metricfilter__text');
|
|
4002
|
-
textEl.textContent =
|
|
4038
|
+
textEl.textContent = Globals.strings.showRelevantAudits;
|
|
4003
4039
|
|
|
4004
4040
|
const filterChoices = /** @type {LH.ReportResult.AuditRef[]} */ ([
|
|
4005
4041
|
({acronym: 'All'}),
|
|
@@ -4008,7 +4044,7 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
|
|
|
4008
4044
|
|
|
4009
4045
|
// Form labels need to reference unique IDs, but multiple reports rendered in the same DOM (eg PSI)
|
|
4010
4046
|
// would mean ID conflict. To address this, we 'scope' these radio inputs with a unique suffix.
|
|
4011
|
-
const uniqSuffix =
|
|
4047
|
+
const uniqSuffix = Globals.getUniqueSuffix();
|
|
4012
4048
|
for (const metric of filterChoices) {
|
|
4013
4049
|
const elemId = `metric-${metric.acronym}-${uniqSuffix}`;
|
|
4014
4050
|
const radioEl = this.dom.createChildOf(metricFilterEl, 'input', 'lh-metricfilter__radio');
|
|
@@ -4159,7 +4195,7 @@ class PwaCategoryRenderer extends CategoryRenderer {
|
|
|
4159
4195
|
|
|
4160
4196
|
// Remove any that have a failing audit.
|
|
4161
4197
|
for (const auditRef of auditRefs) {
|
|
4162
|
-
if (!
|
|
4198
|
+
if (!ReportUtils.showAsPassed(auditRef.result) && auditRef.group) {
|
|
4163
4199
|
uniqueGroupIds.delete(auditRef.group);
|
|
4164
4200
|
}
|
|
4165
4201
|
}
|
|
@@ -4180,7 +4216,7 @@ class PwaCategoryRenderer extends CategoryRenderer {
|
|
|
4180
4216
|
for (const groupId of groupIds) {
|
|
4181
4217
|
const groupAuditRefs = auditRefs.filter(ref => ref.group === groupId);
|
|
4182
4218
|
const auditCount = groupAuditRefs.length;
|
|
4183
|
-
const passedCount = groupAuditRefs.filter(ref =>
|
|
4219
|
+
const passedCount = groupAuditRefs.filter(ref => ReportUtils.showAsPassed(ref.result)).length;
|
|
4184
4220
|
|
|
4185
4221
|
const title = groupDefinitions[groupId].title;
|
|
4186
4222
|
tips.push(`${title}: ${passedCount}/${auditCount}`);
|
|
@@ -4220,7 +4256,7 @@ class PwaCategoryRenderer extends CategoryRenderer {
|
|
|
4220
4256
|
const defsEl = svgRoot.querySelector('defs');
|
|
4221
4257
|
if (!defsEl) return;
|
|
4222
4258
|
|
|
4223
|
-
const idSuffix =
|
|
4259
|
+
const idSuffix = Globals.getUniqueSuffix();
|
|
4224
4260
|
const elementsToUpdate = defsEl.querySelectorAll('[id]');
|
|
4225
4261
|
for (const el of elementsToUpdate) {
|
|
4226
4262
|
const oldId = el.id;
|
|
@@ -4262,7 +4298,6 @@ class PwaCategoryRenderer extends CategoryRenderer {
|
|
|
4262
4298
|
* (this is handled by terser)
|
|
4263
4299
|
*/
|
|
4264
4300
|
|
|
4265
|
-
|
|
4266
4301
|
class ReportRenderer {
|
|
4267
4302
|
/**
|
|
4268
4303
|
* @param {DOM} dom
|
|
@@ -4301,7 +4336,7 @@ class ReportRenderer {
|
|
|
4301
4336
|
|
|
4302
4337
|
this._dom.setLighthouseChannel(lhr.configSettings.channel || 'unknown');
|
|
4303
4338
|
|
|
4304
|
-
const report =
|
|
4339
|
+
const report = ReportUtils.prepareReportResult(lhr);
|
|
4305
4340
|
|
|
4306
4341
|
this._dom.rootEl.textContent = ''; // Remove previous report.
|
|
4307
4342
|
this._dom.rootEl.append(this._renderReport(report));
|
|
@@ -4342,7 +4377,7 @@ class ReportRenderer {
|
|
|
4342
4377
|
|
|
4343
4378
|
this._renderMetaBlock(report, footer);
|
|
4344
4379
|
|
|
4345
|
-
this._dom.find('.lh-footer__version_issue', footer).textContent =
|
|
4380
|
+
this._dom.find('.lh-footer__version_issue', footer).textContent = Globals.strings.footerIssue;
|
|
4346
4381
|
this._dom.find('.lh-footer__version', footer).textContent = report.lighthouseVersion;
|
|
4347
4382
|
return footer;
|
|
4348
4383
|
}
|
|
@@ -4352,7 +4387,7 @@ class ReportRenderer {
|
|
|
4352
4387
|
* @param {DocumentFragment} footer
|
|
4353
4388
|
*/
|
|
4354
4389
|
_renderMetaBlock(report, footer) {
|
|
4355
|
-
const envValues =
|
|
4390
|
+
const envValues = ReportUtils.getEmulationDescriptions(report.configSettings || {});
|
|
4356
4391
|
const match = report.userAgent.match(/(\w*Chrome\/[\d.]+)/); // \w* to include 'HeadlessChrome'
|
|
4357
4392
|
const chromeVer = Array.isArray(match)
|
|
4358
4393
|
? match[1].replace('/', ' ').replace('Chrome', 'Chromium')
|
|
@@ -4362,35 +4397,35 @@ class ReportRenderer {
|
|
|
4362
4397
|
const axeVersion = report.environment.credits?.['axe-core'];
|
|
4363
4398
|
|
|
4364
4399
|
const devicesTooltipTextLines = [
|
|
4365
|
-
`${
|
|
4366
|
-
`${
|
|
4400
|
+
`${Globals.strings.runtimeSettingsBenchmark}: ${benchmarkIndex}`,
|
|
4401
|
+
`${Globals.strings.runtimeSettingsCPUThrottling}: ${envValues.cpuThrottling}`,
|
|
4367
4402
|
];
|
|
4368
4403
|
if (envValues.screenEmulation) {
|
|
4369
4404
|
devicesTooltipTextLines.push(
|
|
4370
|
-
`${
|
|
4405
|
+
`${Globals.strings.runtimeSettingsScreenEmulation}: ${envValues.screenEmulation}`);
|
|
4371
4406
|
}
|
|
4372
4407
|
if (axeVersion) {
|
|
4373
|
-
devicesTooltipTextLines.push(`${
|
|
4408
|
+
devicesTooltipTextLines.push(`${Globals.strings.runtimeSettingsAxeVersion}: ${axeVersion}`);
|
|
4374
4409
|
}
|
|
4375
4410
|
|
|
4376
4411
|
// [CSS icon class, textContent, tooltipText]
|
|
4377
4412
|
const metaItems = [
|
|
4378
4413
|
['date',
|
|
4379
|
-
`Captured at ${
|
|
4414
|
+
`Captured at ${Globals.i18n.formatDateTime(report.fetchTime)}`],
|
|
4380
4415
|
['devices',
|
|
4381
4416
|
`${envValues.deviceEmulation} with Lighthouse ${report.lighthouseVersion}`,
|
|
4382
4417
|
devicesTooltipTextLines.join('\n')],
|
|
4383
4418
|
['samples-one',
|
|
4384
|
-
|
|
4385
|
-
|
|
4419
|
+
Globals.strings.runtimeSingleLoad,
|
|
4420
|
+
Globals.strings.runtimeSingleLoadTooltip],
|
|
4386
4421
|
['stopwatch',
|
|
4387
|
-
|
|
4422
|
+
Globals.strings.runtimeAnalysisWindow],
|
|
4388
4423
|
['networkspeed',
|
|
4389
4424
|
`${envValues.summary}`,
|
|
4390
|
-
`${
|
|
4425
|
+
`${Globals.strings.runtimeSettingsNetworkThrottling}: ${envValues.networkThrottling}`],
|
|
4391
4426
|
['chrome',
|
|
4392
4427
|
`Using ${chromeVer}` + (channel ? ` with ${channel}` : ''),
|
|
4393
|
-
`${
|
|
4428
|
+
`${Globals.strings.runtimeSettingsUANetwork}: "${report.environment.networkUserAgent}"`],
|
|
4394
4429
|
];
|
|
4395
4430
|
|
|
4396
4431
|
const metaItemsEl = this._dom.find('.lh-meta__items', footer);
|
|
@@ -4418,7 +4453,7 @@ class ReportRenderer {
|
|
|
4418
4453
|
|
|
4419
4454
|
const container = this._dom.createComponent('warningsToplevel');
|
|
4420
4455
|
const message = this._dom.find('.lh-warnings__msg', container);
|
|
4421
|
-
message.textContent =
|
|
4456
|
+
message.textContent = Globals.strings.toplevelWarningsMessage;
|
|
4422
4457
|
|
|
4423
4458
|
const warnings = [];
|
|
4424
4459
|
for (const warningString of report.runWarnings) {
|
|
@@ -4472,7 +4507,7 @@ class ReportRenderer {
|
|
|
4472
4507
|
}
|
|
4473
4508
|
|
|
4474
4509
|
|
|
4475
|
-
if (
|
|
4510
|
+
if (ReportUtils.isPluginCategory(category.id)) {
|
|
4476
4511
|
pluginGauges.push(categoryGauge);
|
|
4477
4512
|
} else if (renderer.renderCategoryScore === categoryRenderer.renderCategoryScore) {
|
|
4478
4513
|
// The renderer for default categories is just the default CategoryRenderer.
|
|
@@ -4494,9 +4529,11 @@ class ReportRenderer {
|
|
|
4494
4529
|
* @return {!DocumentFragment}
|
|
4495
4530
|
*/
|
|
4496
4531
|
_renderReport(report) {
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4532
|
+
Globals.apply({
|
|
4533
|
+
providedStrings: report.i18n.rendererFormattedStrings,
|
|
4534
|
+
i18n: new I18nFormatter(report.configSettings.locale),
|
|
4535
|
+
reportJson: report,
|
|
4536
|
+
});
|
|
4500
4537
|
|
|
4501
4538
|
const detailsRenderer = new DetailsRenderer(this._dom, {
|
|
4502
4539
|
fullPageScreenshot: report.fullPageScreenshot ?? undefined,
|
|
@@ -5477,7 +5514,7 @@ class ReportUIFeatures {
|
|
|
5477
5514
|
this.json.audits['script-treemap-data'] && this.json.audits['script-treemap-data'].details;
|
|
5478
5515
|
if (showTreemapApp) {
|
|
5479
5516
|
this.addButton({
|
|
5480
|
-
text:
|
|
5517
|
+
text: Globals.strings.viewTreemapLabel,
|
|
5481
5518
|
icon: 'treemap',
|
|
5482
5519
|
onClick: () => openTreemap(this.json),
|
|
5483
5520
|
});
|
|
@@ -5486,8 +5523,8 @@ class ReportUIFeatures {
|
|
|
5486
5523
|
if (this._opts.onViewTrace) {
|
|
5487
5524
|
this.addButton({
|
|
5488
5525
|
text: lhr.configSettings.throttlingMethod === 'simulate' ?
|
|
5489
|
-
|
|
5490
|
-
|
|
5526
|
+
Globals.strings.viewOriginalTraceLabel :
|
|
5527
|
+
Globals.strings.viewTraceLabel,
|
|
5491
5528
|
onClick: () => this._opts.onViewTrace?.(),
|
|
5492
5529
|
});
|
|
5493
5530
|
}
|
|
@@ -5498,11 +5535,11 @@ class ReportUIFeatures {
|
|
|
5498
5535
|
|
|
5499
5536
|
// Fill in all i18n data.
|
|
5500
5537
|
for (const node of this._dom.findAll('[data-i18n]', this._dom.rootEl)) {
|
|
5501
|
-
// These strings are guaranteed to (at least) have a default English string in
|
|
5538
|
+
// These strings are guaranteed to (at least) have a default English string in UIStrings,
|
|
5502
5539
|
// so this cannot be undefined as long as `report-ui-features.data-i18n` test passes.
|
|
5503
5540
|
const i18nKey = node.getAttribute('data-i18n');
|
|
5504
|
-
const i18nAttr = /** @type {keyof typeof
|
|
5505
|
-
node.textContent =
|
|
5541
|
+
const i18nAttr = /** @type {keyof typeof Globals.strings} */ (i18nKey);
|
|
5542
|
+
node.textContent = Globals.strings[i18nAttr];
|
|
5506
5543
|
}
|
|
5507
5544
|
}
|
|
5508
5545
|
|
|
@@ -5644,7 +5681,7 @@ class ReportUIFeatures {
|
|
|
5644
5681
|
this._dom.find('.lh-3p-filter-count', filterTemplate).textContent =
|
|
5645
5682
|
`${thirdPartyRows.length}`;
|
|
5646
5683
|
this._dom.find('.lh-3p-ui-string', filterTemplate).textContent =
|
|
5647
|
-
|
|
5684
|
+
Globals.strings.thirdPartyResourcesLabel;
|
|
5648
5685
|
|
|
5649
5686
|
const allThirdParty = thirdPartyRows.length === rowEls.length;
|
|
5650
5687
|
const allFirstParty = !thirdPartyRows.length;
|