Repository: GoogleChrome/lighthouse
Pull request #7628 (2019-03-22 00:41:35)

misc: remove obsolete Util.formatDisplayValue
#6767 finally put to bed the non-`string` version of `displayValue`, but we forgot to remove `Util.formatDisplayValue`, which was always returning on the first two lines of the function (either identity or `''` :)




Patch:
diff --git a/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js b/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js
index c3337b04e..9c99a78fc 100644
--- a/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js
+++ b/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js
@@ -197,7 +197,6 @@ class UnusedBytes extends Audit {
     const wastedKb = Math.round(wastedBytes / KB_IN_BYTES);
     const wastedMs = this.computeWasteWithTTIGraph(results, graph, simulator);
 
-    /** @type {LH.Audit.DisplayValue} */
     let displayValue = result.displayValue || '';
     if (typeof result.displayValue === 'undefined' && wastedBytes) {
       displayValue = str_(i18n.UIStrings.displayValueByteSavings, {wastedBytes});
diff --git a/lighthouse-core/audits/load-fast-enough-for-pwa.js b/lighthouse-core/audits/load-fast-enough-for-pwa.js
index 28614db8b..102fd9796 100644
--- a/lighthouse-core/audits/load-fast-enough-for-pwa.js
+++ b/lighthouse-core/audits/load-fast-enough-for-pwa.js
@@ -80,7 +80,7 @@ class LoadFastEnough4Pwa extends Audit {
 
     const score = Number(tti.timing < MAXIMUM_TTI);
 
-    /** @type {LH.Audit.DisplayValue|undefined} */
+    /** @type {string|undefined} */
     let displayValue;
     /** @type {string|undefined} */
     let explanation;
diff --git a/lighthouse-core/audits/seo/font-size.js b/lighthouse-core/audits/seo/font-size.js
index 2dad98bc9..2faab58c8 100644
--- a/lighthouse-core/audits/seo/font-size.js
+++ b/lighthouse-core/audits/seo/font-size.js
@@ -296,7 +296,6 @@ class FontSize extends Audit {
     }
 
     const decimalProportion = (percentageOfPassingText / 100);
-    /** @type {LH.Audit.DisplayValue} */
     const displayValue = str_(UIStrings.displayValue, {decimalProportion});
     const details = Audit.makeTableDetails(headings, tableData);
     const passed = percentageOfPassingText >= MINIMAL_PERCENTAGE_OF_LEGIBLE_TEXT;
diff --git a/lighthouse-core/audits/user-timings.js b/lighthouse-core/audits/user-timings.js
index 339b528a5..2d8f64d27 100644
--- a/lighthouse-core/audits/user-timings.js
+++ b/lighthouse-core/audits/user-timings.js
@@ -105,7 +105,7 @@ class UserTimings extends Audit {
 
       const details = Audit.makeTableDetails(headings, tableRows);
 
-      /** @type {LH.Audit.Product['displayValue']} */
+      /** @type {string|undefined} */
       let displayValue;
       if (userTimings.length) {
         displayValue = str_(UIStrings.displayValue, {itemCount: userTimings.length});
diff --git a/lighthouse-core/gather/driver.js b/lighthouse-core/gather/driver.js
index e93e9d016..b800c9adf 100644
--- a/lighthouse-core/gather/driver.js
+++ b/lighthouse-core/gather/driver.js
@@ -47,8 +47,8 @@ class Driver {
      */
     this._eventEmitter = /** @type {CrdpEventEmitter} */ (new EventEmitter());
     this._connection = connection;
-    // Used to save network and lifecycle protocol traffic. Just Page, Network, and Target are needed.
-    this._devtoolsLog = new DevtoolsLog(/^(Page|Network|Target)\./);
+    // Used to save network and lifecycle protocol traffic. Just Page and Network are needed.
+    this._devtoolsLog = new DevtoolsLog(/^(Page|Network)\./);
     this.online = true;
     /** @type {Map<string, number>} */
     this._domainEnabledCounts = new Map();
@@ -69,32 +69,25 @@ class Driver {
      */
     this._monitoredUrl = null;
 
-    let targetProxyMessageId = 0;
+    /**
+     * Used for sending messages to subtargets. Each message needs a unique ID even if we don't bother
+     * reading back the result of each command.
+     *
+     * @type {number}
+     * @private
+     */
+    this._targetProxyMessageId = 0;
+
     this.on('Target.attachedToTarget', event => {
-      targetProxyMessageId++;
-      // We're only interested in network requests from iframes for now as those are "part of the page".
-      if (event.targetInfo.type !== 'iframe') return;
-
-      // We want to receive information about network requests from iframes, so enable the Network domain.
-      // Network events from subtargets will be stringified and sent back on `Target.receivedMessageFromTarget`.
-      this.sendCommand('Target.sendMessageToTarget', {
-        message: JSON.stringify({id: targetProxyMessageId, method: 'Network.enable'}),
-        sessionId: event.sessionId,
-      });
+      this._handleTargetAttached(event, []).catch(this._handleEventError);
     });
 
-    connection.on('protocolevent', event => {
-      this._devtoolsLog.record(event);
-      if (this._networkStatusMonitor) {
-        this._networkStatusMonitor.dispatch(event);
-      }
-
-      // @ts-ignore TODO(bckenny): tsc can't type event.params correctly yet,
-      // typing as property of union instead of narrowing from union of
-      // properties. See https://github.com/Microsoft/TypeScript/pull/22348.
-      this._eventEmitter.emit(event.method, event.params);
+    this.on('Target.receivedMessageFromTarget', event => {
+      this._handleReceivedMessageFromTarget(event, []).catch(this._handleEventError);
     });
 
+    connection.on('protocolevent', this._handleProtocolEvent.bind(this));
+
     /**
      * @type {number}
      * @private
@@ -275,6 +268,121 @@ class Driver {
     this._nextProtocolTimeout = timeout;
   }
 
+  /**
+   * @param {LH.Protocol.RawEventMessage} event
+   */
+  _handleProtocolEvent(event) {
+    this._devtoolsLog.record(event);
+    if (this._networkStatusMonitor) {
+      this._networkStatusMonitor.dispatch(event);
+    }
+
+    // @ts-ignore TODO(bckenny): tsc can't type event.params correctly yet,
+    // typing as property of union instead of narrowing from union of
+    // properties. See https://github.com/Microsoft/TypeScript/pull/22348.
+    this._eventEmitter.emit(event.method, event.params);
+  }
+
+  /**
+   * @param {Error} error
+   */
+  _handleEventError(error) {
+    log.error('Driver', 'Unhandled event error', error.message);
+  }
+
+  /**
+   * @param {LH.Crdp.Target.ReceivedMessageFromTargetEvent} event
+   * @param {string[]} parentSessionIds The list of session ids of the parents from youngest to oldest.
+   */
+  async _handleReceivedMessageFromTarget(event, parentSessionIds) {
+    const {sessionId, message} = event;
+    /** @type {LH.Protocol.RawMessage} */
+    const protocolMessage = JSON.parse(message);
+
+    // Message was a response to some command, not an event, so we'll ignore it.
+    if ('id' in protocolMessage) return;
+
+    // We receive messages from the outermost subtarget which wraps the messages from the inner subtargets.
+    // We are recursively processing them from outside in, so build the list of parentSessionIds accordingly.
+    const sessionIdPath = [sessionId, ...parentSessionIds];
+
+    if (protocolMessage.method === 'Target.receivedMessageFromTarget') {
+      // Unravel any messages from subtargets by recursively processing
+      await this._handleReceivedMessageFromTarget(protocolMessage.params, sessionIdPath);
+    }
+
+    if (protocolMessage.method === 'Target.attachedToTarget') {
+      // Process any attachedToTarget messages from subtargets
+      await this._handleTargetAttached(protocolMessage.params, sessionIdPath);
+    }
+
+    if (protocolMessage.method.startsWith('Network')) {
+      this._handleProtocolEvent(protocolMessage);
+    }
+  }
+
+  /**
+   * @param {LH.Crdp.Target.AttachedToTargetEvent} event
+   * @param {string[]} parentSessionIds The list of session ids of the parents from youngest to oldest.
+   */
+  async _handleTargetAttached(event, parentSessionIds) {
+    const sessionIdPath = [event.sessionId, ...parentSessionIds];
+
+    // We're only interested in network requests from iframes for now as those are "part of the page".
+    // If it's not an iframe, just resume it and move on.
+    if (event.targetInfo.type !== 'iframe') {
+      // We suspended the target when we auto-attached, so make sure it goes back to being normal.
+      await this.sendMessageToTarget(sessionIdPath, 'Runtime.runIfWaitingForDebugger');
+      return;
+    }
+
+    // Events from subtargets will be stringified and sent back on `Target.receivedMessageFromTarget`.
+    // We want to receive information about network requests from iframes, so enable the Network domain.
+    await this.sendMessageToTarget(sessionIdPath, 'Network.enable');
+    // We also want to receive information about subtargets of subtargets, so make sure we autoattach recursively.
+    await this.sendMessageToTarget(sessionIdPath, 'Target.setAutoAttach', {
+      autoAttach: true,
+      // Pause targets on startup so we don't miss anything
+      waitForDebuggerOnStart: true,
+    });
+
+    // We suspended the target when we auto-attached, so make sure it goes back to being normal.
+    await this.sendMessageToTarget(sessionIdPath, 'Runtime.runIfWaitingForDebugger');
+  }
+
+  /**
+   * Send protocol commands to a subtarget, wraps the message in as many `Target.sendMessageToTarget`
+   * commands as necessary.
+   *
+   * @template {keyof LH.CrdpCommands} C
+   * @param {string[]} sessionIdPath List of session ids to send to, from youngest-oldest/inside-out.
+   * @param {C} method
+   * @param {LH.CrdpCommands[C]['paramsType']} params
+   * @return {Promise<LH.CrdpCommands[C]['returnType']>}
+   */
+  sendMessageToTarget(sessionIdPath, method, ...params) {
+    this._targetProxyMessageId++;
+    /** @type {LH.Crdp.Target.SendMessageToTargetRequest} */
+    let payload = {
+      sessionId: sessionIdPath[0],
+      message: JSON.stringify({id: this._targetProxyMessageId, method, params: params[0]}),
+    };
+
+    for (const sessionId of sessionIdPath.slice(1)) {
+      this._targetProxyMessageId++;
+      payload = {
+        sessionId,
+        message: JSON.stringify({
+          id: this._targetProxyMessageId,
+          method: 'Target.sendMessageToTarget',
+          params: payload,
+        }),
+      };
+    }
+
+    return this.sendCommand('Target.sendMessageToTarget', payload);
+  }
+
   /**
    * Call protocol methods, with a timeout.
    * To configure the timeout for the next call, use 'setNextProtocolTimeout'.
@@ -1044,7 +1152,8 @@ class Driver {
     // Enable auto-attaching to subtargets so we receive iframe information
     await this.sendCommand('Target.setAutoAttach', {
       autoAttach: true,
-      waitForDebuggerOnStart: false,
+      // Pause targets on startup so we don't miss anything
+      waitForDebuggerOnStart: true,
     });
 
     await this.sendCommand('Page.enable');
diff --git a/lighthouse-core/lib/network-recorder.js b/lighthouse-core/lib/network-recorder.js
index 005f06edf..f0f05e64a 100644
--- a/lighthouse-core/lib/network-recorder.js
+++ b/lighthouse-core/lib/network-recorder.js
@@ -101,26 +101,13 @@ class NetworkRecorder extends EventEmitter {
     return !!(isQUIC && receivedHeaders && record.endTime);
   }
 
-  /**
-   * frame root network requests don't always "finish" even when they're done loading data, use responseReceived instead
-   * @see https://github.com/GoogleChrome/lighthouse/issues/6067#issuecomment-423211201
-   * @param {LH.Artifacts.NetworkRequest} record
-   * @return {boolean}
-   */
-  static _isFrameRootRequestAndFinished(record) {
-    const isFrameRootRequest = record.url === record.documentURL;
-    const responseReceived = record.responseReceivedTime > 0;
-    return !!(isFrameRootRequest && responseReceived && record.endTime);
-  }
-
   /**
    * @param {LH.Artifacts.NetworkRequest} record
    * @return {boolean}
    */
   static isNetworkRecordFinished(record) {
     return record.finished ||
-      NetworkRecorder._isQUICAndFinished(record) ||
-      NetworkRecorder._isFrameRootRequestAndFinished(record);
+      NetworkRecorder._isQUICAndFinished(record);
   }
 
   /**
@@ -303,21 +290,6 @@ class NetworkRecorder extends EventEmitter {
     request.onResourceChangedPriority(data);
   }
 
-  /**
-   * Events from targets other than the main frame are proxied through `Target.receivedMessageFromTarget`.
-   * Their payloads are JSON-stringified into the `.message` property
-   * @param {LH.Crdp.Target.ReceivedMessageFromTargetEvent} data
-   */
-  onReceivedMessageFromTarget(data) {
-    /** @type {LH.Protocol.RawMessage} */
-    const protocolMessage = JSON.parse(data.message);
-
-    // Message was a response to some command, not an event, so we'll ignore it.
-    if ('id' in protocolMessage) return;
-    // Message was an event, replay it through our normal dispatch process.
-    this.dispatch(protocolMessage);
-  }
-
   /**
    * Routes network events to their handlers, so we can construct networkRecords
    * @param {LH.Protocol.RawEventMessage} event
@@ -331,7 +303,6 @@ class NetworkRecorder extends EventEmitter {
       case 'Network.loadingFinished': return this.onLoadingFinished(event.params);
       case 'Network.loadingFailed': return this.onLoadingFailed(event.params);
       case 'Network.resourceChangedPriority': return this.onResourceChangedPriority(event.params);
-      case 'Target.receivedMessageFromTarget': return this.onReceivedMessageFromTarget(event.params); // eslint-disable-line max-len
       default: return;
     }
   }
diff --git a/lighthouse-core/report/html/renderer/category-renderer.js b/lighthouse-core/report/html/renderer/category-renderer.js
index 377cbe3e9..549572ca9 100644
--- a/lighthouse-core/report/html/renderer/category-renderer.js
+++ b/lighthouse-core/report/html/renderer/category-renderer.js
@@ -75,8 +75,7 @@ class CategoryRenderer {
     const scoreDisplayMode = audit.result.scoreDisplayMode;
 
     if (audit.result.displayValue) {
-      const displayValue = Util.formatDisplayValue(audit.result.displayValue);
-      this.dom.find('.lh-audit__display-text', auditEl).textContent = displayValue;
+      this.dom.find('.lh-audit__display-text', auditEl).textContent = audit.result.displayValue;
     }
 
     const titleEl = this.dom.find('.lh-audit__title', auditEl);
diff --git a/lighthouse-core/report/html/renderer/performance-category-renderer.js b/lighthouse-core/report/html/renderer/performance-category-renderer.js
index 42a3e6c2c..b3be31a80 100644
--- a/lighthouse-core/report/html/renderer/performance-category-renderer.js
+++ b/lighthouse-core/report/html/renderer/performance-category-renderer.js
@@ -36,7 +36,7 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
     titleEl.textContent = audit.result.title;
 
     const valueEl = this.dom.find('.lh-metric__value', tmpl);
-    valueEl.textContent = Util.formatDisplayValue(audit.result.displayValue);
+    valueEl.textContent = audit.result.displayValue || '';
 
     const descriptionEl = this.dom.find('.lh-metric__description', tmpl);
     descriptionEl.appendChild(this.dom.convertMarkdownLinkSnippets(audit.result.description));
@@ -78,7 +78,7 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
 
     // Set [title] tooltips
     if (audit.result.displayValue) {
-      const displayValue = Util.formatDisplayValue(audit.result.displayValue);
+      const displayValue = audit.result.displayValue;
       this.dom.find('.lh-load-opportunity__sparkline', element).title = displayValue;
       displayEl.title = displayValue;
     }
diff --git a/lighthouse-core/report/html/renderer/util.js b/lighthouse-core/report/html/renderer/util.js
index 5da98dfaa..5e7ce4884 100644
--- a/lighthouse-core/report/html/renderer/util.js
+++ b/lighthouse-core/report/html/renderer/util.js
@@ -94,46 +94,6 @@ class Util {
     }
   }
 
-  /**
-   * @param {string|Array<string|number>=} displayValue
-   * @return {string}
-   */
-  static formatDisplayValue(displayValue) {
-    if (typeof displayValue === 'string') return displayValue;
-    if (!displayValue) return '';
-
-    const replacementRegex = /%([0-9]*(\.[0-9]+)?d|s)/;
-    const template = /** @type {string} */ (displayValue[0]);
-    if (typeof template !== 'string') {
-      // First value should always be the format string, but we don't want to fail to build
-      // a report, return a placeholder.
-      return 'UNKNOWN';
-    }
-
-    let output = template;
-    for (const replacement of displayValue.slice(1)) {
-      if (!replacementRegex.test(output)) {
-        // eslint-disable-next-line no-console
-        console.warn('Too many replacements given');
-        break;
-      }
-
-      output = output.replace(replacementRegex, match => {
-        const granularity = Number(match.match(/[0-9.]+/)) || 1;
-        return match === '%s' ?
-          replacement.toLocaleString() :
-          (Math.round(Number(replacement) / granularity) * granularity).toLocaleString();
-      });
-    }
-
-    if (replacementRegex.test(output)) {
-      // eslint-disable-next-line no-console
-      console.warn('Not enough replacements given');
-    }
-
-    return output;
-  }
-
   /**
    * Used to determine if the "passed" for the purposes of showing up in the "failed" or "passed"
    * sections of the report.
diff --git a/types/audit.d.ts b/types/audit.d.ts
index 0afbc416f..dddf009cf 100644
--- a/types/audit.d.ts
+++ b/types/audit.d.ts
@@ -38,8 +38,6 @@ declare global {
 
     export type ScoreDisplayMode = Audit.ScoreDisplayModes[keyof Audit.ScoreDisplayModes];
 
-    export type DisplayValue = string;
-
     export interface Meta {
       /** The string identifier of the audit, in kebab case. */
       id: string;
@@ -65,7 +63,7 @@ declare global {
     // Type returned by Audit.audit(). Only rawValue is required.
     export interface Product {
       rawValue: boolean | number | null;
-      displayValue?: DisplayValue;
+      displayValue?: string;
       explanation?: string;
       errorMessage?: string;
       warnings?: string[];
@@ -79,7 +77,7 @@ declare global {
     /* Audit result returned in Lighthouse report. All audits offer a description and score of 0-1 */
     export interface Result {
       rawValue: boolean | number | null;
-      displayValue?: DisplayValue;
+      displayValue?: string;
       explanation?: string;
       errorMessage?: string;
       warnings?: string[];


Test patch:
diff --git a/lighthouse-cli/test/fixtures/oopif.html b/lighthouse-cli/test/fixtures/oopif.html
index b27db2d3c..df3cb0c1b 100644
--- a/lighthouse-cli/test/fixtures/oopif.html
+++ b/lighthouse-cli/test/fixtures/oopif.html
@@ -5,6 +5,6 @@
   </head>
   <body>
     <h1>Hello frames</h1>
-    <iframe name="oopif" src="https://airhorner.com"></iframe>
+    <iframe name="oopif" src="https://www.paulirish.com/2012/why-moving-elements-with-translate-is-better-than-posabs-topleft/" style="width: 100vw; height: 100vh;"></iframe>
   </body>
 </html>
diff --git a/lighthouse-cli/test/smokehouse/oopif-expectations.js b/lighthouse-cli/test/smokehouse/oopif-expectations.js
index 94eac432d..f12e459ec 100644
--- a/lighthouse-cli/test/smokehouse/oopif-expectations.js
+++ b/lighthouse-cli/test/smokehouse/oopif-expectations.js
@@ -16,10 +16,11 @@ module.exports = [
       'network-requests': {
         details: {
           items: {
-            // The page itself only makes a few requests.
-            // We want to make sure we are finding the iframe's requests (airhorner) while being flexible enough
-            // to allow changes to the live site.
-            length: '>10',
+            // We want to make sure we are finding the iframe's requests (paulirish.com) *AND*
+            // the iframe's iframe's iframe's requests (youtube.com/doubleclick/etc).
+            // - paulirish.com ~40-60 requests
+            // - paulirish.com + all descendant iframes ~80-90 requests
+            length: '>70',
           },
         },
       },
diff --git a/lighthouse-core/test/gather/driver-test.js b/lighthouse-core/test/gather/driver-test.js
index 89ec69f61..7d63f3afa 100644
--- a/lighthouse-core/test/gather/driver-test.js
+++ b/lighthouse-core/test/gather/driver-test.js
@@ -1000,6 +1000,8 @@ describe('.goOnline', () => {
 describe('Multi-target management', () => {
   it('enables the Network domain for iframes', async () => {
     connectionStub.sendCommand = createMockSendCommandFn()
+      .mockResponse('Target.sendMessageToTarget', {})
+      .mockResponse('Target.sendMessageToTarget', {})
       .mockResponse('Target.sendMessageToTarget', {});
 
     driver._eventEmitter.emit('Target.attachedToTarget', {
@@ -1016,16 +1018,73 @@ describe('Multi-target management', () => {
     });
   });
 
-  it('ignores other target types', async () => {
+  it('enables the Network domain for iframes of iframes of iframes', async () => {
     connectionStub.sendCommand = createMockSendCommandFn()
-    .mockResponse('Target.sendMessageToTarget', {});
+      .mockResponse('Target.sendMessageToTarget', {})
+      .mockResponse('Target.sendMessageToTarget', {})
+      .mockResponse('Target.sendMessageToTarget', {});
+
+    driver._eventEmitter.emit('Target.receivedMessageFromTarget', {
+      sessionId: 'Outer',
+      message: JSON.stringify({
+        method: 'Target.receivedMessageFromTarget',
+        params: {
+          sessionId: 'Middle',
+          message: JSON.stringify({
+            method: 'Target.attachedToTarget',
+            params: {
+              sessionId: 'Inner',
+              targetInfo: {type: 'iframe'},
+            },
+          }),
+        },
+      }),
+    });
+
+    await flushAllTimersAndMicrotasks();
+
+    const sendMessageArgs = connectionStub.sendCommand
+      .findInvocation('Target.sendMessageToTarget');
+    const stringified = `{
+      "id": 3,
+      "method": "Target.sendMessageToTarget",
+      "params": {
+        "sessionId": "Middle",
+        "message": "{
+          \\"id\\": 2,
+          \\"method\\": \\"Target.sendMessageToTarget\\",
+          \\"params\\": {
+            \\"sessionId\\": \\"Inner\\",
+            \\"message\\":\\ "{
+              \\\\\\"id\\\\\\":1,
+              \\\\\\"method\\\\\\":\\\\\\"Network.enable\\\\\\"
+            }\\"
+          }}"
+        }
+      }`.replace(/\s+/g, '');
+
+    expect(sendMessageArgs).toEqual({
+      message: stringified,
+      sessionId: 'Outer',
+    });
+  });
+
+  it('ignores other target types, but still resumes them', async () => {
+    connectionStub.sendCommand = createMockSendCommandFn()
+      .mockResponse('Target.sendMessageToTarget', {});
 
     driver._eventEmitter.emit('Target.attachedToTarget', {
-      sessionId: 123,
+      sessionId: 'SW1',
       targetInfo: {type: 'service_worker'},
     });
     await flushAllTimersAndMicrotasks();
 
-    expect(connectionStub.sendCommand).not.toHaveBeenCalled();
+
+    const sendMessageArgs = connectionStub.sendCommand
+      .findInvocation('Target.sendMessageToTarget');
+    expect(sendMessageArgs).toEqual({
+      message: JSON.stringify({id: 1, method: 'Runtime.runIfWaitingForDebugger'}),
+      sessionId: 'SW1',
+    });
   });
 });
diff --git a/lighthouse-core/test/lib/network-recorder-test.js b/lighthouse-core/test/lib/network-recorder-test.js
index b3f724c96..671ad53a8 100644
--- a/lighthouse-core/test/lib/network-recorder-test.js
+++ b/lighthouse-core/test/lib/network-recorder-test.js
@@ -267,9 +267,7 @@ describe('network recorder', function() {
       ];
 
       const periods = NetworkRecorder.findNetworkQuietPeriods(records, 0);
-      assert.deepStrictEqual(periods, [
-        {start: 1200, end: Infinity},
-      ]);
+      assert.deepStrictEqual(periods, []);
     });
 
     it('should handle QUIC requests', () => {
diff --git a/lighthouse-core/test/report/html/renderer/util-test.js b/lighthouse-core/test/report/html/renderer/util-test.js
index 6fcd9b5a3..f65c59bc7 100644
--- a/lighthouse-core/test/report/html/renderer/util-test.js
+++ b/lighthouse-core/test/report/html/renderer/util-test.js
@@ -138,38 +138,6 @@ describe('util helpers', () => {
     assert.equal(descriptions.cpuThrottling, '2x slowdown (Simulated)');
   });
 
-  it('formats display values', () => {
-    const format = arg => Util.formatDisplayValue(arg);
-    assert.equal(format(undefined), '');
-    assert.equal(format('Foo %s %d'), 'Foo %s %d');
-    assert.equal(format([]), 'UNKNOWN');
-    assert.equal(format(['%s %s', 'Hello', 'formatDisplayValue']), 'Hello formatDisplayValue');
-    assert.equal(format(['%s%', 99.9]), '99.9%');
-    assert.equal(format(['%d%', 99.9]), '100%');
-    assert.equal(format(['%s ms', 12345.678]), '12,345.678 ms');
-    assert.equal(format(['%10d ms', 12345.678]), '12,350 ms');
-    assert.equal(format(['%.01d ms', 12345.678]), '12,345.68 ms');
-    // handle edge cases
-    assert.equal(format(['%.01s literal', 1234]), '%.01s literal');
-    assert.equal(format(['%1.01.1d junk', 1234]), '%1.01.1d junk');
-  });
-
-  it('warns on improper display value formatting', () => {
-    assert.equal(Util.formatDisplayValue(['%s']), '%s');
-    assert.equal(Util.formatDisplayValue(['%s', 'foo', 'bar']), 'foo');
-    assert.deepEqual(consoleWarnCalls, [
-      'Not enough replacements given',
-      'Too many replacements given',
-    ]);
-  });
-
-  it('does not mutate the provided array', () => {
-    const displayValue = ['one:%s, two:%s', 'foo', 'bar'];
-    const cloned = JSON.parse(JSON.stringify(displayValue));
-    Util.formatDisplayValue(displayValue);
-    assert.deepStrictEqual(displayValue, cloned, 'displayValue was mutated');
-  });
-
   describe('#prepareReportResult', () => {
     it('corrects underscored `notApplicable` scoreDisplayMode', () => {
       const clonedSampleResult = JSON.parse(JSON.stringify(sampleResult));