lighthouse 8.6.0-dev.20211030 → 8.6.0-dev.20211103

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 (132) hide show
  1. package/dist/report/flow.js +13 -13
  2. package/dist/report/standalone.js +41 -35
  3. package/flow-report/.eslintrc.cjs +2 -0
  4. package/flow-report/assets/standalone-flow-template.html +3 -2
  5. package/flow-report/assets/styles.css +1 -1
  6. package/flow-report/src/app.tsx +10 -11
  7. package/flow-report/src/common.tsx +22 -9
  8. package/flow-report/src/header.tsx +3 -3
  9. package/flow-report/src/i18n/i18n.tsx +10 -6
  10. package/flow-report/src/icons.tsx +30 -9
  11. package/flow-report/src/sidebar/flow.tsx +3 -3
  12. package/flow-report/src/sidebar/sidebar.tsx +30 -11
  13. package/flow-report/src/summary/category.tsx +9 -4
  14. package/flow-report/src/summary/summary.tsx +9 -3
  15. package/flow-report/src/util.ts +31 -19
  16. package/flow-report/src/wrappers/report-renderer.tsx +10 -3
  17. package/flow-report/src/wrappers/report.tsx +19 -10
  18. package/flow-report/standalone-flow.tsx +4 -3
  19. package/flow-report/test/common-test.tsx +25 -10
  20. package/flow-report/test/header-test.tsx +6 -6
  21. package/flow-report/test/setup/env-setup.ts +1 -1
  22. package/flow-report/test/sidebar/sidebar-test.tsx +43 -1
  23. package/flow-report/test/summary/category-test.tsx +5 -5
  24. package/flow-report/test/util-test.tsx +32 -11
  25. package/lighthouse-cli/.eslintrc.cjs +2 -0
  26. package/lighthouse-cli/cli-flags.js +1 -1
  27. package/lighthouse-cli/test/smokehouse/lighthouse-runners/bundle.js +4 -2
  28. package/lighthouse-cli/test/smokehouse/smokehouse.js +1 -1
  29. package/lighthouse-core/audits/audit.js +1 -1
  30. package/lighthouse-core/computed/metrics/lantern-total-blocking-time.js +3 -4
  31. package/lighthouse-core/computed/metrics/tbt-utils.js +57 -0
  32. package/lighthouse-core/computed/metrics/total-blocking-time.js +3 -53
  33. package/lighthouse-core/computed/resource-summary.js +1 -1
  34. package/lighthouse-core/config/config-helpers.js +20 -10
  35. package/lighthouse-core/config/default-config.js +59 -55
  36. package/lighthouse-core/fraggle-rock/config/default-config.js +1 -1
  37. package/lighthouse-core/fraggle-rock/gather/base-gatherer.js +6 -1
  38. package/lighthouse-core/gather/connections/cri.js +1 -1
  39. package/lighthouse-core/gather/gatherers/dobetterweb/response-compression.js +1 -0
  40. package/lighthouse-core/lib/asset-saver.js +4 -1
  41. package/lighthouse-core/lib/lh-env.js +2 -0
  42. package/lighthouse-core/lib/proto-preprocessor.js +14 -2
  43. package/lighthouse-core/lib/tappable-rects.js +8 -15
  44. package/lighthouse-core/lib/url-shim.js +1 -1
  45. package/lighthouse-core/runner.js +1 -1
  46. package/lighthouse-core/util-commonjs.js +25 -11
  47. package/package.json +11 -8
  48. package/report/.eslintrc.cjs +2 -0
  49. package/report/assets/standalone-template.html +2 -3
  50. package/report/assets/styles.css +227 -251
  51. package/report/assets/templates.html +19 -29
  52. package/report/clients/bundle.js +1 -0
  53. package/report/clients/standalone.js +6 -15
  54. package/report/renderer/api.js +32 -0
  55. package/report/renderer/category-renderer.js +22 -18
  56. package/report/renderer/components.js +46 -79
  57. package/report/renderer/crc-details-renderer.js +15 -12
  58. package/report/renderer/dom.js +16 -1
  59. package/report/renderer/drop-down-menu.js +2 -2
  60. package/report/renderer/element-screenshot-renderer.js +8 -8
  61. package/report/renderer/features-util.js +1 -1
  62. package/report/renderer/open-tab.js +9 -3
  63. package/report/renderer/performance-category-renderer.js +51 -24
  64. package/report/renderer/report-renderer.js +34 -10
  65. package/report/renderer/report-ui-features.js +36 -33
  66. package/report/renderer/swap-locale-feature.js +3 -3
  67. package/report/renderer/topbar-features.js +19 -21
  68. package/report/renderer/util.js +26 -11
  69. package/report/test/clients/bundle-test.js +3 -0
  70. package/report/test/renderer/category-renderer-test.js +2 -2
  71. package/report/test/renderer/performance-category-renderer-test.js +35 -9
  72. package/report/test/renderer/report-renderer-axe-test.js +5 -2
  73. package/report/test/renderer/report-renderer-test.js +14 -11
  74. package/report/test/renderer/report-ui-features-test.js +14 -10
  75. package/report/test/renderer/util-test.js +13 -31
  76. package/report/test-assets/faux-psi-template.html +2 -7
  77. package/report/test-assets/faux-psi.js +20 -39
  78. package/report/types/html-renderer.d.ts +2 -0
  79. package/report/types/report-renderer.d.ts +23 -0
  80. package/shared/localization/format.js +9 -4
  81. package/shared/localization/locales/ar-XB.json +0 -15
  82. package/shared/localization/locales/ar.json +0 -15
  83. package/shared/localization/locales/bg.json +0 -15
  84. package/shared/localization/locales/ca.json +0 -15
  85. package/shared/localization/locales/cs.json +0 -15
  86. package/shared/localization/locales/da.json +0 -15
  87. package/shared/localization/locales/de.json +0 -15
  88. package/shared/localization/locales/el.json +0 -15
  89. package/shared/localization/locales/en-GB.json +0 -15
  90. package/shared/localization/locales/en-US.json +19 -16
  91. package/shared/localization/locales/en-XA.json +0 -15
  92. package/shared/localization/locales/en-XL.json +19 -16
  93. package/shared/localization/locales/es-419.json +0 -15
  94. package/shared/localization/locales/es.json +0 -15
  95. package/shared/localization/locales/fi.json +0 -15
  96. package/shared/localization/locales/fil.json +0 -15
  97. package/shared/localization/locales/fr.json +0 -15
  98. package/shared/localization/locales/he.json +0 -15
  99. package/shared/localization/locales/hi.json +0 -15
  100. package/shared/localization/locales/hr.json +0 -15
  101. package/shared/localization/locales/hu.json +0 -15
  102. package/shared/localization/locales/id.json +0 -15
  103. package/shared/localization/locales/it.json +0 -15
  104. package/shared/localization/locales/ja.json +0 -15
  105. package/shared/localization/locales/ko.json +0 -15
  106. package/shared/localization/locales/lt.json +0 -15
  107. package/shared/localization/locales/lv.json +0 -15
  108. package/shared/localization/locales/nl.json +0 -15
  109. package/shared/localization/locales/no.json +0 -15
  110. package/shared/localization/locales/pl.json +0 -15
  111. package/shared/localization/locales/pt-PT.json +0 -15
  112. package/shared/localization/locales/pt.json +0 -15
  113. package/shared/localization/locales/ro.json +0 -15
  114. package/shared/localization/locales/ru.json +0 -15
  115. package/shared/localization/locales/sk.json +0 -15
  116. package/shared/localization/locales/sl.json +0 -15
  117. package/shared/localization/locales/sr-Latn.json +0 -15
  118. package/shared/localization/locales/sr.json +0 -15
  119. package/shared/localization/locales/sv.json +0 -15
  120. package/shared/localization/locales/ta.json +0 -15
  121. package/shared/localization/locales/te.json +0 -15
  122. package/shared/localization/locales/th.json +0 -15
  123. package/shared/localization/locales/tr.json +0 -15
  124. package/shared/localization/locales/uk.json +0 -15
  125. package/shared/localization/locales/vi.json +0 -15
  126. package/shared/localization/locales/zh-HK.json +0 -15
  127. package/shared/localization/locales/zh-TW.json +0 -15
  128. package/shared/localization/locales/zh.json +0 -15
  129. package/third-party/chromium-synchronization/installability-errors-test.js +2 -0
  130. package/types/lhr/flow.d.ts +3 -2
  131. package/types/rollup-plugin-postprocess.d.ts +10 -0
  132. package/lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js +0 -96
@@ -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,6 +152,21 @@ 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
@@ -168,15 +183,26 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
168
183
  // Metrics.
169
184
  const metricAudits = category.auditRefs.filter(audit => audit.group === 'metrics');
170
185
  if (metricAudits.length) {
171
- const metricAuditsEl = this.renderAuditGroup(groups.metrics);
186
+ const [metricsGroupEl, metricsFooterEl] = this.renderAuditGroup(groups.metrics);
172
187
 
173
188
  // Metric descriptions toggle.
174
- const toggleTmpl = this.dom.createComponent('metricsToggle');
175
- const _toggleEl = this.dom.find('.lh-metrics-toggle', toggleTmpl);
176
- metricAuditsEl.append(..._toggleEl.childNodes);
177
-
178
- const metricsBoxesEl = this.dom.createChildOf(metricAuditsEl, 'div', 'lh-metrics-container');
179
-
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 metricAudits = category.auditRefs.filter(audit => audit.group === 'metrics');
204
+ const metricsBoxesEl = this.dom.createElement('div', 'lh-metrics-container');
205
+ metricsGroupEl.insertBefore(metricsBoxesEl, metricsFooterEl);
180
206
  metricAudits.forEach(item => {
181
207
  metricsBoxesEl.appendChild(this._renderMetric(item));
182
208
  });
@@ -193,8 +219,8 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
193
219
  this.dom.safelySetHref(calculatorLink, this._getScoringCalculatorHref(category.auditRefs));
194
220
 
195
221
 
196
- metricAuditsEl.classList.add('lh-audit-group--metrics');
197
- element.appendChild(metricAuditsEl);
222
+ metricsGroupEl.classList.add('lh-audit-group--metrics');
223
+ element.appendChild(metricsGroupEl);
198
224
  }
199
225
 
200
226
  // Filmstrip
@@ -209,10 +235,10 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
209
235
 
210
236
  // Opportunities
211
237
  const opportunityAudits = category.auditRefs
212
- .filter(audit => audit.group === 'load-opportunities' && !Util.showAsPassed(audit.result))
238
+ .filter(audit => this._classifyPerformanceAudit(audit) === 'load-opportunity')
239
+ .filter(audit => !Util.showAsPassed(audit.result))
213
240
  .sort((auditA, auditB) => this._getWastedMs(auditB) - this._getWastedMs(auditA));
214
241
 
215
-
216
242
  const filterableMetrics = metricAudits.filter(a => !!a.relevantAudits);
217
243
  // TODO: only add if there are opportunities & diagnostics rendered.
218
244
  if (filterableMetrics.length) {
@@ -225,7 +251,7 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
225
251
  const wastedMsValues = opportunityAudits.map(audit => this._getWastedMs(audit));
226
252
  const maxWaste = Math.max(...wastedMsValues);
227
253
  const scale = Math.max(Math.ceil(maxWaste / 1000) * 1000, minimumScale);
228
- const groupEl = this.renderAuditGroup(groups['load-opportunities']);
254
+ const [groupEl, footerEl] = this.renderAuditGroup(groups['load-opportunities']);
229
255
  const tmpl = this.dom.createComponent('opportunityHeader');
230
256
 
231
257
  this.dom.find('.lh-load-opportunity__col--one', tmpl).textContent =
@@ -234,15 +260,17 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
234
260
  strings.opportunitySavingsColumnLabel;
235
261
 
236
262
  const headerEl = this.dom.find('.lh-load-opportunity__header', tmpl);
237
- groupEl.appendChild(headerEl);
238
- opportunityAudits.forEach(item => groupEl.appendChild(this._renderOpportunity(item, scale)));
263
+ groupEl.insertBefore(headerEl, footerEl);
264
+ opportunityAudits.forEach(item =>
265
+ groupEl.insertBefore(this._renderOpportunity(item, scale), footerEl));
239
266
  groupEl.classList.add('lh-audit-group--load-opportunities');
240
267
  element.appendChild(groupEl);
241
268
  }
242
269
 
243
270
  // Diagnostics
244
271
  const diagnosticAudits = category.auditRefs
245
- .filter(audit => audit.group === 'diagnostics' && !Util.showAsPassed(audit.result))
272
+ .filter(audit => this._classifyPerformanceAudit(audit) === 'diagnostic')
273
+ .filter(audit => !Util.showAsPassed(audit.result))
246
274
  .sort((a, b) => {
247
275
  const scoreA = a.result.scoreDisplayMode === 'informative' ? 100 : Number(a.result.score);
248
276
  const scoreB = b.result.scoreDisplayMode === 'informative' ? 100 : Number(b.result.score);
@@ -250,16 +278,15 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
250
278
  });
251
279
 
252
280
  if (diagnosticAudits.length) {
253
- const groupEl = this.renderAuditGroup(groups['diagnostics']);
254
- diagnosticAudits.forEach(item => groupEl.appendChild(this.renderAudit(item)));
281
+ const [groupEl, footerEl] = this.renderAuditGroup(groups['diagnostics']);
282
+ diagnosticAudits.forEach(item => groupEl.insertBefore(this.renderAudit(item), footerEl));
255
283
  groupEl.classList.add('lh-audit-group--diagnostics');
256
284
  element.appendChild(groupEl);
257
285
  }
258
286
 
259
287
  // Passed audits
260
288
  const passedAudits = category.auditRefs
261
- .filter(audit => (audit.group === 'load-opportunities' || audit.group === 'diagnostics') &&
262
- Util.showAsPassed(audit.result));
289
+ .filter(audit => this._classifyPerformanceAudit(audit) && Util.showAsPassed(audit.result));
263
290
 
264
291
  if (!passedAudits.length) return element;
265
292
 
@@ -279,16 +306,16 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
279
306
  const table = this.detailsRenderer.render(audit.result.details);
280
307
  if (table) {
281
308
  table.id = id;
282
- table.classList.add('lh-audit');
309
+ table.classList.add('lh-details', 'lh-details--budget', 'lh-audit');
283
310
  budgetTableEls.push(table);
284
311
  }
285
312
  }
286
313
  });
287
314
  if (budgetTableEls.length > 0) {
288
- const budgetsGroupEl = this.renderAuditGroup(groups.budgets);
289
- budgetTableEls.forEach(table => budgetsGroupEl.appendChild(table));
290
- budgetsGroupEl.classList.add('lh-audit-group--budgets');
291
- element.appendChild(budgetsGroupEl);
315
+ const [groupEl, footerEl] = this.renderAuditGroup(groups.budgets);
316
+ budgetTableEls.forEach(table => groupEl.insertBefore(table, footerEl));
317
+ groupEl.classList.add('lh-audit-group--budgets');
318
+ element.appendChild(groupEl);
292
319
  }
293
320
 
294
321
  return element;
@@ -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
  /**
@@ -197,7 +218,7 @@ export class ReportRenderer {
197
218
  gaugeWrapperEl.addEventListener('click', e => {
198
219
  if (!gaugeWrapperEl.matches('[href^="#"]')) return;
199
220
  const selector = gaugeWrapperEl.getAttribute('href');
200
- const reportRoot = gaugeWrapperEl.closest('.lh-vars');
221
+ const reportRoot = this._dom.rootEl;
201
222
  if (!selector || !reportRoot) return;
202
223
  const destEl = this._dom.find(selector, reportRoot);
203
224
  e.preventDefault();
@@ -268,6 +289,7 @@ export class ReportRenderer {
268
289
  }
269
290
 
270
291
  const scoreScale = this._dom.createElement('div');
292
+ scoreScale.classList.add('lh-scorescale-wrap');
271
293
  scoreScale.append(this._dom.createComponent('scorescale'));
272
294
  if (scoreHeader) {
273
295
  const scoresContainer = this._dom.find('.lh-scores-container', headerContainer);
@@ -287,7 +309,7 @@ export class ReportRenderer {
287
309
  for (const category of Object.values(report.categories)) {
288
310
  const renderer = specificCategoryRenderers[category.id] || categoryRenderer;
289
311
  // .lh-category-wrapper is full-width and provides horizontal rules between categories.
290
- // .lh-category within has the max-width: var(--report-width);
312
+ // .lh-category within has the max-width: var(--report-content-width);
291
313
  const wrapper = renderer.dom.createChildOf(categories, 'div', 'lh-category-wrapper');
292
314
  wrapper.appendChild(renderer.render(
293
315
  category,
@@ -300,9 +322,11 @@ export class ReportRenderer {
300
322
 
301
323
  const reportFragment = this._dom.createFragment();
302
324
  reportFragment.append(this._dom.createComponent('styles'));
303
- const topbarDocumentFragment = this._renderReportTopbar(report);
304
325
 
305
- reportFragment.appendChild(topbarDocumentFragment);
326
+ if (!this._opts.omitTopbar) {
327
+ reportFragment.appendChild(this._renderReportTopbar(report));
328
+ }
329
+
306
330
  reportFragment.appendChild(reportContainer);
307
331
  reportContainer.appendChild(headerContainer);
308
332
  reportContainer.appendChild(reportSection);
@@ -310,7 +334,7 @@ export class ReportRenderer {
310
334
 
311
335
  if (fullPageScreenshot) {
312
336
  ElementScreenshotRenderer.installFullPageScreenshot(
313
- reportContainer, fullPageScreenshot.screenshot);
337
+ this._dom.rootEl, fullPageScreenshot.screenshot);
314
338
  }
315
339
 
316
340
  return reportFragment;
@@ -40,16 +40,17 @@ function getTableRows(tableEl) {
40
40
  export class ReportUIFeatures {
41
41
  /**
42
42
  * @param {DOM} dom
43
+ * @param {LH.Renderer.Options} opts
43
44
  */
44
- constructor(dom) {
45
+ constructor(dom, opts = {}) {
45
46
  /** @type {LH.Result} */
46
47
  this.json; // eslint-disable-line no-unused-expressions
47
48
  /** @type {DOM} */
48
49
  this._dom = dom;
49
- /** @type {Document} */
50
- this._document = this._dom.document();
51
- this._topbar = new TopbarFeatures(this, dom);
52
50
 
51
+ this._opts = opts;
52
+
53
+ this._topbar = opts.omitTopbar ? null : new TopbarFeatures(this, dom);
53
54
  this.onMediaQueryChange = this.onMediaQueryChange.bind(this);
54
55
  }
55
56
 
@@ -61,16 +62,19 @@ export class ReportUIFeatures {
61
62
  initFeatures(lhr) {
62
63
  this.json = lhr;
63
64
 
64
- this._topbar.enable(lhr);
65
- this._topbar.resetUIState();
65
+ if (this._topbar) {
66
+ this._topbar.enable(lhr);
67
+ this._topbar.resetUIState();
68
+ }
66
69
  this._setupMediaQueryListeners();
67
70
  this._setupThirdPartyFilter();
68
- this._setupElementScreenshotOverlay(this._dom.find('.lh-container', this._document));
71
+ this._setupElementScreenshotOverlay(this._dom.rootEl);
69
72
 
70
73
  let turnOffTheLights = false;
71
74
  // Do not query the system preferences for DevTools - DevTools should only apply dark theme
72
75
  // if dark is selected in the settings panel.
73
- if (!this._dom.isDevTools() && window.matchMedia('(prefers-color-scheme: dark)').matches) {
76
+ const disableDarkMode = this._dom.isDevTools() || this._opts.disableAutoDarkModeAndFireworks;
77
+ if (!disableDarkMode && window.matchMedia('(prefers-color-scheme: dark)').matches) {
74
78
  turnOffTheLights = true;
75
79
  }
76
80
 
@@ -94,7 +98,7 @@ export class ReportUIFeatures {
94
98
  const hasMetricError = lhr.categories.performance && lhr.categories.performance.auditRefs
95
99
  .some(audit => Boolean(audit.group === 'metrics' && lhr.audits[audit.id].errorMessage));
96
100
  if (hasMetricError) {
97
- const toggleInputEl = this._dom.find('input.lh-metrics-toggle__input', this._document);
101
+ const toggleInputEl = this._dom.find('input.lh-metrics-toggle__input', this._dom.rootEl);
98
102
  toggleInputEl.checked = true;
99
103
  }
100
104
 
@@ -109,7 +113,7 @@ export class ReportUIFeatures {
109
113
  }
110
114
 
111
115
  // Fill in all i18n data.
112
- for (const node of this._dom.findAll('[data-i18n]', this._dom.document())) {
116
+ for (const node of this._dom.findAll('[data-i18n]', this._dom.rootEl)) {
113
117
  // These strings are guaranteed to (at least) have a default English string in Util.UIStrings,
114
118
  // so this cannot be undefined as long as `report-ui-features.data-i18n` test passes.
115
119
  const i18nKey = node.getAttribute('data-i18n');
@@ -119,18 +123,15 @@ export class ReportUIFeatures {
119
123
  }
120
124
 
121
125
  /**
122
- * @param {{container?: Element, text: string, icon?: string, onClick: () => void}} opts
126
+ * @param {{text: string, icon?: string, onClick: () => void}} opts
123
127
  */
124
128
  addButton(opts) {
125
- // report-ui-features doesn't have a reference to the root report el, and PSI has
126
- // 2 reports on the page (and not even attached to DOM when installFeatures is called..)
127
- // so we need a container option to specify where the element should go.
128
- const metricsEl = this._document.querySelector('.lh-audit-group--metrics');
129
- const containerEl = opts.container || metricsEl;
130
- if (!containerEl) return;
129
+ // Use qSA directly to as we don't want to throw (if this element is missing).
130
+ const metricsEl = this._dom.rootEl.querySelector('.lh-audit-group--metrics');
131
+ if (!metricsEl) return;
131
132
 
132
- let buttonsEl = containerEl.querySelector('.lh-buttons');
133
- if (!buttonsEl) buttonsEl = this._dom.createChildOf(containerEl, 'div', 'lh-buttons');
133
+ let buttonsEl = metricsEl.querySelector('.lh-buttons');
134
+ if (!buttonsEl) buttonsEl = this._dom.createChildOf(metricsEl, 'div', 'lh-buttons');
134
135
 
135
136
  const classes = [
136
137
  'lh-button',
@@ -150,8 +151,10 @@ export class ReportUIFeatures {
150
151
  * @return {string}
151
152
  */
152
153
  getReportHtml() {
153
- this._topbar.resetUIState();
154
- return this._document.documentElement.outerHTML;
154
+ if (this._topbar) {
155
+ this._topbar.resetUIState();
156
+ }
157
+ return `<!doctype html><body>${this._dom.rootEl.outerHTML}`;
155
158
  }
156
159
 
157
160
  /**
@@ -163,7 +166,7 @@ export class ReportUIFeatures {
163
166
  }
164
167
 
165
168
  _enableFireworks() {
166
- const scoresContainer = this._dom.find('.lh-scores-container', this._document);
169
+ const scoresContainer = this._dom.find('.lh-scores-container', this._dom.rootEl);
167
170
  scoresContainer.classList.add('lh-score100');
168
171
  scoresContainer.addEventListener('click', _ => {
169
172
  scoresContainer.classList.toggle('lh-fireworks-paused');
@@ -183,7 +186,9 @@ export class ReportUIFeatures {
183
186
  * be in their closed state (not opened) and the templates should be unstamped.
184
187
  */
185
188
  _resetUIState() {
186
- this._topbar.resetUIState();
189
+ if (this._topbar) {
190
+ this._topbar.resetUIState();
191
+ }
187
192
  }
188
193
 
189
194
  /**
@@ -191,8 +196,7 @@ export class ReportUIFeatures {
191
196
  * @param {MediaQueryList|MediaQueryListEvent} mql
192
197
  */
193
198
  onMediaQueryChange(mql) {
194
- const root = this._dom.find('.lh-root', this._document);
195
- root.classList.toggle('lh-narrow', mql.matches);
199
+ this._dom.rootEl.classList.toggle('lh-narrow', mql.matches);
196
200
  }
197
201
 
198
202
  _setupThirdPartyFilter() {
@@ -209,7 +213,7 @@ export class ReportUIFeatures {
209
213
  ];
210
214
 
211
215
  // Get all tables with a text url column.
212
- const tables = Array.from(this._document.querySelectorAll('table.lh-table'));
216
+ const tables = Array.from(this._dom.rootEl.querySelectorAll('table.lh-table'));
213
217
  const tablesWithUrls = tables
214
218
  .filter(el =>
215
219
  el.querySelector('td.lh-table-column--url, td.lh-table-column--source-location'))
@@ -256,10 +260,9 @@ export class ReportUIFeatures {
256
260
  const allThirdParty = thirdPartyRows.length === rowEls.length;
257
261
  const allFirstParty = !thirdPartyRows.length;
258
262
 
259
- // If all or none of the rows are 3rd party, disable the checkbox.
263
+ // If all or none of the rows are 3rd party, hide the control.
260
264
  if (allThirdParty || allFirstParty) {
261
- filterInput.disabled = true;
262
- filterInput.checked = allThirdParty;
265
+ this._dom.find('div.lh-3p-filter', filterTemplate).hidden = true;
263
266
  }
264
267
 
265
268
  // Add checkbox to the DOM.
@@ -276,9 +279,9 @@ export class ReportUIFeatures {
276
279
  }
277
280
 
278
281
  /**
279
- * @param {Element} el
282
+ * @param {Element} rootEl
280
283
  */
281
- _setupElementScreenshotOverlay(el) {
284
+ _setupElementScreenshotOverlay(rootEl) {
282
285
  const fullPageScreenshot =
283
286
  this.json.audits['full-page-screenshot'] &&
284
287
  this.json.audits['full-page-screenshot'].details &&
@@ -288,8 +291,8 @@ export class ReportUIFeatures {
288
291
 
289
292
  ElementScreenshotRenderer.installOverlayFeature({
290
293
  dom: this._dom,
291
- reportEl: el,
292
- overlayContainerEl: el,
294
+ rootEl: rootEl,
295
+ overlayContainerEl: rootEl,
293
296
  fullPageScreenshot,
294
297
  });
295
298
  }
@@ -33,17 +33,17 @@ export class SwapLocaleFeature {
33
33
  throw new Error('missing icuMessagePaths');
34
34
  }
35
35
 
36
- this._dom.find('.lh-tools-locale', this._dom.document()).classList.remove('lh-hidden');
36
+ this._dom.find('.lh-tools-locale', this._dom.rootEl).classList.remove('lh-hidden');
37
37
 
38
38
  const currentLocale = this._reportUIFeatures.json.configSettings.locale;
39
39
 
40
- const containerEl = this._dom.find('.lh-tools-locale__selector-wrapper', this._dom.document());
40
+ const containerEl = this._dom.find('.lh-tools-locale__selector-wrapper', this._dom.rootEl);
41
41
  containerEl.removeAttribute('aria-hidden');
42
42
  const selectEl = this._dom.createChildOf(containerEl, 'select', 'lh-locale-selector');
43
43
  selectEl.name = 'lh-locale-list';
44
44
  selectEl.setAttribute('role', 'menuitem');
45
45
 
46
- const toggleEl = this._dom.find('.lh-tool-locale__button', this._dom.document());
46
+ const toggleEl = this._dom.find('.lh-tool-locale__button', this._dom.rootEl);
47
47
  toggleEl.addEventListener('click', () => {
48
48
  toggleEl.classList.toggle('lh-active');
49
49
  });