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.
Files changed (109) hide show
  1. package/.codecov.yml +0 -5
  2. package/core/audits/audit.js +1 -1
  3. package/core/audits/byte-efficiency/legacy-javascript.d.ts +1 -0
  4. package/core/audits/byte-efficiency/legacy-javascript.js +7 -3
  5. package/core/audits/dobetterweb/uses-http2.d.ts +5 -2
  6. package/core/audits/dobetterweb/uses-http2.js +12 -7
  7. package/core/audits/third-party-facades.d.ts +3 -2
  8. package/core/audits/third-party-facades.js +12 -9
  9. package/core/audits/third-party-summary.d.ts +7 -6
  10. package/core/audits/third-party-summary.js +16 -14
  11. package/core/audits/valid-source-maps.d.ts +5 -3
  12. package/core/audits/valid-source-maps.js +11 -9
  13. package/core/computed/entity-classification.d.ts +29 -0
  14. package/core/computed/entity-classification.js +102 -0
  15. package/core/computed/resource-summary.d.ts +4 -2
  16. package/core/computed/resource-summary.js +8 -5
  17. package/core/lib/url-utils.js +1 -1
  18. package/core/runner.d.ts +10 -0
  19. package/core/runner.js +48 -0
  20. package/core/util.cjs +1 -1
  21. package/dist/report/bundle.esm.js +724 -687
  22. package/dist/report/flow.js +25 -15
  23. package/dist/report/standalone.js +25 -15
  24. package/flow-report/src/common.tsx +1 -1
  25. package/flow-report/src/i18n/i18n.tsx +17 -16
  26. package/flow-report/src/sidebar/sidebar.tsx +2 -2
  27. package/flow-report/src/summary/category.tsx +6 -6
  28. package/flow-report/src/summary/summary.tsx +2 -2
  29. package/package.json +4 -4
  30. package/report/renderer/category-renderer.js +19 -17
  31. package/report/renderer/crc-details-renderer.js +6 -6
  32. package/report/renderer/details-renderer.js +7 -6
  33. package/report/renderer/dom.js +1 -1
  34. package/report/renderer/element-screenshot-renderer.js +3 -3
  35. package/report/renderer/performance-category-renderer.js +17 -15
  36. package/report/renderer/pwa-category-renderer.js +5 -4
  37. package/report/renderer/report-globals.d.ts +21 -0
  38. package/report/renderer/report-globals.js +49 -0
  39. package/report/renderer/report-renderer.js +22 -20
  40. package/report/renderer/report-ui-features.js +9 -8
  41. package/report/renderer/report-utils.d.ts +116 -0
  42. package/report/renderer/{util.js → report-utils.js} +81 -429
  43. package/report/renderer/snippet-renderer.js +3 -2
  44. package/report/test/renderer/category-renderer-test.js +9 -4
  45. package/report/test/renderer/crc-details-renderer-test.js +7 -3
  46. package/report/test/renderer/details-renderer-test.js +7 -3
  47. package/report/test/renderer/dom-test.js +7 -3
  48. package/report/test/renderer/element-screenshot-renderer-test.js +8 -4
  49. package/report/test/renderer/performance-category-renderer-test.js +14 -9
  50. package/report/test/renderer/pwa-category-renderer-test.js +10 -5
  51. package/report/test/renderer/report-renderer-test.js +2 -2
  52. package/report/test/renderer/report-ui-features-test.js +3 -3
  53. package/report/test/renderer/{util-test.js → report-utils-test.js} +36 -234
  54. package/report/test/renderer/snippet-renderer-test.js +7 -3
  55. package/shared/localization/format.js +5 -2
  56. package/shared/localization/locales/ar-XB.json +50 -50
  57. package/shared/localization/locales/ar.json +50 -50
  58. package/shared/localization/locales/bg.json +50 -50
  59. package/shared/localization/locales/ca.json +50 -50
  60. package/shared/localization/locales/cs.json +50 -50
  61. package/shared/localization/locales/da.json +50 -50
  62. package/shared/localization/locales/de.json +50 -50
  63. package/shared/localization/locales/el.json +50 -50
  64. package/shared/localization/locales/en-GB.json +50 -50
  65. package/shared/localization/locales/en-US.json +54 -54
  66. package/shared/localization/locales/en-XA.json +50 -50
  67. package/shared/localization/locales/en-XL.json +54 -54
  68. package/shared/localization/locales/es-419.json +50 -50
  69. package/shared/localization/locales/es.json +50 -50
  70. package/shared/localization/locales/fi.json +50 -50
  71. package/shared/localization/locales/fil.json +50 -50
  72. package/shared/localization/locales/fr.json +50 -50
  73. package/shared/localization/locales/he.json +50 -50
  74. package/shared/localization/locales/hi.json +50 -50
  75. package/shared/localization/locales/hr.json +50 -50
  76. package/shared/localization/locales/hu.json +50 -50
  77. package/shared/localization/locales/id.json +50 -50
  78. package/shared/localization/locales/it.json +50 -50
  79. package/shared/localization/locales/ja.json +50 -50
  80. package/shared/localization/locales/ko.json +50 -50
  81. package/shared/localization/locales/lt.json +50 -50
  82. package/shared/localization/locales/lv.json +50 -50
  83. package/shared/localization/locales/nl.json +50 -50
  84. package/shared/localization/locales/no.json +50 -50
  85. package/shared/localization/locales/pl.json +50 -50
  86. package/shared/localization/locales/pt-PT.json +50 -50
  87. package/shared/localization/locales/pt.json +50 -50
  88. package/shared/localization/locales/ro.json +50 -50
  89. package/shared/localization/locales/ru.json +50 -50
  90. package/shared/localization/locales/sk.json +50 -50
  91. package/shared/localization/locales/sl.json +50 -50
  92. package/shared/localization/locales/sr-Latn.json +50 -50
  93. package/shared/localization/locales/sr.json +50 -50
  94. package/shared/localization/locales/sv.json +50 -50
  95. package/shared/localization/locales/ta.json +50 -50
  96. package/shared/localization/locales/te.json +50 -50
  97. package/shared/localization/locales/th.json +50 -50
  98. package/shared/localization/locales/tr.json +50 -50
  99. package/shared/localization/locales/uk.json +50 -50
  100. package/shared/localization/locales/vi.json +50 -50
  101. package/shared/localization/locales/zh-HK.json +50 -50
  102. package/shared/localization/locales/zh-TW.json +50 -50
  103. package/shared/localization/locales/zh.json +50 -50
  104. package/shared/test/util-test.js +214 -0
  105. package/shared/util.d.ts +122 -0
  106. package/shared/util.js +332 -0
  107. package/types/artifacts.d.ts +14 -0
  108. package/types/lhr/lhr.d.ts +33 -0
  109. 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('./i18n-formatter').I18nFormatter} I18nFormatter */
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
- /** @type {I18nFormatter} */
42
- // @ts-expect-error: Is set in report renderer.
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
- * @param {LH.Result['configSettings']} settings
529
- * @return {!{deviceEmulation: string, screenEmulation?: string, networkThrottling: string, cpuThrottling: string, summary: string}}
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 getEmulationDescriptions(settings) {
532
- let cpuThrottling;
533
- let networkThrottling;
534
- let summary;
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
- * Only create blob URLs for JSON & HTML
1736
- * @param {HTMLAnchorElement} elem
1737
- * @param {Blob} blob
1738
- */
1739
- safelySetBlobHref(elem, blob) {
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
- const href = URL.createObjectURL(blob);
1744
- elem.href = href;
1703
+ return rating;
1745
1704
  }
1746
1705
 
1747
1706
  /**
1748
- * @param {string} markdownText
1749
- * @return {Element}
1707
+ * @param {LH.ReportResult.Category} category
1750
1708
  */
1751
- convertMarkdownCodeSnippets(markdownText) {
1752
- const element = this.createElement('span');
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
- for (const segment of Util.splitMarkdownCodeSpans(markdownText)) {
1755
- if (segment.isCode) {
1756
- const pre = this.createElement('code');
1757
- pre.textContent = segment.text;
1758
- element.append(pre);
1759
- } else {
1760
- element.append(this._document.createTextNode(segment.text));
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
- return element;
1729
+ ++numPassableAudits;
1730
+ totalWeight += auditRef.weight;
1731
+ if (auditPassed) numPassed++;
1732
+ }
1733
+ return {numPassed, numPassableAudits, numInformative, totalWeight};
1765
1734
  }
1766
1735
 
1767
1736
  /**
1768
- * The channel to use for UTM data when rendering links to the documentation.
1769
- * @param {string} lighthouseChannel
1737
+ * @param {string} categoryId
1770
1738
  */
1771
- setLighthouseChannel(lighthouseChannel) {
1772
- this._lighthouseChannel = lighthouseChannel;
1739
+ static isPluginCategory(categoryId) {
1740
+ return categoryId.startsWith('lighthouse-plugin-');
1773
1741
  }
1774
1742
 
1775
1743
  /**
1776
- * ONLY use if `dom.rootEl` isn't sufficient for your needs. `dom.rootEl` is preferred
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
- document() {
1781
- return this._document;
1746
+ static shouldDisplayAsFraction(gatherMode) {
1747
+ return gatherMode === 'timespan' || gatherMode === 'snapshot';
1782
1748
  }
1749
+ }
1783
1750
 
1784
- /**
1785
- * TODO(paulirish): import and conditionally apply the DevTools frontend subclasses instead of this
1786
- * @return {boolean}
1787
- */
1788
- isDevTools() {
1789
- return !!this._document.querySelector('.lh-devtools');
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
- * Guaranteed context.querySelector. Always returns an element or throws if
1794
- * nothing matches query.
1795
- * @template {string} T
1796
- * @param {T} query
1797
- * @param {ParentNode} context
1798
- * @return {ParseSelector<T>}
1799
- */
1800
- find(query, context) {
1801
- const result = context.querySelector(query);
1802
- if (result === null) {
1803
- throw new Error(`query ${query} not found`);
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
- // Because we control the report layout and templates, use the simpler
1807
- // `typed-query-selector` types that don't require differentiating between
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
- * Helper for context.querySelectorAll. Returns an Array instead of a NodeList.
1814
- * @template {string} T
1815
- * @param {T} query
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
- * Fires a custom DOM event on target.
1825
- * @param {string} name Name of the event.
1826
- * @param {Node=} target DOM node to fire the event on.
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
- * Downloads a file (blob) using a[download].
1836
- * @param {Blob|File} blob The file to save.
1837
- * @param {string} filename
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
- // cleanup.
1847
- this._document.body.removeChild(a);
1848
- setTimeout(() => URL.revokeObjectURL(a.href), 500);
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: Util.strings.warningAuditsGroupTitle,
1887
- manual: Util.strings.manualAuditsGroupTitle,
1888
- passed: Util.strings.passedAuditsGroupTitle,
1889
- notApplicable: Util.strings.notApplicableAuditsGroupTitle,
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 = Util.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 = Util.calculateRating(score, scoreDisplayMode);
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 = Util.strings.show;
2182
- this.dom.find('.lh-clump-toggletext--hide', el).textContent = Util.strings.hide;
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 && Util.shouldDisplayAsFraction(options.gatherMode)) {
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 (Util.isPluginCategory(category.id)) {
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 = Util.strings.errorLabel;
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--${Util.calculateRating(category.score)}`);
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 = Util.strings.notApplicableAuditsGroupTitle;
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} = Util.calculateCategoryFraction(category);
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 = Util.calculateRating(fraction);
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 (Util.showAsPassed(auditRef.result)) {
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
- ' - ' + Util.i18n.formatMilliseconds((endTime - startTime) * 1000) + ', ';
2575
+ ' - ' + Globals.i18n.formatMilliseconds((endTime - startTime) * 1000) + ', ';
2553
2576
  const span2 = dom.createElement('span', 'lh-crc-node__chain-duration');
2554
- span2.textContent = Util.i18n.formatBytesToKiB(transferSize, 0.01);
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 = Util.strings.crcInitialNavigation;
2616
+ dom.find('.lh-crc-initial-nav', tmpl).textContent = Globals.strings.crcInitialNavigation;
2594
2617
  dom.find('.lh-crc__longest_duration_label', tmpl).textContent =
2595
- Util.strings.crcLongestDurationLabel;
2618
+ Globals.strings.crcLongestDurationLabel;
2596
2619
  dom.find('.lh-crc__longest_duration', tmpl).textContent =
2597
- Util.i18n.formatMilliseconds(details.longestChain.duration);
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-${Util.getUniqueSuffix()}`;
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 = Util.i18n.formatBytesToKiB(details.value, details.granularity || 0.1);
2981
+ const value = Globals.i18n.formatBytesToKiB(details.value, details.granularity || 0.1);
2947
2982
  const textEl = this._renderText(value);
2948
- textEl.title = Util.i18n.formatBytes(details.value);
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 = Util.i18n.formatDuration(details.value);
2994
+ value = Globals.i18n.formatDuration(details.value);
2960
2995
  } else {
2961
- value = Util.i18n.formatMilliseconds(details.value, details.granularity || 10);
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 = Util.i18n.formatNumber(details.value, details.granularity || 0.1);
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 = Util.calculateRating(audit.result.score, audit.result.scoreDisplayMode);
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 = Util.i18n.formatSeconds(details.overallSavingsMs, 0.01);
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 (Util.reportJson) {
3814
- paramPairs.push(['device', Util.reportJson.configSettings.formFactor]);
3815
- paramPairs.push(['version', Util.reportJson.lighthouseVersion]);
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 = Util.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${Util.getUniqueSuffix()}`;
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 = Util.strings.expandView;
3870
- hideEl.textContent = Util.strings.collapseView;
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 => !Util.showAsPassed(audit.result))
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 => !Util.showAsPassed(audit.result))
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 => this._classifyPerformanceAudit(audit) && Util.showAsPassed(audit.result));
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 = Util.strings.showRelevantAudits;
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 = Util.getUniqueSuffix();
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 (!Util.showAsPassed(auditRef.result) && auditRef.group) {
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 => Util.showAsPassed(ref.result)).length;
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 = Util.getUniqueSuffix();
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 = Util.prepareReportResult(lhr);
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 = Util.strings.footerIssue;
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 = Util.getEmulationDescriptions(report.configSettings || {});
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
- `${Util.strings.runtimeSettingsBenchmark}: ${benchmarkIndex}`,
4366
- `${Util.strings.runtimeSettingsCPUThrottling}: ${envValues.cpuThrottling}`,
4400
+ `${Globals.strings.runtimeSettingsBenchmark}: ${benchmarkIndex}`,
4401
+ `${Globals.strings.runtimeSettingsCPUThrottling}: ${envValues.cpuThrottling}`,
4367
4402
  ];
4368
4403
  if (envValues.screenEmulation) {
4369
4404
  devicesTooltipTextLines.push(
4370
- `${Util.strings.runtimeSettingsScreenEmulation}: ${envValues.screenEmulation}`);
4405
+ `${Globals.strings.runtimeSettingsScreenEmulation}: ${envValues.screenEmulation}`);
4371
4406
  }
4372
4407
  if (axeVersion) {
4373
- devicesTooltipTextLines.push(`${Util.strings.runtimeSettingsAxeVersion}: ${axeVersion}`);
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 ${Util.i18n.formatDateTime(report.fetchTime)}`],
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
- Util.strings.runtimeSingleLoad,
4385
- Util.strings.runtimeSingleLoadTooltip],
4419
+ Globals.strings.runtimeSingleLoad,
4420
+ Globals.strings.runtimeSingleLoadTooltip],
4386
4421
  ['stopwatch',
4387
- Util.strings.runtimeAnalysisWindow],
4422
+ Globals.strings.runtimeAnalysisWindow],
4388
4423
  ['networkspeed',
4389
4424
  `${envValues.summary}`,
4390
- `${Util.strings.runtimeSettingsNetworkThrottling}: ${envValues.networkThrottling}`],
4425
+ `${Globals.strings.runtimeSettingsNetworkThrottling}: ${envValues.networkThrottling}`],
4391
4426
  ['chrome',
4392
4427
  `Using ${chromeVer}` + (channel ? ` with ${channel}` : ''),
4393
- `${Util.strings.runtimeSettingsUANetwork}: "${report.environment.networkUserAgent}"`],
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 = Util.strings.toplevelWarningsMessage;
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 (Util.isPluginCategory(category.id)) {
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
- Util.applyStrings(report.i18n.rendererFormattedStrings);
4498
- Util.i18n = new I18nFormatter(report.configSettings.locale);
4499
- Util.reportJson = report;
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: Util.strings.viewTreemapLabel,
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
- Util.strings.viewOriginalTraceLabel :
5490
- Util.strings.viewTraceLabel,
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 Util.UIStrings,
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 Util.strings} */ (i18nKey);
5505
- node.textContent = Util.strings[i18nAttr];
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
- Util.strings.thirdPartyResourcesLabel;
5684
+ Globals.strings.thirdPartyResourcesLabel;
5648
5685
 
5649
5686
  const allThirdParty = thirdPartyRows.length === rowEls.length;
5650
5687
  const allFirstParty = !thirdPartyRows.length;