lighthouse 10.0.2 → 10.1.0-dev.20230324

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 (117) hide show
  1. package/cli/bin.js +1 -1
  2. package/cli/run.js +1 -1
  3. package/cli/test/smokehouse/config/exclusions.js +0 -2
  4. package/cli/test/smokehouse/lighthouse-runners/bundle.js +1 -1
  5. package/core/audits/audit.d.ts +6 -0
  6. package/core/audits/audit.js +16 -4
  7. package/core/audits/bootup-time.js +2 -3
  8. package/core/audits/byte-efficiency/byte-efficiency-audit.d.ts +4 -0
  9. package/core/audits/byte-efficiency/byte-efficiency-audit.js +8 -2
  10. package/core/audits/byte-efficiency/legacy-javascript.d.ts +5 -0
  11. package/core/audits/byte-efficiency/legacy-javascript.js +42 -16
  12. package/core/audits/byte-efficiency/polyfill-graph-data.json +50 -50
  13. package/core/audits/byte-efficiency/total-byte-weight.js +1 -1
  14. package/core/audits/byte-efficiency/unminified-javascript.js +2 -1
  15. package/core/audits/byte-efficiency/uses-long-cache-ttl.js +2 -2
  16. package/core/audits/byte-efficiency/uses-responsive-images-snapshot.js +4 -0
  17. package/core/audits/dobetterweb/doctype.js +3 -2
  18. package/core/audits/long-tasks.js +2 -1
  19. package/core/audits/mainthread-work-breakdown.js +2 -1
  20. package/core/audits/network-rtt.js +1 -1
  21. package/core/audits/network-server-latency.js +2 -1
  22. package/core/audits/performance-budget.js +1 -1
  23. package/core/audits/prioritize-lcp-image.js +1 -1
  24. package/core/audits/seo/tap-targets.js +1 -1
  25. package/core/audits/third-party-summary.js +4 -1
  26. package/core/audits/timing-budget.js +2 -1
  27. package/core/audits/uses-rel-preconnect.js +1 -1
  28. package/core/audits/uses-rel-preload.js +1 -1
  29. package/core/audits/valid-source-maps.js +1 -1
  30. package/core/audits/work-during-interaction.js +1 -1
  31. package/core/computed/metrics/cumulative-layout-shift.d.ts +3 -3
  32. package/core/computed/metrics/cumulative-layout-shift.js +21 -9
  33. package/core/computed/unused-css.js +8 -7
  34. package/core/config/lr-mobile-config.js +0 -11
  35. package/core/gather/driver/prepare.js +15 -0
  36. package/core/gather/gatherers/image-elements.js +1 -0
  37. package/core/gather/gatherers/link-elements.js +2 -0
  38. package/core/lib/asset-saver.js +2 -12
  39. package/core/lib/i18n/i18n.d.ts +1 -1
  40. package/core/lib/i18n/i18n.js +1 -1
  41. package/core/lib/lighthouse-compatibility.js +17 -0
  42. package/core/lib/network-request.js +1 -1
  43. package/core/lib/page-functions.d.ts +17 -5
  44. package/core/lib/page-functions.js +41 -22
  45. package/core/lib/tracehouse/trace-processor.d.ts +8 -0
  46. package/core/lib/tracehouse/trace-processor.js +27 -6
  47. package/core/lib/url-utils.js +1 -1
  48. package/core/runner.js +1 -0
  49. package/dist/report/bundle.esm.js +258 -45
  50. package/dist/report/flow.js +18 -18
  51. package/dist/report/standalone.js +17 -17
  52. package/flow-report/src/i18n/i18n.d.ts +6 -0
  53. package/flow-report/src/summary/category.tsx +0 -1
  54. package/package.json +6 -7
  55. package/report/assets/styles.css +89 -5
  56. package/report/renderer/components.js +1 -1
  57. package/report/renderer/details-renderer.d.ts +26 -6
  58. package/report/renderer/details-renderer.js +144 -21
  59. package/report/renderer/performance-category-renderer.js +3 -3
  60. package/report/renderer/report-renderer.js +1 -0
  61. package/report/renderer/report-ui-features.js +18 -9
  62. package/report/renderer/report-utils.d.ts +13 -4
  63. package/report/renderer/report-utils.js +43 -11
  64. package/shared/localization/locales/ar-XB.json +1 -1
  65. package/shared/localization/locales/ar.json +1 -1
  66. package/shared/localization/locales/bg.json +1 -1
  67. package/shared/localization/locales/ca.json +1 -1
  68. package/shared/localization/locales/cs.json +1 -1
  69. package/shared/localization/locales/da.json +1 -1
  70. package/shared/localization/locales/de.json +1 -1
  71. package/shared/localization/locales/el.json +1 -1
  72. package/shared/localization/locales/en-GB.json +1 -1
  73. package/shared/localization/locales/en-US.json +11 -2
  74. package/shared/localization/locales/en-XA.json +1 -1
  75. package/shared/localization/locales/en-XL.json +11 -2
  76. package/shared/localization/locales/es-419.json +1 -1
  77. package/shared/localization/locales/es.json +1 -1
  78. package/shared/localization/locales/fi.json +1 -1
  79. package/shared/localization/locales/fil.json +1 -1
  80. package/shared/localization/locales/fr.json +1 -1
  81. package/shared/localization/locales/he.json +1 -1
  82. package/shared/localization/locales/hi.json +1 -1
  83. package/shared/localization/locales/hr.json +1 -1
  84. package/shared/localization/locales/hu.json +1 -1
  85. package/shared/localization/locales/id.json +1 -1
  86. package/shared/localization/locales/it.json +1 -1
  87. package/shared/localization/locales/ja.json +1 -1
  88. package/shared/localization/locales/ko.json +1 -1
  89. package/shared/localization/locales/lt.json +1 -1
  90. package/shared/localization/locales/lv.json +1 -1
  91. package/shared/localization/locales/nl.json +1 -1
  92. package/shared/localization/locales/no.json +1 -1
  93. package/shared/localization/locales/pl.json +1 -1
  94. package/shared/localization/locales/pt-PT.json +1 -1
  95. package/shared/localization/locales/pt.json +1 -1
  96. package/shared/localization/locales/ro.json +1 -1
  97. package/shared/localization/locales/ru.json +1 -1
  98. package/shared/localization/locales/sk.json +1 -1
  99. package/shared/localization/locales/sl.json +1 -1
  100. package/shared/localization/locales/sr-Latn.json +1 -1
  101. package/shared/localization/locales/sr.json +1 -1
  102. package/shared/localization/locales/sv.json +1 -1
  103. package/shared/localization/locales/ta.json +1 -1
  104. package/shared/localization/locales/te.json +1 -1
  105. package/shared/localization/locales/th.json +1 -1
  106. package/shared/localization/locales/tr.json +1 -1
  107. package/shared/localization/locales/uk.json +1 -1
  108. package/shared/localization/locales/vi.json +1 -1
  109. package/shared/localization/locales/zh-HK.json +1 -1
  110. package/shared/localization/locales/zh-TW.json +1 -1
  111. package/shared/localization/locales/zh.json +1 -1
  112. package/shared/tsconfig.json +1 -1
  113. package/shared/util.d.ts +6 -0
  114. package/shared/util.js +33 -0
  115. package/types/artifacts.d.ts +6 -1
  116. package/types/internal/node.d.ts +15 -2
  117. package/types/lhr/audit-details.d.ts +35 -11
@@ -24,22 +24,26 @@
24
24
  /** @typedef {LH.FormattedIcu<LH.Audit.Details.TableItem>} TableItem */
25
25
  /** @typedef {LH.FormattedIcu<LH.Audit.Details.ItemValue>} TableItemValue */
26
26
  /** @typedef {LH.FormattedIcu<LH.Audit.Details.TableColumnHeading>} TableColumnHeading */
27
+ /** @typedef {LH.FormattedIcu<LH.Audit.Details.Table | LH.Audit.Details.Opportunity>} TableLike */
27
28
 
28
29
  import {Util} from '../../shared/util.js';
29
30
  import {CriticalRequestChainRenderer} from './crc-details-renderer.js';
30
31
  import {ElementScreenshotRenderer} from './element-screenshot-renderer.js';
31
32
  import {Globals} from './report-globals.js';
33
+ import {ReportUtils} from './report-utils.js';
32
34
 
33
35
  const URL_PREFIXES = ['http://', 'https://', 'data:'];
36
+ const SUMMABLE_VALUETYPES = ['bytes', 'numeric', 'ms', 'timespanMs'];
34
37
 
35
38
  export class DetailsRenderer {
36
39
  /**
37
40
  * @param {DOM} dom
38
- * @param {{fullPageScreenshot?: LH.Result.FullPageScreenshot}} [options]
41
+ * @param {{fullPageScreenshot?: LH.Result.FullPageScreenshot, entities?: LH.Result.Entities}} [options]
39
42
  */
40
43
  constructor(dom, options = {}) {
41
44
  this._dom = dom;
42
45
  this._fullPageScreenshot = options.fullPageScreenshot;
46
+ this._entities = options.entities;
43
47
  }
44
48
 
45
49
  /**
@@ -376,7 +380,110 @@ export class DetailsRenderer {
376
380
  }
377
381
 
378
382
  /**
379
- * @param {{headings: TableColumnHeading[], items: TableItem[]}} details
383
+ * Adorn a table row element with entity chips based on [data-entity] attribute.
384
+ * @param {HTMLTableRowElement} rowEl
385
+ */
386
+ _adornEntityGroupRow(rowEl) {
387
+ const entityName = rowEl.dataset.entity;
388
+ if (!entityName) return;
389
+ const matchedEntity = this._entities?.find(e => e.name === entityName);
390
+ if (!matchedEntity) return;
391
+
392
+ const firstTdEl = this._dom.find('td', rowEl);
393
+
394
+ if (matchedEntity.category) {
395
+ const categoryChipEl = this._dom.createElement('span');
396
+ categoryChipEl.classList.add('lh-audit__adorn');
397
+ categoryChipEl.textContent = matchedEntity.category;
398
+ firstTdEl.append(' ', categoryChipEl);
399
+ }
400
+
401
+ if (matchedEntity.isFirstParty) {
402
+ const firstPartyChipEl = this._dom.createElement('span');
403
+ firstPartyChipEl.classList.add('lh-audit__adorn', 'lh-audit__adorn1p');
404
+ firstPartyChipEl.textContent = Globals.strings.firstPartyChipLabel;
405
+ firstTdEl.append(' ', firstPartyChipEl);
406
+ }
407
+
408
+ if (matchedEntity.homepage) {
409
+ const entityLinkEl = this._dom.createElement('a');
410
+ entityLinkEl.href = matchedEntity.homepage;
411
+ entityLinkEl.target = '_blank';
412
+ entityLinkEl.title = Globals.strings.openInANewTabTooltip;
413
+ entityLinkEl.classList.add('lh-report-icon--external');
414
+ firstTdEl.append(' ', entityLinkEl);
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Renders an entity-grouped row.
420
+ * @param {TableItem} item
421
+ * @param {LH.Audit.Details.TableColumnHeading[]} headings
422
+ */
423
+ _renderEntityGroupRow(item, headings) {
424
+ const entityColumnHeading = {...headings[0]};
425
+ // In subitem-situations (unused-javascript), ensure Entity name is not rendered as code, etc.
426
+ entityColumnHeading.valueType = 'text';
427
+ const groupedRowHeadings = [entityColumnHeading, ...headings.slice(1)];
428
+ const fragment = this._dom.createFragment();
429
+ fragment.append(this._renderTableRow(item, groupedRowHeadings));
430
+ this._dom.find('tr', fragment).classList.add('lh-row--group');
431
+ return fragment;
432
+ }
433
+
434
+ /**
435
+ * Returns an array of entity-grouped TableItems to use as the top-level rows in
436
+ * an grouped table. Each table item returned represents a unique entity, with every
437
+ * applicable key that can be grouped as a property. Optionally, supported columns are
438
+ * summed by entity, and sorted by specified keys.
439
+ * @param {TableLike} details
440
+ * @return {TableItem[]}
441
+ */
442
+ _getEntityGroupItems(details) {
443
+ const {items, headings, sortedBy} = details;
444
+ // Exclude entity-grouped audits and results without entity classification.
445
+ // Eg. Third-party Summary comes entity-grouped.
446
+ if (!items.length || details.isEntityGrouped || !items.some(item => item.entity)) {
447
+ return [];
448
+ }
449
+
450
+ const skippedColumns = new Set(details.skipSumming || []);
451
+ /** @type {string[]} */
452
+ const summableColumns = [];
453
+ for (const heading of headings) {
454
+ if (!heading.key || skippedColumns.has(heading.key)) continue;
455
+ if (SUMMABLE_VALUETYPES.includes(heading.valueType)) {
456
+ summableColumns.push(heading.key);
457
+ }
458
+ }
459
+
460
+ // Grab the first column's key to group by entity
461
+ const firstColumnKey = headings[0].key;
462
+ if (!firstColumnKey) return [];
463
+
464
+ /** @type {Map<string | undefined, TableItem>} */
465
+ const byEntity = new Map();
466
+ for (const item of items) {
467
+ const entityName = typeof item.entity === 'string' ? item.entity : undefined;
468
+ const groupedItem = byEntity.get(entityName) || {
469
+ [firstColumnKey]: entityName || Globals.strings.unattributable,
470
+ entity: entityName,
471
+ };
472
+ for (const key of summableColumns) {
473
+ groupedItem[key] = Number(groupedItem[key] || 0) + Number(item[key] || 0);
474
+ }
475
+ byEntity.set(entityName, groupedItem);
476
+ }
477
+
478
+ const result = [...byEntity.values()];
479
+ if (sortedBy) {
480
+ result.sort(ReportUtils.getTableItemSortComparator(sortedBy));
481
+ }
482
+ return result;
483
+ }
484
+
485
+ /**
486
+ * @param {TableLike} details
380
487
  * @return {Element}
381
488
  */
382
489
  _renderTable(details) {
@@ -394,29 +501,45 @@ export class DetailsRenderer {
394
501
  this._dom.createChildOf(theadTrElem, 'th', classes).append(labelEl);
395
502
  }
396
503
 
504
+ const entityItems = this._getEntityGroupItems(details);
397
505
  const tbodyElem = this._dom.createChildOf(tableElem, 'tbody');
398
- let even = true;
399
- for (const item of details.items) {
400
- const rowsFragment = this._renderTableRowsFromItem(item, details.headings);
401
-
402
- // The attribute item.entity could be a string (entity-classification), or
403
- // a LinkValue for ThirdPartySummary audit.
404
- let entityName;
405
- if (typeof item.entity === 'object' && item.entity.type === 'link') {
406
- entityName = item.entity.text;
407
- } else if (typeof item.entity === 'string') {
408
- entityName = item.entity;
506
+ if (entityItems.length) {
507
+ for (const entityItem of entityItems) {
508
+ const entityName = typeof entityItem.entity === 'string' ? entityItem.entity : undefined;
509
+ const entityGroupFragment = this._renderEntityGroupRow(entityItem, details.headings);
510
+ // Render all the items that match the heading row
511
+ for (const item of details.items.filter((item) => item.entity === entityName)) {
512
+ entityGroupFragment.append(this._renderTableRowsFromItem(item, details.headings));
513
+ }
514
+ const rowEls = this._dom.findAll('tr', entityGroupFragment);
515
+ if (entityName && rowEls.length) {
516
+ rowEls.forEach(row => row.dataset.entity = entityName);
517
+ this._adornEntityGroupRow(rowEls[0]);
518
+ }
519
+ tbodyElem.append(entityGroupFragment);
409
520
  }
410
-
411
- for (const rowEl of this._dom.findAll('tr', rowsFragment)) {
412
- // For zebra styling.
413
- rowEl.classList.add(even ? 'lh-row--even' : 'lh-row--odd');
414
- if (entityName && !rowEl.classList.contains('lh-sub-item-row')) {
415
- rowEl.dataset.entity = entityName;
521
+ } else {
522
+ let even = true;
523
+ for (const item of details.items) {
524
+ const rowsFragment = this._renderTableRowsFromItem(item, details.headings);
525
+ const rowEls = this._dom.findAll('tr', rowsFragment);
526
+ const firstRowEl = rowEls[0];
527
+ if (typeof item.entity === 'string') {
528
+ firstRowEl.dataset.entity = item.entity;
529
+ }
530
+ if (details.isEntityGrouped && item.entity) {
531
+ // If the audit is already grouped, consider first row as a heading row.
532
+ firstRowEl.classList.add('lh-row--group');
533
+ this._adornEntityGroupRow(firstRowEl);
534
+ } else {
535
+ for (const rowEl of rowEls) {
536
+ // For zebra styling (same shade for a row and its sub-rows).
537
+ rowEl.classList.add(even ? 'lh-row--even' : 'lh-row--odd');
538
+ }
416
539
  }
540
+ even = !even;
541
+ tbodyElem.append(rowsFragment);
417
542
  }
418
- even = !even;
419
- tbodyElem.append(rowsFragment);
420
543
  }
421
544
 
422
545
  return tableElem;
@@ -68,7 +68,7 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
68
68
  return element;
69
69
  }
70
70
  const details = audit.result.details;
71
- if (details.type !== 'opportunity') {
71
+ if (details.overallSavingsMs === undefined) {
72
72
  return element;
73
73
  }
74
74
 
@@ -98,7 +98,7 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
98
98
  * @return {number}
99
99
  */
100
100
  _getWastedMs(audit) {
101
- if (audit.result.details && audit.result.details.type === 'opportunity') {
101
+ if (audit.result.details) {
102
102
  const details = audit.result.details;
103
103
  if (typeof details.overallSavingsMs !== 'number') {
104
104
  throw new Error('non-opportunity details passed to _getWastedMs');
@@ -165,7 +165,7 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
165
165
  */
166
166
  _classifyPerformanceAudit(audit) {
167
167
  if (audit.group) return null;
168
- if (audit.result.details && audit.result.details.type === 'opportunity') {
168
+ if (audit.result.details?.overallSavingsMs !== undefined) {
169
169
  return 'load-opportunity';
170
170
  }
171
171
  return 'diagnostic';
@@ -268,6 +268,7 @@ export class ReportRenderer {
268
268
 
269
269
  const detailsRenderer = new DetailsRenderer(this._dom, {
270
270
  fullPageScreenshot: report.fullPageScreenshot ?? undefined,
271
+ entities: report.entities,
271
272
  });
272
273
 
273
274
  const categoryRenderer = new CategoryRenderer(this._dom, detailsRenderer);
@@ -244,9 +244,11 @@ export class ReportUIFeatures {
244
244
 
245
245
  tablesWithUrls.forEach((tableEl) => {
246
246
  const rowEls = getTableRows(tableEl);
247
- const primaryRowEls = rowEls.filter(rowEl => !rowEl.classList.contains('lh-sub-item-row'));
248
- const thirdPartyRowEls = this._getThirdPartyRows(
249
- primaryRowEls, Util.getFinalDisplayedUrl(this.json));
247
+ const nonSubItemRows = rowEls.filter(rowEl => !rowEl.classList.contains('lh-sub-item-row'));
248
+ const thirdPartyRowEls = this._getThirdPartyRows(nonSubItemRows,
249
+ Util.getFinalDisplayedUrl(this.json));
250
+ // Entity-grouped tables don't have zebra lines.
251
+ const hasZebraStyle = rowEls.some(rowEl => rowEl.classList.contains('lh-row--even'));
250
252
 
251
253
  // create input box
252
254
  const filterTemplate = this._dom.createComponent('3pFilter');
@@ -255,16 +257,19 @@ export class ReportUIFeatures {
255
257
  filterInput.addEventListener('change', e => {
256
258
  const shouldHideThirdParty = e.target instanceof HTMLInputElement && !e.target.checked;
257
259
  let even = true;
258
- let rowEl = primaryRowEls[0];
260
+ let rowEl = nonSubItemRows[0];
259
261
  while (rowEl) {
260
262
  const shouldHide = shouldHideThirdParty && thirdPartyRowEls.includes(rowEl);
261
263
 
262
264
  // Iterate subsequent associated sub item rows.
263
265
  do {
264
266
  rowEl.classList.toggle('lh-row--hidden', shouldHide);
265
- // Adjust for zebra styling.
266
- rowEl.classList.toggle('lh-row--even', !shouldHide && even);
267
- rowEl.classList.toggle('lh-row--odd', !shouldHide && !even);
267
+
268
+ if (hasZebraStyle) {
269
+ // Adjust for zebra styling.
270
+ rowEl.classList.toggle('lh-row--even', !shouldHide && even);
271
+ rowEl.classList.toggle('lh-row--odd', !shouldHide && !even);
272
+ }
268
273
 
269
274
  rowEl = /** @type {HTMLElement} */ (rowEl.nextElementSibling);
270
275
  } while (rowEl && rowEl.classList.contains('lh-sub-item-row'));
@@ -273,12 +278,16 @@ export class ReportUIFeatures {
273
278
  }
274
279
  });
275
280
 
281
+ // thirdPartyRowEls contains both heading and item rows.
282
+ // Filter out heading rows to get third party resource count.
283
+ const thirdPartyResourceCount = thirdPartyRowEls.filter(
284
+ rowEl => !rowEl.classList.contains('lh-row--group')).length;
276
285
  this._dom.find('.lh-3p-filter-count', filterTemplate).textContent =
277
- `${thirdPartyRowEls.length}`;
286
+ `${thirdPartyResourceCount}`;
278
287
  this._dom.find('.lh-3p-ui-string', filterTemplate).textContent =
279
288
  Globals.strings.thirdPartyResourcesLabel;
280
289
 
281
- const allThirdParty = thirdPartyRowEls.length === primaryRowEls.length;
290
+ const allThirdParty = thirdPartyRowEls.length === nonSubItemRows.length;
282
291
  const allFirstParty = !thirdPartyRowEls.length;
283
292
 
284
293
  // If all or none of the rows are 3rd party, hide the control.
@@ -13,15 +13,21 @@ export class ReportUtils {
13
13
  * Given an audit's details, identify and return a URL locator function that
14
14
  * can be called later with an `item` to extract the URL of it.
15
15
  * @param {LH.FormattedIcu<LH.Audit.Details.TableColumnHeading[]>} headings
16
- * @return {{(item: LH.FormattedIcu<LH.Audit.Details.TableItem>): string|undefined}=}
16
+ * @return {((item: LH.FormattedIcu<LH.Audit.Details.TableItem>) => string|undefined)=}
17
17
  */
18
18
  static getUrlLocatorFn(headings: LH.FormattedIcu<LH.Audit.Details.TableColumnHeading[]>): ((item: LH.FormattedIcu<LH.Audit.Details.TableItem>) => string | undefined) | undefined;
19
19
  /**
20
20
  * Mark TableItems/OpportunityItems with entity names.
21
- * @param {LH.Result.Entities|undefined} entities
22
- * @param {import('../../types/lhr/audit-result').Result} audit
21
+ * @param {LH.Result.Entities} entities
22
+ * @param {LH.FormattedIcu<LH.Audit.Details.Opportunity|LH.Audit.Details.Table>} details
23
23
  */
24
- static classifyEntities(entities: LH.Result.Entities | undefined, audit: import('../../types/lhr/audit-result').Result): void;
24
+ static classifyEntities(entities: LH.Result.Entities, details: LH.FormattedIcu<LH.Audit.Details.Opportunity | LH.Audit.Details.Table>): void;
25
+ /**
26
+ * Returns a comparator created from the supplied list of keys
27
+ * @param {Array<string>} sortedBy
28
+ * @return {((a: LH.Audit.Details.TableItem, b: LH.Audit.Details.TableItem) => number)}
29
+ */
30
+ static getTableItemSortComparator(sortedBy: Array<string>): (a: LH.Audit.Details.TableItem, b: LH.Audit.Details.TableItem) => number;
25
31
  /**
26
32
  * @param {LH.Result['configSettings']} settings
27
33
  * @return {!{deviceEmulation: string, screenEmulation?: string, networkThrottling: string, cpuThrottling: string, summary: string}}
@@ -125,5 +131,8 @@ export namespace UIStrings {
125
131
  const collapseView: string;
126
132
  const runtimeSlow4g: string;
127
133
  const runtimeCustom: string;
134
+ const firstPartyChipLabel: string;
135
+ const openInANewTabTooltip: string;
136
+ const unattributable: string;
128
137
  }
129
138
  //# sourceMappingURL=report-utils.d.ts.map
@@ -27,7 +27,13 @@ class ReportUtils {
27
27
 
28
28
  for (const audit of Object.values(clone.audits)) {
29
29
  // Attach table/opportunity items with entity information.
30
- ReportUtils.classifyEntities(clone.entities, audit);
30
+ if (audit.details) {
31
+ if (audit.details.type === 'opportunity' || audit.details.type === 'table') {
32
+ if (!audit.details.isEntityGrouped && clone.entities) {
33
+ ReportUtils.classifyEntities(clone.entities, audit.details);
34
+ }
35
+ }
36
+ }
31
37
  }
32
38
 
33
39
  // For convenience, smoosh all AuditResults into their auditRef (which has just weight & group)
@@ -79,7 +85,7 @@ class ReportUtils {
79
85
  * Given an audit's details, identify and return a URL locator function that
80
86
  * can be called later with an `item` to extract the URL of it.
81
87
  * @param {LH.FormattedIcu<LH.Audit.Details.TableColumnHeading[]>} headings
82
- * @return {{(item: LH.FormattedIcu<LH.Audit.Details.TableItem>): string|undefined}=}
88
+ * @return {((item: LH.FormattedIcu<LH.Audit.Details.TableItem>) => string|undefined)=}
83
89
  */
84
90
  static getUrlLocatorFn(headings) {
85
91
  // The most common type, valueType=url.
@@ -109,17 +115,12 @@ class ReportUtils {
109
115
 
110
116
  /**
111
117
  * Mark TableItems/OpportunityItems with entity names.
112
- * @param {LH.Result.Entities|undefined} entities
113
- * @param {import('../../types/lhr/audit-result').Result} audit
118
+ * @param {LH.Result.Entities} entities
119
+ * @param {LH.FormattedIcu<LH.Audit.Details.Opportunity|LH.Audit.Details.Table>} details
114
120
  */
115
- static classifyEntities(entities, audit) {
116
- if (!entities) return;
117
- if (audit.details?.type !== 'opportunity' && audit.details?.type !== 'table') {
118
- return;
119
- }
120
-
121
+ static classifyEntities(entities, details) {
121
122
  // If details.items are already marked with entity attribute during an audit, nothing to do here.
122
- const {items, headings} = audit.details;
123
+ const {items, headings} = details;
123
124
  if (!items.length || items.some(item => item.entity)) return;
124
125
 
125
126
  // Identify a URL-locator function that we could call against each item to get its URL.
@@ -142,6 +143,30 @@ class ReportUtils {
142
143
  }
143
144
  }
144
145
 
146
+ /**
147
+ * Returns a comparator created from the supplied list of keys
148
+ * @param {Array<string>} sortedBy
149
+ * @return {((a: LH.Audit.Details.TableItem, b: LH.Audit.Details.TableItem) => number)}
150
+ */
151
+ static getTableItemSortComparator(sortedBy) {
152
+ return (a, b) => {
153
+ for (const key of sortedBy) {
154
+ const aVal = a[key];
155
+ const bVal = b[key];
156
+ if (typeof aVal !== typeof bVal || !['number', 'string'].includes(typeof aVal)) {
157
+ console.warn(`Warning: Attempting to sort unsupported value type: ${key}.`);
158
+ }
159
+ if (typeof aVal === 'number' && typeof bVal === 'number' && aVal !== bVal) {
160
+ return bVal - aVal;
161
+ }
162
+ if (typeof aVal === 'string' && typeof bVal === 'string' && aVal !== bVal) {
163
+ return aVal.localeCompare(bVal);
164
+ }
165
+ }
166
+ return 0;
167
+ };
168
+ }
169
+
145
170
  /**
146
171
  * @param {LH.Result['configSettings']} settings
147
172
  * @return {!{deviceEmulation: string, screenEmulation?: string, networkThrottling: string, cpuThrottling: string, summary: string}}
@@ -440,6 +465,13 @@ const UIStrings = {
440
465
  runtimeSlow4g: 'Slow 4G throttling',
441
466
  /** Label indicating that Lighthouse throttled the page using custom throttling settings. */
442
467
  runtimeCustom: 'Custom throttling',
468
+
469
+ /** This label is for a decorative chip that is included in a table row. The label indicates that the entity/company name in the row belongs to the first-party (or "1st-party"). First-party label is used to identify resources that are directly controlled by the owner of the web page. */
470
+ firstPartyChipLabel: '1st party',
471
+ /** Descriptive explanation in a tooltip form for a link to be opened in a new tab of the browser. */
472
+ openInANewTabTooltip: 'Open in a new tab',
473
+ /** Generic category name for all resources that could not be attributed to a 1st or 3rd party entity. */
474
+ unattributable: 'Unattributable',
443
475
  };
444
476
 
445
477
  export {
@@ -1497,7 +1497,7 @@
1497
1497
  "message": "‏‮Map‬‏ ‏‮URL‬‏"
1498
1498
  },
1499
1499
  "core/audits/valid-source-maps.js | description": {
1500
- "message": "‏‮Source‬‏ ‏‮maps‬‏ ‏‮translate‬‏ ‏‮minified‬‏ ‏‮code‬‏ ‏‮to‬‏ ‏‮the‬‏ ‏‮original‬‏ ‏‮source‬‏ ‏‮code‬‏. ‏‮This‬‏ ‏‮helps‬‏ ‏‮developers‬‏ ‏‮debug‬‏ ‏‮in‬‏ ‏‮production‬‏. ‏‮In‬‏ ‏‮addition‬‏, ‏‮Lighthouse‬‏ ‏‮is‬‏ ‏‮able‬‏ ‏‮to‬‏ ‏‮provide‬‏ ‏‮further‬‏ ‏‮insights‬‏. ‏‮Consider‬‏ ‏‮deploying‬‏ ‏‮source‬‏ ‏‮maps‬‏ ‏‮to‬‏ ‏‮take‬‏ ‏‮advantage‬‏ ‏‮of‬‏ ‏‮these‬‏ ‏‮benefits‬‏. [‏‮Learn‬‏ ‏‮more‬‏ ‏‮about‬‏ ‏‮source‬‏ ‏‮maps‬‏](https://developers.google.com/web/tools/chrome-devtools/javascript/source-maps)."
1500
+ "message": "‏‮Source‬‏ ‏‮maps‬‏ ‏‮translate‬‏ ‏‮minified‬‏ ‏‮code‬‏ ‏‮to‬‏ ‏‮the‬‏ ‏‮original‬‏ ‏‮source‬‏ ‏‮code‬‏. ‏‮This‬‏ ‏‮helps‬‏ ‏‮developers‬‏ ‏‮debug‬‏ ‏‮in‬‏ ‏‮production‬‏. ‏‮In‬‏ ‏‮addition‬‏, ‏‮Lighthouse‬‏ ‏‮is‬‏ ‏‮able‬‏ ‏‮to‬‏ ‏‮provide‬‏ ‏‮further‬‏ ‏‮insights‬‏. ‏‮Consider‬‏ ‏‮deploying‬‏ ‏‮source‬‏ ‏‮maps‬‏ ‏‮to‬‏ ‏‮take‬‏ ‏‮advantage‬‏ ‏‮of‬‏ ‏‮these‬‏ ‏‮benefits‬‏. [‏‮Learn‬‏ ‏‮more‬‏ ‏‮about‬‏ ‏‮source‬‏ ‏‮maps‬‏](https://developer.chrome.com/docs/devtools/javascript/source-maps/)."
1501
1501
  },
1502
1502
  "core/audits/valid-source-maps.js | failureTitle": {
1503
1503
  "message": "‏‮Missing‬‏ ‏‮source‬‏ ‏‮maps‬‏ ‏‮for‬‏ ‏‮large‬‏ ‏‮first‬‏-‏‮party‬‏ ‏‮JavaScript‬‏"
@@ -1497,7 +1497,7 @@
1497
1497
  "message": "عنوان URL لخريطة المصدر"
1498
1498
  },
1499
1499
  "core/audits/valid-source-maps.js | description": {
1500
- "message": "تحوِّل خرائط المصدر الرمز المصغَّر إلى رمز المصدر الأصلي. ويساعد هذا الإجراء المطوّرين على تصحيح الأخطاء في مرحلة الإنتاج. ويمكن لأداة Lighthouse أيضًا أن توفّر معلومات إضافية. ننصحك بنشر خرائط المصدر للاستفادة من هذه المزايا. [مزيد من المعلومات حول خرائط المصدر](https://developers.google.com/web/tools/chrome-devtools/javascript/source-maps)"
1500
+ "message": "تحوِّل خرائط المصدر الرمز المصغَّر إلى رمز المصدر الأصلي. ويساعد هذا الإجراء المطوّرين على تصحيح الأخطاء في مرحلة الإنتاج. ويمكن لأداة Lighthouse أيضًا أن توفّر معلومات إضافية. ننصحك بنشر خرائط المصدر للاستفادة من هذه المزايا. [مزيد من المعلومات حول خرائط المصدر](https://developer.chrome.com/docs/devtools/javascript/source-maps/)"
1501
1501
  },
1502
1502
  "core/audits/valid-source-maps.js | failureTitle": {
1503
1503
  "message": "عدم توفّر خرائط مصدر للغة JavaScript من الطرف الأول الكبير"
@@ -1497,7 +1497,7 @@
1497
1497
  "message": "URL адрес на карта"
1498
1498
  },
1499
1499
  "core/audits/valid-source-maps.js | description": {
1500
- "message": "Картите на изходния код преобразуват минимизирания код в оригиналния му вид. Това помага на програмистите да отстраняват грешки в стандартния канал. Също така Lighthouse може да предоставя допълнителни статистически данни. Препоръчваме да внедрите карти на изходния код, за да се възползвате от тези предимства. [Научете повече за картите](https://developers.google.com/web/tools/chrome-devtools/javascript/source-maps)."
1500
+ "message": "Картите на изходния код преобразуват минимизирания код в оригиналния му вид. Това помага на програмистите да отстраняват грешки в стандартния канал. Също така Lighthouse може да предоставя допълнителни статистически данни. Препоръчваме да внедрите карти на изходния код, за да се възползвате от тези предимства. [Научете повече за картите](https://developer.chrome.com/docs/devtools/javascript/source-maps/)."
1501
1501
  },
1502
1502
  "core/audits/valid-source-maps.js | failureTitle": {
1503
1503
  "message": "Липсват карти на изходния код за големи файлове на JavaScript"
@@ -1497,7 +1497,7 @@
1497
1497
  "message": "URL del mapa"
1498
1498
  },
1499
1499
  "core/audits/valid-source-maps.js | description": {
1500
- "message": "Els mapes d'origen tradueixen el codi reduït al codi font original. Això ajuda els desenvolupadors a depurar durant la producció. A més, Lighthouse pot proporcionar estadístiques addicionals. Planteja't implementar mapes d'origen per treure partit d'aquests avantatges. [Obtén més informació sobre els mapes d'origen](https://developers.google.com/web/tools/chrome-devtools/javascript/source-maps)."
1500
+ "message": "Els mapes d'origen tradueixen el codi reduït al codi font original. Això ajuda els desenvolupadors a depurar durant la producció. A més, Lighthouse pot proporcionar estadístiques addicionals. Planteja't implementar mapes d'origen per treure partit d'aquests avantatges. [Obtén més informació sobre els mapes d'origen](https://developer.chrome.com/docs/devtools/javascript/source-maps/)."
1501
1501
  },
1502
1502
  "core/audits/valid-source-maps.js | failureTitle": {
1503
1503
  "message": "Falten mapes d'origen per a un fitxer JavaScript propi gran"
@@ -1497,7 +1497,7 @@
1497
1497
  "message": "Adresa URL souboru sourcemap"
1498
1498
  },
1499
1499
  "core/audits/valid-source-maps.js | description": {
1500
- "message": "Soubory sourcemap překládají minifikovaný kód na původní zdrojový kód. To vývojářům usnadňuje ladění produkčních verzí. Kromě toho to nástroji Lighthouse umožňuje poskytovat více informací. Zvažte implementaci souborů sourcemap, abyste tyto výhody získali. [Další informace o souborech sourcemap](https://developers.google.com/web/tools/chrome-devtools/javascript/source-maps)"
1500
+ "message": "Soubory sourcemap překládají minifikovaný kód na původní zdrojový kód. To vývojářům usnadňuje ladění produkčních verzí. Kromě toho to nástroji Lighthouse umožňuje poskytovat více informací. Zvažte implementaci souborů sourcemap, abyste tyto výhody získali. [Další informace o souborech sourcemap](https://developer.chrome.com/docs/devtools/javascript/source-maps/)"
1501
1501
  },
1502
1502
  "core/audits/valid-source-maps.js | failureTitle": {
1503
1503
  "message": "Chybí soubory sourcemap pro velké soubory JavaScript první strany"
@@ -1497,7 +1497,7 @@
1497
1497
  "message": "Kortets webadresse"
1498
1498
  },
1499
1499
  "core/audits/valid-source-maps.js | description": {
1500
- "message": "Kildekort oversætter formindsket kode til den originale kildekode. Dette hjælper udviklere med at fejlrette under produktion. Derudover kan Lighthouse levere yderligere indsigter. Overvej at implementere kildekort for at drage fordel af disse. [Få flere oplysninger om kildekort](https://developers.google.com/web/tools/chrome-devtools/javascript/source-maps)."
1500
+ "message": "Kildekort oversætter formindsket kode til den originale kildekode. Dette hjælper udviklere med at fejlrette under produktion. Derudover kan Lighthouse levere yderligere indsigter. Overvej at implementere kildekort for at drage fordel af disse. [Få flere oplysninger om kildekort](https://developer.chrome.com/docs/devtools/javascript/source-maps/)."
1501
1501
  },
1502
1502
  "core/audits/valid-source-maps.js | failureTitle": {
1503
1503
  "message": "Manglende kildekort til stor førsteparts-JavaScript"
@@ -1497,7 +1497,7 @@
1497
1497
  "message": "Zuordnungs-URL"
1498
1498
  },
1499
1499
  "core/audits/valid-source-maps.js | description": {
1500
- "message": "Quellzuordnungen übersetzen reduzierten Code in den ursprünglichen Quellcode. Dies hilft Entwicklern beim Debugging in der Produktionsphase. Zusätzlich kann Lighthouse weitere Informationen liefern. Wir empfehlen, Quellzuordnungen bereitzustellen, um diese Vorteile zu nutzen. [Weitere Informationen zu Quellzuordnungen.](https://developers.google.com/web/tools/chrome-devtools/javascript/source-maps)"
1500
+ "message": "Quellzuordnungen übersetzen reduzierten Code in den ursprünglichen Quellcode. Dies hilft Entwicklern beim Debugging in der Produktionsphase. Zusätzlich kann Lighthouse weitere Informationen liefern. Wir empfehlen, Quellzuordnungen bereitzustellen, um diese Vorteile zu nutzen. [Weitere Informationen zu Quellzuordnungen.](https://developer.chrome.com/docs/devtools/javascript/source-maps/)"
1501
1501
  },
1502
1502
  "core/audits/valid-source-maps.js | failureTitle": {
1503
1503
  "message": "Fehlende Quellzuordnungen für große eigene JavaScript-Dateien"
@@ -1497,7 +1497,7 @@
1497
1497
  "message": "URL αντιστοίχισης"
1498
1498
  },
1499
1499
  "core/audits/valid-source-maps.js | description": {
1500
- "message": "Οι αντιστοιχίσεις πηγής μεταφράζουν ελαχιστοποιημένο κώδικα στον αρχικό πηγαίο κώδικα. Αυτό βοηθά τους προγραμματιστές με τον εντοπισμό σφαλμάτων στην παραγωγή. Επίσης, το Lighthouse μπορεί να παράσχει περαιτέρω insight. Εξετάστε το ενδεχόμενο ανάπτυξης αντιστοιχίσεων πηγών, για να αξιοποιήσετε αυτά τα οφέλη. [Μάθετε περισσότερα σχετικά με τις αντιστοιχίσεις πηγών](https://developers.google.com/web/tools/chrome-devtools/javascript/source-maps)."
1500
+ "message": "Οι αντιστοιχίσεις πηγής μεταφράζουν ελαχιστοποιημένο κώδικα στον αρχικό πηγαίο κώδικα. Αυτό βοηθά τους προγραμματιστές με τον εντοπισμό σφαλμάτων στην παραγωγή. Επίσης, το Lighthouse μπορεί να παράσχει περαιτέρω insight. Εξετάστε το ενδεχόμενο ανάπτυξης αντιστοιχίσεων πηγών, για να αξιοποιήσετε αυτά τα οφέλη. [Μάθετε περισσότερα σχετικά με τις αντιστοιχίσεις πηγών](https://developer.chrome.com/docs/devtools/javascript/source-maps/)."
1501
1501
  },
1502
1502
  "core/audits/valid-source-maps.js | failureTitle": {
1503
1503
  "message": "Λείπουν οι αντιστοιχίσεις πηγής για μεγάλες βιβλιοθήκες JavaScript πρώτου μέρους"
@@ -1497,7 +1497,7 @@
1497
1497
  "message": "Map URL"
1498
1498
  },
1499
1499
  "core/audits/valid-source-maps.js | description": {
1500
- "message": "Source maps translate minified code to the original source code. This helps developers to debug in production. In addition, Lighthouse is able to provide further insights. Consider deploying source maps to take advantage of these benefits. [Learn more about source maps](https://developers.google.com/web/tools/chrome-devtools/javascript/source-maps)."
1500
+ "message": "Source maps translate minified code to the original source code. This helps developers to debug in production. In addition, Lighthouse is able to provide further insights. Consider deploying source maps to take advantage of these benefits. [Learn more about source maps](https://developer.chrome.com/docs/devtools/javascript/source-maps/)."
1501
1501
  },
1502
1502
  "core/audits/valid-source-maps.js | failureTitle": {
1503
1503
  "message": "Missing source maps for large first-party JavaScript"
@@ -1323,7 +1323,7 @@
1323
1323
  "message": "robots.txt is valid"
1324
1324
  },
1325
1325
  "core/audits/seo/tap-targets.js | description": {
1326
- "message": "Interactive elements like buttons and links should be large enough (48x48px), and have enough space around them, to be easy enough to tap without overlapping onto other elements. [Learn more about tap targets](https://developer.chrome.com/docs/lighthouse/seo/tap-targets/)."
1326
+ "message": "Interactive elements like buttons and links should be large enough (48x48px), or have enough space around them, to be easy enough to tap without overlapping onto other elements. [Learn more about tap targets](https://developer.chrome.com/docs/lighthouse/seo/tap-targets/)."
1327
1327
  },
1328
1328
  "core/audits/seo/tap-targets.js | displayValue": {
1329
1329
  "message": "{decimalProportion, number, percent} appropriately sized tap targets"
@@ -1497,7 +1497,7 @@
1497
1497
  "message": "Map URL"
1498
1498
  },
1499
1499
  "core/audits/valid-source-maps.js | description": {
1500
- "message": "Source maps translate minified code to the original source code. This helps developers debug in production. In addition, Lighthouse is able to provide further insights. Consider deploying source maps to take advantage of these benefits. [Learn more about source maps](https://developers.google.com/web/tools/chrome-devtools/javascript/source-maps)."
1500
+ "message": "Source maps translate minified code to the original source code. This helps developers debug in production. In addition, Lighthouse is able to provide further insights. Consider deploying source maps to take advantage of these benefits. [Learn more about source maps](https://developer.chrome.com/docs/devtools/javascript/source-maps/)."
1501
1501
  },
1502
1502
  "core/audits/valid-source-maps.js | failureTitle": {
1503
1503
  "message": "Missing source maps for large first-party JavaScript"
@@ -3047,6 +3047,9 @@
3047
3047
  "report/renderer/report-utils.js | expandView": {
3048
3048
  "message": "Expand view"
3049
3049
  },
3050
+ "report/renderer/report-utils.js | firstPartyChipLabel": {
3051
+ "message": "1st party"
3052
+ },
3050
3053
  "report/renderer/report-utils.js | footerIssue": {
3051
3054
  "message": "File an issue"
3052
3055
  },
@@ -3065,6 +3068,9 @@
3065
3068
  "report/renderer/report-utils.js | notApplicableAuditsGroupTitle": {
3066
3069
  "message": "Not applicable"
3067
3070
  },
3071
+ "report/renderer/report-utils.js | openInANewTabTooltip": {
3072
+ "message": "Open in a new tab"
3073
+ },
3068
3074
  "report/renderer/report-utils.js | opportunityResourceColumnLabel": {
3069
3075
  "message": "Opportunity"
3070
3076
  },
@@ -3143,6 +3149,9 @@
3143
3149
  "report/renderer/report-utils.js | toplevelWarningsMessage": {
3144
3150
  "message": "There were issues affecting this run of Lighthouse:"
3145
3151
  },
3152
+ "report/renderer/report-utils.js | unattributable": {
3153
+ "message": "Unattributable"
3154
+ },
3146
3155
  "report/renderer/report-utils.js | varianceDisclaimer": {
3147
3156
  "message": "Values are estimated and may vary. The [performance score is calculated](https://developer.chrome.com/docs/lighthouse/performance/performance-scoring/) directly from these metrics."
3148
3157
  },
@@ -1497,7 +1497,7 @@
1497
1497
  "message": "[Måþ ÛŔĻ one]"
1498
1498
  },
1499
1499
  "core/audits/valid-source-maps.js | description": {
1500
- "message": "[Šöûŕçé måþš ţŕåñšļåţé mîñîƒîéð çöðé ţö ţĥé öŕîĝîñåļ šöûŕçé çöðé. Ţĥîš ĥéļþš ðévéļöþéŕš ðéбûĝ îñ þŕöðûçţîöñ. Îñ åððîţîöñ, Ļîĝĥţĥöûšé îš åбļé ţö þŕövîðé ƒûŕţĥéŕ îñšîĝĥţš. Çöñšîðéŕ ðéþļöýîñĝ šöûŕçé måþš ţö ţåķé åðvåñţåĝé öƒ ţĥéšé бéñéƒîţš. ᐅ[ᐊĻéåŕñ möŕé åбöûţ šöûŕçé måþšᐅ](https://developers.google.com/web/tools/chrome-devtools/javascript/source-maps)ᐊ. one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty twentyone twentytwo twentythree twentyfour twentyfive twentysix twentyseven twentyeight twentynine thirty thirtyone thirtytwo thirtythree thirtyfour]"
1500
+ "message": "[Šöûŕçé måþš ţŕåñšļåţé mîñîƒîéð çöðé ţö ţĥé öŕîĝîñåļ šöûŕçé çöðé. Ţĥîš ĥéļþš ðévéļöþéŕš ðéбûĝ îñ þŕöðûçţîöñ. Îñ åððîţîöñ, Ļîĝĥţĥöûšé îš åбļé ţö þŕövîðé ƒûŕţĥéŕ îñšîĝĥţš. Çöñšîðéŕ ðéþļöýîñĝ šöûŕçé måþš ţö ţåķé åðvåñţåĝé öƒ ţĥéšé бéñéƒîţš. ᐅ[ᐊĻéåŕñ möŕé åбöûţ šöûŕçé måþšᐅ](https://developer.chrome.com/docs/devtools/javascript/source-maps/)ᐊ. one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty twentyone twentytwo twentythree twentyfour twentyfive twentysix twentyseven twentyeight twentynine thirty thirtyone thirtytwo thirtythree thirtyfour]"
1501
1501
  },
1502
1502
  "core/audits/valid-source-maps.js | failureTitle": {
1503
1503
  "message": "[Mîššîñĝ šöûŕçé måþš ƒöŕ ļåŕĝé ƒîŕšţ-þåŕţý ĴåvåŠçŕîþţ one two three four five six seven eight nine ten eleven]"