lighthouse 8.6.0 → 9.0.0

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 (209) hide show
  1. package/dist/report/bundle.esm.js +5994 -0
  2. package/dist/report/flow.js +30 -13
  3. package/dist/report/standalone.js +41 -35
  4. package/flow-report/.eslintrc.cjs +2 -0
  5. package/flow-report/assets/standalone-flow-template.html +3 -2
  6. package/flow-report/assets/styles.css +108 -31
  7. package/flow-report/src/app.tsx +18 -18
  8. package/flow-report/src/common.tsx +28 -15
  9. package/flow-report/src/header.tsx +12 -13
  10. package/flow-report/src/help-dialog.tsx +3 -4
  11. package/flow-report/src/i18n/i18n.tsx +57 -16
  12. package/flow-report/src/i18n/ui-strings.js +50 -0
  13. package/flow-report/src/icons.tsx +36 -8
  14. package/flow-report/src/sidebar/flow.tsx +3 -3
  15. package/flow-report/src/sidebar/sidebar.tsx +35 -14
  16. package/flow-report/src/summary/category.tsx +97 -24
  17. package/flow-report/src/summary/summary.tsx +21 -14
  18. package/flow-report/src/topbar.tsx +9 -5
  19. package/flow-report/src/util.ts +33 -26
  20. package/flow-report/src/wrappers/category-score.tsx +0 -3
  21. package/flow-report/src/wrappers/markdown.tsx +27 -0
  22. package/flow-report/src/wrappers/report-renderer.tsx +10 -3
  23. package/flow-report/src/wrappers/report.tsx +27 -6
  24. package/flow-report/standalone-flow.tsx +4 -3
  25. package/flow-report/test/common-test.tsx +32 -17
  26. package/flow-report/test/flow-report-pptr-test.ts +46 -0
  27. package/flow-report/test/header-test.tsx +6 -6
  28. package/flow-report/test/setup/env-setup.ts +1 -1
  29. package/flow-report/test/sidebar/sidebar-test.tsx +43 -1
  30. package/flow-report/test/summary/category-test.tsx +120 -24
  31. package/flow-report/test/summary/summary-test.tsx +1 -1
  32. package/flow-report/test/topbar-test.tsx +1 -0
  33. package/flow-report/test/util-test.tsx +32 -11
  34. package/flow-report/test/wrappers/category-score-test.tsx +87 -0
  35. package/flow-report/test/wrappers/markdown-test.tsx +29 -0
  36. package/flow-report/tsconfig.json +1 -0
  37. package/jest.config.js +4 -2
  38. package/lighthouse-cli/.eslintrc.cjs +2 -0
  39. package/lighthouse-cli/cli-flags.js +1 -1
  40. package/lighthouse-cli/run.js +1 -1
  41. package/lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js +0 -0
  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/changelog.md +0 -5490
  198. package/lighthouse-core/audits/dobetterweb/appcache-manifest.js +0 -67
  199. package/lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js +0 -96
  200. package/lighthouse-core/audits/redirects-http.js +0 -59
  201. package/lighthouse-core/gather/gatherers/dobetterweb/appcache.js +0 -34
  202. package/lighthouse-core/gather/gatherers/http-redirect.js +0 -46
  203. package/lighthouse-core/lib/i18n/locales/en-US.ctc.json +0 -6699
  204. package/lighthouse-core/lib/i18n/locales/en-XL.ctc.json +0 -6672
  205. package/lighthouse-core/scripts/package.json +0 -4
  206. package/report/clients/psi.js +0 -158
  207. package/report/test/clients/psi-test.js +0 -143
  208. package/shared/localization/locales/en-US.ctc.json +0 -6877
  209. package/shared/localization/locales/en-XL.ctc.json +0 -6852
@@ -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
  });
@@ -24,15 +24,13 @@ export class TopbarFeatures {
24
24
  this.lhr; // eslint-disable-line no-unused-expressions
25
25
  this._reportUIFeatures = reportUIFeatures;
26
26
  this._dom = dom;
27
- /** @type {Document} */
28
- this._document = this._dom.document();
29
27
  this._dropDownMenu = new DropDownMenu(this._dom);
30
28
  this._copyAttempt = false;
31
29
  /** @type {HTMLElement} */
32
30
  this.topbarEl; // eslint-disable-line no-unused-expressions
33
31
  /** @type {HTMLElement} */
34
- this.scoreScaleEl; // eslint-disable-line no-unused-expressions
35
- /** @type {HTMLElement} */
32
+ this.categoriesEl; // eslint-disable-line no-unused-expressions
33
+ /** @type {HTMLElement?} */
36
34
  this.stickyHeaderEl; // eslint-disable-line no-unused-expressions
37
35
  /** @type {HTMLElement} */
38
36
  this.highlightEl; // eslint-disable-line no-unused-expressions
@@ -40,7 +38,6 @@ export class TopbarFeatures {
40
38
  this.onKeyUp = this.onKeyUp.bind(this);
41
39
  this.onCopy = this.onCopy.bind(this);
42
40
  this.collapseAllDetails = this.collapseAllDetails.bind(this);
43
- this._updateStickyHeaderOnScroll = this._updateStickyHeaderOnScroll.bind(this);
44
41
  }
45
42
 
46
43
  /**
@@ -48,33 +45,15 @@ export class TopbarFeatures {
48
45
  */
49
46
  enable(lhr) {
50
47
  this.lhr = lhr;
51
- this._document.addEventListener('keyup', this.onKeyUp);
52
- this._document.addEventListener('copy', this.onCopy);
48
+ this._dom.rootEl.addEventListener('keyup', this.onKeyUp);
49
+ this._dom.document().addEventListener('copy', this.onCopy);
53
50
  this._dropDownMenu.setup(this.onDropDownMenuClick);
54
51
  this._setUpCollapseDetailsAfterPrinting();
55
52
 
56
- const topbarLogo = this._dom.find('.lh-topbar__logo', this._document);
53
+ const topbarLogo = this._dom.find('.lh-topbar__logo', this._dom.rootEl);
57
54
  topbarLogo.addEventListener('click', () => toggleDarkTheme(this._dom));
58
55
 
59
- // There is only a sticky header when at least 2 categories are present.
60
- if (Object.keys(this.lhr.categories).length >= 2) {
61
- this._setupStickyHeaderElements();
62
- const containerEl = this._dom.find('.lh-container', this._document);
63
- const elToAddScrollListener = this._getScrollParent(containerEl);
64
- elToAddScrollListener.addEventListener('scroll', this._updateStickyHeaderOnScroll);
65
-
66
- // Use ResizeObserver where available.
67
- // TODO: there is an issue with incorrect position numbers and, as a result, performance
68
- // issues due to layout thrashing.
69
- // See https://github.com/GoogleChrome/lighthouse/pull/9023/files#r288822287 for details.
70
- // For now, limit to DevTools.
71
- if (this._dom.isDevTools()) {
72
- const resizeObserver = new window.ResizeObserver(this._updateStickyHeaderOnScroll);
73
- resizeObserver.observe(containerEl);
74
- } else {
75
- window.addEventListener('resize', this._updateStickyHeaderOnScroll);
76
- }
77
- }
56
+ this._setupStickyHeader();
78
57
  }
79
58
 
80
59
  /**
@@ -112,7 +91,7 @@ export class TopbarFeatures {
112
91
  try {
113
92
  this._reportUIFeatures._saveFile(new Blob([htmlStr], {type: 'text/html'}));
114
93
  } catch (e) {
115
- this._dom.fireEventOn('lh-log', this._document, {
94
+ this._dom.fireEventOn('lh-log', this._dom.document(), {
116
95
  cmd: 'error', msg: 'Could not export as HTML. ' + e.message,
117
96
  });
118
97
  }
@@ -152,7 +131,7 @@ export class TopbarFeatures {
152
131
  e.preventDefault();
153
132
  e.clipboardData.setData('text/plain', JSON.stringify(this.lhr, null, 2));
154
133
 
155
- this._dom.fireEventOn('lh-log', this._document, {
134
+ this._dom.fireEventOn('lh-log', this._dom.document(), {
156
135
  cmd: 'log', msg: 'Report JSON copied to clipboard',
157
136
  });
158
137
  }
@@ -164,28 +143,28 @@ export class TopbarFeatures {
164
143
  * Copies the report JSON to the clipboard (if supported by the browser).
165
144
  */
166
145
  onCopyButtonClick() {
167
- this._dom.fireEventOn('lh-analytics', this._document, {
146
+ this._dom.fireEventOn('lh-analytics', this._dom.document(), {
168
147
  cmd: 'send',
169
148
  fields: {hitType: 'event', eventCategory: 'report', eventAction: 'copy'},
170
149
  });
171
150
 
172
151
  try {
173
- if (this._document.queryCommandSupported('copy')) {
152
+ if (this._dom.document().queryCommandSupported('copy')) {
174
153
  this._copyAttempt = true;
175
154
 
176
155
  // Note: In Safari 10.0.1, execCommand('copy') returns true if there's
177
156
  // a valid text selection on the page. See http://caniuse.com/#feat=clipboard.
178
- if (!this._document.execCommand('copy')) {
157
+ if (!this._dom.document().execCommand('copy')) {
179
158
  this._copyAttempt = false; // Prevent event handler from seeing this as a copy attempt.
180
159
 
181
- this._dom.fireEventOn('lh-log', this._document, {
160
+ this._dom.fireEventOn('lh-log', this._dom.document(), {
182
161
  cmd: 'warn', msg: 'Your browser does not support copy to clipboard.',
183
162
  });
184
163
  }
185
164
  }
186
165
  } catch (e) {
187
166
  this._copyAttempt = false;
188
- this._dom.fireEventOn('lh-log', this._document, {cmd: 'log', msg: e.message});
167
+ this._dom.fireEventOn('lh-log', this._dom.document(), {cmd: 'log', msg: e.message});
189
168
  }
190
169
  }
191
170
 
@@ -206,7 +185,7 @@ export class TopbarFeatures {
206
185
  * open a `<details>` element.
207
186
  */
208
187
  expandAllDetails() {
209
- const details = this._dom.findAll('.lh-categories details', this._document);
188
+ const details = this._dom.findAll('.lh-categories details', this._dom.rootEl);
210
189
  details.map(detail => detail.open = true);
211
190
  }
212
191
 
@@ -215,7 +194,7 @@ export class TopbarFeatures {
215
194
  * open a `<details>` element.
216
195
  */
217
196
  collapseAllDetails() {
218
- const details = this._dom.findAll('.lh-categories details', this._document);
197
+ const details = this._dom.findAll('.lh-categories details', this._dom.rootEl);
219
198
  details.map(detail => detail.open = false);
220
199
  }
221
200
 
@@ -235,7 +214,7 @@ export class TopbarFeatures {
235
214
  /**
236
215
  * Finds the first scrollable ancestor of `element`. Falls back to the document.
237
216
  * @param {Element} element
238
- * @return {Node}
217
+ * @return {Element | Document}
239
218
  */
240
219
  _getScrollParent(element) {
241
220
  const {overflowY} = window.getComputedStyle(element);
@@ -273,24 +252,50 @@ export class TopbarFeatures {
273
252
  }
274
253
  }
275
254
 
276
- _setupStickyHeaderElements() {
277
- this.topbarEl = this._dom.find('div.lh-topbar', this._document);
278
- this.scoreScaleEl = this._dom.find('div.lh-scorescale', this._document);
279
- this.stickyHeaderEl = this._dom.find('div.lh-sticky-header', this._document);
255
+ _setupStickyHeader() {
256
+ // Cache these elements to avoid qSA on each onscroll.
257
+ this.topbarEl = this._dom.find('div.lh-topbar', this._dom.rootEl);
258
+ this.categoriesEl = this._dom.find('div.lh-categories', this._dom.rootEl);
259
+
260
+ // Defer behind rAF to avoid forcing layout.
261
+ window.requestAnimationFrame(() => window.requestAnimationFrame(() => {
262
+ // Only present in the DOM if it'll be used (>=2 categories)
263
+ try {
264
+ this.stickyHeaderEl = this._dom.find('div.lh-sticky-header', this._dom.rootEl);
265
+ } catch {
266
+ return;
267
+ }
280
268
 
281
- // Highlighter will be absolutely positioned at first gauge, then transformed on scroll.
282
- this.highlightEl = this._dom.createChildOf(this.stickyHeaderEl, 'div', 'lh-highlighter');
269
+ // Highlighter will be absolutely positioned at first gauge, then transformed on scroll.
270
+ this.highlightEl = this._dom.createChildOf(this.stickyHeaderEl, 'div', 'lh-highlighter');
271
+
272
+ // Update sticky header visibility and highlight when page scrolls/resizes.
273
+ const scrollParent = this._getScrollParent(
274
+ this._dom.find('.lh-container', this._dom.rootEl));
275
+ // The 'scroll' handler must be should be on {Element | Document}...
276
+ scrollParent.addEventListener('scroll', () => this._updateStickyHeader());
277
+ // However resizeObserver needs an element, *not* the document.
278
+ const resizeTarget = scrollParent instanceof window.Document
279
+ ? document.documentElement
280
+ : scrollParent;
281
+ new window.ResizeObserver(() => this._updateStickyHeader()).observe(resizeTarget);
282
+ }));
283
283
  }
284
284
 
285
- _updateStickyHeaderOnScroll() {
286
- // Show sticky header when the score scale begins to go underneath the topbar.
285
+ /**
286
+ * Toggle visibility and update highlighter position
287
+ */
288
+ _updateStickyHeader() {
289
+ if (!this.stickyHeaderEl) return;
290
+
291
+ // Show sticky header when the main 5 gauges clear the topbar.
287
292
  const topbarBottom = this.topbarEl.getBoundingClientRect().bottom;
288
- const scoreScaleTop = this.scoreScaleEl.getBoundingClientRect().top;
289
- const showStickyHeader = topbarBottom >= scoreScaleTop;
293
+ const categoriesTop = this.categoriesEl.getBoundingClientRect().top;
294
+ const showStickyHeader = topbarBottom >= categoriesTop;
290
295
 
291
296
  // Highlight mini gauge when section is in view.
292
297
  // In view = the last category that starts above the middle of the window.
293
- const categoryEls = Array.from(this._document.querySelectorAll('.lh-category'));
298
+ const categoryEls = Array.from(this._dom.rootEl.querySelectorAll('.lh-category'));
294
299
  const categoriesAboveTheMiddle =
295
300
  categoryEls.filter(el => el.getBoundingClientRect().top - window.innerHeight / 2 < 0);
296
301
  const highlightIndex =
@@ -38,7 +38,11 @@ const listOfTlds = [
38
38
  'web', 'spb', 'blog', 'jus', 'kiev', 'mil', 'wi', 'qc', 'ca', 'bel', 'on',
39
39
  ];
40
40
 
41
- export class Util {
41
+ class Util {
42
+ /** @type {I18n<typeof UIStrings>} */
43
+ // @ts-expect-error: Is set in report renderer.
44
+ static i18n = null;
45
+
42
46
  static get PASS_THRESHOLD() {
43
47
  return PASS_THRESHOLD;
44
48
  }
@@ -104,6 +108,23 @@ export class Util {
104
108
  /** @type {Map<string, Array<LH.ReportResult.AuditRef>>} */
105
109
  const relevantAuditToMetricsMap = new Map();
106
110
 
111
+ // This backcompat converts old LHRs (<9.0.0) to use the new "hidden" group.
112
+ // Old LHRs used "no group" to identify audits that should be hidden in performance instead of the "hidden" group.
113
+ // Newer LHRs use "no group" to identify opportunities and diagnostics whose groups are assigned by details type.
114
+ const [majorVersion] = clone.lighthouseVersion.split('.').map(Number);
115
+ const perfCategory = clone.categories['performance'];
116
+ if (majorVersion < 9 && perfCategory) {
117
+ if (!clone.categoryGroups) clone.categoryGroups = {};
118
+ clone.categoryGroups['hidden'] = {title: ''};
119
+ for (const auditRef of perfCategory.auditRefs) {
120
+ if (!auditRef.group) {
121
+ auditRef.group = 'hidden';
122
+ } else if (['load-opportunities', 'diagnostics'].includes(auditRef.group)) {
123
+ delete auditRef.group;
124
+ }
125
+ }
126
+ }
127
+
107
128
  for (const category of Object.values(clone.categories)) {
108
129
  // Make basic lookup table for relevantAudits
109
130
  category.auditRefs.forEach(metricRef => {
@@ -167,6 +188,8 @@ export class Util {
167
188
 
168
189
  /**
169
190
  * Convert a score to a rating label.
191
+ * TODO: Return `'error'` for `score === null && !scoreDisplayMode`.
192
+ *
170
193
  * @param {number|null} score
171
194
  * @param {string=} scoreDisplayMode
172
195
  * @return {string}
@@ -387,43 +410,21 @@ export class Util {
387
410
  return hostname.split('.').slice(-splitTld.length).join('.');
388
411
  }
389
412
 
390
- /**
391
- * @param {LH.Result['configSettings']} settings
392
- * @return {!Array<{name: string, description: string}>}
393
- */
394
- static getEnvironmentDisplayValues(settings) {
395
- const emulationDesc = Util.getEmulationDescriptions(settings);
396
-
397
- return [
398
- {
399
- name: Util.i18n.strings.runtimeSettingsDevice,
400
- description: emulationDesc.deviceEmulation,
401
- },
402
- {
403
- name: Util.i18n.strings.runtimeSettingsNetworkThrottling,
404
- description: emulationDesc.networkThrottling,
405
- },
406
- {
407
- name: Util.i18n.strings.runtimeSettingsCPUThrottling,
408
- description: emulationDesc.cpuThrottling,
409
- },
410
- ];
411
- }
412
413
 
413
414
  /**
414
415
  * @param {LH.Result['configSettings']} settings
415
- * @return {{deviceEmulation: string, networkThrottling: string, cpuThrottling: string}}
416
+ * @return {!{deviceEmulation: string, networkThrottling: string, cpuThrottling: string, summary: string}}
416
417
  */
417
418
  static getEmulationDescriptions(settings) {
418
419
  let cpuThrottling;
419
420
  let networkThrottling;
421
+ let summary;
420
422
 
421
423
  const throttling = settings.throttling;
422
424
 
423
425
  switch (settings.throttlingMethod) {
424
426
  case 'provided':
425
- cpuThrottling = Util.i18n.strings.throttlingProvided;
426
- networkThrottling = Util.i18n.strings.throttlingProvided;
427
+ summary = networkThrottling = cpuThrottling = Util.i18n.strings.throttlingProvided;
427
428
  break;
428
429
  case 'devtools': {
429
430
  const {cpuSlowdownMultiplier, requestLatencyMs} = throttling;
@@ -431,6 +432,13 @@ export class Util {
431
432
  networkThrottling = `${Util.i18n.formatNumber(requestLatencyMs)}${NBSP}ms HTTP RTT, ` +
432
433
  `${Util.i18n.formatNumber(throttling.downloadThroughputKbps)}${NBSP}Kbps down, ` +
433
434
  `${Util.i18n.formatNumber(throttling.uploadThroughputKbps)}${NBSP}Kbps up (DevTools)`;
435
+
436
+ const isSlow4G = () => {
437
+ return requestLatencyMs === 150 * 3.75 &&
438
+ throttling.downloadThroughputKbps === 1.6 * 1024 * 0.9 &&
439
+ throttling.uploadThroughputKbps === 750 * 0.9;
440
+ };
441
+ summary = isSlow4G() ? Util.i18n.strings.runtimeSlow4g : Util.i18n.strings.runtimeCustom;
434
442
  break;
435
443
  }
436
444
  case 'simulate': {
@@ -438,11 +446,15 @@ export class Util {
438
446
  cpuThrottling = `${Util.i18n.formatNumber(cpuSlowdownMultiplier)}x slowdown (Simulated)`;
439
447
  networkThrottling = `${Util.i18n.formatNumber(rttMs)}${NBSP}ms TCP RTT, ` +
440
448
  `${Util.i18n.formatNumber(throughputKbps)}${NBSP}Kbps throughput (Simulated)`;
449
+
450
+ const isSlow4G = () => {
451
+ return rttMs === 150 && throughputKbps === 1.6 * 1024;
452
+ };
453
+ summary = isSlow4G() ? Util.i18n.strings.runtimeSlow4g : Util.i18n.strings.runtimeCustom;
441
454
  break;
442
455
  }
443
456
  default:
444
- cpuThrottling = Util.i18n.strings.runtimeUnknown;
445
- networkThrottling = Util.i18n.strings.runtimeUnknown;
457
+ summary = cpuThrottling = networkThrottling = Util.i18n.strings.runtimeUnknown;
446
458
  }
447
459
 
448
460
  // TODO(paulirish): revise Runtime Settings strings: https://github.com/GoogleChrome/lighthouse/pull/11796
@@ -455,6 +467,7 @@ export class Util {
455
467
  deviceEmulation,
456
468
  cpuThrottling,
457
469
  networkThrottling,
470
+ summary,
458
471
  };
459
472
  }
460
473
 
@@ -524,10 +537,9 @@ export class Util {
524
537
  let totalWeight = 0;
525
538
  for (const auditRef of category.auditRefs) {
526
539
  const auditPassed = Util.showAsPassed(auditRef.result);
527
- const notDisplayed = !auditRef.group && category.id === 'performance';
528
540
 
529
541
  // Don't count the audit if it's manual, N/A, or isn't displayed.
530
- if (notDisplayed ||
542
+ if (auditRef.group === 'hidden' ||
531
543
  auditRef.result.scoreDisplayMode === 'manual' ||
532
544
  auditRef.result.scoreDisplayMode === 'notApplicable') {
533
545
  continue;
@@ -563,14 +575,10 @@ Util.getUniqueSuffix = (() => {
563
575
  };
564
576
  })();
565
577
 
566
- /** @type {I18n<typeof Util['UIStrings']>} */
567
- // @ts-expect-error: Is set in report renderer.
568
- Util.i18n = null;
569
-
570
578
  /**
571
579
  * Report-renderer-specific strings.
572
580
  */
573
- Util.UIStrings = {
581
+ const UIStrings = {
574
582
  /** Disclaimer shown to users below the metric values (First Contentful Paint, Time to Interactive, etc) to warn them that the numbers they see will likely change slightly the next time they run Lighthouse. */
575
583
  varianceDisclaimer: 'Values are estimated and may vary. The [performance score is calculated](https://web.dev/performance-scoring/) directly from these metrics.',
576
584
  /** Text link pointing to an interactive calculator that explains Lighthouse scoring. The link text should be fairly short. */
@@ -637,22 +645,12 @@ Util.UIStrings = {
637
645
  /** Option in a dropdown menu that toggles the themeing of the report between Light(default) and Dark themes. */
638
646
  dropdownDarkTheme: 'Toggle Dark Theme',
639
647
 
640
- /** Title of the Runtime settings table in a Lighthouse report. Runtime settings are the environment configurations that a specific report used at auditing time. */
641
- runtimeSettingsTitle: 'Runtime Settings',
642
- /** Label for a row in a table that shows the URL that was audited during a Lighthouse run. */
643
- runtimeSettingsUrl: 'URL',
644
- /** Label for a row in a table that shows the time at which a Lighthouse run was conducted; formatted as a timestamp, e.g. Jan 1, 1970 12:00 AM UTC. */
645
- runtimeSettingsFetchTime: 'Fetch Time',
646
648
  /** Label for a row in a table that describes the kind of device that was emulated for the Lighthouse run. Example values for row elements: 'No Emulation', 'Emulated Desktop', etc. */
647
649
  runtimeSettingsDevice: 'Device',
648
650
  /** Label for a row in a table that describes the network throttling conditions that were used during a Lighthouse run, if any. */
649
651
  runtimeSettingsNetworkThrottling: 'Network throttling',
650
652
  /** Label for a row in a table that describes the CPU throttling conditions that were used during a Lighthouse run, if any.*/
651
653
  runtimeSettingsCPUThrottling: 'CPU throttling',
652
- /** Label for a row in a table that shows in what tool Lighthouse is being run (e.g. The lighthouse CLI, Chrome DevTools, Lightrider, WebPageTest, etc). */
653
- runtimeSettingsChannel: 'Channel',
654
- /** Label for a row in a table that shows the User Agent that was detected on the Host machine that ran Lighthouse. */
655
- runtimeSettingsUA: 'User agent (host)',
656
654
  /** Label for a row in a table that shows the User Agent that was used to send out all network requests during the Lighthouse run. */
657
655
  runtimeSettingsUANetwork: 'User agent (network)',
658
656
  /** Label for a row in a table that shows the estimated CPU power of the machine running Lighthouse. Example row values: 532, 1492, 783. */
@@ -671,9 +669,31 @@ Util.UIStrings = {
671
669
  runtimeDesktopEmulation: 'Emulated Desktop',
672
670
  /** Descriptive explanation for a runtime setting that is set to an unknown value. */
673
671
  runtimeUnknown: 'Unknown',
672
+ /** Descriptive label that this analysis run was from a single pageload of a browser (not a summary of hundreds of loads) */
673
+ runtimeSingleLoad: 'Single page load',
674
+ /** Descriptive label that this analysis only considers the initial load of the page, and no interaction beyond when the page had "fully loaded" */
675
+ runtimeAnalysisWindow: 'Initial page load',
676
+ /** Descriptive explanation that this analysis run was from a single pageload of a browser, whereas field data often summarizes hundreds+ of page loads */
677
+ runtimeSingleLoadTooltip: 'This data is taken from a single page load, as opposed to field data summarizing many sessions.', // eslint-disable-line max-len
674
678
 
675
679
  /** Descriptive explanation for environment throttling that was provided by the runtime environment instead of provided by Lighthouse throttling. */
676
680
  throttlingProvided: 'Provided by environment',
681
+ /** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Show' and 'Hide'. */
682
+ show: 'Show',
683
+ /** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Show' and 'Hide'. */
684
+ hide: 'Hide',
685
+ /** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Expand view' and 'Collapse view'. */
686
+ expandView: 'Expand view',
687
+ /** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Expand view' and 'Collapse view'. */
688
+ collapseView: 'Collapse view',
689
+ /** Label indicating that Lighthouse throttled the page to emulate a slow 4G network connection. */
690
+ runtimeSlow4g: 'Slow 4G throttling',
691
+ /** Label indicating that Lighthouse throttled the page using custom throttling settings. */
692
+ runtimeCustom: 'Custom throttling',
677
693
  };
694
+ Util.UIStrings = UIStrings;
678
695
 
679
- export const UIStrings = Util.UIStrings;
696
+ export {
697
+ Util,
698
+ UIStrings,
699
+ };