Repository: GoogleChrome/lighthouse
Pull request #5734 (2018-07-26 23:43:24)

report(i18n): use LHR locale for toLocaleString
another piece of #5719, localize the numbers according to the locale specified in the LHR


note the 0,3 s

![image](https://user-images.githubusercontent.com/2301202/43294254-2fa57f38-90f3-11e8-8bdf-2ef4b3c696f2.png)


paulirish: Now that #5746 is in, we'll want a test here that confirms we use whatever locale `lookupLocale()` fell back to -- also here in .toLocaleString calls, rather then having diff locales for strings and numbers.

Patch:
diff --git a/lighthouse-core/audits/final-screenshot.js b/lighthouse-core/audits/final-screenshot.js
new file mode 100644
index 000000000..c59b7ee56
--- /dev/null
+++ b/lighthouse-core/audits/final-screenshot.js
@@ -0,0 +1,49 @@
+/**
+ * @license Copyright 2018 Google Inc. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+'use strict';
+
+const Audit = require('./audit');
+const LHError = require('../lib/errors');
+
+class FinalScreenshot extends Audit {
+  /**
+   * @return {LH.Audit.Meta}
+   */
+  static get meta() {
+    return {
+      id: 'final-screenshot',
+      scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
+      title: 'Final Screenshot',
+      description: 'The last screenshot captured of the pageload.',
+      requiredArtifacts: ['traces'],
+    };
+  }
+
+  /**
+   * @param {LH.Artifacts} artifacts
+   * @return {Promise<LH.Audit.Product>}
+   */
+  static async audit(artifacts) {
+    const trace = artifacts.traces[Audit.DEFAULT_PASS];
+    const screenshots = await artifacts.requestScreenshots(trace);
+    const finalScreenshot = screenshots[screenshots.length - 1];
+
+    if (!finalScreenshot) {
+      throw new LHError(LHError.errors.NO_SCREENSHOTS);
+    }
+
+    return {
+      rawValue: true,
+      details: {
+        type: 'screenshot',
+        timestamp: finalScreenshot.timestamp,
+        data: finalScreenshot.datauri,
+      },
+    };
+  }
+}
+
+module.exports = FinalScreenshot;
diff --git a/lighthouse-core/config/default-config.js b/lighthouse-core/config/default-config.js
index 1d234e80f..5521c28f5 100644
--- a/lighthouse-core/config/default-config.js
+++ b/lighthouse-core/config/default-config.js
@@ -105,6 +105,7 @@ module.exports = {
     'load-fast-enough-for-pwa',
     'metrics/speed-index',
     'screenshot-thumbnails',
+    'final-screenshot',
     'metrics/estimated-input-latency',
     'errors-in-console',
     'time-to-first-byte',
@@ -304,6 +305,7 @@ module.exports = {
         {id: 'user-timings', weight: 0, group: 'diagnostics'},
         {id: 'bootup-time', weight: 0, group: 'diagnostics'},
         {id: 'screenshot-thumbnails', weight: 0},
+        {id: 'final-screenshot', weight: 0},
         {id: 'mainthread-work-breakdown', weight: 0, group: 'diagnostics'},
         {id: 'font-display', weight: 0, group: 'diagnostics'},
       ],
diff --git a/lighthouse-core/gather/computed/screenshots.js b/lighthouse-core/gather/computed/screenshots.js
index 74ecaf0fa..625616a51 100644
--- a/lighthouse-core/gather/computed/screenshots.js
+++ b/lighthouse-core/gather/computed/screenshots.js
@@ -24,7 +24,7 @@ class ScreenshotFilmstrip extends ComputedArtifact {
       .map(evt => {
         return {
           timestamp: evt.ts / 1000,
-          datauri: `data:image/jpg;base64,${evt.args.snapshot}`,
+          datauri: `data:image/jpeg;base64,${evt.args.snapshot}`,
         };
       });
   }
diff --git a/lighthouse-core/lib/asset-saver.js b/lighthouse-core/lib/asset-saver.js
index 7fa75246e..d3ae67d07 100644
--- a/lighthouse-core/lib/asset-saver.js
+++ b/lighthouse-core/lib/asset-saver.js
@@ -172,7 +172,6 @@ async function prepareAssets(artifacts, audits) {
     const Runner = require('../runner.js');
     const computedArtifacts = Runner.instantiateComputedArtifacts();
     /** @type {Array<Screenshot>} */
-    // @ts-ignore TODO(bckenny): need typed computed artifacts
     const screenshots = await computedArtifacts.requestScreenshots(trace);
 
     const traceData = Object.assign({}, trace);
diff --git a/lighthouse-core/report/html/renderer/report-renderer.js b/lighthouse-core/report/html/renderer/report-renderer.js
index 053cafff0..c6011e3e2 100644
--- a/lighthouse-core/report/html/renderer/report-renderer.js
+++ b/lighthouse-core/report/html/renderer/report-renderer.js
@@ -47,7 +47,12 @@ class ReportRenderer {
     // If any mutations happen to the report within the renderers, we want the original object untouched
     const clone = /** @type {LH.ReportResult} */ (JSON.parse(JSON.stringify(report)));
     // Mutate the UIStrings if necessary (while saving originals)
-    const clonedStrings = JSON.parse(JSON.stringify(Util.UIStrings));
+    const originalUIStrings = JSON.parse(JSON.stringify(Util.UIStrings));
+    // If LHR is older (≤3.0.3), it has no locale setting. Set default.
+    if (!clone.configSettings.locale) {
+      clone.configSettings.locale = 'en-US';
+    }
+    Util.setNumberDateLocale(clone.configSettings.locale);
     if (clone.i18n && clone.i18n.rendererFormattedStrings) {
       ReportRenderer.updateAllUIStrings(clone.i18n.rendererFormattedStrings);
     }
@@ -61,7 +66,7 @@ class ReportRenderer {
     container.appendChild(this._renderReport(clone));
 
     // put the UIStrings back into original state
-    ReportRenderer.updateAllUIStrings(clonedStrings);
+    ReportRenderer.updateAllUIStrings(originalUIStrings);
 
     return /** @type {Element} **/ (container);
   }
diff --git a/lighthouse-core/report/html/renderer/util.js b/lighthouse-core/report/html/renderer/util.js
index cbe48870d..16f450d1a 100644
--- a/lighthouse-core/report/html/renderer/util.js
+++ b/lighthouse-core/report/html/renderer/util.js
@@ -137,7 +137,7 @@ class Util {
    */
   static formatNumber(number, granularity = 0.1) {
     const coarseValue = Math.round(number / granularity) * granularity;
-    return coarseValue.toLocaleString();
+    return coarseValue.toLocaleString(Util.numberDateLocale);
   }
 
   /**
@@ -146,7 +146,8 @@ class Util {
    * @return {string}
    */
   static formatBytesToKB(size, granularity = 0.1) {
-    const kbs = (Math.round(size / 1024 / granularity) * granularity).toLocaleString();
+    const kbs = (Math.round(size / 1024 / granularity) * granularity)
+      .toLocaleString(Util.numberDateLocale);
     return `${kbs}${NBSP}KB`;
   }
 
@@ -157,7 +158,7 @@ class Util {
    */
   static formatMilliseconds(ms, granularity = 10) {
     const coarseTime = Math.round(ms / granularity) * granularity;
-    return `${coarseTime.toLocaleString()}${NBSP}ms`;
+    return `${coarseTime.toLocaleString(Util.numberDateLocale)}${NBSP}ms`;
   }
 
   /**
@@ -167,7 +168,7 @@ class Util {
    */
   static formatSeconds(ms, granularity = 0.1) {
     const coarseTime = Math.round(ms / 1000 / granularity) * granularity;
-    return `${coarseTime.toLocaleString()}${NBSP}s`;
+    return `${coarseTime.toLocaleString(Util.numberDateLocale)}${NBSP}s`;
   }
 
   /**
@@ -180,14 +181,14 @@ class Util {
       month: 'short', day: 'numeric', year: 'numeric',
       hour: 'numeric', minute: 'numeric', timeZoneName: 'short',
     };
-    let formatter = new Intl.DateTimeFormat('en-US', options);
+    let formatter = new Intl.DateTimeFormat(Util.numberDateLocale, options);
 
     // Force UTC if runtime timezone could not be detected.
     // See https://github.com/GoogleChrome/lighthouse/issues/1056
     const tz = formatter.resolvedOptions().timeZone;
     if (!tz || tz.toLowerCase() === 'etc/unknown') {
       options.timeZone = 'UTC';
-      formatter = new Intl.DateTimeFormat('en-US', options);
+      formatter = new Intl.DateTimeFormat(Util.numberDateLocale, options);
     }
     return formatter.format(new Date(date));
   }
@@ -339,6 +340,18 @@ class Util {
     ];
   }
 
+  /**
+   * @param {LH.ReportResult} result
+   * @return {null|string}
+   */
+  static getFinalScreenshot(result) {
+    const category = result.reportCategories.find(cat => cat.id === 'performance');
+    if (!category) return null;
+    const auditRef = category.auditRefs.find(audit => audit.id === 'final-screenshot');
+    if (!auditRef || !auditRef.result || auditRef.result.scoreDisplayMode === 'error') return null;
+    return auditRef.result.details.data;
+  }
+
   /**
    * @param {LH.Config.Settings} settings
    * @return {{deviceEmulation: string, networkThrottling: string, cpuThrottling: string, summary: string}}
@@ -387,8 +400,26 @@ class Util {
       summary: `${deviceEmulation}, ${summary}`,
     };
   }
+
+  /**
+   * Set the locale to be used for Util's number and date formatting functions.
+   * @param {LH.Locale} locale
+   */
+  static setNumberDateLocale(locale) {
+    Util.numberDateLocale = locale;
+
+    // When testing, use a locale with more exciting numeric formatting
+    // @ts-ignore - TODO: until `de-DE` is in LH.Locale
+    if (Util.numberDateLocale === 'en-XA') Util.numberDateLocale = 'de-DE';
+  }
 }
 
+/**
+ * This value is updated on each run to the locale of the report
+ * @type {LH.Locale}
+ */
+Util.numberDateLocale = 'en-US';
+
 Util.UIStrings = {
   /** Disclaimer shown to users below the metric values (First Contentful Paint, Time to Interactive, etc) to warn them that the numbers they see will likely change slightly the next time they run Lighthouse. */
   varianceDisclaimer: 'Values are estimated and may vary.',


Test patch:
diff --git a/lighthouse-core/test/audits/final-screenshot-test.js b/lighthouse-core/test/audits/final-screenshot-test.js
new file mode 100644
index 000000000..86ed9d564
--- /dev/null
+++ b/lighthouse-core/test/audits/final-screenshot-test.js
@@ -0,0 +1,33 @@
+/**
+ * @license Copyright 2018 Google Inc. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+'use strict';
+
+const assert = require('assert');
+
+const Runner = require('../../runner.js');
+const FinalScreenshotAudit = require('../../audits/final-screenshot');
+const pwaTrace = require('../fixtures/traces/progressive-app-m60.json');
+
+/* eslint-env jest */
+
+describe('Final screenshot', () => {
+  let computedArtifacts;
+
+  beforeAll(() => {
+    computedArtifacts = Runner.instantiateComputedArtifacts();
+  });
+
+  it('should extract a final screenshot from a trace', async () => {
+    const artifacts = Object.assign({
+      traces: {defaultPass: pwaTrace},
+    }, computedArtifacts);
+    const results = await FinalScreenshotAudit.audit(artifacts);
+
+    assert.ok(results.rawValue);
+    assert.equal(results.details.timestamp, 225414990.064);
+    assert.ok(results.details.data.startsWith('data:image/jpeg;base64,/9j/4AAQSkZJRgABA'));
+  });
+});
diff --git a/lighthouse-core/test/gather/computed/screenshots-test.js b/lighthouse-core/test/gather/computed/screenshots-test.js
index 962ef3014..4c7ddf123 100644
--- a/lighthouse-core/test/gather/computed/screenshots-test.js
+++ b/lighthouse-core/test/gather/computed/screenshots-test.js
@@ -26,7 +26,7 @@ describe('Screenshot gatherer', () => {
       assert.equal(screenshots.length, 7);
 
       const firstScreenshot = screenshots[0];
-      assert.ok(firstScreenshot.datauri.startsWith('data:image/jpg;base64,'));
+      assert.ok(firstScreenshot.datauri.startsWith('data:image/jpeg;base64,'));
       assert.ok(firstScreenshot.datauri.length > 42);
     });
   });
diff --git a/lighthouse-core/test/report/html/renderer/category-renderer-test.js b/lighthouse-core/test/report/html/renderer/category-renderer-test.js
index ff0bd7244..a420f4ee7 100644
--- a/lighthouse-core/test/report/html/renderer/category-renderer-test.js
+++ b/lighthouse-core/test/report/html/renderer/category-renderer-test.js
@@ -147,10 +147,6 @@ describe('CategoryRenderer', () => {
     assert.ok(categoryDOM.querySelector('.lh-audit-group--manual .lh-audit-group__summary'));
     assert.equal(categoryDOM.querySelectorAll('.lh-audit--manual').length, 3,
         'score shows informative and dash icon');
-
-    const perfCategory = sampleResults.reportCategories.find(cat => cat.id === 'performance');
-    const categoryDOM2 = renderer.render(perfCategory, sampleResults.categoryGroups);
-    assert.ok(!categoryDOM2.querySelector('.lh-audit-group--manual'));
   });
 
   it('renders not applicable audits if the category contains them', () => {
diff --git a/lighthouse-core/test/report/html/renderer/performance-category-renderer-test.js b/lighthouse-core/test/report/html/renderer/performance-category-renderer-test.js
index d77645672..c02df8362 100644
--- a/lighthouse-core/test/report/html/renderer/performance-category-renderer-test.js
+++ b/lighthouse-core/test/report/html/renderer/performance-category-renderer-test.js
@@ -157,6 +157,10 @@ describe('PerfCategoryRenderer', () => {
     assert.equal(passedElements.length, passedAudits.length);
   });
 
+  // Unsupported by perf cat renderer right now.
+  it.skip('renders any manual audits', () => {
+  });
+
   describe('getWastedMs', () => {
     it('handles erroring opportunities', () => {
       const auditWithDebug = {
diff --git a/lighthouse-core/test/report/html/renderer/util-test.js b/lighthouse-core/test/report/html/renderer/util-test.js
index 11ec3de9e..2937c2771 100644
--- a/lighthouse-core/test/report/html/renderer/util-test.js
+++ b/lighthouse-core/test/report/html/renderer/util-test.js
@@ -7,6 +7,10 @@
 
 const assert = require('assert');
 const Util = require('../../../../report/html/renderer/util.js');
+
+const ReportRenderer = require('../../../../report/html/renderer/report-renderer.js');
+const sampleResults = require('../../../results/sample_v2.json');
+
 const NBSP = '\xa0';
 
 /* eslint-env jest */
@@ -58,6 +62,27 @@ describe('util helpers', () => {
     assert.equal(Util.formatDuration(28 * 60 * 60 * 1000 + 5000), `1${NBSP}d 4${NBSP}h 5${NBSP}s`);
   });
 
+  // TODO: need ICU support in node on Travis/Appveyor
+  it.skip('formats based on locale', () => {
+    const number = 12346.858558;
+
+    const originalLocale = Util.numberDateLocale;
+    Util.setNumberDateLocale('de-DE');
+    assert.strictEqual(Util.formatNumber(number), '12.346,9');
+    Util.setNumberDateLocale(originalLocale); // reset
+    assert.strictEqual(Util.formatNumber(number), '12,346.9');
+  });
+
+  it.skip('uses decimal comma with en-XA test locale', () => {
+    const number = 12346.858558;
+
+    const originalLocale = Util.numberDateLocale;
+    Util.setNumberDateLocale('en-XA');
+    assert.strictEqual(Util.formatNumber(number), '12.346,9');
+    Util.setNumberDateLocale(originalLocale); // reset
+    assert.strictEqual(Util.formatNumber(number), '12,346.9');
+  });
+
   it('calculates a score ratings', () => {
     assert.equal(Util.calculateRating(0.0), 'fail');
     assert.equal(Util.calculateRating(0.10), 'fail');
@@ -142,4 +167,37 @@ describe('util helpers', () => {
     Util.formatDisplayValue(displayValue);
     assert.deepStrictEqual(displayValue, cloned, 'displayValue was mutated');
   });
+
+  describe('getFinalScreenshot', () => {
+    const cloneResults = JSON.parse(JSON.stringify(sampleResults));
+    cloneResults.reportCategories = Object.values(cloneResults.categories);
+    ReportRenderer.smooshAuditResultsIntoCategories(cloneResults.audits,
+      cloneResults.reportCategories);
+
+    it('gets a datauri as a string', () => {
+      const datauri = Util.getFinalScreenshot(cloneResults);
+      assert.equal(typeof datauri, 'string');
+      assert.ok(datauri.startsWith('data:image/jpeg;base64,'));
+    });
+
+    it('returns null if there is no perf category', () => {
+      const lhrWithoutPerf = JSON.parse(JSON.stringify(sampleResults));
+      delete lhrWithoutPerf.categories.performance;
+      lhrWithoutPerf.reportCategories = Object.values(lhrWithoutPerf.categories);
+
+      const datauri = Util.getFinalScreenshot(lhrWithoutPerf);
+      assert.equal(datauri, null);
+    });
+
+    it('returns null if there is no final-screenshot audit', () => {
+      const lhrNoFinalSS = JSON.parse(JSON.stringify(sampleResults));
+      delete lhrNoFinalSS.audits['final-screenshot'];
+      lhrNoFinalSS.reportCategories = Object.values(lhrNoFinalSS.categories);
+      ReportRenderer.smooshAuditResultsIntoCategories(lhrNoFinalSS.audits,
+        lhrNoFinalSS.reportCategories);
+
+      const datauri = Util.getFinalScreenshot(lhrNoFinalSS);
+      assert.equal(datauri, null);
+    });
+  });
 });
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index 2259651ed..7d695dc79 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -171,6 +171,19 @@
         ]
       }
     },
+    "final-screenshot": {
+      "id": "final-screenshot",
+      "title": "Final Screenshot",
+      "description": "The last screenshot captured of the pageload.",
+      "score": null,
+      "scoreDisplayMode": "informative",
+      "rawValue": true,
+      "details": {
+        "type": "screenshot",
+        "timestamp": 185608111.383,
+        "data": "data:image/jpeg;base64,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"
+      }
+    },
     "estimated-input-latency": {
       "id": "estimated-input-latency",
       "title": "Estimated Input Latency",
@@ -2869,6 +2882,10 @@
           "id": "screenshot-thumbnails",
           "weight": 0
         },
+        {
+          "id": "final-screenshot",
+          "weight": 0
+        },
         {
           "id": "mainthread-work-breakdown",
           "weight": 0,