Repository: GoogleChrome/lighthouse
Pull request #12477 (2021-05-13 19:48:49)

report: metric filter refactor to JS and adornments
some (potential) followups from #11732

**JS refactor:** After [connor raised the question](https://github.com/GoogleChrome/lighthouse/pull/11732#discussion_r606377369), I tried out the JS approach to see. I'm fine with either, so if folks prefer the JS style, wfm.

**adornments [idea](https://github.com/GoogleChrome/lighthouse/pull/11732#issuecomment-814359015)**: the data side of it was annoying enough to split into this. The UI bit seems interesting, no strong feelings, tbh.

<img src="https://user-images.githubusercontent.com/39191/118178186-80d29100-b3e8-11eb-9ff5-f9e02137c912.png" width=500>


demo: https://lighthouse-git-adorn-googlechrome.vercel.app/english/


connorjclark: In both demo reports, selecting "CLS" removes any relevant audits from the "Passed Audits" section. Ideally that entire section would be hidden while in this state. Same for "Opportunities". Can that be done with just CSS? Probably, because it's not just CSS, you're dynamically writing whatever CSS you want with JS.

For the report, something weird is happening when you toggle to CLS and then back to All–there is then nothing left in "Passed Audits".

I still think that JS is simpler to understand than dynamically generated CSS.
paulirish: > selecting "CLS" removes any relevant audits from the "Passed Audits" section. Ideally that entire section would be hidden while in this state. 

yeah i was skipping that functionality when this was CSS only. now that it's JS-driven i can add it.


> For this PR's demo, something weird is happening when you toggle to CLS and then back to All–there is then nothing left in "Passed Audits".

good catch. i'll fix that.

Patch:
diff --git a/lighthouse-core/lib/i18n/locales/en-US.json b/lighthouse-core/lib/i18n/locales/en-US.json
index 3c8ece5da..2adbda395 100644
--- a/lighthouse-core/lib/i18n/locales/en-US.json
+++ b/lighthouse-core/lib/i18n/locales/en-US.json
@@ -2069,6 +2069,9 @@
   "lighthouse-core/report/html/renderer/util.js | runtimeUnknown": {
     "message": "Unknown"
   },
+  "lighthouse-core/report/html/renderer/util.js | showRelevantAudits": {
+    "message": "Show audits relevant to:"
+  },
   "lighthouse-core/report/html/renderer/util.js | snippetCollapseButtonLabel": {
     "message": "Collapse snippet"
   },
diff --git a/lighthouse-core/lib/i18n/locales/en-XL.json b/lighthouse-core/lib/i18n/locales/en-XL.json
index 61a927903..491703aeb 100644
--- a/lighthouse-core/lib/i18n/locales/en-XL.json
+++ b/lighthouse-core/lib/i18n/locales/en-XL.json
@@ -2069,6 +2069,9 @@
   "lighthouse-core/report/html/renderer/util.js | runtimeUnknown": {
     "message": "Ûńk̂ńôẃn̂"
   },
+  "lighthouse-core/report/html/renderer/util.js | showRelevantAudits": {
+    "message": "Ŝh́ôẃ âúd̂ít̂ś r̂él̂év̂án̂t́ t̂ó:"
+  },
   "lighthouse-core/report/html/renderer/util.js | snippetCollapseButtonLabel": {
     "message": "Ĉól̂ĺâṕŝé ŝńîṕp̂ét̂"
   },
diff --git a/lighthouse-core/report/html/renderer/category-renderer.js b/lighthouse-core/report/html/renderer/category-renderer.js
index f9655b850..b47aabf94 100644
--- a/lighthouse-core/report/html/renderer/category-renderer.js
+++ b/lighthouse-core/report/html/renderer/category-renderer.js
@@ -79,8 +79,15 @@ class CategoryRenderer {
 
     const titleEl = this.dom.find('.lh-audit__title', auditEl);
     titleEl.appendChild(this.dom.convertMarkdownCodeSnippets(audit.result.title));
-    this.dom.find('.lh-audit__description', auditEl)
-        .appendChild(this.dom.convertMarkdownLinkSnippets(audit.result.description));
+    const descEl = this.dom.find('.lh-audit__description', auditEl);
+    descEl.appendChild(this.dom.convertMarkdownLinkSnippets(audit.result.description));
+
+    for (const relevantMetric of audit.relevantMetrics || []) {
+      const adornEl = this.dom.createChildOf(descEl, 'span', 'lh-audit__adorn', {
+        title: `Relevant to ${relevantMetric.result.title}`,
+      });
+      adornEl.textContent = relevantMetric.acronym || relevantMetric.id;
+    }
 
     if (audit.stackPacks) {
       audit.stackPacks.forEach(pack => {
diff --git a/lighthouse-core/report/html/renderer/performance-category-renderer.js b/lighthouse-core/report/html/renderer/performance-category-renderer.js
index 60ce9cb41..f5dfa2c32 100644
--- a/lighthouse-core/report/html/renderer/performance-category-renderer.js
+++ b/lighthouse-core/report/html/renderer/performance-category-renderer.js
@@ -301,66 +301,63 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
    * @param {HTMLDivElement} categoryEl
    */
   renderMetricAuditFilter(filterableMetrics, categoryEl) {
-    // thx https://codepen.io/surjithctly/pen/weEJvX
     const metricFilterEl = this.dom.createElement('div', 'lh-metricfilter');
     const textEl = this.dom.createChildOf(metricFilterEl, 'span', 'lh-metricfilter__text');
-    textEl.textContent = 'Show audits relevant to: ';
-    const labelSelectors = [];
-    const auditSelectors = [];
+    textEl.textContent = Util.i18n.strings.showRelevantAudits;
 
     const filterChoices = /** @type {LH.ReportResult.AuditRef[]} */ ([
       ({acronym: 'All'}),
       ...filterableMetrics,
     ]);
     for (const metric of filterChoices) {
-      // The radio elements are appended into `categoryEl` to allow the sweet ~ selectors to work
       const elemId = `metric-${metric.acronym}`;
-      const radioEl = this.dom.createChildOf(categoryEl, 'input', 'lh-metricfilter__radio', {
-        type: 'radio',
-        name: 'metricsfilter',
-        id: elemId,
-        hidden: 'true',
-      });
       const labelEl = this.dom.createChildOf(metricFilterEl, 'label', 'lh-metricfilter__label', {
         for: elemId,
         title: metric.result && metric.result.title,
       });
       labelEl.textContent = metric.acronym || metric.id;
+      const radioEl = this.dom.createChildOf(labelEl, 'input', 'lh-metricfilter__radio', {
+        type: 'radio',
+        name: 'metricsfilter',
+        id: elemId,
+        hidden: 'true',
+      });
+
       if (metric.acronym === 'All') {
         radioEl.checked = true;
+        labelEl.classList.add('lh-metricfilter__label--active');
       }
-      // Dynamically write some CSS, for the CSS-only filtering
-      labelSelectors.push(`.lh-metricfilter__radio#${elemId}:checked ~ .lh-metricfilter > .lh-metricfilter__label[for="${elemId}"]`); // eslint-disable-line max-len
-      if (metric.relevantAudits) {
-        /* Generate some CSS selectors like this:
-            #metric-CLS:checked ~ .lh-audit-group > #layout-shift-elements,
-            #metric-CLS:checked ~ .lh-audit-group > #non-composited-animations,
-            #metric-CLS:checked ~ .lh-audit-group > #unsized-images
-        */
-        auditSelectors.push(metric.relevantAudits.map(auditId => `#${elemId}:checked ~ .lh-audit-group > #${auditId}`).join(',\n')); // eslint-disable-line max-len
-      }
-    }
+      categoryEl.append(metricFilterEl);
 
-    const styleEl = this.dom.createChildOf(metricFilterEl, 'style');
-    // eslint-disable-next-line max-len
-    styleEl.textContent = `
-${labelSelectors.join(',\n')} {
-  background: var(--color-blue-A700);
-  color: var(--color-white);
-}
-/* If selecting non-All, hide all audits (and also the group header/description… */
-.lh-metricfilter__radio:checked:not(#metric-All) ~ .lh-audit-group .lh-audit,
-.lh-metricfilter__radio:checked:not(#metric-All) ~ .lh-audit-group .lh-audit-group__description,
-.lh-metricfilter__radio:checked:not(#metric-All) ~ .lh-audit-group .lh-audit-group__itemcount {
-  display: none;
-}
-/* …And then display:block the relevant ones */
-${auditSelectors.join(',\n')} {
-  display: block;
-}
-/*# sourceURL=metricfilter.css */
-    `;
-    categoryEl.append(metricFilterEl);
+      // Toggle class/hidden state based on filter choice.
+      radioEl.addEventListener('input', _ => {
+        for (const elem of categoryEl.querySelectorAll('label.lh-metricfilter__label')) {
+          elem.classList.toggle('lh-metricfilter__label--active', elem.htmlFor === elemId);
+        }
+        categoryEl.classList.toggle('lh-category--filtered', metric.acronym !== 'All');
+
+        for (const perfAuditEl of categoryEl.querySelectorAll('div.lh-audit')) {
+          if (metric.acronym === 'All') {
+            perfAuditEl.hidden = false;
+            continue;
+          }
+
+          perfAuditEl.hidden = true;
+          if (metric.relevantAudits && metric.relevantAudits.includes(perfAuditEl.id)) {
+            perfAuditEl.hidden = false;
+          }
+        }
+
+        // Hide groups/clumps if all child audits are also hidden.
+        const groupEls = categoryEl.querySelectorAll('div.lh-audit-group, details.lh-audit-group');
+        for (const groupEl of groupEls) {
+          groupEl.hidden = false;
+          const childEls = Array.from(groupEl.querySelectorAll('div.lh-audit'));
+          const areAllHidden = !!childEls.length && childEls.every(auditEl => auditEl.hidden);
+          groupEl.hidden = areAllHidden;
+        }
+      });
+    }
   }
 }
 
diff --git a/lighthouse-core/report/html/renderer/util.js b/lighthouse-core/report/html/renderer/util.js
index fe14255b9..aabc4c0af 100644
--- a/lighthouse-core/report/html/renderer/util.js
+++ b/lighthouse-core/report/html/renderer/util.js
@@ -102,11 +102,30 @@ class Util {
 
     // For convenience, smoosh all AuditResults into their auditRef (which has just weight & group)
     if (typeof clone.categories !== 'object') throw new Error('No categories provided.');
+
+    /** @type {Map<string, Array<LH.ReportResult.AuditRef>>} */
+    const relevantAuditToMetricsMap = new Map();
+
     for (const category of Object.values(clone.categories)) {
+      // Make basic lookup table for relevantAudits
+      category.auditRefs.forEach(metricRef => {
+        if (!metricRef.relevantAudits) return;
+        metricRef.relevantAudits.forEach(auditId => {
+          const arr = relevantAuditToMetricsMap.get(auditId) || [];
+          arr.push(metricRef);
+          relevantAuditToMetricsMap.set(auditId, arr);
+        });
+      });
+
       category.auditRefs.forEach(auditRef => {
         const result = clone.audits[auditRef.id];
         auditRef.result = result;
 
+        // Attach any relevantMetric auditRefs
+        if (relevantAuditToMetricsMap.has(auditRef.id)) {
+          auditRef.relevantMetrics = relevantAuditToMetricsMap.get(auditRef.id);
+        }
+
         // attach the stackpacks to the auditRef object
         if (clone.stackPacks) {
           clone.stackPacks.forEach(pack => {
@@ -520,6 +539,8 @@ Util.UIStrings = {
   varianceDisclaimer: 'Values are estimated and may vary. The [performance score is calculated](https://web.dev/performance-scoring/) directly from these metrics.',
   /** Text link pointing to an interactive calculator that explains Lighthouse scoring. The link text should be fairly short. */
   calculatorLink: 'See calculator.',
+  /** Label preceding a radio control for filtering the list of audits. The radio choices are various performance metrics (FCP, LCP, TBT), and if chosen, the audits in the report are hidden if they are not relevant to the selected metric. */
+  showRelevantAudits: 'Show audits relevant to:',
   /** Column heading label for the listing of opportunity audits. Each audit title represents an opportunity. There are only 2 columns, so no strict character limit.  */
   opportunityResourceColumnLabel: 'Opportunity',
   /** Column heading label for the estimated page load savings of opportunity audits. Estimated Savings is the total amount of time (in seconds) that Lighthouse computed could be reduced from the total page load time, if the suggested action is taken. There are only 2 columns, so no strict character limit. */
diff --git a/lighthouse-core/report/html/report-styles.css b/lighthouse-core/report/html/report-styles.css
index 7a7e1d1a8..b14342a83 100644
--- a/lighthouse-core/report/html/report-styles.css
+++ b/lighthouse-core/report/html/report-styles.css
@@ -522,6 +522,16 @@
 .lh-audit__stackpack {
   color: var(--report-text-color-secondary);
 }
+.lh-audit__adorn {
+  border: 1px solid slategray;
+  border-radius: 3px;
+  margin: 0 3px;
+  padding: 0 2px;
+  line-height: 1.1;
+  display: inline-block;
+  font-size: 90%;
+}
+
 .lh-category-header__description  {
   font-size: var(--report-font-size);
   text-align: center;
@@ -589,20 +599,28 @@
 .lh-metricfilter__label:first-of-type {
   border-top-left-radius: 5px;
   border-bottom-left-radius: 5px;
+  margin-left: 5px;
 }
 .lh-metricfilter__label:last-of-type {
   border-top-right-radius: 5px;
   border-bottom-right-radius: 5px;
 }
 
+.lh-metricfilter__label--active {
+  background: var(--color-blue-A700);
+  color: var(--color-white);
+}
 /* Give the 'All' choice a more muted display */
-.lh-metricfilter__radio#metric-All:checked ~ .lh-metricfilter > .lh-metricfilter__label[for="metric-All"] {
+.lh-metricfilter__label--active[for="metric-All"] {
   background-color: var(--color-blue-200) !important;
   color: black !important;
 }
 
+/* If audits are filtered, hide the itemcount for Passed Audits… */
+.lh-category--filtered .lh-audit-group .lh-audit-group__itemcount {
+  display: none;
+}
 
-/* checked styles are generated by JS */
 
 .lh-audit__header:hover {
   background-color: var(--color-hover);
diff --git a/proto/lighthouse-result.proto b/proto/lighthouse-result.proto
index 9cedf7f4b..cb022ef9b 100644
--- a/proto/lighthouse-result.proto
+++ b/proto/lighthouse-result.proto
@@ -494,6 +494,9 @@ message I18n {
 
     // Label for a button that opens the Treemap App
     string view_treemap_label = 47;
+
+    // Label preceding a radio control for filtering the list of audits. The radio choices are various performance metrics (FCP, LCP, TBT), and if chosen, the audits in the report are hidden if they are not relevant to the selected metric.
+    string show_relevant_audits = 48;
   }
 
   // The message holding all formatted strings
diff --git a/types/html-renderer.d.ts b/types/html-renderer.d.ts
index 4c4a11d79..daf4c75ed 100644
--- a/types/html-renderer.d.ts
+++ b/types/html-renderer.d.ts
@@ -65,6 +65,7 @@ declare global {
       export interface AuditRef extends Result.AuditRef {
         result: Audit.Result;
         stackPacks?: StackPackDescription[];
+        relevantMetrics?: LH.ReportResult.AuditRef[];
       }
 
       export interface StackPackDescription {


Test patch:
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index e3d936fae..aab9cd781 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -7073,6 +7073,7 @@
       "runtimeSettingsUANetwork": "User agent (network)",
       "runtimeSettingsUrl": "URL",
       "runtimeUnknown": "Unknown",
+      "showRelevantAudits": "Show audits relevant to:",
       "snippetCollapseButtonLabel": "Collapse snippet",
       "snippetExpandButtonLabel": "Expand snippet",
       "thirdPartyResourcesLabel": "Show 3rd-party resources",
diff --git a/third-party/chromium-synchronization/installability-errors-test.js b/third-party/chromium-synchronization/installability-errors-test.js
index 4bbc1a531..276f3399c 100644
--- a/third-party/chromium-synchronization/installability-errors-test.js
+++ b/third-party/chromium-synchronization/installability-errors-test.js
@@ -16,7 +16,7 @@ describe('installabilityErrors', () => {
 
   beforeAll(async () => {
     const installableLoggingGitTilesUrl =
-      'https://chromium.googlesource.com/chromium/src/+/master/components/webapps/installable/installable_logging.cc?format=TEXT';
+      'https://chromium.googlesource.com/chromium/src/+/master/components/webapps/browser/installable/installable_logging.cc?format=TEXT';
     const resp = await fetch(installableLoggingGitTilesUrl);
     if (!resp.ok) {
       throw new Error(`Chromium source fetch failed: ${resp.status} ${resp.statusText}`);