lighthouse 8.6.0-dev.20211101 → 8.6.0-dev.20211105

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 (107) hide show
  1. package/dist/report/flow.js +14 -14
  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/common.tsx +11 -4
  7. package/flow-report/src/i18n/i18n.tsx +10 -6
  8. package/flow-report/src/icons.tsx +30 -9
  9. package/flow-report/src/sidebar/sidebar.tsx +27 -8
  10. package/flow-report/src/summary/category.tsx +7 -2
  11. package/flow-report/src/summary/summary.tsx +9 -3
  12. package/flow-report/src/util.ts +21 -9
  13. package/flow-report/src/wrappers/report-renderer.tsx +10 -3
  14. package/flow-report/src/wrappers/report.tsx +9 -5
  15. package/flow-report/standalone-flow.tsx +4 -3
  16. package/flow-report/test/setup/env-setup.ts +1 -1
  17. package/flow-report/test/sidebar/sidebar-test.tsx +43 -1
  18. package/lighthouse-cli/.eslintrc.cjs +2 -0
  19. package/lighthouse-cli/cli-flags.js +1 -1
  20. package/lighthouse-core/audits/audit.js +1 -1
  21. package/lighthouse-core/computed/resource-summary.js +1 -1
  22. package/lighthouse-core/config/default-config.js +0 -2
  23. package/lighthouse-core/gather/connections/cri.js +1 -1
  24. package/lighthouse-core/lib/proto-preprocessor.js +14 -2
  25. package/lighthouse-core/lib/url-shim.js +1 -1
  26. package/lighthouse-core/util-commonjs.js +24 -9
  27. package/package.json +3 -3
  28. package/report/.eslintrc.cjs +2 -0
  29. package/report/assets/standalone-template.html +2 -3
  30. package/report/assets/styles.css +227 -251
  31. package/report/assets/templates.html +19 -29
  32. package/report/clients/bundle.js +1 -0
  33. package/report/clients/standalone.js +6 -13
  34. package/report/renderer/api.js +32 -0
  35. package/report/renderer/category-renderer.js +22 -18
  36. package/report/renderer/components.js +46 -79
  37. package/report/renderer/crc-details-renderer.js +15 -12
  38. package/report/renderer/dom.js +16 -1
  39. package/report/renderer/drop-down-menu.js +2 -2
  40. package/report/renderer/element-screenshot-renderer.js +6 -6
  41. package/report/renderer/features-util.js +1 -1
  42. package/report/renderer/open-tab.js +9 -3
  43. package/report/renderer/performance-category-renderer.js +31 -19
  44. package/report/renderer/report-renderer.js +34 -10
  45. package/report/renderer/report-ui-features.js +36 -33
  46. package/report/renderer/swap-locale-feature.js +3 -3
  47. package/report/renderer/topbar-features.js +52 -48
  48. package/report/renderer/util.js +25 -9
  49. package/report/test/clients/bundle-test.js +8 -0
  50. package/report/test/renderer/category-renderer-test.js +2 -2
  51. package/report/test/renderer/performance-category-renderer-test.js +1 -1
  52. package/report/test/renderer/report-renderer-axe-test.js +5 -2
  53. package/report/test/renderer/report-renderer-test.js +13 -10
  54. package/report/test/renderer/report-ui-features-test.js +17 -10
  55. package/report/test-assets/faux-psi-template.html +2 -7
  56. package/report/test-assets/faux-psi.js +20 -39
  57. package/report/types/html-renderer.d.ts +2 -0
  58. package/report/types/report-renderer.d.ts +23 -0
  59. package/shared/localization/locales/ar-XB.json +0 -15
  60. package/shared/localization/locales/ar.json +0 -15
  61. package/shared/localization/locales/bg.json +0 -15
  62. package/shared/localization/locales/ca.json +0 -15
  63. package/shared/localization/locales/cs.json +0 -15
  64. package/shared/localization/locales/da.json +0 -15
  65. package/shared/localization/locales/de.json +0 -15
  66. package/shared/localization/locales/el.json +0 -15
  67. package/shared/localization/locales/en-GB.json +0 -15
  68. package/shared/localization/locales/en-US.json +18 -15
  69. package/shared/localization/locales/en-XA.json +0 -15
  70. package/shared/localization/locales/en-XL.json +18 -15
  71. package/shared/localization/locales/es-419.json +0 -15
  72. package/shared/localization/locales/es.json +0 -15
  73. package/shared/localization/locales/fi.json +0 -15
  74. package/shared/localization/locales/fil.json +0 -15
  75. package/shared/localization/locales/fr.json +0 -15
  76. package/shared/localization/locales/he.json +0 -15
  77. package/shared/localization/locales/hi.json +0 -15
  78. package/shared/localization/locales/hr.json +0 -15
  79. package/shared/localization/locales/hu.json +0 -15
  80. package/shared/localization/locales/id.json +0 -15
  81. package/shared/localization/locales/it.json +0 -15
  82. package/shared/localization/locales/ja.json +0 -15
  83. package/shared/localization/locales/ko.json +0 -15
  84. package/shared/localization/locales/lt.json +0 -15
  85. package/shared/localization/locales/lv.json +0 -15
  86. package/shared/localization/locales/nl.json +0 -15
  87. package/shared/localization/locales/no.json +0 -15
  88. package/shared/localization/locales/pl.json +0 -15
  89. package/shared/localization/locales/pt-PT.json +0 -15
  90. package/shared/localization/locales/pt.json +0 -15
  91. package/shared/localization/locales/ro.json +0 -15
  92. package/shared/localization/locales/ru.json +0 -15
  93. package/shared/localization/locales/sk.json +0 -15
  94. package/shared/localization/locales/sl.json +0 -15
  95. package/shared/localization/locales/sr-Latn.json +0 -15
  96. package/shared/localization/locales/sr.json +0 -15
  97. package/shared/localization/locales/sv.json +0 -15
  98. package/shared/localization/locales/ta.json +0 -15
  99. package/shared/localization/locales/te.json +0 -15
  100. package/shared/localization/locales/th.json +0 -15
  101. package/shared/localization/locales/tr.json +0 -15
  102. package/shared/localization/locales/uk.json +0 -15
  103. package/shared/localization/locales/vi.json +0 -15
  104. package/shared/localization/locales/zh-HK.json +0 -15
  105. package/shared/localization/locales/zh-TW.json +0 -15
  106. package/shared/localization/locales/zh.json +0 -15
  107. 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
  */
@@ -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
+ };
@@ -183,15 +183,26 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
183
183
  // Metrics.
184
184
  const metricAudits = category.auditRefs.filter(audit => audit.group === 'metrics');
185
185
  if (metricAudits.length) {
186
- const metricAuditsEl = this.renderAuditGroup(groups.metrics);
186
+ const [metricsGroupEl, metricsFooterEl] = this.renderAuditGroup(groups.metrics);
187
187
 
188
188
  // Metric descriptions toggle.
189
- const toggleTmpl = this.dom.createComponent('metricsToggle');
190
- const _toggleEl = this.dom.find('.lh-metrics-toggle', toggleTmpl);
191
- metricAuditsEl.append(..._toggleEl.childNodes);
192
-
193
- const metricsBoxesEl = this.dom.createChildOf(metricAuditsEl, 'div', 'lh-metrics-container');
194
-
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);
195
206
  metricAudits.forEach(item => {
196
207
  metricsBoxesEl.appendChild(this._renderMetric(item));
197
208
  });
@@ -208,8 +219,8 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
208
219
  this.dom.safelySetHref(calculatorLink, this._getScoringCalculatorHref(category.auditRefs));
209
220
 
210
221
 
211
- metricAuditsEl.classList.add('lh-audit-group--metrics');
212
- element.appendChild(metricAuditsEl);
222
+ metricsGroupEl.classList.add('lh-audit-group--metrics');
223
+ element.appendChild(metricsGroupEl);
213
224
  }
214
225
 
215
226
  // Filmstrip
@@ -240,7 +251,7 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
240
251
  const wastedMsValues = opportunityAudits.map(audit => this._getWastedMs(audit));
241
252
  const maxWaste = Math.max(...wastedMsValues);
242
253
  const scale = Math.max(Math.ceil(maxWaste / 1000) * 1000, minimumScale);
243
- const groupEl = this.renderAuditGroup(groups['load-opportunities']);
254
+ const [groupEl, footerEl] = this.renderAuditGroup(groups['load-opportunities']);
244
255
  const tmpl = this.dom.createComponent('opportunityHeader');
245
256
 
246
257
  this.dom.find('.lh-load-opportunity__col--one', tmpl).textContent =
@@ -249,8 +260,9 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
249
260
  strings.opportunitySavingsColumnLabel;
250
261
 
251
262
  const headerEl = this.dom.find('.lh-load-opportunity__header', tmpl);
252
- groupEl.appendChild(headerEl);
253
- 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));
254
266
  groupEl.classList.add('lh-audit-group--load-opportunities');
255
267
  element.appendChild(groupEl);
256
268
  }
@@ -266,8 +278,8 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
266
278
  });
267
279
 
268
280
  if (diagnosticAudits.length) {
269
- const groupEl = this.renderAuditGroup(groups['diagnostics']);
270
- 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));
271
283
  groupEl.classList.add('lh-audit-group--diagnostics');
272
284
  element.appendChild(groupEl);
273
285
  }
@@ -294,16 +306,16 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
294
306
  const table = this.detailsRenderer.render(audit.result.details);
295
307
  if (table) {
296
308
  table.id = id;
297
- table.classList.add('lh-audit');
309
+ table.classList.add('lh-details', 'lh-details--budget', 'lh-audit');
298
310
  budgetTableEls.push(table);
299
311
  }
300
312
  }
301
313
  });
302
314
  if (budgetTableEls.length > 0) {
303
- const budgetsGroupEl = this.renderAuditGroup(groups.budgets);
304
- budgetTableEls.forEach(table => budgetsGroupEl.appendChild(table));
305
- budgetsGroupEl.classList.add('lh-audit-group--budgets');
306
- 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);
307
319
  }
308
320
 
309
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
  });