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
@@ -1,89 +1,16 @@
1
1
  /**
2
- * @license
3
- * Copyright 2017 The Lighthouse Authors. All Rights Reserved.
4
- *
5
- * Licensed under the Apache License, Version 2.0 (the "License");
6
- * you may not use this file except in compliance with the License.
7
- * You may obtain a copy of the License at
8
- *
9
- * http://www.apache.org/licenses/LICENSE-2.0
10
- *
11
- * Unless required by applicable law or agreed to in writing, software
12
- * distributed under the License is distributed on an "AS-IS" BASIS,
13
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- * See the License for the specific language governing permissions and
15
- * limitations under the License.
2
+ * @license Copyright 2023 The Lighthouse Authors. All Rights Reserved.
3
+ * 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
4
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
16
5
  */
17
6
 
18
- /** @typedef {import('./i18n-formatter').I18nFormatter} I18nFormatter */
7
+ import {Util} from '../../shared/util.js';
8
+ import {Globals} from './report-globals.js';
19
9
 
20
- const ELLIPSIS = '\u2026';
21
- const NBSP = '\xa0';
22
- const PASS_THRESHOLD = 0.9;
23
10
  const SCREENSHOT_PREFIX = 'data:image/jpeg;base64,';
11
+ const RATINGS = Util.RATINGS;
24
12
 
25
- const RATINGS = {
26
- PASS: {label: 'pass', minScore: PASS_THRESHOLD},
27
- AVERAGE: {label: 'average', minScore: 0.5},
28
- FAIL: {label: 'fail'},
29
- ERROR: {label: 'error'},
30
- };
31
-
32
- // 25 most used tld plus one domains (aka public suffixes) from http archive.
33
- // @see https://github.com/GoogleChrome/lighthouse/pull/5065#discussion_r191926212
34
- // The canonical list is https://publicsuffix.org/learn/ but we're only using subset to conserve bytes
35
- const listOfTlds = [
36
- 'com', 'co', 'gov', 'edu', 'ac', 'org', 'go', 'gob', 'or', 'net', 'in', 'ne', 'nic', 'gouv',
37
- 'web', 'spb', 'blog', 'jus', 'kiev', 'mil', 'wi', 'qc', 'ca', 'bel', 'on',
38
- ];
39
-
40
- 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
- };
55
- }
56
-
57
- static get PASS_THRESHOLD() {
58
- return PASS_THRESHOLD;
59
- }
60
-
61
- static get MS_DISPLAY_VALUE() {
62
- return `%10d${NBSP}ms`;
63
- }
64
-
65
- /**
66
- * If LHR is older than 10.0 it will not have the `finalDisplayedUrl` property.
67
- * Old LHRs should have the `finalUrl` property which will work fine for the report.
68
- *
69
- * @param {LH.Result} lhr
70
- */
71
- static getFinalDisplayedUrl(lhr) {
72
- if (lhr.finalDisplayedUrl) return lhr.finalDisplayedUrl;
73
- if (lhr.finalUrl) return lhr.finalUrl;
74
- throw new Error('Could not determine final displayed URL');
75
- }
76
-
77
- /**
78
- * If LHR is older than 10.0 it will not have the `mainDocumentUrl` property.
79
- * Old LHRs should have the `finalUrl` property which is the same as `mainDocumentUrl`.
80
- *
81
- * @param {LH.Result} lhr
82
- */
83
- static getMainDocumentUrl(lhr) {
84
- return lhr.mainDocumentUrl || lhr.finalUrl;
85
- }
86
-
13
+ class ReportUtils {
87
14
  /**
88
15
  * Returns a new LHR that's reshaped for slightly better ergonomics within the report rendereer.
89
16
  * Also, sets up the localized UI strings used within renderer and makes changes to old LHRs to be
@@ -106,8 +33,8 @@ class Util {
106
33
  clone.configSettings.formFactor = clone.configSettings.emulatedFormFactor;
107
34
  }
108
35
 
109
- clone.finalDisplayedUrl = this.getFinalDisplayedUrl(clone);
110
- clone.mainDocumentUrl = this.getMainDocumentUrl(clone);
36
+ clone.finalDisplayedUrl = Util.getFinalDisplayedUrl(clone);
37
+ clone.mainDocumentUrl = Util.getMainDocumentUrl(clone);
111
38
 
112
39
  for (const audit of Object.values(clone.audits)) {
113
40
  // Turn 'not-applicable' (LHR <4.0) and 'not_applicable' (older proto versions)
@@ -260,270 +187,6 @@ class Util {
260
187
  return clone;
261
188
  }
262
189
 
263
- /**
264
- * @param {LH.Result} lhr
265
- * @return {LH.Result.FullPageScreenshot=}
266
- */
267
- static getFullPageScreenshot(lhr) {
268
- if (lhr.fullPageScreenshot) {
269
- return lhr.fullPageScreenshot;
270
- }
271
-
272
- // Prior to 10.0.
273
- const details = /** @type {LH.Result.FullPageScreenshot=} */ (
274
- lhr.audits['full-page-screenshot']?.details);
275
- return details;
276
- }
277
-
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
- /**
329
- * Split a string by markdown code spans (enclosed in `backticks`), splitting
330
- * into segments that were enclosed in backticks (marked as `isCode === true`)
331
- * and those that outside the backticks (`isCode === false`).
332
- * @param {string} text
333
- * @return {Array<{isCode: true, text: string}|{isCode: false, text: string}>}
334
- */
335
- static splitMarkdownCodeSpans(text) {
336
- /** @type {Array<{isCode: true, text: string}|{isCode: false, text: string}>} */
337
- const segments = [];
338
-
339
- // Split on backticked code spans.
340
- const parts = text.split(/`(.*?)`/g);
341
- for (let i = 0; i < parts.length; i ++) {
342
- const text = parts[i];
343
-
344
- // Empty strings are an artifact of splitting, not meaningful.
345
- if (!text) continue;
346
-
347
- // Alternates between plain text and code segments.
348
- const isCode = i % 2 !== 0;
349
- segments.push({
350
- isCode,
351
- text,
352
- });
353
- }
354
-
355
- return segments;
356
- }
357
-
358
- /**
359
- * Split a string on markdown links (e.g. [some link](https://...)) into
360
- * segments of plain text that weren't part of a link (marked as
361
- * `isLink === false`), and segments with text content and a URL that did make
362
- * up a link (marked as `isLink === true`).
363
- * @param {string} text
364
- * @return {Array<{isLink: true, text: string, linkHref: string}|{isLink: false, text: string}>}
365
- */
366
- static splitMarkdownLink(text) {
367
- /** @type {Array<{isLink: true, text: string, linkHref: string}|{isLink: false, text: string}>} */
368
- const segments = [];
369
-
370
- const parts = text.split(/\[([^\]]+?)\]\((https?:\/\/.*?)\)/g);
371
- while (parts.length) {
372
- // Shift off the same number of elements as the pre-split and capture groups.
373
- const [preambleText, linkText, linkHref] = parts.splice(0, 3);
374
-
375
- if (preambleText) { // Skip empty text as it's an artifact of splitting, not meaningful.
376
- segments.push({
377
- isLink: false,
378
- text: preambleText,
379
- });
380
- }
381
-
382
- // Append link if there are any.
383
- if (linkText && linkHref) {
384
- segments.push({
385
- isLink: true,
386
- text: linkText,
387
- linkHref,
388
- });
389
- }
390
- }
391
-
392
- return segments;
393
- }
394
-
395
- /**
396
- * @param {URL} parsedUrl
397
- * @param {{numPathParts?: number, preserveQuery?: boolean, preserveHost?: boolean}=} options
398
- * @return {string}
399
- */
400
- static getURLDisplayName(parsedUrl, options) {
401
- // Closure optional properties aren't optional in tsc, so fallback needs undefined values.
402
- options = options || {numPathParts: undefined, preserveQuery: undefined,
403
- preserveHost: undefined};
404
- const numPathParts = options.numPathParts !== undefined ? options.numPathParts : 2;
405
- const preserveQuery = options.preserveQuery !== undefined ? options.preserveQuery : true;
406
- const preserveHost = options.preserveHost || false;
407
-
408
- let name;
409
-
410
- if (parsedUrl.protocol === 'about:' || parsedUrl.protocol === 'data:') {
411
- // Handle 'about:*' and 'data:*' URLs specially since they have no path.
412
- name = parsedUrl.href;
413
- } else {
414
- name = parsedUrl.pathname;
415
- const parts = name.split('/').filter(part => part.length);
416
- if (numPathParts && parts.length > numPathParts) {
417
- name = ELLIPSIS + parts.slice(-1 * numPathParts).join('/');
418
- }
419
-
420
- if (preserveHost) {
421
- name = `${parsedUrl.host}/${name.replace(/^\//, '')}`;
422
- }
423
- if (preserveQuery) {
424
- name = `${name}${parsedUrl.search}`;
425
- }
426
- }
427
-
428
- const MAX_LENGTH = 64;
429
- if (parsedUrl.protocol !== 'data:') {
430
- // Always elide hexadecimal hash
431
- name = name.replace(/([a-f0-9]{7})[a-f0-9]{13}[a-f0-9]*/g, `$1${ELLIPSIS}`);
432
- // Also elide other hash-like mixed-case strings
433
- name = name.replace(/([a-zA-Z0-9-_]{9})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9-_]{10,}/g,
434
- `$1${ELLIPSIS}`);
435
- // Also elide long number sequences
436
- name = name.replace(/(\d{3})\d{6,}/g, `$1${ELLIPSIS}`);
437
- // Merge any adjacent ellipses
438
- name = name.replace(/\u2026+/g, ELLIPSIS);
439
-
440
- // Elide query params first
441
- if (name.length > MAX_LENGTH && name.includes('?')) {
442
- // Try to leave the first query parameter intact
443
- name = name.replace(/\?([^=]*)(=)?.*/, `?$1$2${ELLIPSIS}`);
444
-
445
- // Remove it all if it's still too long
446
- if (name.length > MAX_LENGTH) {
447
- name = name.replace(/\?.*/, `?${ELLIPSIS}`);
448
- }
449
- }
450
- }
451
-
452
- // Elide too long names next
453
- if (name.length > MAX_LENGTH) {
454
- const dotIndex = name.lastIndexOf('.');
455
- if (dotIndex >= 0) {
456
- name = name.slice(0, MAX_LENGTH - 1 - (name.length - dotIndex)) +
457
- // Show file extension
458
- `${ELLIPSIS}${name.slice(dotIndex)}`;
459
- } else {
460
- name = name.slice(0, MAX_LENGTH - 1) + ELLIPSIS;
461
- }
462
- }
463
-
464
- return name;
465
- }
466
-
467
- /**
468
- * Split a URL into a file, hostname and origin for easy display.
469
- * @param {string} url
470
- * @return {{file: string, hostname: string, origin: string}}
471
- */
472
- static parseURL(url) {
473
- const parsedUrl = new URL(url);
474
- return {
475
- file: Util.getURLDisplayName(parsedUrl),
476
- hostname: parsedUrl.hostname,
477
- origin: parsedUrl.origin,
478
- };
479
- }
480
-
481
- /**
482
- * @param {string|URL} value
483
- * @return {!URL}
484
- */
485
- static createOrReturnURL(value) {
486
- if (value instanceof URL) {
487
- return value;
488
- }
489
-
490
- return new URL(value);
491
- }
492
-
493
- /**
494
- * Gets the tld of a domain
495
- *
496
- * @param {string} hostname
497
- * @return {string} tld
498
- */
499
- static getTld(hostname) {
500
- const tlds = hostname.split('.').slice(-2);
501
-
502
- if (!listOfTlds.includes(tlds[0])) {
503
- return `.${tlds[tlds.length - 1]}`;
504
- }
505
-
506
- return `.${tlds.join('.')}`;
507
- }
508
-
509
- /**
510
- * Returns a primary domain for provided hostname (e.g. www.example.com -> example.com).
511
- * @param {string|URL} url hostname or URL object
512
- * @return {string}
513
- */
514
- static getRootDomain(url) {
515
- const hostname = Util.createOrReturnURL(url).hostname;
516
- const tld = Util.getTld(hostname);
517
-
518
- // tld is .com or .co.uk which means we means that length is 1 to big
519
- // .com => 2 & .co.uk => 3
520
- const splitTld = tld.split('.');
521
-
522
- // get TLD + root domain
523
- return hostname.split('.').slice(-splitTld.length).join('.');
524
- }
525
-
526
-
527
190
  /**
528
191
  * @param {LH.Result['configSettings']} settings
529
192
  * @return {!{deviceEmulation: string, screenEmulation?: string, networkThrottling: string, cpuThrottling: string, summary: string}}
@@ -534,44 +197,45 @@ class Util {
534
197
  let summary;
535
198
 
536
199
  const throttling = settings.throttling;
200
+ const i18n = Globals.i18n;
201
+ const strings = Globals.strings;
537
202
 
538
203
  switch (settings.throttlingMethod) {
539
204
  case 'provided':
540
- summary = networkThrottling = cpuThrottling = Util.strings.throttlingProvided;
205
+ summary = networkThrottling = cpuThrottling = strings.throttlingProvided;
541
206
  break;
542
207
  case 'devtools': {
543
208
  const {cpuSlowdownMultiplier, requestLatencyMs} = throttling;
544
209
  // 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)`;
210
+ cpuThrottling = `${i18n.formatNumber(cpuSlowdownMultiplier)}x slowdown (DevTools)`;
211
+ networkThrottling = `${i18n.formatMilliseconds(requestLatencyMs)} HTTP RTT, ` +
212
+ `${i18n.formatKbps(throttling.downloadThroughputKbps)} down, ` +
213
+ `${i18n.formatKbps(throttling.uploadThroughputKbps)} up (DevTools)`;
549
214
 
550
215
  const isSlow4G = () => {
551
216
  return requestLatencyMs === 150 * 3.75 &&
552
217
  throttling.downloadThroughputKbps === 1.6 * 1024 * 0.9 &&
553
218
  throttling.uploadThroughputKbps === 750 * 0.9;
554
219
  };
555
- summary = isSlow4G() ?
556
- Util.strings.runtimeSlow4g : Util.strings.runtimeCustom;
220
+ summary = isSlow4G() ? strings.runtimeSlow4g : strings.runtimeCustom;
557
221
  break;
558
222
  }
559
223
  case 'simulate': {
560
224
  const {cpuSlowdownMultiplier, rttMs, throughputKbps} = throttling;
561
225
  // 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)`;
226
+ cpuThrottling = `${i18n.formatNumber(cpuSlowdownMultiplier)}x slowdown (Simulated)`;
227
+ networkThrottling = `${i18n.formatMilliseconds(rttMs)} TCP RTT, ` +
228
+ `${i18n.formatKbps(throughputKbps)} throughput (Simulated)`;
565
229
 
566
230
  const isSlow4G = () => {
567
231
  return rttMs === 150 && throughputKbps === 1.6 * 1024;
568
232
  };
569
233
  summary = isSlow4G() ?
570
- Util.strings.runtimeSlow4g : Util.strings.runtimeCustom;
234
+ strings.runtimeSlow4g : strings.runtimeCustom;
571
235
  break;
572
236
  }
573
237
  default:
574
- summary = cpuThrottling = networkThrottling = Util.strings.runtimeUnknown;
238
+ summary = cpuThrottling = networkThrottling = strings.runtimeUnknown;
575
239
  }
576
240
 
577
241
  // devtools-entry.js always sets `screenEmulation.disabled` when using mobile emulation,
@@ -584,11 +248,11 @@ class Util {
584
248
  settings.formFactor === 'mobile' :
585
249
  settings.screenEmulation.mobile;
586
250
 
587
- let deviceEmulation = Util.strings.runtimeMobileEmulation;
251
+ let deviceEmulation = strings.runtimeMobileEmulation;
588
252
  if (isScreenEmulationDisabled) {
589
- deviceEmulation = Util.strings.runtimeNoEmulation;
253
+ deviceEmulation = strings.runtimeNoEmulation;
590
254
  } else if (!isScreenEmulationMobile) {
591
- deviceEmulation = Util.strings.runtimeDesktopEmulation;
255
+ deviceEmulation = strings.runtimeDesktopEmulation;
592
256
  }
593
257
 
594
258
  const screenEmulation = isScreenEmulationDisabled ?
@@ -606,59 +270,53 @@ class Util {
606
270
  }
607
271
 
608
272
  /**
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.
273
+ * Used to determine if the "passed" for the purposes of showing up in the "failed" or "passed"
274
+ * sections of the report.
275
+ *
276
+ * @param {{score: (number|null), scoreDisplayMode: string}} audit
277
+ * @return {boolean}
615
278
  */
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);
279
+ static showAsPassed(audit) {
280
+ switch (audit.scoreDisplayMode) {
281
+ case 'manual':
282
+ case 'notApplicable':
283
+ return true;
284
+ case 'error':
285
+ case 'informative':
286
+ return false;
287
+ case 'numeric':
288
+ case 'binary':
289
+ default:
290
+ return Number(audit.score) >= RATINGS.PASS.minScore;
620
291
  }
621
-
622
- const minGapSize = 3;
623
- const lineNumbersToKeep = new Set();
624
- // Sort messages so we can check lineNumbersToKeep to see how big the gap to
625
- // the previous line is.
626
- lineMessages = lineMessages.sort((a, b) => (a.lineNumber || 0) - (b.lineNumber || 0));
627
- lineMessages.forEach(({lineNumber}) => {
628
- let firstSurroundingLineNumber = lineNumber - surroundingLineCount;
629
- let lastSurroundingLineNumber = lineNumber + surroundingLineCount;
630
-
631
- while (firstSurroundingLineNumber < 1) {
632
- // make sure we still show (surroundingLineCount * 2 + 1) lines in total
633
- firstSurroundingLineNumber++;
634
- lastSurroundingLineNumber++;
635
- }
636
- // If only a few lines would be omitted normally then we prefer to include
637
- // extra lines to avoid the tiny gap
638
- if (lineNumbersToKeep.has(firstSurroundingLineNumber - minGapSize - 1)) {
639
- firstSurroundingLineNumber -= minGapSize;
640
- }
641
- for (let i = firstSurroundingLineNumber; i <= lastSurroundingLineNumber; i++) {
642
- const surroundingLineNumber = i;
643
- lineNumbersToKeep.add(surroundingLineNumber);
644
- }
645
- });
646
-
647
- return lines.filter(line => lineNumbersToKeep.has(line.lineNumber));
648
292
  }
649
293
 
650
294
  /**
651
- * @param {string} categoryId
295
+ * Convert a score to a rating label.
296
+ * TODO: Return `'error'` for `score === null && !scoreDisplayMode`.
297
+ *
298
+ * @param {number|null} score
299
+ * @param {string=} scoreDisplayMode
300
+ * @return {string}
652
301
  */
653
- static isPluginCategory(categoryId) {
654
- return categoryId.startsWith('lighthouse-plugin-');
655
- }
302
+ static calculateRating(score, scoreDisplayMode) {
303
+ // Handle edge cases first, manual and not applicable receive 'pass', errored audits receive 'error'
304
+ if (scoreDisplayMode === 'manual' || scoreDisplayMode === 'notApplicable') {
305
+ return RATINGS.PASS.label;
306
+ } else if (scoreDisplayMode === 'error') {
307
+ return RATINGS.ERROR.label;
308
+ } else if (score === null) {
309
+ return RATINGS.FAIL.label;
310
+ }
656
311
 
657
- /**
658
- * @param {LH.Result.GatherMode} gatherMode
659
- */
660
- static shouldDisplayAsFraction(gatherMode) {
661
- return gatherMode === 'timespan' || gatherMode === 'snapshot';
312
+ // At this point, we're rating a standard binary/numeric audit
313
+ let rating = RATINGS.FAIL.label;
314
+ if (score >= RATINGS.PASS.minScore) {
315
+ rating = RATINGS.PASS.label;
316
+ } else if (score >= RATINGS.AVERAGE.minScore) {
317
+ rating = RATINGS.AVERAGE.label;
318
+ }
319
+ return rating;
662
320
  }
663
321
 
664
322
  /**
@@ -670,7 +328,7 @@ class Util {
670
328
  let numInformative = 0;
671
329
  let totalWeight = 0;
672
330
  for (const auditRef of category.auditRefs) {
673
- const auditPassed = Util.showAsPassed(auditRef.result);
331
+ const auditPassed = ReportUtils.showAsPassed(auditRef.result);
674
332
 
675
333
  // Don't count the audit if it's manual, N/A, or isn't displayed.
676
334
  if (auditRef.group === 'hidden' ||
@@ -690,25 +348,21 @@ class Util {
690
348
  }
691
349
  return {numPassed, numPassableAudits, numInformative, totalWeight};
692
350
  }
693
- }
694
351
 
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;
352
+ /**
353
+ * @param {string} categoryId
354
+ */
355
+ static isPluginCategory(categoryId) {
356
+ return categoryId.startsWith('lighthouse-plugin-');
357
+ }
701
358
 
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
- };
359
+ /**
360
+ * @param {LH.Result.GatherMode} gatherMode
361
+ */
362
+ static shouldDisplayAsFraction(gatherMode) {
363
+ return gatherMode === 'timespan' || gatherMode === 'snapshot';
364
+ }
365
+ }
712
366
 
713
367
  /**
714
368
  * Report-renderer-specific strings.
@@ -793,7 +447,7 @@ const UIStrings = {
793
447
  /** 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
448
  runtimeSettingsUANetwork: 'User agent (network)',
795
449
  /** 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',
450
+ runtimeSettingsBenchmark: 'Unthrottled CPU/Memory Power',
797
451
  /** 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
452
  runtimeSettingsAxeVersion: 'Axe version',
799
453
  /** 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' */
@@ -832,10 +486,8 @@ const UIStrings = {
832
486
  /** Label indicating that Lighthouse throttled the page using custom throttling settings. */
833
487
  runtimeCustom: 'Custom throttling',
834
488
  };
835
- Util.UIStrings = UIStrings;
836
- Util.strings = {...UIStrings};
837
489
 
838
490
  export {
839
- Util,
491
+ ReportUtils,
840
492
  UIStrings,
841
493
  };
@@ -7,7 +7,8 @@
7
7
  /** @typedef {import('./details-renderer').DetailsRenderer} DetailsRenderer */
8
8
  /** @typedef {import('./dom').DOM} DOM */
9
9
 
10
- import {Util} from './util.js';
10
+ import {Util} from '../../shared/util.js';
11
+ import {Globals} from './report-globals.js';
11
12
 
12
13
  /** @enum {number} */
13
14
  const LineVisibility = {
@@ -104,7 +105,7 @@ export class SnippetRenderer {
104
105
  const {
105
106
  snippetCollapseButtonLabel,
106
107
  snippetExpandButtonLabel,
107
- } = Util.strings;
108
+ } = Globals.strings;
108
109
  dom.find(
109
110
  '.lh-snippet__btn-label-collapse',
110
111
  header
@@ -8,12 +8,13 @@ import assert from 'assert/strict';
8
8
 
9
9
  import jsdom from 'jsdom';
10
10
 
11
- import {Util} from '../../renderer/util.js';
11
+ import {ReportUtils} from '../../renderer/report-utils.js';
12
12
  import {I18nFormatter} from '../../renderer/i18n-formatter.js';
13
13
  import {DOM} from '../../renderer/dom.js';
14
14
  import {DetailsRenderer} from '../../renderer/details-renderer.js';
15
15
  import {CategoryRenderer} from '../../renderer/category-renderer.js';
16
16
  import {readJson} from '../../../core/test/test-utils.js';
17
+ import {Globals} from '../../renderer/report-globals.js';
17
18
 
18
19
  const sampleResultsOrig = readJson('../../../core/test/results/sample_v2.json', import.meta);
19
20
 
@@ -22,18 +23,22 @@ describe('CategoryRenderer', () => {
22
23
  let sampleResults;
23
24
 
24
25
  before(() => {
25
- Util.i18n = new I18nFormatter('en');
26
+ Globals.apply({
27
+ providedStrings: {},
28
+ i18n: new I18nFormatter('en'),
29
+ reportJson: null,
30
+ });
26
31
 
27
32
  const {document} = new jsdom.JSDOM().window;
28
33
  const dom = new DOM(document);
29
34
  const detailsRenderer = new DetailsRenderer(dom);
30
35
  renderer = new CategoryRenderer(dom, detailsRenderer);
31
36
 
32
- sampleResults = Util.prepareReportResult(sampleResultsOrig);
37
+ sampleResults = ReportUtils.prepareReportResult(sampleResultsOrig);
33
38
  });
34
39
 
35
40
  after(() => {
36
- Util.i18n = undefined;
41
+ Globals.i18n = undefined;
37
42
  });
38
43
 
39
44
  it('renders an audit', () => {