Repository: GoogleChrome/lighthouse
Pull request #6504 (2018-11-07 23:53:25)

report(pwa): give badges to groups with all passing audits
part of #6395

When all the audits in a PWA category group are passing, the icon goes from grayscale to color to indicate its badge (score gauge is still unstyled).


Patch:
diff --git a/lighthouse-core/config/default-config.js b/lighthouse-core/config/default-config.js
index 6fa2b3076..39ad7100f 100644
--- a/lighthouse-core/config/default-config.js
+++ b/lighthouse-core/config/default-config.js
@@ -372,7 +372,7 @@ const defaultConfig = {
         {id: 'is-on-https', weight: 2, group: 'pwa-installable'},
         {id: 'service-worker', weight: 1, group: 'pwa-installable'},
         {id: 'webapp-install-banner', weight: 2, group: 'pwa-installable'},
-        // Engaging
+        // PWA Optimized
         {id: 'redirects-http', weight: 2, group: 'pwa-optimized'},
         {id: 'splash-screen', weight: 1, group: 'pwa-optimized'},
         {id: 'themed-omnibox', weight: 1, group: 'pwa-optimized'},
diff --git a/lighthouse-core/report/html/renderer/pwa-category-renderer.js b/lighthouse-core/report/html/renderer/pwa-category-renderer.js
index 72d3a6fd5..57a5710b2 100644
--- a/lighthouse-core/report/html/renderer/pwa-category-renderer.js
+++ b/lighthouse-core/report/html/renderer/pwa-category-renderer.js
@@ -16,7 +16,7 @@
  */
 'use strict';
 
-/* globals self, CategoryRenderer */
+/* globals self, Util, CategoryRenderer */
 
 class PwaCategoryRenderer extends CategoryRenderer {
   /**
@@ -34,7 +34,7 @@ class PwaCategoryRenderer extends CategoryRenderer {
     // Regular audits aren't split up into pass/fail/not-applicable clumps, they're
     // all put in a top-level clump that isn't expandable/collapsable.
     const regularAuditRefs = auditRefs.filter(ref => ref.result.scoreDisplayMode !== 'manual');
-    const auditsElem = this.renderUnexpandableClump(regularAuditRefs, groupDefinitions);
+    const auditsElem = this._renderAudits(regularAuditRefs, groupDefinitions);
     categoryElem.appendChild(auditsElem);
 
     // Manual audits are still in a manual clump.
@@ -45,6 +45,45 @@ class PwaCategoryRenderer extends CategoryRenderer {
 
     return categoryElem;
   }
+
+  /**
+   * Returns the group IDs whose audits are all considered passing.
+   * @param {Array<LH.ReportResult.AuditRef>} auditRefs
+   * @return {Set<string>}
+   */
+  _getPassingGroupIds(auditRefs) {
+    const groupIds = auditRefs.map(ref => ref.group).filter(/** @return {g is string} */ g => !!g);
+    const uniqueGroupIds = new Set(groupIds);
+
+    // Remove any that have a failing audit.
+    for (const auditRef of auditRefs) {
+      if (!Util.showAsPassed(auditRef.result) && auditRef.group) {
+        uniqueGroupIds.delete(auditRef.group);
+      }
+    }
+
+    return uniqueGroupIds;
+  }
+
+  /**
+   * Render non-manual audits in groups, giving a badge to any group that has
+   * all passing audits.
+   * @param {Array<LH.ReportResult.AuditRef>} auditRefs
+   * @param {Object<string, LH.Result.ReportGroup>} groupDefinitions
+   * @return {Element}
+   */
+  _renderAudits(auditRefs, groupDefinitions) {
+    const auditsElem = this.renderUnexpandableClump(auditRefs, groupDefinitions);
+
+    // Add a 'badged' class to group if all audits in that group pass.
+    const passsingGroupIds = this._getPassingGroupIds(auditRefs);
+    for (const groupId of passsingGroupIds) {
+      const groupElem = this.dom.find(`.lh-audit-group--${groupId}`, auditsElem);
+      groupElem.classList.add('lh-badged');
+    }
+
+    return auditsElem;
+  }
 }
 
 if (typeof module !== 'undefined' && module.exports) {
diff --git a/lighthouse-core/report/html/report-styles.css b/lighthouse-core/report/html/report-styles.css
index eb3eddc61..c9723c168 100644
--- a/lighthouse-core/report/html/report-styles.css
+++ b/lighthouse-core/report/html/report-styles.css
@@ -97,6 +97,10 @@
   --pwa-fast-reliable-gray-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="nonzero"><circle fill="#DAE0E3" cx="12" cy="12" r="12"/><path d="M12.3 4l6.3 2.8V11c0 3.88-2.69 7.52-6.3 8.4C8.69 18.52 6 14.89 6 11V6.8L12.3 4zm-.56 12.88l3.3-5.79.04-.08c.05-.1.01-.29-.26-.29h-1.96l.56-3.92h-.56L9.6 12.52c0 .03.07-.12-.03.07-.11.2-.12.37.2.37h1.97l-.56 3.92h.56z" fill="#FFF"/></g></svg>');
   --pwa-installable-gray-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="nonzero"><circle fill="#DAE0E3" cx="12" cy="12" r="12"/><path d="M12 5a7 7 0 1 0 0 14 7 7 0 0 0 0-14zm3.5 7.7h-2.8v2.8h-1.4v-2.8H8.5v-1.4h2.8V8.5h1.4v2.8h2.8v1.4z" fill="#FFF"/></g></svg>');
   --pwa-optimized-gray-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><rect fill="#DAE0E3" width="24" height="24" rx="12"/><path fill="#FFF" d="M12 15.07l3.6 2.18-.95-4.1 3.18-2.76-4.2-.36L12 6.17l-1.64 3.86-4.2.36 3.2 2.76-.96 4.1z"/><path d="M5 5h14v14H5z"/></g></svg>');
+
+  --pwa-fast-reliable-color-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" fill="none"><circle fill="#CCE3F6" cx="12" cy="12" r="12"/><path d="M12 4.3l6.3 2.8v4.2c0 3.88-2.69 7.52-6.3 8.4-3.61-.88-6.3-4.51-6.3-8.4V7.1L12 4.3zm-.56 12.88l3.3-5.79.04-.08c.05-.1.01-.29-.26-.29h-1.96l.56-3.92h-.56L9.3 12.82c0 .03.07-.12-.03.07-.11.2-.12.37.2.37h1.97l-.56 3.92h.56z" fill="#304FFE"/></g></svg>');
+  --pwa-installable-color-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" fill="none"><circle fill="#D4ECD5" cx="12" cy="12" r="12"/><path d="M12 5a7 7 0 1 0 0 14 7 7 0 0 0 0-14zm3.5 7.7h-2.8v2.8h-1.4v-2.8H8.5v-1.4h2.8V8.5h1.4v2.8h2.8v1.4z" fill="#009688"/></g></svg>');
+  --pwa-optimized-color-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><rect fill="#FCE4EC" width="24" height="24" rx="12"/><path d="M5 5h14v14H5z"/><path fill="#EC3F7A" d="M12 15.07l3.6 2.18-.95-4.1 3.18-2.76-4.2-.36L12 6.17l-1.64 3.86-4.2.36 3.2 2.76-.96 4.1z"/></g></svg>');
 }
 
 .lh-vars.lh-devtools {
@@ -636,6 +640,15 @@ details, summary {
   background-image: var(--pwa-optimized-gray-url);
   background-size: var(--lh-group-icon-background-size);
 }
+.lh-audit-group--pwa-fast-reliable.lh-badged .lh-audit-group__header::before {
+  background-image: var(--pwa-fast-reliable-color-url);
+}
+.lh-audit-group--pwa-installable.lh-badged .lh-audit-group__header::before {
+  background-image: var(--pwa-installable-color-url);
+}
+.lh-audit-group--pwa-optimized.lh-badged .lh-audit-group__header::before {
+  background-image: var(--pwa-optimized-color-url);
+}
 
 /* Removing too much whitespace */
 .lh-audit-group--metrics {


Test patch:
diff --git a/lighthouse-core/test/report/html/renderer/pwa-category-renderer-test.js b/lighthouse-core/test/report/html/renderer/pwa-category-renderer-test.js
index 92500c311..11a12a0cd 100644
--- a/lighthouse-core/test/report/html/renderer/pwa-category-renderer-test.js
+++ b/lighthouse-core/test/report/html/renderer/pwa-category-renderer-test.js
@@ -39,7 +39,12 @@ describe('PwaCategoryRenderer', () => {
     pwaRenderer = new PwaCategoryRenderer(dom, detailsRenderer);
 
     sampleResults = Util.prepareReportResult(sampleResultsOrig);
-    category = sampleResults.reportCategories.find(cat => cat.id === 'pwa');
+  });
+
+  beforeEach(() => {
+    // Clone category to allow modifications.
+    const pwaCategory = sampleResults.reportCategories.find(cat => cat.id === 'pwa');
+    category = JSON.parse(JSON.stringify(pwaCategory));
   });
 
   afterAll(() => {
@@ -90,4 +95,82 @@ describe('PwaCategoryRenderer', () => {
         `trouble with selector '${selector}'`);
     });
   });
+
+  describe('badging groups', () => {
+    let auditRefs;
+    let groupIds;
+
+    beforeEach(() => {
+      auditRefs = category.auditRefs
+        .filter(audit => audit.result.scoreDisplayMode !== 'manual');
+
+      // Expect results to all be scorable.
+      for (const auditRef of auditRefs) {
+        assert.strictEqual(auditRef.result.scoreDisplayMode, 'binary');
+      }
+
+      groupIds = [...new Set(auditRefs.map(ref => ref.group))];
+    });
+
+    it('only gives a group a badge when all the group\'s audits are passing', () => {
+      for (const auditRef of auditRefs) {
+        auditRef.result.score = 0;
+      }
+
+      const targetGroupId = groupIds[2];
+      assert.ok(targetGroupId);
+      const targetAuditRefs = auditRefs.filter(ref => ref.group === targetGroupId);
+
+      // Try every permutation of audit scoring.
+      const totalPermutations = Math.pow(2, targetAuditRefs.length);
+      for (let i = 0; i < totalPermutations; i++) {
+        for (let j = 0; j < targetAuditRefs.length; j++) {
+          // Set as passing if jth bit in i is set.
+          targetAuditRefs[j].result.score = i >> j & 1;
+        }
+
+        const categoryElem = pwaRenderer.render(category, sampleResults.categoryGroups);
+        const badgedElems = categoryElem.querySelectorAll(`.lh-badged`);
+
+        // Only expect a badge on last permutation (all bits are set).
+        const expectedBadgeCount = i === totalPermutations - 1 ? 1 : 0;
+        assert.strictEqual(badgedElems.length, expectedBadgeCount);
+      }
+    });
+
+    it('renders all badges when all audits are passing', () => {
+      for (const auditRef of auditRefs) {
+        auditRef.result.score = 1;
+      }
+
+      const categoryElem = pwaRenderer.render(category, sampleResults.categoryGroups);
+      assert.strictEqual(categoryElem.querySelectorAll('.lh-badged').length, groupIds.length);
+    });
+
+    it('renders no badges when no audit groups are passing', () => {
+      for (const auditRef of auditRefs) {
+        auditRef.result.score = 0;
+      }
+
+      const categoryElem = pwaRenderer.render(category, sampleResults.categoryGroups);
+      assert.strictEqual(categoryElem.querySelectorAll('.lh-badged').length, 0);
+    });
+
+    it('renders all but one badge when all groups but one are passing', () => {
+      for (const auditRef of auditRefs) {
+        auditRef.result.score = 1;
+      }
+      auditRefs[0].result.score = 0;
+      const failingGroupId = auditRefs[0].group;
+
+      const categoryElem = pwaRenderer.render(category, sampleResults.categoryGroups);
+
+      for (const groupId of groupIds) {
+        const expectedCount = groupId === failingGroupId ? 0 : 1;
+
+        const groupElems = categoryElem.querySelectorAll(`.lh-audit-group--${groupId}.lh-badged`);
+        assert.strictEqual(groupElems.length, expectedCount);
+      }
+    });
+  });
 });
diff --git a/lighthouse-core/test/report/html/renderer/report-renderer-test.js b/lighthouse-core/test/report/html/renderer/report-renderer-test.js
index 87c072f2d..22c3fee12 100644
--- a/lighthouse-core/test/report/html/renderer/report-renderer-test.js
+++ b/lighthouse-core/test/report/html/renderer/report-renderer-test.js
@@ -16,9 +16,6 @@ const DOM = require('../../../../report/html/renderer/dom.js');
 const DetailsRenderer = require('../../../../report/html/renderer/details-renderer.js');
 const ReportUIFeatures = require('../../../../report/html/renderer/report-ui-features.js');
 const CategoryRenderer = require('../../../../report/html/renderer/category-renderer.js');
-// lazy loaded because they depend on CategoryRenderer to be available globally
-let PerformanceCategoryRenderer = null;
-let PwaCategoryRenderer = null;
 const CriticalRequestChainRenderer = require(
     '../../../../report/html/renderer/crc-details-renderer.js');
 const ReportRenderer = require('../../../../report/html/renderer/report-renderer.js');
@@ -39,16 +36,12 @@ describe('ReportRenderer', () => {
     global.CriticalRequestChainRenderer = CriticalRequestChainRenderer;
     global.DetailsRenderer = DetailsRenderer;
     global.CategoryRenderer = CategoryRenderer;
-    if (!PerformanceCategoryRenderer) {
-      PerformanceCategoryRenderer =
+
+    // lazy loaded because they depend on CategoryRenderer to be available globally
+    global.PerformanceCategoryRenderer =
         require('../../../../report/html/renderer/performance-category-renderer.js');
-    }
-    global.PerformanceCategoryRenderer = PerformanceCategoryRenderer;
-    if (!PwaCategoryRenderer) {
-      PwaCategoryRenderer =
+    global.PwaCategoryRenderer =
         require('../../../../report/html/renderer/pwa-category-renderer.js');
-    }
-    global.PwaCategoryRenderer = PwaCategoryRenderer;
 
     // Stub out matchMedia for Node.
     global.matchMedia = function() {
diff --git a/lighthouse-core/test/report/html/renderer/report-ui-features-test.js b/lighthouse-core/test/report/html/renderer/report-ui-features-test.js
index cea3db85e..1b3566fca 100644
--- a/lighthouse-core/test/report/html/renderer/report-ui-features-test.js
+++ b/lighthouse-core/test/report/html/renderer/report-ui-features-test.js
@@ -16,9 +16,6 @@ const DOM = require('../../../../report/html/renderer/dom.js');
 const DetailsRenderer = require('../../../../report/html/renderer/details-renderer.js');
 const ReportUIFeatures = require('../../../../report/html/renderer/report-ui-features.js');
 const CategoryRenderer = require('../../../../report/html/renderer/category-renderer.js');
-// lazy loaded because they depend on CategoryRenderer to be available globally
-let PerformanceCategoryRenderer = null;
-let PwaCategoryRenderer = null;
 const CriticalRequestChainRenderer = require(
     '../../../../report/html/renderer/crc-details-renderer.js');
 const ReportRenderer = require('../../../../report/html/renderer/report-renderer.js');
@@ -41,16 +38,12 @@ describe('ReportUIFeatures', () => {
     global.CriticalRequestChainRenderer = CriticalRequestChainRenderer;
     global.DetailsRenderer = DetailsRenderer;
     global.CategoryRenderer = CategoryRenderer;
-    if (!PerformanceCategoryRenderer) {
-      PerformanceCategoryRenderer =
+
+    // lazy loaded because they depend on CategoryRenderer to be available globally
+    global.PerformanceCategoryRenderer =
         require('../../../../report/html/renderer/performance-category-renderer.js');
-    }
-    global.PerformanceCategoryRenderer = PerformanceCategoryRenderer;
-    if (!PwaCategoryRenderer) {
-      PwaCategoryRenderer =
+    global.PwaCategoryRenderer =
         require('../../../../report/html/renderer/pwa-category-renderer.js');
-    }
-    global.PwaCategoryRenderer = PwaCategoryRenderer;
 
     // Stub out matchMedia for Node.
     global.matchMedia = function() {