Repository: GoogleChrome/lighthouse
Pull request #6609 (2018-11-20 02:56:40)

core(service-worker): check that test page is in SW scope
part of #6395

add an additional check to the `service-worker` audit that the test page is within the scope of a SW.

I'll follow up with a second PR that tests that the `start_url` (if it exists) is within scope. Right now this is just pass/fail `rawValue`, but we'll probably need an audit `explanation` with the second PR.

brendankenny: > is the goal of this really to just have more helpful stepping stones?

Yeah, stepping stones, I think, to help figure out why the SW isn't working (it shouldn't show passing when the offline check is failing specially because the SW has the wrong scope). I imagine it will only fail when first starting to use a SW or someone makes a typo :)

Patch:
diff --git a/lighthouse-core/audits/service-worker.js b/lighthouse-core/audits/service-worker.js
index 720418907..a89c186f0 100644
--- a/lighthouse-core/audits/service-worker.js
+++ b/lighthouse-core/audits/service-worker.js
@@ -5,8 +5,8 @@
  */
 'use strict';
 
-const URL = require('../lib/url-shim');
-const Audit = require('./audit');
+const URL = require('../lib/url-shim.js');
+const Audit = require('./audit.js');
 
 class ServiceWorker extends Audit {
   /**
@@ -29,17 +29,30 @@ class ServiceWorker extends Audit {
    * @return {LH.Audit.Product}
    */
   static audit(artifacts) {
-    // Find active service worker for this URL. Match against
+    const {versions, registrations} = artifacts.ServiceWorker;
+    const pageUrl = new URL(artifacts.URL.finalUrl);
+
+    // Find active service workers for this origin. Match against
     // artifacts.URL.finalUrl so audit accounts for any redirects.
-    const versions = artifacts.ServiceWorker.versions;
-    const url = artifacts.URL.finalUrl;
+    const matchingSWVersions = versions.filter(v => v.status === 'activated')
+      .filter(v => new URL(v.scriptURL).origin === pageUrl.origin);
+
+    if (matchingSWVersions.length === 0) {
+      return {rawValue: false};
+    }
+
+    // Find the normalized scope URLs of possibly-controlling SWs.
+    const matchingScopeUrls = matchingSWVersions
+      .map(v => registrations.find(r => r.registrationId === v.registrationId))
+      .filter(/** @return {r is LH.Crdp.ServiceWorker.ServiceWorkerRegistration} */ r => !!r)
+      .map(r => new URL(r.scopeURL).href);
 
-    const origin = new URL(url).origin;
-    const matchingSW = versions.filter(v => v.status === 'activated')
-        .find(v => new URL(v.scriptURL).origin === origin);
+    // Ensure page is included in a SW's scope.
+    // See https://w3c.github.io/ServiceWorker/v1/#scope-match-algorithm
+    const inScope = matchingScopeUrls.some(scopeUrl => pageUrl.href.startsWith(scopeUrl));
 
     return {
-      rawValue: !!matchingSW,
+      rawValue: inScope,
     };
   }
 }
diff --git a/lighthouse-core/audits/webapp-install-banner.js b/lighthouse-core/audits/webapp-install-banner.js
index d0cbc2b0f..bbe114413 100644
--- a/lighthouse-core/audits/webapp-install-banner.js
+++ b/lighthouse-core/audits/webapp-install-banner.js
@@ -6,7 +6,6 @@
 'use strict';
 
 const MultiCheckAudit = require('./multi-check-audit');
-const SWAudit = require('./service-worker');
 const ManifestValues = require('../computed/manifest-values.js');
 
 /**
@@ -80,20 +79,6 @@ class WebappInstallBanner extends MultiCheckAudit {
     return failures;
   }
 
-  /**
-   * @param {LH.Artifacts} artifacts
-   * @return {Array<string>}
-   */
-  static assessServiceWorker(artifacts) {
-    const failures = [];
-    const hasServiceWorker = SWAudit.audit(artifacts).rawValue;
-    if (!hasServiceWorker) {
-      failures.push('Site does not register a service worker');
-    }
-
-    return failures;
-  }
-
   /**
    * @param {LH.Artifacts} artifacts
    * @param {LH.Audit.Context} context
@@ -102,12 +87,10 @@ class WebappInstallBanner extends MultiCheckAudit {
   static async audit_(artifacts, context) {
     const manifestValues = await ManifestValues.request(artifacts.Manifest, context);
     const manifestFailures = WebappInstallBanner.assessManifest(manifestValues);
-    const swFailures = WebappInstallBanner.assessServiceWorker(artifacts);
 
     return {
       failures: [
         ...manifestFailures,
-        ...swFailures,
       ],
       manifestValues,
     };
diff --git a/lighthouse-core/computed/computed-artifact.js b/lighthouse-core/computed/computed-artifact.js
index 471a027e4..618c8520b 100644
--- a/lighthouse-core/computed/computed-artifact.js
+++ b/lighthouse-core/computed/computed-artifact.js
@@ -6,6 +6,7 @@
 'use strict';
 
 const ArbitraryEqualityMap = require('../lib/arbitrary-equality-map.js');
+const log = require('lighthouse-logger');
 
 /**
  * Decorate computableArtifact with a caching `request()` method which will
@@ -25,18 +26,25 @@ function makeComputedArtifact(computableArtifact) {
    */
   const request = (artifacts, context) => {
     const computedCache = context.computedCache;
-    const cache = computedCache.get(computableArtifact.name) || new ArbitraryEqualityMap();
-    computedCache.set(computableArtifact.name, cache);
+    const computedName = computableArtifact.name;
+
+    const cache = computedCache.get(computedName) || new ArbitraryEqualityMap();
+    computedCache.set(computedName, cache);
 
     const computed = /** @type {ReturnType<C['compute_']>|undefined} */ (cache.get(artifacts));
     if (computed) {
       return computed;
     }
 
+    const status = {msg: `Computing artifact: ${computedName}`, id: `lh:computed:${computedName}`};
+    log.time(status, 'verbose');
+
     const artifactPromise = /** @type {ReturnType<C['compute_']>} */
         (computableArtifact.compute_(artifacts, context));
     cache.set(artifacts, artifactPromise);
 
+    artifactPromise.then(() => log.timeEnd(status)).catch(() => log.timeEnd(status));
+
     return artifactPromise;
   };
 
diff --git a/lighthouse-core/gather/gatherers/service-worker.js b/lighthouse-core/gather/gatherers/service-worker.js
index 0a9cad683..6a27febb6 100644
--- a/lighthouse-core/gather/gatherers/service-worker.js
+++ b/lighthouse-core/gather/gatherers/service-worker.js
@@ -14,8 +14,11 @@ class ServiceWorker extends Gatherer {
    */
   async beforePass(passContext) {
     const {versions} = await passContext.driver.getServiceWorkerVersions();
+    const {registrations} = await passContext.driver.getServiceWorkerRegistrations();
+
     return {
       versions,
+      registrations,
     };
   }
 }
diff --git a/lighthouse-core/lib/arbitrary-equality-map.js b/lighthouse-core/lib/arbitrary-equality-map.js
index 78a3fd5e8..15bd8caa4 100644
--- a/lighthouse-core/lib/arbitrary-equality-map.js
+++ b/lighthouse-core/lib/arbitrary-equality-map.js
@@ -14,7 +14,7 @@ const isEqual = require('lodash.isequal');
  */
 class ArbitraryEqualityMap {
   constructor() {
-    this._equalsFn = /** @type {function(*,*):boolean} */ ((a, b) => a === b);
+    this._equalsFn = ArbitraryEqualityMap.deepEquals;
     /** @type {Array<{key: *, value: *}>} */
     this._entries = [];
   }
diff --git a/proto/sample_v2_round_trip.json b/proto/sample_v2_round_trip.json
index 551bb7fb9..9e8c3c343 100644
--- a/proto/sample_v2_round_trip.json
+++ b/proto/sample_v2_round_trip.json
@@ -2548,15 +2548,14 @@
                 "items": [
                     {
                         "failures": [
-                            "No manifest was fetched", 
-                            "Site does not register a service worker"
+                            "No manifest was fetched"
                         ], 
                         "isParseFailure": true, 
                         "parseFailureReason": "No manifest was fetched"
                     }
                 ]
             }, 
-            "explanation": "Failures: No manifest was fetched,\nSite does not register a service worker.", 
+            "explanation": "Failures: No manifest was fetched.", 
             "id": "webapp-install-banner", 
             "score": 0.0, 
             "scoreDisplayMode": "binary", 
@@ -3330,6 +3329,12 @@
                 "name": "lh:audit:is-on-https", 
                 "startTime": 0.0
             }, 
+            {
+                "duration": 100.0, 
+                "entryType": "measure", 
+                "name": "lh:computed:NetworkRecords", 
+                "startTime": 0.0
+            }, 
             {
                 "duration": 100.0, 
                 "entryType": "measure", 
@@ -3366,24 +3371,60 @@
                 "name": "lh:audit:first-contentful-paint", 
                 "startTime": 0.0
             }, 
+            {
+                "duration": 100.0, 
+                "entryType": "measure", 
+                "name": "lh:computed:FirstContentfulPaint", 
+                "startTime": 0.0
+            }, 
+            {
+                "duration": 100.0, 
+                "entryType": "measure", 
+                "name": "lh:computed:TraceOfTab", 
+                "startTime": 0.0
+            }, 
             {
                 "duration": 100.0, 
                 "entryType": "measure", 
                 "name": "lh:audit:first-meaningful-paint", 
                 "startTime": 0.0
             }, 
+            {
+                "duration": 100.0, 
+                "entryType": "measure", 
+                "name": "lh:computed:FirstMeaningfulPaint", 
+                "startTime": 0.0
+            }, 
             {
                 "duration": 100.0, 
                 "entryType": "measure", 
                 "name": "lh:audit:load-fast-enough-for-pwa", 
                 "startTime": 0.0
             }, 
+            {
+                "duration": 100.0, 
+                "entryType": "measure", 
+                "name": "lh:computed:Interactive", 
+                "startTime": 0.0
+            }, 
             {
                 "duration": 100.0, 
                 "entryType": "measure", 
                 "name": "lh:audit:speed-index", 
                 "startTime": 0.0
             }, 
+            {
+                "duration": 100.0, 
+                "entryType": "measure", 
+                "name": "lh:computed:SpeedIndex", 
+                "startTime": 0.0
+            }, 
+            {
+                "duration": 100.0, 
+                "entryType": "measure", 
+                "name": "lh:computed:Speedline", 
+                "startTime": 0.0
+            }, 
             {
                 "duration": 100.0, 
                 "entryType": "measure", 
@@ -3396,12 +3437,24 @@
                 "name": "lh:audit:final-screenshot", 
                 "startTime": 0.0
             }, 
+            {
+                "duration": 100.0, 
+                "entryType": "measure", 
+                "name": "lh:computed:Screenshots", 
+                "startTime": 0.0
+            }, 
             {
                 "duration": 100.0, 
                 "entryType": "measure", 
                 "name": "lh:audit:estimated-input-latency", 
                 "startTime": 0.0
             }, 
+            {
+                "duration": 100.0, 
+                "entryType": "measure", 
+                "name": "lh:computed:EstimatedInputLatency", 
+                "startTime": 0.0
+            }, 
             {
                 "duration": 100.0, 
                 "entryType": "measure", 
@@ -3414,12 +3467,24 @@
                 "name": "lh:audit:time-to-first-byte", 
                 "startTime": 0.0
             }, 
+            {
+                "duration": 100.0, 
+                "entryType": "measure", 
+                "name": "lh:computed:MainResource", 
+                "startTime": 0.0
+            }, 
             {
                 "duration": 100.0, 
                 "entryType": "measure", 
                 "name": "lh:audit:first-cpu-idle", 
                 "startTime": 0.0
             }, 
+            {
+                "duration": 100.0, 
+                "entryType": "measure", 
+                "name": "lh:computed:FirstCPUIdle", 
+                "startTime": 0.0
+            }, 
             {
                 "duration": 100.0, 
                 "entryType": "measure", 
@@ -3438,18 +3503,66 @@
                 "name": "lh:audit:critical-request-chains", 
                 "startTime": 0.0
             }, 
+            {
+                "duration": 100.0, 
+                "entryType": "measure", 
+                "name": "lh:computed:CriticalRequestChains", 
+                "startTime": 0.0
+            }, 
             {
                 "duration": 100.0, 
                 "entryType": "measure", 
                 "name": "lh:audit:redirects", 
                 "startTime": 0.0
             }, 
+            {
+                "duration": 100.0, 
+                "entryType": "measure", 
+                "name": "lh:computed:LanternInteractive", 
+                "startTime": 0.0
+            }, 
+            {
+                "duration": 100.0, 
+                "entryType": "measure", 
+                "name": "lh:computed:LanternFirstMeaningfulPaint", 
+                "startTime": 0.0
+            }, 
+            {
+                "duration": 100.0, 
+                "entryType": "measure", 
+                "name": "lh:computed:LanternFirstContentfulPaint", 
+                "startTime": 0.0
+            }, 
+            {
+                "duration": 100.0, 
+                "entryType": "measure", 
+                "name": "lh:computed:PageDependencyGraph", 
+                "startTime": 0.0
+            }, 
+            {
+                "duration": 100.0, 
+                "entryType": "measure", 
+                "name": "lh:computed:LoadSimulator", 
+                "startTime": 0.0
+            }, 
+            {
+                "duration": 100.0, 
+                "entryType": "measure", 
+                "name": "lh:computed:NetworkAnalysis", 
+                "startTime": 0.0
+            }, 
             {
                 "duration": 100.0, 
                 "entryType": "measure", 
                 "name": "lh:audit:webapp-install-banner", 
                 "startTime": 0.0
             }, 
+            {
+                "duration": 100.0, 
+                "entryType": "measure", 
+                "name": "lh:computed:ManifestValues", 
+                "startTime": 0.0
+            }, 
             {
                 "duration": 100.0, 
                 "entryType": "measure", 
@@ -3486,6 +3599,12 @@
                 "name": "lh:audit:mainthread-work-breakdown", 
                 "startTime": 0.0
             }, 
+            {
+                "duration": 100.0, 
+                "entryType": "measure", 
+                "name": "lh:computed:MainThreadTasks", 
+                "startTime": 0.0
+            }, 
             {
                 "duration": 100.0, 
                 "entryType": "measure", 
@@ -3498,6 +3617,12 @@
                 "name": "lh:audit:uses-rel-preload", 
                 "startTime": 0.0
             }, 
+            {
+                "duration": 100.0, 
+                "entryType": "measure", 
+                "name": "lh:computed:LoadSimulator", 
+                "startTime": 0.0
+            }, 
             {
                 "duration": 100.0, 
                 "entryType": "measure", 
@@ -3846,6 +3971,18 @@
                 "name": "lh:audit:render-blocking-resources", 
                 "startTime": 0.0
             }, 
+            {
+                "duration": 100.0, 
+                "entryType": "measure", 
+                "name": "lh:computed:FirstContentfulPaint", 
+                "startTime": 0.0
+            }, 
+            {
+                "duration": 100.0, 
+                "entryType": "measure", 
+                "name": "lh:computed:LanternFirstContentfulPaint", 
+                "startTime": 0.0
+            }, 
             {
                 "duration": 100.0, 
                 "entryType": "measure", 
diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts
index b8ee7ff34..a8dff54d2 100644
--- a/types/artifacts.d.ts
+++ b/types/artifacts.d.ts
@@ -108,7 +108,7 @@ declare global {
       /** The content of all scripts loaded by the page, keyed by networkRecord requestId. */
       Scripts: Record<string, string>;
       /** Version information for all ServiceWorkers active after the first page load. */
-      ServiceWorker: {versions: Crdp.ServiceWorker.ServiceWorkerVersion[]};
+      ServiceWorker: {versions: Crdp.ServiceWorker.ServiceWorkerVersion[], registrations: Crdp.ServiceWorker.ServiceWorkerRegistration[]};
       /** The status of an offline fetch of the page's start_url. -1 and a explanation if missing or there was an error. */
       StartUrl: {statusCode: number, explanation?: string};
       /** Information on <script> and <link> tags blocking first paint. */


Test patch:
diff --git a/lighthouse-core/test/audits/service-worker-test.js b/lighthouse-core/test/audits/service-worker-test.js
index fa7adb2fa..da0a8b243 100644
--- a/lighthouse-core/test/audits/service-worker-test.js
+++ b/lighthouse-core/test/audits/service-worker-test.js
@@ -5,39 +5,168 @@
  */
 'use strict';
 
-const Audit = require('../../audits/service-worker.js');
+const ServiceWorker = require('../../audits/service-worker.js');
+const URL = require('../../lib/url-shim.js');
 const assert = require('assert');
 
 /* eslint-env jest */
 
+function getBaseDirectory(urlStr) {
+  const url = new URL(urlStr);
+  return url.origin + url.pathname.substring(0, url.pathname.lastIndexOf('/') + 1);
+}
+
+/**
+ * Create a ServiceWorker artifact from an array of SW config opts.
+ * @param {Array<{scriptURL: string, status: string, scopeURL?: string}>} swOpts
+ * @param {string} finalUrl
+ */
+function createArtifacts(swOpts, finalUrl) {
+  const artifact = {versions: [], registrations: []};
+
+  swOpts.forEach((sw, index) => {
+    artifact.versions.push({
+      registrationId: `${index}`,
+      status: sw.status,
+      scriptURL: sw.scriptURL,
+    });
+
+    const scopeURL = sw.scopeURL || getBaseDirectory(sw.scriptURL);
+    assert.ok(scopeURL.endsWith('/')); // required by SW spec.
+
+    artifact.registrations.push({
+      registrationId: `${index}`,
+      scopeURL,
+    });
+  });
+
+  return {
+    ServiceWorker: artifact,
+    URL: {finalUrl},
+  };
+}
+
 describe('Offline: service worker audit', () => {
   it('passes when given a matching service worker version', () => {
-    const output = Audit.audit({
-      ServiceWorker: {
-        versions: [{
-          status: 'activated',
-          scriptURL: 'https://example.com/sw.js',
-        }],
-      },
-      URL: {finalUrl: 'https://example.com'},
-    });
+    const finalUrl = 'https://example.com';
+    const swOpts = [{
+      status: 'activated',
+      scriptURL: 'https://example.com/sw.js',
+    }];
 
+    const output = ServiceWorker.audit(createArtifacts(swOpts, finalUrl));
     assert.equal(output.rawValue, true);
   });
 
+  it('fails when matching service worker is not activated', () => {
+    const finalUrl = 'https://example.com';
+    const swOpts = [{
+      status: 'redundant',
+      scriptURL: 'https://example.com/sw.js',
+    }];
+
+    const output = ServiceWorker.audit(createArtifacts(swOpts, finalUrl));
+    assert.equal(output.rawValue, false);
+  });
+
   it('discards service worker registrations for other origins', () => {
-    const versions = [{
+    const finalUrl = 'https://example.com';
+    const swOpts = [{
       status: 'activated',
       scriptURL: 'https://other-example.com',
     }];
 
-    const output = Audit.audit({
-      ServiceWorker: {
-        versions,
-      },
-      URL: {finalUrl: 'https://example.com'},
-    });
+    const output = ServiceWorker.audit(createArtifacts(swOpts, finalUrl));
+    assert.equal(output.rawValue, false);
+  });
+
+  it('fails when URL is out of scope', () => {
+    const finalUrl = 'https://example.com/index.html';
+    const swOpts = [{
+      status: 'activated',
+      scriptURL: 'https://example.com/serviceworker/sw.js',
+    }];
+
+    const output = ServiceWorker.audit(createArtifacts(swOpts, finalUrl));
+    assert.equal(output.rawValue, false);
+  });
+
+  it('fails when explicit scopeURL puts the URL out of scope', () => {
+    const finalUrl = 'https://example.com/index.html';
+    const swOpts = [{
+      status: 'activated',
+      scriptURL: 'https://example.com/sw.js',
+      scopeURL: 'https://example.com/serviceworker/',
+    }];
+
+    const output = ServiceWorker.audit(createArtifacts(swOpts, finalUrl));
+    assert.equal(output.rawValue, false);
+  });
+
+  it('passes when outside default scope but explicit scopeURL puts it back in', () => {
+    const finalUrl = 'https://example.com/index.html';
+    const swOpts = [{
+      status: 'activated',
+      scriptURL: 'https://example.com/serviceworker/sw.js',
+      // can happen when 'Service-Worker-Allowed' header widens max scope.
+      scopeURL: 'https://example.com/',
+    }];
+
+    const output = ServiceWorker.audit(createArtifacts(swOpts, finalUrl));
+    assert.equal(output.rawValue, true);
+  });
+
+  it('passes when multiple SWs control the scope', () => {
+    const finalUrl = 'https://example.com/project/index.html';
+    const swOpts = [{
+      status: 'activated',
+      scriptURL: 'https://example.com/sw.js',
+    }, {
+      status: 'activated',
+      scriptURL: 'https://example.com/project/sw.js',
+    }];
+
+    const output = ServiceWorker.audit(createArtifacts(swOpts, finalUrl));
+    assert.equal(output.rawValue, true);
+  });
+
+  it('passes when multiple SWs control the origin but only one is in scope', () => {
+    const finalUrl = 'https://example.com/index.html';
+    const swOpts = [{
+      status: 'activated',
+      scriptURL: 'https://example.com/sw.js',
+      scopeURL: 'https://example.com/project/',
+    }, {
+      status: 'activated',
+      scriptURL: 'https://example.com/project/sw.js',
+      scopeURL: 'https://example.com/project/',
+    }, {
+      status: 'activated',
+      scriptURL: 'https://example.com/project/subproject/sw.js',
+      scopeURL: 'https://example.com/',
+    }];
+
+    const output = ServiceWorker.audit(createArtifacts(swOpts, finalUrl));
+    assert.equal(output.rawValue, true);
+  });
+
+  it('fails when multiple SWs control the origin but are all out of scope', () => {
+    const finalUrl = 'https://example.com/index.html';
+    const swOpts = [{
+      status: 'activated',
+      scriptURL: 'https://example.com/sw.js',
+      scopeURL: 'https://example.com/project/',
+    }, {
+      status: 'activated',
+      scriptURL: 'https://example.com/project/sw.js',
+      scopeURL: 'https://example.com/project/',
+    }, {
+      status: 'activated',
+      scriptURL: 'https://example.com/project/subproject/sw.js',
+      scopeURL: 'https://example.com/project/',
+    }];
 
+    const output = ServiceWorker.audit(createArtifacts(swOpts, finalUrl));
     assert.equal(output.rawValue, false);
   });
 });
diff --git a/lighthouse-core/test/audits/webapp-install-banner-test.js b/lighthouse-core/test/audits/webapp-install-banner-test.js
index 9e9d4b2b6..2835a33a1 100644
--- a/lighthouse-core/test/audits/webapp-install-banner-test.js
+++ b/lighthouse-core/test/audits/webapp-install-banner-test.js
@@ -19,12 +19,6 @@ function generateMockArtifacts(src = manifestSrc) {
 
   const clonedArtifacts = JSON.parse(JSON.stringify({
     Manifest: exampleManifest,
-    ServiceWorker: {
-      versions: [{
-        status: 'activated',
-        scriptURL: 'https://example.com/sw.js',
-      }],
-    },
     URL: {finalUrl: 'https://example.com'},
   }));
   return clonedArtifacts;
@@ -146,17 +140,4 @@ describe('PWA: webapp install banner audit', () => {
       assert.strictEqual(failures.length, 1, failures);
     });
   });
-
-  it('fails if page had no SW', () => {
-    const artifacts = generateMockArtifacts();
-    artifacts.ServiceWorker.versions = [];
-    const context = generateMockAuditContext();
-
-    return WebappInstallBannerAudit.audit(artifacts, context).then(result => {
-      assert.strictEqual(result.rawValue, false);
-      assert.ok(result.explanation.includes('service worker'), result.explanation);
-      const failures = result.details.items[0].failures;
-      assert.strictEqual(failures.length, 1, failures);
-    });
-  });
 });
diff --git a/lighthouse-core/test/computed/computed-artifact-test.js b/lighthouse-core/test/computed/computed-artifact-test.js
index 2c7d3a866..d59fdd4e4 100644
--- a/lighthouse-core/test/computed/computed-artifact-test.js
+++ b/lighthouse-core/test/computed/computed-artifact-test.js
@@ -12,19 +12,19 @@ const assert = require('assert');
 const makeComputedArtifact = require('../../computed/computed-artifact.js');
 
 describe('ComputedArtifact base class', () => {
-  it.skip('caches computed artifacts by strict equality', async () => {
+  it('caches computed artifacts by strict equality', async () => {
     let computeCounter = 0;
-
-    const TestComputedArtifact = makeComputedArtifact(class TestComputedArtifact {
+    class RawTestComputedArtifact {
       static async compute_() {
         return computeCounter++;
       }
-    });
+    }
 
     const context = {
       computedCache: new Map(),
     };
 
+    const TestComputedArtifact = makeComputedArtifact(RawTestComputedArtifact);
     let result = await TestComputedArtifact.request({x: 1}, context);
     assert.equal(result, 0);
 
diff --git a/lighthouse-core/test/gather/gatherers/service-worker-test.js b/lighthouse-core/test/gather/gatherers/service-worker-test.js
index 9d4c5916e..e5fd4a3e8 100644
--- a/lighthouse-core/test/gather/gatherers/service-worker-test.js
+++ b/lighthouse-core/test/gather/gatherers/service-worker-test.js
@@ -7,32 +7,37 @@
 
 /* eslint-env jest */
 
-const ServiceWorkerGather = require('../../../gather/gatherers/service-worker');
+const ServiceWorkerGather = require('../../../gather/gatherers/service-worker.js');
 const assert = require('assert');
-let serviceWorkerGatherer;
 
 describe('service worker gatherer', () => {
-  // Reset the Gatherer before each test.
-  beforeEach(() => {
-    serviceWorkerGatherer = new ServiceWorkerGather();
-  });
-
-  it('obtains the active service worker registration', () => {
+  it('obtains the active service worker registration', async () => {
     const url = 'https://example.com/';
     const versions = [{
+      registrationId: '123',
       status: 'activated',
       scriptURL: url,
     }];
+    const registrations = [{
+      registrationId: '123',
+      scopeUrl: url,
+      isDeleted: false,
+    }];
 
-    return serviceWorkerGatherer.beforePass({
+    const serviceWorkerGatherer = new ServiceWorkerGather();
+    const artifact = await serviceWorkerGatherer.beforePass({
       driver: {
-        getServiceWorkerVersions() {
-          return Promise.resolve({versions});
+        async getServiceWorkerVersions() {
+          return {versions};
+        },
+        async getServiceWorkerRegistrations() {
+          return {registrations};
         },
       },
       url,
-    }).then(artifact => {
-      assert.deepEqual(artifact.versions, versions);
     });
+
+    assert.deepEqual(artifact.versions, versions);
+    assert.deepEqual(artifact.registrations, registrations);
   });
 });
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index 053f381a5..c527d7ac5 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -467,13 +467,12 @@
       "score": 0,
       "scoreDisplayMode": "binary",
       "rawValue": false,
-      "explanation": "Failures: No manifest was fetched,\nSite does not register a service worker.",
+      "explanation": "Failures: No manifest was fetched.",
       "details": {
         "items": [
           {
             "failures": [
-              "No manifest was fetched",
-              "Site does not register a service worker"
+              "No manifest was fetched"
             ],
             "isParseFailure": true,
             "parseFailureReason": "No manifest was fetched"
@@ -3474,6 +3473,12 @@
         "duration": 100,
         "entryType": "measure"
       },
+      {
+        "startTime": 0,
+        "name": "lh:computed:NetworkRecords",
+        "duration": 100,
+        "entryType": "measure"
+      },
       {
         "startTime": 0,
         "name": "lh:audit:redirects-http",
@@ -3510,24 +3515,60 @@
         "duration": 100,
         "entryType": "measure"
       },
+      {
+        "startTime": 0,
+        "name": "lh:computed:FirstContentfulPaint",
+        "duration": 100,
+        "entryType": "measure"
+      },
+      {
+        "startTime": 0,
+        "name": "lh:computed:TraceOfTab",
+        "duration": 100,
+        "entryType": "measure"
+      },
       {
         "startTime": 0,
         "name": "lh:audit:first-meaningful-paint",
         "duration": 100,
         "entryType": "measure"
       },
+      {
+        "startTime": 0,
+        "name": "lh:computed:FirstMeaningfulPaint",
+        "duration": 100,
+        "entryType": "measure"
+      },
       {
         "startTime": 0,
         "name": "lh:audit:load-fast-enough-for-pwa",
         "duration": 100,
         "entryType": "measure"
       },
+      {
+        "startTime": 0,
+        "name": "lh:computed:Interactive",
+        "duration": 100,
+        "entryType": "measure"
+      },
       {
         "startTime": 0,
         "name": "lh:audit:speed-index",
         "duration": 100,
         "entryType": "measure"
       },
+      {
+        "startTime": 0,
+        "name": "lh:computed:SpeedIndex",
+        "duration": 100,
+        "entryType": "measure"
+      },
+      {
+        "startTime": 0,
+        "name": "lh:computed:Speedline",
+        "duration": 100,
+        "entryType": "measure"
+      },
       {
         "startTime": 0,
         "name": "lh:audit:screenshot-thumbnails",
@@ -3540,12 +3581,24 @@
         "duration": 100,
         "entryType": "measure"
       },
+      {
+        "startTime": 0,
+        "name": "lh:computed:Screenshots",
+        "duration": 100,
+        "entryType": "measure"
+      },
       {
         "startTime": 0,
         "name": "lh:audit:estimated-input-latency",
         "duration": 100,
         "entryType": "measure"
       },
+      {
+        "startTime": 0,
+        "name": "lh:computed:EstimatedInputLatency",
+        "duration": 100,
+        "entryType": "measure"
+      },
       {
         "startTime": 0,
         "name": "lh:audit:errors-in-console",
@@ -3558,12 +3611,24 @@
         "duration": 100,
         "entryType": "measure"
       },
+      {
+        "startTime": 0,
+        "name": "lh:computed:MainResource",
+        "duration": 100,
+        "entryType": "measure"
+      },
       {
         "startTime": 0,
         "name": "lh:audit:first-cpu-idle",
         "duration": 100,
         "entryType": "measure"
       },
+      {
+        "startTime": 0,
+        "name": "lh:computed:FirstCPUIdle",
+        "duration": 100,
+        "entryType": "measure"
+      },
       {
         "startTime": 0,
         "name": "lh:audit:interactive",
@@ -3582,18 +3647,66 @@
         "duration": 100,
         "entryType": "measure"
       },
+      {
+        "startTime": 0,
+        "name": "lh:computed:CriticalRequestChains",
+        "duration": 100,
+        "entryType": "measure"
+      },
       {
         "startTime": 0,
         "name": "lh:audit:redirects",
         "duration": 100,
         "entryType": "measure"
       },
+      {
+        "startTime": 0,
+        "name": "lh:computed:LanternInteractive",
+        "duration": 100,
+        "entryType": "measure"
+      },
+      {
+        "startTime": 0,
+        "name": "lh:computed:LanternFirstMeaningfulPaint",
+        "duration": 100,
+        "entryType": "measure"
+      },
+      {
+        "startTime": 0,
+        "name": "lh:computed:LanternFirstContentfulPaint",
+        "duration": 100,
+        "entryType": "measure"
+      },
+      {
+        "startTime": 0,
+        "name": "lh:computed:PageDependencyGraph",
+        "duration": 100,
+        "entryType": "measure"
+      },
+      {
+        "startTime": 0,
+        "name": "lh:computed:LoadSimulator",
+        "duration": 100,
+        "entryType": "measure"
+      },
+      {
+        "startTime": 0,
+        "name": "lh:computed:NetworkAnalysis",
+        "duration": 100,
+        "entryType": "measure"
+      },
       {
         "startTime": 0,
         "name": "lh:audit:webapp-install-banner",
         "duration": 100,
         "entryType": "measure"
       },
+      {
+        "startTime": 0,
+        "name": "lh:computed:ManifestValues",
+        "duration": 100,
+        "entryType": "measure"
+      },
       {
         "startTime": 0,
         "name": "lh:audit:splash-screen",
@@ -3630,6 +3743,12 @@
         "duration": 100,
         "entryType": "measure"
       },
+      {
+        "startTime": 0,
+        "name": "lh:computed:MainThreadTasks",
+        "duration": 100,
+        "entryType": "measure"
+      },
       {
         "startTime": 0,
         "name": "lh:audit:bootup-time",
@@ -3642,6 +3761,12 @@
         "duration": 100,
         "entryType": "measure"
       },
+      {
+        "startTime": 0,
+        "name": "lh:computed:LoadSimulator",
+        "duration": 100,
+        "entryType": "measure"
+      },
       {
         "startTime": 0,
         "name": "lh:audit:uses-rel-preconnect",
@@ -3990,6 +4115,18 @@
         "duration": 100,
         "entryType": "measure"
       },
+      {
+        "startTime": 0,
+        "name": "lh:computed:FirstContentfulPaint",
+        "duration": 100,
+        "entryType": "measure"
+      },
+      {
+        "startTime": 0,
+        "name": "lh:computed:LanternFirstContentfulPaint",
+        "duration": 100,
+        "entryType": "measure"
+      },
       {
         "startTime": 0,
         "name": "lh:audit:unminified-css",