lighthouse 8.6.0-dev.20211029 → 8.6.0-dev.20211102

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 (116) hide show
  1. package/dist/report/flow.js +14 -14
  2. package/dist/report/standalone.js +12 -12
  3. package/flow-report/.eslintrc.cjs +2 -0
  4. package/flow-report/assets/styles.css +1 -1
  5. package/flow-report/src/app.tsx +10 -11
  6. package/flow-report/src/common.tsx +22 -9
  7. package/flow-report/src/header.tsx +3 -3
  8. package/flow-report/src/i18n/i18n.tsx +10 -6
  9. package/flow-report/src/icons.tsx +21 -9
  10. package/flow-report/src/sidebar/flow.tsx +3 -3
  11. package/flow-report/src/sidebar/sidebar.tsx +12 -6
  12. package/flow-report/src/summary/category.tsx +9 -4
  13. package/flow-report/src/summary/summary.tsx +9 -3
  14. package/flow-report/src/util.ts +31 -19
  15. package/flow-report/src/wrappers/report-renderer.tsx +9 -3
  16. package/flow-report/src/wrappers/report.tsx +17 -7
  17. package/flow-report/test/common-test.tsx +25 -10
  18. package/flow-report/test/header-test.tsx +6 -6
  19. package/flow-report/test/setup/env-setup.ts +1 -1
  20. package/flow-report/test/summary/category-test.tsx +5 -5
  21. package/flow-report/test/util-test.tsx +32 -11
  22. package/lighthouse-cli/.eslintrc.cjs +2 -0
  23. package/lighthouse-cli/cli-flags.js +1 -1
  24. package/lighthouse-cli/test/smokehouse/lighthouse-runners/bundle.js +4 -2
  25. package/lighthouse-cli/test/smokehouse/smokehouse.js +1 -1
  26. package/lighthouse-core/audits/audit.js +1 -1
  27. package/lighthouse-core/computed/metrics/lantern-total-blocking-time.js +3 -4
  28. package/lighthouse-core/computed/metrics/tbt-utils.js +57 -0
  29. package/lighthouse-core/computed/metrics/total-blocking-time.js +3 -53
  30. package/lighthouse-core/computed/resource-summary.js +1 -1
  31. package/lighthouse-core/config/config-helpers.js +20 -10
  32. package/lighthouse-core/config/default-config.js +59 -55
  33. package/lighthouse-core/fraggle-rock/config/default-config.js +1 -1
  34. package/lighthouse-core/fraggle-rock/gather/base-gatherer.js +6 -1
  35. package/lighthouse-core/gather/connections/cri.js +1 -1
  36. package/lighthouse-core/gather/gatherers/dobetterweb/response-compression.js +1 -0
  37. package/lighthouse-core/lib/asset-saver.js +4 -1
  38. package/lighthouse-core/lib/lh-env.js +2 -0
  39. package/lighthouse-core/lib/proto-preprocessor.js +14 -2
  40. package/lighthouse-core/lib/tappable-rects.js +8 -15
  41. package/lighthouse-core/lib/url-shim.js +1 -1
  42. package/lighthouse-core/runner.js +1 -1
  43. package/lighthouse-core/util-commonjs.js +25 -11
  44. package/package.json +11 -8
  45. package/report/.eslintrc.cjs +2 -0
  46. package/report/assets/styles.css +226 -250
  47. package/report/assets/templates.html +19 -29
  48. package/report/clients/standalone.js +0 -2
  49. package/report/renderer/category-renderer.js +21 -17
  50. package/report/renderer/components.js +21 -54
  51. package/report/renderer/crc-details-renderer.js +15 -12
  52. package/report/renderer/element-screenshot-renderer.js +2 -2
  53. package/report/renderer/open-tab.js +9 -3
  54. package/report/renderer/performance-category-renderer.js +51 -24
  55. package/report/renderer/report-renderer.js +2 -1
  56. package/report/renderer/report-ui-features.js +2 -3
  57. package/report/renderer/util.js +26 -11
  58. package/report/test/renderer/category-renderer-test.js +2 -2
  59. package/report/test/renderer/performance-category-renderer-test.js +35 -9
  60. package/report/test/renderer/report-renderer-test.js +1 -1
  61. package/report/test/renderer/report-ui-features-test.js +10 -9
  62. package/report/test/renderer/util-test.js +13 -31
  63. package/report/test-assets/faux-psi.js +5 -5
  64. package/shared/localization/format.js +9 -4
  65. package/shared/localization/locales/ar-XB.json +0 -15
  66. package/shared/localization/locales/ar.json +0 -15
  67. package/shared/localization/locales/bg.json +0 -15
  68. package/shared/localization/locales/ca.json +0 -15
  69. package/shared/localization/locales/cs.json +0 -15
  70. package/shared/localization/locales/da.json +0 -15
  71. package/shared/localization/locales/de.json +0 -15
  72. package/shared/localization/locales/el.json +0 -15
  73. package/shared/localization/locales/en-GB.json +0 -15
  74. package/shared/localization/locales/en-US.json +19 -16
  75. package/shared/localization/locales/en-XA.json +0 -15
  76. package/shared/localization/locales/en-XL.json +19 -16
  77. package/shared/localization/locales/es-419.json +0 -15
  78. package/shared/localization/locales/es.json +0 -15
  79. package/shared/localization/locales/fi.json +0 -15
  80. package/shared/localization/locales/fil.json +0 -15
  81. package/shared/localization/locales/fr.json +0 -15
  82. package/shared/localization/locales/he.json +0 -15
  83. package/shared/localization/locales/hi.json +0 -15
  84. package/shared/localization/locales/hr.json +0 -15
  85. package/shared/localization/locales/hu.json +0 -15
  86. package/shared/localization/locales/id.json +0 -15
  87. package/shared/localization/locales/it.json +0 -15
  88. package/shared/localization/locales/ja.json +0 -15
  89. package/shared/localization/locales/ko.json +0 -15
  90. package/shared/localization/locales/lt.json +0 -15
  91. package/shared/localization/locales/lv.json +0 -15
  92. package/shared/localization/locales/nl.json +0 -15
  93. package/shared/localization/locales/no.json +0 -15
  94. package/shared/localization/locales/pl.json +0 -15
  95. package/shared/localization/locales/pt-PT.json +0 -15
  96. package/shared/localization/locales/pt.json +0 -15
  97. package/shared/localization/locales/ro.json +0 -15
  98. package/shared/localization/locales/ru.json +0 -15
  99. package/shared/localization/locales/sk.json +0 -15
  100. package/shared/localization/locales/sl.json +0 -15
  101. package/shared/localization/locales/sr-Latn.json +0 -15
  102. package/shared/localization/locales/sr.json +0 -15
  103. package/shared/localization/locales/sv.json +0 -15
  104. package/shared/localization/locales/ta.json +0 -15
  105. package/shared/localization/locales/te.json +0 -15
  106. package/shared/localization/locales/th.json +0 -15
  107. package/shared/localization/locales/tr.json +0 -15
  108. package/shared/localization/locales/uk.json +0 -15
  109. package/shared/localization/locales/vi.json +0 -15
  110. package/shared/localization/locales/zh-HK.json +0 -15
  111. package/shared/localization/locales/zh-TW.json +0 -15
  112. package/shared/localization/locales/zh.json +0 -15
  113. package/third-party/chromium-synchronization/installability-errors-test.js +2 -0
  114. package/types/lhr/flow.d.ts +3 -2
  115. package/types/rollup-plugin-postprocess.d.ts +10 -0
  116. package/lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js +0 -96
@@ -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;
@@ -268,6 +268,7 @@ export class ReportRenderer {
268
268
  }
269
269
 
270
270
  const scoreScale = this._dom.createElement('div');
271
+ scoreScale.classList.add('lh-scorescale-wrap');
271
272
  scoreScale.append(this._dom.createComponent('scorescale'));
272
273
  if (scoreHeader) {
273
274
  const scoresContainer = this._dom.find('.lh-scores-container', headerContainer);
@@ -287,7 +288,7 @@ export class ReportRenderer {
287
288
  for (const category of Object.values(report.categories)) {
288
289
  const renderer = specificCategoryRenderers[category.id] || categoryRenderer;
289
290
  // .lh-category-wrapper is full-width and provides horizontal rules between categories.
290
- // .lh-category within has the max-width: var(--report-width);
291
+ // .lh-category within has the max-width: var(--report-content-width);
291
292
  const wrapper = renderer.dom.createChildOf(categories, 'div', 'lh-category-wrapper');
292
293
  wrapper.appendChild(renderer.render(
293
294
  category,
@@ -256,10 +256,9 @@ export class ReportUIFeatures {
256
256
  const allThirdParty = thirdPartyRows.length === rowEls.length;
257
257
  const allFirstParty = !thirdPartyRows.length;
258
258
 
259
- // If all or none of the rows are 3rd party, disable the checkbox.
259
+ // If all or none of the rows are 3rd party, hide the control.
260
260
  if (allThirdParty || allFirstParty) {
261
- filterInput.disabled = true;
262
- filterInput.checked = allThirdParty;
261
+ this._dom.find('div.lh-3p-filter', filterTemplate).hidden = true;
263
262
  }
264
263
 
265
264
  // Add checkbox to the DOM.
@@ -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
  }
@@ -417,7 +421,7 @@ export class Util {
417
421
  throttling.downloadThroughputKbps === 1.6 * 1024 * 0.9 &&
418
422
  throttling.uploadThroughputKbps === 750 * 0.9;
419
423
  };
420
- summary = `${isSlow4G() ? 'Slow 4G' : 'Custom'} throttling by DevTools`;
424
+ summary = isSlow4G() ? Util.i18n.strings.runtimeSlow4g : Util.i18n.strings.runtimeCustom;
421
425
  break;
422
426
  }
423
427
  case 'simulate': {
@@ -429,7 +433,7 @@ export class Util {
429
433
  const isSlow4G = () => {
430
434
  return rttMs === 150 && throughputKbps === 1.6 * 1024;
431
435
  };
432
- summary = isSlow4G() ? 'Simulated slow 4G' : 'Custom simulated throttling';
436
+ summary = isSlow4G() ? Util.i18n.strings.runtimeSlow4g : Util.i18n.strings.runtimeCustom;
433
437
  break;
434
438
  }
435
439
  default:
@@ -516,10 +520,9 @@ export class Util {
516
520
  let totalWeight = 0;
517
521
  for (const auditRef of category.auditRefs) {
518
522
  const auditPassed = Util.showAsPassed(auditRef.result);
519
- const notDisplayed = !auditRef.group && category.id === 'performance';
520
523
 
521
524
  // Don't count the audit if it's manual, N/A, or isn't displayed.
522
- if (notDisplayed ||
525
+ if (auditRef.group === 'hidden' ||
523
526
  auditRef.result.scoreDisplayMode === 'manual' ||
524
527
  auditRef.result.scoreDisplayMode === 'notApplicable') {
525
528
  continue;
@@ -555,14 +558,10 @@ Util.getUniqueSuffix = (() => {
555
558
  };
556
559
  })();
557
560
 
558
- /** @type {I18n<typeof Util['UIStrings']>} */
559
- // @ts-expect-error: Is set in report renderer.
560
- Util.i18n = null;
561
-
562
561
  /**
563
562
  * Report-renderer-specific strings.
564
563
  */
565
- Util.UIStrings = {
564
+ const UIStrings = {
566
565
  /** 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. */
567
566
  varianceDisclaimer: 'Values are estimated and may vary. The [performance score is calculated](https://web.dev/performance-scoring/) directly from these metrics.',
568
567
  /** Text link pointing to an interactive calculator that explains Lighthouse scoring. The link text should be fairly short. */
@@ -662,6 +661,22 @@ Util.UIStrings = {
662
661
 
663
662
  /** Descriptive explanation for environment throttling that was provided by the runtime environment instead of provided by Lighthouse throttling. */
664
663
  throttlingProvided: 'Provided by environment',
664
+ /** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Show' and 'Hide'. */
665
+ show: 'Show',
666
+ /** Label for an interactive control that will reveal or hide a group of content. This control toggles between the text 'Show' and 'Hide'. */
667
+ hide: 'Hide',
668
+ /** 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'. */
669
+ expandView: 'Expand view',
670
+ /** 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'. */
671
+ collapseView: 'Collapse view',
672
+ /** Label indicating that Lighthouse throttled the page to emulate a slow 4G network connection. */
673
+ runtimeSlow4g: 'Slow 4G throttling',
674
+ /** Label indicating that Lighthouse throttled the page using custom throttling settings. */
675
+ runtimeCustom: 'Custom throttling',
665
676
  };
677
+ Util.UIStrings = UIStrings;
666
678
 
667
- export const UIStrings = Util.UIStrings;
679
+ export {
680
+ Util,
681
+ UIStrings,
682
+ };
@@ -197,8 +197,8 @@ describe('CategoryRenderer', () => {
197
197
  'score shows informative and dash icon');
198
198
 
199
199
  assert.ok(pwaCategory.manualDescription);
200
- const description = categoryDOM
201
- .querySelector('.lh-clump--manual .lh-audit-group__description').textContent;
200
+ const description = categoryDOM.querySelector('.lh-clump--manual').closest('.lh-audit-group')
201
+ .querySelector('.lh-audit-group__description').textContent;
202
202
  // may need to be adjusted if description includes a link at the beginning
203
203
  assert.ok(description.startsWith(pwaCategory.manualDescription.substring(0, 20)),
204
204
  'no manual description');
@@ -96,11 +96,23 @@ describe('PerfCategoryRenderer', () => {
96
96
  assert.strictEqual(new URL(calcLink.href).hostname, 'googlechrome.github.io');
97
97
  });
98
98
 
99
+ it('ignores hidden audits', () => {
100
+ const categoryDOM = renderer.render(category, sampleResults.categoryGroups);
101
+
102
+ const hiddenAudits = category.auditRefs.filter(audit => audit.group === 'hidden');
103
+ const auditElements = [...categoryDOM.querySelectorAll('.lh-audit')];
104
+ const matchingElements = auditElements
105
+ .filter(el => hiddenAudits.some(audit => audit.id === el.id));
106
+ expect(matchingElements).toHaveLength(0);
107
+ });
108
+
99
109
  it('renders the failing performance opportunities', () => {
100
110
  const categoryDOM = renderer.render(category, sampleResults.categoryGroups);
101
111
 
102
- const oppAudits = category.auditRefs.filter(audit => audit.group === 'load-opportunities' &&
103
- !Util.showAsPassed(audit.result));
112
+ const oppAudits = category.auditRefs.filter(audit =>
113
+ audit.result.details &&
114
+ audit.result.details.type === 'opportunity' &&
115
+ !Util.showAsPassed(audit.result));
104
116
  const oppElements = [...categoryDOM.querySelectorAll('.lh-audit--load-opportunity')];
105
117
  expect(oppElements.map(e => e.id).sort()).toEqual(oppAudits.map(a => a.id).sort());
106
118
  expect(oppElements.length).toBeGreaterThan(0);
@@ -120,10 +132,14 @@ describe('PerfCategoryRenderer', () => {
120
132
  it('renders performance opportunities with an errorMessage', () => {
121
133
  const auditWithError = {
122
134
  score: 0,
123
- group: 'load-opportunities',
124
135
  result: {
125
136
  score: null, scoreDisplayMode: 'error', errorMessage: 'Yikes!!', title: 'Bug #2',
126
137
  description: '',
138
+ details: {
139
+ overallSavingsMs: 0,
140
+ items: [],
141
+ type: 'opportunity',
142
+ },
127
143
  },
128
144
  };
129
145
 
@@ -137,10 +153,14 @@ describe('PerfCategoryRenderer', () => {
137
153
  it('renders performance opportunities\' explanation', () => {
138
154
  const auditWithExplanation = {
139
155
  score: 0,
140
- group: 'load-opportunities',
141
156
  result: {
142
157
  score: 0, scoreDisplayMode: 'numeric',
143
158
  numericValue: 100, explanation: 'Yikes!!', title: 'Bug #2', description: '',
159
+ details: {
160
+ overallSavingsMs: 0,
161
+ items: [],
162
+ type: 'opportunity',
163
+ },
144
164
  },
145
165
  };
146
166
 
@@ -159,7 +179,9 @@ describe('PerfCategoryRenderer', () => {
159
179
  '.lh-category > .lh-audit-group.lh-audit-group--diagnostics');
160
180
 
161
181
  const diagnosticAuditIds = category.auditRefs.filter(audit => {
162
- return audit.group === 'diagnostics' && !Util.showAsPassed(audit.result);
182
+ return !audit.group &&
183
+ !(audit.result.details && audit.result.details.type === 'opportunity') &&
184
+ !Util.showAsPassed(audit.result);
163
185
  }).map(audit => audit.id).sort();
164
186
  assert.ok(diagnosticAuditIds.length > 0);
165
187
 
@@ -170,11 +192,11 @@ describe('PerfCategoryRenderer', () => {
170
192
 
171
193
  it('renders the passed audits', () => {
172
194
  const categoryDOM = renderer.render(category, sampleResults.categoryGroups);
173
- const passedSection = categoryDOM.querySelector('.lh-category > .lh-clump--passed');
195
+ const passedSection = categoryDOM.querySelector('.lh-clump--passed');
174
196
 
175
197
  const passedAudits = category.auditRefs.filter(audit =>
176
- audit.group && audit.group !== 'metrics' && audit.id !== 'performance-budget'
177
- && Util.showAsPassed(audit.result));
198
+ !audit.group &&
199
+ Util.showAsPassed(audit.result));
178
200
  const passedElements = passedSection.querySelectorAll('.lh-audit');
179
201
  assert.equal(passedElements.length, passedAudits.length);
180
202
  });
@@ -187,10 +209,14 @@ describe('PerfCategoryRenderer', () => {
187
209
  it('handles erroring opportunities', () => {
188
210
  const auditWithDebug = {
189
211
  score: 0,
190
- group: 'load-opportunities',
191
212
  result: {
192
213
  error: true, score: 0,
193
214
  numericValue: 100, explanation: 'Yikes!!', title: 'Bug #2',
215
+ details: {
216
+ overallSavingsMs: 0,
217
+ items: [],
218
+ type: 'opportunity',
219
+ },
194
220
  },
195
221
  };
196
222
  const wastedMs = renderer._getWastedMs(auditWithDebug);
@@ -90,7 +90,7 @@ describe('ReportRenderer', () => {
90
90
  const output = renderer.renderReport(sampleResultsCopy, container);
91
91
 
92
92
  function isPWAGauge(el) {
93
- return el.querySelector('.lh-gauge__label').textContent === 'Progressive Web App';
93
+ return el.querySelector('.lh-gauge__label').textContent === 'PWA';
94
94
  }
95
95
  function isPluginGauge(el) {
96
96
  return el.querySelector('.lh-gauge__label').textContent === 'Some Plugin';
@@ -256,17 +256,15 @@ describe('ReportUIFeatures', () => {
256
256
  .toThrowError('query #uses-rel-preconnect .lh-3p-filter-input not found');
257
257
  });
258
258
 
259
- it('filter is disabled and checked for when just third party resources', () => {
260
- const filterCheckbox =
261
- dom.find('#render-blocking-resources .lh-3p-filter-input', container);
262
- expect(filterCheckbox.disabled).toEqual(true);
263
- expect(filterCheckbox.checked).toEqual(true);
259
+ it('filter is hidden when just third party resources', () => {
260
+ const filterControl =
261
+ dom.find('#render-blocking-resources .lh-3p-filter', container);
262
+ expect(filterControl.hidden).toEqual(true);
264
263
  });
265
264
 
266
- it('filter is disabled and not checked for just first party resources', () => {
267
- const filterCheckbox = dom.find('#uses-text-compression .lh-3p-filter-input', container);
268
- expect(filterCheckbox.disabled).toEqual(true);
269
- expect(filterCheckbox.checked).toEqual(false);
265
+ it('filter is hidden for just first party resources', () => {
266
+ const filterControl = dom.find('#uses-text-compression .lh-3p-filter', container);
267
+ expect(filterControl.hidden).toEqual(true);
270
268
  });
271
269
  });
272
270
  });
@@ -515,6 +513,9 @@ describe('ReportUIFeatures', () => {
515
513
  expect(render(lhr).querySelector('.lh-button.lh-report-icon--treemap')).toBeTruthy();
516
514
 
517
515
  delete lhr.audits['script-treemap-data'];
516
+ const newAuditRefs = lhr.categories['performance'].auditRefs
517
+ .filter(a => a.id !== 'script-treemap-data');
518
+ lhr.categories['performance'].auditRefs = newAuditRefs;
518
519
  expect(render(lhr).querySelector('.lh-button.lh-report-icon--treemap')).toBeNull();
519
520
  });
520
521
  });
@@ -401,10 +401,10 @@ describe('util helpers', () => {
401
401
  const category = {
402
402
  id: 'performance',
403
403
  auditRefs: [
404
- {weight: 3, result: {score: 1, scoreDisplayMode: 'binary'}, group: 'diagnostics'},
405
- {weight: 2, result: {score: 1, scoreDisplayMode: 'binary'}, group: 'diagnostics'},
406
- {weight: 0, result: {score: 1, scoreDisplayMode: 'binary'}, group: 'diagnostics'},
407
- {weight: 1, result: {score: 0, scoreDisplayMode: 'binary'}, group: 'diagnostics'},
404
+ {weight: 3, result: {score: 1, scoreDisplayMode: 'binary'}, group: 'metrics'},
405
+ {weight: 2, result: {score: 1, scoreDisplayMode: 'binary'}, group: 'metrics'},
406
+ {weight: 0, result: {score: 1, scoreDisplayMode: 'binary'}, group: 'metrics'},
407
+ {weight: 1, result: {score: 0, scoreDisplayMode: 'binary'}, group: 'metrics'},
408
408
  ],
409
409
  };
410
410
  const fraction = Util.calculateCategoryFraction(category);
@@ -416,14 +416,14 @@ describe('util helpers', () => {
416
416
  });
417
417
  });
418
418
 
419
- it('ignores manual audits, N/A audits, and performance audits with no group', () => {
419
+ it('ignores manual audits, N/A audits, and hidden audits', () => {
420
420
  const category = {
421
421
  id: 'performance',
422
422
  auditRefs: [
423
- {weight: 1, result: {score: 1, scoreDisplayMode: 'binary'}, group: 'diagnostics'},
424
- {weight: 1, result: {score: 1, scoreDisplayMode: 'binary'}},
425
- {weight: 1, result: {score: 0, scoreDisplayMode: 'manual'}, group: 'diagnostics'},
426
- {weight: 1, result: {score: 0, scoreDisplayMode: 'notApplicable'}, group: 'diagnostics'},
423
+ {weight: 1, result: {score: 1, scoreDisplayMode: 'binary'}, group: 'metrics'},
424
+ {weight: 1, result: {score: 1, scoreDisplayMode: 'binary'}, group: 'hidden'},
425
+ {weight: 1, result: {score: 0, scoreDisplayMode: 'manual'}, group: 'metrics'},
426
+ {weight: 1, result: {score: 0, scoreDisplayMode: 'notApplicable'}, group: 'metrics'},
427
427
  ],
428
428
  };
429
429
  const fraction = Util.calculateCategoryFraction(category);
@@ -435,32 +435,14 @@ describe('util helpers', () => {
435
435
  });
436
436
  });
437
437
 
438
- it('does not ignore audits with no group in non-performance category', () => {
439
- const category = {
440
- id: 'seo',
441
- auditRefs: [
442
- {weight: 1, result: {score: 1, scoreDisplayMode: 'binary'}, group: 'diagnostics'},
443
- {weight: 1, result: {score: 1, scoreDisplayMode: 'binary'}},
444
- {weight: 1, result: {score: 0, scoreDisplayMode: 'manual'}, group: 'diagnostics'},
445
- ],
446
- };
447
- const fraction = Util.calculateCategoryFraction(category);
448
- expect(fraction).toEqual({
449
- numPassableAudits: 2,
450
- numPassed: 2,
451
- numInformative: 0,
452
- totalWeight: 2,
453
- });
454
- });
455
-
456
438
  it('tracks informative audits separately', () => {
457
439
  const category = {
458
440
  id: 'performance',
459
441
  auditRefs: [
460
- {weight: 1, result: {score: 1, scoreDisplayMode: 'binary'}, group: 'diagnostics'},
461
- {weight: 1, result: {score: 1, scoreDisplayMode: 'binary'}, group: 'diagnostics'},
462
- {weight: 0, result: {score: 1, scoreDisplayMode: 'informative'}, group: 'diagnostics'},
463
- {weight: 1, result: {score: 0, scoreDisplayMode: 'informative'}, group: 'diagnostics'},
442
+ {weight: 1, result: {score: 1, scoreDisplayMode: 'binary'}, group: 'metrics'},
443
+ {weight: 1, result: {score: 1, scoreDisplayMode: 'binary'}, group: 'metrics'},
444
+ {weight: 0, result: {score: 1, scoreDisplayMode: 'informative'}, group: 'metrics'},
445
+ {weight: 1, result: {score: 0, scoreDisplayMode: 'informative'}, group: 'metrics'},
464
446
  ],
465
447
  };
466
448
  const fraction = Util.calculateCategoryFraction(category);
@@ -101,15 +101,15 @@ async function decorateScreenshot(datauri, tabId) {
101
101
  img.src = datauri;
102
102
  });
103
103
  const c = document.createElement('canvas');
104
- c.width = img.width;
105
- c.height = img.height;
104
+ c.width = tabId === 'desktop' ? 280 : img.width;
105
+ c.height = tabId === 'desktop' ? 194 : img.height;
106
106
 
107
107
  const ctx = c.getContext('2d');
108
108
  if (!ctx) throw new Error();
109
- ctx.drawImage(img, 0, 0);
110
- ctx.font = `${img.width / 2}px serif`;
109
+ ctx.drawImage(img, 0, 0, c.width, c.height);
110
+ ctx.font = `${c.width / 2}px serif`;
111
111
  ctx.textAlign = 'center';
112
112
  ctx.globalAlpha = 0.7;
113
- ctx.fillText(tabId === 'mobile' ? '📱' : '💻', img.width / 2, Math.min(img.height / 2, 700));
113
+ ctx.fillText(tabId === 'mobile' ? '📱' : '💻', c.width / 2, Math.min(c.height / 2, 700));
114
114
  return c.toDataURL();
115
115
  }
@@ -7,11 +7,13 @@
7
7
 
8
8
  const fs = require('fs');
9
9
 
10
- const MessageFormat = require('intl-messageformat').default;
10
+ const MessageFormat = require('intl-messageformat');
11
11
  const {isObjectOfUnknownValues, isObjectOrArrayOfUnknownValues} = require('../type-verifiers.js');
12
12
 
13
13
  /** Contains available locales with messages. May be an empty object if bundled. */
14
- const LOCALE_MESSAGES = require('./locales.js');
14
+ /** @type {import('./locales')} */
15
+ // @ts-expect-error TODO(esmodules): remove when file is es modules.
16
+ const LOCALE_MESSAGES = require('./locales.js').default || require('./locales.js');
15
17
 
16
18
  const DEFAULT_LOCALE = 'en-US';
17
19
 
@@ -93,7 +95,7 @@ function collectAllCustomElementsFromICU(icuElements, seenElementsById = new Map
93
95
  * Returns a copy of the `values` object, with the values formatted based on how
94
96
  * they will be used in their icuMessage, e.g. KB or milliseconds. The original
95
97
  * object is unchanged.
96
- * @param {MessageFormat} messageFormatter
98
+ * @param {MessageFormat.IntlMessageFormat} messageFormatter
97
99
  * @param {Readonly<Record<string, string | number>>} values
98
100
  * @param {string} lhlMessage Used for clear error logging.
99
101
  * @return {Record<string, string | number>}
@@ -171,7 +173,10 @@ function formatMessage(message, values = {}, locale) {
171
173
  // When using accented english, force the use of a different locale for number formatting.
172
174
  const localeForMessageFormat = (locale === 'en-XA' || locale === 'en-XL') ? 'de-DE' : locale;
173
175
 
174
- const formatter = new MessageFormat(message, localeForMessageFormat, formats);
176
+ // This package is not correctly bundled by Rollup.
177
+ /** @type {typeof MessageFormat.IntlMessageFormat} */
178
+ const MessageFormatCtor = MessageFormat.IntlMessageFormat || MessageFormat;
179
+ const formatter = new MessageFormatCtor(message, localeForMessageFormat, formats);
175
180
 
176
181
  // Preformat values for the message format like KB and milliseconds.
177
182
  const valuesForMessageFormat = _preformatValues(formatter, values, message);
@@ -800,21 +800,6 @@
800
800
  "lighthouse-core/audits/dobetterweb/dom-size.js | title": {
801
801
  "message": "‏‮Avoids‬‏ ‏‮an‬‏ ‏‮excessive‬‏ ‏‮DOM‬‏ ‏‮size‬‏"
802
802
  },
803
- "lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | columnFailingAnchors": {
804
- "message": "‏‮Failing‬‏ ‏‮Anchors‬‏"
805
- },
806
- "lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | description": {
807
- "message": "‏‮Add‬‏ `rel=\"noopener\"` ‏‮or‬‏ `rel=\"noreferrer\"` ‏‮to‬‏ ‏‮any‬‏ ‏‮external‬‏ ‏‮links‬‏ ‏‮to‬‏ ‏‮improve‬‏ ‏‮performance‬‏ ‏‮and‬‏ ‏‮prevent‬‏ ‏‮security‬‏ ‏‮vulnerabilities‬‏. [‏‮Learn‬‏ ‏‮more‬‏](https://web.dev/external-anchors-use-rel-noopener/)."
808
- },
809
- "lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | failureTitle": {
810
- "message": "‏‮Links‬‏ ‏‮to‬‏ ‏‮cross‬‏-‏‮origin‬‏ ‏‮destinations‬‏ ‏‮are‬‏ ‏‮unsafe‬‏"
811
- },
812
- "lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | title": {
813
- "message": "‏‮Links‬‏ ‏‮to‬‏ ‏‮cross‬‏-‏‮origin‬‏ ‏‮destinations‬‏ ‏‮are‬‏ ‏‮safe‬‏"
814
- },
815
- "lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | warning": {
816
- "message": "‏‮Unable‬‏ ‏‮to‬‏ ‏‮determine‬‏ ‏‮the‬‏ ‏‮destination‬‏ ‏‮for‬‏ ‏‮anchor‬‏ ({anchorHTML}). ‏‮If‬‏ ‏‮not‬‏ ‏‮used‬‏ ‏‮as‬‏ ‏‮a‬‏ ‏‮hyperlink‬‏, ‏‮consider‬‏ ‏‮removing‬‏ ‏‮target‬‏=_‏‮blank‬‏."
817
- },
818
803
  "lighthouse-core/audits/dobetterweb/geolocation-on-start.js | description": {
819
804
  "message": "‏‮Users‬‏ ‏‮are‬‏ ‏‮mistrustful‬‏ ‏‮of‬‏ ‏‮or‬‏ ‏‮confused‬‏ ‏‮by‬‏ ‏‮sites‬‏ ‏‮that‬‏ ‏‮request‬‏ ‏‮their‬‏ ‏‮location‬‏ ‏‮without‬‏ ‏‮context‬‏. ‏‮Consider‬‏ ‏‮tying‬‏ ‏‮the‬‏ ‏‮request‬‏ ‏‮to‬‏ ‏‮a‬‏ ‏‮user‬‏ ‏‮action‬‏ ‏‮instead‬‏. [‏‮Learn‬‏ ‏‮more‬‏](https://web.dev/geolocation-on-start/)."
820
805
  },
@@ -800,21 +800,6 @@
800
800
  "lighthouse-core/audits/dobetterweb/dom-size.js | title": {
801
801
  "message": "تجنُب حجم DOM الزائد"
802
802
  },
803
- "lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | columnFailingAnchors": {
804
- "message": "علامات الارتساء التي تعذّرت مراجعتها"
805
- },
806
- "lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | description": {
807
- "message": "يمكنك إضافة `rel=\"noopener\"` أو `rel=\"noreferrer\"` إلى أي روابط خارجية لتحسين الأداء ومنع الثغرات الأمنية. [مزيد من المعلومات](https://web.dev/external-anchors-use-rel-noopener/)"
808
- },
809
- "lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | failureTitle": {
810
- "message": "الروابط إلى وجهات مشتركة المصدر هي غير آمنة"
811
- },
812
- "lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | title": {
813
- "message": "الروابط إلى وجهات مشتركة المصدر هي آمنة"
814
- },
815
- "lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | warning": {
816
- "message": "يتعذّر تحديد الوجهة لإعلان ثابت {anchorHTML}). في حال عدم استخدامه كرابط تشعبي، يمكنك إزالة target=_blank."
817
- },
818
803
  "lighthouse-core/audits/dobetterweb/geolocation-on-start.js | description": {
819
804
  "message": "لا يثق المستخدمون في المواقع الإلكترونية التي تطلب مواقعهم الجغرافية بدون سياق أو قد يؤدي ذلك إلى إرباكهم. يمكنك ربط الطلب بإجراء المستخدم بدلاً من ذلك. [مزيد من المعلومات](https://web.dev/geolocation-on-start/)"
820
805
  },
@@ -800,21 +800,6 @@
800
800
  "lighthouse-core/audits/dobetterweb/dom-size.js | title": {
801
801
  "message": "Не се използва DOM с твърде голям размер"
802
802
  },
803
- "lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | columnFailingAnchors": {
804
- "message": "Anchor маркери с грешки"
805
- },
806
- "lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | description": {
807
- "message": "Добавете `rel=\"noopener\"` или `rel=\"noreferrer\"` към връзките към външни сайтове, за да подобрите ефективността и да избегнете уязвимости в сигурността. [Научете повече](https://web.dev/external-anchors-use-rel-noopener/)."
808
- },
809
- "lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | failureTitle": {
810
- "message": "Връзките към външни дестинации не са безопасни"
811
- },
812
- "lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | title": {
813
- "message": "Връзките към външни дестинации са безопасни"
814
- },
815
- "lighthouse-core/audits/dobetterweb/external-anchors-use-rel-noopener.js | warning": {
816
- "message": "Не можахме да определим дестинацията за котвата ({anchorHTML}). Ако не се използва като хипервръзка, бихте могли да премахнете target=_blank."
817
- },
818
803
  "lighthouse-core/audits/dobetterweb/geolocation-on-start.js | description": {
819
804
  "message": "Потребителите се объркват или нямат доверие на сайтове, които искат да узнаят местоположението им без контекст. Вместо това бихте могли да обвържете заявката към действие на потребителя. [Научете повече](https://web.dev/geolocation-on-start/)."
820
805
  },