Repository: GoogleChrome/lighthouse
Pull request #8590 (2019-04-24 19:13:48)

report(redesign): adjust score scale to match new design
#8185


Patch:
diff --git a/.travis.yml b/.travis.yml
index a69b1aa6d..eb9afebd5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -26,6 +26,7 @@ before_script:
   # see comment above about puppeteer
   - export CHROME_PATH="$(which google-chrome-stable)"
   - sh -e /etc/init.d/xvfb start
+  - google-chrome-stable --version
   - yarn build-all
 script:
   - yarn bundlesize
diff --git a/lighthouse-core/gather/connections/cri.js b/lighthouse-core/gather/connections/cri.js
index 8b47e11b9..8fae2b3c4 100644
--- a/lighthouse-core/gather/connections/cri.js
+++ b/lighthouse-core/gather/connections/cri.js
@@ -108,21 +108,17 @@ class CriConnection extends Connection {
         });
       });
 
-      request.setTimeout(CONNECT_TIMEOUT, () => {
-        request.abort();
-
-        // After aborting, we expect an ECONNRESET error. Ignore.
-        request.on('error', err => {
-          // @ts-ignore - not in @types yet. See https://nodejs.org/api/errors.html#errors_class_systemerror.
-          if (err.code !== 'ECONNRESET') {
-            throw err;
-          }
-        });
+      // This error handler is critical to ensuring Lighthouse exits cleanly even when Chrome crashes.
+      // See https://github.com/GoogleChrome/lighthouse/pull/8583.
+      request.on('error', reject);
 
+      request.setTimeout(CONNECT_TIMEOUT, () => {
         // Reject on error with code specifically indicating timeout in connection setup.
         const err = new LighthouseError(LighthouseError.errors.CRI_TIMEOUT);
         log.error('CriConnection', err.friendlyMessage);
         reject(err);
+
+        request.abort();
       });
     });
   }
diff --git a/lighthouse-core/lib/i18n/en-US.json b/lighthouse-core/lib/i18n/en-US.json
index edda6fb8d..3c37fd85c 100644
--- a/lighthouse-core/lib/i18n/en-US.json
+++ b/lighthouse-core/lib/i18n/en-US.json
@@ -1260,8 +1260,8 @@
     "description": "Error message explaining that Lighthouse couldn't complete because the page has stopped responding to its instructions."
   },
   "lighthouse-core/lib/lh-error.js | pageLoadFailedInsecure": {
-    "message": "The URL you have provided does not have valid security credentials. {securityMessages}",
-    "description": "Error message explaining that the credentials included in the Lighthouse run were invalid, so the URL cannot be accessed. securityMessages will be replaced with one or more strings from the browser explaining what was insecure about the page load."
+    "message": "The URL you have provided does not have a valid security certificate. {securityMessages}",
+    "description": "Error message explaining that the security certificate of the page Lighthouse observed was invalid, so the URL cannot be accessed. securityMessages will be replaced with one or more strings from the browser explaining what was insecure about the page load."
   },
   "lighthouse-core/lib/lh-error.js | pageLoadFailedWithDetails": {
     "message": "Lighthouse was unable to reliably load the page you requested. Make sure you are testing the correct URL and that the server is properly responding to all requests. (Details: {errorDetails})",
diff --git a/lighthouse-core/lib/lh-error.js b/lighthouse-core/lib/lh-error.js
index a349667fb..fdbf06d7a 100644
--- a/lighthouse-core/lib/lh-error.js
+++ b/lighthouse-core/lib/lh-error.js
@@ -21,8 +21,8 @@ const UIStrings = {
   pageLoadFailedWithStatusCode: 'Lighthouse was unable to reliably load the page you requested. Make sure you are testing the correct URL and that the server is properly responding to all requests. (Status code: {statusCode})',
   /** Error message explaining that Lighthouse could not load the requested URL and the steps that might be taken to fix the unreliability. */
   pageLoadFailedWithDetails: 'Lighthouse was unable to reliably load the page you requested. Make sure you are testing the correct URL and that the server is properly responding to all requests. (Details: {errorDetails})',
-  /** Error message explaining that the credentials included in the Lighthouse run were invalid, so the URL cannot be accessed. securityMessages will be replaced with one or more strings from the browser explaining what was insecure about the page load. */
-  pageLoadFailedInsecure: 'The URL you have provided does not have valid security credentials. {securityMessages}',
+  /** Error message explaining that the security certificate of the page Lighthouse observed was invalid, so the URL cannot be accessed. securityMessages will be replaced with one or more strings from the browser explaining what was insecure about the page load. */
+  pageLoadFailedInsecure: 'The URL you have provided does not have a valid security certificate. {securityMessages}',
   /** Error message explaining that Chrome has encountered an error during the Lighthouse run, and that Chrome should be restarted. */
   internalChromeError: 'An internal Chrome error occurred. Please restart Chrome and try re-running Lighthouse.',
   /** Error message explaining that fetching the resources of the webpage has taken longer than the maximum time. */
diff --git a/lighthouse-core/report/html/renderer/report-renderer.js b/lighthouse-core/report/html/renderer/report-renderer.js
index 42d8b7466..8b5dedf66 100644
--- a/lighthouse-core/report/html/renderer/report-renderer.js
+++ b/lighthouse-core/report/html/renderer/report-renderer.js
@@ -157,6 +157,39 @@ class ReportRenderer {
     return container;
   }
 
+  /**
+   * @param {LH.ReportResult} report
+   * @param {CategoryRenderer} categoryRenderer
+   * @param {Record<string, CategoryRenderer>} specificCategoryRenderers
+   * @return {DocumentFragment[]}
+   */
+  _renderScoreGauges(report, categoryRenderer, specificCategoryRenderers) {
+    // Group gauges in this order: default, pwa, plugins.
+    const defaultGauges = [];
+    const customGauges = []; // PWA.
+    const pluginGauges = [];
+
+    for (const category of report.reportCategories) {
+      const renderer = specificCategoryRenderers[category.id] || categoryRenderer;
+      const categoryGauge = renderer.renderScoreGauge(category, report.categoryGroups || {});
+
+      if (Util.isPluginCategory(category.id)) {
+        pluginGauges.push(categoryGauge);
+      } else if (renderer.renderScoreGauge === categoryRenderer.renderScoreGauge) {
+        // The renderer for default categories is just the default CategoryRenderer.
+        // If the functions are equal, then renderer is an instance of CategoryRenderer.
+        // For example, the PWA category uses PwaCategoryRenderer, which overrides
+        // CategoryRenderer.renderScoreGauge, so it would fail this check and be placed
+        // in the customGauges bucket.
+        defaultGauges.push(categoryGauge);
+      } else {
+        customGauges.push(categoryGauge);
+      }
+    }
+
+    return [...defaultGauges, ...customGauges, ...pluginGauges];
+  }
+
   /**
    * @param {LH.ReportResult} report
    * @return {DocumentFragment}
@@ -223,29 +256,9 @@ class ReportRenderer {
     // }
 
     if (scoreHeader) {
-      // Group gauges in this order: default, pwa, plugins.
-      const defaultGauges = [];
-      const customGauges = []; // PWA.
-      const pluginGauges = [];
-      for (const category of report.reportCategories) {
-        const renderer = specificCategoryRenderers[category.id] || categoryRenderer;
-        const categoryGauge = renderer.renderScoreGauge(category, report.categoryGroups || {});
-
-        if (Util.isPluginCategory(category.id)) {
-          pluginGauges.push(categoryGauge);
-        } else if (renderer.renderScoreGauge === categoryRenderer.renderScoreGauge) {
-          // The renderer for default categories is just the default CategoryRenderer.
-          // If the functions are equal, then renderer is an instance of CategoryRenderer.
-          // For example, the PWA category uses PwaCategoryRenderer, which overrides
-          // CategoryRenderer.renderScoreGauge, so it would fail this check and be placed
-          // in the customGauges bucket.
-          defaultGauges.push(categoryGauge);
-        } else {
-          customGauges.push(categoryGauge);
-        }
-      }
-      scoreHeader.append(...defaultGauges, ...customGauges, ...pluginGauges);
-
+      const scoreGauges =
+        this._renderScoreGauges(report, categoryRenderer, specificCategoryRenderers);
+      scoreHeader.append(...scoreGauges);
       const scoreScale = this._dom.cloneTemplate('#tmpl-lh-scorescale', this._templateContext);
       const scoresContainer = this._dom.find('.lh-scores-container', headerContainer);
       scoresContainer.appendChild(scoreHeader);
@@ -255,10 +268,23 @@ class ReportRenderer {
     reportSection.appendChild(this._renderReportFooter(report));
 
     const reportFragment = this._dom.createFragment();
+
     if (!this._dom.isDevTools()) {
       const topbarDocumentFragment = this._renderReportTopbar(report);
       reportFragment.appendChild(topbarDocumentFragment);
     }
+
+    if (scoreHeader && !this._dom.isDevTools()) {
+      const stickyHeader = this._dom.createElement('div', 'lh-sticky-header');
+      this._dom.createChildOf(stickyHeader, 'div', 'lh-highlighter');
+
+      const scoreGauges =
+        this._renderScoreGauges(report, categoryRenderer, specificCategoryRenderers);
+      stickyHeader.append(...scoreGauges);
+
+      reportFragment.appendChild(stickyHeader);
+    }
+
     reportFragment.appendChild(headerContainer);
     reportFragment.appendChild(container);
 
diff --git a/lighthouse-core/report/html/renderer/report-ui-features.js b/lighthouse-core/report/html/renderer/report-ui-features.js
index 4d8bdd6a4..62e5e2e8d 100644
--- a/lighthouse-core/report/html/renderer/report-ui-features.js
+++ b/lighthouse-core/report/html/renderer/report-ui-features.js
@@ -40,6 +40,14 @@ class ReportUIFeatures {
     this._copyAttempt = false;
     /** @type {HTMLElement} */
     this.exportButton; // eslint-disable-line no-unused-expressions
+    /** @type {HTMLElement} */
+    this.topbarEl; // eslint-disable-line no-unused-expressions
+    /** @type {HTMLElement} */
+    this.scoreScaleEl; // eslint-disable-line no-unused-expressions
+    /** @type {HTMLElement} */
+    this.stickyHeaderEl; // eslint-disable-line no-unused-expressions
+    /** @type {HTMLElement} */
+    this.highlightEl; // eslint-disable-line no-unused-expressions
 
     this.onMediaQueryChange = this.onMediaQueryChange.bind(this);
     this.onCopy = this.onCopy.bind(this);
@@ -48,6 +56,7 @@ class ReportUIFeatures {
     this.onKeyDown = this.onKeyDown.bind(this);
     this.printShortCutDetect = this.printShortCutDetect.bind(this);
     this.onChevronClick = this.onChevronClick.bind(this);
+    this._updateStickyHeaderOnScroll = this._updateStickyHeaderOnScroll.bind(this);
   }
 
   /**
@@ -61,10 +70,13 @@ class ReportUIFeatures {
     this.json = report;
     this._setupMediaQueryListeners();
     this._setupExportButton();
+    this._setupStickyHeaderElements();
     this._setUpCollapseDetailsAfterPrinting();
     this._resetUIState();
     this._document.addEventListener('keydown', this.printShortCutDetect);
     this._document.addEventListener('copy', this.onCopy);
+    this._document.addEventListener('scroll', this._updateStickyHeaderOnScroll);
+    window.addEventListener('resize', this._updateStickyHeaderOnScroll);
   }
 
   /**
@@ -102,6 +114,13 @@ class ReportUIFeatures {
     dropdown.addEventListener('click', this.onExport);
   }
 
+  _setupStickyHeaderElements() {
+    this.topbarEl = this._dom.find('.lh-topbar', this._document);
+    this.scoreScaleEl = this._dom.find('.lh-scorescale', this._document);
+    this.stickyHeaderEl = this._dom.find('.lh-sticky-header', this._document);
+    this.highlightEl = this._dom.find('.lh-highlighter', this._document);
+  }
+
   /**
    * Handle copy events.
    * @param {ClipboardEvent} e
@@ -380,6 +399,30 @@ class ReportUIFeatures {
     this._document.body.removeChild(a);
     setTimeout(_ => URL.revokeObjectURL(href), 500);
   }
+
+  _updateStickyHeaderOnScroll() {
+    // Show sticky header when the score scale begins to go underneath the topbar.
+    const topbarBottom = this.topbarEl.getBoundingClientRect().bottom;
+    const scoreScaleTop = this.scoreScaleEl.getBoundingClientRect().top;
+    const showStickyHeader = topbarBottom >= scoreScaleTop;
+
+    // Highlight mini gauge when section is in view.
+    // In view = the last category that starts above the middle of the window.
+    const categoryEls = Array.from(this._document.querySelectorAll('.lh-category'));
+    const categoriesAboveTheMiddle =
+      categoryEls.filter(el => el.getBoundingClientRect().top - window.innerHeight / 2 < 0);
+    const highlightIndex =
+      categoriesAboveTheMiddle.length > 0 ? categoriesAboveTheMiddle.length - 1 : 0;
+
+    // Category order matches gauge order in sticky header.
+    const gaugeWrapperEls = this.stickyHeaderEl.querySelectorAll('.lh-gauge__wrapper');
+    const gaugeToHighlight = gaugeWrapperEls[highlightIndex];
+    const offset = gaugeToHighlight.getBoundingClientRect().left + 'px';
+
+    // Mutate at end to avoid layout thrashing.
+    this.stickyHeaderEl.classList.toggle('lh-sticky-header--visible', showStickyHeader);
+    this.highlightEl.style.left = offset;
+  }
 }
 
 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 9130bab3f..0a5a5d345 100644
--- a/lighthouse-core/report/html/report-styles.css
+++ b/lighthouse-core/report/html/report-styles.css
@@ -118,6 +118,8 @@
   --score-title-font-size: 20px;
   --score-title-line-height-big: 36px;
   --score-title-line-height: 26px;
+  --scorescale-height: 6px;
+  --scorescale-width: 18px;
   --section-padding: 40px;
   --topbar-bg: var(--color-black-100);
   --topbar-height: 36px;
@@ -130,6 +132,8 @@
   --color-fail: var(--color-red);
   --color-pass-secondary: var(--color-green-700);
   --color-pass: var(--color-green);
+  --color-sticky-header-bg: var(--color-white);
+  --color-highlighter-bg: var(--color-black);
 
   --plugin-icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24" fill="%23757575"><path d="M0 0h24v24H0z" fill="none"/><path d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z"/></svg>');
   --logo-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192"><g fill="none" fill-rule="evenodd"><path d="M0 0h192v192H0z"/><path d="M67.705 179.352l2.603-20.82 49.335-16.39 4.652 37.21A87.893 87.893 0 0 1 96 184a87.893 87.893 0 0 1-28.295-4.648zM52.44 172.48C25.894 157.328 8 128.754 8 96 8 47.399 47.399 8 96 8s88 39.399 88 88c0 32.754-17.894 61.328-44.44 76.48L130 96h6V80h-8V48L96 28 64 48v32h-8v16h6l-9.56 76.48zM113.875 96l2.882 23.05-43.318 14.433L78.125 96h35.75zM80 80V56.868l16-10 16 10V80H80z" fill="%23000000"/></g></svg>');
@@ -835,7 +839,9 @@
 }
 
 .lh-scores-header .lh-gauge__wrapper,
-.lh-scores-header .lh-gauge--pwa__wrapper {
+.lh-scores-header .lh-gauge--pwa__wrapper,
+.lh-sticky-header .lh-gauge__wrapper,
+.lh-sticky-header .lh-gauge--pwa__wrapper {
   width: var(--score-container-width);
 }
 
@@ -853,6 +859,8 @@
 }
 
 .lh-scorescale-range {
+  display: flex;
+  align-items: center;
   margin: 0 12px;
   font-family: var(--monospace-font-family);
   white-space: nowrap;
@@ -860,11 +868,11 @@
 
 .lh-scorescale-range::before {
   content: '';
-  width: var(--body-font-size);
-  height: calc(var(--body-font-size) * .50);
+  width: var(--scorescale-width);
+  height: var(--scorescale-height);
   border-radius: 10px;
-  display: inline-block;
-  margin: 0 5px;
+  display: block;
+  margin-right: 10px;
 }
 
 .lh-scorescale-range--pass::before {
diff --git a/lighthouse-core/report/html/templates.html b/lighthouse-core/report/html/templates.html
index 1b16ab052..dbe7fb8fc 100644
--- a/lighthouse-core/report/html/templates.html
+++ b/lighthouse-core/report/html/templates.html
@@ -26,9 +26,9 @@ limitations under the License.
 <!-- Lighthouse score scale -->
 <template id="tmpl-lh-scorescale">
   <div class="lh-scorescale">
-      <span class="lh-scorescale-range lh-scorescale-range--fail">0-49</span>
-      <span class="lh-scorescale-range lh-scorescale-range--average">50-89</span>
-      <span class="lh-scorescale-range lh-scorescale-range--pass">90-100</span>
+      <span class="lh-scorescale-range lh-scorescale-range--fail">0&ndash;49</span>
+      <span class="lh-scorescale-range lh-scorescale-range--average">50&ndash;89</span>
+      <span class="lh-scorescale-range lh-scorescale-range--pass">90&ndash;100</span>
   </div>
 </template>
 
@@ -144,10 +144,49 @@ limitations under the License.
       position: relative;
       width: 100%;
     }
+
+    .lh-sticky-header {
+      --gauge-circle-size: 36px;
+      --plugin-badge-size: 18px;
+      --plugin-icon-size: 75%;
+      --score-container-width: 60px;
+      --score-number-font-size: 13px;
+      position: fixed;
+      left: 0;
+      right: 0;
+      top: var(--topbar-height);
+      font-weight: 700;
+      display: flex;
+      justify-content: center;
+      background-color: var(--color-sticky-header-bg);
+      border-bottom: 1px solid var(--color-black-200);
+      padding-top: var(--score-container-padding);
+      padding-bottom: 4px;
+      z-index: 1000;
+      pointer-events: none;
+      opacity: 0;
+    }
+
+    .lh-sticky-header--visible {
+      pointer-events: auto;
+      opacity: 1;
+    }
+
+    .lh-sticky-header .lh-gauge__label {
+      display: none;
+    }
+
+    .lh-highlighter {
+      width: var(--score-container-width);
+      height: 1px;
+      background: var(--color-highlighter-bg);
+      position: absolute;
+      bottom: -1px;
+      left: 0;
+    }
   </style>
   <div class="lh-scores-wrapper">
-    <div class="lh-scores-container">
-    </div>
+    <div class="lh-scores-container"></div>
   </div>
 </template>
 
@@ -513,7 +552,7 @@ limitations under the License.
       content: "";
       position: absolute;
       right: -6px;
-      bottom: -2px;
+      bottom: 5px;
       display: block;
       z-index: 100;
       box-shadow: 0 0 4px rgba(0,0,0,.2);


Test patch:
diff --git a/lighthouse-core/test/gather/driver-test.js b/lighthouse-core/test/gather/driver-test.js
index e3afa14fa..cdc961b2a 100644
--- a/lighthouse-core/test/gather/driver-test.js
+++ b/lighthouse-core/test/gather/driver-test.js
@@ -772,7 +772,8 @@ describe('.gotoURL', () => {
       } catch (err) {
         expect(err).toHaveProperty('code', 'INSECURE_DOCUMENT_REQUEST');
         expect(err.friendlyMessage).toBeDisplayString(
-          'The URL you have provided does not have valid security credentials. reason 1. reason 2.'
+          'The URL you have provided does not have a valid security certificate. ' +
+          'reason 1. reason 2.'
         );
       }
     });
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 f788bebba..155fd19d3 100644
--- a/lighthouse-core/test/report/html/renderer/report-renderer-test.js
+++ b/lighthouse-core/test/report/html/renderer/report-renderer-test.js
@@ -79,8 +79,9 @@ describe('ReportRenderer', () => {
       const output = renderer.renderReport(sampleResults, container);
       assert.ok(output.querySelector('.lh-header-sticky'), 'has a header');
       assert.ok(output.querySelector('.lh-report'), 'has report body');
+      // 3 sets of gauges - one in sticky header, one in scores header, and one in each section.
       assert.equal(output.querySelectorAll('.lh-gauge__wrapper, .lh-gauge--pwa__wrapper').length,
-          sampleResults.reportCategories.length * 2, 'renders category gauges');
+          sampleResults.reportCategories.length * 3, 'renders category gauges');
       // no fireworks
       assert.ok(output.querySelector('.score100') === null, 'has no fireworks treatment');
     });
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 b9d4b8d40..4db93baeb 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
@@ -60,7 +60,7 @@ describe('ReportUIFeatures', () => {
       };
     };
 
-    global.window = {};
+    global.window = document.window;
     global.window.getComputedStyle = function() {
       return {
         marginTop: '10px',