lighthouse 8.6.0 → 9.0.0-dev.20211115

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 (207) hide show
  1. package/changelog.md +121 -0
  2. package/dist/report/bundle.esm.js +5994 -0
  3. package/dist/report/flow.js +30 -13
  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 -2
  7. package/flow-report/assets/styles.css +108 -31
  8. package/flow-report/src/app.tsx +18 -18
  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 +9 -5
  20. package/flow-report/src/util.ts +33 -26
  21. package/flow-report/src/wrappers/category-score.tsx +0 -3
  22. package/flow-report/src/wrappers/markdown.tsx +27 -0
  23. package/flow-report/src/wrappers/report-renderer.tsx +10 -3
  24. package/flow-report/src/wrappers/report.tsx +27 -6
  25. package/flow-report/standalone-flow.tsx +4 -3
  26. package/flow-report/test/common-test.tsx +32 -17
  27. package/flow-report/test/flow-report-pptr-test.ts +46 -0
  28. package/flow-report/test/header-test.tsx +6 -6
  29. package/flow-report/test/setup/env-setup.ts +1 -1
  30. package/flow-report/test/sidebar/sidebar-test.tsx +43 -1
  31. package/flow-report/test/summary/category-test.tsx +120 -24
  32. package/flow-report/test/summary/summary-test.tsx +1 -1
  33. package/flow-report/test/topbar-test.tsx +1 -0
  34. package/flow-report/test/util-test.tsx +32 -11
  35. package/flow-report/test/wrappers/category-score-test.tsx +87 -0
  36. package/flow-report/test/wrappers/markdown-test.tsx +29 -0
  37. package/flow-report/tsconfig.json +1 -0
  38. package/jest.config.js +4 -2
  39. package/lighthouse-cli/.eslintrc.cjs +2 -0
  40. package/lighthouse-cli/cli-flags.js +1 -1
  41. package/lighthouse-cli/run.js +1 -1
  42. package/lighthouse-cli/test/smokehouse/lighthouse-runners/bundle.js +4 -2
  43. package/lighthouse-cli/test/smokehouse/lighthouse-runners/cli.js +1 -1
  44. package/lighthouse-cli/test/smokehouse/smokehouse.js +1 -1
  45. package/lighthouse-core/audits/accessibility/axe-audit.js +7 -1
  46. package/lighthouse-core/audits/audit.js +1 -1
  47. package/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js +2 -0
  48. package/lighthouse-core/audits/byte-efficiency/modern-image-formats.js +4 -2
  49. package/lighthouse-core/audits/byte-efficiency/offscreen-images.js +11 -10
  50. package/lighthouse-core/audits/byte-efficiency/uses-optimized-images.js +5 -3
  51. package/lighthouse-core/audits/byte-efficiency/uses-responsive-images-snapshot.js +2 -1
  52. package/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js +2 -1
  53. package/lighthouse-core/audits/deprecations.js +33 -11
  54. package/lighthouse-core/audits/dobetterweb/uses-http2.js +27 -11
  55. package/lighthouse-core/audits/errors-in-console.js +0 -2
  56. package/lighthouse-core/audits/image-aspect-ratio.js +5 -5
  57. package/lighthouse-core/audits/image-size-responsive.js +6 -6
  58. package/lighthouse-core/audits/installable-manifest.js +5 -3
  59. package/lighthouse-core/audits/preload-lcp-image.js +7 -5
  60. package/lighthouse-core/audits/script-treemap-data.js +1 -1
  61. package/lighthouse-core/audits/unsized-images.js +2 -3
  62. package/lighthouse-core/computed/image-records.js +4 -3
  63. package/lighthouse-core/computed/metrics/lantern-total-blocking-time.js +3 -4
  64. package/lighthouse-core/computed/metrics/tbt-utils.js +57 -0
  65. package/lighthouse-core/computed/metrics/total-blocking-time.js +3 -53
  66. package/lighthouse-core/computed/resource-summary.js +1 -1
  67. package/lighthouse-core/config/config-helpers.js +21 -10
  68. package/lighthouse-core/config/constants.js +3 -4
  69. package/lighthouse-core/config/default-config.js +61 -71
  70. package/lighthouse-core/fraggle-rock/config/config.js +1 -1
  71. package/lighthouse-core/fraggle-rock/config/default-config.js +4 -5
  72. package/lighthouse-core/fraggle-rock/gather/base-gatherer.js +6 -1
  73. package/lighthouse-core/fraggle-rock/gather/session.js +3 -4
  74. package/lighthouse-core/gather/connections/cri.js +1 -1
  75. package/lighthouse-core/gather/driver/storage.js +0 -1
  76. package/lighthouse-core/gather/driver.js +3 -7
  77. package/lighthouse-core/gather/fetcher.js +4 -7
  78. package/lighthouse-core/gather/gather-runner.js +29 -6
  79. package/lighthouse-core/gather/gatherers/accessibility.js +27 -0
  80. package/lighthouse-core/gather/gatherers/dobetterweb/response-compression.js +1 -0
  81. package/lighthouse-core/gather/gatherers/image-elements.js +0 -3
  82. package/lighthouse-core/gather/gatherers/inspector-issues.js +5 -0
  83. package/lighthouse-core/lib/asset-saver.js +7 -4
  84. package/lighthouse-core/lib/emulation.js +45 -1
  85. package/lighthouse-core/lib/i18n/i18n.js +19 -8
  86. package/lighthouse-core/lib/lh-env.js +2 -0
  87. package/lighthouse-core/lib/proto-preprocessor.js +14 -2
  88. package/lighthouse-core/lib/tappable-rects.js +8 -15
  89. package/lighthouse-core/lib/url-shim.js +1 -1
  90. package/lighthouse-core/runner.js +1 -1
  91. package/lighthouse-core/util-commonjs.js +65 -46
  92. package/package.json +27 -17
  93. package/readme.md +3 -3
  94. package/report/.eslintrc.cjs +2 -0
  95. package/report/README.md +1 -1
  96. package/report/assets/standalone-template.html +2 -3
  97. package/report/assets/styles.css +360 -254
  98. package/report/assets/templates.html +21 -76
  99. package/report/clients/bundle.js +1 -0
  100. package/report/clients/standalone.js +6 -15
  101. package/report/generator/README.md +1 -1
  102. package/report/renderer/api.js +32 -0
  103. package/report/renderer/category-renderer.js +59 -19
  104. package/report/renderer/components.js +59 -110
  105. package/report/renderer/crc-details-renderer.js +15 -12
  106. package/report/renderer/dom.js +16 -1
  107. package/report/renderer/drop-down-menu.js +2 -2
  108. package/report/renderer/element-screenshot-renderer.js +8 -8
  109. package/report/renderer/features-util.js +1 -1
  110. package/report/renderer/open-tab.js +9 -3
  111. package/report/renderer/performance-category-renderer.js +55 -34
  112. package/report/renderer/pwa-category-renderer.js +0 -10
  113. package/report/renderer/report-renderer.js +91 -44
  114. package/report/renderer/report-ui-features.js +36 -33
  115. package/report/renderer/swap-locale-feature.js +3 -3
  116. package/report/renderer/topbar-features.js +53 -48
  117. package/report/renderer/util.js +66 -46
  118. package/report/test/clients/bundle-test.js +70 -0
  119. package/report/test/renderer/category-renderer-test.js +48 -3
  120. package/report/test/renderer/i18n-test.js +0 -7
  121. package/report/test/renderer/performance-category-renderer-test.js +51 -14
  122. package/report/test/renderer/pwa-category-renderer-test.js +6 -6
  123. package/report/test/renderer/report-renderer-axe-test.js +10 -12
  124. package/report/test/renderer/report-renderer-test.js +23 -22
  125. package/report/test/renderer/report-ui-features-test.js +20 -10
  126. package/report/test/renderer/text-encoding-test.js +1 -1
  127. package/report/test/renderer/util-test.js +35 -31
  128. package/report/test-assets/faux-psi-template.html +3 -11
  129. package/report/test-assets/faux-psi.js +26 -22
  130. package/report/types/html-renderer.d.ts +2 -0
  131. package/report/types/report-renderer.d.ts +23 -0
  132. package/shared/localization/format.js +64 -30
  133. package/shared/localization/locales/ar-XB.json +72 -75
  134. package/shared/localization/locales/ar.json +72 -75
  135. package/shared/localization/locales/bg.json +72 -75
  136. package/shared/localization/locales/ca.json +72 -75
  137. package/shared/localization/locales/cs.json +89 -92
  138. package/shared/localization/locales/da.json +72 -75
  139. package/shared/localization/locales/de.json +89 -92
  140. package/shared/localization/locales/el.json +72 -75
  141. package/shared/localization/locales/en-GB.json +72 -75
  142. package/shared/localization/locales/en-US.json +75 -75
  143. package/shared/localization/locales/en-XA.json +72 -75
  144. package/shared/localization/locales/en-XL.json +75 -75
  145. package/shared/localization/locales/es-419.json +90 -93
  146. package/shared/localization/locales/es.json +90 -93
  147. package/shared/localization/locales/fi.json +72 -75
  148. package/shared/localization/locales/fil.json +72 -75
  149. package/shared/localization/locales/fr.json +89 -92
  150. package/shared/localization/locales/he.json +73 -76
  151. package/shared/localization/locales/hi.json +72 -75
  152. package/shared/localization/locales/hr.json +72 -75
  153. package/shared/localization/locales/hu.json +89 -92
  154. package/shared/localization/locales/id.json +89 -92
  155. package/shared/localization/locales/it.json +89 -92
  156. package/shared/localization/locales/ja.json +89 -92
  157. package/shared/localization/locales/ko.json +89 -92
  158. package/shared/localization/locales/lt.json +89 -92
  159. package/shared/localization/locales/lv.json +95 -98
  160. package/shared/localization/locales/nl.json +89 -92
  161. package/shared/localization/locales/no.json +72 -75
  162. package/shared/localization/locales/pl.json +72 -75
  163. package/shared/localization/locales/pt-PT.json +72 -75
  164. package/shared/localization/locales/pt.json +89 -92
  165. package/shared/localization/locales/ro.json +90 -93
  166. package/shared/localization/locales/ru.json +89 -92
  167. package/shared/localization/locales/sk.json +89 -92
  168. package/shared/localization/locales/sl.json +89 -92
  169. package/shared/localization/locales/sr-Latn.json +89 -92
  170. package/shared/localization/locales/sr.json +89 -92
  171. package/shared/localization/locales/sv.json +72 -75
  172. package/shared/localization/locales/ta.json +89 -92
  173. package/shared/localization/locales/te.json +89 -92
  174. package/shared/localization/locales/th.json +89 -92
  175. package/shared/localization/locales/tr.json +89 -92
  176. package/shared/localization/locales/uk.json +92 -95
  177. package/shared/localization/locales/vi.json +89 -92
  178. package/shared/localization/locales/zh-HK.json +89 -92
  179. package/shared/localization/locales/zh-TW.json +89 -92
  180. package/shared/localization/locales/zh.json +89 -92
  181. package/shared/localization/locales.js +6 -0
  182. package/shared/test/localization/format-test.js +45 -9
  183. package/shared/test/localization/swap-locale-test.js +0 -12
  184. package/third-party/chromium-synchronization/inspector-issueAdded-types-test.js +19 -11
  185. package/third-party/chromium-synchronization/installability-errors-test.js +2 -0
  186. package/third-party/download-content-shell/download-content-shell.js +2 -2
  187. package/third-party/download-content-shell/utils.js +0 -24
  188. package/tsconfig-all.json +2 -2
  189. package/tsconfig.json +0 -1
  190. package/types/artifacts.d.ts +7 -7
  191. package/types/enquirer.d.ts +3 -1
  192. package/types/lhr/audit-details.d.ts +6 -5
  193. package/types/lhr/flow.d.ts +3 -2
  194. package/types/lhr/lhr.d.ts +1 -1
  195. package/types/node.d.ts +5 -5
  196. package/types/rollup-plugin-postprocess.d.ts +10 -0
  197. package/lighthouse-core/audits/dobetterweb/appcache-manifest.js +0 -67
  198. package/lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js +0 -96
  199. package/lighthouse-core/audits/redirects-http.js +0 -59
  200. package/lighthouse-core/gather/gatherers/dobetterweb/appcache.js +0 -34
  201. package/lighthouse-core/gather/gatherers/http-redirect.js +0 -46
  202. package/lighthouse-core/lib/i18n/locales/en-US.ctc.json +0 -6699
  203. package/lighthouse-core/lib/i18n/locales/en-XL.ctc.json +0 -6672
  204. package/report/clients/psi.js +0 -158
  205. package/report/test/clients/psi-test.js +0 -143
  206. package/shared/localization/locales/en-US.ctc.json +0 -6877
  207. package/shared/localization/locales/en-XL.ctc.json +0 -6852
@@ -21,12 +21,21 @@
21
21
  * critical request chains network tree.
22
22
  */
23
23
 
24
+ import {Util} from './util.js';
25
+
24
26
  /** @typedef {import('./dom.js').DOM} DOM */
25
27
  /** @typedef {import('./details-renderer.js').DetailsRenderer} DetailsRenderer */
28
+ /**
29
+ * @typedef CRCSegment
30
+ * @property {LH.Audit.Details.SimpleCriticalRequestNode[string]} node
31
+ * @property {boolean} isLastChild
32
+ * @property {boolean} hasChildren
33
+ * @property {number} startTime
34
+ * @property {number} transferSize
35
+ * @property {boolean[]} treeMarkers
36
+ */
26
37
 
27
- import {Util} from './util.js';
28
-
29
- export class CriticalRequestChainRenderer {
38
+ class CriticalRequestChainRenderer {
30
39
  /**
31
40
  * Create render context for critical-request-chain tree display.
32
41
  * @param {LH.Audit.Details.SimpleCriticalRequestNode} tree
@@ -191,12 +200,6 @@ export class CriticalRequestChainRenderer {
191
200
  // Alias b/c the name is really long.
192
201
  const CRCRenderer = CriticalRequestChainRenderer;
193
202
 
194
- /** @typedef {{
195
- node: LH.Audit.Details.SimpleCriticalRequestNode[string],
196
- isLastChild: boolean,
197
- hasChildren: boolean,
198
- startTime: number,
199
- transferSize: number,
200
- treeMarkers: Array<boolean>
201
- }} CRCSegment
202
- */
203
+ export {
204
+ CriticalRequestChainRenderer,
205
+ };
@@ -27,14 +27,18 @@ import {createComponent} from './components.js';
27
27
  export class DOM {
28
28
  /**
29
29
  * @param {Document} document
30
+ * @param {HTMLElement} rootEl
30
31
  */
31
- constructor(document) {
32
+ constructor(document, rootEl) {
32
33
  /** @type {Document} */
33
34
  this._document = document;
34
35
  /** @type {string} */
35
36
  this._lighthouseChannel = 'unknown';
36
37
  /** @type {Map<string, DocumentFragment>} */
37
38
  this._componentCache = new Map();
39
+ /** @type {HTMLElement} */
40
+ // For legacy Report API users, this'll be undefined, but set in renderReport
41
+ this.rootEl = rootEl;
38
42
  }
39
43
 
40
44
  /**
@@ -76,6 +80,15 @@ export class DOM {
76
80
  return this._document.createDocumentFragment();
77
81
  }
78
82
 
83
+ /**
84
+ * @param {string} data
85
+ * @return {!Node}
86
+ */
87
+ createTextNode(data) {
88
+ return this._document.createTextNode(data);
89
+ }
90
+
91
+
79
92
  /**
80
93
  * @template {string} T
81
94
  * @param {Element} parentElem
@@ -216,6 +229,8 @@ export class DOM {
216
229
  }
217
230
 
218
231
  /**
232
+ * ONLY use if `dom.rootEl` isn't sufficient for your needs. `dom.rootEl` is preferred
233
+ * for all scoping, because a document can have multiple reports within it.
219
234
  * @return {Document}
220
235
  */
221
236
  document() {
@@ -36,11 +36,11 @@ export class DropDownMenu {
36
36
  * @param {function(MouseEvent): any} menuClickHandler
37
37
  */
38
38
  setup(menuClickHandler) {
39
- this._toggleEl = this._dom.find('button.lh-tools__button', this._dom.document());
39
+ this._toggleEl = this._dom.find('.lh-topbar button.lh-tools__button', this._dom.rootEl);
40
40
  this._toggleEl.addEventListener('click', this.onToggleClick);
41
41
  this._toggleEl.addEventListener('keydown', this.onToggleKeydown);
42
42
 
43
- this._menuEl = this._dom.find('div.lh-tools__dropdown', this._dom.document());
43
+ this._menuEl = this._dom.find('.lh-topbar div.lh-tools__dropdown', this._dom.rootEl);
44
44
  this._menuEl.addEventListener('keydown', this.onMenuKeydown);
45
45
  this._menuEl.addEventListener('click', menuClickHandler);
46
46
  }
@@ -18,7 +18,7 @@
18
18
  /**
19
19
  * @typedef InstallOverlayFeatureParams
20
20
  * @property {DOM} dom
21
- * @property {Element} reportEl
21
+ * @property {Element} rootEl
22
22
  * @property {Element} overlayContainerEl
23
23
  * @property {LH.Audit.Details.FullPageScreenshot} fullPageScreenshot
24
24
  */
@@ -51,7 +51,7 @@ function clamp(value, min, max) {
51
51
  /**
52
52
  * @param {Rect} rect
53
53
  */
54
- function getRectCenterPoint(rect) {
54
+ function getElementRectCenterPoint(rect) {
55
55
  return {
56
56
  x: rect.left + rect.width / 2,
57
57
  y: rect.top + rect.height / 2,
@@ -68,7 +68,7 @@ export class ElementScreenshotRenderer {
68
68
  * @param {Size} screenshotSize
69
69
  */
70
70
  static getScreenshotPositions(elementRectSC, elementPreviewSizeSC, screenshotSize) {
71
- const elementRectCenter = getRectCenterPoint(elementRectSC);
71
+ const elementRectCenter = getElementRectCenterPoint(elementRectSC);
72
72
 
73
73
  // Try to center clipped region.
74
74
  const screenshotLeftVisibleEdge = clamp(
@@ -135,7 +135,7 @@ export class ElementScreenshotRenderer {
135
135
  * @param {LH.Audit.Details.FullPageScreenshot['screenshot']} screenshot
136
136
  */
137
137
  static installFullPageScreenshot(el, screenshot) {
138
- el.style.setProperty('--element-screenshot-url', `url(${screenshot.data})`);
138
+ el.style.setProperty('--element-screenshot-url', `url('${screenshot.data}')`);
139
139
  }
140
140
 
141
141
  /**
@@ -143,14 +143,14 @@ export class ElementScreenshotRenderer {
143
143
  * @param {InstallOverlayFeatureParams} opts
144
144
  */
145
145
  static installOverlayFeature(opts) {
146
- const {dom, reportEl, overlayContainerEl, fullPageScreenshot} = opts;
146
+ const {dom, rootEl, overlayContainerEl, fullPageScreenshot} = opts;
147
147
  const screenshotOverlayClass = 'lh-screenshot-overlay--enabled';
148
148
  // Don't install the feature more than once.
149
- if (reportEl.classList.contains(screenshotOverlayClass)) return;
150
- reportEl.classList.add(screenshotOverlayClass);
149
+ if (rootEl.classList.contains(screenshotOverlayClass)) return;
150
+ rootEl.classList.add(screenshotOverlayClass);
151
151
 
152
152
  // Add a single listener to the provided element to handle all clicks within (event delegation).
153
- reportEl.addEventListener('click', e => {
153
+ rootEl.addEventListener('click', e => {
154
154
  const target = /** @type {?HTMLElement} */ (e.target);
155
155
  if (!target) return;
156
156
  // Only activate the overlay for clicks on the screenshot *preview* of an element, not the full-size too.
@@ -14,7 +14,7 @@
14
14
  * @param {boolean} [force]
15
15
  */
16
16
  export function toggleDarkTheme(dom, force) {
17
- const el = dom.find('.lh-vars', dom.document());
17
+ const el = dom.rootEl;
18
18
  // This seems unnecessary, but in DevTools, passing "undefined" as the second
19
19
  // parameter acts like passing "false".
20
20
  // https://github.com/ChromeDevTools/devtools-frontend/blob/dd6a6d4153647c2a4203c327c595692c5e0a4256/front_end/dom_extension/DOMExtension.js#L809-L819
@@ -90,7 +90,7 @@ async function openTabWithUrlData(data, url_, windowName) {
90
90
  * @param {LH.Result} lhr
91
91
  * @protected
92
92
  */
93
- export async function openViewer(lhr) {
93
+ async function openViewer(lhr) {
94
94
  const windowName = 'viewer-' + computeWindowNameSuffix(lhr);
95
95
  const url = getAppsOrigin() + '/viewer/';
96
96
  await openTabWithUrlData({lhr}, url, windowName);
@@ -101,7 +101,7 @@ export async function openViewer(lhr) {
101
101
  * @param {LH.Result} lhr
102
102
  * @protected
103
103
  */
104
- export async function openViewerAndSendData(lhr) {
104
+ async function openViewerAndSendData(lhr) {
105
105
  const windowName = 'viewer-' + computeWindowNameSuffix(lhr);
106
106
  const url = getAppsOrigin() + '/viewer/';
107
107
  openTabAndSendData({lhr}, url, windowName);
@@ -111,7 +111,7 @@ export async function openViewerAndSendData(lhr) {
111
111
  * Opens a new tab to the treemap app and sends the JSON results using URL.fragment
112
112
  * @param {LH.Result} json
113
113
  */
114
- export function openTreemap(json) {
114
+ function openTreemap(json) {
115
115
  const treemapData = json.audits['script-treemap-data'].details;
116
116
  if (!treemapData) {
117
117
  throw new Error('no script treemap data found');
@@ -135,3 +135,9 @@ export function openTreemap(json) {
135
135
 
136
136
  openTabWithUrlData(treemapOptions, url, windowName);
137
137
  }
138
+
139
+ export {
140
+ openViewer,
141
+ openViewerAndSendData,
142
+ openTreemap,
143
+ };
@@ -152,42 +152,62 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
152
152
  return url.href;
153
153
  }
154
154
 
155
+ /**
156
+ * For performance, audits with no group should be a diagnostic or opportunity.
157
+ * The audit details type will determine which of the two groups an audit is in.
158
+ *
159
+ * @param {LH.ReportResult.AuditRef} audit
160
+ * @return {'load-opportunity'|'diagnostic'|null}
161
+ */
162
+ _classifyPerformanceAudit(audit) {
163
+ if (audit.group) return null;
164
+ if (audit.result.details && audit.result.details.type === 'opportunity') {
165
+ return 'load-opportunity';
166
+ }
167
+ return 'diagnostic';
168
+ }
169
+
155
170
  /**
156
171
  * @param {LH.ReportResult.Category} category
157
172
  * @param {Object<string, LH.Result.ReportGroup>} groups
158
- * @param {{gatherMode: LH.Result.GatherMode, environment?: 'PSI'}=} options
173
+ * @param {{gatherMode: LH.Result.GatherMode}=} options
159
174
  * @return {Element}
160
175
  * @override
161
176
  */
162
177
  render(category, groups, options) {
163
178
  const strings = Util.i18n.strings;
164
179
  const element = this.dom.createElement('div', 'lh-category');
165
- if (options && options.environment === 'PSI') {
166
- const gaugeEl = this.dom.createElement('div', 'lh-score__gauge');
167
- gaugeEl.appendChild(this.renderCategoryScore(category, groups, options));
168
- element.appendChild(gaugeEl);
169
- } else {
170
- element.id = category.id;
171
- element.appendChild(this.renderCategoryHeader(category, groups, options));
172
- }
180
+ element.id = category.id;
181
+ element.appendChild(this.renderCategoryHeader(category, groups, options));
173
182
 
174
183
  // Metrics.
175
184
  const metricAudits = category.auditRefs.filter(audit => audit.group === 'metrics');
176
185
  if (metricAudits.length) {
177
- const metricAuditsEl = this.renderAuditGroup(groups.metrics);
186
+ const [metricsGroupEl, metricsFooterEl] = this.renderAuditGroup(groups.metrics);
178
187
 
179
188
  // Metric descriptions toggle.
180
- const toggleTmpl = this.dom.createComponent('metricsToggle');
181
- const _toggleEl = this.dom.find('.lh-metrics-toggle', toggleTmpl);
182
- metricAuditsEl.append(..._toggleEl.childNodes);
183
-
184
- const metricsBoxesEl = this.dom.createChildOf(metricAuditsEl, 'div', 'lh-metrics-container');
185
-
189
+ const checkboxEl = this.dom.createElement('input', 'lh-metrics-toggle__input');
190
+ const checkboxId = `lh-metrics-toggle${Util.getUniqueSuffix()}`;
191
+ checkboxEl.setAttribute('aria-label', 'Toggle the display of metric descriptions');
192
+ checkboxEl.type = 'checkbox';
193
+ checkboxEl.id = checkboxId;
194
+ metricsGroupEl.prepend(checkboxEl);
195
+ const metricHeaderEl = this.dom.find('.lh-audit-group__header', metricsGroupEl);
196
+ const labelEl = this.dom.createChildOf(metricHeaderEl, 'label', 'lh-metrics-toggle__label');
197
+ labelEl.htmlFor = checkboxId;
198
+ const showEl = this.dom.createChildOf(labelEl, 'span', 'lh-metrics-toggle__labeltext--show');
199
+ const hideEl = this.dom.createChildOf(labelEl, 'span', 'lh-metrics-toggle__labeltext--hide');
200
+ showEl.textContent = Util.i18n.strings.expandView;
201
+ hideEl.textContent = Util.i18n.strings.collapseView;
202
+
203
+ const metricsBoxesEl = this.dom.createElement('div', 'lh-metrics-container');
204
+ metricsGroupEl.insertBefore(metricsBoxesEl, metricsFooterEl);
186
205
  metricAudits.forEach(item => {
187
206
  metricsBoxesEl.appendChild(this._renderMetric(item));
188
207
  });
189
208
 
190
- const estValuesEl = this.dom.createChildOf(metricAuditsEl, 'div', 'lh-metrics__disclaimer');
209
+ const descriptionEl = this.dom.find('.lh-category-header__description', element);
210
+ const estValuesEl = this.dom.createChildOf(descriptionEl, 'div', 'lh-metrics__disclaimer');
191
211
  const disclaimerEl = this.dom.convertMarkdownLinkSnippets(strings.varianceDisclaimer);
192
212
  estValuesEl.appendChild(disclaimerEl);
193
213
 
@@ -198,8 +218,8 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
198
218
  this.dom.safelySetHref(calculatorLink, this._getScoringCalculatorHref(category.auditRefs));
199
219
 
200
220
 
201
- metricAuditsEl.classList.add('lh-audit-group--metrics');
202
- element.appendChild(metricAuditsEl);
221
+ metricsGroupEl.classList.add('lh-audit-group--metrics');
222
+ element.appendChild(metricsGroupEl);
203
223
  }
204
224
 
205
225
  // Filmstrip
@@ -214,10 +234,10 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
214
234
 
215
235
  // Opportunities
216
236
  const opportunityAudits = category.auditRefs
217
- .filter(audit => audit.group === 'load-opportunities' && !Util.showAsPassed(audit.result))
237
+ .filter(audit => this._classifyPerformanceAudit(audit) === 'load-opportunity')
238
+ .filter(audit => !Util.showAsPassed(audit.result))
218
239
  .sort((auditA, auditB) => this._getWastedMs(auditB) - this._getWastedMs(auditA));
219
240
 
220
-
221
241
  const filterableMetrics = metricAudits.filter(a => !!a.relevantAudits);
222
242
  // TODO: only add if there are opportunities & diagnostics rendered.
223
243
  if (filterableMetrics.length) {
@@ -230,7 +250,7 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
230
250
  const wastedMsValues = opportunityAudits.map(audit => this._getWastedMs(audit));
231
251
  const maxWaste = Math.max(...wastedMsValues);
232
252
  const scale = Math.max(Math.ceil(maxWaste / 1000) * 1000, minimumScale);
233
- const groupEl = this.renderAuditGroup(groups['load-opportunities']);
253
+ const [groupEl, footerEl] = this.renderAuditGroup(groups['load-opportunities']);
234
254
  const tmpl = this.dom.createComponent('opportunityHeader');
235
255
 
236
256
  this.dom.find('.lh-load-opportunity__col--one', tmpl).textContent =
@@ -239,15 +259,17 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
239
259
  strings.opportunitySavingsColumnLabel;
240
260
 
241
261
  const headerEl = this.dom.find('.lh-load-opportunity__header', tmpl);
242
- groupEl.appendChild(headerEl);
243
- opportunityAudits.forEach(item => groupEl.appendChild(this._renderOpportunity(item, scale)));
262
+ groupEl.insertBefore(headerEl, footerEl);
263
+ opportunityAudits.forEach(item =>
264
+ groupEl.insertBefore(this._renderOpportunity(item, scale), footerEl));
244
265
  groupEl.classList.add('lh-audit-group--load-opportunities');
245
266
  element.appendChild(groupEl);
246
267
  }
247
268
 
248
269
  // Diagnostics
249
270
  const diagnosticAudits = category.auditRefs
250
- .filter(audit => audit.group === 'diagnostics' && !Util.showAsPassed(audit.result))
271
+ .filter(audit => this._classifyPerformanceAudit(audit) === 'diagnostic')
272
+ .filter(audit => !Util.showAsPassed(audit.result))
251
273
  .sort((a, b) => {
252
274
  const scoreA = a.result.scoreDisplayMode === 'informative' ? 100 : Number(a.result.score);
253
275
  const scoreB = b.result.scoreDisplayMode === 'informative' ? 100 : Number(b.result.score);
@@ -255,16 +277,15 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
255
277
  });
256
278
 
257
279
  if (diagnosticAudits.length) {
258
- const groupEl = this.renderAuditGroup(groups['diagnostics']);
259
- diagnosticAudits.forEach(item => groupEl.appendChild(this.renderAudit(item)));
280
+ const [groupEl, footerEl] = this.renderAuditGroup(groups['diagnostics']);
281
+ diagnosticAudits.forEach(item => groupEl.insertBefore(this.renderAudit(item), footerEl));
260
282
  groupEl.classList.add('lh-audit-group--diagnostics');
261
283
  element.appendChild(groupEl);
262
284
  }
263
285
 
264
286
  // Passed audits
265
287
  const passedAudits = category.auditRefs
266
- .filter(audit => (audit.group === 'load-opportunities' || audit.group === 'diagnostics') &&
267
- Util.showAsPassed(audit.result));
288
+ .filter(audit => this._classifyPerformanceAudit(audit) && Util.showAsPassed(audit.result));
268
289
 
269
290
  if (!passedAudits.length) return element;
270
291
 
@@ -284,16 +305,16 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
284
305
  const table = this.detailsRenderer.render(audit.result.details);
285
306
  if (table) {
286
307
  table.id = id;
287
- table.classList.add('lh-audit');
308
+ table.classList.add('lh-details', 'lh-details--budget', 'lh-audit');
288
309
  budgetTableEls.push(table);
289
310
  }
290
311
  }
291
312
  });
292
313
  if (budgetTableEls.length > 0) {
293
- const budgetsGroupEl = this.renderAuditGroup(groups.budgets);
294
- budgetTableEls.forEach(table => budgetsGroupEl.appendChild(table));
295
- budgetsGroupEl.classList.add('lh-audit-group--budgets');
296
- element.appendChild(budgetsGroupEl);
314
+ const [groupEl, footerEl] = this.renderAuditGroup(groups.budgets);
315
+ budgetTableEls.forEach(table => groupEl.insertBefore(table, footerEl));
316
+ groupEl.classList.add('lh-audit-group--budgets');
317
+ element.appendChild(groupEl);
297
318
  }
298
319
 
299
320
  return element;
@@ -47,16 +47,6 @@ export class PwaCategoryRenderer extends CategoryRenderer {
47
47
  return categoryElem;
48
48
  }
49
49
 
50
- /**
51
- * Alias for backcompat.
52
- * @param {LH.ReportResult.Category} category
53
- * @param {Record<string, LH.Result.ReportGroup>} groupDefinitions
54
- * @return {DocumentFragment}
55
- */
56
- renderScoreGauge(category, groupDefinitions) {
57
- return this.renderCategoryScore(category, groupDefinitions);
58
- }
59
-
60
50
  /**
61
51
  * @param {LH.ReportResult.Category} category
62
52
  * @param {Record<string, LH.Result.ReportGroup>} groupDefinitions
@@ -36,22 +36,43 @@ export class ReportRenderer {
36
36
  constructor(dom) {
37
37
  /** @type {DOM} */
38
38
  this._dom = dom;
39
+ /** @type {LH.Renderer.Options} */
40
+ this._opts = {};
39
41
  }
40
42
 
41
43
  /**
42
44
  * @param {LH.Result} lhr
43
- * @param {Element} container Parent element to render the report into.
45
+ * @param {HTMLElement?} rootEl Report root element containing the report
46
+ * @param {LH.Renderer.Options=} opts
44
47
  * @return {!Element}
45
48
  */
46
- renderReport(lhr, container) {
49
+ renderReport(lhr, rootEl, opts) {
50
+ // Allow legacy report rendering API
51
+ if (!this._dom.rootEl && rootEl) {
52
+ console.warn('Please adopt the new report API in renderer/api.js.');
53
+ const closestRoot = rootEl.closest('.lh-root');
54
+ if (closestRoot) {
55
+ this._dom.rootEl = /** @type {HTMLElement} */ (closestRoot);
56
+ } else {
57
+ rootEl.classList.add('lh-root', 'lh-vars');
58
+ this._dom.rootEl = rootEl;
59
+ }
60
+ } else if (this._dom.rootEl && rootEl) {
61
+ // Handle legacy flow-report case
62
+ this._dom.rootEl = rootEl;
63
+ }
64
+ if (opts) {
65
+ this._opts = opts;
66
+ }
67
+
47
68
  this._dom.setLighthouseChannel(lhr.configSettings.channel || 'unknown');
48
69
 
49
70
  const report = Util.prepareReportResult(lhr);
50
71
 
51
- container.textContent = ''; // Remove previous report.
52
- container.appendChild(this._renderReport(report));
72
+ this._dom.rootEl.textContent = ''; // Remove previous report.
73
+ this._dom.rootEl.appendChild(this._renderReport(report));
53
74
 
54
- return container;
75
+ return this._dom.rootEl;
55
76
  }
56
77
 
57
78
  /**
@@ -85,44 +106,64 @@ export class ReportRenderer {
85
106
  _renderReportFooter(report) {
86
107
  const footer = this._dom.createComponent('footer');
87
108
 
88
- const env = this._dom.find('.lh-env__items', footer);
89
- env.id = 'runtime-settings';
90
- this._dom.find('.lh-env__title', footer).textContent = Util.i18n.strings.runtimeSettingsTitle;
91
-
92
- const envValues = Util.getEnvironmentDisplayValues(report.configSettings || {});
93
- const runtimeValues = [
94
- {name: Util.i18n.strings.runtimeSettingsUrl, description: report.finalUrl},
95
- {name: Util.i18n.strings.runtimeSettingsFetchTime,
96
- description: Util.i18n.formatDateTime(report.fetchTime)},
97
- ...envValues,
98
- {name: Util.i18n.strings.runtimeSettingsChannel, description: report.configSettings.channel},
99
- {name: Util.i18n.strings.runtimeSettingsUA, description: report.userAgent},
100
- {name: Util.i18n.strings.runtimeSettingsUANetwork, description: report.environment &&
101
- report.environment.networkUserAgent},
102
- {name: Util.i18n.strings.runtimeSettingsBenchmark, description: report.environment &&
103
- report.environment.benchmarkIndex.toFixed(0)},
104
- ];
105
- if (report.environment.credits && report.environment.credits['axe-core']) {
106
- runtimeValues.push({
107
- name: Util.i18n.strings.runtimeSettingsAxeVersion,
108
- description: report.environment.credits['axe-core'],
109
- });
110
- }
111
-
112
- for (const runtime of runtimeValues) {
113
- if (!runtime.description) continue;
114
-
115
- const item = this._dom.createComponent('envItem');
116
- this._dom.find('.lh-env__name', item).textContent = runtime.name;
117
- this._dom.find('.lh-env__description', item).textContent = runtime.description;
118
- env.appendChild(item);
119
- }
109
+ this._renderMetaBlock(report, footer);
120
110
 
121
111
  this._dom.find('.lh-footer__version_issue', footer).textContent = Util.i18n.strings.footerIssue;
122
112
  this._dom.find('.lh-footer__version', footer).textContent = report.lighthouseVersion;
123
113
  return footer;
124
114
  }
125
115
 
116
+ /**
117
+ * @param {LH.ReportResult} report
118
+ * @param {DocumentFragment} footer
119
+ */
120
+ _renderMetaBlock(report, footer) {
121
+ const envValues = Util.getEmulationDescriptions(report.configSettings || {});
122
+
123
+
124
+ const match = report.userAgent.match(/(\w*Chrome\/[\d.]+)/); // \w* to include 'HeadlessChrome'
125
+ const chromeVer = Array.isArray(match)
126
+ ? match[1].replace('/', ' ').replace('Chrome', 'Chromium')
127
+ : 'Chromium';
128
+ const channel = report.configSettings.channel;
129
+ const benchmarkIndex = report.environment.benchmarkIndex.toFixed(0);
130
+ const axeVersion = report.environment.credits?.['axe-core'];
131
+
132
+ // [CSS icon class, textContent, tooltipText]
133
+ const metaItems = [
134
+ ['date',
135
+ `Captured at ${Util.i18n.formatDateTime(report.fetchTime)}`],
136
+ ['devices',
137
+ `${envValues.deviceEmulation} with Lighthouse ${report.lighthouseVersion}`,
138
+ `${Util.i18n.strings.runtimeSettingsBenchmark}: ${benchmarkIndex}` +
139
+ `\n${Util.i18n.strings.runtimeSettingsCPUThrottling}: ${envValues.cpuThrottling}` +
140
+ (axeVersion ? `\n${Util.i18n.strings.runtimeSettingsAxeVersion}: ${axeVersion}` : '')],
141
+ ['samples-one',
142
+ Util.i18n.strings.runtimeSingleLoad,
143
+ Util.i18n.strings.runtimeSingleLoadTooltip],
144
+ ['stopwatch',
145
+ Util.i18n.strings.runtimeAnalysisWindow],
146
+ ['networkspeed',
147
+ `${envValues.summary}`,
148
+ `${Util.i18n.strings.runtimeSettingsNetworkThrottling}: ${envValues.networkThrottling}`],
149
+ ['chrome',
150
+ `Using ${chromeVer}` + (channel ? ` with ${channel}` : ''),
151
+ `${Util.i18n.strings.runtimeSettingsUANetwork}: "${report.environment.networkUserAgent}"`],
152
+ ];
153
+
154
+ const metaItemsEl = this._dom.find('.lh-meta__items', footer);
155
+ for (const [iconname, text, tooltip] of metaItems) {
156
+ const itemEl = this._dom.createChildOf(metaItemsEl, 'li', 'lh-meta__item');
157
+ itemEl.textContent = text;
158
+ if (tooltip) {
159
+ itemEl.classList.add('lh-tooltip-boundary');
160
+ const tooltipEl = this._dom.createChildOf(itemEl, 'div', 'lh-tooltip');
161
+ tooltipEl.textContent = tooltip;
162
+ }
163
+ itemEl.classList.add('lh-report-icon', `lh-report-icon--${iconname}`);
164
+ }
165
+ }
166
+
126
167
  /**
127
168
  * Returns a div with a list of top-level warnings, or an empty div if no warnings.
128
169
  * @param {LH.ReportResult} report
@@ -177,7 +218,7 @@ export class ReportRenderer {
177
218
  gaugeWrapperEl.addEventListener('click', e => {
178
219
  if (!gaugeWrapperEl.matches('[href^="#"]')) return;
179
220
  const selector = gaugeWrapperEl.getAttribute('href');
180
- const reportRoot = gaugeWrapperEl.closest('.lh-vars');
221
+ const reportRoot = this._dom.rootEl;
181
222
  if (!selector || !reportRoot) return;
182
223
  const destEl = this._dom.find(selector, reportRoot);
183
224
  e.preventDefault();
@@ -192,7 +233,7 @@ export class ReportRenderer {
192
233
  // The renderer for default categories is just the default CategoryRenderer.
193
234
  // If the functions are equal, then renderer is an instance of CategoryRenderer.
194
235
  // For example, the PWA category uses PwaCategoryRenderer, which overrides
195
- // CategoryRenderer.renderScoreGauge, so it would fail this check and be placed
236
+ // CategoryRenderer.renderCategoryScore, so it would fail this check and be placed
196
237
  // in the customGauges bucket.
197
238
  defaultGauges.push(categoryGauge);
198
239
  } else {
@@ -247,8 +288,10 @@ export class ReportRenderer {
247
288
  headerContainer.classList.add('lh-header--solo-category');
248
289
  }
249
290
 
291
+ const scoreScale = this._dom.createElement('div');
292
+ scoreScale.classList.add('lh-scorescale-wrap');
293
+ scoreScale.append(this._dom.createComponent('scorescale'));
250
294
  if (scoreHeader) {
251
- const scoreScale = this._dom.createComponent('scorescale');
252
295
  const scoresContainer = this._dom.find('.lh-scores-container', headerContainer);
253
296
  scoreHeader.append(
254
297
  ...this._renderScoreGauges(report, categoryRenderer, specificCategoryRenderers));
@@ -266,7 +309,7 @@ export class ReportRenderer {
266
309
  for (const category of Object.values(report.categories)) {
267
310
  const renderer = specificCategoryRenderers[category.id] || categoryRenderer;
268
311
  // .lh-category-wrapper is full-width and provides horizontal rules between categories.
269
- // .lh-category within has the max-width: var(--report-width);
312
+ // .lh-category within has the max-width: var(--report-content-width);
270
313
  const wrapper = renderer.dom.createChildOf(categories, 'div', 'lh-category-wrapper');
271
314
  wrapper.appendChild(renderer.render(
272
315
  category,
@@ -275,11 +318,15 @@ export class ReportRenderer {
275
318
  ));
276
319
  }
277
320
 
321
+ categoryRenderer.injectFinalScreenshot(categories, report.audits, scoreScale);
322
+
278
323
  const reportFragment = this._dom.createFragment();
279
324
  reportFragment.append(this._dom.createComponent('styles'));
280
- const topbarDocumentFragment = this._renderReportTopbar(report);
281
325
 
282
- reportFragment.appendChild(topbarDocumentFragment);
326
+ if (!this._opts.omitTopbar) {
327
+ reportFragment.appendChild(this._renderReportTopbar(report));
328
+ }
329
+
283
330
  reportFragment.appendChild(reportContainer);
284
331
  reportContainer.appendChild(headerContainer);
285
332
  reportContainer.appendChild(reportSection);
@@ -287,7 +334,7 @@ export class ReportRenderer {
287
334
 
288
335
  if (fullPageScreenshot) {
289
336
  ElementScreenshotRenderer.installFullPageScreenshot(
290
- reportContainer, fullPageScreenshot.screenshot);
337
+ this._dom.rootEl, fullPageScreenshot.screenshot);
291
338
  }
292
339
 
293
340
  return reportFragment;