Repository: GoogleChrome/lighthouse
Pull request #7212 (2019-02-11 21:26:42)

tests: re-organize driver tests by method
**tl;dr - for EASIER REVIEWING just look at https://github.com/GoogleChrome/lighthouse/pull/7212/commits/877264306c5351120c3a79c69f4e2609a6a62d80**


**Summary**
It's no secret that driver tests have been somewhat of a mess for a long-time. Driver is one of the most critical pieces of Lighthouse logic and yet adding tests has always been intimidating and somewhat unconvincing that things are working. This PR is a first step in an attempt to change that.

**The Changes**

- Organizes all the tests by the method. Already much easier to see where we have coverage and where we're lacking.
- Minor renaming of driverStub -> driver. The driver here is a real driver, its the connection and sendCommand functions that are stubs.
- Moves to a more readable format where each method stubs the `sendCommand` methods that it actually uses. This should hopefully make it much easier and less magical for contributors to understand what each method on driver requires, how to add tests that use new protocol methods, etc. A nice side effect is decent documentation about what each protocol method will actually do.

![image](https://user-images.githubusercontent.com/2301202/52594336-ece07c00-2e10-11e9-9963-cc93d7fd898b.png)


**Related Issues/PRs**
#7174,  didn't want to proceed with more major gotoURL without the promised cleanup and testing first



Patch:
diff --git a/.travis.yml b/.travis.yml
index bfd347b23..b42b9cebf 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,6 +8,8 @@ matrix:
     - node_js: "8"
     - node_js: "10"
       if: head_branch IS blank AND branch = master
+    - node_js: "11"
+      if: head_branch IS blank AND branch = master
 dist: trusty
 cache:
   yarn: true
diff --git a/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js b/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js
index 792cd8c84..c3337b04e 100644
--- a/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js
+++ b/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js
@@ -66,15 +66,26 @@ class UnusedBytes extends Audit {
    * @param {LH.Artifacts.NetworkRequest=} networkRecord
    * @param {number} totalBytes Uncompressed size of the resource
    * @param {LH.Crdp.Page.ResourceType=} resourceType
-   * @param {number=} compressionRatio
    * @return {number}
    */
-  static estimateTransferSize(networkRecord, totalBytes, resourceType, compressionRatio = 0.5) {
+  static estimateTransferSize(networkRecord, totalBytes, resourceType) {
     if (!networkRecord) {
       // We don't know how many bytes this asset used on the network, but we can guess it was
       // roughly the size of the content gzipped.
-      // See https://discuss.httparchive.org/t/file-size-and-compression-savings/145 for multipliers
-      return Math.round(totalBytes * compressionRatio);
+      // See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/optimize-encoding-and-transfer for specific CSS/Script examples
+      // See https://discuss.httparchive.org/t/file-size-and-compression-savings/145 for fallback multipliers
+      switch (resourceType) {
+        case 'Stylesheet':
+          // Stylesheets tend to compress extremely well.
+          return Math.round(totalBytes * 0.2);
+        case 'Script':
+        case 'Document':
+          // Scripts and HTML compress fairly well too.
+          return Math.round(totalBytes * 0.33);
+        default:
+          // Otherwise we'll just fallback to the average savings in HTTPArchive
+          return Math.round(totalBytes * 0.5);
+      }
     } else if (networkRecord.resourceType === resourceType) {
       // This was a regular standalone asset, just use the transfer size.
       return networkRecord.transferSize || 0;
diff --git a/lighthouse-core/audits/byte-efficiency/total-byte-weight.js b/lighthouse-core/audits/byte-efficiency/total-byte-weight.js
index 3cfc199c5..542b726e8 100644
--- a/lighthouse-core/audits/byte-efficiency/total-byte-weight.js
+++ b/lighthouse-core/audits/byte-efficiency/total-byte-weight.js
@@ -78,7 +78,10 @@ class TotalByteWeight extends ByteEfficiencyAudit {
       results.push(result);
     });
     const totalCompletedRequests = results.length;
-    results = results.sort((itemA, itemB) => itemB.totalBytes - itemA.totalBytes).slice(0, 10);
+    results = results.sort((itemA, itemB) => {
+      return itemB.totalBytes - itemA.totalBytes ||
+        itemA.url.localeCompare(itemB.url);
+    }).slice(0, 10);
 
     const score = ByteEfficiencyAudit.computeLogNormalScore(
       totalBytes,
diff --git a/lighthouse-core/audits/byte-efficiency/uses-long-cache-ttl.js b/lighthouse-core/audits/byte-efficiency/uses-long-cache-ttl.js
index 4fd21e9f3..13346411a 100644
--- a/lighthouse-core/audits/byte-efficiency/uses-long-cache-ttl.js
+++ b/lighthouse-core/audits/byte-efficiency/uses-long-cache-ttl.js
@@ -257,9 +257,11 @@ class CacheHeaders extends Audit {
         });
       }
 
-      results.sort(
-        (a, b) => a.cacheLifetimeMs - b.cacheLifetimeMs || b.totalBytes - a.totalBytes
-      );
+      results.sort((a, b) => {
+        return a.cacheLifetimeMs - b.cacheLifetimeMs ||
+          b.totalBytes - a.totalBytes ||
+          a.url.localeCompare(b.url);
+      });
 
       const score = Audit.computeLogNormalScore(
         totalWastedBytes,
diff --git a/lighthouse-core/audits/font-display.js b/lighthouse-core/audits/font-display.js
index 2a1ce2609..53953b73c 100644
--- a/lighthouse-core/audits/font-display.js
+++ b/lighthouse-core/audits/font-display.js
@@ -7,7 +7,7 @@
 
 const Audit = require('./audit');
 const URL = require('../lib/url-shim').URL;
-const PASSING_FONT_DISPLAY_REGEX = /block|fallback|optional|swap/;
+const PASSING_FONT_DISPLAY_REGEX = /^(block|fallback|optional|swap)$/;
 const CSS_URL_REGEX = /url\((.*?)\)/;
 const CSS_URL_GLOBAL_REGEX = new RegExp(CSS_URL_REGEX, 'g');
 const i18n = require('../lib/i18n/i18n.js');
@@ -57,11 +57,13 @@ class FontDisplay extends Audit {
       const fontFaceDeclarations = newlinesStripped.match(/@font-face\s*{(.*?)}/g) || [];
       // Go through all the @font-face declarations to find a declared `font-display: ` property
       for (const declaration of fontFaceDeclarations) {
-        const rawFontDisplay = declaration.match(/font-display:(.*?);/);
+        // Find the font-display value by matching a single token, optionally surrounded by whitespace,
+        // followed either by a semicolon or the end of a block.
+        const rawFontDisplay = declaration.match(/font-display\s*:\s*(\w+)\s*(;|\})/);
         // If they didn't have a font-display property, it's the default, and it's failing; bail
         if (!rawFontDisplay) continue;
         // If they don't have one of the passing font-display values, it's failing; bail
-        const hasPassingFontDisplay = PASSING_FONT_DISPLAY_REGEX.test(rawFontDisplay[0]);
+        const hasPassingFontDisplay = PASSING_FONT_DISPLAY_REGEX.test(rawFontDisplay[1]);
         if (!hasPassingFontDisplay) continue;
 
         // If it's passing, we'll try to find the URL it's referencing.
diff --git a/lighthouse-core/audits/seo/manual/mobile-friendly.js b/lighthouse-core/audits/seo/manual/mobile-friendly.js
deleted file mode 100644
index 402416a47..000000000
--- a/lighthouse-core/audits/seo/manual/mobile-friendly.js
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * @license Copyright 2017 Google Inc. All Rights Reserved.
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- */
-'use strict';
-
-const ManualAudit = require('../../manual/manual-audit');
-
-/**
- * @fileoverview Manual SEO audit to check if page is mobile friendly.
- */
-
-class MobileFriendly extends ManualAudit {
-  /**
-   * @return {LH.Audit.Meta}
-   */
-  static get meta() {
-    return Object.assign({
-      id: 'mobile-friendly',
-      description: 'Take the [Mobile-Friendly Test](https://search.google.com/test/mobile-friendly) to check for audits not covered by Lighthouse, like sizing tap targets appropriately. [Learn more](https://developers.google.com/search/mobile-sites/).',
-      title: 'Page is mobile friendly',
-    }, super.partialMeta);
-  }
-}
-
-module.exports = MobileFriendly;
diff --git a/lighthouse-core/config/default-config.js b/lighthouse-core/config/default-config.js
index 5a8370e57..78f30a9b2 100644
--- a/lighthouse-core/config/default-config.js
+++ b/lighthouse-core/config/default-config.js
@@ -256,7 +256,6 @@ const defaultConfig = {
     'seo/hreflang',
     'seo/plugins',
     'seo/canonical',
-    'seo/manual/mobile-friendly',
     'seo/manual/structured-data',
   ],
 
@@ -466,7 +465,6 @@ const defaultConfig = {
         {id: 'plugins', weight: 1, group: 'seo-content'},
         {id: 'tap-targets', weight: 1, group: 'seo-mobile'},
         // Manual audits
-        {id: 'mobile-friendly', weight: 0},
         {id: 'structured-data', weight: 0},
       ],
     },
diff --git a/lighthouse-core/lib/emulation.js b/lighthouse-core/lib/emulation.js
index 73a443747..b4b8ba2c9 100644
--- a/lighthouse-core/lib/emulation.js
+++ b/lighthouse-core/lib/emulation.js
@@ -40,12 +40,12 @@ const DESKTOP_EMULATION_METRICS = {
 
 const NEXUS5X_USERAGENT = {
   // eslint-disable-next-line max-len
-  userAgent: 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3559.0 Mobile Safari/537.36',
+  userAgent: 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3694.0 Mobile Safari/537.36',
 };
 
 const DESKTOP_USERAGENT = {
   // eslint-disable-next-line max-len
-  userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3559.0 Safari/537.36',
+  userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3694.0 Safari/537.36',
 };
 
 const OFFLINE_METRICS = {
diff --git a/proto/sample_v2_round_trip.json b/proto/sample_v2_round_trip.json
index f365ce24c..865e6e180 100644
--- a/proto/sample_v2_round_trip.json
+++ b/proto/sample_v2_round_trip.json
@@ -1606,13 +1606,6 @@
             "scoreDisplayMode": "informative", 
             "title": "Metrics"
         }, 
-        "mobile-friendly": {
-            "description": "Take the [Mobile-Friendly Test](https://search.google.com/test/mobile-friendly) to check for audits not covered by Lighthouse, like sizing tap targets appropriately. [Learn more](https://developers.google.com/search/mobile-sites/).", 
-            "id": "mobile-friendly", 
-            "score": null, 
-            "scoreDisplayMode": "manual", 
-            "title": "Page is mobile friendly"
-        }, 
         "network-requests": {
             "description": "Lists the network requests that were made during page load.", 
             "details": {
@@ -2467,15 +2460,15 @@
                     }, 
                     {
                         "totalBytes": 821.0, 
-                        "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=2200"
+                        "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=100"
                     }, 
                     {
                         "totalBytes": 821.0, 
-                        "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=3000&async=true"
+                        "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=2000&async=true"
                     }, 
                     {
                         "totalBytes": 821.0, 
-                        "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=2000&async=true"
+                        "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=2200"
                     }
                 ], 
                 "type": "table"
@@ -2713,35 +2706,35 @@
                         "cacheHitProbability": 0.0, 
                         "cacheLifetimeMs": 0.0, 
                         "totalBytes": 821.0, 
-                        "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=3000&async=true", 
+                        "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=100", 
                         "wastedBytes": 821.0
                     }, 
                     {
                         "cacheHitProbability": 0.0, 
                         "cacheLifetimeMs": 0.0, 
                         "totalBytes": 821.0, 
-                        "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=100", 
+                        "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=2000&async=true", 
                         "wastedBytes": 821.0
                     }, 
                     {
                         "cacheHitProbability": 0.0, 
                         "cacheLifetimeMs": 0.0, 
                         "totalBytes": 821.0, 
-                        "url": "http://localhost:10200/dobetterweb/dbw_tester.css?scriptActivated&delay=200", 
+                        "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=2200", 
                         "wastedBytes": 821.0
                     }, 
                     {
                         "cacheHitProbability": 0.0, 
                         "cacheLifetimeMs": 0.0, 
                         "totalBytes": 821.0, 
-                        "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=2200", 
+                        "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=3000&async=true", 
                         "wastedBytes": 821.0
                     }, 
                     {
                         "cacheHitProbability": 0.0, 
                         "cacheLifetimeMs": 0.0, 
                         "totalBytes": 821.0, 
-                        "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=2000&async=true", 
+                        "url": "http://localhost:10200/dobetterweb/dbw_tester.css?scriptActivated&delay=200", 
                         "wastedBytes": 821.0
                     }, 
                     {
@@ -3622,10 +3615,6 @@
                     "id": "tap-targets", 
                     "weight": 1.0
                 }, 
-                {
-                    "id": "mobile-friendly", 
-                    "weight": 0.0
-                }, 
                 {
                     "id": "structured-data", 
                     "weight": 0.0
@@ -4654,12 +4643,6 @@
                 "name": "lh:audit:canonical", 
                 "startTime": 0.0
             }, 
-            {
-                "duration": 100.0, 
-                "entryType": "measure", 
-                "name": "lh:audit:mobile-friendly", 
-                "startTime": 0.0
-            }, 
             {
                 "duration": 100.0, 
                 "entryType": "measure", 
diff --git a/third-party/snyk/snapshot.json b/third-party/snyk/snapshot.json
index 299aae90e..0c7924b62 100644
--- a/third-party/snyk/snapshot.json
+++ b/third-party/snyk/snapshot.json
@@ -52,7 +52,7 @@
       {"id":"npm:handlebars:20110425","severity":"medium","semver":{"vulnerable":["<=1.0.0-beta.3"]}}
     ],
     "highcharts":[
-      {"id":"npm:highcharts:20180225","severity":"low","semver":{"vulnerable":["<6.1.0"]}}
+      {"id":"npm:highcharts:20180225","severity":"high","semver":{"vulnerable":["<6.1.0"]}}
     ],
     "jquery":[
       {"id":"npm:jquery:20160529","severity":"low","semver":{"vulnerable":["=3.0.0-rc.1"]}},
@@ -81,7 +81,7 @@
     "moment":[
       {"id":"npm:moment:20170905","severity":"low","semver":{"vulnerable":["<2.19.3"]}},
       {"id":"npm:moment:20161019","severity":"medium","semver":{"vulnerable":["<2.15.2"]}},
-      {"id":"npm:moment:20160126","severity":"low","semver":{"vulnerable":["<=2.11.1"]}}
+      {"id":"npm:moment:20160126","severity":"medium","semver":{"vulnerable":["<=2.11.1"]}}
     ],
     "mustache":[
       {"id":"npm:mustache:20151207","severity":"medium","semver":{"vulnerable":["<2.2.1"]}},


Test patch:
diff --git a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
index 40666ee60..1f34fbe16 100644
--- a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
+++ b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
@@ -369,9 +369,6 @@ Object {
     Object {
       "path": "seo/canonical",
     },
-    Object {
-      "path": "seo/manual/mobile-friendly",
-    },
     Object {
       "path": "seo/manual/structured-data",
     },
@@ -989,10 +986,6 @@ Object {
           "id": "tap-targets",
           "weight": 1,
         },
-        Object {
-          "id": "mobile-friendly",
-          "weight": 0,
-        },
         Object {
           "id": "structured-data",
           "weight": 0,
diff --git a/lighthouse-cli/test/fixtures/byte-efficiency/tester.html b/lighthouse-cli/test/fixtures/byte-efficiency/tester.html
index e355ea648..4b617c90b 100644
--- a/lighthouse-cli/test/fixtures/byte-efficiency/tester.html
+++ b/lighthouse-cli/test/fixtures/byte-efficiency/tester.html
@@ -23,7 +23,7 @@ function generateInlineStyleWithSize(sizeInBytes, firstContent = '', used = fals
       div.classList.add(className);
       document.body.appendChild(div);
     }
-    sizeInBytes -= rule.length / 3; // 1 byte per character, GZip estimate is 3x
+    sizeInBytes -= rule.length * 0.2; // 1 byte per character, GZip estimate is 20% for Stylesheets
   }
 
   const style = document.createElement('style');
@@ -157,11 +157,11 @@ setTimeout(() => {
 // PASS: unused but too small savings
 generateInlineStyleWithSize(512, '.too-small { background: none; }\n');
 // PASS: used
-generateInlineStyleWithSize(4000, '.mostly-used { background: none; }\n', true);
-// PASSWARN: unused and a bit of savings
-generateInlineStyleWithSize(2000, '.kinda-unused { background: none; }\n');
+generateInlineStyleWithSize(5000, '.mostly-used { background: none; }\n', true);
+// FAIL: unused and a bit of savings
+generateInlineStyleWithSize(5000, '.kinda-unused { background: none; }\n');
 // FAIL: unused and lots of savings
-generateInlineStyleWithSize(24000, '.definitely-unused { background: none; }\n');
+generateInlineStyleWithSize(30000, '.definitely-unused { background: none; }\n');
 </script>
 
 <!-- Ensure the page takes at least 7 seconds and we don't exit before the lazily loaded image -->
diff --git a/lighthouse-cli/test/smokehouse/byte-efficiency/expectations.js b/lighthouse-cli/test/smokehouse/byte-efficiency/expectations.js
index d4a379638..d26e7cf15 100644
--- a/lighthouse-cli/test/smokehouse/byte-efficiency/expectations.js
+++ b/lighthouse-cli/test/smokehouse/byte-efficiency/expectations.js
@@ -17,7 +17,7 @@ module.exports = [
         details: {
           overallSavingsBytes: '>17000',
           items: {
-            length: 1,
+            length: 2,
           },
         },
       },
@@ -33,7 +33,7 @@ module.exports = [
       },
       'unused-css-rules': {
         details: {
-          overallSavingsBytes: '>39000',
+          overallSavingsBytes: '>35000',
           items: {
             length: 2,
           },
diff --git a/lighthouse-core/test/audits/byte-efficiency/byte-efficiency-audit-test.js b/lighthouse-core/test/audits/byte-efficiency/byte-efficiency-audit-test.js
index 135c661fb..d000649f6 100644
--- a/lighthouse-core/test/audits/byte-efficiency/byte-efficiency-audit-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/byte-efficiency-audit-test.js
@@ -53,9 +53,11 @@ describe('Byte efficiency base audit', () => {
   describe('#estimateTransferSize', () => {
     const estimate = ByteEfficiencyAudit.estimateTransferSize;
 
-    it('should estimate by compression ratio when no network record available', () => {
-      const result = estimate(undefined, 1000, '', 0.345);
-      assert.equal(result, 345);
+    it('should estimate by resource type compression ratio when no network info available', () => {
+      assert.equal(estimate(undefined, 1000, 'Stylesheet'), 200);
+      assert.equal(estimate(undefined, 1000, 'Script'), 330);
+      assert.equal(estimate(undefined, 1000, 'Document'), 330);
+      assert.equal(estimate(undefined, 1000, ''), 500);
     });
 
     it('should return transferSize when asset matches', () => {
diff --git a/lighthouse-core/test/audits/byte-efficiency/unused-css-rules-test.js b/lighthouse-core/test/audits/byte-efficiency/unused-css-rules-test.js
index 3a5304cf6..693ea43e7 100644
--- a/lighthouse-core/test/audits/byte-efficiency/unused-css-rules-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/unused-css-rules-test.js
@@ -166,8 +166,8 @@ describe('Best Practices: unused css rules audit', () => {
     it('fails when lots of rules are unused', () => {
       return UnusedCSSAudit.audit_(getArtifacts({
         CSSUsage: {rules: [
-          {styleSheetId: 'a', used: true, startOffset: 0, endOffset: 11}, // 44 * 1 / 4
-          {styleSheetId: 'b', used: true, startOffset: 0, endOffset: 6000}, // 4000 * 3 / 2
+          {styleSheetId: 'a', used: true, startOffset: 0, endOffset: 11}, // 44 * 25% = 11
+          {styleSheetId: 'b', used: true, startOffset: 0, endOffset: 60000}, // 40000 * 3 * 50% = 60000
         ], stylesheets: [
           {
             header: {styleSheetId: 'a', sourceURL: 'file://a.css'},
@@ -175,7 +175,7 @@ describe('Best Practices: unused css rules audit', () => {
           },
           {
             header: {styleSheetId: 'b', sourceURL: 'file://b.css'},
-            content: `${generate('123', 4000)}`,
+            content: `${generate('123', 40000)}`,
           },
           {
             header: {styleSheetId: 'c', sourceURL: ''},
@@ -185,7 +185,7 @@ describe('Best Practices: unused css rules audit', () => {
       }), networkRecords).then(result => {
         assert.equal(result.items.length, 2);
         assert.equal(result.items[0].totalBytes, 10 * 1024);
-        assert.equal(result.items[1].totalBytes, 6000);
+        assert.equal(result.items[1].totalBytes, 40000 * 3 * 0.2);
         assert.equal(result.items[0].wastedPercent, 75);
         assert.equal(result.items[1].wastedPercent, 50);
       });
diff --git a/lighthouse-core/test/audits/font-display-test.js b/lighthouse-core/test/audits/font-display-test.js
index 6a9dadd0d..0d20f4951 100644
--- a/lighthouse-core/test/audits/font-display-test.js
+++ b/lighthouse-core/test/audits/font-display-test.js
@@ -37,11 +37,13 @@ describe('Performance: Font Display audit', () => {
       }
 
       @font-face {
+        font-display: 'optional'; // invalid quotes, should still fail for being invalid rule
         /* try up a directory with ' */
         src: url('../font-b.woff');
       }
 
       @font-face {
+        font-display: optional // missing a semi-colon, should still fail for being invalid block
         /* try no path with no quotes ' */
         src: url(font.woff);
       }
@@ -72,32 +74,32 @@ describe('Performance: Font Display audit', () => {
       {url: networkRecords[2].url, wastedMs: 1000},
     ];
     assert.strictEqual(result.rawValue, false);
-    assert.deepEqual(result.details.items, items);
+    expect(result.details.items).toEqual(items);
   });
 
   it('resolves URLs relative to stylesheet URL when available', async () => {
     stylesheet.header.sourceURL = 'https://cdn.example.com/foo/bar/file.css';
     stylesheet.content = `
       @font-face {
-        font-display: 'block';
+        font-display: block;
         /* try with " and same directory */
         src: url("./font-a.woff");
       }
 
       @font-face {
-        font-display: 'block';
+        font-display: block;
         /* try with " and site origin */
         src: url("https://example.com/foo/bar/document-font.woff");
       }
 
       @font-face {
-        font-display: 'fallback';
+        font-display: fallback;
         /* try up a directory with ' */
         src: url('../font-b.woff');
       }
 
       @font-face {
-        font-display: 'optional';
+        font-display: optional;
         /* try no path with no quotes ' */
         src: url(font.woff);
       }
@@ -128,25 +130,25 @@ describe('Performance: Font Display audit', () => {
 
     const result = await FontDisplayAudit.audit(getArtifacts(), context);
     assert.strictEqual(result.rawValue, true);
-    assert.deepEqual(result.details.items, []);
+    expect(result.details.items).toEqual([]);
   });
 
   it('passes when all fonts have a correct font-display rule', async () => {
     stylesheet.content = `
       @font-face {
-        font-display: 'block';
+        font-display: block;
         /* try with " */
         src: url("./font-a.woff");
       }
 
       @font-face {
-        font-display: 'fallback';
+        font-display: fallback;
         /* try up a directory with ' */
         src: url('../font-b.woff');
       }
 
       @font-face {
-        font-display: 'optional';
+        font-display: optional;
         /* try no path with no quotes ' */
         src: url(font.woff);
       }
@@ -172,7 +174,7 @@ describe('Performance: Font Display audit', () => {
 
     const result = await FontDisplayAudit.audit(getArtifacts(), context);
     assert.strictEqual(result.rawValue, true);
-    assert.deepEqual(result.details.items, []);
+    expect(result.details.items).toEqual([]);
   });
 
   it('should handle real-world font-face declarations', async () => {
@@ -212,7 +214,7 @@ describe('Performance: Font Display audit', () => {
 
     const result = await FontDisplayAudit.audit(getArtifacts(), context);
     assert.strictEqual(result.rawValue, false);
-    assert.deepEqual(result.details.items.map(item => item.url), [
+    expect(result.details.items.map(item => item.url)).toEqual([
       'https://edition.i.cdn.cnn.com/.a/fonts/cnn/3.7.2/cnnclock-black.woff2',
       'https://registry.api.cnn.io/assets/fave/fonts/2.0.15/cnnsans-bold.woff',
       // FontAwesome should pass
@@ -220,4 +222,36 @@ describe('Performance: Font Display audit', () => {
       'https://fonts.gstatic.com/s/lato/v14/S6u9w4BMUTPHh50XSwiPGQ3q5d0.woff2',
     ]);
   });
+
+  it('handles varied font-display declarations', async () => {
+    stylesheet.content = `
+      @font-face {
+        src: url(font-0.woff);
+        font-display: swap
+      }
+
+      @font-face {src: url(font-1.woff);font-display   : fallback
+      }
+
+      @font-face {
+        src: url(font-2.woff);
+        font-display:optional}
+
+      @font-face {
+        src: url(font-3.woff);
+        font-display    :  swap  ;  }
+
+      @font-face {src: url(font-4.woff);font-display:swap;}
+    `;
+
+    networkRecords = Array.from({length: 5}).map((_, idx) => ({
+      url: `https://example.com/foo/bar/font-${idx}.woff`,
+      endTime: 2, startTime: 1,
+      resourceType: 'Font',
+    }));
+
+    const result = await FontDisplayAudit.audit(getArtifacts(), context);
+    expect(result.details.items).toEqual([]);
+    assert.strictEqual(result.rawValue, true);
+  });
 });
diff --git a/lighthouse-core/test/gather/driver-test.js b/lighthouse-core/test/gather/driver-test.js
index 9dc52c175..54d17fe2e 100644
--- a/lighthouse-core/test/gather/driver-test.js
+++ b/lighthouse-core/test/gather/driver-test.js
@@ -5,9 +5,6 @@
  */
 'use strict';
 
-let sendCommandParams = [];
-const sendCommandMockResponses = new Map();
-
 const Driver = require('../../gather/driver.js');
 const Connection = require('../../gather/connections/connection.js');
 const Element = require('../../lib/element.js');
@@ -30,37 +27,39 @@ function createOnceStub(events) {
   };
 }
 
-function createSWRegistration(id, url, isDeleted) {
-  return {
-    isDeleted: !!isDeleted,
-    registrationId: id,
-    scopeURL: url,
+/**
+ * Creates a jest mock function whose implementation consumes mocked protocol responses matching the
+ * requested command in the order they were mocked.
+ *
+ * It is decorated with two methods:
+ *    - `mockResponse` which pushes protocol message responses for consumption
+ *    - `findInvocation` which asserts that `sendCommand` was invoked with the given command and
+ *      returns the protocol message argument.
+ */
+function createMockSendCommandFn() {
+  const mockResponses = [];
+  const mockFn = jest.fn().mockImplementation(command => {
+    const indexOfResponse = mockResponses.findIndex(entry => entry.command === command);
+    if (indexOfResponse === -1) throw new Error(`${command} unimplemented`);
+    const {response} = mockResponses[indexOfResponse];
+    mockResponses.splice(indexOfResponse, 1);
+    return Promise.resolve(response);
+  });
+
+  mockFn.mockResponse = (command, response) => {
+    mockResponses.push({command, response});
+    return mockFn;
   };
-}
 
-function createActiveWorker(id, url, controlledClients, status = 'activated') {
-  return {
-    registrationId: id,
-    scriptURL: url,
-    controlledClients,
-    status,
+  mockFn.findInvocation = command => {
+    expect(mockFn).toHaveBeenCalledWith(command, expect.anything());
+    return mockFn.mock.calls.find(call => call[0] === command)[1];
   };
-}
 
-function createOnceMethodResponse(method, response) {
-  assert.equal(sendCommandMockResponses.has(method), false, 'stub response already defined');
-  sendCommandMockResponses.set(method, response);
+  return mockFn;
 }
 
-function sendCommandStub(command, params) {
-  sendCommandParams.push({command, params});
-
-  if (sendCommandMockResponses.has(command)) {
-    const mockResponse = sendCommandMockResponses.get(command);
-    sendCommandMockResponses.delete(command);
-    return Promise.resolve(mockResponse);
-  }
-
+function sendCommandOldStub(command, params) {
   switch (command) {
     case 'Browser.getVersion':
       return Promise.resolve(protocolGetVersionResponse);
@@ -83,9 +82,11 @@ function sendCommandStub(command, params) {
           value: {
             value: '123',
           },
-        }, {
-          name: 'novalue',
-        }],
+        },
+          {
+            name: 'novalue',
+          },
+        ],
       });
     case 'Page.getResourceTree':
       return Promise.resolve({frameTree: {frame: {id: 1}}});
@@ -116,97 +117,269 @@ function sendCommandStub(command, params) {
 
 /* eslint-env jest */
 
-let driverStub;
+let driver;
 let connectionStub;
+
 beforeEach(() => {
   connectionStub = new Connection();
-  connectionStub.sendCommand = sendCommandStub;
-  driverStub = new Driver(connectionStub);
+  connectionStub.sendCommand = sendCommandOldStub;
+  driver = new Driver(connectionStub);
 });
 
-describe('Browser Driver', () => {
-  beforeEach(() => {
-    sendCommandParams = [];
-    sendCommandMockResponses.clear();
-  });
-
+describe('.querySelector(All)', () => {
   it('returns null when DOM.querySelector finds no node', () => {
-    return driverStub.querySelector('invalid').then(value => {
+    return driver.querySelector('invalid').then(value => {
       assert.equal(value, null);
     });
   });
 
   it('returns element when DOM.querySelector finds node', () => {
-    return driverStub.querySelector('meta head').then(value => {
+    return driver.querySelector('meta head').then(value => {
       assert.equal(value instanceof Element, true);
     });
   });
 
   it('returns [] when DOM.querySelectorAll finds no node', () => {
-    return driverStub.querySelectorAll('invalid').then(value => {
+    return driver.querySelectorAll('invalid').then(value => {
       assert.deepEqual(value, []);
     });
   });
 
   it('returns element when DOM.querySelectorAll finds node', () => {
-    return driverStub.querySelectorAll('a').then(value => {
+    return driver.querySelectorAll('a').then(value => {
       assert.equal(value.length, 1);
       assert.equal(value[0] instanceof Element, true);
     });
   });
+});
 
+describe('.getObjectProperty', () => {
   it('returns value when getObjectProperty finds property name', () => {
-    return driverStub.getObjectProperty('test', 'test').then(value => {
+    return driver.getObjectProperty('test', 'test').then(value => {
       assert.deepEqual(value, 123);
     });
   });
 
   it('returns null when getObjectProperty finds no property name', () => {
-    return driverStub.getObjectProperty('invalid', 'invalid').then(value => {
+    return driver.getObjectProperty('invalid', 'invalid').then(value => {
       assert.deepEqual(value, null);
     });
   });
 
   it('returns null when getObjectProperty finds property name with no value', () => {
-    return driverStub.getObjectProperty('test', 'novalue').then(value => {
+    return driver.getObjectProperty('test', 'novalue').then(value => {
       assert.deepEqual(value, null);
     });
   });
+});
 
+describe('.getRequestContent', () => {
   it('throws if getRequestContent takes too long', () => {
-    return driverStub.getRequestContent('', MAX_WAIT_FOR_PROTOCOL).then(_ => {
-      assert.ok(false, 'long-running getRequestContent supposed to reject');
-    }, e => {
-      assert.equal(e.code, 'PROTOCOL_TIMEOUT');
-      expect(e.friendlyMessage).toBeDisplayString(
-        /^Waiting for DevTools.*Method: Network.getResponseBody/);
-    });
+    return driver.getRequestContent('', MAX_WAIT_FOR_PROTOCOL).then(
+      _ => {
+        assert.ok(false, 'long-running getRequestContent supposed to reject');
+      },
+      e => {
+        assert.equal(e.code, 'PROTOCOL_TIMEOUT');
+        expect(e.friendlyMessage).toBeDisplayString(
+          /^Waiting for DevTools.*Method: Network.getResponseBody/
+        );
+      }
+    );
   });
+});
 
-  it('evaluates an expression', () => {
-    return driverStub.evaluateAsync('120 + 3').then(value => {
-      assert.deepEqual(value, 123);
-      assert.equal(sendCommandParams[0].command, 'Runtime.evaluate');
+describe('.evaluateAsync', () => {
+  it('evaluates an expression', async () => {
+    connectionStub.sendCommand = createMockSendCommandFn()
+      .mockResponse('Runtime.evaluate', {result: {value: 2}});
+
+    const value = await driver.evaluateAsync('1 + 1');
+    expect(value).toEqual(2);
+    connectionStub.sendCommand.findInvocation('Runtime.evaluate');
+  });
+
+  it('evaluates an expression in isolation', async () => {
+    connectionStub.sendCommand = createMockSendCommandFn()
+      .mockResponse('Page.getResourceTree', {frameTree: {frame: {id: 1337}}})
+      .mockResponse('Page.createIsolatedWorld', {executionContextId: 1})
+      .mockResponse('Runtime.evaluate', {result: {value: 2}});
+
+    const value = await driver.evaluateAsync('1 + 1', {useIsolation: true});
+    expect(value).toEqual(2);
+
+    // Check that we used the correct frame when creating the isolated context
+    const createWorldArgs = connectionStub.sendCommand.findInvocation('Page.createIsolatedWorld');
+    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',
+      {result: {value: 2}}
+    );
+    await driver.evaluateAsync('1 + 1', {useIsolation: true});
+    expect(connectionStub.sendCommand).not.toHaveBeenCalledWith(
+      'Page.createIsolatedWorld',
+      expect.anything()
+    );
+  });
+});
+
+describe('.sendCommand', () => {
+  it('.sendCommand timesout when commands take too long', async () => {
+    class SlowConnection extends EventEmitter {
+      connect() {
+        return Promise.resolve();
+      }
+      disconnect() {
+        return Promise.resolve();
+      }
+      sendCommand() {
+        return new Promise(resolve => setTimeout(resolve, 15));
+      }
+    }
+    const slowConnection = new SlowConnection();
+    const driver = new Driver(slowConnection);
+    driver.setNextProtocolTimeout(25);
+    await driver.sendCommand('Page.enable');
+
+    driver.setNextProtocolTimeout(5);
+    try {
+      await driver.sendCommand('Page.disable');
+      assert.fail('expected driver.sendCommand to timeout');
+    } catch (err) {
+      assert.equal(err.code, 'PROTOCOL_TIMEOUT');
+    }
+  });
+});
+
+describe('.beginTrace', () => {
+  beforeEach(() => {
+    connectionStub.sendCommand = createMockSendCommandFn()
+      .mockResponse('Browser.getVersion', protocolGetVersionResponse)
+      .mockResponse('Page.enable', {})
+      .mockResponse('Tracing.start', {});
+  });
+
+  it('will request default traceCategories', async () => {
+    await driver.beginTrace();
+
+    const tracingStartArgs = connectionStub.sendCommand.findInvocation('Tracing.start');
+    expect(tracingStartArgs.categories).toContain('devtools.timeline');
+    expect(tracingStartArgs.categories).not.toContain('toplevel');
+    expect(tracingStartArgs.categories).toContain('disabled-by-default-lighthouse');
+  });
+
+  it('will use requested additionalTraceCategories', async () => {
+    await driver.beginTrace({additionalTraceCategories: 'loading,xtra_cat'});
+
+    const tracingStartArgs = connectionStub.sendCommand.findInvocation('Tracing.start');
+    expect(tracingStartArgs.categories).toContain('blink.user_timing');
+    expect(tracingStartArgs.categories).toContain('xtra_cat');
+    // Make sure it deduplicates categories too
+    expect(tracingStartArgs.categories).not.toMatch(/loading.*loading/);
+  });
+
+  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', {});
+
+    await driver.beginTrace();
+
+    const tracingStartArgs = connectionStub.sendCommand.findInvocation('Tracing.start');
+    // m70 doesn't have disabled-by-default-lighthouse, so 'toplevel' is used instead.
+    expect(tracingStartArgs.categories).toContain('toplevel');
+    expect(tracingStartArgs.categories).not.toContain('disabled-by-default-lighthouse');
+  });
+});
+
+describe('.setExtraHTTPHeaders', () => {
+  it('should Network.setExtraHTTPHeaders when there are extra-headers', async () => {
+    connectionStub.sendCommand = createMockSendCommandFn().mockResponse(
+      'Network.setExtraHTTPHeaders',
+      {}
+    );
+
+    await driver.setExtraHTTPHeaders({
+      'Cookie': 'monster',
+      'x-men': 'wolverine',
     });
+
+    expect(connectionStub.sendCommand).toHaveBeenCalledWith(
+      'Network.setExtraHTTPHeaders',
+      expect.anything()
+    );
   });
 
-  it('evaluates an expression in isolation', () => {
-    return driverStub.evaluateAsync('120 + 3', {useIsolation: true}).then(value => {
-      assert.deepEqual(value, 123);
+  it('should Network.setExtraHTTPHeaders when there are extra-headers', async () => {
+    connectionStub.sendCommand = createMockSendCommandFn();
+    await driver.setExtraHTTPHeaders();
+
+    expect(connectionStub.sendCommand).not.toHaveBeenCalled();
+  });
+});
 
-      assert.ok(sendCommandParams.length > 1, 'did not create execution context');
-      const evaluateCommand = sendCommandParams.find(data => data.command === 'Runtime.evaluate');
-      assert.equal(evaluateCommand.params.contextId, 1);
+describe('.getAppManifest', () => {
+  it('should return null when no manifest', async () => {
+    connectionStub.sendCommand = createMockSendCommandFn().mockResponse(
+      'Page.getAppManifest',
+      {data: undefined, url: '/manifest'}
+    );
+    const result = await driver.getAppManifest();
+    expect(result).toEqual(null);
+  });
 
-      // test repeat isolation evaluations
-      sendCommandParams = [];
-      return driverStub.evaluateAsync('120 + 3', {useIsolation: true});
-    }).then(value => {
-      assert.deepEqual(value, 123);
-      assert.ok(sendCommandParams.length === 1, 'created unnecessary 2nd execution context');
+  it('should return the manifest', async () => {
+    const manifest = {name: 'The App'};
+    connectionStub.sendCommand = createMockSendCommandFn().mockResponse(
+      'Page.getAppManifest',
+      {data: JSON.stringify(manifest), url: '/manifest'}
+    );
+    const result = await driver.getAppManifest();
+    expect(result).toEqual({data: JSON.stringify(manifest), url: '/manifest'});
+  });
+
+  it('should handle BOM-encoded manifest', async () => {
+    const fs = require('fs');
+    const manifestWithoutBOM = fs.readFileSync(__dirname + '/../fixtures/manifest.json').toString();
+    const manifestWithBOM = fs
+      .readFileSync(__dirname + '/../fixtures/manifest-bom.json')
+      .toString();
+
+    connectionStub.sendCommand = createMockSendCommandFn().mockResponse(
+      'Page.getAppManifest',
+      {data: manifestWithBOM, url: '/manifest'}
+    );
+    const result = await driver.getAppManifest();
+    expect(result).toEqual({data: manifestWithoutBOM, url: '/manifest'});
+  });
+});
+
+describe('.goOffline', () => {
+  it('should send offline emulation', async () => {
+    connectionStub.sendCommand = createMockSendCommandFn()
+      .mockResponse('Network.enable', {})
+      .mockResponse('Network.emulateNetworkConditions', {});
+
+    await driver.goOffline();
+    const emulateArgs = connectionStub.sendCommand
+      .findInvocation('Network.emulateNetworkConditions');
+    expect(emulateArgs).toEqual({
+      offline: true,
+      latency: 0,
+      downloadThroughput: 0,
+      uploadThroughput: 0,
     });
   });
+});
 
+describe('.gotoURL', () => {
   it('will track redirects through gotoURL load', () => {
     const delay = _ => new Promise(resolve => setTimeout(resolve));
 
@@ -254,135 +427,115 @@ describe('Browser Driver', () => {
     });
   });
 
-  it('.sendCommand timesout when commands take too long', async () => {
-    class SlowConnection extends EventEmitter {
-      connect() {
-        return Promise.resolve();
-      }
-      disconnect() {
-        return Promise.resolve();
-      }
-      sendCommand() {
-        return new Promise(resolve => setTimeout(resolve, 15));
-      }
-    }
-    const slowConnection = new SlowConnection();
-    const driver = new Driver(slowConnection);
-    driver.setNextProtocolTimeout(25);
-    await driver.sendCommand('Page.enable');
+  describe('when waitForNavigated', () => {});
 
-    driver.setNextProtocolTimeout(5);
-    try {
-      await driver.sendCommand('Page.disable');
-      assert.fail('expected driver.sendCommand to timeout');
-    } catch (err) {
-      assert.equal(err.code, 'PROTOCOL_TIMEOUT');
-    }
-  });
-
-  it('will request default traceCategories', () => {
-    return driverStub.beginTrace().then(() => {
-      const traceCmd = sendCommandParams.find(obj => obj.command === 'Tracing.start');
-      const categories = traceCmd.params.categories;
-      assert.ok(categories.includes('devtools.timeline'), 'contains devtools.timeline');
-    });
-  });
-
-  it('will use requested additionalTraceCategories', () => {
-    return driverStub.beginTrace({additionalTraceCategories: 'loading,xtra_cat'}).then(() => {
-      const traceCmd = sendCommandParams.find(obj => obj.command === 'Tracing.start');
-      const categories = traceCmd.params.categories;
-      assert.ok(categories.includes('blink.user_timing'), 'contains default categories');
-      assert.ok(categories.includes('xtra_cat'), 'contains added categories');
-      assert.ok(categories.indexOf('loading') === categories.lastIndexOf('loading'),
-          'de-dupes categories');
-    });
-  });
-
-  it('will adjust traceCategories based on chrome version', async () => {
-    // m70 doesn't have disabled-by-default-lighthouse, so 'toplevel' is used instead.
-    const m70ProtocolGetVersionResponse = Object.assign({}, protocolGetVersionResponse, {
-      product: 'Chrome/70.0.3577.0',
-      // eslint-disable-next-line max-len
-      userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3577.0 Safari/537.36',
-    });
-    createOnceMethodResponse('Browser.getVersion', m70ProtocolGetVersionResponse);
-
-    // eslint-disable-next-line max-len
-    await driverStub.beginTrace();
-    const traceCmd = sendCommandParams.find(obj => obj.command === 'Tracing.start');
-    const categories = traceCmd.params.categories;
-    assert.ok(categories.includes('toplevel'), 'contains old toplevel category');
-    assert.equal(categories.indexOf('disabled-by-default-lighthouse'), -1, 'excludes new cat');
-  });
-
-  it('should send the Network.setExtraHTTPHeaders command when there are extra-headers', () => {
-    return driverStub.setExtraHTTPHeaders({
-      'Cookie': 'monster',
-      'x-men': 'wolverine',
-    }).then(() => {
-      assert.equal(sendCommandParams[0].command, 'Network.setExtraHTTPHeaders');
-    });
-  });
+  describe('when waitForLoad', () => {
+    it('does not reject when page is secure', async () => {
+      const secureSecurityState = {
+        explanations: [],
+        securityState: 'secure',
+      };
+      driver.on = driver.once = createOnceStub({
+        'Security.securityStateChanged': secureSecurityState,
+        'Page.loadEventFired': {},
+        'Page.domContentEventFired': {},
+      });
 
-  it('should not send the Network.setExtraHTTPHeaders command when there no extra-headers', () => {
-    return driverStub.setExtraHTTPHeaders().then(() => {
-      assert.equal(sendCommandParams[0], undefined);
+      const startUrl = 'https://www.example.com';
+      const loadOptions = {
+        waitForLoad: true,
+        passContext: {
+          passConfig: {
+            networkQuietThresholdMs: 1,
+          },
+          settings: {
+            maxWaitForLoad: 1,
+          },
+        },
+      };
+      await driver.gotoURL(startUrl, loadOptions);
     });
-  });
 
-  describe('.getAppManifest', () => {
-    it('should return null when no manifest', async () => {
-      connectionStub.sendCommand = jest.fn()
-        .mockResolvedValueOnce({data: undefined, url: '/manifest'});
-      const result = await driverStub.getAppManifest();
-      expect(result).toEqual(null);
-    });
+    it('rejects when page is insecure', async () => {
+      const insecureSecurityState = {
+        explanations: [
+          {
+            description: 'reason 1.',
+            securityState: 'insecure',
+          },
+          {
+            description: 'blah.',
+            securityState: 'info',
+          },
+          {
+            description: 'reason 2.',
+            securityState: 'insecure',
+          },
+        ],
+        securityState: 'insecure',
+      };
+      driver.on = createOnceStub({
+        'Security.securityStateChanged': insecureSecurityState,
+      });
 
-    it('should return the manifest', async () => {
-      const manifest = {name: 'The App'};
-      connectionStub.sendCommand = jest.fn()
-        .mockResolvedValueOnce({data: JSON.stringify(manifest), url: '/manifest'});
-      const result = await driverStub.getAppManifest();
-      expect(result).toEqual({data: JSON.stringify(manifest), url: '/manifest'});
-    });
+      const startUrl = 'https://www.example.com';
+      const loadOptions = {
+        waitForLoad: true,
+        passContext: {
+          passConfig: {
+            networkQuietThresholdMs: 1,
+          },
+        },
+      };
 
-    it('should handle BOM-encoded manifest', async () => {
-      const fs = require('fs');
-      const manifestWithoutBOM = fs.readFileSync(__dirname + '/../fixtures/manifest.json')
-        .toString();
-      const manifestWithBOM = fs.readFileSync(__dirname + '/../fixtures/manifest-bom.json')
-        .toString();
-
-      connectionStub.sendCommand = jest.fn()
-        .mockResolvedValueOnce({data: manifestWithBOM, url: '/manifest'});
-      const result = await driverStub.getAppManifest();
-      expect(result).toEqual({data: manifestWithoutBOM, url: '/manifest'});
+      try {
+        await driver.gotoURL(startUrl, loadOptions);
+        assert.fail('security check should have rejected');
+      } catch (err) {
+        assert.equal(err.message, 'INSECURE_DOCUMENT_REQUEST');
+        assert.equal(err.code, 'INSECURE_DOCUMENT_REQUEST');
+        /* eslint-disable-next-line max-len */
+        expect(err.friendlyMessage).toBeDisplayString(
+          'The URL you have provided does not have valid security credentials. reason 1. reason 2.'
+        );
+      }
     });
   });
 });
 
-describe('Multiple tab check', () => {
-  beforeEach(() => {
-    sendCommandParams = [];
-    sendCommandMockResponses.clear();
-  });
+describe('.assertNoSameOriginServiceWorkerClients', () => {
+  function createSWRegistration(id, url, isDeleted) {
+    return {
+      isDeleted: !!isDeleted,
+      registrationId: id,
+      scopeURL: url,
+    };
+  }
+
+  function createActiveWorker(id, url, controlledClients, status = 'activated') {
+    return {
+      registrationId: id,
+      scriptURL: url,
+      controlledClients,
+      status,
+    };
+  }
 
   it('will pass if there are no current service workers', () => {
     const pageUrl = 'https://example.com/';
-    driverStub.once = createOnceStub({
+    driver.once = createOnceStub({
       'ServiceWorker.workerRegistrationUpdated': {
         registrations: [],
       },
     });
 
-    driverStub.on = createOnceStub({
+    driver.on = createOnceStub({
       'ServiceWorker.workerVersionUpdated': {
         versions: [],
       },
     });
 
-    return driverStub.assertNoSameOriginServiceWorkerClients(pageUrl);
+    return driver.assertNoSameOriginServiceWorkerClients(pageUrl);
   });
 
   it('will pass if there is an active service worker for a different origin', () => {
@@ -390,55 +543,48 @@ describe('Multiple tab check', () => {
     const secondUrl = 'https://example.edu';
     const swUrl = `${secondUrl}sw.js`;
 
-    const registrations = [
-      createSWRegistration(1, secondUrl),
-    ];
-    const versions = [
-      createActiveWorker(1, swUrl, ['uniqueId']),
-    ];
+    const registrations = [createSWRegistration(1, secondUrl)];
+    const versions = [createActiveWorker(1, swUrl, ['uniqueId'])];
 
-    driverStub.once = createOnceStub({
+    driver.once = createOnceStub({
       'ServiceWorker.workerRegistrationUpdated': {
         registrations,
       },
     });
 
-    driverStub.on = createOnceStub({
+    driver.on = createOnceStub({
       'ServiceWorker.workerVersionUpdated': {
         versions,
       },
     });
 
-    return driverStub.assertNoSameOriginServiceWorkerClients(pageUrl);
+    return driver.assertNoSameOriginServiceWorkerClients(pageUrl);
   });
 
   it('will fail if a service worker with a matching origin has a controlled client', () => {
     const pageUrl = 'https://example.com/';
     const swUrl = `${pageUrl}sw.js`;
-    const registrations = [
-      createSWRegistration(1, pageUrl),
-    ];
-    const versions = [
-      createActiveWorker(1, swUrl, ['uniqueId']),
-    ];
-
-    driverStub.once = createOnceStub({
+    const registrations = [createSWRegistration(1, pageUrl)];
+    const versions = [createActiveWorker(1, swUrl, ['uniqueId'])];
+
+    driver.once = createOnceStub({
       'ServiceWorker.workerRegistrationUpdated': {
         registrations,
       },
     });
 
-    driverStub.on = createOnceStub({
+    driver.on = createOnceStub({
       'ServiceWorker.workerVersionUpdated': {
         versions,
       },
     });
 
-    return driverStub.assertNoSameOriginServiceWorkerClients(pageUrl)
-      .then(_ => assert.ok(false),
-          err => {
-            assert.ok(err.message.toLowerCase().includes('multiple tabs'));
-          });
+    return driver.assertNoSameOriginServiceWorkerClients(pageUrl).then(
+      _ => assert.ok(false),
+      err => {
+        assert.ok(err.message.toLowerCase().includes('multiple tabs'));
+      }
+    );
   });
 
   it('will succeed if a service worker with a matching origin has no controlled clients', () => {
@@ -447,19 +593,19 @@ describe('Multiple tab check', () => {
     const registrations = [createSWRegistration(1, pageUrl)];
     const versions = [createActiveWorker(1, swUrl, [])];
 
-    driverStub.once = createOnceStub({
+    driver.once = createOnceStub({
       'ServiceWorker.workerRegistrationUpdated': {
         registrations,
       },
     });
 
-    driverStub.on = createOnceStub({
+    driver.on = createOnceStub({
       'ServiceWorker.workerVersionUpdated': {
         versions,
       },
     });
 
-    return driverStub.assertNoSameOriginServiceWorkerClients(pageUrl);
+    return driver.assertNoSameOriginServiceWorkerClients(pageUrl);
   });
 
   it('will wait for serviceworker to be activated', () => {
@@ -468,21 +614,19 @@ describe('Multiple tab check', () => {
     const registrations = [createSWRegistration(1, pageUrl)];
     const versions = [createActiveWorker(1, swUrl, [], 'installing')];
 
-    driverStub.once = createOnceStub({
+    driver.once = createOnceStub({
       'ServiceWorker.workerRegistrationUpdated': {
         registrations,
       },
     });
 
-    driverStub.on = (eventName, cb) => {
+    driver.on = (eventName, cb) => {
       if (eventName === 'ServiceWorker.workerVersionUpdated') {
         cb({versions});
 
         setTimeout(() => {
           cb({
-            versions: [
-              createActiveWorker(1, swUrl, [], 'activated'),
-            ],
+            versions: [createActiveWorker(1, swUrl, [], 'activated')],
           });
         }, 1000);
 
@@ -492,161 +636,79 @@ describe('Multiple tab check', () => {
       throw Error(`Stub not implemented: ${eventName}`);
     };
 
-    return driverStub.assertNoSameOriginServiceWorkerClients(pageUrl);
+    return driver.assertNoSameOriginServiceWorkerClients(pageUrl);
   });
+});
 
-  describe('.goOnline', () => {
-    it('re-establishes previous throttling settings', async () => {
-      await driverStub.goOnline({
-        passConfig: {useThrottling: true},
-        settings: {
-          throttlingMethod: 'devtools',
-          throttling: {
-            requestLatencyMs: 500,
-            downloadThroughputKbps: 1000,
-            uploadThroughputKbps: 1000,
-          },
-        },
-      });
-
-      const emulateCommand = sendCommandParams
-        .find(item => item.command === 'Network.emulateNetworkConditions');
-      assert.ok(emulateCommand, 'did not call emulate network');
-      assert.deepStrictEqual(emulateCommand.params, {
-        offline: false,
-        latency: 500,
-        downloadThroughput: 1000 * 1024 / 8,
-        uploadThroughput: 1000 * 1024 / 8,
-      });
-    });
+describe('.goOnline', () => {
+  beforeEach(() => {
+    connectionStub.sendCommand = createMockSendCommandFn()
+      .mockResponse('Network.enable', {})
+      .mockResponse('Emulation.setCPUThrottlingRate', {})
+      .mockResponse('Network.emulateNetworkConditions', {});
+  });
 
-    it('clears network emulation when throttling is not devtools', async () => {
-      await driverStub.goOnline({
-        passConfig: {useThrottling: true},
-        settings: {
-          throttlingMethod: 'provided',
+  it('re-establishes previous throttling settings', async () => {
+    await driver.goOnline({
+      passConfig: {useThrottling: true},
+      settings: {
+        throttlingMethod: 'devtools',
+        throttling: {
+          requestLatencyMs: 500,
+          downloadThroughputKbps: 1000,
+          uploadThroughputKbps: 1000,
         },
-      });
-
-      const emulateCommand = sendCommandParams
-        .find(item => item.command === 'Network.emulateNetworkConditions');
-      assert.ok(emulateCommand, 'did not call emulate network');
-      assert.deepStrictEqual(emulateCommand.params, {
-        offline: false,
-        latency: 0,
-        downloadThroughput: 0,
-        uploadThroughput: 0,
-      });
+      },
     });
 
-    it('clears network emulation when useThrottling is false', async () => {
-      await driverStub.goOnline({
-        passConfig: {useThrottling: false},
-        settings: {
-          throttlingMethod: 'devtools',
-          throttling: {
-            requestLatencyMs: 500,
-            downloadThroughputKbps: 1000,
-            uploadThroughputKbps: 1000,
-          },
-        },
-      });
-
-      const emulateCommand = sendCommandParams
-        .find(item => item.command === 'Network.emulateNetworkConditions');
-      assert.ok(emulateCommand, 'did not call emulate network');
-      assert.deepStrictEqual(emulateCommand.params, {
-        offline: false,
-        latency: 0,
-        downloadThroughput: 0,
-        uploadThroughput: 0,
-      });
+    const emulateArgs = connectionStub.sendCommand
+      .findInvocation('Network.emulateNetworkConditions');
+    expect(emulateArgs).toEqual({
+      offline: false,
+      latency: 500,
+      downloadThroughput: (1000 * 1024) / 8,
+      uploadThroughput: (1000 * 1024) / 8,
     });
   });
 
-  describe('.goOffline', () => {
-    it('should send offline emulation', async () => {
-      await driverStub.goOffline();
-      const emulateCommand = sendCommandParams
-        .find(item => item.command === 'Network.emulateNetworkConditions');
-      assert.ok(emulateCommand, 'did not call emulate network');
-      assert.deepStrictEqual(emulateCommand.params, {
-        offline: true,
-        latency: 0,
-        downloadThroughput: 0,
-        uploadThroughput: 0,
-      });
+  it('clears network emulation when throttling is not devtools', async () => {
+    await driver.goOnline({
+      passConfig: {useThrottling: true},
+      settings: {
+        throttlingMethod: 'provided',
+      },
     });
-  });
-
-  describe('Security check', () => {
-    it('does not reject when page is secure', async () => {
-      const secureSecurityState = {
-        explanations: [],
-        securityState: 'secure',
-      };
-      driverStub.on = driverStub.once = createOnceStub({
-        'Security.securityStateChanged': secureSecurityState,
-        'Page.loadEventFired': {},
-        'Page.domContentEventFired': {},
-      });
 
-      const startUrl = 'https://www.example.com';
-      const loadOptions = {
-        waitForLoad: true,
-        passContext: {
-          passConfig: {
-            networkQuietThresholdMs: 1,
-          },
-          settings: {
-            maxWaitForLoad: 1,
-          },
-        },
-      };
-      await driverStub.gotoURL(startUrl, loadOptions);
+    const emulateArgs = connectionStub.sendCommand
+      .findInvocation('Network.emulateNetworkConditions');
+    expect(emulateArgs).toEqual({
+      offline: false,
+      latency: 0,
+      downloadThroughput: 0,
+      uploadThroughput: 0,
     });
+  });
 
-    it('rejects when page is insecure', async () => {
-      const insecureSecurityState = {
-        explanations: [
-          {
-            description: 'reason 1.',
-            securityState: 'insecure',
-          },
-          {
-            description: 'blah.',
-            securityState: 'info',
-          },
-          {
-            description: 'reason 2.',
-            securityState: 'insecure',
-          },
-        ],
-        securityState: 'insecure',
-      };
-      driverStub.on = createOnceStub({
-        'Security.securityStateChanged': insecureSecurityState,
-      });
-
-      const startUrl = 'https://www.example.com';
-      const loadOptions = {
-        waitForLoad: true,
-        passContext: {
-          passConfig: {
-            networkQuietThresholdMs: 1,
-          },
+  it('clears network emulation when useThrottling is false', async () => {
+    await driver.goOnline({
+      passConfig: {useThrottling: false},
+      settings: {
+        throttlingMethod: 'devtools',
+        throttling: {
+          requestLatencyMs: 500,
+          downloadThroughputKbps: 1000,
+          uploadThroughputKbps: 1000,
         },
-      };
+      },
+    });
 
-      try {
-        await driverStub.gotoURL(startUrl, loadOptions);
-        assert.fail('security check should have rejected');
-      } catch (err) {
-        assert.equal(err.message, 'INSECURE_DOCUMENT_REQUEST');
-        assert.equal(err.code, 'INSECURE_DOCUMENT_REQUEST');
-        /* eslint-disable-next-line max-len */
-        expect(err.friendlyMessage).toBeDisplayString('The URL you have provided does not have valid security credentials. reason 1. reason 2.');
-      }
+    const emulateArgs = connectionStub.sendCommand
+      .findInvocation('Network.emulateNetworkConditions');
+    expect(emulateArgs).toEqual({
+      offline: false,
+      latency: 0,
+      downloadThroughput: 0,
+      uploadThroughput: 0,
     });
   });
 });
diff --git a/lighthouse-core/test/gather/gatherers/seo/font-size-test.js b/lighthouse-core/test/gather/gatherers/seo/font-size-test.js
index 58b09939f..6569b4f98 100644
--- a/lighthouse-core/test/gather/gatherers/seo/font-size-test.js
+++ b/lighthouse-core/test/gather/gatherers/seo/font-size-test.js
@@ -172,69 +172,63 @@ describe('Font size gatherer', () => {
 
     it('should identify inline styles', () => {
       const result = FontSizeGather.getEffectiveFontRule({inlineStyle});
-      expect(result).toMatchInlineSnapshot(`
-Object {
-  "cssProperties": Array [
-    Object {
-      "name": "font-size",
-      "value": "1em",
-    },
-  ],
-  "styleSheetId": 1,
-  "type": "Inline",
-}
-`);
+      expect(result).toEqual({
+        cssProperties: [
+          {
+            name: 'font-size',
+            value: '1em',
+          },
+        ],
+        styleSheetId: 1,
+        type: 'Inline',
+      });
     });
 
     it('should identify direct CSS rules', () => {
       const result = FontSizeGather.getEffectiveFontRule({matchedCSSRules});
-      expect(result).toMatchInlineSnapshot(`
-Object {
-  "cssProperties": Array [
-    Object {
-      "name": "font-size",
-      "value": "1em",
-    },
-  ],
-  "parentRule": Object {
-    "origin": "regular",
-    "selectors": Array [
-      Object {
-        "text": "html body *",
-      },
-      Object {
-        "text": "#main",
-      },
-    ],
-  },
-  "styleSheetId": 123,
-  "type": "Regular",
-}
-`);
+      expect(result).toEqual({
+        cssProperties: [
+          {
+            name: 'font-size',
+            value: '1em',
+          },
+        ],
+        parentRule: {
+          origin: 'regular',
+          selectors: [
+            {
+              text: 'html body *',
+            },
+            {
+              text: '#main',
+            },
+          ],
+        },
+        styleSheetId: 123,
+        type: 'Regular',
+      });
     });
 
     it('should identify inherited CSS rules', () => {
       const result = FontSizeGather.getEffectiveFontRule({inherited});
-      expect(result).toMatchInlineSnapshot(`
-Object {
-  "cssProperties": Array [
-    Object {
-      "name": "font-size",
-      "value": 12,
-    },
-  ],
-  "parentRule": Object {
-    "origin": "user-agent",
-    "selectors": Array [
-      Object {
-        "text": "body",
-      },
-    ],
-  },
-  "styleSheetId": undefined,
-  "type": "Regular",
-}
-`);
+      expect(result).toEqual({
+        cssProperties: [
+          {
+            name: 'font-size',
+            value: 12,
+          },
+        ],
+        parentRule: {
+          origin: 'user-agent',
+          selectors: [
+            {
+              text: 'body',
+            },
+          ],
+        },
+        styleSheetId: undefined,
+        type: 'Regular',
+      });
     });
 
     it('should respect precendence', () => {
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index 950cfeddd..61ad1078c 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -2158,35 +2158,35 @@
             "wastedBytes": 1108
           },
           {
-            "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=3000&async=true",
+            "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=100",
             "cacheLifetimeMs": 0,
             "cacheHitProbability": 0,
             "totalBytes": 821,
             "wastedBytes": 821
           },
           {
-            "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=100",
+            "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=2000&async=true",
             "cacheLifetimeMs": 0,
             "cacheHitProbability": 0,
             "totalBytes": 821,
             "wastedBytes": 821
           },
           {
-            "url": "http://localhost:10200/dobetterweb/dbw_tester.css?scriptActivated&delay=200",
+            "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=2200",
             "cacheLifetimeMs": 0,
             "cacheHitProbability": 0,
             "totalBytes": 821,
             "wastedBytes": 821
           },
           {
-            "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=2200",
+            "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=3000&async=true",
             "cacheLifetimeMs": 0,
             "cacheHitProbability": 0,
             "totalBytes": 821,
             "wastedBytes": 821
           },
           {
-            "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=2000&async=true",
+            "url": "http://localhost:10200/dobetterweb/dbw_tester.css?scriptActivated&delay=200",
             "cacheLifetimeMs": 0,
             "cacheHitProbability": 0,
             "totalBytes": 821,
@@ -2264,15 +2264,15 @@
             "totalBytes": 1108
           },
           {
-            "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=2200",
+            "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=100",
             "totalBytes": 821
           },
           {
-            "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=3000&async=true",
+            "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=2000&async=true",
             "totalBytes": 821
           },
           {
-            "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=2000&async=true",
+            "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=2200",
             "totalBytes": 821
           }
         ]
@@ -3140,14 +3140,6 @@
       "scoreDisplayMode": "notApplicable",
       "rawValue": true
     },
-    "mobile-friendly": {
-      "id": "mobile-friendly",
-      "title": "Page is mobile friendly",
-      "description": "Take the [Mobile-Friendly Test](https://search.google.com/test/mobile-friendly) to check for audits not covered by Lighthouse, like sizing tap targets appropriately. [Learn more](https://developers.google.com/search/mobile-sites/).",
-      "score": null,
-      "scoreDisplayMode": "manual",
-      "rawValue": false
-    },
     "structured-data": {
       "id": "structured-data",
       "title": "Structured data is valid",
@@ -3726,10 +3718,6 @@
           "weight": 1,
           "group": "seo-mobile"
         },
-        {
-          "id": "mobile-friendly",
-          "weight": 0
-        },
         {
           "id": "structured-data",
           "weight": 0
@@ -4800,12 +4788,6 @@
         "duration": 100,
         "entryType": "measure"
       },
-      {
-        "startTime": 0,
-        "name": "lh:audit:mobile-friendly",
-        "duration": 100,
-        "entryType": "measure"
-      },
       {
         "startTime": 0,
         "name": "lh:audit:structured-data",