Bug 1293357: Add code coverage tests for WebExtensions. r?aswan draft
authorKris Maglione <maglione.k@gmail.com>
Thu, 02 Feb 2017 19:43:09 -0800
changeset 470034 c8217f3214adba53127e873f9ba1f8d5aab4d0b7
parent 469418 bc59eff957ebf8d522b91f275ec3838e50df3392
child 544368 ff70cdc7b3b615c8cddb97ef6817a2e8fc03a8a9
push id43912
push usermaglione.k@gmail.com
push dateFri, 03 Feb 2017 04:04:22 +0000
reviewersaswan
bugs1293357
milestone54.0a1
Bug 1293357: Add code coverage tests for WebExtensions. r?aswan MozReview-Commit-ID: 7Fo5lowNIin
browser/components/extensions/ExtensionPopups.jsm
browser/components/extensions/ext-browserAction.js
browser/components/extensions/ext-contextMenus.js
browser/components/extensions/ext-tabs.js
browser/components/extensions/ext-utils.js
browser/components/extensions/test/xpcshell/head.js
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ExtensionAPI.jsm
toolkit/components/extensions/ExtensionChild.jsm
toolkit/components/extensions/ExtensionCommon.jsm
toolkit/components/extensions/ExtensionContent.jsm
toolkit/components/extensions/ExtensionManagement.jsm
toolkit/components/extensions/ExtensionParent.jsm
toolkit/components/extensions/ExtensionStorage.jsm
toolkit/components/extensions/ExtensionStorageSync.jsm
toolkit/components/extensions/ExtensionTabs.jsm
toolkit/components/extensions/ExtensionTestCommon.jsm
toolkit/components/extensions/ExtensionUtils.jsm
toolkit/components/extensions/ExtensionXPCShellUtils.jsm
toolkit/components/extensions/LegacyExtensionsUtils.jsm
toolkit/components/extensions/MessageChannel.jsm
toolkit/components/extensions/NativeMessaging.jsm
toolkit/components/extensions/Schemas.jsm
toolkit/components/extensions/ext-browser-content.js
toolkit/components/extensions/ext-cookies.js
toolkit/components/extensions/instrument_code.py
toolkit/components/extensions/test/mochitest/test_ext_webrequest_filter.html
toolkit/components/extensions/test/xpcshell/head.js
toolkit/components/extensions/test_coverage.sh
toolkit/components/utils/simpleServices.js
toolkit/modules/addons/MatchPattern.jsm
toolkit/modules/addons/WebExtCoverage.jsm
toolkit/modules/addons/WebNavigation.jsm
toolkit/modules/addons/WebNavigationContent.js
toolkit/modules/addons/WebNavigationFrames.jsm
toolkit/modules/addons/WebRequest.jsm
toolkit/modules/addons/WebRequestCommon.jsm
toolkit/modules/addons/WebRequestContent.js
toolkit/modules/moz.build
--- a/browser/components/extensions/ExtensionPopups.jsm
+++ b/browser/components/extensions/ExtensionPopups.jsm
@@ -17,16 +17,20 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionParent",
                                   "resource://gre/modules/ExtensionParent.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
                                   "resource://gre/modules/Timer.jsm");
 
 Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
+
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 var {
   DefaultWeakMap,
   promiseEvent,
 } = ExtensionUtils;
 
 
@@ -152,16 +156,17 @@ class BasePopup {
     } else if (finalize) {
       this.receiveMessage = () => {};
     }
   }
 
   // Returns the name of the event fired on `viewNode` when the popup is being
   // destroyed. This must be implemented by every subclass.
   get DESTROY_EVENT() {
+    /* istanbul ignore next */
     throw new Error("Not implemented");
   }
 
   get STYLESHEETS() {
     let sheets = [];
 
     if (this.browserStyle) {
       sheets.push(...popupStylesheets);
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -93,16 +93,17 @@ BrowserAction.prototype = {
         view.setAttribute("flex", "1");
 
         document.getElementById("PanelUI-multiView").appendChild(view);
         document.addEventListener("popupshowing", this);
       },
 
       onDestroyed: document => {
         let view = document.getElementById(this.viewId);
+        /* istanbul ignore else */
         if (view) {
           this.clearPopup();
           CustomizableUI.hidePanelForNode(view);
           view.remove();
         }
         document.removeEventListener("popupshowing", this);
       },
 
@@ -127,17 +128,19 @@ BrowserAction.prototype = {
         // Popups are shown only if a popup URL is defined; otherwise
         // a "click" event is dispatched. This is done for compatibility with the
         // Google Chrome onClicked extension API.
         if (popupURL) {
           try {
             let popup = this.getPopup(document.defaultView, popupURL);
             event.detail.addBlocker(popup.attach(event.target));
           } catch (e) {
+            /* istanbul ignore next */
             Cu.reportError(e);
+            /* istanbul ignore next */
             event.preventDefault();
           }
         } else {
           // This isn't not a hack, but it seems to provide the correct behavior
           // with the fewest complications.
           event.preventDefault();
           this.emit("click");
         }
@@ -368,16 +371,17 @@ BrowserAction.prototype = {
       --webextension-toolbar-image: url("${escape(icon)}");
       --webextension-toolbar-image-2x: url("${getIcon(baseSize * 2)}");
     `);
   },
 
   // Update the toolbar button for a given window.
   updateWindow(window) {
     let widget = this.widget.forWindow(window);
+    /* istanbul ignore else */
     if (widget) {
       let tab = window.gBrowser.selectedTab;
       this.updateButton(widget.node, this.tabContext.get(tab));
     }
   },
 
   // Update the toolbar button when the extension changes the icon,
   // title, badge, etc. If it only changes a parameter for a single
--- a/browser/components/extensions/ext-contextMenus.js
+++ b/browser/components/extensions/ext-contextMenus.js
@@ -418,29 +418,31 @@ MenuItem.prototype = {
       this.root.addChild(this);
     } else {
       let menuMap = gContextMenuMap.get(this.extension);
       menuMap.get(parentId).addChild(this);
     }
   },
 
   get parentId() {
-    return this.parent ? this.parent.id : undefined;
+    return this.parent ? this.parent.id : /* istanbul ignore next */ undefined;
   },
 
   addChild(child) {
+    /* istanbul ignore if */
     if (child.parent) {
       throw new Error("Child MenuItem already has a parent.");
     }
     this.children.push(child);
     child.parent = this;
   },
 
   detachChild(child) {
     let idx = this.children.indexOf(child);
+    /* istanbul ignore if */
     if (idx < 0) {
       throw new Error("Child MenuItem not found, it cannot be removed.");
     }
     this.children.splice(idx, 1);
     child.parent = null;
   },
 
   get root() {
@@ -623,16 +625,17 @@ extensions.registerSchemaAPI("contextMen
         let menuItem = gContextMenuMap.get(extension).get(id);
         if (menuItem) {
           menuItem.remove();
         }
       },
 
       removeAll: function() {
         let root = gRootItems.get(extension);
+        /* istanbul ignore else */
         if (root) {
           root.remove();
         }
       },
 
       onClicked: new SingletonEventManager(context, "contextMenus.onClicked", fire => {
         let listener = (event, info, tab) => {
           fire.async(info, tab);
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -338,16 +338,17 @@ extensions.registerSchemaAPI("tabs", "ad
       }).api(),
 
       create(createProperties) {
         return new Promise((resolve, reject) => {
           let window = createProperties.windowId !== null ?
             windowTracker.getWindow(createProperties.windowId, context) :
             windowTracker.topWindow;
 
+          /* istanbul ignore if */
           if (!window.gBrowser) {
             let obs = (finishedWindow, topic, data) => {
               if (finishedWindow != window) {
                 return;
               }
               Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
               resolve(window);
             };
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -57,16 +57,17 @@ TabContext.prototype = {
     return this.tabData.get(tab);
   },
 
   clear(tab) {
     this.tabData.delete(tab);
   },
 
   handleEvent(event) {
+    /* istanbul ignore else */
     if (event.type == "TabSelect") {
       let tab = event.target;
       this.emit("tab-select", tab);
       this.emit("location-change", tab);
     }
   },
 
   onStateChange(browser, webProgress, request, stateFlags, statusCode) {
--- a/browser/components/extensions/test/xpcshell/head.js
+++ b/browser/components/extensions/test/xpcshell/head.js
@@ -24,16 +24,20 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TestUtils",
                                   "resource://testing-common/TestUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WebExtCoverage",
+                                  "resource://gre/modules/WebExtCoverage.jsm");
+
+do_register_cleanup(() => WebExtCoverage.saveAllCoverage(false));
 
 ExtensionTestUtils.init(this);
 
 
 /**
  * Creates a new HttpServer for testing, and begins listening on the
  * specified port. Automatically shuts down the server when the test
  * unit ends.
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -64,16 +64,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/Schemas.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 
 Cu.import("resource://gre/modules/ExtensionContent.jsm");
 Cu.import("resource://gre/modules/ExtensionManagement.jsm");
 Cu.import("resource://gre/modules/ExtensionParent.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
 
 XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
 
 var {
   GlobalManager,
   ParentAPIManager,
@@ -342,16 +345,17 @@ this.ExtensionData = class {
     }.bind(this));
   }
 
   readJSON(path) {
     return new Promise((resolve, reject) => {
       let uri = this.rootURI.resolve(`./${path}`);
 
       NetUtil.asyncFetch({uri, loadUsingSystemPrincipal: true}, (inputStream, status) => {
+        /* istanbul ignore if */
         if (!Components.isSuccessCode(status)) {
           // Convert status code to a string
           let e = Components.Exception("", status);
           reject(new Error(`Error while loading '${uri}' (${e.name})`));
           return;
         }
         try {
           let text = NetUtil.readInputStreamToString(inputStream, inputStream.available(),
@@ -520,16 +524,17 @@ this.ExtensionData = class {
   // Map(gecko-locale-code -> locale-directory-name)
   promiseLocales() {
     if (!this._promiseLocales) {
       this._promiseLocales = Task.spawn(function* () {
         let locales = new Map();
 
         let entries = yield this.readDirectory("_locales");
         for (let file of entries) {
+          /* istanbul ignore else */
           if (file.isDir) {
             let locale = this.normalizeLocaleCode(file.name);
             locales.set(locale, file.name);
           }
         }
 
         this.localeData = new LocaleData({
           defaultLocale: this.defaultLocale,
@@ -853,16 +858,17 @@ this.Extension = class extends Extension
     return this.readManifest().then(() => {
       ExtensionManagement.startupExtension(this.uuid, this.addonData.resourceURI, this);
       started = true;
 
       if (!this.hasShutdown) {
         return this.initLocale();
       }
     }).then(() => {
+      /* istanbul ignore if */
       if (this.errors.length) {
         return Promise.reject({errors: this.errors});
       }
 
       if (this.hasShutdown) {
         return;
       }
 
@@ -873,17 +879,17 @@ this.Extension = class extends Extension
       // and it is used to run code that needs to be executed before
       // any of the "startup" listeners.
       this.emit("startup", this);
       Management.emit("startup", this);
 
       return this.runManifest(this.manifest);
     }).then(() => {
       Management.emit("ready", this);
-    }).catch(e => {
+    }).catch(/* istanbul ignore next */ e => {
       dump(`Extension error: ${e.message} ${e.filename || e.fileName}:${e.lineNumber} :: ${e.stack || new Error().stack}\n`);
       Cu.reportError(e);
 
       if (started) {
         ExtensionManagement.shutdownExtension(this.uuid);
       }
 
       this.cleanupGeneratedFile();
@@ -941,16 +947,17 @@ this.Extension = class extends Extension
 
     MessageChannel.abortResponses({extensionId: this.id});
 
     ExtensionManagement.shutdownExtension(this.uuid);
 
     this.cleanupGeneratedFile();
   }
 
+  /* istanbul ignore next */
   observe(subject, topic, data) {
     if (topic === "xpcom-shutdown") {
       this.cleanupGeneratedFile();
     }
   }
 
   hasPermission(perm) {
     let match = /^manifest:(.*)/.exec(perm);
--- a/toolkit/components/extensions/ExtensionAPI.jsm
+++ b/toolkit/components/extensions/ExtensionAPI.jsm
@@ -16,16 +16,20 @@ Cu.import("resource://gre/modules/XPCOMU
 
 XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
                                   "resource://devtools/shared/event-emitter.js");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
+
 const global = this;
 
 class ExtensionAPI {
   constructor(extension) {
     this.extension = extension;
   }
 
   destroy() {
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -33,16 +33,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 
 const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon";
 const CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS = "webextension-scripts-devtools";
 
 Cu.import("resource://gre/modules/ExtensionCommon.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
 
 const {
   DefaultMap,
   LimitedSet,
   SingletonEventManager,
   SpreadArgs,
   defineLazyGetter,
   getInnerWindowID,
--- a/toolkit/components/extensions/ExtensionCommon.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -21,16 +21,19 @@ Cu.import("resource://gre/modules/XPCOMU
 XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
                                   "resource://gre/modules/MessageChannel.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
 
 var {
   EventEmitter,
   ExtensionError,
   SpreadArgs,
   getConsole,
   getInnerWindowID,
   getUniqueId,
@@ -70,23 +73,25 @@ class BaseContext {
 
     if (this.incognito == null) {
       this.incognito = PrivateBrowsingUtils.isContentWindowPrivate(contentWindow);
     }
 
     MessageChannel.setupMessageManagers([this.messageManager]);
 
     let onPageShow = event => {
+      /* istanbul ignore else */
       if (!event || event.target === document) {
         this.docShell = docShell;
         this.contentWindow = contentWindow;
         this.active = true;
       }
     };
     let onPageHide = event => {
+      /* istanbul ignore else */
       if (!event || event.target === document) {
         // Put this off until the next tick.
         Promise.resolve().then(() => {
           this.docShell = null;
           this.contentWindow = null;
           this.active = false;
         });
       }
@@ -101,35 +106,41 @@ class BaseContext {
         if (this.active) {
           contentWindow.removeEventListener("pagehide", onPageHide, true);
           contentWindow.removeEventListener("pageshow", onPageShow, true);
         }
       },
     });
   }
 
+  /* istanbul ignore next */
   get cloneScope() {
+    /* istanbul ignore next */
     throw new Error("Not implemented");
   }
 
+  /* istanbul ignore next */
   get principal() {
+    /* istanbul ignore next */
     throw new Error("Not implemented");
   }
 
   runSafe(...args) {
+    /* istanbul ignore if */
     if (this.unloaded) {
       Cu.reportError("context.runSafe called after context unloaded");
     } else if (!this.active) {
       Cu.reportError("context.runSafe called while context is inactive");
     } else {
       return runSafeSync(this, ...args);
     }
   }
 
   runSafeWithoutClone(...args) {
+    /* istanbul ignore if */
     if (this.unloaded) {
       Cu.reportError("context.runSafeWithoutClone called after context unloaded");
     } else if (!this.active) {
       Cu.reportError("context.runSafeWithoutClone called while context is inactive");
     } else {
       return runSafeSyncWithoutClone(...args);
     }
   }
@@ -192,17 +203,17 @@ class BaseContext {
    * @param {object} data
    * @param {object} [options]
    * @param {object} [options.sender]
    * @param {object} [options.recipient]
    *
    * @returns {Promise}
    */
   sendMessage(target, messageName, data, options = {}) {
-    options.recipient = options.recipient || {};
+    options.recipient = options.recipient || /* istanbul ignore next */ {};
     options.sender = options.sender || {};
 
     options.recipient.extensionId = this.extension.id;
     options.sender.extensionId = this.extension.id;
     options.sender.contextId = this.contextId;
 
     return MessageChannel.sendMessage(target, messageName, data, options);
   }
@@ -309,16 +320,17 @@ class BaseContext {
           } else if (args instanceof SpreadArgs) {
             runSafe(callback, ...args);
           } else {
             runSafe(callback, args);
           }
         },
         error => {
           this.withLastError(error, () => {
+            /* istanbul ignore if */
             if (this.unloaded) {
               dump(`Promise rejected after context unloaded\n`);
             } else if (!this.active) {
               dump(`Promise rejected while context is inactive\n`);
             } else {
               this.runSafeWithoutClone(callback);
             }
           });
@@ -566,16 +578,19 @@ class SchemaAPIManager extends EventEmit
    * @returns {object} A sandbox that is used as the global by `loadScript`.
    */
   _createExtGlobal() {
     let global = Cu.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), {
       wantXrays: false,
       sandboxName: `Namespace of ext-*.js scripts for ${this.processType}`,
     });
 
+    global.__coverage__ = {};
+    WebExtCoverage.register(global);
+
     Object.assign(global, {global, Cc, Ci, Cu, Cr, XPCOMUtils, extensions: this});
 
     XPCOMUtils.defineLazyGetter(global, "console", getConsole);
 
     XPCOMUtils.defineLazyModuleGetter(global, "require",
                                       "resource://devtools/shared/Loader.jsm");
 
     return global;
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -42,16 +42,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
                                   "resource://gre/modules/WebNavigationFrames.jsm");
 
 Cu.import("resource://gre/modules/ExtensionChild.jsm");
 Cu.import("resource://gre/modules/ExtensionCommon.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
 
 const {
   EventEmitter,
   LocaleData,
   defineLazyGetter,
   flushJarCache,
   getInnerWindowID,
   promiseDocumentReady,
@@ -342,16 +345,17 @@ class ContentScriptContextChild extends 
       // Make sure we don't hand out the system principal by accident.
       // also make sure that the null principal has the right origin attributes
       principal = ssm.createNullPrincipal(attrs);
     } else {
       principal = [contentPrincipal, extensionPrincipal];
     }
 
     if (isExtensionPage) {
+      /* istanbul ignore if */
       if (ExtensionManagement.getAddonIdForWindow(this.contentWindow) != this.extension.id) {
         throw new Error("Invalid target window for this extension context");
       }
       // This is an iframe with content script API enabled and its principal should be the
       // contentWindow itself. (we create a sandbox with the contentWindow as principal and with X-rays disabled
       // because it enables us to create the APIs object in this sandbox object and then copying it
       // into the iframe's window, see Bug 1214658 for rationale)
       this.sandbox = Cu.Sandbox(contentWindow, {
@@ -574,16 +578,17 @@ DocumentManager = {
         this.loadInto(window);
         this.trigger("document_start", window);
       }
 
       /* eslint-disable mozilla/balanced-listeners */
       window.addEventListener("DOMContentLoaded", this, true);
       window.addEventListener("load", this, true);
       /* eslint-enable mozilla/balanced-listeners */
+      /* istanbul ignore else */
     } else if (topic == "inner-window-destroyed") {
       let windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
 
       MessageChannel.abortResponses({innerWindowID: windowId});
 
       // Close any existent content-script context for the destroyed window.
       if (this.contentScriptWindows.has(windowId)) {
         let extensions = this.contentScriptWindows.get(windowId);
@@ -602,16 +607,17 @@ DocumentManager = {
       }
 
       ExtensionChild.destroyExtensionContext(windowId);
     }
   },
 
   handleEvent: function(event) {
     let window = event.currentTarget;
+    /* istanbul ignore if */
     if (event.target != window.document) {
       // We use capturing listeners so we have precedence over content script
       // listeners, but only care about events targeted to the element we're
       // listening on.
       return;
     }
     window.removeEventListener(event.type, this, true);
 
@@ -701,16 +707,17 @@ DocumentManager = {
 
     return extensions.get(extension.id);
   },
 
   getExtensionPageContext(extension, window) {
     let winId = getInnerWindowID(window);
 
     let context = this.extensionPageWindows.get(winId);
+    /* istanbul ignore else */
     if (!context) {
       let context = new ContentScriptContextChild(extension, window, {isExtensionPage: true});
       this.extensionPageWindows.set(winId, context);
     }
 
     return context;
   },
 
@@ -743,16 +750,17 @@ DocumentManager = {
       if (context) {
         context.close();
         extensions.delete(extensionId);
       }
     }
 
     // Clean up iframe extension page contexts on extension shutdown.
     for (let [winId, context] of this.extensionPageWindows) {
+      /* istanbul ignore else */
       if (context.extension.id == extensionId) {
         context.close();
         this.extensionPageWindows.delete(winId);
       }
     }
 
     ExtensionChild.shutdownExtension(extensionId);
 
--- a/toolkit/components/extensions/ExtensionManagement.jsm
+++ b/toolkit/components/extensions/ExtensionManagement.jsm
@@ -9,16 +9,19 @@ this.EXPORTED_SYMBOLS = ["ExtensionManag
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 Cu.import("resource://gre/modules/AppConstants.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
 
 XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
                                   "resource:///modules/E10SUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
                                   "resource://gre/modules/ExtensionUtils.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionUtils.getConsole());
 
@@ -193,16 +196,17 @@ var Service = {
   extensionURILoadableByAnyone(uri) {
     let uuid = uri.host;
     let extension = this.uuidMap.get(uuid);
     if (!extension || !extension.webAccessibleResources) {
       return false;
     }
 
     let path = uri.QueryInterface(Ci.nsIURL).filePath;
+    /* istanbul ignore else */
     if (path.length > 0 && path[0] == "/") {
       path = path.substr(1);
     }
     return extension.webAccessibleResources.matches(path);
   },
 
   // Checks whether a given extension can load this URI (typically via
   // an XML HTTP request). The manifest.json |permissions| directive
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -33,16 +33,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 
 Cu.import("resource://gre/modules/ExtensionCommon.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
 
 var {
   BaseContext,
   SchemaAPIManager,
 } = ExtensionCommon;
 
 var {
   MessageManagerProxy,
--- a/toolkit/components/extensions/ExtensionStorage.jsm
+++ b/toolkit/components/extensions/ExtensionStorage.jsm
@@ -8,21 +8,26 @@ this.EXPORTED_SYMBOLS = ["ExtensionStora
 
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
                                   "resource://gre/modules/AsyncShutdown.jsm");
 
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
+
 /**
  * Helper function used to sanitize the objects that have to be saved in the ExtensionStorage.
  *
  * @param {BaseContext} context
  *   The current extension context.
  * @param {string} key
  *   The key of the current JSON property.
  * @param {any} value
--- a/toolkit/components/extensions/ExtensionStorageSync.jsm
+++ b/toolkit/components/extensions/ExtensionStorageSync.jsm
@@ -13,35 +13,32 @@ this.EXPORTED_SYMBOLS = ["ExtensionStora
 
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 const Cr = Components.results;
 const global = this;
 
 Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
 const KINTO_PROD_SERVER_URL = "https://webextensions.settings.services.mozilla.com/v1";
 const KINTO_DEFAULT_SERVER_URL = KINTO_PROD_SERVER_URL;
 
 const STORAGE_SYNC_ENABLED_PREF = "webextensions.storage.sync.enabled";
 const STORAGE_SYNC_SERVER_URL_PREF = "webextensions.storage.sync.serverURL";
 const STORAGE_SYNC_SCOPE = "sync:addon_storage";
 const STORAGE_SYNC_CRYPTO_COLLECTION_NAME = "storage-sync-crypto";
 const STORAGE_SYNC_CRYPTO_KEYRING_RECORD_ID = "keys";
 const FXA_OAUTH_OPTIONS = {
   scope: STORAGE_SYNC_SCOPE,
 };
 // Default is 5sec, which seems a bit aggressive on the open internet
 const KINTO_REQUEST_TIMEOUT = 30000;
 
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-const {
-  runSafeSyncWithoutClone,
-} = Cu.import("resource://gre/modules/ExtensionUtils.jsm", {});
-
 XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
                                   "resource://gre/modules/AsyncShutdown.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "CollectionKeyManager",
                                   "resource://services-sync/record.js");
 XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
                                   "resource://services-common/utils.js");
 XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils",
                                   "resource://services-crypto/utils.js");
@@ -62,22 +59,32 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Observers",
                                   "resource://services-common/observers.js");
 XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
                                   "resource://gre/modules/Sqlite.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "KeyRingEncryptionRemoteTransformer",
                                   "resource://services-sync/engines/extension-storage.js");
+
 XPCOMUtils.defineLazyPreferenceGetter(this, "prefPermitsStorageSync",
                                       STORAGE_SYNC_ENABLED_PREF, true);
 XPCOMUtils.defineLazyPreferenceGetter(this, "prefStorageSyncServerURL",
                                       STORAGE_SYNC_SERVER_URL_PREF,
                                       KINTO_DEFAULT_SERVER_URL);
 
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
+
+const {
+  runSafeSyncWithoutClone,
+} = ExtensionUtils;
+
 /* globals prefPermitsStorageSync, prefStorageSyncServerURL */
 
 // Map of Extensions to Set<Contexts> to track contexts that are still
 // "live" and use storage.sync.
 const extensionContexts = new Map();
 // Borrow logger from Sync.
 const log = Log.repository.getLogger("Sync.Engine.Extension-Storage");
 
--- a/toolkit/components/extensions/ExtensionTabs.jsm
+++ b/toolkit/components/extensions/ExtensionTabs.jsm
@@ -14,16 +14,20 @@ var EXPORTED_SYMBOLS = ["TabTrackerBase"
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
+
 
 const {
   DefaultMap,
   DefaultWeakMap,
   EventEmitter,
   ExtensionError,
 } = ExtensionUtils;
 
--- a/toolkit/components/extensions/ExtensionTestCommon.jsm
+++ b/toolkit/components/extensions/ExtensionTestCommon.jsm
@@ -31,16 +31,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "apiManager",
                             () => ExtensionParent.apiManager);
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
 
 XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
 
 const {
   flushJarCache,
   instanceOf,
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -10,16 +10,19 @@ const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 const INTEGER = /^[1-9]\d*$/;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPI",
                                   "resource://gre/modules/Console.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
@@ -34,16 +37,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/Preferences.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
                                    "@mozilla.org/content/style-sheet-service;1",
                                    "nsIStyleSheetService");
 
+/* istanbul ignore next */
 function getConsole() {
   return new ConsoleAPI({
     maxLogLevelPref: "extensions.webextensions.log.level",
     prefix: "WebExtensions",
   });
 }
 
 XPCOMUtils.defineLazyGetter(this, "console", getConsole);
@@ -56,68 +60,78 @@ function getUniqueId() {
 }
 
 /**
  * An Error subclass for which complete error messages are always passed
  * to extensions, rather than being interpreted as an unknown error.
  */
 class ExtensionError extends Error {}
 
+/* istanbul ignore next */
 function filterStack(error) {
   return String(error.stack).replace(/(^.*(Task\.jsm|Promise-backend\.js).*\n)+/gm, "<Promise Chain>\n");
 }
 
 // Run a function and report exceptions.
 function runSafeSyncWithoutClone(f, ...args) {
   try {
     return f(...args);
   } catch (e) {
+    /* istanbul ignore next */
     dump(`Extension error: ${e} ${e.fileName} ${e.lineNumber}\n[[Exception stack\n${filterStack(e)}Current stack\n${filterStack(Error())}]]\n`);
+    /* istanbul ignore next */
     Cu.reportError(e);
   }
 }
 
 // Run a function and report exceptions.
 function runSafeWithoutClone(f, ...args) {
+  /* istanbul ignore if */
   if (typeof(f) != "function") {
     dump(`Extension error: expected function\n${filterStack(Error())}`);
     return;
   }
 
   Promise.resolve().then(() => {
     runSafeSyncWithoutClone(f, ...args);
   });
 }
 
 // Run a function, cloning arguments into context.cloneScope, and
 // report exceptions. |f| is expected to be in context.cloneScope.
 function runSafeSync(context, f, ...args) {
+  /* istanbul ignore if */
   if (context.unloaded) {
     Cu.reportError("runSafeSync called after context unloaded");
     return;
   }
 
   try {
     args = Cu.cloneInto(args, context.cloneScope);
   } catch (e) {
+    /* istanbul ignore next */
     Cu.reportError(e);
+    /* istanbul ignore next */
     dump(`runSafe failure: cloning into ${context.cloneScope}: ${e}\n\n${filterStack(Error())}`);
   }
   return runSafeSyncWithoutClone(f, ...args);
 }
 
 // Run a function, cloning arguments into context.cloneScope, and
 // report exceptions. |f| is expected to be in context.cloneScope.
 function runSafe(context, f, ...args) {
   try {
     args = Cu.cloneInto(args, context.cloneScope);
   } catch (e) {
+    /* istanbul ignore next */
     Cu.reportError(e);
+    /* istanbul ignore next */
     dump(`runSafe failure: cloning into ${context.cloneScope}: ${e}\n\n${filterStack(Error())}`);
   }
+  /* istanbul ignore if */
   if (context.unloaded) {
     dump(`runSafe failure: context is already unloaded ${filterStack(new Error())}\n`);
     return undefined;
   }
   return runSafeWithoutClone(f, ...args);
 }
 
 function getInnerWindowID(window) {
@@ -129,16 +143,17 @@ function getInnerWindowID(window) {
 // Return true if the given value is an instance of the given
 // native type.
 function instanceOf(value, type) {
   return {}.toString.call(value) == `[object ${type}]`;
 }
 
 // Extend the object |obj| with the property descriptors of each object in
 // |args|.
+/* istanbul ignore next */
 function extend(obj, ...args) {
   for (let arg of args) {
     let props = [...Object.getOwnPropertyNames(arg),
                  ...Object.getOwnPropertySymbols(arg)];
     for (let prop of props) {
       let descriptor = Object.getOwnPropertyDescriptor(arg, prop);
       Object.defineProperty(obj, prop, descriptor);
     }
@@ -250,16 +265,17 @@ let IconDetails = {
     } catch (e) {
       // Function is called from extension code, delegate error.
       if (context) {
         throw e;
       }
       // If there's no context, it's because we're handling this
       // as a manifest directive. Log a warning rather than
       // raising an error.
+      /* istanbul ignore next */
       extension.manifestError(`Invalid icon data: ${e}`);
     }
 
     return result;
   },
 
   // Returns the appropriate icon URL for the given icons object and the
   // screen resolution of the given window.
@@ -350,16 +366,17 @@ class EventEmitter {
    * Removes the given function as a listener for the given event.
    *
    * @param {string} event
    *       The name of the event to stop listening for.
    * @param {function(string, ...any)} listener
    *        The listener function to remove.
    */
   off(event, listener) {
+    /* istanbul ignore else */
     if (this[LISTENERS].has(event)) {
       let set = this[LISTENERS].get(event);
 
       set.delete(listener);
       if (!set.size) {
         this[LISTENERS].delete(event);
       }
     }
@@ -386,17 +403,17 @@ class EventEmitter {
 
     return Promise.all(promises);
   }
 }
 
 function LocaleData(data) {
   this.defaultLocale = data.defaultLocale;
   this.selectedLocale = data.selectedLocale;
-  this.locales = data.locales || new Map();
+  this.locales = data.locales || /* istanbul ignore next */ new Map();
   this.warnedMissingKeys = new Set();
 
   // Map(locale-name -> Map(message-key -> localized-string))
   //
   // Contains a key for each loaded locale, each of which is a
   // Map of message keys to their localized strings.
   this.messages = data.messages || new Map();
 
@@ -479,16 +496,17 @@ LocaleData.prototype = {
         return rtl ? "right" : "left";
       } else if (message == "@@bidi_end_edge") {
         return rtl ? "left" : "right";
       }
     }
 
     if (!this.warnedMissingKeys.has(message)) {
       let error = `Unknown localization message ${message}`;
+      /* istanbul ignore else */
       if (options.cloneScope) {
         error = new options.cloneScope.Error(error);
       }
       Cu.reportError(error);
       this.warnedMissingKeys.add(message);
     }
     return options.defaultValue;
   },
@@ -520,24 +538,26 @@ LocaleData.prototype = {
     // replacements. Later, it processes the resulting string for
     // |$[0-9]| replacements.
     //
     // Again, it does not document this, but it accepts any number
     // of sequential |$|s, and replaces them with that number minus
     // 1. It also accepts |$| followed by any number of sequential
     // digits, but refuses to process a localized string which
     // provides more than 9 substitutions.
+    /* istanbul ignore if */
     if (!instanceOf(messages, "Object")) {
       extension.packagingError(`Invalid locale data for ${locale}`);
       return result;
     }
 
     for (let key of Object.keys(messages)) {
       let msg = messages[key];
 
+      /* istanbul ignore if */
       if (!instanceOf(msg, "Object") || typeof(msg.message) != "string") {
         extension.packagingError(`Invalid locale message data for ${locale}, message ${JSON.stringify(key)}`);
         continue;
       }
 
       // Substitutions are case-insensitive, so normalize all of their names
       // to lower-case.
       let placeholders = new Map();
@@ -612,16 +632,17 @@ SingletonEventManager.prototype = {
       return;
     }
 
     let shouldFire = () => {
       if (this.context.unloaded) {
         dump(`${this.name} event fired after context unloaded.\n`);
       } else if (!this.context.active) {
         dump(`${this.name} event fired while context is inactive.\n`);
+      /* istanbul ignore else */
       } else if (this.unregister.has(callback)) {
         return true;
       }
       return false;
     };
 
     let fire = {
       sync: (...args) => {
@@ -653,16 +674,17 @@ SingletonEventManager.prototype = {
 
 
     let unregister = this.register(fire, ...args);
     this.unregister.set(callback, unregister);
     this.context.callOnClose(this);
   },
 
   removeListener(callback) {
+    /* istanbul ignore if */
     if (!this.unregister.has(callback)) {
       return;
     }
 
     let unregister = this.unregister.get(callback);
     this.unregister.delete(callback);
     unregister();
     if (this.unregister.size == 0) {
@@ -700,25 +722,28 @@ function ignoreEvent(context, name) {
         .createInstance(Ci.nsIScriptError);
       scriptError.init(msg, frame.filename, null, frame.lineNumber,
                        frame.columnNumber, Ci.nsIScriptError.warningFlag,
                        "content javascript");
       let consoleService = Cc["@mozilla.org/consoleservice;1"]
         .getService(Ci.nsIConsoleService);
       consoleService.logMessage(scriptError);
     },
+    /* istanbul ignore next */
     removeListener: function(callback) {},
+    /* istanbul ignore next */
     hasListener: function(callback) {},
   };
 }
 
 // Copy an API object from |source| into the scope |dest|.
 function injectAPI(source, dest) {
   for (let prop in source) {
     // Skip names prefixed with '_'.
+    /* istanbul ignore if */
     if (prop[0] == "_") {
       continue;
     }
 
     let desc = Object.getOwnPropertyDescriptor(source, prop);
     if (typeof(desc.value) == "function") {
       Cu.exportFunction(desc.value, dest, {defineAs: prop});
     } else if (typeof(desc.value) == "object") {
@@ -766,32 +791,34 @@ class LimitedSet extends Set {
  */
 function promiseDocumentReady(doc) {
   if (doc.readyState == "interactive" || doc.readyState == "complete") {
     return Promise.resolve(doc);
   }
 
   return new Promise(resolve => {
     doc.addEventListener("DOMContentLoaded", function onReady(event) {
+      /* istanbul ignore else */
       if (event.target === event.currentTarget) {
         doc.removeEventListener("DOMContentLoaded", onReady, true);
         resolve(doc);
       }
     }, true);
   });
 }
 
 /**
  * Returns a Promise which resolves when the given document is fully
  * loaded.
  *
  * @param {Document} doc The document to await the load of.
  * @returns {Promise<Document>}
  */
 function promiseDocumentLoaded(doc) {
+  /* istanbul ignore if */
   if (doc.readyState == "complete") {
     return Promise.resolve(doc);
   }
 
   return new Promise(resolve => {
     doc.defaultView.addEventListener("load", function(event) {
       resolve(doc);
     }, {once: true});
--- a/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
+++ b/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
@@ -14,16 +14,18 @@ Components.utils.import("resource://gre/
 XPCOMUtils.defineLazyModuleGetter(this, "Extension",
                                   "resource://gre/modules/Extension.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WebExtCoverage",
+                                  "resource://gre/modules/WebExtCoverage.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "Management", () => {
   const {Management} = Cu.import("resource://gre/modules/Extension.jsm", {});
   return Management;
 });
 
 /* exported ExtensionTestUtils */
 
@@ -232,16 +234,18 @@ var ExtensionTestUtils = {
 
   currentScope: null,
 
   profileDir: null,
 
   init(scope) {
     this.currentScope = scope;
 
+    scope.do_register_cleanup(() => WebExtCoverage.saveAllCoverage(false));
+
     this.profileDir = scope.do_get_profile();
 
     // We need to load at least one frame script into every message
     // manager to ensure that the scriptable wrapper for its global gets
     // created before we try to access it externally. If we don't, we
     // fail sanity checks on debug builds the first time we try to
     // create a wrapper, because we should never have a global without a
     // cached wrapper.
--- a/toolkit/components/extensions/LegacyExtensionsUtils.jsm
+++ b/toolkit/components/extensions/LegacyExtensionsUtils.jsm
@@ -19,16 +19,19 @@ Cu.import("resource://gre/modules/XPCOMU
 
 XPCOMUtils.defineLazyModuleGetter(this, "Extension",
                                   "resource://gre/modules/Extension.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
 Cu.import("resource://gre/modules/ExtensionChild.jsm");
 Cu.import("resource://gre/modules/ExtensionCommon.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
 
 var {
   BaseContext,
 } = ExtensionCommon;
 
 var {
   Messenger,
 } = ExtensionChild;
--- a/toolkit/components/extensions/MessageChannel.jsm
+++ b/toolkit/components/extensions/MessageChannel.jsm
@@ -1,14 +1,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+Components.utils.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
+
 /**
  * This module provides wrappers around standard message managers to
  * simplify bidirectional communication. It currently allows a caller to
  * send a message to a single listener, and receive a reply. If there
  * are no matching listeners, or the message manager disconnects before
  * a reply is received, the caller is returned an error.
  *
  * The listener end may specify filters for the messages it wishes to
--- a/toolkit/components/extensions/NativeMessaging.jsm
+++ b/toolkit/components/extensions/NativeMessaging.jsm
@@ -5,16 +5,19 @@
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["HostManifestManager", "NativeApp"];
 /* globals NativeApp */
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
 
 const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js", {});
 
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
                                   "resource://gre/modules/AsyncShutdown.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionChild",
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -13,16 +13,20 @@ const global = this;
 
 Cu.importGlobalProperties(["URL"]);
 
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
+
 var {
   DefaultMap,
   instanceOf,
 } = ExtensionUtils;
 
 class DeepMap extends DefaultMap {
   constructor() {
     super(() => new DeepMap());
@@ -39,16 +43,17 @@ XPCOMUtils.defineLazyServiceGetter(this,
 
 this.EXPORTED_SYMBOLS = ["Schemas"];
 
 /* globals Schemas, URL */
 
 function readJSON(url) {
   return new Promise((resolve, reject) => {
     NetUtil.asyncFetch({uri: url, loadUsingSystemPrincipal: true}, (inputStream, status) => {
+      /* istanbul ignore if */
       if (!Components.isSuccessCode(status)) {
         // Convert status code to a string
         let e = Components.Exception("", status);
         reject(new Error(`Error while loading '${url}' (${e.name})`));
         return;
       }
       try {
         let text = NetUtil.readInputStreamToString(inputStream, inputStream.available());
@@ -57,16 +62,17 @@ function readJSON(url) {
         // strip off for this to be valid JSON. As a hack, we just
         // look for the first '[' character, which signals the start
         // of the JSON content.
         let index = text.indexOf("[");
         text = text.slice(index);
 
         resolve(JSON.parse(text));
       } catch (e) {
+        /* istanbul ignore next */
         reject(e);
       }
     });
   });
 }
 
 /**
  * Defines a lazy getter for the given property on the given object. Any
@@ -610,16 +616,17 @@ class Entry {
   logDeprecation(context, value = null) {
     let message = "This property is deprecated";
     if (typeof(this.deprecated) == "string") {
       message = this.deprecated;
       if (message.includes("${value}")) {
         try {
           value = JSON.stringify(value);
         } catch (e) {
+          /* istanbul ignore next */
           value = String(value);
         }
         message = message.replace(/\$\{value\}/g, () => value);
       }
     }
 
     context.logError(context.makeError(message));
   }
@@ -718,25 +725,27 @@ class Type extends Entry {
     }
   }
 
   // Takes a value, checks that it has the correct type, and returns a
   // "normalized" version of the value. The normalized version will
   // include "nulls" in place of omitted optional properties. The
   // result of this function is either {error: "Some type error"} or
   // {value: <normalized-value>}.
+  /* istanbul ignore next */
   normalize(value, context) {
     return context.error("invalid type");
   }
 
   // Unlike normalize, this function does a shallow check to see if
   // |baseType| (one of the possible getValueBaseType results) is
   // valid for this type. It returns true or false. It's used to fill
   // in optional arguments to functions before actually type checking
-
+  // the arguments.
+  /* istanbul ignore next */
   checkBaseType(baseType) {
     return false;
   }
 
   // Helper method that simply relies on checkBaseType to implement
   // normalize. Subclasses can choose to use it or not.
   normalizeBase(type, value, context) {
     if (this.checkBaseType(getValueBaseType(value))) {
@@ -850,16 +859,17 @@ class RefType extends Type {
     super(schema);
     this.namespaceName = namespaceName;
     this.reference = reference;
   }
 
   get targetType() {
     let ns = Schemas.namespaces.get(this.namespaceName);
     let type = ns.get(this.reference);
+    /* istanbul ignore if */
     if (!type) {
       throw new Error(`Internal error: Type ${this.reference} not found`);
     }
     return type;
   }
 
   normalize(value, context) {
     this.checkDeprecated(context, value);
@@ -1053,16 +1063,17 @@ class ObjectType extends Type {
     this.properties = properties;
     this.additionalProperties = additionalProperties;
     this.patternProperties = patternProperties;
     this.isInstanceOf = isInstanceOf;
   }
 
   extend(type) {
     for (let key of Object.keys(type.properties)) {
+      /* istanbul ignore if */
       if (key in this.properties) {
         throw new Error(`InternalError: Attempt to extend an object with conflicting property "${key}"`);
       }
       this.properties[key] = type.properties[key];
     }
 
     this.patternProperties.push(...type.patternProperties);
 
@@ -1849,18 +1860,20 @@ this.Schemas = {
 
   extendType(namespaceName, type) {
     let ns = Schemas.namespaces.get(namespaceName);
     let targetType = ns && ns.get(type.$extend);
 
     // Only allow extending object and choices types for now.
     if (targetType instanceof ObjectType) {
       type.type = "object";
+    /* istanbul ignore if */
     } else if (!targetType) {
       throw new Error(`Internal error: Attempt to extend a nonexistant type ${type.$extend}`);
+    /* istanbul ignore if */
     } else if (!(targetType instanceof ChoiceType)) {
       throw new Error(`Internal error: Attempt to extend a non-extensible type ${type.$extend}`);
     }
 
     let parsed = this.parseSchema(type, [namespaceName], ["$extend"]);
     if (parsed.constructor !== targetType.constructor) {
       throw new Error(`Internal error: Bad attempt to extend ${type.$extend}`);
     }
--- a/toolkit/components/extensions/ext-browser-content.js
+++ b/toolkit/components/extensions/ext-browser-content.js
@@ -2,18 +2,21 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
+WebExtCoverage.register(this);
+
 XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout",
                                   "resource://gre/modules/Timer.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "require",
                                   "resource://devtools/shared/Loader.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
                                   "resource://gre/modules/Timer.jsm");
--- a/toolkit/components/extensions/ext-cookies.js
+++ b/toolkit/components/extensions/ext-cookies.js
@@ -144,23 +144,25 @@ function checkSetCookiePermissions(exten
   cookie.host = cookie.host.toLowerCase();
 
   if (cookie.host != uri.host) {
     // Not an exact match, so check for a valid subdomain.
     let baseDomain;
     try {
       baseDomain = Services.eTLD.getBaseDomain(uri);
     } catch (e) {
+      /* istanbul ignore else */
       if (e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS ||
           e.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
         // The cookie service uses these to determine whether the domain
         // requires an exact match. We already know we don't have an exact
         // match, so return false. In all other cases, re-raise the error.
         return false;
       }
+      /* istanbul ignore next */
       throw e;
     }
 
     // The cookie domain must be a subdomain of the base domain. This prevents
     // us from setting cookies for domains like ".co.uk".
     // The domain of the requesting URL must likewise be a subdomain of the
     // cookie domain. This prevents us from setting cookies for entirely
     // unrelated domains.
new file mode 100755
--- /dev/null
+++ b/toolkit/components/extensions/instrument_code.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+from __future__ import print_function
+import json
+import os
+import subprocess
+
+
+EXTS = '.js', '.jsm'
+
+ROOT = 'dist/bin'
+
+DATA_DIRS = ('dist/bin', 'dist/bin/browser')
+
+PATTERNS = ('%s/dist/bin/%s/%s',
+            '%s/dist/Nightly.app/Contents/Resources/%s/%s')
+
+CODE_DIRS = ('toolkit/components/extensions/',
+             'browser/components/extensions/',
+             'toolkit/modules/addons/',
+             'toolkit/components/utils/simpleServices.js')
+
+REPO = os.path.abspath('.')
+CODE_DIRS = tuple(os.path.join(REPO, d) for d in CODE_DIRS)
+
+processes = {}
+
+
+mach = subprocess.Popen(['./mach', 'environment', '--format=json'],
+                        stdout=subprocess.PIPE)
+
+data = mach.communicate()[0]
+if not isinstance(data, type(u'')):
+    # Oh, Python...
+    data = data.decode('utf-8')
+
+config = json.loads(data)
+
+print('Entering object directory %s' % config['topobjdir'])
+objdir = config['topobjdir']
+
+
+def instrument(input, output):
+    print('Instrumenting %s' % input[len(REPO) + 1:])
+
+    os.unlink(output)
+    proc = subprocess.Popen(
+        ['babel',
+         '--plugins', 'transform-async-to-generator,istanbul',
+         '-o', output, input],
+        stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    processes[output] = proc
+
+for data_dir in DATA_DIRS:
+    data_file = '%s/faster/install_%s' % (objdir, data_dir.replace('/', '_'))
+    with open(data_file) as f:
+        for line in f:
+            fields = line.rstrip().split('\x1f')
+            if len(fields) == 3:
+                _, output, source = fields
+                if source.startswith(CODE_DIRS) and source.endswith(EXTS):
+                    dir_ = data_dir[len(ROOT):]
+                    for pat in PATTERNS:
+                        file_ = pat % (objdir, dir_, output)
+                        if os.path.exists(file_):
+                            instrument(source, file_)
+
+for path, proc in processes.items():
+    stdout, stderr = proc.communicate()
+    if not stderr:
+        pass
+    else:
+        print('Error processing "%s": %s' % (path, stderr))
--- a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_filter.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_filter.html
@@ -14,22 +14,24 @@
 
 if (AppConstants.platform === "android") {
   SimpleTest.requestLongerTimeout(6);
 }
 
 let windowData, testWindow;
 
 add_task(function* setup() {
-  let chromeScript = SpecialPowers.loadChromeScript(function() {
-    let cache = Components.classes["@mozilla.org/netwerk/cache-storage-service;1"]
-                          .getService(Components.interfaces.nsICacheStorageService);
-    cache.clear();
-  });
-  chromeScript.destroy();
+  if (AppConstants.platform === "android") {
+    let chromeScript = SpecialPowers.loadChromeScript(function() {
+      let cache = Components.classes["@mozilla.org/netwerk/cache-storage-service;1"]
+                            .getService(Components.interfaces.nsICacheStorageService);
+      cache.clear();
+    });
+    chromeScript.destroy();
+  }
 
   testWindow = window.open("about:blank", "_blank", "width=100,height=100");
   yield waitForLoad(testWindow);
 
   // Fetch the windowId and tabId we need to filter with WebRequest.
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       permissions: [
--- a/toolkit/components/extensions/test/xpcshell/head.js
+++ b/toolkit/components/extensions/test/xpcshell/head.js
@@ -24,16 +24,20 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
                                   "resource://testing-common/httpd.js");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WebExtCoverage",
+                                  "resource://gre/modules/WebExtCoverage.jsm");
+
+do_register_cleanup(() => WebExtCoverage.saveAllCoverage(false));
 
 ExtensionTestUtils.init(this);
 
 /**
  * Creates a new HttpServer for testing, and begins listening on the
  * specified port. Automatically shuts down the server when the test
  * unit ends.
  *
new file mode 100755
--- /dev/null
+++ b/toolkit/components/extensions/test_coverage.sh
@@ -0,0 +1,52 @@
+#!/bin/sh
+set -e
+
+IFS="$(echo)"
+cd $(hg root)
+
+echo Instrumenting WebExtension code
+${PYTHON:-python} toolkit/components/extensions/instrument_code.py
+
+: ${MACH:=./mach}
+
+if test -n "$GECKO_JS_COVERAGE_OUTPUT_DIR"
+then tmpdir="$GECKO_JS_COVERAGE_OUTPUT_DIR"
+else tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/webext-coverage.XXXXXX")
+fi
+export GECKO_JS_COVERAGE_OUTPUT_DIR="$tmpdir"
+
+echo
+echo Outputting coverage data to: $tmpdir
+echo
+
+mochi() {
+  ${MACH} mochitest --keep-open=false "$@"
+  ${MACH} mochitest --keep-open=false --disable-e10s "$@"
+}
+
+mochi --quiet toolkit/components/extensions/test/mochitest
+mochi --quiet browser/components/extensions/test/browser
+mochi --quiet --tag webextensions \
+  devtools/client/aboutdebugging/test
+${MACH} xpcshell-test --tag webextensions \
+  toolkit/components/extensions/test/xpcshell \
+  browser/components/extensions/test/xpcshell \
+  toolkit/mozapps/extensions/test/xpcshell
+
+
+cd "$tmpdir"
+mkdir coverage
+
+echo
+echo Generating full coverage report at "$tmpdir/coverage/index.html"
+istanbul report html
+echo
+
+for dir in content default
+do
+  echo Generating $dir process coverage report at "$tmpdir/coverage/$dir/index.html"
+  istanbul report --dir "coverage/$dir" --include "coverage-$dir-*.json" html
+  echo
+done
+
+# vim:se sts=2 sw=2 et ft=sh:
--- a/toolkit/components/utils/simpleServices.js
+++ b/toolkit/components/utils/simpleServices.js
@@ -13,16 +13,19 @@
 "use strict";
 
 const Cc = Components.classes;
 const Cu = Components.utils;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
 
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
 function AddonPolicyService() {
   this.wrappedJSObject = this;
--- a/toolkit/modules/addons/MatchPattern.jsm
+++ b/toolkit/modules/addons/MatchPattern.jsm
@@ -9,16 +9,20 @@ const Ci = Components.interfaces;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
+
 this.EXPORTED_SYMBOLS = ["MatchPattern", "MatchGlobs", "MatchURLFilters"];
 
 /* globals MatchPattern, MatchGlobs */
 
 const PERMITTED_SCHEMES = ["http", "https", "file", "ftp", "data"];
 const PERMITTED_SCHEMES_REGEXP = PERMITTED_SCHEMES.join("|");
 
 // This function converts a glob pattern (containing * and possibly ?
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/addons/WebExtCoverage.jsm
@@ -0,0 +1,149 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+this.EXPORTED_SYMBOLS = ["WebExtCoverage"];
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+                                  "resource://gre/modules/osfile.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "environment",
+                                   "@mozilla.org/process/environment;1",
+                                   Ci.nsIEnvironment);
+
+
+const COVERAGE_PROPERTY = "__coverage__";
+const ENV_COVERAGE_OUTPUT_DIR = "GECKO_JS_COVERAGE_OUTPUT_DIR";
+
+const COVERAGE_MESSAGE = "WebExtCoverage:Update";
+
+const PROCESS_TYPES = Object.freeze({
+  [Services.appinfo.PROCESS_TYPE_DEFAULT]: "default",
+  [Services.appinfo.PROCESS_TYPE_CONTENT]: "content",
+});
+
+
+let globalID = 0;
+let initialized = false;
+
+const isContent = Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT;
+
+function mangleFilename(filename) {
+  return filename.replace(new RegExp("^(resource|chrome)://[^/]+/|^file://.*/obj[^/]*/dist/bin/"), "")
+                 .replace(/\//g, "-");
+}
+
+this.WebExtCoverage = {
+  promises: new Set(),
+
+  coverageObjects: new Map(),
+
+  init() {
+    if (initialized) {
+      return this.outputDir;
+    }
+    initialized = true;
+
+    this.outputDir = environment.get(ENV_COVERAGE_OUTPUT_DIR);
+    if (!this.outputDir) {
+      return false;
+    }
+
+    let {processID, processType} = Services.appinfo;
+    this.baseFilename = `coverage-${PROCESS_TYPES[processType]}-pid_${processID}`;
+
+    if (!isContent) {
+      OS.File.profileBeforeChange.addBlocker("WebExtension coverage data flush",
+                                             () => this.saveAllCoverage());
+
+      Services.ppmm.addMessageListener(COVERAGE_MESSAGE, this, true);
+    } else {
+      Services.obs.addObserver(this, "content-child-shutdown", false);
+    }
+
+    return true;
+  },
+
+  observe() {
+    this.saveAllCoverage();
+  },
+
+  receiveMessage({data}) {
+    for (let [key, coverage] of data.entries()) {
+      this.writeCoverageFile(key, coverage);
+    }
+  },
+
+  register(global) {
+    if (!(COVERAGE_PROPERTY in global)) {
+      return;
+    }
+    if (!this.init()) {
+      return;
+    }
+
+    let name = [this.baseFilename,
+                mangleFilename(Components.stack.caller.filename),
+                globalID++].join("-");
+
+    let outputFile = `${name}.json`;
+
+    this.coverageObjects.set(outputFile, global[COVERAGE_PROPERTY]);
+
+    if ("addEventListener" in global) {
+      let listener = () => {
+        global.removeEventListener("unload", listener);
+        this.saveCoverage(outputFile, true);
+      };
+      global.addEventListener("unload", listener);
+    }
+  },
+
+  writeCoverageFile(filename, coverage) {
+    if (isContent) {
+      Services.cpmm.sendAsyncMessage(
+        COVERAGE_MESSAGE,
+        new Map([[filename, coverage]]));
+
+      return Promise.resolve();
+    }
+
+    let path = OS.Path.join(this.outputDir, filename);
+    let promise = OS.File.writeAtomic(path, JSON.stringify(coverage),
+                                      {tmpPath: `${path}.tmp`});
+
+    this.promises.add(promise);
+    return promise.then(() => {
+      this.promises.delete(promise);
+    });
+  },
+
+  saveCoverage(filename, finalize = true) {
+    let coverage = this.coverageObjects.get(filename);
+    if (finalize) {
+      this.coverageObjects.delete(filename);
+    }
+
+    return this.writeCoverageFile(filename, coverage);
+  },
+
+  saveAllCoverage(finalize = true) {
+    if (isContent) {
+      Services.cpmm.sendAsyncMessage(COVERAGE_MESSAGE, this.coverageObjects);
+      return Promise.resolve();
+    }
+
+    for (let filename of this.coverageObjects.keys()) {
+      this.saveCoverage(filename, finalize);
+    }
+
+    return Promise.all(this.promises);
+  },
+};
--- a/toolkit/modules/addons/WebNavigation.jsm
+++ b/toolkit/modules/addons/WebNavigation.jsm
@@ -7,16 +7,19 @@
 const EXPORTED_SYMBOLS = ["WebNavigation"];
 
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
 
 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
                                   "resource:///modules/RecentWindow.jsm");
 
 // Maximum amount of time that can be passed and still consider
 // the data recent (similar to how is done in nsNavHistory,
 // e.g. nsNavHistory::CheckIsRecentEvent, but with a lower threshold value).
 const RECENT_DATA_THRESHOLD = 5 * 1000000;
--- a/toolkit/modules/addons/WebNavigationContent.js
+++ b/toolkit/modules/addons/WebNavigationContent.js
@@ -1,15 +1,18 @@
 "use strict";
 
 /* globals docShell */
 
 var Ci = Components.interfaces;
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
 
 XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
                                   "resource://gre/modules/WebNavigationFrames.jsm");
 
 function loadListener(event) {
   let document = event.target;
   let window = document.defaultView;
   let url = document.documentURI;
--- a/toolkit/modules/addons/WebNavigationFrames.jsm
+++ b/toolkit/modules/addons/WebNavigationFrames.jsm
@@ -3,16 +3,20 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const EXPORTED_SYMBOLS = ["WebNavigationFrames"];
 
 var Ci = Components.interfaces;
 
+Components.utils.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
+
 /* exported WebNavigationFrames */
 
 function getWindowId(window) {
   return window.QueryInterface(Ci.nsIInterfaceRequestor)
                .getInterface(Ci.nsIDOMWindowUtils)
                .outerWindowID;
 }
 
--- a/toolkit/modules/addons/WebRequest.jsm
+++ b/toolkit/modules/addons/WebRequest.jsm
@@ -13,16 +13,19 @@ const Cc = Components.classes;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 const {nsIHttpActivityObserver, nsISocketTransport} = Ci;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
 
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
                                   "resource://gre/modules/BrowserUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
                                   "resource://gre/modules/ExtensionUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "WebRequestCommon",
                                   "resource://gre/modules/WebRequestCommon.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "WebRequestUpload",
--- a/toolkit/modules/addons/WebRequestCommon.jsm
+++ b/toolkit/modules/addons/WebRequestCommon.jsm
@@ -8,16 +8,20 @@ const EXPORTED_SYMBOLS = ["WebRequestCom
 
 /* exported WebRequestCommon */
 
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 const Cr = Components.results;
 
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
+
 var WebRequestCommon = {
   typeForPolicyType(type) {
     switch (type) {
       case Ci.nsIContentPolicy.TYPE_DOCUMENT: return "main_frame";
       case Ci.nsIContentPolicy.TYPE_SUBDOCUMENT: return "sub_frame";
       case Ci.nsIContentPolicy.TYPE_STYLESHEET: return "stylesheet";
       case Ci.nsIContentPolicy.TYPE_SCRIPT: return "script";
       case Ci.nsIContentPolicy.TYPE_IMAGE: return "image";
--- a/toolkit/modules/addons/WebRequestContent.js
+++ b/toolkit/modules/addons/WebRequestContent.js
@@ -6,16 +6,19 @@
 
 var Ci = Components.interfaces;
 var Cc = Components.classes;
 var Cu = Components.utils;
 var Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/WebExtCoverage.jsm");
+
+WebExtCoverage.register(this);
 
 XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
                                   "resource://gre/modules/MatchPattern.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "WebRequestCommon",
                                   "resource://gre/modules/WebRequestCommon.jsm");
 
 const IS_HTTP = /^https?:/;
 
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -14,16 +14,17 @@ TESTING_JS_MODULES += [
     'tests/PromiseTestUtils.jsm',
     'tests/xpcshell/TestIntegration.jsm',
 ]
 
 SPHINX_TREES['toolkit_modules'] = 'docs'
 
 EXTRA_JS_MODULES += [
     'addons/MatchPattern.jsm',
+    'addons/WebExtCoverage.jsm',
     'addons/WebNavigation.jsm',
     'addons/WebNavigationContent.js',
     'addons/WebNavigationFrames.jsm',
     'addons/WebRequest.jsm',
     'addons/WebRequestCommon.jsm',
     'addons/WebRequestContent.js',
     'addons/WebRequestUpload.jsm',
     'AsyncPrefs.jsm',