lighthouse 9.5.0-dev.20230126 → 9.5.0-dev.20230128

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.
@@ -26,6 +26,7 @@ const lcpRelevantAudits = [
26
26
  'unused-javascript',
27
27
  'efficient-animated-content',
28
28
  'total-byte-weight',
29
+ 'lcp-lazy-loaded',
29
30
  ];
30
31
 
31
32
  const tbtRelevantAudits = [
@@ -1474,6 +1474,9 @@ class ReportUtils {
1474
1474
  }
1475
1475
  }
1476
1476
 
1477
+ // Attach table/opportunity items with entity information.
1478
+ ReportUtils.classifyEntities(result.entities, audit);
1479
+
1477
1480
  // TODO: convert printf-style displayValue.
1478
1481
  // Added: #5099, v3
1479
1482
  // Removed: #6767, v4
@@ -1571,6 +1574,75 @@ class ReportUtils {
1571
1574
  return clone;
1572
1575
  }
1573
1576
 
1577
+ /**
1578
+ * Given an audit's details, identify and return a URL locator function that
1579
+ * can be called later with an `item` to extract the URL of it.
1580
+ * @param {LH.FormattedIcu<LH.Audit.Details.TableColumnHeading[]>} headings
1581
+ * @return {{(item: LH.FormattedIcu<LH.Audit.Details.TableItem>): string|undefined}=}
1582
+ */
1583
+ static getUrlLocatorFn(headings) {
1584
+ // The most common type, valueType=url.
1585
+ const urlKey = headings.find(heading => heading.valueType === 'url')?.key;
1586
+ if (urlKey && typeof urlKey === 'string') {
1587
+ // Return a function that extracts item.url.
1588
+ return (item) => {
1589
+ const url = item[urlKey];
1590
+ if (typeof url === 'string') return url;
1591
+ };
1592
+ }
1593
+
1594
+ // The second common type, valueType=source-location.
1595
+ const srcLocationKey = headings.find(heading => heading.valueType === 'source-location')?.key;
1596
+ if (srcLocationKey) {
1597
+ // Return a function that extracts item.source.url.
1598
+ return (item) => {
1599
+ const sourceLocation = item[srcLocationKey];
1600
+ if (typeof sourceLocation === 'object' && sourceLocation.type === 'source-location') {
1601
+ return sourceLocation.url;
1602
+ }
1603
+ };
1604
+ }
1605
+
1606
+ // More specific tests go here, as we need to identify URLs in more audits.
1607
+ }
1608
+
1609
+ /**
1610
+ * Mark TableItems/OpportunityItems with entity names.
1611
+ * @param {LH.Result.Entities|undefined} entityClassification
1612
+ * @param {import('../../types/lhr/audit-result').Result} audit
1613
+ */
1614
+ static classifyEntities(entityClassification, audit) {
1615
+ if (!entityClassification) return;
1616
+ if (audit.details?.type !== 'opportunity' && audit.details?.type !== 'table') {
1617
+ return;
1618
+ }
1619
+
1620
+ // If details.items are already marked with entity attribute during an audit, nothing to do here.
1621
+ const {items, headings} = audit.details;
1622
+ if (!items.length || items.some(item => item.entity)) return;
1623
+
1624
+ // Identify a URL-locator function that we could call against each item to get its URL.
1625
+ const urlLocatorFn = ReportUtils.getUrlLocatorFn(headings);
1626
+ if (!urlLocatorFn) return;
1627
+
1628
+ for (const item of items) {
1629
+ const url = urlLocatorFn(item);
1630
+ if (!url) continue;
1631
+
1632
+ let origin;
1633
+ try {
1634
+ // Non-URLs can appear in valueType: url columns, like 'Unattributable'
1635
+ origin = Util.parseURL(url).origin;
1636
+ } catch {}
1637
+ if (!origin) continue;
1638
+
1639
+ const entityIndex = entityClassification.entityIndexByOrigin[origin];
1640
+ if (entityIndex === undefined) return;
1641
+ const entity = entityClassification.list[entityIndex];
1642
+ item.entity = entity.name;
1643
+ }
1644
+ }
1645
+
1574
1646
  /**
1575
1647
  * @param {LH.Result['configSettings']} settings
1576
1648
  * @return {!{deviceEmulation: string, screenEmulation?: string, networkThrottling: string, cpuThrottling: string, summary: string}}
@@ -2965,8 +3037,8 @@ class DetailsRenderer {
2965
3037
  return null;
2966
3038
 
2967
3039
  default: {
2968
- // @ts-expect-error tsc thinks this is unreachable, but be forward compatible
2969
- // with new unexpected detail types.
3040
+ // @ts-expect-error - all detail types need to be handled above so tsc thinks this is unreachable.
3041
+ // Call _renderUnknown() to be forward compatible with new, unexpected detail types.
2970
3042
  return this._renderUnknown(details.type, details);
2971
3043
  }
2972
3044
  }
@@ -3298,9 +3370,22 @@ class DetailsRenderer {
3298
3370
  let even = true;
3299
3371
  for (const item of details.items) {
3300
3372
  const rowsFragment = this._renderTableRowsFromItem(item, details.headings);
3373
+
3374
+ // The attribute item.entity could be a string (entity-classification), or
3375
+ // a LinkValue for ThirdPartySummary audit.
3376
+ let entityName;
3377
+ if (typeof item.entity === 'object' && item.entity.type === 'link') {
3378
+ entityName = item.entity.text;
3379
+ } else if (typeof item.entity === 'string') {
3380
+ entityName = item.entity;
3381
+ }
3382
+
3301
3383
  for (const rowEl of this._dom.findAll('tr', rowsFragment)) {
3302
3384
  // For zebra styling.
3303
3385
  rowEl.classList.add(even ? 'lh-row--even' : 'lh-row--odd');
3386
+ if (entityName && !rowEl.classList.contains('lh-sub-item-row')) {
3387
+ rowEl.dataset.entity = entityName;
3388
+ }
3304
3389
  }
3305
3390
  even = !even;
3306
3391
  tbodyElem.append(rowsFragment);
@@ -5651,7 +5736,9 @@ class ReportUIFeatures {
5651
5736
 
5652
5737
  tablesWithUrls.forEach((tableEl) => {
5653
5738
  const rowEls = getTableRows(tableEl);
5654
- const thirdPartyRows = this._getThirdPartyRows(rowEls, Util.getFinalDisplayedUrl(this.json));
5739
+ const primaryRowEls = rowEls.filter(rowEl => !rowEl.classList.contains('lh-sub-item-row'));
5740
+ const thirdPartyRowEls = this._getThirdPartyRows(
5741
+ primaryRowEls, Util.getFinalDisplayedUrl(this.json));
5655
5742
 
5656
5743
  // create input box
5657
5744
  const filterTemplate = this._dom.createComponent('3pFilter');
@@ -5660,9 +5747,9 @@ class ReportUIFeatures {
5660
5747
  filterInput.addEventListener('change', e => {
5661
5748
  const shouldHideThirdParty = e.target instanceof HTMLInputElement && !e.target.checked;
5662
5749
  let even = true;
5663
- let rowEl = rowEls[0];
5750
+ let rowEl = primaryRowEls[0];
5664
5751
  while (rowEl) {
5665
- const shouldHide = shouldHideThirdParty && thirdPartyRows.includes(rowEl);
5752
+ const shouldHide = shouldHideThirdParty && thirdPartyRowEls.includes(rowEl);
5666
5753
 
5667
5754
  // Iterate subsequent associated sub item rows.
5668
5755
  do {
@@ -5679,12 +5766,12 @@ class ReportUIFeatures {
5679
5766
  });
5680
5767
 
5681
5768
  this._dom.find('.lh-3p-filter-count', filterTemplate).textContent =
5682
- `${thirdPartyRows.length}`;
5769
+ `${thirdPartyRowEls.length}`;
5683
5770
  this._dom.find('.lh-3p-ui-string', filterTemplate).textContent =
5684
5771
  Globals.strings.thirdPartyResourcesLabel;
5685
5772
 
5686
- const allThirdParty = thirdPartyRows.length === rowEls.length;
5687
- const allFirstParty = !thirdPartyRows.length;
5773
+ const allThirdParty = thirdPartyRowEls.length === primaryRowEls.length;
5774
+ const allFirstParty = !thirdPartyRowEls.length;
5688
5775
 
5689
5776
  // If all or none of the rows are 3rd party, hide the control.
5690
5777
  if (allThirdParty || allFirstParty) {
@@ -5726,25 +5813,29 @@ class ReportUIFeatures {
5726
5813
  * @return {Array<HTMLElement>}
5727
5814
  */
5728
5815
  _getThirdPartyRows(rowEls, finalDisplayedUrl) {
5729
- /** @type {Array<HTMLElement>} */
5730
- const thirdPartyRows = [];
5731
5816
  const finalDisplayedUrlRootDomain = Util.getRootDomain(finalDisplayedUrl);
5817
+ const firstPartyEntityName = this.json.entities?.firstParty;
5732
5818
 
5819
+ /** @type {Array<HTMLElement>} */
5820
+ const thirdPartyRowEls = [];
5733
5821
  for (const rowEl of rowEls) {
5734
- if (rowEl.classList.contains('lh-sub-item-row')) continue;
5735
-
5736
- const urlItem = rowEl.querySelector('div.lh-text__url');
5737
- if (!urlItem) continue;
5738
-
5739
- const datasetUrl = urlItem.dataset.url;
5740
- if (!datasetUrl) continue;
5741
- const isThirdParty = Util.getRootDomain(datasetUrl) !== finalDisplayedUrlRootDomain;
5742
- if (!isThirdParty) continue;
5822
+ if (firstPartyEntityName) {
5823
+ // We rely on entity-classification for new LHRs that support it.
5824
+ if (!rowEl.dataset.entity || rowEl.dataset.entity === firstPartyEntityName) continue;
5825
+ } else {
5826
+ // Without 10.0's entity classification, fallback to the older root domain-based filtering.
5827
+ const urlItem = rowEl.querySelector('div.lh-text__url');
5828
+ if (!urlItem) continue;
5829
+ const datasetUrl = urlItem.dataset.url;
5830
+ if (!datasetUrl) continue;
5831
+ const isThirdParty = Util.getRootDomain(datasetUrl) !== finalDisplayedUrlRootDomain;
5832
+ if (!isThirdParty) continue;
5833
+ }
5743
5834
 
5744
- thirdPartyRows.push(rowEl);
5835
+ thirdPartyRowEls.push(rowEl);
5745
5836
  }
5746
5837
 
5747
- return thirdPartyRows;
5838
+ return thirdPartyRowEls;
5748
5839
  }
5749
5840
 
5750
5841
  /**