lighthouse 8.6.0 → 9.0.0-dev.20211118

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 (210) hide show
  1. package/changelog.md +121 -0
  2. package/dist/report/bundle.esm.js +6009 -0
  3. package/dist/report/flow.js +98 -25
  4. package/dist/report/standalone.js +41 -35
  5. package/flow-report/.eslintrc.cjs +2 -0
  6. package/flow-report/assets/standalone-flow-template.html +3 -4
  7. package/flow-report/assets/styles.css +108 -31
  8. package/flow-report/src/app.tsx +25 -28
  9. package/flow-report/src/common.tsx +28 -15
  10. package/flow-report/src/header.tsx +12 -13
  11. package/flow-report/src/help-dialog.tsx +3 -4
  12. package/flow-report/src/i18n/i18n.tsx +57 -16
  13. package/flow-report/src/i18n/ui-strings.js +50 -0
  14. package/flow-report/src/icons.tsx +36 -8
  15. package/flow-report/src/sidebar/flow.tsx +3 -3
  16. package/flow-report/src/sidebar/sidebar.tsx +35 -14
  17. package/flow-report/src/summary/category.tsx +97 -24
  18. package/flow-report/src/summary/summary.tsx +21 -14
  19. package/flow-report/src/topbar.tsx +13 -12
  20. package/flow-report/src/util.ts +61 -27
  21. package/flow-report/src/wrappers/category-score.tsx +9 -27
  22. package/flow-report/src/wrappers/markdown.tsx +18 -0
  23. package/flow-report/src/wrappers/report.tsx +26 -37
  24. package/flow-report/standalone-flow.tsx +5 -4
  25. package/flow-report/test/common-test.tsx +32 -17
  26. package/flow-report/test/flow-report-pptr-test.ts +46 -0
  27. package/flow-report/test/header-test.tsx +6 -6
  28. package/flow-report/test/setup/env-setup.ts +11 -5
  29. package/flow-report/test/sidebar/sidebar-test.tsx +43 -1
  30. package/flow-report/test/summary/category-test.tsx +114 -21
  31. package/flow-report/test/summary/summary-test.tsx +4 -7
  32. package/flow-report/test/topbar-test.tsx +15 -14
  33. package/flow-report/test/util-test.tsx +69 -11
  34. package/flow-report/test/wrappers/category-score-test.tsx +84 -0
  35. package/flow-report/test/wrappers/markdown-test.tsx +17 -0
  36. package/flow-report/tsconfig.json +1 -0
  37. package/jest.config.js +4 -2
  38. package/lighthouse-cli/.eslintrc.cjs +2 -0
  39. package/lighthouse-cli/cli-flags.js +1 -1
  40. package/lighthouse-cli/run.js +1 -1
  41. package/lighthouse-cli/test/smokehouse/lighthouse-runners/bundle.js +4 -2
  42. package/lighthouse-cli/test/smokehouse/lighthouse-runners/cli.js +1 -1
  43. package/lighthouse-cli/test/smokehouse/smokehouse.js +1 -1
  44. package/lighthouse-core/audits/accessibility/axe-audit.js +7 -1
  45. package/lighthouse-core/audits/audit.js +1 -1
  46. package/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js +2 -0
  47. package/lighthouse-core/audits/byte-efficiency/modern-image-formats.js +4 -2
  48. package/lighthouse-core/audits/byte-efficiency/offscreen-images.js +11 -10
  49. package/lighthouse-core/audits/byte-efficiency/uses-optimized-images.js +5 -3
  50. package/lighthouse-core/audits/byte-efficiency/uses-responsive-images-snapshot.js +2 -1
  51. package/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js +2 -1
  52. package/lighthouse-core/audits/deprecations.js +33 -11
  53. package/lighthouse-core/audits/dobetterweb/uses-http2.js +27 -11
  54. package/lighthouse-core/audits/errors-in-console.js +0 -2
  55. package/lighthouse-core/audits/image-aspect-ratio.js +5 -5
  56. package/lighthouse-core/audits/image-size-responsive.js +6 -6
  57. package/lighthouse-core/audits/installable-manifest.js +16 -7
  58. package/lighthouse-core/audits/preload-lcp-image.js +7 -5
  59. package/lighthouse-core/audits/script-treemap-data.js +1 -1
  60. package/lighthouse-core/audits/unsized-images.js +2 -3
  61. package/lighthouse-core/computed/image-records.js +4 -3
  62. package/lighthouse-core/computed/metrics/lantern-total-blocking-time.js +3 -4
  63. package/lighthouse-core/computed/metrics/tbt-utils.js +57 -0
  64. package/lighthouse-core/computed/metrics/total-blocking-time.js +3 -53
  65. package/lighthouse-core/computed/resource-summary.js +1 -1
  66. package/lighthouse-core/config/config-helpers.js +21 -10
  67. package/lighthouse-core/config/constants.js +3 -4
  68. package/lighthouse-core/config/default-config.js +61 -71
  69. package/lighthouse-core/fraggle-rock/config/config.js +1 -1
  70. package/lighthouse-core/fraggle-rock/config/default-config.js +4 -5
  71. package/lighthouse-core/fraggle-rock/gather/base-gatherer.js +6 -1
  72. package/lighthouse-core/fraggle-rock/gather/session.js +3 -4
  73. package/lighthouse-core/gather/connections/cri.js +1 -1
  74. package/lighthouse-core/gather/driver/storage.js +0 -1
  75. package/lighthouse-core/gather/driver.js +3 -7
  76. package/lighthouse-core/gather/fetcher.js +4 -7
  77. package/lighthouse-core/gather/gather-runner.js +29 -6
  78. package/lighthouse-core/gather/gatherers/accessibility.js +27 -0
  79. package/lighthouse-core/gather/gatherers/dobetterweb/response-compression.js +1 -0
  80. package/lighthouse-core/gather/gatherers/image-elements.js +0 -3
  81. package/lighthouse-core/gather/gatherers/inspector-issues.js +5 -0
  82. package/lighthouse-core/lib/asset-saver.js +7 -4
  83. package/lighthouse-core/lib/emulation.js +45 -1
  84. package/lighthouse-core/lib/i18n/i18n.js +19 -8
  85. package/lighthouse-core/lib/lh-env.js +2 -0
  86. package/lighthouse-core/lib/page-functions.js +2 -1
  87. package/lighthouse-core/lib/proto-preprocessor.js +14 -2
  88. package/lighthouse-core/lib/statistics.js +26 -39
  89. package/lighthouse-core/lib/tappable-rects.js +8 -15
  90. package/lighthouse-core/lib/url-shim.js +1 -1
  91. package/lighthouse-core/runner.js +1 -1
  92. package/lighthouse-core/util-commonjs.js +65 -46
  93. package/package.json +27 -17
  94. package/readme.md +3 -3
  95. package/report/.eslintrc.cjs +2 -0
  96. package/report/README.md +1 -1
  97. package/report/assets/standalone-template.html +2 -3
  98. package/report/assets/styles.css +362 -256
  99. package/report/assets/templates.html +21 -76
  100. package/report/clients/bundle.js +1 -0
  101. package/report/clients/standalone.js +6 -15
  102. package/report/generator/README.md +1 -1
  103. package/report/renderer/api.js +66 -0
  104. package/report/renderer/category-renderer.js +76 -22
  105. package/report/renderer/components.js +59 -110
  106. package/report/renderer/crc-details-renderer.js +15 -12
  107. package/report/renderer/dom.js +16 -1
  108. package/report/renderer/drop-down-menu.js +2 -2
  109. package/report/renderer/element-screenshot-renderer.js +8 -8
  110. package/report/renderer/features-util.js +1 -1
  111. package/report/renderer/open-tab.js +9 -3
  112. package/report/renderer/performance-category-renderer.js +55 -34
  113. package/report/renderer/pwa-category-renderer.js +0 -10
  114. package/report/renderer/report-renderer.js +92 -44
  115. package/report/renderer/report-ui-features.js +36 -33
  116. package/report/renderer/swap-locale-feature.js +3 -3
  117. package/report/renderer/topbar-features.js +53 -48
  118. package/report/renderer/util.js +66 -46
  119. package/report/test/clients/bundle-test.js +70 -0
  120. package/report/test/renderer/category-renderer-test.js +76 -3
  121. package/report/test/renderer/i18n-test.js +0 -7
  122. package/report/test/renderer/performance-category-renderer-test.js +51 -14
  123. package/report/test/renderer/pwa-category-renderer-test.js +6 -6
  124. package/report/test/renderer/report-renderer-axe-test.js +11 -13
  125. package/report/test/renderer/report-renderer-test.js +52 -22
  126. package/report/test/renderer/report-ui-features-test.js +20 -10
  127. package/report/test/renderer/text-encoding-test.js +1 -1
  128. package/report/test/renderer/util-test.js +35 -31
  129. package/report/test-assets/faux-psi-template.html +3 -11
  130. package/report/test-assets/faux-psi.js +26 -22
  131. package/report/types/html-renderer.d.ts +2 -0
  132. package/report/types/report-renderer.d.ts +28 -0
  133. package/shared/localization/format.js +64 -30
  134. package/shared/localization/locales/ar-XB.json +72 -75
  135. package/shared/localization/locales/ar.json +72 -75
  136. package/shared/localization/locales/bg.json +72 -75
  137. package/shared/localization/locales/ca.json +72 -75
  138. package/shared/localization/locales/cs.json +89 -92
  139. package/shared/localization/locales/da.json +72 -75
  140. package/shared/localization/locales/de.json +89 -92
  141. package/shared/localization/locales/el.json +72 -75
  142. package/shared/localization/locales/en-GB.json +72 -75
  143. package/shared/localization/locales/en-US.json +78 -75
  144. package/shared/localization/locales/en-XA.json +72 -75
  145. package/shared/localization/locales/en-XL.json +78 -75
  146. package/shared/localization/locales/es-419.json +90 -93
  147. package/shared/localization/locales/es.json +90 -93
  148. package/shared/localization/locales/fi.json +72 -75
  149. package/shared/localization/locales/fil.json +72 -75
  150. package/shared/localization/locales/fr.json +89 -92
  151. package/shared/localization/locales/he.json +73 -76
  152. package/shared/localization/locales/hi.json +72 -75
  153. package/shared/localization/locales/hr.json +72 -75
  154. package/shared/localization/locales/hu.json +89 -92
  155. package/shared/localization/locales/id.json +89 -92
  156. package/shared/localization/locales/it.json +89 -92
  157. package/shared/localization/locales/ja.json +89 -92
  158. package/shared/localization/locales/ko.json +89 -92
  159. package/shared/localization/locales/lt.json +89 -92
  160. package/shared/localization/locales/lv.json +95 -98
  161. package/shared/localization/locales/nl.json +89 -92
  162. package/shared/localization/locales/no.json +72 -75
  163. package/shared/localization/locales/pl.json +72 -75
  164. package/shared/localization/locales/pt-PT.json +72 -75
  165. package/shared/localization/locales/pt.json +89 -92
  166. package/shared/localization/locales/ro.json +90 -93
  167. package/shared/localization/locales/ru.json +89 -92
  168. package/shared/localization/locales/sk.json +89 -92
  169. package/shared/localization/locales/sl.json +89 -92
  170. package/shared/localization/locales/sr-Latn.json +89 -92
  171. package/shared/localization/locales/sr.json +89 -92
  172. package/shared/localization/locales/sv.json +72 -75
  173. package/shared/localization/locales/ta.json +89 -92
  174. package/shared/localization/locales/te.json +89 -92
  175. package/shared/localization/locales/th.json +89 -92
  176. package/shared/localization/locales/tr.json +89 -92
  177. package/shared/localization/locales/uk.json +92 -95
  178. package/shared/localization/locales/vi.json +89 -92
  179. package/shared/localization/locales/zh-HK.json +89 -92
  180. package/shared/localization/locales/zh-TW.json +89 -92
  181. package/shared/localization/locales/zh.json +89 -92
  182. package/shared/localization/locales.js +6 -0
  183. package/shared/test/localization/format-test.js +45 -9
  184. package/shared/test/localization/swap-locale-test.js +0 -12
  185. package/third-party/chromium-synchronization/inspector-issueAdded-types-test.js +19 -11
  186. package/third-party/chromium-synchronization/installability-errors-test.js +3 -2
  187. package/third-party/download-content-shell/download-content-shell.js +2 -2
  188. package/third-party/download-content-shell/utils.js +0 -24
  189. package/third-party/snyk/snapshot.json +4 -1
  190. package/tsconfig-all.json +2 -2
  191. package/tsconfig.json +0 -1
  192. package/types/artifacts.d.ts +7 -7
  193. package/types/enquirer.d.ts +3 -1
  194. package/types/lhr/audit-details.d.ts +6 -5
  195. package/types/lhr/flow.d.ts +3 -2
  196. package/types/lhr/lhr.d.ts +1 -1
  197. package/types/node.d.ts +5 -5
  198. package/types/rollup-plugin-postprocess.d.ts +10 -0
  199. package/flow-report/src/wrappers/report-renderer.tsx +0 -46
  200. package/lighthouse-core/audits/dobetterweb/appcache-manifest.js +0 -67
  201. package/lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js +0 -96
  202. package/lighthouse-core/audits/redirects-http.js +0 -59
  203. package/lighthouse-core/gather/gatherers/dobetterweb/appcache.js +0 -34
  204. package/lighthouse-core/gather/gatherers/http-redirect.js +0 -46
  205. package/lighthouse-core/lib/i18n/locales/en-US.ctc.json +0 -6699
  206. package/lighthouse-core/lib/i18n/locales/en-XL.ctc.json +0 -6672
  207. package/report/clients/psi.js +0 -158
  208. package/report/test/clients/psi-test.js +0 -143
  209. package/shared/localization/locales/en-US.ctc.json +0 -6877
  210. package/shared/localization/locales/en-XL.ctc.json +0 -6852
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @license Copyright 2021 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.
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ import fs from 'fs';
10
+
11
+ import jsdom from 'jsdom';
12
+ import {jest} from '@jest/globals';
13
+
14
+ import * as lighthouseRenderer from '../../clients/bundle.js';
15
+ import {LH_ROOT} from '../../../root.js';
16
+
17
+ const sampleResultsStr =
18
+ fs.readFileSync(LH_ROOT + '/lighthouse-core/test/results/sample_v2.json', 'utf-8');
19
+
20
+
21
+ /* eslint-env jest */
22
+
23
+ describe('lighthouseRenderer bundle', () => {
24
+ let document;
25
+ beforeAll(() => {
26
+ global.console.warn = jest.fn();
27
+
28
+ const {window} = new jsdom.JSDOM();
29
+ document = window.document;
30
+
31
+ global.window = global.self = window;
32
+ global.window.requestAnimationFrame = fn => fn();
33
+ // Stub out matchMedia for Node.
34
+ global.self.matchMedia = function() {
35
+ return {
36
+ addListener: function() {},
37
+ };
38
+ };
39
+ global.window.ResizeObserver = class ResizeObserver {
40
+ observe() { }
41
+ unobserve() { }
42
+ };
43
+ });
44
+
45
+ afterAll(() => {
46
+ global.window = global.self = undefined;
47
+ });
48
+
49
+
50
+ it('renders an LHR to DOM', () => {
51
+ const lhr = /** @type {LH.Result} */ JSON.parse(sampleResultsStr);
52
+ const reportContainer = document.body;
53
+ reportContainer.classList.add('lh-vars', 'lh-root');
54
+
55
+ const dom = new lighthouseRenderer.DOM(reportContainer.ownerDocument);
56
+ const renderer = new lighthouseRenderer.ReportRenderer(dom);
57
+ renderer.renderReport(lhr, reportContainer);
58
+ const features = new lighthouseRenderer.ReportUIFeatures(renderer._dom);
59
+ features.initFeatures(lhr);
60
+
61
+
62
+ // Check that the report exists and has some content.
63
+ expect(reportContainer instanceof document.defaultView.Element).toBeTruthy();
64
+ expect(reportContainer.outerHTML.length).toBeGreaterThan(50000);
65
+
66
+ const title = reportContainer.querySelector('.lh-audit-group--metrics')
67
+ .querySelector('.lh-audit-group__title');
68
+ expect(title.textContent).toEqual('Metrics');
69
+ });
70
+ });
@@ -197,8 +197,8 @@ describe('CategoryRenderer', () => {
197
197
  'score shows informative and dash icon');
198
198
 
199
199
  assert.ok(pwaCategory.manualDescription);
200
- const description = categoryDOM
201
- .querySelector('.lh-clump--manual .lh-audit-group__description').textContent;
200
+ const description = categoryDOM.querySelector('.lh-clump--manual').closest('.lh-audit-group')
201
+ .querySelector('.lh-audit-group__description').textContent;
202
202
  // may need to be adjusted if description includes a link at the beginning
203
203
  assert.ok(description.startsWith(pwaCategory.manualDescription.substring(0, 20)),
204
204
  'no manual description');
@@ -409,6 +409,10 @@ describe('CategoryRenderer', () => {
409
409
  const passingRefs = categoryClone.auditRefs.filter(ref => ref.result.score === 1);
410
410
  passingRefs[0].result.warnings = ['Some warning'];
411
411
  passingRefs[1].result.warnings = ['Some warning'];
412
+ // Make one audit n/a
413
+ const audit = categoryClone.auditRefs.find(ref => ref.id === 'themed-omnibox');
414
+ audit.result.scoreDisplayMode = 'notApplicable';
415
+ audit.result.score = null;
412
416
 
413
417
  const elem = renderer.render(categoryClone, sampleResults.categoryGroups);
414
418
  const passedAudits = elem.querySelectorAll('.lh-clump--passed .lh-audit');
@@ -418,7 +422,7 @@ describe('CategoryRenderer', () => {
418
422
  const naAudits = elem.querySelectorAll('.lh-clump--notapplicable .lh-audit');
419
423
 
420
424
  assert.equal(passedAudits.length, 0);
421
- assert.equal(failedAudits.length, 6);
425
+ assert.equal(failedAudits.length, 5);
422
426
  assert.equal(warningAudits.length, 2);
423
427
  assert.equal(manualAudits.length, 3);
424
428
  assert.equal(naAudits.length, 1);
@@ -495,4 +499,73 @@ describe('CategoryRenderer', () => {
495
499
  assert.ok(shouldBeWarning[0].textContent.includes(passingWarning));
496
500
  });
497
501
  });
502
+
503
+ describe('renderCategoryScore', () => {
504
+ it('removes label if omitLabel is true', () => {
505
+ const options = {omitLabel: true};
506
+ const categoryScore = renderer.renderCategoryScore(
507
+ sampleResults.categories.performance,
508
+ {},
509
+ options
510
+ );
511
+ const label = categoryScore.querySelector('.lh-gauge__label,.lh-fraction__label');
512
+ assert.ok(!label);
513
+ });
514
+
515
+ it('uses custom callback if present', () => {
516
+ const options = {
517
+ onPageAnchorRendered: link => {
518
+ link.href = '#index=0&anchor=performance';
519
+ },
520
+ };
521
+ const categoryScore = renderer.renderCategoryScore(
522
+ sampleResults.categories.performance,
523
+ {},
524
+ options
525
+ );
526
+ const link = categoryScore.querySelector('a');
527
+ assert.equal(link.hash, '#index=0&anchor=performance');
528
+ });
529
+ });
530
+
531
+ it('renders audits by weight', () => {
532
+ const defaultAuditRef = {
533
+ title: '',
534
+ description: '',
535
+ scoreDisplayMode: 'numeric',
536
+ score: 0,
537
+ warnings: [],
538
+ };
539
+ const category = {
540
+ id: 'test',
541
+ title: 'Test',
542
+ score: 0,
543
+ auditRefs: [{
544
+ id: 'audit-1',
545
+ weight: 0,
546
+ result: {
547
+ id: 'audit-1',
548
+ ...defaultAuditRef,
549
+ },
550
+ }, {
551
+ id: 'audit-2',
552
+ weight: 1,
553
+ result: {
554
+ id: 'audit-2',
555
+ ...defaultAuditRef,
556
+ },
557
+ }, {
558
+ id: 'audit-3',
559
+ weight: 0.5,
560
+ result: {
561
+ id: 'audit-3',
562
+ ...defaultAuditRef,
563
+ },
564
+ }],
565
+ };
566
+ const categoryDOM = renderer.render(category);
567
+
568
+ const auditEls = [...categoryDOM.querySelectorAll('.lh-audit')];
569
+ expect(auditEls.map(el => el.id)).toEqual(['audit-2', 'audit-3', 'audit-1']);
570
+ });
498
571
  });
@@ -9,7 +9,6 @@ import {strict as assert} from 'assert';
9
9
 
10
10
  import {Util} from '../../renderer/util.js';
11
11
  import {I18n} from '../../renderer/i18n.js';
12
- import {isNode12SmallIcu} from '../../../lighthouse-core/test/test-utils.js';
13
12
 
14
13
  // Require i18n to make sure Intl is polyfilled in Node without full-icu for testing.
15
14
  // When Util is run in a browser, Intl will be supplied natively (IE11+).
@@ -108,9 +107,6 @@ describe('util helpers', () => {
108
107
  });
109
108
 
110
109
  it('formats numbers based on locale', () => {
111
- // COMPAT: Node 12 only has 'en' by default.
112
- if (isNode12SmallIcu()) return;
113
-
114
110
  // Requires full-icu or Intl polyfill.
115
111
  const number = 12346.858558;
116
112
 
@@ -122,9 +118,6 @@ describe('util helpers', () => {
122
118
  });
123
119
 
124
120
  it('uses decimal comma with en-XA test locale', () => {
125
- // COMPAT: Node 12 only has 'en' by default.
126
- if (isNode12SmallIcu()) return;
127
-
128
121
  // Requires full-icu or Intl polyfill.
129
122
  const number = 12346.858558;
130
123
 
@@ -67,6 +67,17 @@ describe('PerfCategoryRenderer', () => {
67
67
  const timelineElements = metricsSection.querySelectorAll('.lh-metric');
68
68
  const nontimelineElements = metricsSection.querySelectorAll('.lh-audit');
69
69
  assert.equal(timelineElements.length + nontimelineElements.length, metricAudits.length);
70
+ assert.deepStrictEqual(
71
+ Array.from(timelineElements).map(el => el.id),
72
+ [
73
+ 'first-contentful-paint',
74
+ 'interactive',
75
+ 'speed-index',
76
+ 'total-blocking-time',
77
+ 'largest-contentful-paint',
78
+ 'cumulative-layout-shift',
79
+ ]
80
+ );
70
81
  });
71
82
 
72
83
  it('does not render metrics section if no metric group audits', () => {
@@ -84,7 +95,7 @@ describe('PerfCategoryRenderer', () => {
84
95
  it('renders the metrics variance disclaimer as markdown', () => {
85
96
  const categoryDOM = renderer.render(category, sampleResults.categoryGroups);
86
97
  const disclaimerEl =
87
- categoryDOM.querySelector('.lh-audit-group--metrics > .lh-metrics__disclaimer');
98
+ categoryDOM.querySelector('.lh-category-header__description > .lh-metrics__disclaimer');
88
99
 
89
100
  assert.ok(disclaimerEl.textContent.includes('Values are estimated'));
90
101
  const disclamerLink = disclaimerEl.querySelector('a');
@@ -96,11 +107,23 @@ describe('PerfCategoryRenderer', () => {
96
107
  assert.strictEqual(new URL(calcLink.href).hostname, 'googlechrome.github.io');
97
108
  });
98
109
 
110
+ it('ignores hidden audits', () => {
111
+ const categoryDOM = renderer.render(category, sampleResults.categoryGroups);
112
+
113
+ const hiddenAudits = category.auditRefs.filter(audit => audit.group === 'hidden');
114
+ const auditElements = [...categoryDOM.querySelectorAll('.lh-audit')];
115
+ const matchingElements = auditElements
116
+ .filter(el => hiddenAudits.some(audit => audit.id === el.id));
117
+ expect(matchingElements).toHaveLength(0);
118
+ });
119
+
99
120
  it('renders the failing performance opportunities', () => {
100
121
  const categoryDOM = renderer.render(category, sampleResults.categoryGroups);
101
122
 
102
- const oppAudits = category.auditRefs.filter(audit => audit.group === 'load-opportunities' &&
103
- !Util.showAsPassed(audit.result));
123
+ const oppAudits = category.auditRefs.filter(audit =>
124
+ audit.result.details &&
125
+ audit.result.details.type === 'opportunity' &&
126
+ !Util.showAsPassed(audit.result));
104
127
  const oppElements = [...categoryDOM.querySelectorAll('.lh-audit--load-opportunity')];
105
128
  expect(oppElements.map(e => e.id).sort()).toEqual(oppAudits.map(a => a.id).sort());
106
129
  expect(oppElements.length).toBeGreaterThan(0);
@@ -120,10 +143,14 @@ describe('PerfCategoryRenderer', () => {
120
143
  it('renders performance opportunities with an errorMessage', () => {
121
144
  const auditWithError = {
122
145
  score: 0,
123
- group: 'load-opportunities',
124
146
  result: {
125
147
  score: null, scoreDisplayMode: 'error', errorMessage: 'Yikes!!', title: 'Bug #2',
126
148
  description: '',
149
+ details: {
150
+ overallSavingsMs: 0,
151
+ items: [],
152
+ type: 'opportunity',
153
+ },
127
154
  },
128
155
  };
129
156
 
@@ -137,10 +164,14 @@ describe('PerfCategoryRenderer', () => {
137
164
  it('renders performance opportunities\' explanation', () => {
138
165
  const auditWithExplanation = {
139
166
  score: 0,
140
- group: 'load-opportunities',
141
167
  result: {
142
168
  score: 0, scoreDisplayMode: 'numeric',
143
169
  numericValue: 100, explanation: 'Yikes!!', title: 'Bug #2', description: '',
170
+ details: {
171
+ overallSavingsMs: 0,
172
+ items: [],
173
+ type: 'opportunity',
174
+ },
144
175
  },
145
176
  };
146
177
 
@@ -159,7 +190,9 @@ describe('PerfCategoryRenderer', () => {
159
190
  '.lh-category > .lh-audit-group.lh-audit-group--diagnostics');
160
191
 
161
192
  const diagnosticAuditIds = category.auditRefs.filter(audit => {
162
- return audit.group === 'diagnostics' && !Util.showAsPassed(audit.result);
193
+ return !audit.group &&
194
+ !(audit.result.details && audit.result.details.type === 'opportunity') &&
195
+ !Util.showAsPassed(audit.result);
163
196
  }).map(audit => audit.id).sort();
164
197
  assert.ok(diagnosticAuditIds.length > 0);
165
198
 
@@ -170,11 +203,11 @@ describe('PerfCategoryRenderer', () => {
170
203
 
171
204
  it('renders the passed audits', () => {
172
205
  const categoryDOM = renderer.render(category, sampleResults.categoryGroups);
173
- const passedSection = categoryDOM.querySelector('.lh-category > .lh-clump--passed');
206
+ const passedSection = categoryDOM.querySelector('.lh-clump--passed');
174
207
 
175
208
  const passedAudits = category.auditRefs.filter(audit =>
176
- audit.group && audit.group !== 'metrics' && audit.id !== 'performance-budget'
177
- && Util.showAsPassed(audit.result));
209
+ !audit.group &&
210
+ Util.showAsPassed(audit.result));
178
211
  const passedElements = passedSection.querySelectorAll('.lh-audit');
179
212
  assert.equal(passedElements.length, passedAudits.length);
180
213
  });
@@ -187,10 +220,14 @@ describe('PerfCategoryRenderer', () => {
187
220
  it('handles erroring opportunities', () => {
188
221
  const auditWithDebug = {
189
222
  score: 0,
190
- group: 'load-opportunities',
191
223
  result: {
192
224
  error: true, score: 0,
193
225
  numericValue: 100, explanation: 'Yikes!!', title: 'Bug #2',
226
+ details: {
227
+ overallSavingsMs: 0,
228
+ items: [],
229
+ type: 'opportunity',
230
+ },
194
231
  },
195
232
  };
196
233
  const wastedMs = renderer._getWastedMs(auditWithDebug);
@@ -256,10 +293,10 @@ describe('PerfCategoryRenderer', () => {
256
293
  expect(url.hash.split('&')).toMatchInlineSnapshot(`
257
294
  Array [
258
295
  "#FCP=6844",
259
- "SI=8114",
260
- "LCP=6844",
261
296
  "TTI=8191",
297
+ "SI=8114",
262
298
  "TBT=1221",
299
+ "LCP=6844",
263
300
  "CLS=0",
264
301
  "FMP=6844",
265
302
  ]
@@ -277,10 +314,10 @@ Array [
277
314
  expect(url.hash.split('&')).toMatchInlineSnapshot(`
278
315
  Array [
279
316
  "#FCP=6844",
280
- "SI=8114",
281
- "LCP=6844",
282
317
  "TTI=8191",
318
+ "SI=8114",
283
319
  "TBT=1221",
320
+ "LCP=6844",
284
321
  "CLS=0.14",
285
322
  "FMP=6844",
286
323
  "device=mobile",
@@ -107,12 +107,12 @@ describe('PwaCategoryRenderer', () => {
107
107
  const clone = JSON.parse(JSON.stringify(sampleResults));
108
108
  const category = clone.categories.pwa;
109
109
 
110
- // Set everything to passing, except redirects-http set to n/a (as it is on localhost)
110
+ // Set everything to passing, except for one. (themed-omnibox chosen randomly)
111
111
  for (const auditRef of category.auditRefs) {
112
112
  auditRef.result.score = 1;
113
113
  auditRef.result.scoreDisplayMode = 'binary';
114
114
  }
115
- const audit = category.auditRefs.find(ref => ref.id === 'redirects-http');
115
+ const audit = category.auditRefs.find(ref => ref.id === 'themed-omnibox');
116
116
  audit.result.scoreDisplayMode = 'notApplicable';
117
117
  audit.result.score = null;
118
118
 
@@ -249,10 +249,10 @@ describe('PwaCategoryRenderer', () => {
249
249
  });
250
250
  });
251
251
 
252
- describe('#renderScoreGauge', () => {
252
+ describe('#renderCategoryScore', () => {
253
253
  it('renders an error score gauge in case of category error', () => {
254
254
  category.score = null;
255
- const badgeGauge = pwaRenderer.renderScoreGauge(category, sampleResults.categoryGroups);
255
+ const badgeGauge = pwaRenderer.renderCategoryScore(category, sampleResults.categoryGroups);
256
256
 
257
257
  // Not a PWA gauge.
258
258
  assert.strictEqual(badgeGauge.querySelector('.lh-gauge--pwa__wrapper'), null);
@@ -263,11 +263,11 @@ describe('PwaCategoryRenderer', () => {
263
263
  });
264
264
 
265
265
  it('renders score gauges with unique ids for items in <defs>', () => {
266
- const gauge1 = pwaRenderer.renderScoreGauge(category, sampleResults.categoryGroups);
266
+ const gauge1 = pwaRenderer.renderCategoryScore(category, sampleResults.categoryGroups);
267
267
  const gauge1Ids = [...gauge1.querySelectorAll('defs [id]')].map(el => el.id);
268
268
  assert.ok(gauge1Ids.length > 2);
269
269
 
270
- const gauge2 = pwaRenderer.renderScoreGauge(category, sampleResults.categoryGroups);
270
+ const gauge2 = pwaRenderer.renderCategoryScore(category, sampleResults.categoryGroups);
271
271
  const gauge2Ids = [...gauge2.querySelectorAll('defs [id]')].map(el => el.id);
272
272
  assert.ok(gauge2Ids.length === gauge1Ids.length);
273
273
 
@@ -8,6 +8,7 @@
8
8
  /* eslint-env jest */
9
9
 
10
10
  import jsdom from 'jsdom';
11
+ import {jest} from '@jest/globals';
11
12
 
12
13
  import {Util} from '../../renderer/util.js';
13
14
  import {DOM} from '../../renderer/dom.js';
@@ -23,6 +24,8 @@ describe('ReportRendererAxe', () => {
23
24
  let sampleResults;
24
25
 
25
26
  beforeAll(async () => {
27
+ global.console.warn = jest.fn();
28
+
26
29
  const {window} = new jsdom.JSDOM();
27
30
  const dom = new DOM(window.document);
28
31
  const detailsRenderer = new DetailsRenderer(dom);
@@ -46,11 +49,11 @@ describe('ReportRendererAxe', () => {
46
49
  global.Element = undefined;
47
50
  });
48
51
 
49
- it('renders without axe violations', () => {
50
- const container = renderer._dom._document.createElement('main');
52
+ it('renders without axe violations', async () => {
53
+ const container = renderer._dom.document().createElement('main');
51
54
  const output = renderer.renderReport(sampleResults, container);
52
55
 
53
- renderer._dom._document.body.appendChild(container);
56
+ renderer._dom.document().body.appendChild(container);
54
57
 
55
58
  const config = {
56
59
  rules: {
@@ -70,17 +73,12 @@ describe('ReportRendererAxe', () => {
70
73
  },
71
74
  };
72
75
 
73
- return new Promise(resolve => {
74
- axe.run(output, config, (error, {violations}) => {
75
- expect(error).toBeNull();
76
- expect(violations).toEqual([]);
77
- resolve();
78
- });
79
- });
80
- // This test takes 40s on fast hardware, and 50-60s on GHA.
81
- // https://github.com/dequelabs/axe-core/tree/b573b1c1/doc/examples/jest_react#timeout-issues
76
+ const axeResults = await axe.run(output, config);
77
+ expect(axeResults.violations).toEqual([]);
82
78
  },
83
- /* timeout= */ 100 * 1000
79
+ // This test takes 40s on fast hardware, and 50-60s on GHA (but can take longer).
80
+ // https://github.com/dequelabs/axe-core/tree/b573b1c1/doc/examples/jest_react#timeout-issues
81
+ /* timeout= */ process.env.CI ? 200_000 : 60_000
84
82
  );
85
83
  });
86
84
  });
@@ -10,6 +10,7 @@
10
10
  import {strict as assert} from 'assert';
11
11
 
12
12
  import jsdom from 'jsdom';
13
+ import {jest} from '@jest/globals';
13
14
 
14
15
  import {Util} from '../../renderer/util.js';
15
16
  import URL from '../../../lighthouse-core/lib/url-shim.js';
@@ -26,6 +27,8 @@ describe('ReportRenderer', () => {
26
27
  let sampleResults;
27
28
 
28
29
  beforeAll(() => {
30
+ global.console.warn = jest.fn();
31
+
29
32
  // Stub out matchMedia for Node.
30
33
  global.matchMedia = function() {
31
34
  return {
@@ -50,7 +53,7 @@ describe('ReportRenderer', () => {
50
53
 
51
54
  describe('renderReport', () => {
52
55
  it('should render a report', () => {
53
- const container = renderer._dom._document.body;
56
+ const container = renderer._dom.document().body;
54
57
  const output = renderer.renderReport(sampleResults, container);
55
58
  assert.ok(output.querySelector('.lh-header-container'), 'has a header');
56
59
  assert.ok(output.querySelector('.lh-report'), 'has report body');
@@ -60,7 +63,7 @@ describe('ReportRenderer', () => {
60
63
  });
61
64
 
62
65
  it('renders additional reports by replacing the existing one', () => {
63
- const container = renderer._dom._document.body;
66
+ const container = renderer._dom.document().body;
64
67
  const oldReport = Array.from(renderer.renderReport(sampleResults, container).children);
65
68
  const newReport = Array.from(renderer.renderReport(sampleResults, container).children);
66
69
  assert.ok(!oldReport.find(node => container.contains(node)), 'old report was removed');
@@ -86,11 +89,11 @@ describe('ReportRenderer', () => {
86
89
  auditRefs: [],
87
90
  };
88
91
 
89
- const container = renderer._dom._document.body;
92
+ const container = renderer._dom.document().body;
90
93
  const output = renderer.renderReport(sampleResultsCopy, container);
91
94
 
92
95
  function isPWAGauge(el) {
93
- return el.querySelector('.lh-gauge__label').textContent === 'Progressive Web App';
96
+ return el.querySelector('.lh-gauge__label').textContent === 'PWA';
94
97
  }
95
98
  function isPluginGauge(el) {
96
99
  return el.querySelector('.lh-gauge__label').textContent === 'Some Plugin';
@@ -119,6 +122,35 @@ describe('ReportRenderer', () => {
119
122
  }
120
123
  });
121
124
 
125
+ it('renders score gauges with custom callback', () => {
126
+ const sampleResultsCopy = JSON.parse(JSON.stringify(sampleResults));
127
+
128
+ const opts = {
129
+ onPageAnchorRendered: link => {
130
+ const id = link.hash.substring(1);
131
+ link.hash = `#index=0&anchor=${id}`;
132
+ },
133
+ };
134
+ const container = renderer._dom.document().body;
135
+ const output = renderer.renderReport(sampleResultsCopy, container, opts);
136
+ const anchors = output.querySelectorAll('a.lh-gauge__wrapper, a.lh-fraction__wrapper');
137
+ const hashes = Array.from(anchors).map(anchor => anchor.hash).filter(hash => hash);
138
+
139
+ // One set for the sticky header, on set for the gauges at the top.
140
+ assert.deepStrictEqual(hashes, [
141
+ '#index=0&anchor=performance',
142
+ '#index=0&anchor=accessibility',
143
+ '#index=0&anchor=best-practices',
144
+ '#index=0&anchor=seo',
145
+ '#index=0&anchor=pwa',
146
+ '#index=0&anchor=performance',
147
+ '#index=0&anchor=accessibility',
148
+ '#index=0&anchor=best-practices',
149
+ '#index=0&anchor=seo',
150
+ '#index=0&anchor=pwa',
151
+ ]);
152
+ });
153
+
122
154
  it('renders plugin score gauge', () => {
123
155
  const sampleResultsCopy = JSON.parse(JSON.stringify(sampleResults));
124
156
  sampleResultsCopy.categories['lighthouse-plugin-someplugin'] = {
@@ -126,7 +158,7 @@ describe('ReportRenderer', () => {
126
158
  title: 'Some Plugin',
127
159
  auditRefs: [],
128
160
  };
129
- const container = renderer._dom._document.body;
161
+ const container = renderer._dom.document().body;
130
162
  const output = renderer.renderReport(sampleResultsCopy, container);
131
163
  const scoresHeaderElem = output.querySelector('.lh-scores-header');
132
164
 
@@ -140,7 +172,7 @@ describe('ReportRenderer', () => {
140
172
  });
141
173
 
142
174
  it('should not mutate a report object', () => {
143
- const container = renderer._dom._document.body;
175
+ const container = renderer._dom.document().body;
144
176
  const originalResults = JSON.parse(JSON.stringify(sampleResults));
145
177
  renderer.renderReport(sampleResults, container);
146
178
  assert.deepStrictEqual(sampleResults, originalResults);
@@ -148,13 +180,13 @@ describe('ReportRenderer', () => {
148
180
 
149
181
  it('renders no warning section when no lighthouseRunWarnings occur', () => {
150
182
  const warningResults = Object.assign({}, sampleResults, {runWarnings: []});
151
- const container = renderer._dom._document.body;
183
+ const container = renderer._dom.document().body;
152
184
  const output = renderer.renderReport(warningResults, container);
153
185
  assert.strictEqual(output.querySelector('.lh-warnings--toplevel'), null);
154
186
  });
155
187
 
156
188
  it('renders a warning section', () => {
157
- const container = renderer._dom._document.body;
189
+ const container = renderer._dom.document().body;
158
190
  const output = renderer.renderReport(sampleResults, container);
159
191
 
160
192
  const warningEls = output.querySelectorAll('.lh-warnings--toplevel > ul > li');
@@ -165,7 +197,7 @@ describe('ReportRenderer', () => {
165
197
  const warningResults = Object.assign({}, sampleResults, {
166
198
  runWarnings: ['[I am a link](https://example.com/)'],
167
199
  });
168
- const container = renderer._dom._document.body;
200
+ const container = renderer._dom.document().body;
169
201
  const output = renderer.renderReport(warningResults, container);
170
202
 
171
203
  const warningEls = output.querySelectorAll('.lh-warnings--toplevel ul li a');
@@ -179,17 +211,15 @@ describe('ReportRenderer', () => {
179
211
  assert.ok(/Generated by Lighthouse \d/.test(footerContent), 'includes lh version');
180
212
  assert.ok(footerContent.match(TIMESTAMP_REGEX), 'includes timestamp');
181
213
 
182
- // Check runtime settings were populated.
183
- const names = Array.from(footer.querySelectorAll('.lh-env__name'));
184
- const descriptions = Array.from(footer.querySelectorAll('.lh-env__description'));
185
- assert.ok(names.length >= 3);
186
- assert.ok(descriptions.length >= 3);
187
-
188
- const descriptionsTxt = descriptions.map(el => el.textContent).join('\n');
189
- expect(descriptionsTxt).toContain('Moto G4');
190
- expect(descriptionsTxt).toContain('RTT');
191
- expect(descriptionsTxt).toMatch(/\dx/);
192
- expect(descriptionsTxt).toContain(sampleResults.userAgent);
214
+ // Check env items were populated.
215
+ const items = Array.from(footer.querySelectorAll('.lh-meta__item'));
216
+ expect(items.length).toBeGreaterThanOrEqual(6);
217
+
218
+ const itemsTxt = items.map(el => `${el.textContent} ${el.title}`).join('\n');
219
+ expect(itemsTxt).toContain('Moto G4');
220
+ expect(itemsTxt).toContain('RTT');
221
+ expect(itemsTxt).toMatch(/\dx/);
222
+ expect(itemsTxt).toContain(sampleResults.environment.networkUserAgent);
193
223
  });
194
224
  });
195
225
 
@@ -198,7 +228,7 @@ describe('ReportRenderer', () => {
198
228
  // Make sure we have a channel in the LHR.
199
229
  assert.ok(lhrChannel.length > 2);
200
230
 
201
- const container = renderer._dom._document.body;
231
+ const container = renderer._dom.document().body;
202
232
  const output = renderer.renderReport(sampleResults, container);
203
233
 
204
234
  const DOCS_ORIGINS = ['https://developers.google.com', 'https://web.dev'];
@@ -227,7 +257,7 @@ describe('ReportRenderer', () => {
227
257
 
228
258
  assert.ok(notApplicableCount > 20); // Make sure something's being tested.
229
259
 
230
- const container = renderer._dom._document.body;
260
+ const container = renderer._dom.document().body;
231
261
  const reportElement = renderer.renderReport(sampleResults, container);
232
262
  const notApplicableElementCount = reportElement
233
263
  .querySelectorAll('.lh-audit--notapplicable').length;