Home

Awesome

WebExtensions JSDOM

When testing WebExtensions you might want to test your browser_action/page_action default_popup, sidebar_action default_panel or background page/scripts inside JSDOM. This package lets you do that based on the manifest.json. It will automatically stub window.browser with webextensions-api-mock.

Installation

npm install --save-dev webextensions-jsdom sinon

Important: sinon is a peer dependency, so you have to install it yourself. That's because it can otherwise lead to unexpected assertion behavior when sinon does instanceof checks internally. It also allows to upgrade sinon without the need to bump the version in webextensions-api-mock.

Usage

const webExtensionsJSDOM = require("webextensions-jsdom");
const webExtension = await webExtensionsJSDOM.fromManifest(
  "/absolute/path/to/manifest.json"
);

Based on what's given in your manifest.json this will create JSDOM instances with stubbed browser and load popup/sidebar/background in it.

The resolved return value is an <object> with several properties:

dom is a new JSDOM instance. window is a shortcut to dom.window. document is a shortcut to dom.window.document. browser is a new webextensions-api-mock instance that is also exposed on dom.window.browser. And destroy is a function to clean up. More infos in the API docs.

If you expose variables in your code on window, you can access them now, or trigger registered listeners by e.g. browser.webRequest.onBeforeRequest.addListener.yield([arguments]).

Automatic wiring

If popup/sidebar and background are defined and loaded then runtime.sendMessage in the popup is automatically wired with runtime.onMessage in the background if you pass the wiring: true option. That makes it possible to e.g. "click" elements in the popup and then check if the background was called accordingly, making it ideal for feature-testing.

await webExtensionsJSDOM.fromManifest("/absolute/path/to/manifest.json", {
  wiring: true
});

API Fake

Passing apiFake: true in the options to fromManifest automatically applies webextensions-api-fake to the browser stubs. It will imitate some of the WebExtensions API behavior (like an in-memory storage), so you don't have to manually define behavior. This is especially useful when feature-testing.

await webExtensionsJSDOM.fromManifest("/absolute/path/to/manifest.json", {
  apiFake: true
});

Code Coverage

Code coverage with nyc / istanbul is supported if you execute the test using webextensions-jsdom with nyc. To get coverage-output you need to call the exposed destroy function after the background, popup and/or sidebar are no longer needed. This should ideally be after each test.

If you want to know how that's possible you can check out this excellent article by @freaktechnik.

Chrome Extensions

Not supported, but you could use webextension-polyfill.

Example

In your manifest.json you have default_popup and background page defined:

{
  "browser_action": {
    "default_popup": "popup.html"
  },

  "background": {
    "page": "background.html"
  }
}

Note: "scripts" are supported too.

In your popup.js loaded from popup.html you have something like this:

document.getElementById("doSomethingUseful").addEventListener("click", () => {
  browser.runtime.sendMessage({
    method: "usefulMessage"
  });
});

and in your background.js loaded from the background.html

browser.runtime.onMessage.addListener(message => {
  // do something useful with the message
});

To test this with webextensions-jsdom you can do (using mocha, chai and sinon-chai in this case):

const path = require("path");
const sinonChai = require("sinon-chai");
const chai = require("chai");
const expect = chai.expect;
chai.use(sinonChai);

const webExtensionsJSDOM = require("webextensions-jsdom");
const manifestPath = path.resolve(
  path.join(__dirname, "path/to/manifest.json")
);

describe("Example", () => {
  let webExtension;
  beforeEach(async () => {
    webExtension = await webExtensionsJSDOM.fromManifest(manifestPath, {
      wiring: true
    });
  });

  describe("Clicking in the popup", () => {
    beforeEach(async () => {
      await webExtension.popup.document
        .getElementById("doSomethingUseful")
        .click();
    });

    it("should call the background", async () => {
      expect(
        webExtension.background.browser.onMessage.addListener
      ).to.have.been.calledWithMatch({
        method: "usefulMessage"
      });
    });
  });

  afterEach(async () => {
    await webExtension.destroy();
  });
});

There's a fully functional example in examples/random-container-tab.

API

Exported function fromManifest(path[, options])

Returns a Promise that resolves an <object> with the following properties in case of success:

Exported function fromFile(path[, options])

Load an arbitrary .html file, accepts the following parameters:

Returns a Promise that resolves an <object> with the following properties in case of success:

GeckoDriver

If you're looking for a way to do functional testing with GeckoDriver then webextensions-geckodriver might be for you.

Sinon useFakeTimers

sinon.useFakeTimers({
  toFake: ["setTimeout", "clearTimeout", "setInterval", "clearInterval"]
});