Bug 1314492 refactor webrequest tests, r=kmag
authorShane Caraveo <scaraveo@mozilla.com>
Thu, 10 Nov 2016 16:01:50 -0800
changeset 364854 df489872d561d35b4b416651d338784575de4911
parent 364853 0d8346f8bbcbf47bcae441656caaf13f17d4a83a
child 364855 95ef280ccc1bafebc7bdcbf47c399713195e5b62
push id6795
push userjlund@mozilla.com
push dateMon, 23 Jan 2017 14:19:46 +0000
treeherdermozilla-beta@76101b503191 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1314492
milestone52.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1314492 refactor webrequest tests, r=kmag MozReview-Commit-ID: D0dleERLM3K
toolkit/components/extensions/test/mochitest/.eslintrc.js
toolkit/components/extensions/test/mochitest/file_WebRequest_page1.html
toolkit/components/extensions/test/mochitest/file_WebRequest_page2.html
toolkit/components/extensions/test/mochitest/head_webrequest.js
toolkit/components/extensions/test/mochitest/mochitest.ini
toolkit/components/extensions/test/mochitest/test_ext_webrequest.html
toolkit/components/extensions/test/mochitest/test_ext_webrequest_basic.html
toolkit/modules/addons/WebRequest.jsm
--- a/toolkit/components/extensions/test/mochitest/.eslintrc.js
+++ b/toolkit/components/extensions/test/mochitest/.eslintrc.js
@@ -14,14 +14,22 @@ module.exports = { // eslint-disable-lin
 
     "waitForLoad": true,
     "promiseConsoleOutput": true,
 
     "ExtensionTestUtils": false,
     "NetUtil": true,
     "webrequest_test": false,
     "XPCOMUtils": true,
+
+    // head_webrequest.js symbols
+    "addStylesheet": true,
+    "addLink": true,
+    "addImage": true,
+    "addScript": true,
+    "addFrame": true,
+    "makeExtension": false,
   },
 
   "rules": {
     "no-shadow": 0,
   },
 };
deleted file mode 100644
--- a/toolkit/components/extensions/test/mochitest/file_WebRequest_page1.html
+++ /dev/null
@@ -1,43 +0,0 @@
-<!DOCTYPE HTML>
-
-<html>
-<head>
-<meta charset="utf-8">
-<link rel="stylesheet" href="file_style_good.css">
-<link rel="stylesheet" href="file_style_bad.css">
-<link rel="stylesheet" href="file_style_redirect.css">
-</head>
-<body>
-
-<div id="test">Sample text</div>
-
-<img id="img_redirect" src="file_image_redirect.png">
-<img id="img_good" src="file_image_good.png">
-<img id="img_bad" src="file_image_bad.png">
-
-<script src="file_script_good.js"></script>
-<script src="file_script_bad.js"></script>
-<script src="file_script_redirect.js"></script>
-
-<script src="file_script_xhr.js"></script>
-
-<script src="nonexistent_script_url.js"></script>
-
-<iframe src="file_WebRequest_page2.html" width="200" height="200"></iframe>
-<iframe src="redirection.sjs" width="200" height="200"></iframe>
-<iframe src="data:text/plain,webRequestTest" width="200" height="200"></iframe>
-<iframe src="data:text/plain,webRequestTest_bad" width="200" height="200"></iframe>
-<iframe src="https://invalid.localhost/" width="200" height="200"></iframe>
-<a href="file_WebRequest_page3.html?trigger=a" target="webrequest_link">link</a>
-<form method="post" action="file_WebRequest_page3.html?trigger=form" target="webrequest_form"></form>
-<script>
-"use strict";
-for (let a of document.links) {
-  a.click();
-}
-for (let f of document.forms) {
-  f.submit();
-}
-</script>
-</body>
-</html>
deleted file mode 100644
--- a/toolkit/components/extensions/test/mochitest/file_WebRequest_page2.html
+++ /dev/null
@@ -1,25 +0,0 @@
-<!DOCTYPE HTML>
-
-<html>
-<head>
-<meta charset="utf-8">
-<link rel="stylesheet" href="file_style_good.css">
-<link rel="stylesheet" href="file_style_bad.css">
-<link rel="stylesheet" href="file_style_redirect.css">
-</head>
-<body>
-
-<div class="test">Sample text</div>
-
-<img id="img_good" src="file_image_good.png">
-<img id="img_bad" src="file_image_bad.png">
-<img id="img_redirect" src="file_image_redirect.png">
-
-<script src="file_script_good.js"></script>
-<script src="file_script_bad.js"></script>
-<script src="file_script_redirect.js"></script>
-
-<script src="nonexistent_script_url.js"></script>
-
-</body>
-</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/head_webrequest.js
@@ -0,0 +1,315 @@
+"use strict";
+
+let commonEvents = {
+  "onBeforeRequest":     [{urls: ["<all_urls>"]}, ["blocking"]],
+  "onBeforeSendHeaders": [{urls: ["<all_urls>"]}, ["blocking", "requestHeaders"]],
+  "onSendHeaders":       [{urls: ["<all_urls>"]}, ["requestHeaders"]],
+  "onBeforeRedirect":    [{urls: ["<all_urls>"]}],
+  "onHeadersReceived":   [{urls: ["<all_urls>"]}, ["blocking", "responseHeaders"]],
+  "onResponseStarted":   [{urls: ["<all_urls>"]}],
+  "onCompleted":         [{urls: ["<all_urls>"]}, ["responseHeaders"]],
+  "onErrorOccurred":     [{urls: ["<all_urls>"]}],
+};
+
+function background(events) {
+  let expect;
+  let defaultOrigin;
+
+  browser.test.onMessage.addListener((msg, expected) => {
+    if (msg !== "set-expected") {
+      return;
+    }
+    expect = expected.expect;
+    defaultOrigin = expected.origin;
+    let promises = [];
+    // Initialize some stuff we'll need in the tests.
+    for (let entry of Object.values(expect)) {
+      // a place for the test infrastructure to store some state.
+      entry.test = {};
+      // Each entry in expected gets a Promise that will be resolved in the
+      // last event for that entry.  This will either be onCompleted, or the
+      // last entry if an events list was provided.
+      promises.push(new Promise(resolve => { entry.test.resolve = resolve; }));
+      // If events was left undefined, we're expecting all normal events we're
+      // listening for, exclude onBeforeRedirect and onErrorOccurred
+      if (entry.events === undefined) {
+        entry.events = Object.keys(events).filter(name => name != "onErrorOccurred" && name != "onBeforeRedirect");
+      }
+    }
+    // When every expected entry has finished our test is done.
+    Promise.all(promises).then(() => {
+      browser.test.sendMessage("done");
+    });
+    browser.test.sendMessage("continue");
+  });
+
+  // Retrieve the per-file/test expected values.
+  function getExpected(details) {
+    let url = new URL(details.url);
+    let filename;
+    if (url.protocol == "data:") {
+      // pathname is everything after protocol.
+      filename = url.pathname;
+    } else {
+      filename = url.pathname.split("/").pop();
+    }
+    let expected = expect[filename];
+    if (!expected) {
+      browser.test.fail(`unexpected request ${filename}`);
+      return;
+    }
+    // Save filename for redirect verification.
+    expected.test.filename = filename;
+    return expected;
+  }
+
+  // Process any test header modifications that can happen in request or response phases.
+  // If a test includes headers, it needs a complete header object, no undefined
+  // objects even if empty:
+  // request: {
+  //   add: {"HeaderName": "value",},
+  //   modify: {"HeaderName": "value",},
+  //   remove: ["HeaderName",],
+  // },
+  // response: {
+  //   add: {"HeaderName": "value",},
+  //   modify: {"HeaderName": "value",},
+  //   remove: ["HeaderName",],
+  // },
+  function processHeaders(phase, expected, details) {
+    // This should only happen once per phase [request|response].
+    browser.test.assertFalse(!!expected.test[phase], `First processing of headers for ${phase}`);
+    expected.test[phase] = true;
+
+    let headers = details[`${phase}Headers`];
+    browser.test.assertTrue(Array.isArray(headers), `${phase}Headers array present`);
+
+    let {add, modify, remove} = expected.headers[phase];
+
+    for (let name in add) {
+      browser.test.assertTrue(!headers.find(h => h.name === name), `header ${name} to be added not present yet in ${phase}Headers`);
+      let header = {name: name};
+      if (name.endsWith("-binary")) {
+        header.binaryValue = Array.from(add[name], c => c.charCodeAt(0));
+      } else {
+        header.value = add[name];
+      }
+      headers.push(header);
+    }
+
+    let modifiedAny = false;
+    for (let header of headers) {
+      if (header.name.toLowerCase() in modify) {
+        header.value = modify[header.name.toLowerCase()];
+        modifiedAny = true;
+      }
+    }
+    browser.test.assertTrue(modifiedAny, `at least one ${phase}Headers element to modify`);
+
+    let deletedAny = false;
+    for (let j = headers.length; j-- > 0;) {
+      if (remove.includes(headers[j].name.toLowerCase())) {
+        headers.splice(j, 1);
+        deletedAny = true;
+      }
+    }
+    browser.test.assertTrue(deletedAny, `at least one ${phase}Headers element to delete`);
+
+    return headers;
+  }
+
+  // phase is request or response.
+  function checkHeaders(phase, expected, details) {
+    if (!/^https?:/.test(details.url)) {
+      return;
+    }
+
+    let headers = details[`${phase}Headers`];
+    browser.test.assertTrue(Array.isArray(headers), `valid ${phase}Headers array`);
+
+    let {add, modify, remove} = expected.headers[phase];
+    for (let name in add) {
+      let value = headers.find(h => h.name.toLowerCase() === name.toLowerCase()).value;
+      browser.test.assertEq(value, add[name], `header ${name} correctly injected in ${phase}Headers`);
+    }
+
+    for (let name in modify) {
+      let value = headers.find(h => h.name.toLowerCase() === name.toLowerCase()).value;
+      browser.test.assertEq(value, modify[name], `header ${name} matches modified value`);
+    }
+
+    for (let name of remove) {
+      let found = headers.find(h => h.name.toLowerCase() === name.toLowerCase());
+      browser.test.assertFalse(!!found, `deleted header ${name} still found in ${phase}Headers`);
+    }
+  }
+
+  function getListener(name) {
+    return details => {
+      let result = {};
+      browser.test.log(`${name} ${details.requestId} ${details.url}`);
+      let expected = getExpected(details);
+      if (!expected) {
+        return result;
+      }
+      let expectedEvent = expected.events[0] == name;
+      browser.test.assertTrue(expectedEvent, `recieved ${name}`);
+      if (expectedEvent) {
+        expected.events.shift();
+      }
+      browser.test.assertEq(expected.type, details.type, "resource type is correct");
+      browser.test.assertEq(expected.origin || defaultOrigin, details.originUrl, "origin is correct");
+
+      if (name == "onBeforeRequest") {
+        // Save some values to test request consistency in later events.
+        browser.test.assertTrue(details.tabId !== undefined, `tabId ${details.tabId}`);
+        browser.test.assertTrue(details.requestId !== undefined, `requestId ${details.requestId}`);
+        // Validate requestId if it's already set, this happens with redirects.
+        if (expected.test.requestId !== undefined) {
+          browser.test.assertEq("string", typeof expected.test.requestId, `requestid ${expected.test.requestId} is string`);
+          browser.test.assertEq("string", typeof details.requestId, `requestid ${details.requestId} is string`);
+          browser.test.assertEq("number", typeof parseInt(details.requestId, 10), "parsed requestid is number");
+          browser.test.assertNotEq(expected.test.requestId, details.requestId,
+                                  `last requestId ${expected.test.requestId} different from this one ${details.requestId}`);
+        } else {
+          // Save any values we want to validate in later events.
+          expected.test.requestId = details.requestId;
+          expected.test.tabId = details.tabId;
+        }
+        // Tests we don't need to do every event.
+        browser.test.assertTrue(details.type.toUpperCase() in browser.webRequest.ResourceType, `valid resource type ${details.type}`);
+      } else {
+        // On events after onBeforeRequest, check the previous values.
+        browser.test.assertEq(expected.test.requestId, details.requestId, "correct requestId");
+        browser.test.assertEq(expected.test.tabId, details.tabId, "correct tabId");
+      }
+      if (name == "onBeforeSendHeaders") {
+        if (expected.headers && expected.headers.request) {
+          result.requestHeaders = processHeaders("request", expected, details);
+        }
+        if (expected.redirect) {
+          browser.test.log(`${name} redirect request`);
+          result.redirectUrl = details.url.replace(expected.test.filename, expected.redirect);
+        }
+      }
+      if (name == "onSendHeaders") {
+        if (expected.headers && expected.headers.request) {
+          checkHeaders("request", expected, details);
+        }
+      }
+      if (name == "onHeadersReceived") {
+        browser.test.assertEq(expected.status || 200, details.statusCode,
+                              `expected HTTP status recieved for ${details.url}`);
+        if (expected.headers && expected.headers.response) {
+          result.responseHeaders = processHeaders("response", expected, details);
+        }
+      }
+      if (name == "onCompleted") {
+        // If we have already completed a GET request for this url,
+        // and it was found, we expect for the response to come fromCache.
+        // expected.cached may be undefined, force boolean.
+        let expectCached = !!expected.cached && details.method === "GET" && details.statusCode != 404;
+        browser.test.assertEq(expectCached, details.fromCache, "fromCache is correct");
+        // We can only tell IPs for non-cached HTTP requests.
+        if (!details.fromCache && /^https?:/.test(details.url)) {
+          browser.test.assertEq("127.0.0.1", details.ip, `correct ip for ${details.url}`);
+        }
+        if (expected.headers && expected.headers.response) {
+          checkHeaders("response", expected, details);
+        }
+      }
+
+      if (expected.cancel && expected.cancel == name) {
+        browser.test.log(`${name} cancel request`);
+        browser.test.sendMessage("cancelled");
+        result.cancel = true;
+      }
+      // If we've used up all the events for this test, resolve the promise.
+      // If something wrong happens and more events come through, there will be
+      // failures.
+      if (expected.events.length <= 0) {
+        expected.test.resolve();
+      }
+      return result;
+    };
+  }
+
+  for (let [name, args] of Object.entries(events)) {
+    browser.test.log(`adding listener for ${name}`);
+    try {
+      browser.webRequest[name].addListener(getListener(name), ...args);
+    } catch (e) {
+      browser.test.assertTrue(/\brequestBody\b/.test(e.message),
+                              "Request body is unsupported");
+
+      // RequestBody is disabled in release builds.
+      if (!/\brequestBody\b/.test(e.message)) {
+        throw e;
+      }
+
+      args.splice(args.indexOf("requestBody"), 1);
+      browser.webRequest[name].addListener(getListener(name), ...args);
+    }
+  }
+}
+
+/* exported makeExtension */
+
+function makeExtension(events = commonEvents) {
+  return ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: [
+        "webRequest",
+        "webRequestBlocking",
+        "<all_urls>",
+      ],
+    },
+    background: `(${background})(${JSON.stringify(events)})`,
+  });
+}
+
+/* exported addStylesheet */
+
+function addStylesheet(file) {
+  let link = document.createElement("link");
+  link.setAttribute("rel", "stylesheet");
+  link.setAttribute("href", file);
+  document.body.appendChild(link);
+}
+
+/* exported addLink */
+
+function addLink(file) {
+  let a = document.createElement("a");
+  a.setAttribute("href", file);
+  a.setAttribute("target", "_blank");
+  document.body.appendChild(a);
+  return a;
+}
+
+/* exported addImage */
+
+function addImage(file) {
+  let img = document.createElement("img");
+  img.setAttribute("src", file);
+  document.body.appendChild(img);
+}
+
+/* exported addScript */
+
+function addScript(file) {
+  let script = document.createElement("script");
+  script.setAttribute("type", "text/javascript");
+  script.setAttribute("src", file);
+  document.getElementsByTagName("head").item(0).appendChild(script);
+}
+
+/* exported addFrame */
+
+function addFrame(file) {
+  let frame = document.createElement("iframe");
+  frame.setAttribute("width", "200");
+  frame.setAttribute("height", "200");
+  frame.setAttribute("src", file);
+  document.body.appendChild(frame);
+}
--- a/toolkit/components/extensions/test/mochitest/mochitest.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest.ini
@@ -1,16 +1,15 @@
 [DEFAULT]
 support-files =
   head.js
   file_mixed.html
+  head_webrequest.js
   file_csp.html
   file_csp.html^headers^
-  file_WebRequest_page1.html
-  file_WebRequest_page2.html
   file_WebRequest_page3.html
   file_webNavigation_clientRedirect.html
   file_webNavigation_clientRedirect_httpHeaders.html
   file_webNavigation_clientRedirect_httpHeaders.html^headers^
   file_webNavigation_frameClientRedirect.html
   file_webNavigation_frameRedirect.html
   file_webNavigation_manualSubframe.html
   file_webNavigation_manualSubframe_page1.html
@@ -88,19 +87,19 @@ skip-if = os == 'android' # Bug 1258975 
 [test_ext_background_teardown.html]
 [test_ext_tab_teardown.html]
 skip-if = (os == 'android') # Android does not support tabs API. Bug 1260250
 [test_ext_unload_frame.html]
 [test_ext_i18n.html]
 skip-if = (os == 'android') # Bug 1258975 on android.
 [test_ext_web_accessible_resources.html]
 skip-if = (os == 'android') # Bug 1258975 on android.
-[test_ext_webrequest.html]
+[test_ext_webrequest_background_events.html]
 skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
-[test_ext_webrequest_background_events.html]
+[test_ext_webrequest_basic.html]
 skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
 [test_ext_webrequest_suspend.html]
 skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
 [test_ext_webrequest_upload.html]
 skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
 [test_ext_webnavigation.html]
 skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
 [test_ext_webnavigation_filters.html]
deleted file mode 100644
--- a/toolkit/components/extensions/test/mochitest/test_ext_webrequest.html
+++ /dev/null
@@ -1,548 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<head>
-  <title>Test for simple WebExtension</title>
-  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
-  <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
-  <script type="text/javascript" src="head.js"></script>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-</head>
-<body>
-
-<script type="text/javascript">
-"use strict";
-
-SimpleTest.requestCompleteLog();
-
-const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest";
-
-const expected_requested = [BASE + "/file_WebRequest_page1.html",
-                            BASE + "/file_style_good.css",
-                            BASE + "/file_style_bad.css",
-                            BASE + "/file_style_redirect.css",
-                            BASE + "/file_image_good.png",
-                            BASE + "/file_image_bad.png",
-                            BASE + "/file_image_redirect.png",
-                            BASE + "/file_script_good.js",
-                            BASE + "/file_script_bad.js",
-                            BASE + "/file_script_redirect.js",
-                            BASE + "/file_script_xhr.js",
-                            BASE + "/file_WebRequest_page2.html",
-                            BASE + "/nonexistent_script_url.js",
-                            BASE + "/redirection.sjs",
-                            BASE + "/dummy_page.html",
-                            BASE + "/xhr_resource",
-                            "https://invalid.localhost/",
-                            "data:text/plain,webRequestTest_bad",
-                            "data:text/plain,webRequestTest"];
-
-const expected_beforeSendHeaders = [BASE + "/file_WebRequest_page1.html",
-                              BASE + "/file_style_good.css",
-                              BASE + "/file_style_redirect.css",
-                              BASE + "/file_image_good.png",
-                              BASE + "/file_image_redirect.png",
-                              BASE + "/file_script_good.js",
-                              BASE + "/file_script_redirect.js",
-                              BASE + "/file_script_xhr.js",
-                              BASE + "/file_WebRequest_page2.html",
-                              BASE + "/nonexistent_script_url.js",
-                              BASE + "/redirection.sjs",
-                              BASE + "/dummy_page.html",
-                              BASE + "/xhr_resource",
-                              "https://invalid.localhost/"];
-
-const expected_sendHeaders = expected_beforeSendHeaders.filter(u => !/_redirect\./.test(u))
-                            .concat(BASE + "/redirection.sjs");
-
-const expected_redirect = expected_beforeSendHeaders.filter(u => /_redirect\./.test(u))
-                            .concat(BASE + "/redirection.sjs");
-
-const expected_response = [BASE + "/file_WebRequest_page1.html",
-                           BASE + "/file_style_good.css",
-                           BASE + "/file_image_good.png",
-                           BASE + "/file_script_good.js",
-                           BASE + "/file_script_xhr.js",
-                           BASE + "/file_WebRequest_page2.html",
-                           BASE + "/nonexistent_script_url.js",
-                           BASE + "/dummy_page.html",
-                           BASE + "/xhr_resource"];
-
-const expected_error = expected_requested.filter(u => /_bad\b|\binvalid\b/.test(u));
-
-const expected_complete = expected_response.concat("data:text/plain,webRequestTest");
-
-function removeDupes(list) {
-  let j = 0;
-  for (let i = 1; i < list.length; i++) {
-    if (list[i] != list[j]) {
-      j++;
-      if (i != j) {
-        list[j] = list[i];
-      }
-    }
-  }
-  list.length = j + 1;
-}
-
-function compareLists(list1, list2, kind) {
-  list1.sort();
-  removeDupes(list1);
-  list2.sort();
-  removeDupes(list2);
-  is(String(list1), String(list2), `${kind} URLs correct`);
-}
-
-function backgroundScript() {
-  let checkCompleted = true;
-  let savedTabId = -1;
-
-  function shouldRecord(url) {
-    return url.startsWith(BASE) && !url.includes("_page3.html") ||
-           /^data:.*\bwebRequestTest|\/invalid\./.test(url);
-  }
-
-  let statuses = [
-    {url: /_script_good\b/, code: 200, line: /^HTTP\/1.1 200 OK\b/i},
-    {url: /\bredirection\b/, code: 302, line: /^HTTP\/1.1 302\b/},
-    {url: /\bnonexistent_script_/, code: 404, line: /^HTTP\/1.1 404 Not Found\b/i},
-  ];
-  function checkStatus(details) {
-    for (let {url, code, line} of statuses) {
-      if (url.test(details.url)) {
-        browser.test.assertEq(code, details.statusCode, `HTTP status code ${code} for ${details.url} (found ${details.statusCode})`);
-        browser.test.assertTrue(line.test(details.statusLine), `HTTP status line ${line} for ${details.url} (found ${details.statusLine})`);
-      }
-    }
-  }
-
-  function checkOrigin(details) {
-    let isCorrectOrigin = details.url.includes("_page1.html") ? details.originUrl.endsWith("/test_ext_webrequest.html")
-                                                              : /\/file_WebRequest_page\d\.html\b/.test(details.originUrl);
-    browser.test.assertTrue(isCorrectOrigin, `originUrl for ${details.url} is correct (${details.originUrl})`);
-  }
-
-  function checkType(details) {
-    let expected_type = "???";
-    if (details.url.includes("style")) {
-      expected_type = "stylesheet";
-    } else if (details.url.includes("image")) {
-      expected_type = "image";
-    } else if (details.url.includes("script")) {
-      expected_type = "script";
-    } else if (details.url.includes("page1")) {
-      expected_type = "main_frame";
-    } else if (/page2|redirection|dummy_page|data:text\/(?:plain|html),|\/\/invalid\b/.test(details.url)) {
-      expected_type = "sub_frame";
-    } else if (details.url.includes("xhr")) {
-      expected_type = "xmlhttprequest";
-    }
-    browser.test.assertEq(details.type, expected_type, "resource type is correct");
-  }
-
-  let requestIDs = new Map();
-  let idDisposalEvents = new Set(["completed", "error", "redirect"]);
-  function checkRequestId(details, event = "unknown") {
-    let ids = requestIDs.get(details.url);
-    browser.test.assertTrue(ids && ids.has(details.requestId), `correct requestId for ${details.url} in ${event} (${details.requestId} in [${ids && [...ids].join(", ")}])`);
-    if (ids && idDisposalEvents.has(event)) {
-      ids.delete(details.requestId);
-    }
-  }
-
-  let frameIDs = new Map();
-  let skippedRequests = new Set();
-  let redirectedRequests = new Set();
-
-  let recorded = {requested: [],
-                  beforeSendHeaders: [],
-                  beforeRedirect: [],
-                  sendHeaders: [],
-                  responseStarted: [],
-                  responseStarted2: [],
-                  error: [],
-                  completed: [],
-                 };
-  let testHeaders = {
-    request: {
-      added: {
-        "X-WebRequest-request": "text",
-        "X-WebRequest-request-binary": "binary",
-      },
-      modified: {
-        "user-agent": "WebRequest",
-      },
-      deleted: [
-        "referer",
-      ],
-    },
-    response: {
-      added: {
-        "X-WebRequest-response": "text",
-        "X-WebRequest-response-binary": "binary",
-      },
-      modified: {
-        "server": "WebRequest",
-        "content-type": "text/html; charset=utf-8",
-      },
-      deleted: [
-        "connection",
-      ],
-    },
-  };
-
-  function checkResourceType(type) {
-    let key = type.toUpperCase();
-    browser.test.assertTrue(key in browser.webRequest.ResourceType, `valid resource type ${key}`);
-  }
-
-  function processHeaders(phase, details) {
-    let headers = details[`${phase}Headers`];
-    browser.test.assertTrue(Array.isArray(headers), `${phase}Headers array present`);
-
-    let processedMark = "webrequest-processed";
-    if (headers.find(h => h.name.toLowerCase() === processedMark)) {
-      // This may happen because of redirections or cache
-      browser.test.log(`${phase}Headers in ${details.requestId} already processed`);
-      skippedRequests.add(details.requestId);
-      return null;
-    }
-    headers.push({name: processedMark, value: "1"});
-
-    let {added, modified, deleted} = testHeaders[phase];
-
-    for (let name in added) {
-      browser.test.assertTrue(!headers.find(h => h.name === name), `header ${name} to be added not present yet in ${phase}Headers`);
-      let header = {name: name};
-      if (name.endsWith("-binary")) {
-        header.binaryValue = Array.from(added[name], c => c.charCodeAt(0));
-      } else {
-        header.value = added[name];
-      }
-      headers.push(header);
-    }
-
-    let modifiedAny = false;
-    for (let header of headers) {
-      if (header.name.toLowerCase() in modified) {
-        header.value = modified[header.name.toLowerCase()];
-        modifiedAny = true;
-      }
-    }
-    browser.test.assertTrue(modifiedAny, `at least one ${phase}Headers element to modify`);
-
-    let deletedAny = false;
-    for (let j = headers.length; j-- > 0;) {
-      if (deleted.includes(headers[j].name.toLowerCase())) {
-        headers.splice(j, 1);
-        deletedAny = true;
-      }
-    }
-    browser.test.assertTrue(deletedAny, `at least one ${phase}Headers element to delete`);
-
-    return headers;
-  }
-
-  function checkHeaders(phase, details) {
-    if (!/^https?:/.test(details.url)) {
-      return;
-    }
-
-    let headers = details[`${phase}Headers`];
-    browser.test.assertTrue(Array.isArray(headers), `valid ${phase}Headers array`);
-
-    let {added, modified, deleted} = testHeaders[phase];
-    for (let name in added) {
-      browser.test.assertTrue(headers.some(h => h.name.toLowerCase() === name.toLowerCase() && h.value === added[name]), `header ${name} correctly injected in ${phase}Headers`);
-    }
-
-    let modifiedAny = false;
-    for (let header of headers.filter(h => h.name in modified)) {
-      let {name, value} = header;
-      if (name.toLowerCase() === "content-type" && skippedRequests.has(details.requestId)) {
-        // Changes to Content-Type headers are not persisted in the cache.
-        continue;
-      }
-
-      browser.test.assertTrue(value === modified[name], `header "${name}: ${value}" matches modified value ("${modified[name]}")`);
-      modifiedAny = true;
-    }
-    browser.test.assertTrue(modifiedAny, `at least one modified ${phase}Headers element`);
-
-    for (let name of deleted) {
-      browser.test.assertFalse(headers.some(h => h.name === name), `deleted header ${name} still found in ${phase}Headers`);
-    }
-  }
-
-  let lastRequestId = -1;
-  let lastRequestUrl = null;
-  function validateRequestIdType(currentId) {
-    browser.test.assertTrue(typeof lastRequestId === "string");
-    browser.test.assertTrue(typeof currentId === "string");
-    browser.test.assertTrue(typeof parseInt(currentId, 10) === "number");
-    browser.test.assertTrue(parseInt(lastRequestId, 10) !== parseInt(currentId, 10));
-  }
-
-  function onBeforeRequest(details) {
-    browser.test.log(`onBeforeRequest ${details.requestId} ${details.url}`);
-
-    if (!lastRequestUrl) {
-      lastRequestUrl = details.url;
-      lastRequestId = details.requestId;
-    } else if (lastRequestUrl != details.url) {
-      validateRequestIdType(details.requestId);
-    }
-
-    let ids = requestIDs.get(details.url);
-    if (ids) {
-      ids.add(details.requestId);
-    } else {
-      requestIDs.set(details.url, new Set([details.requestId]));
-    }
-    checkResourceType(details.type);
-    checkOrigin(details);
-    if (shouldRecord(details.url)) {
-      recorded.requested.push(details.url);
-
-      if (savedTabId == -1) {
-        browser.test.assertTrue(details.tabId !== undefined, "tab ID defined");
-        savedTabId = details.tabId;
-      }
-
-      browser.test.assertEq(details.tabId, savedTabId, "correct tab ID");
-      checkType(details);
-
-      frameIDs.set(details.url, details.frameId);
-      if (details.url.includes("page1")) {
-        browser.test.assertEq(details.frameId, 0, "frame ID correct");
-        browser.test.assertEq(details.parentFrameId, -1, "parent frame ID correct");
-      }
-      if (details.url.includes("page2")) {
-        browser.test.assertTrue(details.frameId != 0, "sub-frame gets its own frame ID");
-        browser.test.assertTrue(details.frameId !== undefined, "sub-frame ID defined");
-        browser.test.assertEq(details.parentFrameId, 0, "parent frame id is correct");
-      }
-    }
-    if (details.url.includes("_bad")) {
-      return {cancel: true};
-    }
-    return {};
-  }
-
-  function onBeforeSendHeaders(details) {
-    browser.test.log(`onBeforeSendHeaders ${details.url}`);
-    checkRequestId(details);
-    checkOrigin(details);
-    checkResourceType(details.type);
-    processHeaders("request", details);
-    if (shouldRecord(details.url)) {
-      recorded.beforeSendHeaders.push(details.url);
-
-      browser.test.assertEq(details.tabId, savedTabId, "correct tab ID");
-      checkType(details);
-
-      let id = frameIDs.get(details.url);
-      browser.test.assertEq(id, details.frameId, "frame ID same in onBeforeSendHeaders as onBeforeRequest");
-    }
-    if (details.url.includes("_redirect.")) {
-      redirectedRequests.add(details.requestId);
-      return {redirectUrl: details.url.replace("_redirect.", "_good.")};
-    }
-    return {requestHeaders: details.requestHeaders};
-  }
-
-  function onBeforeRedirect(details) {
-    browser.test.log(`onBeforeRedirect ${details.url} -> ${details.redirectUrl}`);
-    checkRequestId(details, "redirect");
-    checkOrigin(details);
-    checkResourceType(details.type);
-    if (shouldRecord(details.url)) {
-      recorded.beforeRedirect.push(details.url);
-
-      browser.test.assertEq(details.tabId, savedTabId, "correct tab ID");
-      checkType(details);
-      checkStatus(details);
-
-      let id = frameIDs.get(details.url);
-      browser.test.assertEq(id, details.frameId, "frame ID same in onBeforeRedirect as onBeforeRequest");
-      frameIDs.set(details.redirectUrl, details.frameId);
-    }
-    if (details.url.includes("_redirect.")) {
-      let expectedUrl = details.url.replace("_redirect.", "_good.");
-      browser.test.assertEq(details.redirectUrl, expectedUrl, "correct redirectUrl value");
-    }
-    return {};
-  }
-
-  function onRecord(kind, details) {
-    browser.test.log(`${kind} ${details.requestId} ${details.url}`);
-    checkResourceType(details.type);
-    checkRequestId(details, kind);
-    checkOrigin(details);
-    if (kind in recorded && shouldRecord(details.url)) {
-      recorded[kind].push(details.url);
-    }
-  }
-
-  function onSendHeaders(details) {
-    onRecord("sendHeaders", details);
-    checkHeaders("request", details);
-  }
-
-  let completedUrls = {};
-
-  function checkIpAndRecord(kind, details) {
-    onRecord(kind, details);
-    // When resources are cached, the ip property is not present,
-    // so only check for the ip property the first time around.
-    if (!(kind in completedUrls)) {
-      completedUrls[kind] = new Set();
-    }
-    if (checkCompleted && !completedUrls[kind].has(details.url)) {
-      // We can only tell IPs for HTTP requests.
-      if (/^https?:/.test(details.url)) {
-        browser.test.assertEq(details.ip, "127.0.0.1", "correct ip");
-      }
-      completedUrls[kind].add(details.url);
-    }
-    checkStatus(details);
-  }
-
-  function checkFromCache(kind, details) {
-    if (checkCompleted) {
-      // If we have already completed a GET request for this url,
-      // and it was found, we expect for the response to come fromCache.
-      const completed = kind in completedUrls && completedUrls[kind].has(details.url);
-      const expected = completed && details.method === "GET" && details.statusCode != 404;
-      browser.test.assertEq(expected, details.fromCache, "fromCache is correct");
-    }
-    checkIpAndRecord(kind, details);
-  }
-
-  function onHeadersReceived(details) {
-    checkIpAndRecord("headersReceived", details);
-    processHeaders("response", details);
-    browser.test.log(`After processing response headers: ${details.responseHeaders.toSource()}`);
-    return {responseHeaders: details.responseHeaders};
-  }
-
-  function onErrorOccurred(details) {
-    if (details.url.endsWith("_good.png") && redirectedRequests.has(details.requestId)) {
-      // Redirected image requests sometimes result in multiple attempts to
-      // load the same image in parallel. In this case, the later request is
-      // canceled, and the same image loading context is shared by both images.
-      redirectedRequests.delete(details.requestId);
-      browser.test.assertEq("NS_BINDING_ABORTED", details.error, `onErrorOccurred reported for ${details.url}`);
-    } else {
-      onRecord("error", details);
-      browser.test.assertTrue(/^NS_ERROR_/.test(details.error), `onErrorOccurred reported for ${details.url} (${details.error})`);
-    }
-  }
-
-  function onCompleted(details) {
-    checkFromCache("completed", details);
-    checkHeaders("response", details);
-  }
-
-  browser.webRequest.onBeforeRequest.addListener(onBeforeRequest, {urls: ["<all_urls>"]}, ["blocking"]);
-  browser.webRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, {urls: ["<all_urls>"]}, ["blocking", "requestHeaders"]);
-  browser.webRequest.onSendHeaders.addListener(onSendHeaders, {urls: ["<all_urls>"]}, ["requestHeaders"]);
-  browser.webRequest.onBeforeRedirect.addListener(onBeforeRedirect, {urls: ["<all_urls>"]});
-  browser.webRequest.onHeadersReceived.addListener(onHeadersReceived, {urls: ["<all_urls>"]}, ["blocking", "responseHeaders"]);
-  browser.webRequest.onResponseStarted.addListener(checkFromCache.bind(null, "responseStarted"), {urls: ["<all_urls>"]});
-  browser.webRequest.onResponseStarted.addListener(checkFromCache.bind(null, "responseStarted2"), {urls: ["<all_urls>"]});
-  browser.webRequest.onErrorOccurred.addListener(onErrorOccurred, {urls: ["<all_urls>"]});
-  browser.webRequest.onCompleted.addListener(onCompleted, {urls: ["<all_urls>"]}, ["responseHeaders"]);
-
-  function onTestMessage(msg) {
-    if (msg == "skipCompleted") {
-      checkCompleted = false;
-      browser.test.sendMessage("ackSkipCompleted");
-    } else {
-      browser.test.sendMessage("results", recorded);
-    }
-  }
-
-  browser.test.onMessage.addListener(onTestMessage);
-
-  browser.test.sendMessage("ready", browser.webRequest.ResourceType);
-}
-
-function* test_once(skipCompleted) {
-  let extensionData = {
-    manifest: {
-      permissions: [
-        "webRequest",
-        "webRequestBlocking",
-      ],
-    },
-    background: `const BASE = ${JSON.stringify(BASE)}; (${backgroundScript})()`,
-  };
-
-  let extension = ExtensionTestUtils.loadExtension(extensionData);
-  yield extension.startup();
-  let resourceTypes = yield extension.awaitMessage("ready");
-  info("webrequest extension loaded");
-
-  if (skipCompleted) {
-    extension.sendMessage("skipCompleted");
-    yield extension.awaitMessage("ackSkipCompleted");
-  }
-
-  for (let key in resourceTypes) {
-    let value = resourceTypes[key];
-    is(key, value.toUpperCase());
-  }
-
-  // Check a few Firefox-specific types.
-  is(resourceTypes.XBL, "xbl", "XBL resource type supported");
-  is(resourceTypes.FONT, "font", "Font resource type supported");
-  is(resourceTypes.WEBSOCKET, "websocket", "Websocket resource type supported");
-
-  yield new Promise(resolve => { setTimeout(resolve, 0); });
-
-  let win = window.open();
-
-  // Clear the image cache, since it gets in the way otherwise.
-  let imgTools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService(SpecialPowers.Ci.imgITools);
-  let cache = imgTools.getImgCacheForDocument(win.document);
-  cache.clearCache(false);
-
-  // yield waitForLoad(win);
-  info("about:blank loaded");
-
-  win.location = "file_WebRequest_page1.html";
-
-  yield waitForLoad(win);
-  info("test page loaded");
-
-  is(win.success, 2, "Good script ran");
-  is(win.failure, undefined, "Failure script didn't run");
-
-  let style = win.getComputedStyle(win.document.getElementById("test"), null);
-  is(style.getPropertyValue("color"), "rgb(255, 0, 0)", "Good CSS loaded");
-
-  win.close();
-
-  extension.sendMessage("getResults");
-  let recorded = yield extension.awaitMessage("results");
-
-  compareLists(recorded.requested, expected_requested, "requested");
-  compareLists(recorded.beforeSendHeaders, expected_beforeSendHeaders, "beforeSendHeaders");
-  compareLists(recorded.sendHeaders, expected_sendHeaders, "sendHeaders");
-  compareLists(recorded.beforeRedirect, expected_redirect, "beforeRedirect");
-  compareLists(recorded.responseStarted, expected_response, "responseStarted");
-  compareLists(recorded.error, expected_error, "error");
-  compareLists(recorded.completed, expected_complete, "completed");
-  compareLists(recorded.responseStarted2, recorded.responseStarted, "multiple non-blocking listeners");
-  yield extension.unload();
-  info("webrequest extension unloaded");
-}
-
-// Run the test twice to make sure it works with caching.
-add_task(function* () { yield test_once(false); });
-add_task(function* () { yield test_once(true); });
-</script>
-
-</body>
-</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_basic.html
@@ -0,0 +1,282 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="head.js"></script>
+  <script type="text/javascript" src="head_webrequest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script>
+"use strict";
+
+let extension;
+add_task(function* setup() {
+  // Clear the image cache, since it gets in the way otherwise.
+  let imgTools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService(SpecialPowers.Ci.imgITools);
+  let cache = imgTools.getImgCacheForDocument(document);
+  cache.clearCache(false);
+
+  extension = makeExtension();
+  yield extension.startup();
+});
+
+// expect is a set of test values used by the background script.
+//
+// type: type of request action
+// events: optional, If defined only the events listed are expected for the
+//                   request. If undefined, all events except onErrorOccurred
+//                   and onBeforeRedirect are expected.  Must be in order received.
+// redirect: url to redirect to during onBeforeSendHeaders
+// status: number    expected status during onHeadersReceived, 200 default
+// cancel: event in which we return cancel=true.  cancelled message is sent.
+// cached: expected fromCache value, default is false, checked in onCompletion
+// headers: request or response headers to modify
+
+add_task(function* test_webRequest_links() {
+  let expect = {
+    "file_style_bad.css": {
+      type: "stylesheet",
+      events: ["onBeforeRequest", "onErrorOccurred"],
+      cancel: "onBeforeRequest",
+    },
+    "file_style_redirect.css": {
+      status: 302,
+      type: "stylesheet",
+      events: ["onBeforeRequest", "onBeforeSendHeaders", "onBeforeRedirect"],
+      redirect: "file_style_good.css",
+    },
+    "file_style_good.css": {
+      type: "stylesheet",
+    },
+  };
+  extension.sendMessage("set-expected", {expect, origin: location.href});
+  yield extension.awaitMessage("continue");
+  addStylesheet("file_style_bad.css");
+  yield extension.awaitMessage("cancelled");
+  // we redirect to style_good which completes the test
+  addStylesheet("file_style_redirect.css");
+  yield extension.awaitMessage("done");
+
+  let style = window.getComputedStyle(document.getElementById("test"), null);
+  is(style.getPropertyValue("color"), "rgb(255, 0, 0)", "Good CSS loaded");
+});
+
+add_task(function* test_webRequest_images() {
+  let expect = {
+    "file_image_bad.png": {
+      type: "image",
+      events: ["onBeforeRequest", "onErrorOccurred"],
+      cancel: "onBeforeRequest",
+    },
+    "file_image_redirect.png": {
+      status: 302,
+      type: "image",
+      events: ["onBeforeRequest", "onBeforeSendHeaders", "onBeforeRedirect"],
+      redirect: "file_image_good.png",
+    },
+    "file_image_good.png": {
+      type: "image",
+    },
+  };
+  extension.sendMessage("set-expected", {expect, origin: location.href});
+  yield extension.awaitMessage("continue");
+  addImage("file_image_bad.png");
+  yield extension.awaitMessage("cancelled");
+  // we redirect to image_good which completes the test
+  addImage("file_image_redirect.png");
+  yield extension.awaitMessage("done");
+});
+
+add_task(function* test_webRequest_scripts() {
+  let expect = {
+    "file_script_bad.js": {
+      type: "script",
+      events: ["onBeforeRequest", "onErrorOccurred"],
+      cancel: "onBeforeRequest",
+    },
+    "file_script_redirect.js": {
+      status: 302,
+      type: "script",
+      events: ["onBeforeRequest", "onBeforeSendHeaders", "onBeforeRedirect"],
+      redirect: "file_script_good.js",
+    },
+    "file_script_good.js": {
+      type: "script",
+    },
+  };
+  extension.sendMessage("set-expected", {expect, origin: location.href});
+  yield extension.awaitMessage("continue");
+  addScript("file_script_bad.js");
+  yield extension.awaitMessage("cancelled");
+  // we redirect to script_good which completes the test
+  addScript("file_script_redirect.js");
+  yield extension.awaitMessage("done");
+
+  is(window.success, 1, "Good script ran");
+  is(window.failure, undefined, "Failure script didn't run");
+});
+
+add_task(function* test_webRequest_xhr_get() {
+  let expect = {
+    "file_script_xhr.js": {
+      type: "script",
+    },
+    "xhr_resource": {
+      status: 404,
+      type: "xmlhttprequest",
+    },
+  };
+  extension.sendMessage("set-expected", {expect, origin: location.href});
+  yield extension.awaitMessage("continue");
+  addScript("file_script_xhr.js");
+  yield extension.awaitMessage("done");
+});
+
+add_task(function* test_webRequest_nonexistent() {
+  let expect = {
+    "nonexistent_script_url.js": {
+      status: 404,
+      type: "script",
+    },
+  };
+  extension.sendMessage("set-expected", {expect, origin: location.href});
+  yield extension.awaitMessage("continue");
+  addScript("nonexistent_script_url.js");
+  yield extension.awaitMessage("done");
+});
+
+add_task(function* test_webRequest_checkCached() {
+  let expect = {
+    "file_image_good.png": {
+      type: "image",
+      cached: true,
+    },
+    "file_script_good.js": {
+      type: "script",
+      cached: true,
+    },
+    "file_style_good.css": {
+      type: "stylesheet",
+      cached: true,
+    },
+    "nonexistent_script_url.js": {
+      status: 404,
+      type: "script",
+      cached: false,
+    },
+  };
+  extension.sendMessage("set-expected", {expect, origin: location.href});
+  yield extension.awaitMessage("continue");
+  addImage("file_image_good.png");
+  addScript("file_script_good.js");
+  addStylesheet("file_style_good.css");
+  addScript("nonexistent_script_url.js");
+  yield extension.awaitMessage("done");
+
+  is(window.success, 2, "Good script ran");
+  is(window.failure, undefined, "Failure script didn't run");
+});
+
+add_task(function* test_webRequest_headers() {
+  let expect = {
+    "file_script_nonexistent.js": {
+      type: "script",
+      status: 404,
+      headers: {
+        request: {
+          add: {
+            "X-WebRequest-request": "text",
+            "X-WebRequest-request-binary": "binary",
+          },
+          modify: {
+            "user-agent": "WebRequest",
+          },
+          remove: [
+            "referer",
+          ],
+        },
+        response: {
+          add: {
+            "X-WebRequest-response": "text",
+            "X-WebRequest-response-binary": "binary",
+          },
+          modify: {
+            "server": "WebRequest",
+            "content-type": "text/html; charset=utf-8",
+          },
+          remove: [
+            "connection",
+          ],
+        },
+      },
+      completion: "onCompleted",
+    },
+  };
+  extension.sendMessage("set-expected", {expect, origin: location.href});
+  yield extension.awaitMessage("continue");
+  addScript("file_script_nonexistent.js");
+  yield extension.awaitMessage("done");
+});
+
+add_task(function* test_webRequest_tabId() {
+  let expect = {
+    "file_WebRequest_page3.html": {
+      type: "main_frame",
+    },
+  };
+  extension.sendMessage("set-expected", {expect, origin: location.href});
+  yield extension.awaitMessage("continue");
+  let a = addLink("file_WebRequest_page3.html?trigger=a");
+  a.click();
+  yield extension.awaitMessage("done");
+});
+
+add_task(function* test_webRequest_frames() {
+  let expect = {
+    "text/plain,webRequestTest": {
+      type: "sub_frame",
+      events: ["onBeforeRequest", "onCompleted"],
+    },
+    "text/plain,webRequestTest_bad": {
+      type: "sub_frame",
+      events: ["onBeforeRequest", "onErrorOccurred"],
+      cancel: "onBeforeRequest",
+    },
+    "redirection.sjs": {
+      status: 302,
+      type: "sub_frame",
+      events: ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived", "onBeforeRedirect"],
+    },
+    "dummy_page.html": {
+      type: "sub_frame",
+      status: 404,
+    },
+    "badrobot": {
+      type: "sub_frame",
+      status: 404,
+      events: ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", "onErrorOccurred"],
+    },
+  };
+  extension.sendMessage("set-expected", {expect, origin: location.href});
+  yield extension.awaitMessage("continue");
+  addFrame("data:text/plain,webRequestTest");
+  addFrame("data:text/plain,webRequestTest_bad");
+  yield extension.awaitMessage("cancelled");
+  addFrame("redirection.sjs");
+  addFrame("https://invalid.localhost/badrobot");
+  yield extension.awaitMessage("done");
+});
+
+add_task(function* teardown() {
+  yield extension.unload();
+});
+</script>
+</head>
+<body>
+<div id="test">Sample text</div>
+
+</body>
+</html>
--- a/toolkit/modules/addons/WebRequest.jsm
+++ b/toolkit/modules/addons/WebRequest.jsm
@@ -190,17 +190,19 @@ class RequestHeaderChanger extends Heade
     try {
       this.channel.setRequestHeader(name, value, false);
     } catch (e) {
       Cu.reportError(new Error(`Error setting request header ${name}: ${e}`));
     }
   }
 
   visitHeaders(visitor) {
-    this.channel.visitRequestHeaders(visitor);
+    if (this.channel instanceof Ci.nsIHttpChannel) {
+      this.channel.visitRequestHeaders(visitor);
+    }
   }
 }
 
 class ResponseHeaderChanger extends HeaderChanger {
   setHeader(name, value) {
     try {
       if (name.toLowerCase() === "content-type" && value) {
         // The Content-Type header value can't be modified, so we
@@ -214,23 +216,25 @@ class ResponseHeaderChanger extends Head
         this.channel.setResponseHeader(name, value, false);
       }
     } catch (e) {
       Cu.reportError(new Error(`Error setting response header ${name}: ${e}`));
     }
   }
 
   visitHeaders(visitor) {
-    this.channel.visitResponseHeaders((name, value) => {
-      if (name.toLowerCase() === "content-type") {
-        value = getData(this.channel).contentType || value;
-      }
+    if (this.channel instanceof Ci.nsIHttpChannel) {
+      this.channel.visitResponseHeaders((name, value) => {
+        if (name.toLowerCase() === "content-type") {
+          value = getData(this.channel).contentType || value;
+        }
 
-      visitor(name, value);
-    });
+        visitor(name, value);
+      });
+    }
   }
 }
 
 var HttpObserverManager;
 
 var ContentPolicyManager = {
   policyData: new Map(),
   policies: new Map(),