Repository: GoogleChrome/lighthouse
Pull request #12375 (2021-04-19 16:41:26)

core(fr): prepare emulation utilities for shared use
**Summary**
Broken out of #12366, just the emulation shared pieces. We already basically had this split in `lib/emulation.js` so it was just moving the last two methods that put it together :)

**Related Issues/PRs**
ref #11313 #12366



Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f1ae3f5b8..274773169 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -28,7 +28,7 @@ jobs:
       with:
         node-version: 12.x
 
-    - run: yarn --frozen-lockfile
+    - run: yarn install --frozen-lockfile --network-timeout 1000000
     - run: yarn build-all
 
     # Run tests that require headfull Chrome.
@@ -106,7 +106,7 @@ jobs:
         python -m pip install --upgrade pip
         pip install protobuf==3.7.1
 
-    - run: yarn --frozen-lockfile
+    - run: yarn install --frozen-lockfile --network-timeout 1000000
 
     - run: yarn test-proto # Run before unit-core because the roundtrip json is needed for proto tests.
 
@@ -172,7 +172,7 @@ jobs:
       working-directory: /home/runner
       run: bash $GITHUB_WORKSPACE/lighthouse-core/scripts/download-chrome.sh && mv chrome-linux chrome-linux-tot
 
-    - run: yarn --frozen-lockfile
+    - run: yarn install --frozen-lockfile --network-timeout 1000000
 
     - run: sudo apt-get install xvfb
     - name: Run smoke tests
@@ -204,7 +204,7 @@ jobs:
       with:
         node-version: 12.x
 
-    - run: yarn --frozen-lockfile
+    - run: yarn install --frozen-lockfile --network-timeout 1000000
 
     - name: Run smoke tests
       run: yarn smoke --debug -j=1 --retries=2 dbw oopif offline lantern metrics
@@ -222,7 +222,7 @@ jobs:
       with:
         node-version: 12.x
 
-    - run: yarn --frozen-lockfile
+    - run: yarn install --frozen-lockfile --network-timeout 1000000
     - run: yarn build-devtools
 
     - run: sudo apt-get install xvfb
diff --git a/lighthouse-core/gather/driver.js b/lighthouse-core/gather/driver.js
index 33a88fb4b..20bf40c22 100644
--- a/lighthouse-core/gather/driver.js
+++ b/lighthouse-core/gather/driver.js
@@ -8,7 +8,6 @@
 const Fetcher = require('./fetcher.js');
 const ExecutionContext = require('./driver/execution-context.js');
 const {waitForFullyLoaded, waitForFrameNavigated} = require('./driver/wait-for-condition.js');
-const emulation = require('../lib/emulation.js');
 const LHElement = require('../lib/lh-element.js');
 const LHError = require('../lib/lh-error.js');
 const NetworkRequest = require('../lib/network-request.js');
@@ -762,35 +761,6 @@ class Driver {
     await this.sendCommand('Debugger.setAsyncCallStackDepth', {maxDepth: 8});
   }
 
-  /**
-   * @param {LH.Config.Settings} settings
-   * @return {Promise<void>}
-   */
-  async beginEmulation(settings) {
-    await emulation.emulate(this, settings);
-    await this.setThrottling(settings, {useThrottling: true});
-  }
-
-  /**
-   * @param {LH.Config.Settings} settings
-   * @param {{useThrottling?: boolean}} passConfig
-   * @return {Promise<void>}
-   */
-  async setThrottling(settings, passConfig) {
-    if (settings.throttlingMethod !== 'devtools') {
-      return emulation.clearAllNetworkEmulation(this);
-    }
-
-    const cpuPromise = passConfig.useThrottling ?
-        emulation.enableCPUThrottling(this, settings.throttling) :
-        emulation.disableCPUThrottling(this);
-    const networkPromise = passConfig.useThrottling ?
-        emulation.enableNetworkThrottling(this, settings.throttling) :
-        emulation.clearAllNetworkEmulation(this);
-
-    await Promise.all([cpuPromise, networkPromise]);
-  }
-
   /**
    * Clear the network cache on disk and in memory.
    * @return {Promise<void>}
diff --git a/lighthouse-core/gather/gather-runner.js b/lighthouse-core/gather/gather-runner.js
index cca8a8e07..61da5387b 100644
--- a/lighthouse-core/gather/gather-runner.js
+++ b/lighthouse-core/gather/gather-runner.js
@@ -9,6 +9,7 @@ const log = require('lighthouse-logger');
 const LHError = require('../lib/lh-error.js');
 const NetworkAnalyzer = require('../lib/dependency-graph/simulator/network-analyzer.js');
 const NetworkRecorder = require('../lib/network-recorder.js');
+const emulation = require('../lib/emulation.js');
 const constants = require('../config/constants.js');
 const i18n = require('../lib/i18n/i18n.js');
 const URL = require('../lib/url-shim.js');
@@ -129,7 +130,7 @@ class GatherRunner {
     log.time(status);
     const resetStorage = !options.settings.disableStorageReset;
     await driver.assertNoSameOriginServiceWorkerClients(options.requestedUrl);
-    await driver.beginEmulation(options.settings);
+    await emulation.emulate(driver.defaultSession, options.settings);
     await driver.enableRuntimeEvents();
     await driver.enableAsyncStacks();
     await driver.cacheNatives();
@@ -344,8 +345,10 @@ class GatherRunner {
     const status = {msg: 'Setting up network for the pass trace', id: `lh:gather:setupPassNetwork`};
     log.time(status);
 
+    const session = passContext.driver.defaultSession;
     const passConfig = passContext.passConfig;
-    await passContext.driver.setThrottling(passContext.settings, passConfig);
+    if (passConfig.useThrottling) await emulation.throttle(session, passContext.settings);
+    else await emulation.clearThrottling(session);
 
     const blockedUrls = (passContext.passConfig.blockedUrlPatterns || [])
       .concat(passContext.settings.blockedUrlPatterns || []);
@@ -752,7 +755,7 @@ class GatherRunner {
     const loadData = await GatherRunner.endRecording(passContext);
 
     // Disable throttling so the afterPass analysis isn't throttled
-    await driver.setThrottling(passContext.settings, {useThrottling: false});
+    await emulation.clearThrottling(driver.defaultSession);
 
     // In case of load error, save log and trace with an error prefix, return no artifacts for this pass.
     const pageLoadError = GatherRunner.getPageLoadError(passContext, loadData, possibleNavError);
diff --git a/lighthouse-core/gather/gatherers/full-page-screenshot.js b/lighthouse-core/gather/gatherers/full-page-screenshot.js
index d52112306..6170f9297 100644
--- a/lighthouse-core/gather/gatherers/full-page-screenshot.js
+++ b/lighthouse-core/gather/gatherers/full-page-screenshot.js
@@ -8,6 +8,7 @@
 /* globals window document getBoundingClientRect */
 
 const Gatherer = require('./gatherer.js');
+const emulation = require('../../lib/emulation.js');
 const pageFunctions = require('../../lib/page-functions.js');
 
 /** @typedef {import('../driver.js')} Driver */
@@ -147,7 +148,7 @@ class FullPageScreenshot extends Gatherer {
     } finally {
       // Revert resized page.
       if (lighthouseControlsEmulation) {
-        await driver.beginEmulation(passContext.settings);
+        await emulation.emulate(driver.defaultSession, passContext.settings);
       } else {
         // Best effort to reset emulation to what it was.
         // https://github.com/GoogleChrome/lighthouse/pull/10716#discussion_r428970681
diff --git a/lighthouse-core/lib/emulation.js b/lighthouse-core/lib/emulation.js
index a96942bbf..798050849 100644
--- a/lighthouse-core/lib/emulation.js
+++ b/lighthouse-core/lib/emulation.js
@@ -5,8 +5,6 @@
  */
 'use strict';
 
-/** @typedef {import('../gather/driver.js')} Driver */
-
 const NO_THROTTLING_METRICS = {
   latency: 0,
   downloadThroughput: 0,
@@ -19,16 +17,15 @@ const NO_CPU_THROTTLE_METRICS = {
 };
 
 /**
- *
- * @param {Driver} driver
+ * @param {LH.Gatherer.FRProtocolSession} session
  * @param {LH.Config.Settings} settings
  * @return {Promise<void>}
  */
-async function emulate(driver, settings) {
+async function emulate(session, settings) {
   if (settings.emulatedUserAgent !== false) {
     // Network.enable must be called for UA overriding to work
-    await driver.sendCommand('Network.enable');
-    await driver.sendCommand('Network.setUserAgentOverride', {
+    await session.sendCommand('Network.enable');
+    await session.sendCommand('Network.setUserAgentOverride', {
       userAgent: /** @type {string} */ (settings.emulatedUserAgent),
     });
   }
@@ -36,20 +33,45 @@ async function emulate(driver, settings) {
   if (settings.screenEmulation.disabled !== true) {
     const {width, height, deviceScaleFactor, mobile} = settings.screenEmulation;
     const params = {width, height, deviceScaleFactor, mobile};
-    await driver.sendCommand('Emulation.setDeviceMetricsOverride', params);
-    await driver.sendCommand('Emulation.setTouchEmulationEnabled', {
+    await session.sendCommand('Emulation.setDeviceMetricsOverride', params);
+    await session.sendCommand('Emulation.setTouchEmulationEnabled', {
       enabled: params.mobile,
     });
   }
 }
 
+/**
+ * Sets the throttling options specified in config settings, clearing existing network throttling if
+ * throttlingMethod is not `devtools` (but not CPU throttling, suspected requirement of WPT-compat).
+ *
+ * @param {LH.Gatherer.FRProtocolSession} session
+ * @param {LH.Config.Settings} settings
+ * @return {Promise<void>}
+ */
+async function throttle(session, settings) {
+  // TODO(FR-COMPAT): reconsider if this should be resetting anything
+  if (settings.throttlingMethod !== 'devtools') return clearNetworkThrottling(session);
+
+  await Promise.all([
+    enableNetworkThrottling(session, settings.throttling),
+    enableCPUThrottling(session, settings.throttling),
+  ]);
+}
+
+/**
+ * @param {LH.Gatherer.FRProtocolSession} session
+ * @return {Promise<void>}
+ */
+async function clearThrottling(session) {
+  await Promise.all([clearNetworkThrottling(session), clearCPUThrottling(session)]);
+}
 
 /**
- * @param {Driver} driver
+ * @param {LH.Gatherer.FRProtocolSession} session
  * @param {Required<LH.ThrottlingSettings>} throttlingSettings
  * @return {Promise<void>}
  */
-function enableNetworkThrottling(driver, throttlingSettings) {
+function enableNetworkThrottling(session, throttlingSettings) {
   /** @type {LH.Crdp.Network.EmulateNetworkConditionsRequest} */
   const conditions = {
     offline: false,
@@ -61,39 +83,41 @@ function enableNetworkThrottling(driver, throttlingSettings) {
   // DevTools expects throughput in bytes per second rather than kbps
   conditions.downloadThroughput = Math.floor(conditions.downloadThroughput * 1024 / 8);
   conditions.uploadThroughput = Math.floor(conditions.uploadThroughput * 1024 / 8);
-  return driver.sendCommand('Network.emulateNetworkConditions', conditions);
+  return session.sendCommand('Network.emulateNetworkConditions', conditions);
 }
 
 /**
- * @param {Driver} driver
+ * @param {LH.Gatherer.FRProtocolSession} session
  * @return {Promise<void>}
  */
-function clearAllNetworkEmulation(driver) {
-  return driver.sendCommand('Network.emulateNetworkConditions', NO_THROTTLING_METRICS);
+function clearNetworkThrottling(session) {
+  return session.sendCommand('Network.emulateNetworkConditions', NO_THROTTLING_METRICS);
 }
 
 /**
- * @param {Driver} driver
+ * @param {LH.Gatherer.FRProtocolSession} session
  * @param {Required<LH.ThrottlingSettings>} throttlingSettings
  * @return {Promise<void>}
  */
-function enableCPUThrottling(driver, throttlingSettings) {
+function enableCPUThrottling(session, throttlingSettings) {
   const rate = throttlingSettings.cpuSlowdownMultiplier;
-  return driver.sendCommand('Emulation.setCPUThrottlingRate', {rate});
+  return session.sendCommand('Emulation.setCPUThrottlingRate', {rate});
 }
 
 /**
- * @param {Driver} driver
+ * @param {LH.Gatherer.FRProtocolSession} session
  * @return {Promise<void>}
  */
-function disableCPUThrottling(driver) {
-  return driver.sendCommand('Emulation.setCPUThrottlingRate', NO_CPU_THROTTLE_METRICS);
+function clearCPUThrottling(session) {
+  return session.sendCommand('Emulation.setCPUThrottlingRate', NO_CPU_THROTTLE_METRICS);
 }
 
 module.exports = {
   emulate,
+  throttle,
+  clearThrottling,
   enableNetworkThrottling,
-  clearAllNetworkEmulation,
+  clearNetworkThrottling,
   enableCPUThrottling,
-  disableCPUThrottling,
+  clearCPUThrottling,
 };
diff --git a/third-party/snyk/snapshot.json b/third-party/snyk/snapshot.json
index 4f40f148e..dec2c138e 100644
--- a/third-party/snyk/snapshot.json
+++ b/third-party/snyk/snapshot.json
@@ -45,7 +45,7 @@
       {"id":"npm:bootstrap:20120510","severity":"medium","semver":{"vulnerable":["<2.1.0"]}}
     ],
     "dojo":[
-      {"id":"SNYK-JS-DOJO-559224","severity":"medium","semver":{"vulnerable":["<1.11.10",">=1.12.0 <1.12.8",">=1.13.0 <1.13.7",">=1.14.0 <1.14.6",">=1.15.0 <1.15.3",">=1.16.0 <1.16.2"]}},
+      {"id":"SNYK-JS-DOJO-559224","severity":"medium","semver":{"vulnerable":["<1.11.10",">=1.12.1 <1.12.8",">=1.13.0 <1.13.7",">=1.14.0 <1.14.6",">=1.15.0 <1.15.3",">=1.16.0 <1.16.2"]}},
       {"id":"SNYK-JS-DOJO-174934","severity":"medium","semver":{"vulnerable":[">=1.0.0 <1.0.3",">=1.1.0 <1.1.2",">=1.2.0 <1.2.4",">=1.3.0 <1.3.3",">=1.4.0 <1.4.2"]}},
       {"id":"SNYK-JS-DOJO-174933","severity":"medium","semver":{"vulnerable":["<1.2.0"]}},
       {"id":"SNYK-JS-DOJO-72305","severity":"medium","semver":{"vulnerable":[">=1.14.0-pre <1.14.0",">=1.13.0 <1.13.1",">=1.12.1 <1.12.4",">=1.11.0-rc1 <1.11.6","<1.10.10"]}},
@@ -85,7 +85,7 @@
     "jquery":[
       {"id":"SNYK-JS-JQUERY-569619","severity":"medium","semver":{"vulnerable":["<1.9.0"]}},
       {"id":"SNYK-JS-JQUERY-567880","severity":"medium","semver":{"vulnerable":[">=1.2.0 <3.5.0"]}},
-      {"id":"SNYK-JS-JQUERY-565129","severity":"medium","semver":{"vulnerable":[">=1.0.3 <3.5.0"]}},
+      {"id":"SNYK-JS-JQUERY-565129","severity":"medium","semver":{"vulnerable":[">=1.5.1 <3.5.0"]}},
       {"id":"SNYK-JS-JQUERY-174006","severity":"medium","semver":{"vulnerable":["<3.4.0"]}},
       {"id":"npm:jquery:20160529","severity":"low","semver":{"vulnerable":[">=3.0.0-rc1 <3.0.0"]}},
       {"id":"npm:jquery:20150627","severity":"medium","semver":{"vulnerable":["<1.12.2",">=1.12.3 <2.2.2",">=2.2.3 <3.0.0"]}},


Test patch:
diff --git a/lighthouse-core/test/gather/fake-driver.js b/lighthouse-core/test/gather/fake-driver.js
index 555464093..46c49c354 100644
--- a/lighthouse-core/test/gather/fake-driver.js
+++ b/lighthouse-core/test/gather/fake-driver.js
@@ -36,12 +36,6 @@ function makeFakeDriver({protocolGetVersionResponse}) {
     gotoURL(url) {
       return Promise.resolve({finalUrl: url, timedOut: false});
     },
-    beginEmulation() {
-      return Promise.resolve();
-    },
-    setThrottling() {
-      return Promise.resolve();
-    },
     dismissJavaScriptDialogs() {
       return Promise.resolve();
     },
diff --git a/lighthouse-core/test/gather/gather-runner-test.js b/lighthouse-core/test/gather/gather-runner-test.js
index 463667cfe..974c250c6 100644
--- a/lighthouse-core/test/gather/gather-runner-test.js
+++ b/lighthouse-core/test/gather/gather-runner-test.js
@@ -129,6 +129,17 @@ function resetDefaultMockResponses() {
     .mockResponse('ServiceWorker.enable');
 }
 
+/**
+ * Restore the emulation to its original implementation for testing emulation-sensitive logic.
+ */
+function restoreActualEmulation() {
+  const actualEmulation = jest.requireActual('../../lib/emulation.js');
+  const emulation = require('../../lib/emulation.js');
+  emulation.emulate = actualEmulation.emulate;
+  emulation.throttle = actualEmulation.throttle;
+  emulation.clearThrottling = actualEmulation.clearThrottling;
+}
+
 beforeEach(() => {
   // @ts-expect-error - connectionStub has a mocked version of sendCommand implemented in each test
   connectionStub = new Connection();
@@ -138,6 +149,11 @@ beforeEach(() => {
   };
   driver = new EmulationDriver(connectionStub);
   resetDefaultMockResponses();
+
+  const emulation = require('../../lib/emulation.js');
+  emulation.emulate = jest.fn();
+  emulation.throttle = jest.fn();
+  emulation.clearThrottling = jest.fn();
 });
 
 describe('GatherRunner', function() {
@@ -275,17 +291,14 @@ describe('GatherRunner', function() {
   });
 
   it('sets up the driver to begin emulation when all flags are undefined', async () => {
+    restoreActualEmulation();
     await GatherRunner.setupDriver(driver, {settings: getSettings('mobile')});
 
     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 formFactor', async () => {
+    restoreActualEmulation();
     await GatherRunner.setupDriver(driver, {settings: getSettings('mobile')});
     expect(connectionStub.sendCommand.findInvocation('Emulation.setDeviceMetricsOverride'))
       .toMatchObject({mobile: true});
@@ -296,30 +309,6 @@ describe('GatherRunner', function() {
       .toMatchObject({mobile: false});
   });
 
-  it('sets throttling according to settings', async () => {
-    await GatherRunner.setupDriver(driver, {
-      settings: {
-        formFactor: 'mobile',
-        screenEmulation: constants.screenEmulationMetrics.mobile,
-        throttlingMethod: 'devtools',
-        throttling: {
-          requestLatencyMs: 100,
-          downloadThroughputKbps: 8,
-          uploadThroughputKbps: 8,
-          cpuSlowdownMultiplier: 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,
-    });
-  });
-
   it('clears origin storage', () => {
     const asyncFunc = () => Promise.resolve();
     /** @type {Record<string, boolean>} */
@@ -334,8 +323,6 @@ describe('GatherRunner', function() {
     };
     const driver = {
       assertNoSameOriginServiceWorkerClients: asyncFunc,
-      beginEmulation: asyncFunc,
-      setThrottling: asyncFunc,
       dismissJavaScriptDialogs: asyncFunc,
       enableRuntimeEvents: asyncFunc,
       enableAsyncStacks: asyncFunc,
@@ -371,7 +358,6 @@ describe('GatherRunner', function() {
       beginTrace: asyncFunc,
       gotoURL: async () => ({}),
       cleanBrowserCaches: createCheck('calledCleanBrowserCaches'),
-      setThrottling: asyncFunc,
       blockUrlPatterns: asyncFunc,
       setExtraHTTPHeaders: asyncFunc,
       endTrace: asyncFunc,
@@ -525,8 +511,6 @@ describe('GatherRunner', function() {
     };
     const driver = {
       assertNoSameOriginServiceWorkerClients: asyncFunc,
-      beginEmulation: asyncFunc,
-      setThrottling: asyncFunc,
       dismissJavaScriptDialogs: asyncFunc,
       enableRuntimeEvents: asyncFunc,
       enableAsyncStacks: asyncFunc,
@@ -548,6 +532,64 @@ describe('GatherRunner', function() {
     });
   });
 
+  it('sets throttling appropriately', async () => {
+    restoreActualEmulation();
+    await GatherRunner.setupPassNetwork({
+      driver,
+      settings: {
+        formFactor: 'mobile',
+        screenEmulation: constants.screenEmulationMetrics.mobile,
+        throttlingMethod: 'devtools',
+        throttling: {
+          requestLatencyMs: 100,
+          downloadThroughputKbps: 8,
+          uploadThroughputKbps: 8,
+          cpuSlowdownMultiplier: 2,
+        },
+      },
+      passConfig: {
+        useThrottling: true,
+        gatherers: [],
+      },
+    });
+
+    expect(connectionStub.sendCommand.findInvocation('Network.emulateNetworkConditions')).toEqual({
+      latency: 100, downloadThroughput: 1024, uploadThroughput: 1024, offline: false,
+    });
+    expect(connectionStub.sendCommand.findInvocation('Emulation.setCPUThrottlingRate')).toEqual({
+      rate: 2,
+    });
+  });
+
+  it('clears throttling when useThrottling=false', async () => {
+    restoreActualEmulation();
+    await GatherRunner.setupPassNetwork({
+      driver,
+      settings: {
+        formFactor: 'mobile',
+        screenEmulation: constants.screenEmulationMetrics.mobile,
+        throttlingMethod: 'devtools',
+        throttling: {
+          requestLatencyMs: 100,
+          downloadThroughputKbps: 8,
+          uploadThroughputKbps: 8,
+          cpuSlowdownMultiplier: 2,
+        },
+      },
+      passConfig: {
+        useThrottling: false,
+        gatherers: [],
+      },
+    });
+
+    expect(connectionStub.sendCommand.findInvocation('Network.emulateNetworkConditions')).toEqual({
+      latency: 0, downloadThroughput: 0, uploadThroughput: 0, offline: false,
+    });
+    expect(connectionStub.sendCommand.findInvocation('Emulation.setCPUThrottlingRate')).toEqual({
+      rate: 1,
+    });
+  });
+
   it('tells the driver to block given URL patterns when blockedUrlPatterns is given', async () => {
     await GatherRunner.setupPassNetwork({
       driver,
diff --git a/lighthouse-core/test/gather/gatherers/full-page-screenshot-test.js b/lighthouse-core/test/gather/gatherers/full-page-screenshot-test.js
index 9aa32015e..d5eda847c 100644
--- a/lighthouse-core/test/gather/gatherers/full-page-screenshot-test.js
+++ b/lighthouse-core/test/gather/gatherers/full-page-screenshot-test.js
@@ -7,6 +7,9 @@
 
 /* eslint-env jest */
 
+let mockEmulate = jest.fn();
+jest.mock('../../../lib/emulation.js', () => ({emulate: (...args) => mockEmulate(...args)}));
+
 const FullPageScreenshotGatherer = require('../../../gather/gatherers/full-page-screenshot.js');
 
 // Headless's default value is (1024 * 16), but this varies by device
@@ -16,6 +19,21 @@ const maxTextureSizeMock = 1024 * 8;
  * @param {{contentSize: {width: number, height: number}, screenSize: {width?: number, height?: number, dpr: number}, screenshotData: string[]}}
  */
 function createMockDriver({contentSize, screenSize, screenshotData}) {
+  const sendCommand = jest.fn().mockImplementation(method => {
+    if (method === 'Page.getLayoutMetrics') {
+      return {
+        contentSize,
+        // See comment within _takeScreenshot() implementation
+        layoutViewport: {clientWidth: contentSize.width, clientHeight: contentSize.height},
+      };
+    }
+    if (method === 'Page.captureScreenshot') {
+      return {
+        data: screenshotData && screenshotData.length ? screenshotData.shift() : 'abc',
+      };
+    }
+  });
+
   return {
     executionContext: {
       evaluate: async function(fn) {
@@ -40,25 +58,16 @@ function createMockDriver({contentSize, screenSize, screenshotData}) {
         }
       },
     },
-    beginEmulation: jest.fn(),
-    sendCommand: jest.fn().mockImplementation(method => {
-      if (method === 'Page.getLayoutMetrics') {
-        return {
-          contentSize,
-          // See comment within _takeScreenshot() implementation
-          layoutViewport: {clientWidth: contentSize.width, clientHeight: contentSize.height},
-        };
-      }
-      if (method === 'Page.captureScreenshot') {
-        return {
-          data: screenshotData && screenshotData.length ? screenshotData.shift() : 'abc',
-        };
-      }
-    }),
+    sendCommand,
+    defaultSession: {sendCommand},
   };
 }
 
 describe('FullPageScreenshot gatherer', () => {
+  beforeEach(() => {
+    mockEmulate = jest.fn();
+  });
+
   it('captures a full-page screenshot', async () => {
     const fpsGatherer = new FullPageScreenshotGatherer();
     const driver = createMockDriver({
@@ -113,8 +122,8 @@ describe('FullPageScreenshot gatherer', () => {
     await fpsGatherer.afterPass(passContext);
 
     const expectedArgs = {formFactor: 'mobile', screenEmulation: {disabled: false, mobile: true}};
-    expect(driver.beginEmulation).toHaveBeenCalledWith(expectedArgs);
-    expect(driver.beginEmulation).toHaveBeenCalledTimes(1);
+    expect(mockEmulate).toHaveBeenCalledTimes(1);
+    expect(mockEmulate).toHaveBeenCalledWith(driver.defaultSession, expectedArgs);
   });
 
   it('resets the emulation correctly when Lighthouse does not control it', async () => {
diff --git a/lighthouse-core/test/test-utils.js b/lighthouse-core/test/test-utils.js
index 7465e5191..2c9ea6dd3 100644
--- a/lighthouse-core/test/test-utils.js
+++ b/lighthouse-core/test/test-utils.js
@@ -235,6 +235,11 @@ function makeMocksForGatherRunner() {
   jest.mock('../gather/gatherers/web-app-manifest.js', () => ({
     getWebAppManifest: async () => null,
   }));
+  jest.mock('../lib/emulation.js', () => ({
+    emulate: jest.fn(),
+    throttle: jest.fn(),
+    clearThrottling: jest.fn(),
+  }));
 }
 
 module.exports = {