Repository: GoogleChrome/lighthouse (js)

Issue:
Always display decimal places in JS library size comparison
**Feature request summary**
 
Currently in the 'Large JavaScript libraries' view, the 'Transfer size' column values omit the decimal place for round sizes. i.e. `18 KiB` is displayed instead of `18.0 KiB`.

This ticket proposes to always display the digit after the decimal place, even if it is `0`.

I am happy to make the necessarily changes if the feature is agreeable with the team 🙂

**What is the motivation or use case for changing this?**

For people who struggle with their vision this can make it harder to compare the numbers across the rows, as the digit columns do not line up. For example in the below image it can look like `date-fns` is `18 KiB` while `luxon` is `204 KiB`.

![lighthouse-sizes](https://user-images.githubusercontent.com/4369552/93003763-4eab1f80-f539-11ea-9268-12324624de08.jpeg)

**How is this beneficial to Lighthouse?**

Improve accessibility.


Pull request description:
i18n: format bytes with consistent fractional width
**Summary**
This changes how numbers with the '_byte_' itemType are formatted when viewed. 

Previously `2.5` and `1` when formatted with a granularity of `0.1` would be shown as `2.5 KiB` and `1 KiB`. Now they would be formatted as `2.5 KiB` and `1.0 KiB`.

This consistency can make it easier for some people when comparing sizes in a column as the decimal places will be aligned.

**Related Issues/PRs**

Fixes #11422


Patch:
diff --git a/lighthouse-core/report/html/renderer/i18n.js b/lighthouse-core/report/html/renderer/i18n.js
index bbbf025d6..8b8ccfe43 100644
--- a/lighthouse-core/report/html/renderer/i18n.js
+++ b/lighthouse-core/report/html/renderer/i18n.js
@@ -45,20 +45,41 @@ class I18n {
    * @return {string}
    */
   formatBytesToKiB(size, granularity = 0.1) {
-    const kbs = this._numberFormatter.format(Math.round(size / 1024 / granularity) * granularity);
+    const formatter = this._byteFormatterForGranularity(granularity);
+    const kbs = formatter.format(Math.round(size / 1024 / granularity) * granularity);
     return `${kbs}${NBSP2}KiB`;
   }
 
   /**
    * @param {number} size
-   * @param {number=} granularity Controls how coarse the displayed value is, defaults to 0.1
+   * @param {number=} granularity Controls how coarse the displayed value is, defaults to 1
    * @return {string}
    */
   formatBytes(size, granularity = 1) {
-    const kbs = this._numberFormatter.format(Math.round(size / granularity) * granularity);
+    const formatter = this._byteFormatterForGranularity(granularity);
+    const kbs = formatter.format(Math.round(size / granularity) * granularity);
     return `${kbs}${NBSP2}bytes`;
   }
 
+  /**
+   * Format bytes with a constant number of fractional digits, i.e for a granularity of 0.1, 10 becomes '10.0'
+   * @param {number} granularity Controls how coarse the displayed value is
+   * @return {Intl.NumberFormat}
+   */
+  _byteFormatterForGranularity(granularity) {
+    // assume any granularity above 1 will not contain fractional parts, i.e. will never be 1.5
+    let numberOfFractionDigits = 0;
+    if (granularity < 1) {
+      numberOfFractionDigits = -Math.floor(Math.log10(granularity));
+    }
+
+    return new Intl.NumberFormat(this._numberDateLocale, {
+      ...this._numberFormatter.resolvedOptions(),
+      maximumFractionDigits: numberOfFractionDigits,
+      minimumFractionDigits: numberOfFractionDigits,
+    });
+  }
+
   /**
    * @param {number} ms
    * @param {number=} granularity Controls how coarse the displayed value is, defaults to 10


Test patch:
diff --git a/lighthouse-core/test/report/html/renderer/i18n-test.js b/lighthouse-core/test/report/html/renderer/i18n-test.js
index 7a4ea47c3..a204edbc2 100644
--- a/lighthouse-core/test/report/html/renderer/i18n-test.js
+++ b/lighthouse-core/test/report/html/renderer/i18n-test.js
@@ -39,8 +39,55 @@ describe('util helpers', () => {
   it('formats bytes', () => {
     const i18n = new I18n('en', {...Util.UIStrings});
     assert.equal(i18n.formatBytesToKiB(100), `0.1${NBSP}KiB`);
-    assert.equal(i18n.formatBytesToKiB(2000), `2${NBSP}KiB`);
-    assert.equal(i18n.formatBytesToKiB(1014 * 1024), `1,014${NBSP}KiB`);
+    assert.equal(i18n.formatBytesToKiB(2000), `2.0${NBSP}KiB`);
+    assert.equal(i18n.formatBytesToKiB(1014 * 1024), `1,014.0${NBSP}KiB`);
+  });
+
+  it('formats bytes with different granularities', () => {
+    const i18n = new I18n('en', {...Util.UIStrings});
+
+    let granularity = 10;
+    assert.strictEqual(i18n.formatBytes(15.0, granularity), `20${NBSP}bytes`);
+    assert.strictEqual(i18n.formatBytes(15.12345, granularity), `20${NBSP}bytes`);
+    assert.strictEqual(i18n.formatBytes(14.99999, granularity), `10${NBSP}bytes`);
+
+    granularity = 1;
+    assert.strictEqual(i18n.formatBytes(15.0, granularity), `15${NBSP}bytes`);
+    assert.strictEqual(i18n.formatBytes(15.12345, granularity), `15${NBSP}bytes`);
+    assert.strictEqual(i18n.formatBytes(15.54321, granularity), `16${NBSP}bytes`);
+
+    granularity = 0.5;
+    assert.strictEqual(i18n.formatBytes(15.0, granularity), `15.0${NBSP}bytes`);
+    assert.strictEqual(i18n.formatBytes(15.12345, granularity), `15.0${NBSP}bytes`);
+    assert.strictEqual(i18n.formatBytes(15.54321, granularity), `15.5${NBSP}bytes`);
+
+    granularity = 0.1;
+    assert.strictEqual(i18n.formatBytes(15.0, granularity), `15.0${NBSP}bytes`);
+    assert.strictEqual(i18n.formatBytes(15.12345, granularity), `15.1${NBSP}bytes`);
+    assert.strictEqual(i18n.formatBytes(15.19999, granularity), `15.2${NBSP}bytes`);
+
+    granularity = 0.01;
+    assert.strictEqual(i18n.formatBytes(15.0, granularity), `15.00${NBSP}bytes`);
+    assert.strictEqual(i18n.formatBytes(15.12345, granularity), `15.12${NBSP}bytes`);
+    assert.strictEqual(i18n.formatBytes(15.19999, granularity), `15.20${NBSP}bytes`);
+  });
+
+  it('formats kibibytes with different granularities', () => {
+    const i18n = new I18n('en', {...Util.UIStrings});
+
+    let granularity = 10;
+    assert.strictEqual(i18n.formatBytesToKiB(5 * 1024, granularity), `10${NBSP}KiB`);
+    assert.strictEqual(i18n.formatBytesToKiB(4 * 1024, granularity), `0${NBSP}KiB`);
+
+    granularity = 1;
+    assert.strictEqual(i18n.formatBytesToKiB(5 * 1024, granularity), `5${NBSP}KiB`);
+    assert.strictEqual(i18n.formatBytesToKiB(4 * 1024 + 512, granularity), `5${NBSP}KiB`);
+    assert.strictEqual(i18n.formatBytesToKiB(4 * 1024 + 511, granularity), `4${NBSP}KiB`);
+
+    granularity = 0.01;
+    assert.strictEqual(i18n.formatBytesToKiB(5 * 1024, granularity), `5.00${NBSP}KiB`);
+    assert.strictEqual(i18n.formatBytesToKiB(5 * 1024 - 5, granularity), `5.00${NBSP}KiB`);
+    assert.strictEqual(i18n.formatBytesToKiB(5 * 1024 - 6, granularity), `4.99${NBSP}KiB`);
   });
 
   it('formats ms', () => {
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 27591b752..fde6aa5ba 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
@@ -270,23 +270,23 @@ describe('ReportUIFeatures', () => {
         }
 
         const initialExpected = [
-          '/script1.js(www.cdn.com)24 KiB8.8 KiB',
-          '10 KiB0 KiB',
-          '20 KiB0 KiB',
-          '/script2.js(www.example.com)24 KiB8.8 KiB',
-          '30 KiB0 KiB',
-          '40 KiB0 KiB',
-          '/script3.js(www.notexample.com)24 KiB8.8 KiB',
-          '50 KiB0 KiB',
-          '60 KiB0 KiB',
+          '/script1.js(www.cdn.com)24.0 KiB8.8 KiB',
+          '10.0 KiB0.0 KiB',
+          '20.0 KiB0.0 KiB',
+          '/script2.js(www.example.com)24.0 KiB8.8 KiB',
+          '30.0 KiB0.0 KiB',
+          '40.0 KiB0.0 KiB',
+          '/script3.js(www.notexample.com)24.0 KiB8.8 KiB',
+          '50.0 KiB0.0 KiB',
+          '60.0 KiB0.0 KiB',
         ];
 
         expect(getRowIdentifiers()).toEqual(initialExpected);
         filterCheckbox.click();
         expect(getRowIdentifiers()).toEqual([
-          '/script2.js(www.example.com)24 KiB8.8 KiB',
-          '30 KiB0 KiB',
-          '40 KiB0 KiB',
+          '/script2.js(www.example.com)24.0 KiB8.8 KiB',
+          '30.0 KiB0.0 KiB',
+          '40.0 KiB0.0 KiB',
         ]);
         filterCheckbox.click();
         expect(getRowIdentifiers()).toEqual(initialExpected);