Repository: GoogleChrome/lighthouse
Pull request #10136 (2019-12-21 00:43:43)

tests(gather-runner): replace getMockedEmulationDriver w/ mock-commands
About to make this file typecheck'd - this was a refactor that needed to be done.


Patch:
diff --git a/tsconfig.json b/tsconfig.json
index f1e90c03c..fe0eecb3d 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -24,5 +24,9 @@
     "lighthouse-core/test/**/*.js",
     "clients/test/**/*.js",
     "lighthouse-cli/test/fixtures/**/*.js",
-  ]
+  ],
+  "files": [
+    // Opt-in to typechecking for some core tests.
+    "lighthouse-core/test/gather/driver-test.js",
+  ],
 }
diff --git a/types/externs.d.ts b/types/externs.d.ts
index 4bc8bf1ba..2c210b209 100644
--- a/types/externs.d.ts
+++ b/types/externs.d.ts
@@ -28,8 +28,9 @@ declare global {
 
   /** Make optional all properties on T and any properties on object properties of T. */
   type RecursivePartial<T> = {
-    [P in keyof T]+?: T[P] extends object ?
-      RecursivePartial<T[P]> :
+    [P in keyof T]+?:
+      T[P] extends (infer U)[] ? RecursivePartial<U>[] :
+      T[P] extends (object|undefined) ? RecursivePartial<T[P]> :
       T[P];
   };
 
diff --git a/types/jest.d.ts b/types/jest.d.ts
new file mode 100644
index 000000000..8476a69d2
--- /dev/null
+++ b/types/jest.d.ts
@@ -0,0 +1,19 @@
+/**
+ * @license Copyright 2019 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.
+ */
+
+declare namespace jest {
+  interface Matchers<R> {
+    /**
+    * Asserts that an inspectable promise created by makePromiseInspectable is currently resolved or rejected.
+    * This is useful for situations where we want to test that we are actually waiting for a particular event.
+    */
+   toBeDone: (failureMessage?: string) => object;
+   /**
+    * Asserts that an i18n string (using en-US) matches an expected pattern.
+    */
+   toBeDisplayString: (pattern: RegExp) => object;
+  }
+}


Test patch:
diff --git a/lighthouse-core/test/gather/driver-test.js b/lighthouse-core/test/gather/driver-test.js
index ba19a8209..b0349f4ea 100644
--- a/lighthouse-core/test/gather/driver-test.js
+++ b/lighthouse-core/test/gather/driver-test.js
@@ -8,11 +8,11 @@
 const Driver = require('../../gather/driver.js');
 const Connection = require('../../gather/connections/connection.js');
 const LHElement = require('../../lib/lh-element.js');
-const EventEmitter = require('events').EventEmitter;
 const {protocolGetVersionResponse} = require('./fake-driver.js');
 const {createMockSendCommandFn, createMockOnceFn} = require('./mock-commands.js');
 
-const redirectDevtoolsLog = require('../fixtures/wikipedia-redirect.devtoolslog.json');
+const redirectDevtoolsLog = /** @type {LH.Protocol.RawEventMessage[]} */ (
+  require('../fixtures/wikipedia-redirect.devtoolslog.json'));
 
 /* eslint-env jest */
 
@@ -23,12 +23,13 @@ jest.useFakeTimers();
  *
  * @template T
  * @param {Promise<T>} promise
- * @return {Promise<T> & {isDone: () => boolean, isResolved: () => boolean, isRejected: () => boolean}}
  */
 function makePromiseInspectable(promise) {
   let isResolved = false;
   let isRejected = false;
+  /** @type {T=} */
   let resolvedValue = undefined;
+  /** @type {any=} */
   let rejectionError = undefined;
   const inspectablePromise = promise.then(value => {
     isResolved = true;
@@ -40,12 +41,51 @@ function makePromiseInspectable(promise) {
     throw err;
   });
 
-  inspectablePromise.isDone = () => isResolved || isRejected;
-  inspectablePromise.isResolved = () => isResolved;
-  inspectablePromise.isRejected = () => isRejected;
-  inspectablePromise.getDebugValues = () => ({resolvedValue, rejectionError});
+  return Object.assign(inspectablePromise, {
+    isDone() {
+      return isResolved || isRejected;
+    },
+    isResolved() {
+      return isResolved;
+    },
+    isRejected() {
+      return isRejected;
+    },
+    getDebugValues() {
+      return {resolvedValue, rejectionError};
+    },
+  });
+}
 
-  return inspectablePromise;
+function createDecomposedPromise() {
+  /** @type {Function} */
+  let resolve;
+  /** @type {Function} */
+  let reject;
+  const promise = new Promise((r1, r2) => {
+    resolve = r1;
+    reject = r2;
+  });
+  // @ts-ignore: Ignore 'unused' error.
+  return {promise, resolve, reject};
+}
+
+function createMockWaitForFn() {
+  const {promise, resolve, reject} = createDecomposedPromise();
+
+  const mockCancelFn = jest.fn();
+  const mockFn = jest.fn().mockReturnValue({promise, cancel: mockCancelFn});
+
+  return Object.assign(mockFn, {
+    mockResolve: resolve,
+    /** @param {Error=} err */
+    mockReject(err) {
+      reject(err || new Error('Rejected'));
+    },
+    getMockCancelFn() {
+      return mockCancelFn;
+    },
+  });
 }
 
 expect.extend({
@@ -53,7 +93,7 @@ expect.extend({
    * Asserts that an inspectable promise created by makePromiseInspectable is currently resolved or rejected.
    * This is useful for situations where we want to test that we are actually waiting for a particular event.
    *
-   * @param {ReturnType<makePromiseInspectable>} received
+   * @param {ReturnType<typeof makePromiseInspectable>} received
    * @param {string} failureMessage
    */
   toBeDone(received, failureMessage) {
@@ -81,14 +121,33 @@ async function flushAllTimersAndMicrotasks(ms = 1000) {
   }
 }
 
+/**
+ * @typedef DriverMockMethods
+ * @property {ReturnType<typeof createMockOnceFn>} on
+ * @property {ReturnType<typeof createMockOnceFn>} once
+ * @property {ReturnType<typeof createMockWaitForFn>} _waitForFCP
+ * @property {ReturnType<typeof createMockWaitForFn>} _waitForLoadEvent
+ * @property {ReturnType<typeof createMockWaitForFn>} _waitForNetworkIdle
+ * @property {ReturnType<typeof createMockWaitForFn>} _waitForCPUIdle
+ * @property {(...args: RecursivePartial<Parameters<Driver['gotoURL']>>) => ReturnType<Driver['gotoURL']>} gotoURL
+ * @property {(...args: RecursivePartial<Parameters<Driver['goOnline']>>) => ReturnType<Driver['goOnline']>} goOnline
+*/
+
+/** @typedef {Driver & DriverMockMethods} TestDriver */
+
+/** @type {TestDriver} */
 let driver;
+/** @type {Connection & {sendCommand: ReturnType<typeof createMockSendCommandFn>}} */
 let connectionStub;
 
 beforeEach(() => {
+  // @ts-ignore - connectionStub has a mocked version of sendCommand implemented in each test
   connectionStub = new Connection();
+  // @ts-ignore
   connectionStub.sendCommand = cmd => {
     throw new Error(`${cmd} not implemented`);
   };
+  // @ts-ignore - driver has a mocked version of on/once implemented in each test
   driver = new Driver(connectionStub);
 });
 
@@ -104,7 +163,7 @@ describe('.querySelector(All)', () => {
 
   it('returns element instance when DOM.querySelector finds a node', async () => {
     connectionStub.sendCommand = createMockSendCommandFn()
-    .mockResponse('DOM.getDocument', {root: {nodeId: 249}})
+      .mockResponse('DOM.getDocument', {root: {nodeId: 249}})
       .mockResponse('DOM.querySelector', {nodeId: 231});
 
     const result = await driver.querySelector('meta head');
@@ -113,7 +172,7 @@ describe('.querySelector(All)', () => {
 
   it('returns [] when DOM.querySelectorAll finds no node', async () => {
     connectionStub.sendCommand = createMockSendCommandFn()
-    .mockResponse('DOM.getDocument', {root: {nodeId: 249}})
+      .mockResponse('DOM.getDocument', {root: {nodeId: 249}})
       .mockResponse('DOM.querySelectorAll', {nodeIds: []});
 
     const result = await driver.querySelectorAll('#no.matches');
@@ -122,7 +181,7 @@ describe('.querySelector(All)', () => {
 
   it('returns element when DOM.querySelectorAll finds node', async () => {
     connectionStub.sendCommand = createMockSendCommandFn()
-    .mockResponse('DOM.getDocument', {root: {nodeId: 249}})
+      .mockResponse('DOM.getDocument', {root: {nodeId: 249}})
       .mockResponse('DOM.querySelectorAll', {nodeIds: [231]});
 
     const result = await driver.querySelectorAll('#no.matches');
@@ -168,6 +227,7 @@ describe('.getRequestContent', () => {
   it('throws if getRequestContent takes too long', async () => {
     const mockTimeout = 5000;
     const driverTimeout = 1000;
+    // @ts-ignore
     connectionStub.sendCommand = jest.fn()
       .mockImplementation(() => new Promise(r => setTimeout(r, mockTimeout)));
 
@@ -227,7 +287,7 @@ describe('.evaluateAsync', () => {
 
   it('evaluates an expression in isolation', async () => {
     connectionStub.sendCommand = createMockSendCommandFn()
-      .mockResponse('Page.getResourceTree', {frameTree: {frame: {id: 1337}}})
+      .mockResponse('Page.getResourceTree', {frameTree: {frame: {id: '1337'}}})
       .mockResponse('Page.createIsolatedWorld', {executionContextId: 1})
       .mockResponse('Runtime.evaluate', {result: {value: 2}});
 
@@ -236,15 +296,14 @@ describe('.evaluateAsync', () => {
 
     // Check that we used the correct frame when creating the isolated context
     const createWorldArgs = connectionStub.sendCommand.findInvocation('Page.createIsolatedWorld');
-    expect(createWorldArgs).toMatchObject({frameId: 1337});
+    expect(createWorldArgs).toMatchObject({frameId: '1337'});
 
     // Check that we used the isolated context when evaluating
     const evaluateArgs = connectionStub.sendCommand.findInvocation('Runtime.evaluate');
     expect(evaluateArgs).toMatchObject({contextId: 1});
 
     // Make sure we cached the isolated context from last time
-    connectionStub.sendCommand = createMockSendCommandFn().mockResponse(
-      'Runtime.evaluate',
+    connectionStub.sendCommand = createMockSendCommandFn().mockResponse('Runtime.evaluate',
       {result: {value: 2}}
     );
     await driver.evaluateAsync('1 + 1', {useIsolation: true});
@@ -256,10 +315,10 @@ describe('.evaluateAsync', () => {
 
   it('recovers from isolation failures', async () => {
     connectionStub.sendCommand = createMockSendCommandFn()
-      .mockResponse('Page.getResourceTree', {frameTree: {frame: {id: 1337}}})
+      .mockResponse('Page.getResourceTree', {frameTree: {frame: {id: '1337'}}})
       .mockResponse('Page.createIsolatedWorld', {executionContextId: 9001})
       .mockResponse('Runtime.evaluate', Promise.reject(new Error('Cannot find context')))
-      .mockResponse('Page.getResourceTree', {frameTree: {frame: {id: 1337}}})
+      .mockResponse('Page.getResourceTree', {frameTree: {frame: {id: '1337'}}})
       .mockResponse('Page.createIsolatedWorld', {executionContextId: 9002})
       .mockResponse('Runtime.evaluate', {result: {value: 'mocked value'}});
 
@@ -271,6 +330,7 @@ describe('.evaluateAsync', () => {
 describe('.sendCommand', () => {
   it('.sendCommand timesout when commands take too long', async () => {
     const mockTimeout = 5000;
+    // @ts-ignore
     connectionStub.sendCommand = jest.fn()
       .mockImplementation(() => new Promise(r => setTimeout(r, mockTimeout)));
 
@@ -294,8 +354,8 @@ describe('.beginTrace', () => {
   beforeEach(() => {
     connectionStub.sendCommand = createMockSendCommandFn()
       .mockResponse('Browser.getVersion', protocolGetVersionResponse)
-      .mockResponse('Page.enable', {})
-      .mockResponse('Tracing.start', {});
+      .mockResponse('Page.enable')
+      .mockResponse('Tracing.start');
   });
 
   it('will request default traceCategories', async () => {
@@ -320,8 +380,8 @@ describe('.beginTrace', () => {
   it('will adjust traceCategories based on chrome version', async () => {
     connectionStub.sendCommand = createMockSendCommandFn()
       .mockResponse('Browser.getVersion', {product: 'Chrome/70.0.3577.0'})
-      .mockResponse('Page.enable', {})
-      .mockResponse('Tracing.start', {});
+      .mockResponse('Page.enable')
+      .mockResponse('Tracing.start');
 
     await driver.beginTrace();
 
@@ -334,10 +394,8 @@ describe('.beginTrace', () => {
 
 describe('.setExtraHTTPHeaders', () => {
   it('should Network.setExtraHTTPHeaders when there are extra-headers', async () => {
-    connectionStub.sendCommand = createMockSendCommandFn().mockResponse(
-      'Network.setExtraHTTPHeaders',
-      {}
-    );
+    connectionStub.sendCommand = createMockSendCommandFn()
+      .mockResponse('Network.setExtraHTTPHeaders');
 
     await driver.setExtraHTTPHeaders({
       'Cookie': 'monster',
@@ -351,10 +409,9 @@ describe('.setExtraHTTPHeaders', () => {
     );
   });
 
-  it('should Network.setExtraHTTPHeaders when there are extra-headers', async () => {
+  it('should not call Network.setExtraHTTPHeaders when there are not extra-headers', async () => {
     connectionStub.sendCommand = createMockSendCommandFn();
-    await driver.setExtraHTTPHeaders();
-
+    await driver.setExtraHTTPHeaders(null);
     expect(connectionStub.sendCommand).not.toHaveBeenCalled();
   });
 });
@@ -398,8 +455,8 @@ describe('.getAppManifest', () => {
 describe('.goOffline', () => {
   it('should send offline emulation', async () => {
     connectionStub.sendCommand = createMockSendCommandFn()
-      .mockResponse('Network.enable', {})
-      .mockResponse('Network.emulateNetworkConditions', {});
+      .mockResponse('Network.enable')
+      .mockResponse('Network.emulateNetworkConditions');
 
     await driver.goOffline();
     const emulateArgs = connectionStub.sendCommand
@@ -414,39 +471,21 @@ describe('.goOffline', () => {
 });
 
 describe('.gotoURL', () => {
-  function createMockWaitForFn() {
-    let resolve;
-    let reject;
-    const promise = new Promise((r1, r2) => {
-      resolve = r1;
-      reject = r2;
-    });
-
-    const mockCancelFn = jest.fn();
-    const mockFn = jest.fn().mockReturnValue({promise, cancel: mockCancelFn});
-
-    mockFn.mockResolve = () => resolve();
-    mockFn.mockReject = err => reject(err || new Error('Rejected'));
-    mockFn.getMockCancelFn = () => mockCancelFn;
-
-    return mockFn;
-  }
-
   beforeEach(() => {
     connectionStub.sendCommand = createMockSendCommandFn()
-      .mockResponse('Network.enable', {})
-      .mockResponse('Page.enable', {})
-      .mockResponse('Page.setLifecycleEventsEnabled', {})
-      .mockResponse('Emulation.setScriptExecutionDisabled', {})
-      .mockResponse('Page.navigate', {})
-      .mockResponse('Target.setAutoAttach', {})
-      .mockResponse('Runtime.evaluate', {});
+      .mockResponse('Network.enable')
+      .mockResponse('Page.enable')
+      .mockResponse('Page.setLifecycleEventsEnabled')
+      .mockResponse('Emulation.setScriptExecutionDisabled')
+      .mockResponse('Page.navigate')
+      .mockResponse('Target.setAutoAttach')
+      .mockResponse('Runtime.evaluate');
   });
 
   it('will track redirects through gotoURL load', async () => {
-    const delay = _ => new Promise(resolve => setTimeout(resolve));
+    const delay = () => new Promise(resolve => setTimeout(resolve));
 
-    class ReplayConnection extends EventEmitter {
+    class ReplayConnection extends Connection {
       connect() {
         return Promise.resolve();
       }
@@ -454,9 +493,13 @@ describe('.gotoURL', () => {
         return Promise.resolve();
       }
       replayLog() {
-        redirectDevtoolsLog.forEach(msg => this.emit('protocolevent', msg));
+        redirectDevtoolsLog.forEach(msg => this.emitProtocolEvent(msg));
       }
-      sendCommand(method) {
+      /**
+       * @param {string} method
+       * @param {any} _
+       */
+      sendCommand(method, _) {
         const resolve = Promise.resolve();
 
         // If navigating, wait, then replay devtools log in parallel to resolve.
@@ -468,7 +511,8 @@ describe('.gotoURL', () => {
       }
     }
     const replayConnection = new ReplayConnection();
-    const driver = new Driver(replayConnection);
+
+    const driver = /** @type {TestDriver} */ (new Driver(replayConnection));
 
     // Redirect in log will go through
     const startUrl = 'http://en.wikipedia.org/';
@@ -486,7 +530,6 @@ describe('.gotoURL', () => {
         },
       },
     };
-
     const loadPromise = driver.gotoURL(startUrl, loadOptions);
 
     await flushAllTimersAndMicrotasks();
@@ -527,6 +570,7 @@ describe('.gotoURL', () => {
         driver._waitForNetworkIdle = createMockWaitForFn();
         driver._waitForCPUIdle = createMockWaitForFn();
 
+        // @ts-ignore - dynamic property access, tests will definitely fail if the property were to change
         const waitForResult = driver[`_waitFor${name}`];
         const otherWaitForResults = [
           driver._waitForFCP,
@@ -685,23 +729,34 @@ describe('._waitForFCP', () => {
 describe('.assertNoSameOriginServiceWorkerClients', () => {
   beforeEach(() => {
     connectionStub.sendCommand = createMockSendCommandFn()
-      .mockResponse('ServiceWorker.enable', {})
-      .mockResponse('ServiceWorker.disable', {})
-      .mockResponse('ServiceWorker.enable', {})
-      .mockResponse('ServiceWorker.disable', {});
+      .mockResponse('ServiceWorker.enable')
+      .mockResponse('ServiceWorker.disable')
+      .mockResponse('ServiceWorker.enable')
+      .mockResponse('ServiceWorker.disable');
   });
 
+  /**
+   * @param {number} id
+   * @param {string} url
+   * @param {boolean=} isDeleted
+   */
   function createSWRegistration(id, url, isDeleted) {
     return {
       isDeleted: !!isDeleted,
-      registrationId: id,
+      registrationId: String(id),
       scopeURL: url,
     };
   }
 
+  /**
+   * @param {number} id
+   * @param {string} url
+   * @param {string[]} controlledClients
+   * @param {LH.Crdp.ServiceWorker.ServiceWorkerVersionStatus=} status
+   */
   function createActiveWorker(id, url, controlledClients, status = 'activated') {
     return {
-      registrationId: id,
+      registrationId: String(id),
       scriptURL: url,
       controlledClients,
       status,
@@ -804,9 +859,9 @@ describe('.assertNoSameOriginServiceWorkerClients', () => {
 describe('.goOnline', () => {
   beforeEach(() => {
     connectionStub.sendCommand = createMockSendCommandFn()
-      .mockResponse('Network.enable', {})
-      .mockResponse('Emulation.setCPUThrottlingRate', {})
-      .mockResponse('Network.emulateNetworkConditions', {});
+      .mockResponse('Network.enable')
+      .mockResponse('Emulation.setCPUThrottlingRate')
+      .mockResponse('Network.emulateNetworkConditions');
   });
 
   it('re-establishes previous throttling settings', async () => {
@@ -882,22 +937,22 @@ describe('Domain.enable/disable State', () => {
       .mockResponse('Fetch.enable')
       .mockResponse('Fetch.disable');
 
-    await driver.sendCommand('Network.enable', {});
-    await driver.sendCommand('Network.enable', {});
+    await driver.sendCommand('Network.enable');
+    await driver.sendCommand('Network.enable');
     expect(connectionStub.sendCommand).toHaveBeenCalledTimes(1);
 
-    await driver.sendCommand('Network.disable', {});
+    await driver.sendCommand('Network.disable');
     expect(connectionStub.sendCommand).toHaveBeenCalledTimes(1);
     // Network still has one enable.
 
-    await driver.sendCommand('Fetch.enable', {});
+    await driver.sendCommand('Fetch.enable');
     expect(connectionStub.sendCommand).toHaveBeenCalledTimes(2);
 
-    await driver.sendCommand('Network.disable', {});
+    await driver.sendCommand('Network.disable');
     expect(connectionStub.sendCommand).toHaveBeenCalledTimes(3);
     // Network is now disabled.
 
-    await driver.sendCommand('Fetch.disable', {});
+    await driver.sendCommand('Fetch.disable');
     expect(connectionStub.sendCommand).toHaveBeenCalledTimes(4);
   });
 
@@ -908,24 +963,24 @@ describe('Domain.enable/disable State', () => {
       .mockResponse('Network.disable')
       .mockResponseToSession('Network.disable', '123');
 
-    await driver.sendCommand('Network.enable', {});
-    await driver.sendCommandToSession('Network.enable', '123', {});
+    await driver.sendCommand('Network.enable');
+    await driver.sendCommandToSession('Network.enable', '123');
     expect(connectionStub.sendCommand).toHaveBeenCalledTimes(2);
 
-    await driver.sendCommand('Network.enable', {});
-    await driver.sendCommandToSession('Network.enable', '123', {});
+    await driver.sendCommand('Network.enable');
+    await driver.sendCommandToSession('Network.enable', '123');
     expect(connectionStub.sendCommand).toHaveBeenCalledTimes(2);
 
-    await driver.sendCommandToSession('Network.disable', '123', {});
+    await driver.sendCommandToSession('Network.disable', '123');
     expect(connectionStub.sendCommand).toHaveBeenCalledTimes(2);
 
-    await driver.sendCommand('Network.disable', {});
+    await driver.sendCommand('Network.disable');
     expect(connectionStub.sendCommand).toHaveBeenCalledTimes(2);
 
-    await driver.sendCommandToSession('Network.disable', '123', {});
+    await driver.sendCommandToSession('Network.disable', '123');
     expect(connectionStub.sendCommand).toHaveBeenCalledTimes(3);
 
-    await driver.sendCommand('Network.disable', {});
+    await driver.sendCommand('Network.disable');
     expect(connectionStub.sendCommand).toHaveBeenCalledTimes(4);
   });
 });
@@ -933,12 +988,13 @@ describe('Domain.enable/disable State', () => {
 describe('Multi-target management', () => {
   it('enables the Network domain for iframes', async () => {
     connectionStub.sendCommand = createMockSendCommandFn()
-      .mockResponseToSession('Network.enable', '123', {})
-      .mockResponseToSession('Target.setAutoAttach', '123', {})
-      .mockResponseToSession('Runtime.runIfWaitingForDebugger', '123', {});
+      .mockResponseToSession('Network.enable', '123')
+      .mockResponseToSession('Target.setAutoAttach', '123')
+      .mockResponseToSession('Runtime.runIfWaitingForDebugger', '123');
 
     driver._eventEmitter.emit('Target.attachedToTarget', {
       sessionId: '123',
+      // @ts-ignore: Ignore partial targetInfo.
       targetInfo: {type: 'iframe'},
     });
     await flushAllTimersAndMicrotasks();
@@ -952,10 +1008,11 @@ describe('Multi-target management', () => {
 
   it('ignores other target types, but still resumes them', async () => {
     connectionStub.sendCommand = createMockSendCommandFn()
-      .mockResponse('Target.sendMessageToTarget', {});
+      .mockResponse('Target.sendMessageToTarget');
 
     driver._eventEmitter.emit('Target.attachedToTarget', {
       sessionId: 'SW1',
+      // @ts-ignore: Ignore partial targetInfo.
       targetInfo: {type: 'service_worker'},
     });
     await flushAllTimersAndMicrotasks();
diff --git a/lighthouse-core/test/gather/fake-driver.js b/lighthouse-core/test/gather/fake-driver.js
index 108a40aa6..f41b055ca 100644
--- a/lighthouse-core/test/gather/fake-driver.js
+++ b/lighthouse-core/test/gather/fake-driver.js
@@ -5,6 +5,9 @@
  */
 'use strict';
 
+/**
+ * @param {{protocolGetVersionResponse: LH.CrdpCommands['Browser.getVersion']['returnType']}} param0
+ */
 function makeFakeDriver({protocolGetVersionResponse}) {
   let scrollPosition = {x: 0, y: 0};
 
@@ -59,6 +62,7 @@ function makeFakeDriver({protocolGetVersionResponse}) {
     evaluateAsync() {
       return Promise.resolve({});
     },
+    /** @param {{x: number, y: number}} position */
     scrollTo(position) {
       scrollPosition = position;
       return Promise.resolve();
@@ -73,13 +77,15 @@ function makeFakeDriver({protocolGetVersionResponse}) {
       return Promise.resolve();
     },
     endTrace() {
-      return Promise.resolve(
-      require('../fixtures/traces/progressive-app.json')
-      );
+      // Minimal indirection so TypeScript doesn't crash trying to infer a type.
+      const modulePath = '../fixtures/traces/progressive-app.json';
+      return Promise.resolve(require(modulePath));
     },
     beginDevtoolsLog() {},
     endDevtoolsLog() {
-      return require('../fixtures/artifacts/perflog/defaultPass.devtoolslog.json');
+      // Minimal indirection so TypeScript doesn't crash trying to infer a type.
+      const modulePath = '../fixtures/artifacts/perflog/defaultPass.devtoolslog.json';
+      return require(modulePath);
     },
     blockUrlPatterns() {
       return Promise.resolve();
@@ -109,6 +115,8 @@ const fakeDriverUsingRealMobileDevice = makeFakeDriver({
   },
 });
 
-module.exports = fakeDriver;
-module.exports.fakeDriverUsingRealMobileDevice = fakeDriverUsingRealMobileDevice;
-module.exports.protocolGetVersionResponse = protocolGetVersionResponse;
+module.exports = {
+  ...fakeDriver,
+  fakeDriverUsingRealMobileDevice,
+  protocolGetVersionResponse,
+};
diff --git a/lighthouse-core/test/gather/gather-runner-test.js b/lighthouse-core/test/gather/gather-runner-test.js
index 74e0ea855..78dd8380b 100644
--- a/lighthouse-core/test/gather/gather-runner-test.js
+++ b/lighthouse-core/test/gather/gather-runner-test.js
@@ -15,6 +15,9 @@ const unresolvedPerfLog = require('./../fixtures/unresolved-perflog.json');
 const NetworkRequest = require('../../lib/network-request.js');
 const LHError = require('../../lib/lh-error.js');
 const networkRecordsToDevtoolsLog = require('../network-records-to-devtools-log.js');
+const Driver = require('../../gather/driver.js');
+const Connection = require('../../gather/connections/connection.js');
+const {createMockSendCommandFn} = require('./mock-commands.js');
 
 jest.mock('../../lib/stack-collector.js', () => () => Promise.resolve([]));
 
@@ -36,62 +39,55 @@ class TestGathererNoArtifact extends Gatherer {
   afterPass() {}
 }
 
+class EmulationDriver extends Driver {
+  enableRuntimeEvents() {
+    return Promise.resolve();
+  }
+  enableAsyncStacks() {
+    return Promise.resolve();
+  }
+  assertNoSameOriginServiceWorkerClients() {
+    return Promise.resolve();
+  }
+  cacheNatives() {
+    return Promise.resolve();
+  }
+  registerPerformanceObserver() {
+    return Promise.resolve();
+  }
+  cleanBrowserCaches() {}
+  clearDataForOrigin() {}
+}
+
 const fakeDriver = require('./fake-driver.js');
 const fakeDriverUsingRealMobileDevice = fakeDriver.fakeDriverUsingRealMobileDevice;
 
-// TODO: Refactor this to use gather/mock-commands.js
-function getMockedEmulationDriver(emulationFn, netThrottleFn, cpuThrottleFn,
-  blockUrlFn, extraHeadersFn) {
-  const Driver = require('../../gather/driver.js');
-  const Connection = require('../../gather/connections/connection.js');
-  const EmulationDriver = class extends Driver {
-    enableRuntimeEvents() {
-      return Promise.resolve();
-    }
-    enableAsyncStacks() {
-      return Promise.resolve();
-    }
-    assertNoSameOriginServiceWorkerClients() {
-      return Promise.resolve();
-    }
-    cacheNatives() {
-      return Promise.resolve();
-    }
-    registerPerformanceObserver() {
-      return Promise.resolve();
-    }
-    cleanBrowserCaches() {}
-    clearDataForOrigin() {}
-  };
-  const EmulationMock = class extends Connection {
-    sendCommand(command, sessionId, params) {
-      let fn = null;
-      switch (command) {
-        case 'Network.emulateNetworkConditions':
-          fn = netThrottleFn;
-          break;
-        case 'Emulation.setCPUThrottlingRate':
-          fn = cpuThrottleFn;
-          break;
-        case 'Emulation.setDeviceMetricsOverride':
-          fn = emulationFn;
-          break;
-        case 'Network.setBlockedURLs':
-          fn = blockUrlFn;
-          break;
-        case 'Network.setExtraHTTPHeaders':
-          fn = extraHeadersFn;
-          break;
-        default:
-          fn = null;
-          break;
-      }
-      return Promise.resolve(fn && fn(params));
-    }
-  };
-  return new EmulationDriver(new EmulationMock());
+let driver;
+let connectionStub;
+
+function resetDefaultMockResponses() {
+  connectionStub.sendCommand = createMockSendCommandFn()
+    .mockResponse('Emulation.setCPUThrottlingRate')
+    .mockResponse('Emulation.setDeviceMetricsOverride')
+    .mockResponse('Emulation.setTouchEmulationEnabled')
+    .mockResponse('Network.emulateNetworkConditions')
+    .mockResponse('Network.enable')
+    .mockResponse('Network.setBlockedURLs')
+    .mockResponse('Network.setExtraHTTPHeaders')
+    .mockResponse('Network.setUserAgentOverride')
+    .mockResponse('Page.enable')
+    .mockResponse('ServiceWorker.enable');
 }
 
+beforeEach(() => {
+  connectionStub = new Connection();
+  connectionStub.sendCommand = cmd => {
+    throw new Error(`${cmd} not implemented`);
+  };
+  driver = new EmulationDriver(connectionStub);
+  resetDefaultMockResponses();
+});
+
 describe('GatherRunner', function() {
   it('loads a page and updates passContext.URL on redirect', () => {
     const url1 = 'https://example.com';
@@ -267,110 +263,46 @@ describe('GatherRunner', function() {
     test('works when running on desktop device', DESKTOP_UA, 'desktop');
   });
 
-  it('sets up the driver to begin emulation when all emulation flags are undefined', () => {
-    const tests = {
-      calledDeviceEmulation: false,
-      calledNetworkEmulation: false,
-      calledCpuEmulation: false,
-    };
-    const createEmulationCheck = variable => (arg) => {
-      tests[variable] = arg;
-
-      return true;
-    };
-    const driver = getMockedEmulationDriver(
-      createEmulationCheck('calledDeviceEmulation'),
-      createEmulationCheck('calledNetworkEmulation'),
-      createEmulationCheck('calledCpuEmulation')
-    );
-
-    return GatherRunner.setupDriver(driver, {
+  it('sets up the driver to begin emulation when all flags are undefined', async () => {
+    await GatherRunner.setupDriver(driver, {
       settings: {
         emulatedFormFactor: 'mobile',
+        throttlingMethod: 'provided',
         internalDisableDeviceScreenEmulation: false,
       },
-    }).then(_ => {
-      assert.ok(tests.calledDeviceEmulation, 'did not call device emulation');
-      assert.deepEqual(tests.calledNetworkEmulation, {
-        latency: 0, downloadThroughput: 0, uploadThroughput: 0, offline: false,
-      });
-      assert.ok(!tests.calledCpuEmulation, 'called cpu emulation');
     });
+
+    connectionStub.sendCommand.findInvocation('Emulation.setDeviceMetricsOverride');
+    expect(connectionStub.sendCommand.findInvocation('Network.emulateNetworkConditions')).toEqual({
+      latency: 0, downloadThroughput: 0, uploadThroughput: 0, offline: false,
+    });
+    expect(() =>
+      connectionStub.sendCommand.findInvocation('Emulation.setCPUThrottlingRate')).toThrow();
   });
 
   it('applies the correct emulation given a particular emulationFormFactor', async () => {
-    const emulationFn = jest.fn();
-    const driver = getMockedEmulationDriver(
-      emulationFn,
-      () => true,
-      () => true
-    );
-
     const getSettings = formFactor => ({
       emulatedFormFactor: formFactor,
       internalDisableDeviceScreenEmulation: false,
     });
 
     await GatherRunner.setupDriver(driver, {settings: getSettings('mobile')});
-    expect(emulationFn.mock.calls.slice(-1)[0][0]).toMatchObject({mobile: true});
+    expect(connectionStub.sendCommand.findInvocation('Emulation.setDeviceMetricsOverride'))
+      .toMatchObject({mobile: true});
 
+    resetDefaultMockResponses();
     await GatherRunner.setupDriver(driver, {settings: getSettings('desktop')});
-    expect(emulationFn.mock.calls.slice(-1)[0][0]).toMatchObject({mobile: false});
+    expect(connectionStub.sendCommand.findInvocation('Emulation.setDeviceMetricsOverride'))
+      .toMatchObject({mobile: false});
 
-    emulationFn.mockClear();
+    resetDefaultMockResponses();
     await GatherRunner.setupDriver(driver, {settings: getSettings('none')});
-    expect(emulationFn).not.toHaveBeenCalled();
+    expect(() =>
+      connectionStub.sendCommand.findInvocation('Emulation.setDeviceMetricsOverride')).toThrow();
   });
 
-  it('stops throttling when not devtools', () => {
-    const tests = {
-      calledDeviceEmulation: false,
-      calledNetworkEmulation: false,
-      calledCpuEmulation: false,
-    };
-    const createEmulationCheck = variable => (...args) => {
-      tests[variable] = args;
-      return true;
-    };
-    const driver = getMockedEmulationDriver(
-      createEmulationCheck('calledDeviceEmulation'),
-      createEmulationCheck('calledNetworkEmulation'),
-      createEmulationCheck('calledCpuEmulation')
-    );
-
-    return GatherRunner.setupDriver(driver, {
-      settings: {
-        emulatedFormFactor: 'mobile',
-        throttlingMethod: 'provided',
-        internalDisableDeviceScreenEmulation: false,
-      },
-    }).then(_ => {
-      assert.ok(tests.calledDeviceEmulation, 'did not call device emulation');
-      assert.deepEqual(tests.calledNetworkEmulation, [{
-        latency: 0, downloadThroughput: 0, uploadThroughput: 0, offline: false,
-      }]);
-      assert.ok(!tests.calledCpuEmulation, 'called CPU emulation');
-    });
-  });
-
-  it('sets throttling according to settings', () => {
-    const tests = {
-      calledDeviceEmulation: false,
-      calledNetworkEmulation: false,
-      calledCpuEmulation: false,
-    };
-    const createEmulationCheck = variable => (...args) => {
-      tests[variable] = args;
-
-      return true;
-    };
-    const driver = getMockedEmulationDriver(
-      createEmulationCheck('calledDeviceEmulation'),
-      createEmulationCheck('calledNetworkEmulation'),
-      createEmulationCheck('calledCpuEmulation')
-    );
-
-    return GatherRunner.setupDriver(driver, {
+  it('sets throttling according to settings', async () => {
+    await GatherRunner.setupDriver(driver, {
       settings: {
         emulatedFormFactor: 'mobile', internalDisableDeviceScreenEmulation: false,
         throttlingMethod: 'devtools',
@@ -381,12 +313,14 @@ describe('GatherRunner', function() {
           cpuSlowdownMultiplier: 1,
         },
       },
-    }).then(_ => {
-      assert.ok(tests.calledDeviceEmulation, 'did not call device emulation');
-      assert.deepEqual(tests.calledNetworkEmulation, [{
-        latency: 100, downloadThroughput: 1024, uploadThroughput: 1024, offline: false,
-      }]);
-      assert.deepEqual(tests.calledCpuEmulation, [{rate: 1}]);
+    });
+
+    connectionStub.sendCommand.findInvocation('Emulation.setDeviceMetricsOverride');
+    expect(connectionStub.sendCommand.findInvocation('Network.emulateNetworkConditions')).toEqual({
+      latency: 100, downloadThroughput: 1024, uploadThroughput: 1024, offline: false,
+    });
+    expect(connectionStub.sendCommand.findInvocation('Emulation.setCPUThrottlingRate')).toEqual({
+      rate: 1,
     });
   });
 
@@ -604,13 +538,8 @@ describe('GatherRunner', function() {
     });
   });
 
-  it('tells the driver to block given URL patterns when blockedUrlPatterns is given', () => {
-    let receivedUrlPatterns = null;
-    const driver = getMockedEmulationDriver(null, null, null, params => {
-      receivedUrlPatterns = params.urls;
-    });
-
-    return GatherRunner.setupPassNetwork({
+  it('tells the driver to block given URL patterns when blockedUrlPatterns is given', async () => {
+    await GatherRunner.setupPassNetwork({
       driver,
       settings: {
         blockedUrlPatterns: ['http://*.evil.com', '.jpg', '.woff2'],
@@ -619,46 +548,41 @@ describe('GatherRunner', function() {
         blockedUrlPatterns: ['*.jpeg'],
         gatherers: [],
       },
-    }).then(() => assert.deepStrictEqual(
-      receivedUrlPatterns.sort(),
-      ['*.jpeg', '.jpg', '.woff2', 'http://*.evil.com']
-    ));
-  });
-
-  it('does not throw when blockedUrlPatterns is not given', () => {
-    let receivedUrlPatterns = null;
-    const driver = getMockedEmulationDriver(null, null, null, params => {
-      receivedUrlPatterns = params.urls;
     });
 
-    return GatherRunner.setupPassNetwork({
+    const blockedUrlsResult = connectionStub.sendCommand.findInvocation('Network.setBlockedURLs');
+    blockedUrlsResult.urls.sort();
+    expect(blockedUrlsResult)
+      .toEqual({urls: ['*.jpeg', '.jpg', '.woff2', 'http://*.evil.com']});
+  });
+
+  it('does not throw when blockedUrlPatterns is not given', async () => {
+    await GatherRunner.setupPassNetwork({
       driver,
       settings: {},
       passConfig: {gatherers: []},
-    }).then(() => assert.deepStrictEqual(receivedUrlPatterns, []));
-  });
+    });
 
+    expect(connectionStub.sendCommand.findInvocation('Network.setBlockedURLs'))
+      .toEqual({urls: []});
+  });
 
-  it('tells the driver to set additional http headers when extraHeaders flag is given', () => {
-    let receivedHeaders = null;
-    const driver = getMockedEmulationDriver(null, null, null, null, params => {
-      receivedHeaders = params.headers;
-    });
-    const headers = {
+  it('tells the driver to set additional http when extraHeaders flag is given', async () => {
+    const extraHeaders = {
       'Cookie': 'monster',
       'x-men': 'wolverine',
     };
 
-    return GatherRunner.setupPassNetwork({
+    await GatherRunner.setupPassNetwork({
       driver,
       settings: {
-        extraHeaders: headers,
+        extraHeaders,
       },
       passConfig: {gatherers: []},
-    }).then(() => assert.deepStrictEqual(
-        receivedHeaders,
-        headers
-      ));
+    });
+
+    expect(connectionStub.sendCommand.findInvocation('Network.setExtraHTTPHeaders'))
+      .toEqual({headers: extraHeaders});
   });
 
   it('tells the driver to begin tracing', async () => {
diff --git a/lighthouse-core/test/gather/mock-commands.js b/lighthouse-core/test/gather/mock-commands.js
index 773935e04..80770e4ac 100644
--- a/lighthouse-core/test/gather/mock-commands.js
+++ b/lighthouse-core/test/gather/mock-commands.js
@@ -11,6 +11,16 @@
 
 /* eslint-env jest */
 
+/**
+ * @template {keyof LH.CrdpCommands} C
+ * @typedef {((...args: LH.CrdpCommands[C]['paramsType']) => MockResponse<C>) | RecursivePartial<LH.CrdpCommands[C]['returnType']> | Promise<Error>} MockResponse
+ */
+
+/**
+ * @template {keyof LH.CrdpEvents} E
+ * @typedef {RecursivePartial<LH.CrdpEvents[E][0]>} MockEvent
+ */
+
 /**
  * Creates a jest mock function whose implementation consumes mocked protocol responses matching the
  * requested command in the order they were mocked.
@@ -21,33 +31,65 @@
  *      returns the protocol message argument.
  */
 function createMockSendCommandFn() {
+  /**
+   * Typescript fails to equate template type `C` here with `C` when pushing to this array.
+   * Instead of sprinkling a couple ts-ignores, make `command` be any, but leave `C` for just
+   * documentation purposes. This is an internal type, so it doesn't matter much.
+   * @template {keyof LH.CrdpCommands} C
+   * @type {Array<{command: C|any, sessionId?: string, response?: MockResponse<C>, delay?: number}>}
+   */
   const mockResponses = [];
-  const mockFn = jest.fn().mockImplementation((command, sessionId, ...args) => {
-    const indexOfResponse = mockResponses
-      .findIndex(entry => entry.command === command && entry.sessionId === sessionId);
-    if (indexOfResponse === -1) throw new Error(`${command} unimplemented`);
-    const {response, delay} = mockResponses[indexOfResponse];
-    mockResponses.splice(indexOfResponse, 1);
-    const returnValue = typeof response === 'function' ? response(...args) : response;
-    if (delay) return new Promise(resolve => setTimeout(() => resolve(returnValue), delay));
-    return Promise.resolve(returnValue);
+  const mockFnImpl = jest.fn().mockImplementation(
+    /**
+     * @template {keyof LH.CrdpCommands} C
+     * @param {C} command
+     * @param {string|undefined=} sessionId
+     * @param {LH.CrdpCommands[C]['paramsType']} args
+     */
+    (command, sessionId, ...args) => {
+      const indexOfResponse = mockResponses
+        .findIndex(entry => entry.command === command && entry.sessionId === sessionId);
+      if (indexOfResponse === -1) throw new Error(`${command} unimplemented`);
+      const {response, delay} = mockResponses[indexOfResponse];
+      mockResponses.splice(indexOfResponse, 1);
+      const returnValue = typeof response === 'function' ? response(...args) : response;
+      if (delay) return new Promise(resolve => setTimeout(() => resolve(returnValue), delay));
+      // @ts-ignore: Some covariant type stuff doesn't work here. idk, I'm not a type scientist.
+      return Promise.resolve(returnValue);
+    });
+
+  const mockFn = Object.assign(mockFnImpl, {
+    /**
+     * @template {keyof LH.CrdpCommands} C
+     * @param {C} command
+     * @param {MockResponse<C>=} response
+     * @param {number=} delay
+     */
+    mockResponse(command, response, delay) {
+      mockResponses.push({command, response, delay});
+      return mockFn;
+    },
+    /**
+     * @template {keyof LH.CrdpCommands} C
+     * @param {C} command
+     * @param {string} sessionId
+     * @param {MockResponse<C>=} response
+     * @param {number=} delay
+     */
+    mockResponseToSession(command, sessionId, response, delay) {
+      mockResponses.push({command, sessionId, response, delay});
+      return mockFn;
+    },
+    /**
+     * @param {keyof LH.CrdpCommands} command
+     * @param {string=} sessionId
+     */
+    findInvocation(command, sessionId) {
+      expect(mockFn).toHaveBeenCalledWith(command, sessionId, expect.anything());
+      return mockFn.mock.calls.find(call => call[0] === command && call[1] === sessionId)[2];
+    },
   });
 
-  mockFn.mockResponse = (command, response, delay) => {
-    mockResponses.push({command, response, delay});
-    return mockFn;
-  };
-
-  mockFn.mockResponseToSession = (command, sessionId, response, delay) => {
-    mockResponses.push({command, sessionId, response, delay});
-    return mockFn;
-  };
-
-  mockFn.findInvocation = (command, sessionId) => {
-    expect(mockFn).toHaveBeenCalledWith(command, sessionId, expect.anything());
-    return mockFn.mock.calls.find(call => call[0] === command && call[1] === sessionId)[2];
-  };
-
   return mockFn;
 }
 
@@ -61,8 +103,12 @@ function createMockSendCommandFn() {
  *      returns the listener .
  */
 function createMockOnceFn() {
+  /**
+   * @template {keyof LH.CrdpEvents} E
+   * @type {Array<{event: E|any, response?: MockEvent<E>}>}
+   */
   const mockEvents = [];
-  const mockFn = jest.fn().mockImplementation((eventName, listener) => {
+  const mockFnImpl = jest.fn().mockImplementation((eventName, listener) => {
     const indexOfResponse = mockEvents.findIndex(entry => entry.event === eventName);
     if (indexOfResponse === -1) return;
     const {response} = mockEvents[indexOfResponse];
@@ -71,15 +117,24 @@ function createMockOnceFn() {
     setTimeout(() => listener(response), 0);
   });
 
-  mockFn.mockEvent = (event, response) => {
-    mockEvents.push({event, response});
-    return mockFn;
-  };
-
-  mockFn.findListener = event => {
-    expect(mockFn).toHaveBeenCalledWith(event, expect.anything());
-    return mockFn.mock.calls.find(call => call[0] === event)[1];
-  };
+  const mockFn = Object.assign(mockFnImpl, {
+    /**
+     * @template {keyof LH.CrdpEvents} E
+     * @param {E} event
+     * @param {MockEvent<E>} response
+     */
+    mockEvent(event, response) {
+      mockEvents.push({event, response});
+      return mockFn;
+    },
+    /**
+     * @param {keyof LH.CrdpEvents} event
+     */
+    findListener(event) {
+      expect(mockFn).toHaveBeenCalledWith(event, expect.anything());
+      return mockFn.mock.calls.find(call => call[0] === event)[1];
+    },
+  });
 
   return mockFn;
 }
@@ -89,8 +144,12 @@ function createMockOnceFn() {
  * So it's good for .on w/ many events.
  */
 function createMockOnFn() {
+  /**
+   * @template {keyof LH.CrdpEvents} E
+   * @type {Array<{event: E|any, response?: MockEvent<E>}>}
+   */
   const mockEvents = [];
-  const mockFn = jest.fn().mockImplementation((eventName, listener) => {
+  const mockFnImpl = jest.fn().mockImplementation((eventName, listener) => {
     const events = mockEvents.filter(entry => entry.event === eventName);
     if (!events.length) return;
     for (const event of events) {
@@ -105,15 +164,24 @@ function createMockOnFn() {
     }, 0);
   });
 
-  mockFn.mockEvent = (event, response) => {
-    mockEvents.push({event, response});
-    return mockFn;
-  };
-
-  mockFn.findListener = event => {
-    expect(mockFn).toHaveBeenCalledWith(event, expect.anything());
-    return mockFn.mock.calls.find(call => call[0] === event)[1];
-  };
+  const mockFn = Object.assign(mockFnImpl, {
+    /**
+     * @template {keyof LH.CrdpEvents} E
+     * @param {E} event
+     * @param {MockEvent<E>} response
+     */
+    mockEvent(event, response) {
+      mockEvents.push({event, response});
+      return mockFn;
+    },
+    /**
+     * @param {keyof LH.CrdpEvents} event
+     */
+    findListener(event) {
+      expect(mockFn).toHaveBeenCalledWith(event, expect.anything());
+      return mockFn.mock.calls.find(call => call[0] === event)[1];
+    },
+  });
 
   return mockFn;
 }