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
.
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
.
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:
- background
<object>
, with propertiesdom
,window
,document
,browser
anddestroy
(If background page or scripts are defined in the manifest) - popup
<object>
, with propertiesdom
,window
,document
,browser
anddestroy
(If browser_action with default_popup is defined in the manifest) - pageActionPopup
<object>
, with propertiesdom
,window
,document
,browser
anddestroy
(If page_action with default_popup is defined in the manifest) - sidebar
<object>
, with propertiesdom
,window
,document
,browser
anddestroy
(If sidebar_action with default_panel is defined in the manifest) - destroy
<function>
, shortcut tobackground.destroy
,popup.destroy
andsidebar.destroy
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])
.
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
});
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 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.
Not supported, but you could use webextension-polyfill.
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
.
- path
<string>
, required, absolute path to themanifest.json
file - options
<object>
, optional- background
<object|false>
optional, iffalse
is given background wont be loaded- jsdom
<object>
, optional, this will set all given properties as options for the JSDOM constructor, an useful example might bebeforeParse(window)
. Note: Settingresources
orrunScripts
might lead to unexpected behavior. - afterBuild(background)
<function>
optional, executed directly after the background dom is build (might be useful to do things before the popup dom starts building). If a Promise is returned it will be resolved before continuing.
- jsdom
- popup
<object|false>
optional, iffalse
is given popup wont be loaded- jsdom
<object>
, optional, this will set all given properties as options for the JSDOM constructor, an useful example might bebeforeParse(window)
. Note: Settingresources
orrunScripts
might lead to unexpected behavior.
- jsdom
- pageActionPopup
<object|false>
optional, iffalse
is given popup wont be loaded- jsdom
<object>
, optional, this will set all given properties as options for the JSDOM constructor, an useful example might bebeforeParse(window)
. Note: Settingresources
orrunScripts
might lead to unexpected behavior. - afterBuild(popup)
<function>
optional, executed after the popup dom is build. If a Promise is returned it will be resolved before continuing.
- jsdom
- sidebar
<object|false>
optional, iffalse
is given sidebar wont be loaded- jsdom
<object>
, optional, this will set all given properties as options for the JSDOM constructor, an useful example might bebeforeParse(window)
. Note: Settingresources
orrunScripts
might lead to unexpected behavior. - afterBuild(sidebar)
<function>
optional, executed after the sidebar dom is build. If a Promise is returned it will be resolved before continuing.
- jsdom
- autoload
<boolean>
optional, iffalse
will not automatically load background/popup/sidebar (might be useful forloadURL
) - apiFake
<boolean>
optional, iftrue
automatically applies API fakes to thebrowser
usingwebextensions-api-fake
and ifpath/_locales
is present its content will get passed down to api-fake. - wiring
<boolean>
optional, iftrue
the automatic wiring is enabled
- background
Returns a Promise that resolves an <object>
with the following properties in case of success:
-
background
<object>
- dom
<object>
the JSDOM object - window
<object>
shortcut todom.window
- document
<object>
shortcut todom.window.document
- browser
<object>
stubbedbrowser
usingwebextensions-api-mock
- destroy
<function>
destroy thedom
and potentially write coverage data if executed withnyc
. Returns a Promise that resolves if destroying is done.
- dom
-
popup
<object>
- dom
<object>
the JSDOM object - window
<object>
shortcut todom.window
- document
<object>
shortcut todom.window.document
- browser
<object>
stubbedbrowser
usingwebextensions-api-mock
- destroy
<function>
destroy thedom
and potentially write coverage data if executed withnyc
. Returns a Promise that resolves if destroying is done. - helper
<object>
- clickElementById(id)
<function>
shortcut fordom.window.document.getElementById(id).click();
, returns a promise
- clickElementById(id)
- dom
-
pageActionPopup
<object>
- dom
<object>
the JSDOM object - window
<object>
shortcut todom.window
- document
<object>
shortcut todom.window.document
- browser
<object>
stubbedbrowser
usingwebextensions-api-mock
- destroy
<function>
destroy thedom
and potentially write coverage data if executed withnyc
. Returns a Promise that resolves if destroying is done. - helper
<object>
- clickElementById(id)
<function>
shortcut fordom.window.document.getElementById(id).click();
, returns a promise
- clickElementById(id)
- dom
-
sidebar
<object>
- dom
<object>
the JSDOM object - window
<object>
shortcut todom.window
- document
<object>
shortcut todom.window.document
- browser
<object>
stubbedbrowser
usingwebextensions-api-mock
- destroy
<function>
destroy thedom
and potentially write coverage data if executed withnyc
. Returns a Promise that resolves if destroying is done. - helper
<object>
- clickElementById(id)
<function>
shortcut fordom.window.document.getElementById(id).click();
, returns a promise
- clickElementById(id)
- dom
-
destroy
<function>
, shortcut to callbackground.destroy
,popup.destroy
andsidebar.destroy
. Returns a Promise that resolves if destroying is done.
Load an arbitrary .html
file, accepts the following parameters:
- path
<string>
, required, absolute path to the html file that should be loaded - options
<object>
, optional, accepts the following parameters- apiFake
<boolean>
optional, iftrue
automatically applies API fakes to thebrowser
usingwebextensions-api-fake
- jsdom
<object>
, optional, this will set all given properties as options for the JSDOM constructor, an useful example might bebeforeParse(window)
. Note: Settingresources
orrunScripts
. might lead to unexpected behavior
- apiFake
Returns a Promise that resolves an <object>
with the following properties in case of success:
- dom
<object>
the JSDOM object - window
<object>
shortcut todom.window
- document
<object>
shortcut todom.window.document
- browser
<object>
stubbedbrowser
usingwebextensions-api-mock
- destroy
<function>
destroy thedom
and potentially write coverage data if executed withnyc
. Returns a Promise that resolves if destroying is done.
If you're looking for a way to do functional testing with GeckoDriver then webextensions-geckodriver
might be for you.
sinon.useFakeTimers({
toFake: ["setTimeout", "clearTimeout", "setInterval", "clearInterval"]
});