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.
- package/core/config/metrics-to-audits.js +1 -0
- package/dist/report/bundle.esm.js +112 -21
- package/dist/report/flow.js +13 -13
- package/dist/report/standalone.js +5 -5
- package/package.json +1 -1
- package/report/renderer/details-renderer.js +15 -2
- package/report/renderer/report-ui-features.js +25 -19
- package/report/renderer/report-utils.d.ts +13 -0
- package/report/renderer/report-utils.js +72 -0
- package/report/test/renderer/report-ui-features-test.js +299 -72
- package/report/test/renderer/report-utils-test.js +51 -0
|
@@ -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
|
|
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
|
|
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 =
|
|
5750
|
+
let rowEl = primaryRowEls[0];
|
|
5664
5751
|
while (rowEl) {
|
|
5665
|
-
const shouldHide = shouldHideThirdParty &&
|
|
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
|
-
`${
|
|
5769
|
+
`${thirdPartyRowEls.length}`;
|
|
5683
5770
|
this._dom.find('.lh-3p-ui-string', filterTemplate).textContent =
|
|
5684
5771
|
Globals.strings.thirdPartyResourcesLabel;
|
|
5685
5772
|
|
|
5686
|
-
const allThirdParty =
|
|
5687
|
-
const allFirstParty = !
|
|
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 (
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
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
|
-
|
|
5835
|
+
thirdPartyRowEls.push(rowEl);
|
|
5745
5836
|
}
|
|
5746
5837
|
|
|
5747
|
-
return
|
|
5838
|
+
return thirdPartyRowEls;
|
|
5748
5839
|
}
|
|
5749
5840
|
|
|
5750
5841
|
/**
|