Repository: GoogleChrome/lighthouse (js)

Issue:
Don't fail localhost pages with INSECURE 
Testing a local site using a self-signed certificate seems pretty reasonable. 
Wdyt about us allowing `localhost` in our INSECURE checks?

Pull request description:
core(driver): security errors are no longer a fatal or pageload error
fix #6655, #7292
closes #7574

Following the investigation and proposal outlined here: https://github.com/GoogleChrome/lighthouse/issues/6655#issuecomment-484964210

I've tested with every badssl page and we never hang indefinitely on any of them. I did see a few PROTOCOL_TIMEOUT on the offlinePass though.

Also every badssl case where a security interstitial is shown hits our FAILED _DOC_REQ case and the `localizedFailDescription` that we show in the warning captures the same error code we got via securityState.insecureDescriptions. \o/

see also #8340


Patch:
diff --git a/clients/devtools-report-assets.js b/clients/devtools-report-assets.js
index a94320496..58b80f645 100644
--- a/clients/devtools-report-assets.js
+++ b/clients/devtools-report-assets.js
@@ -17,8 +17,8 @@
 const cachedResources = Runtime.cachedResources;
 
 module.exports = {
-  REPORT_CSS: cachedResources['audits2/lighthouse/report.css'],
-  REPORT_JAVASCRIPT: cachedResources['audits2/lighthouse/report.js'],
-  REPORT_TEMPLATE: cachedResources['audits2/lighthouse/template.html'],
-  REPORT_TEMPLATES: cachedResources['audits2/lighthouse/templates.html'],
+  REPORT_CSS: cachedResources['audits/lighthouse/report.css'],
+  REPORT_JAVASCRIPT: cachedResources['audits/lighthouse/report.js'],
+  REPORT_TEMPLATE: cachedResources['audits/lighthouse/template.html'],
+  REPORT_TEMPLATES: cachedResources['audits/lighthouse/templates.html'],
 };
diff --git a/lighthouse-core/gather/driver.js b/lighthouse-core/gather/driver.js
index 93f267a8a..a07c4d6d6 100644
--- a/lighthouse-core/gather/driver.js
+++ b/lighthouse-core/gather/driver.js
@@ -903,53 +903,6 @@ class Driver {
     };
   }
 
-  /**
-   * Return a promise that resolves when an insecure security state is encountered
-   * and a method to cancel internal listeners.
-   * @return {{promise: Promise<string>, cancel: function(): void}}
-   * @private
-   */
-  _monitorForInsecureState() {
-    /** @type {(() => void)} */
-    let cancel = () => {
-      throw new Error('_monitorForInsecureState.cancel() called before it was defined');
-    };
-
-    const promise = new Promise((resolve, reject) => {
-      /**
-       * @param {LH.Crdp.Security.SecurityStateChangedEvent} event
-       */
-      const securityStateChangedListener = ({
-        securityState,
-        explanations,
-        schemeIsCryptographic,
-      }) => {
-        if (securityState === 'insecure' && schemeIsCryptographic) {
-          cancel();
-          const insecureDescriptions = explanations
-            .filter(exp => exp.securityState === 'insecure')
-            .map(exp => exp.description);
-          resolve(insecureDescriptions.join(' '));
-        }
-      };
-      let canceled = false;
-      cancel = () => {
-        if (canceled) return;
-        canceled = true;
-        this.off('Security.securityStateChanged', securityStateChangedListener);
-        // TODO(@patrickhulce): cancel() should really be a promise itself to handle things like this
-        this.sendCommand('Security.disable').catch(() => {});
-      };
-      this.on('Security.securityStateChanged', securityStateChangedListener);
-      this.sendCommand('Security.enable').catch(() => {});
-    });
-
-    return {
-      promise,
-      cancel,
-    };
-  }
-
   /**
    * Returns whether the page appears to be hung.
    * @return {Promise<boolean>}
@@ -1001,13 +954,6 @@ class Driver {
     // CPU listener. Resolves when the CPU has been idle for cpuQuietThresholdMs after network idle.
     let waitForCPUIdle = this._waitForNothing();
 
-    const monitorForInsecureState = this._monitorForInsecureState();
-    const securityCheckPromise = monitorForInsecureState.promise.then(securityMessages => {
-      return function() {
-        throw new LHError(LHError.errors.INSECURE_DOCUMENT_REQUEST, {securityMessages});
-      };
-    });
-
     // Wait for both load promises. Resolves on cleanup function the clears load
     // timeout timer.
     const loadPromise = Promise.all([
@@ -1044,9 +990,8 @@ class Driver {
       };
     });
 
-    // Wait for security issue, load or timeout and run the cleanup function the winner returns.
+    // Wait for load or timeout and run the cleanup function the winner returns.
     const cleanupFn = await Promise.race([
-      securityCheckPromise,
       loadPromise,
       maxTimeoutPromise,
     ]);
@@ -1056,7 +1001,6 @@ class Driver {
     waitForLoadEvent.cancel();
     waitForNetworkIdle.cancel();
     waitForCPUIdle.cancel();
-    monitorForInsecureState.cancel();
 
     await cleanupFn();
   }
diff --git a/lighthouse-core/lib/manifest-parser.js b/lighthouse-core/lib/manifest-parser.js
index 763910cfd..64d4fd06b 100644
--- a/lighthouse-core/lib/manifest-parser.js
+++ b/lighthouse-core/lib/manifest-parser.js
@@ -16,7 +16,7 @@ const ALLOWED_DISPLAY_VALUES = [
 ];
 /**
  * All display-mode fallbacks, including when unset, lead to default display mode 'browser'.
- * @see https://w3c.github.io/manifest/#dfn-default-display-mode
+ * @see https://www.w3.org/TR/2016/WD-appmanifest-20160825/#dfn-default-display-mode
  */
 const DEFAULT_DISPLAY_MODE = 'browser';
 
@@ -111,7 +111,7 @@ function checkSameOrigin(url1, url2) {
 }
 
 /**
- * https://w3c.github.io/manifest/#start_url-member
+ * https://www.w3.org/TR/2016/WD-appmanifest-20160825/#start_url-member
  * @param {*} jsonInput
  * @param {string} manifestUrl
  * @param {string} documentUrl
@@ -151,7 +151,7 @@ function parseStartUrl(jsonInput, manifestUrl, documentUrl) {
     return {
       raw,
       value: documentUrl,
-      warning: 'ERROR: invalid start_url relative to ${manifestUrl}',
+      warning: `ERROR: invalid start_url relative to ${manifestUrl}`,
     };
   }
 
@@ -218,6 +218,7 @@ function parseOrientation(jsonInput) {
 }
 
 /**
+ * @see https://www.w3.org/TR/2016/WD-appmanifest-20160825/#src-member
  * @param {*} raw
  * @param {string} manifestUrl
  */
@@ -229,8 +230,14 @@ function parseIcon(raw, manifestUrl) {
     src.value = undefined;
   }
   if (src.value) {
-    // 9.4(4) - construct URL with manifest URL as the base
-    src.value = new URL(src.value, manifestUrl).href;
+    try {
+      // 9.4(4) - construct URL with manifest URL as the base
+      src.value = new URL(src.value, manifestUrl).href;
+    } catch (_) {
+      // 9.4 "This algorithm will return a URL or undefined."
+      src.warning = `ERROR: invalid icon url will be ignored: '${raw.src}'`;
+      src.value = undefined;
+    }
   }
 
   const type = parseString(raw.type, true);
@@ -301,20 +308,31 @@ function parseIcons(jsonInput, manifestUrl) {
     };
   }
 
-  // TODO(bckenny): spec says to skip icons missing `src`, so debug messages on
-  // individual icons are lost. Warn instead?
-  const value = raw
+  const parsedIcons = raw
     // 9.6(3)(1)
     .filter(icon => icon.src !== undefined)
     // 9.6(3)(2)(1)
-    .map(icon => parseIcon(icon, manifestUrl))
+    .map(icon => parseIcon(icon, manifestUrl));
+
+  // NOTE: we still lose the specific message on these icons, but it's not possible to surface them
+  // without a massive change to the structure and paradigms of `manifest-parser`.
+  const ignoredIconsWithWarnings = parsedIcons
+    .filter(icon => {
+      const possibleWarnings = [icon.warning, icon.value.type.warning, icon.value.src.warning,
+        icon.value.sizes.warning, icon.value.density.warning].filter(Boolean);
+      const hasSrc = !!icon.value.src.value;
+      return !!possibleWarnings.length && !hasSrc;
+    });
+
+  const value = parsedIcons
     // 9.6(3)(2)(2)
     .filter(parsedIcon => parsedIcon.value.src.value !== undefined);
 
   return {
     raw,
     value,
-    warning: undefined,
+    warning: ignoredIconsWithWarnings.length ?
+      'WARNING: Some icons were ignored due to warnings.' : undefined,
   };
 }
 
@@ -333,7 +351,7 @@ function parseApplication(raw) {
       appUrl.value = new URL(appUrl.value).href;
     } catch (e) {
       appUrl.value = undefined;
-      appUrl.warning = 'ERROR: invalid application URL ${raw.url}';
+      appUrl.warning = `ERROR: invalid application URL ${raw.url}`;
     }
   }
 
@@ -460,10 +478,21 @@ function parse(string, manifestUrl, documentUrl) {
   };
   /* eslint-enable camelcase */
 
+  /** @type {string|undefined} */
+  let manifestUrlWarning;
+  try {
+    const manifestUrlParsed = new URL(manifestUrl);
+    if (!manifestUrlParsed.protocol.startsWith('http')) {
+      manifestUrlWarning = `WARNING: manifest URL not available over a valid network protocol`;
+    }
+  } catch (_) {
+    manifestUrlWarning = `ERROR: invalid manifest URL: '${manifestUrl}'`;
+  }
+
   return {
     raw: string,
     value: manifest,
-    warning: undefined,
+    warning: manifestUrlWarning,
   };
 }
 
diff --git a/lighthouse-core/report/html/readme.md b/lighthouse-core/report/html/readme.md
index 3f56c76fd..3cc71c1ca 100644
--- a/lighthouse-core/report/html/readme.md
+++ b/lighthouse-core/report/html/readme.md
@@ -32,7 +32,7 @@ The renderer was designed to be portable across various environments.
 
 1. _LH Chrome Extension_: It [creates the HTML as the runner finishes up](https://github.com/GoogleChrome/lighthouse/blob/440155cdda377c458c0efce006bc3a69ce2a351c/lighthouse-core/runner.js#L137-L138) and transforms it [into a blob url in the background page](https://github.com/GoogleChrome/lighthouse/blob/440155cdda377c458c0efce006bc3a69ce2a351c/lighthouse-extension/app/src/lighthouse-ext-background.js#L129-L143).
 1. _LH CLI_: It [creates the HTML as the runner finishes up](https://github.com/GoogleChrome/lighthouse/blob/440155cdda377c458c0efce006bc3a69ce2a351c/lighthouse-core/runner.js#L137-L138) and [saves it to disk](https://github.com/GoogleChrome/lighthouse/blob/440155cdda377c458c0efce006bc3a69ce2a351c/lighthouse-cli/printer.js#L71-L92).
-1. _Chrome DevTools Audits Panel_: The `renderer` files are rolled into the Chromium repo, and they execute within the DevTools context. The audits panel [receives the LHR object from a WebWorker](https://github.com/ChromeDevTools/devtools-frontend/blob/master/front_end/audits2/Audits2ProtocolService.js#L27-L35), through a `postMessage` and then runs [the renderer within DevTools UI](https://github.com/ChromeDevTools/devtools-frontend/blob/fee00605cada877c1f8e3aae758a0f8d05b64476/front_end/audits2/Audits2Panel.js#L519-L542), making a few upgrades ([one](https://github.com/ChromeDevTools/devtools-frontend/blob/fee00605cada877c1f8e3aae758a0f8d05b64476/front_end/audits2/Audits2Panel.js#L570-L583), [two](https://github.com/ChromeDevTools/devtools-frontend/blob/fee00605cada877c1f8e3aae758a0f8d05b64476/front_end/audits2/Audits2Panel.js#L596-L637)) and [simplifications](https://github.com/ChromeDevTools/devtools-frontend/blob/fee00605cada877c1f8e3aae758a0f8d05b64476/front_end/audits2/Audits2Panel.js#L275-L304).
+1. _Chrome DevTools Audits Panel_: The `renderer` files are rolled into the Chromium repo, and they execute within the DevTools context. The audits panel [receives the LHR object from a WebWorker](https://github.com/ChromeDevTools/devtools-frontend/blob/aa1532c2f8bdc37c9886255644ed90ad01c61c77/front_end/audits/AuditsProtocolService.js#L27-L35), through a `postMessage` and then runs [the renderer within DevTools UI](https://github.com/ChromeDevTools/devtools-frontend/blob/aa1532c2f8bdc37c9886255644ed90ad01c61c77/front_end/audits/AuditsPanel.js#L123-L157), making a few [simplifications](https://github.com/ChromeDevTools/devtools-frontend/blob/master/front_end/audits/AuditsReportRenderer.js).
 1. _Hosted [Lighthouse Viewer](https://googlechrome.github.io/lighthouse/viewer/)_: It's a webapp that has the renderer (along with some [additional features](https://github.com/GoogleChrome/lighthouse/blob/master/lighthouse-core/report/html/renderer/report-ui-features.js)) all compiled into a [`viewer.js`](https://googlechrome.github.io/lighthouse/viewer/src/viewer.js) file. Same [basic approach](https://github.com/GoogleChrome/lighthouse/blob/440155cdda377c458c0efce006bc3a69ce2a351c/lighthouse-viewer/app/src/lighthouse-report-viewer.js#L116-L117) there.
 
 ### Polyfills
diff --git a/lighthouse-core/scripts/compile-against-devtools.sh b/lighthouse-core/scripts/compile-against-devtools.sh
index d072579e8..49151e3ef 100644
--- a/lighthouse-core/scripts/compile-against-devtools.sh
+++ b/lighthouse-core/scripts/compile-against-devtools.sh
@@ -13,7 +13,7 @@
 set -x
 
 # This the text here will override the renderer/ files in in the scripts[] array:
-#     https://github.com/ChromeDevTools/devtools-frontend/blob/master/front_end/audits2/module.json#L20
+#     https://github.com/ChromeDevTools/devtools-frontend/blob/master/front_end/audits/module.json#L20
 
 # (Currently this doesnt include logger or report-features)
 files_to_include="\"lighthouse\/renderer\/util.js\", \"lighthouse\/renderer\/dom.js\", \"lighthouse\/renderer\/category-renderer.js\", \"lighthouse\/renderer\/performance-category-renderer.js\", \"lighthouse\/renderer\/crc-details-renderer.js\", \"lighthouse\/renderer\/details-renderer.js\", \"lighthouse\/renderer\/report-renderer.js\","
@@ -54,13 +54,13 @@ cd "$lhroot_path" || exit 1
 yarn devtools "$frontend_path/front_end/"
 
 #
-# monkeypatch the audits2 module.json to include any new files we're added that aren't present
+# monkeypatch the audits module.json to include any new files we're added that aren't present
 #
-audit2_modulejson_path="$frontend_path/front_end/audits2/module.json"
+audit_modulejson_path="$frontend_path/front_end/audits/module.json"
 # remove existing renderer file mentions
-sed -i='' 's/.*\/renderer\/.*//' $audit2_modulejson_path
+sed -i='' 's/.*\/renderer\/.*//' $audit_modulejson_path
 # add in our hardcoded renderer file mentions
-sed -i='' "s/\"Audits2Panel\.js\"/ $files_to_include \"Audits2Panel.js\"/" $audit2_modulejson_path
+sed -i='' "s/\"AuditsPanel\.js\"/ $files_to_include \"AuditsPanel.js\"/" $audit_modulejson_path
 
 # compile, finally
 python "$frontend_path/scripts/compile_frontend.py" --protocol-externs-file "$protocol_path/externs/protocol_externs.js"
diff --git a/lighthouse-core/scripts/roll-to-devtools.sh b/lighthouse-core/scripts/roll-to-devtools.sh
index 963cb42c0..5959fd902 100755
--- a/lighthouse-core/scripts/roll-to-devtools.sh
+++ b/lighthouse-core/scripts/roll-to-devtools.sh
@@ -33,10 +33,10 @@ else
 fi
 
 report_dir="lighthouse-core/report/html"
-fe_lh_dir="$frontend_dir/audits2/lighthouse"
+fe_lh_dir="$frontend_dir/audits/lighthouse"
 
 lh_bg_js="dist/lighthouse-dt-bundle.js"
-lh_worker_dir="$frontend_dir/audits2_worker/lighthouse"
+lh_worker_dir="$frontend_dir/audits_worker/lighthouse"
 
 # copy lighthouse-dt-bundle (potentially stale)
 cp -pPR "$lh_bg_js" "$lh_worker_dir/lighthouse-dt-bundle.js"
diff --git a/readme.md b/readme.md
index f911924eb..3732042ae 100644
--- a/readme.md
+++ b/readme.md
@@ -304,6 +304,7 @@ This section details services that have integrated Lighthouse data. If you're wo
 ## Related Projects
 Other awesome open source projects that use Lighthouse.
 
+* **[Gimbal](https://labs.moduscreate.com/gimbal-web-performance-audit-budgeting)** - An [open source (MIT licensed)](https://github.com/ModusCreateOrg/gimbal) tool used to measure, analyze, and budget aspects of a web application. Gimbal also integrates reports with GitHub pull requests.
 * **[webpack-lighthouse-plugin](https://github.com/addyosmani/webpack-lighthouse-plugin)** - run Lighthouse from a Webpack build.
 * **[lighthouse-mocha-example](https://github.com/justinribeiro/lighthouse-mocha-example)** - gather performance metrics via Lighthouse and tests them in Mocha
 * **[pwmetrics](https://github.com/paulirish/pwmetrics/)** - gather performance metrics


Test patch:
diff --git a/lighthouse-cli/test/smokehouse/error-expectations.js b/lighthouse-cli/test/smokehouse/error-expectations.js
index 04d804e5a..55afc640f 100644
--- a/lighthouse-cli/test/smokehouse/error-expectations.js
+++ b/lighthouse-cli/test/smokehouse/error-expectations.js
@@ -18,10 +18,10 @@ module.exports = [
     },
   },
   {
-    errorCode: 'INSECURE_DOCUMENT_REQUEST',
+    errorCode: undefined,
     lhr: {
       requestedUrl: 'https://expired.badssl.com',
-      finalUrl: 'https://expired.badssl.com',
+      finalUrl: 'https://expired.badssl.com/',
       audits: {},
     },
   },
diff --git a/lighthouse-core/test/gather/driver-test.js b/lighthouse-core/test/gather/driver-test.js
index d96ee191b..dfe5ed6ba 100644
--- a/lighthouse-core/test/gather/driver-test.js
+++ b/lighthouse-core/test/gather/driver-test.js
@@ -675,108 +675,6 @@ describe('.gotoURL', () => {
       // Make sure we still cleaned up our listeners
       expect(driver._waitForLoadEvent.getMockCancelFn()).toHaveBeenCalled();
     });
-
-    it('does not reject when page is secure', async () => {
-      const secureSecurityState = {
-        explanations: [],
-        securityState: 'secure',
-      };
-
-      driver.on = driver.once = createMockOnceFn()
-        .mockEvent('Security.securityStateChanged', secureSecurityState);
-
-      const startUrl = 'https://www.example.com';
-      const loadOptions = {
-        waitForLoad: true,
-        passContext: {
-          settings: {
-            maxWaitForLoad: 1,
-          },
-        },
-      };
-
-      const loadPromise = driver.gotoURL(startUrl, loadOptions);
-      await flushAllTimersAndMicrotasks();
-      await loadPromise;
-    });
-
-    it('does not reject when page is insecure but http', async () => {
-      const secureSecurityState = {
-        explanations: [],
-        securityState: 'insecure',
-        schemeIsCryptographic: false,
-      };
-
-      driver.on = driver.once = createMockOnceFn()
-        .mockEvent('Security.securityStateChanged', secureSecurityState);
-
-      const startUrl = 'https://www.example.com';
-      const loadOptions = {
-        waitForLoad: true,
-        passContext: {
-          settings: {
-            maxWaitForLoad: 1,
-          },
-        },
-      };
-
-      const loadPromise = driver.gotoURL(startUrl, loadOptions);
-      await flushAllTimersAndMicrotasks();
-      await loadPromise;
-    });
-
-    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',
-        schemeIsCryptographic: true,
-      };
-
-      driver.on = driver.once = createMockOnceFn();
-
-      const startUrl = 'https://www.example.com';
-      const loadOptions = {
-        waitForLoad: true,
-        passContext: {
-          passConfig: {
-            networkQuietThresholdMs: 1,
-          },
-        },
-      };
-
-      // 2 assertions in the catch block and the 1 implicit in `findListener`
-      expect.assertions(3);
-
-      try {
-        const loadPromise = driver.gotoURL(startUrl, loadOptions);
-        await flushAllTimersAndMicrotasks();
-
-        // Use `findListener` instead of `mockEvent` so we can control exactly when the promise resolves
-        const listener = driver.on.findListener('Security.securityStateChanged');
-        listener(insecureSecurityState);
-        await flushAllTimersAndMicrotasks();
-        await loadPromise;
-      } catch (err) {
-        expect(err).toHaveProperty('code', 'INSECURE_DOCUMENT_REQUEST');
-        expect(err.friendlyMessage).toBeDisplayString(
-          'The URL you have provided does not have a valid security certificate. ' +
-          'reason 1. reason 2.'
-        );
-      }
-    });
   });
 });
 
diff --git a/lighthouse-core/test/lib/manifest-parser-test.js b/lighthouse-core/test/lib/manifest-parser-test.js
index 9f4fa38e4..9a93de8a1 100644
--- a/lighthouse-core/test/lib/manifest-parser-test.js
+++ b/lighthouse-core/test/lib/manifest-parser-test.js
@@ -11,6 +11,7 @@ const manifestStub = require('../fixtures/manifest.json');
 
 const EXAMPLE_MANIFEST_URL = 'https://example.com/manifest.json';
 const EXAMPLE_DOC_URL = 'https://example.com/index.html';
+const EXAMPLE_MANIFEST_BLOB_URL = 'blob:https://example.com/manifest.json';
 
 /**
  * Simple manifest parsing helper when the manifest URLs aren't material to the
@@ -27,32 +28,48 @@ function noUrlManifestParser(manifestSrc) {
 describe('Manifest Parser', function() {
   it('should not parse empty string input', function() {
     const parsedManifest = noUrlManifestParser('');
-    assert.ok(parsedManifest.warning);
+    expect(parsedManifest.warning)
+      .toEqual('ERROR: file isn\'t valid JSON: SyntaxError: Unexpected end of JSON input');
   });
 
   it('accepts empty dictionary', function() {
     const parsedManifest = noUrlManifestParser('{}');
-    assert(!parsedManifest.warning);
-    assert.equal(parsedManifest.value.name.value, undefined);
-    assert.equal(parsedManifest.value.short_name.value, undefined);
-    assert.equal(parsedManifest.value.start_url.value, EXAMPLE_DOC_URL);
-    assert.equal(parsedManifest.value.display.value, 'browser');
-    assert.equal(parsedManifest.value.orientation.value, undefined);
-    assert.equal(parsedManifest.value.theme_color.value, undefined);
-    assert.equal(parsedManifest.value.background_color.value, undefined);
-    assert.ok(Array.isArray(parsedManifest.value.icons.value));
-    assert.ok(parsedManifest.value.icons.value.length === 0);
+    expect(parsedManifest.warning).toBeUndefined();
+    expect(parsedManifest.value.name.value).toBe(undefined);
+    expect(parsedManifest.value.short_name.value).toBe(undefined);
+    expect(parsedManifest.value.start_url.value).toBe(EXAMPLE_DOC_URL);
+    expect(parsedManifest.value.display.value).toBe('browser');
+    expect(parsedManifest.value.orientation.value).toBe(undefined);
+    expect(parsedManifest.value.theme_color.value).toBe(undefined);
+    expect(parsedManifest.value.background_color.value).toBe(undefined);
+    expect(parsedManifest.value.icons.value).toHaveLength(0);
     // TODO:
     // related_applications
     // prefer_related_applications
   });
 
+  it('should warn on invalid manifest parser URL', function() {
+    const parsedManifest = manifestParser('{}', 'not a URL', EXAMPLE_DOC_URL);
+    expect(parsedManifest.warning)
+      .toEqual('ERROR: invalid manifest URL: \'not a URL\'');
+  });
+
+  it('should warn on valid but non-(HTTP|HTTPS) manifest parser URL', function() {
+    const parsedManifest = manifestParser('{}', EXAMPLE_MANIFEST_BLOB_URL, EXAMPLE_DOC_URL);
+    expect(parsedManifest.warning)
+      .toEqual('WARNING: manifest URL not available over a valid network protocol');
+  });
+
   describe('icon parsing', function() {
     // 9.7
     it('gives an empty array and an error for erroneous icons entry', () => {
-      const parsedManifest = manifestParser('{"icons": {"16": "img/icons/icon16.png"}}',
-          EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL);
-      assert.ok(!parsedManifest.warning);
+      const parsedManifest = manifestParser(
+        '{"icons": {"16": "img/icons/icon16.png"}}',
+        EXAMPLE_MANIFEST_URL,
+        EXAMPLE_DOC_URL
+      );
+
+      expect(parsedManifest.warning).toBeUndefined();
       const icons = parsedManifest.value.icons;
       assert.ok(Array.isArray(icons.value));
       assert.equal(icons.value.length, 0);
@@ -60,9 +77,8 @@ describe('Manifest Parser', function() {
     });
 
     it('gives an empty array and no error for missing icons entry', () => {
-      const parsedManifest = manifestParser('{}',
-          EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL);
-      assert.ok(!parsedManifest.warning);
+      const parsedManifest = manifestParser('{}', EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL);
+      expect(parsedManifest.warning).toBeUndefined();
       const icons = parsedManifest.value.icons;
       assert.ok(Array.isArray(icons.value));
       assert.equal(icons.value.length, 0);
@@ -70,9 +86,12 @@ describe('Manifest Parser', function() {
     });
 
     it('parses basic string', function() {
-      const parsedManifest = manifestParser('{"icons": [{"src": "192.png", "sizes": "192x192"}]}',
-          EXAMPLE_MANIFEST_URL, EXAMPLE_DOC_URL);
-      assert(!parsedManifest.warning);
+      const parsedManifest = manifestParser(
+        '{"icons": [{"src": "192.png", "sizes": "192x192"}]}',
+        EXAMPLE_MANIFEST_URL,
+        EXAMPLE_DOC_URL
+      );
+      expect(parsedManifest.warning).toBeUndefined();
       const icons = parsedManifest.value.icons;
       assert(!icons.warning);
       const icon192 = icons.value[0];
@@ -81,9 +100,12 @@ describe('Manifest Parser', function() {
     });
 
     it('finds four icons in the stub manifest', function() {
-      const parsedManifest = manifestParser(JSON.stringify(manifestStub), EXAMPLE_MANIFEST_URL,
-          EXAMPLE_DOC_URL);
-      assert(!parsedManifest.warning);
+      const parsedManifest = manifestParser(
+        JSON.stringify(manifestStub),
+        EXAMPLE_MANIFEST_URL,
+        EXAMPLE_DOC_URL
+      );
+      expect(parsedManifest.warning).toBeUndefined();
       assert.equal(parsedManifest.value.icons.value.length, 4);
     });
 
@@ -110,6 +132,19 @@ describe('Manifest Parser', function() {
       assert.equal(icons.value.length, 0);
     });
 
+    it('parses icons and discards any with invalid base URL values', () => {
+      const manifestSrc = JSON.stringify({
+        icons: [{
+          src: '/valid/path',
+        }],
+      });
+      const parsedManifest = manifestParser(manifestSrc, EXAMPLE_MANIFEST_BLOB_URL,
+        EXAMPLE_DOC_URL);
+      const icons = parsedManifest.value.icons;
+      expect(icons.value).toHaveLength(0);
+      expect(icons.warning).toEqual('WARNING: Some icons were ignored due to warnings.');
+    });
+
     it('parses icons and discards any with undefined or empty string src values', () => {
       const manifestSrc = JSON.stringify({
         icons: [{
@@ -139,23 +174,23 @@ describe('Manifest Parser', function() {
   describe('name parsing', function() {
     it('parses basic string', function() {
       const parsedManifest = noUrlManifestParser('{"name":"foo"}');
-      assert(!parsedManifest.warning);
+      expect(parsedManifest.warning).toBeUndefined();
       assert.equal(parsedManifest.value.name.value, 'foo');
     });
 
     it('trims whitespaces', function() {
       const parsedManifest = noUrlManifestParser('{"name":" foo "}');
-      assert(!parsedManifest.warning);
+      expect(parsedManifest.warning).toBeUndefined();
       assert.equal(parsedManifest.value.name.value, 'foo');
     });
 
     it('doesn\'t parse non-string', function() {
       let parsedManifest = noUrlManifestParser('{"name": {} }');
-      assert(!parsedManifest.warning);
+      expect(parsedManifest.warning).toBeUndefined();
       assert.equal(parsedManifest.value.name.value, undefined);
 
       parsedManifest = noUrlManifestParser('{"name": 42 }');
-      assert(!parsedManifest.warning);
+      expect(parsedManifest.warning).toBeUndefined();
       assert.equal(parsedManifest.value.name.value, undefined);
     });
   });
@@ -163,23 +198,23 @@ describe('Manifest Parser', function() {
   describe('short_name parsing', function() {
     it('parses basic string', function() {
       const parsedManifest = noUrlManifestParser('{"short_name":"foo"}');
-      assert(!parsedManifest.warning);
+      expect(parsedManifest.warning).toBeUndefined();
       assert.equal(parsedManifest.value.short_name.value, 'foo');
     });
 
     it('trims whitespaces', function() {
       const parsedManifest = noUrlManifestParser('{"short_name":" foo "}');
-      assert(!parsedManifest.warning);
+      expect(parsedManifest.warning).toBeUndefined();
       assert.equal(parsedManifest.value.short_name.value, 'foo');
     });
 
     it('doesn\'t parse non-string', function() {
       let parsedManifest = noUrlManifestParser('{"short_name": {} }');
-      assert(!parsedManifest.warning);
+      expect(parsedManifest.warning).toBeUndefined();
       assert.equal(parsedManifest.value.short_name.value, undefined);
 
       parsedManifest = noUrlManifestParser('{"short_name": 42 }');
-      assert(!parsedManifest.warning);
+      expect(parsedManifest.warning).toBeUndefined();
       assert.equal(parsedManifest.value.short_name.value, undefined);
     });
   });