test: introduced a test suite for unit testing.

Merge pull request #2 from rpl/proposal/unit-testing
This commit is contained in:
Luca Greco 2016-11-14 19:20:38 +01:00 committed by GitHub
commit df71efea17
12 changed files with 621 additions and 6 deletions

View File

@ -69,7 +69,11 @@
"generator-star-spacing": [2, {"before": false, "after": true}], "generator-star-spacing": [2, {"before": false, "after": true}],
// Two space indent // Two space indent
"indent": [2, 2, {"SwitchCase": 1}], "indent": [2, 2, {
"SwitchCase": 1,
"CallExpression": {"arguments": "first"}
}
],
// Space after colon not before in property declarations // Space after colon not before in property declarations
"key-spacing": [2, {"beforeColon": false, "afterColon": true, "mode": "minimum"}], "key-spacing": [2, {"beforeColon": false, "afterColon": true, "mode": "minimum"}],

4
.gitignore vendored
View File

@ -1,2 +1,6 @@
node_modules/* node_modules/*
dist/* dist/*
## code coverage
coverage/
.nyc_output/

18
.travis.yml Normal file
View File

@ -0,0 +1,18 @@
language: node_js
sudo: false
node_js:
## Some of the ES6 syntax used in the browser-polyfill sources is only supported on nodejs >= 6
- '6'
script:
- npm run build
- COVERAGE=y npm run test-coverage
after_script: npm run publish-coverage
notifications:
irc:
channels:
- irc.mozilla.org#webextensions
on_success: change
on_failure: always

View File

@ -12,8 +12,15 @@ module.exports = function(grunt) {
grunt.initConfig({ grunt.initConfig({
pkg: grunt.file.readJSON("package.json"), pkg: grunt.file.readJSON("package.json"),
coveralls: {
all: {
src: "coverage/lcov.info",
},
},
eslint: { eslint: {
src: ["browser-polyfill.in.js", "Gruntfile.js"], src: ["src/browser-polyfill.js", "Gruntfile.js"],
test: ["test/**/*.js"],
}, },
replace: { replace: {
@ -41,7 +48,7 @@ module.exports = function(grunt) {
{ {
expand: true, expand: true,
flatten: true, flatten: true,
src: ["browser-polyfill.in.js"], src: ["src/browser-polyfill.js"],
dest: "dist/", dest: "dist/",
}, },
], ],
@ -81,6 +88,7 @@ module.exports = function(grunt) {
grunt.loadNpmTasks("gruntify-eslint"); grunt.loadNpmTasks("gruntify-eslint");
grunt.loadNpmTasks("grunt-replace"); grunt.loadNpmTasks("grunt-replace");
grunt.loadNpmTasks("grunt-coveralls");
require("google-closure-compiler").grunt(grunt); require("google-closure-compiler").grunt(grunt);
grunt.registerTask("default", ["eslint", "replace", "closure-compiler"]); grunt.registerTask("default", ["eslint", "replace", "closure-compiler"]);

View File

@ -13,9 +13,27 @@
}, },
"homepage": "https://github.com/mozilla/webextension-polyfill", "homepage": "https://github.com/mozilla/webextension-polyfill",
"devDependencies": { "devDependencies": {
"chai": "^3.5.0",
"google-closure-compiler": "^20160911.0.0", "google-closure-compiler": "^20160911.0.0",
"grunt": "^1.0.1", "grunt": "^1.0.1",
"grunt-coveralls": "^1.0.1",
"grunt-replace": "*", "grunt-replace": "*",
"gruntify-eslint": "*" "gruntify-eslint": "*",
"eslint": "3.9.1",
"istanbul-lib-instrument": "^1.1.3",
"jsdom": "^9.6.0",
"mocha": "^3.1.0",
"nyc": "^8.3.1",
"sinon": "^1.17.6"
},
"nyc": {
"reporter": ["lcov", "text", "html"],
"instrument": false
},
"scripts": {
"build": "grunt",
"test": "mocha",
"test-coverage": "COVERAGE=y nyc mocha",
"publish-coverage": "grunt coveralls"
} }
} }

View File

@ -105,13 +105,15 @@ if (typeof browser === "undefined") {
* The generated wrapper function. * The generated wrapper function.
*/ */
const wrapAsyncFunction = (name, metadata) => { const wrapAsyncFunction = (name, metadata) => {
const pluralizeArguments = (numArgs) => numArgs == 1 ? "argument" : "arguments";
return function asyncFunctionWrapper(target, ...args) { return function asyncFunctionWrapper(target, ...args) {
if (args.length < metadata.minArgs) { if (args.length < metadata.minArgs) {
throw new Error(`Expected at least ${metadata.minArgs} arguments for ${name}(), got ${args.length}`); throw new Error(`Expected at least ${metadata.minArgs} ${pluralizeArguments(metadata.minArgs)} for ${name}(), got ${args.length}`);
} }
if (args.length > metadata.maxArgs) { if (args.length > metadata.maxArgs) {
throw new Error(`Expected at most ${metadata.maxArgs} arguments for ${name}(), got ${args.length}`); throw new Error(`Expected at most ${metadata.maxArgs} ${pluralizeArguments(metadata.maxArgs)} for ${name}(), got ${args.length}`);
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

12
test/.eslintrc Normal file
View File

@ -0,0 +1,12 @@
{
"env": {
"mocha": true,
"node": true,
"browser": true,
"webextensions": true
},
"globals": {},
"rules": {
"max-nested-callbacks": ["warn", 6]
}
}

91
test/setup.js Normal file
View File

@ -0,0 +1,91 @@
"use strict";
const fs = require("fs");
const {createInstrumenter} = require("istanbul-lib-instrument");
const {jsdom, createVirtualConsole} = require("jsdom");
var virtualConsole = createVirtualConsole();
// Optionally print console logs from the jsdom window.
if (process.env.ENABLE_JSDOM_CONSOLE == "y") {
virtualConsole.sendTo(console);
}
// Path to the browser-polyfill script, relative to the current work dir
// where mocha is executed.
const BROWSER_POLYFILL_PATH = "./dist/browser-polyfill.js";
// Create the jsdom window used to run the tests
const testDOMWindow = jsdom("", {virtualConsole}).defaultView;
// Copy the code coverage of the browser-polyfill script from the jsdom window
// to the nodejs global, where nyc expects to find the code coverage data to
// render in the reports.
after(() => {
if (testDOMWindow && process.env.COVERAGE == "y") {
global.__coverage__ = testDOMWindow.__coverage__;
}
});
function setupTestDOMWindow(chromeObject, browserObject = undefined) {
return new Promise((resolve, reject) => {
const window = testDOMWindow;
// Inject the fake chrome object used as a fixture for the particular
// browser-polyfill test scenario.
window.chrome = chromeObject;
// Set (or reset) the browser property.
if (browserObject) {
window.browser = browserObject;
} else {
// TODO: change into `delete window.browser` once tmpvar/jsdom#1622 has been fixed.
window.browser = undefined;
}
const scriptEl = window.document.createElement("script");
if (process.env.COVERAGE == "y") {
// If the code coverage is enabled, instrument the code on the fly
// before executing it in the jsdom window.
const inst = createInstrumenter({
compact: false, esModules: false, produceSourceMap: false,
});
const scriptContent = fs.readFileSync(BROWSER_POLYFILL_PATH, "utf-8");
scriptEl.textContent = inst.instrumentSync(scriptContent, BROWSER_POLYFILL_PATH);
} else {
scriptEl.src = BROWSER_POLYFILL_PATH;
}
let onLoad;
let onLoadError;
let onError;
let cleanLoadListeners = () => {
scriptEl.removeEventListener("load", onLoad);
scriptEl.removeEventListener("error", onLoadError);
window.removeEventListener("error", onError);
};
onLoad = () => { cleanLoadListeners(); resolve(window); };
onLoadError = () => {
cleanLoadListeners();
reject(new Error(`Error loading script: ${BROWSER_POLYFILL_PATH}`));
};
onError = (err) => { cleanLoadListeners(); reject(err); };
// Listen to any uncaught errors.
window.addEventListener("error", onError);
scriptEl.addEventListener("error", onLoadError);
scriptEl.addEventListener("load", onLoad);
window.document.body.appendChild(scriptEl);
});
}
module.exports = {
BROWSER_POLYFILL_PATH,
setupTestDOMWindow,
};

View File

@ -0,0 +1,90 @@
"use strict";
const {deepEqual, equal, fail, throws} = require("chai").assert;
const sinon = require("sinon");
const {setupTestDOMWindow} = require("./setup");
describe("browser-polyfill", () => {
describe("wrapped async functions", () => {
it("returns a promise which resolves to the callback parameters", () => {
const fakeChrome = {
alarms: {clear: sinon.stub()},
runtime: {
lastError: null,
requestUpdateCheck: sinon.stub(),
},
tabs: {
query: sinon.stub(),
},
};
return setupTestDOMWindow(fakeChrome).then(window => {
// Test for single callback argument.
fakeChrome.alarms.clear
.onFirstCall().callsArgWith(1, "res1");
// Test for single array callback argument.
fakeChrome.tabs.query
.onFirstCall().callsArgWith(1, ["res1", "res2"]);
// Test for multiple callback arguments.
fakeChrome.runtime.requestUpdateCheck
.onFirstCall().callsArgWith(0, "res1", "res2");
return Promise.all([
window.browser.alarms.clear("test1"),
window.browser.tabs.query({active: true}),
window.browser.runtime.requestUpdateCheck(),
]);
}).then(results => {
equal(results[0], "res1", "Fake alarms.clear call resolved to a single value");
deepEqual(results[1], ["res1", "res2"],
"Fake tabs.query resolved to an array of values");
deepEqual(results[2], ["res1", "res2"],
"Fake runtime.requestUpdateCheck resolved to an array of values");
});
});
it("rejects the returned promise if chrome.runtime.lastError is not null", () => {
const fakeChrome = {
runtime: {
lastError: new Error("fake lastError"),
},
tabs: {
query: sinon.stub(),
},
};
return setupTestDOMWindow(fakeChrome).then(window => {
// Test for single array callback argument.
fakeChrome.tabs.query
.onFirstCall().callsArgWith(1, ["res1", "res2"]);
return window.browser.tabs.query({active: true}).then(
() => fail("Expected a rejected promise"),
(err) => equal(err, fakeChrome.runtime.lastError,
"Got the expected error in the rejected promise")
);
});
});
it("throws if the number of arguments are not in the range defined in the metadata", () => {
const fakeChrome = {
runtime: {
lastError: null,
sendMessage: sinon.spy(),
},
};
return setupTestDOMWindow(fakeChrome).then(window => {
throws(() => {
window.browser.runtime.sendMessage();
}, "Expected at least 1 argument for sendMessage(), got 0");
throws(() => {
window.browser.runtime.sendMessage("0", "1", "2", "3");
}, "Expected at most 3 arguments for sendMessage(), got 4");
});
});
});
});

View File

@ -0,0 +1,65 @@
"use strict";
const {deepEqual, equal, ok} = require("chai").assert;
const {setupTestDOMWindow} = require("./setup");
describe("browser-polyfill", () => {
it("wraps the global chrome namespace with a global browser namespace", () => {
const fakeChrome = {};
return setupTestDOMWindow(fakeChrome).then(window => {
equal(typeof window.browser, "object", "Got the window.browser object");
});
});
it("does not override the global browser namespace if it already exists", () => {
const fakeChrome = {
runtime: {lastError: null},
};
const fakeBrowser = {
mycustomns: {mykey: true},
};
return setupTestDOMWindow(fakeChrome, fakeBrowser).then(window => {
deepEqual(window.browser, fakeBrowser,
"The existing browser has not been wrapped");
});
});
describe("browser wrapper", () => {
it("supports custom properties defined using Object.defineProperty", () => {
const fakeChrome = {};
return setupTestDOMWindow(fakeChrome).then(window => {
Object.defineProperty(window.browser, "myns", {
enumerable: true,
configurable: true,
value: {mykey: true},
});
ok("myns" in window.browser, "The custom property exists");
ok("mykey" in window.browser.myns,
"The content of the custom property exists");
deepEqual(window.browser.myns, {mykey: true},
"The custom property has the expected content");
delete window.browser.myns;
ok(!("myns" in window.browser),
"The deleted custom defined property has been removed");
});
});
it("returns undefined for property undefined in the target", () => {
const fakeChrome = {myns: {mykey: true}};
return setupTestDOMWindow(fakeChrome).then(window => {
equal(window.browser.myns.mykey, true,
"Got the expected result from a wrapped property");
equal(window.browser.myns.nonexistent, undefined,
"Got undefined for non existent property");
equal(window.browser.nonexistent, undefined,
"Got undefined for non existent namespaces");
});
});
});
});

View File

@ -0,0 +1,104 @@
"use strict";
const {deepEqual, equal, ok} = require("chai").assert;
const sinon = require("sinon");
const {setupTestDOMWindow} = require("./setup");
describe("browser-polyfill", () => {
describe("proxies non-wrapped functions", () => {
it("should proxy non-wrapped methods", () => {
const fakeChrome = {
runtime: {
nonwrappedmethod: sinon.spy(),
},
};
return setupTestDOMWindow(fakeChrome).then(window => {
ok(window.browser.runtime.nonwrappedmethod);
const fakeCallback = () => {};
window.browser.runtime.nonwrappedmethod(fakeCallback);
const receivedCallback = fakeChrome.runtime.nonwrappedmethod.firstCall.args[0];
equal(fakeCallback, receivedCallback,
"The callback has not been wrapped for the nonwrappedmethod");
});
});
it("should proxy getters and setters", () => {
const fakeChrome = {
runtime: {myprop: "previous-value"},
nowrapns: {
nowrapkey: "previous-value",
nowrapkey2: "previous-value",
},
};
return setupTestDOMWindow(fakeChrome).then(window => {
// Check that the property values on the generated wrapper.
equal(window.browser.runtime.myprop, "previous-value",
"Got the expected result from setting a wrapped property name");
equal(window.browser.nowrapns.nowrapkey, "previous-value",
"Got the expected result from setting a wrapped property name");
// Update the properties on the generated wrapper.
const setResult = window.browser.runtime.myprop = "new-value";
const setResult2 = window.browser.nowrapns.nowrapkey = "new-value";
// Check the results of setting the new value of the wrapped properties.
equal(setResult, "new-value",
"Got the expected result from setting a wrapped property name");
equal(setResult2, "new-value",
"Got the expected result from setting a wrapped property name");
// Verify that the wrapped properties has been updated.
equal(window.browser.runtime.myprop, "new-value",
"Got the expected updated value from the browser property");
equal(window.browser.nowrapns.nowrapkey, "new-value",
"Got the expected updated value from the browser property");
// Verify that the target properties has been updated.
equal(window.chrome.runtime.myprop, "new-value",
"Got the expected updated value on the related chrome property");
equal(window.chrome.nowrapns.nowrapkey, "new-value",
"Got the expected updated value on the related chrome property");
// Set a property multiple times before read.
window.browser.nowrapns.nowrapkey2 = "new-value2";
window.browser.nowrapns.nowrapkey2 = "new-value3";
equal(window.chrome.nowrapns.nowrapkey2, "new-value3",
"Got the expected updated value on the related chrome property");
equal(window.browser.nowrapns.nowrapkey2, "new-value3",
"Got the expected updated value on the wrapped property");
});
});
it("deletes proxy getter/setter that are not wrapped", () => {
const fakeChrome = {};
return setupTestDOMWindow(fakeChrome).then(window => {
window.browser.newns = {newkey: "test-value"};
ok("newns" in window.browser, "The custom namespace is in the wrapper");
ok("newns" in window.chrome, "The custom namespace is in the target");
equal(window.browser.newns.newkey, "test-value",
"Got the expected result from setting a wrapped property name");
const setRes = window.browser.newns = {newkey2: "new-value"};
equal(window.browser.newns.newkey2, "new-value",
"The new non-wrapped getter is cached");
deepEqual(setRes, {newkey2: "new-value"},
"Got the expected result from setting a new wrapped property name");
deepEqual(window.browser.newns, window.chrome.newns,
"chrome.newns and browser.newns are the same");
delete window.browser.newns.newkey2;
equal(window.browser.newns.newkey2, undefined,
"Got the expected result from setting a wrapped property name");
ok(!("newkey2" in window.browser.newns),
"The deleted property is not listed anymore");
});
});
});
});

View File

@ -0,0 +1,199 @@
"use strict";
const {deepEqual, equal, ok} = require("chai").assert;
const sinon = require("sinon");
const {setupTestDOMWindow} = require("./setup");
describe("browser-polyfill", () => {
describe("wrapped runtime.onMessage listener", () => {
it("does not wrap the listener if it is not a function", () => {
const fakeChrome = {
runtime: {
lastError: null,
onMessage: {
addListener: sinon.spy(),
hasListener: sinon.stub(),
removeListener: sinon.spy(),
},
},
};
return setupTestDOMWindow(fakeChrome).then(window => {
const fakeNonFunctionListener = {fake: "non function listener"};
window.browser.runtime.onMessage.addListener(fakeNonFunctionListener);
deepEqual(fakeChrome.runtime.onMessage.addListener.firstCall.args[0],
fakeNonFunctionListener,
"The non-function listener has not been wrapped");
});
});
it("keeps track of the listeners added", () => {
const messageListener = sinon.spy();
const fakeChromeListeners = new Set();
const fakeChrome = {
runtime: {
lastError: null,
onMessage: {
addListener: sinon.spy((listener, ...args) => {
fakeChromeListeners.add(listener);
}),
hasListener: sinon.spy(listener => fakeChromeListeners.has(listener)),
removeListener: sinon.spy(listener => {
fakeChromeListeners.delete(listener);
}),
},
},
};
return setupTestDOMWindow(fakeChrome).then(window => {
equal(window.browser.runtime.onMessage.hasListener(messageListener),
false, "Got hasListener==false before the listener has been added");
window.browser.runtime.onMessage.addListener(messageListener);
equal(window.browser.runtime.onMessage.hasListener(messageListener),
true, "Got hasListener==true once the listener has been added");
// Add the same listener again to test that it will be called with the
// same wrapped listener.
window.browser.runtime.onMessage.addListener(messageListener);
ok(fakeChrome.runtime.onMessage.addListener.calledTwice,
"addListener has been called twice");
equal(fakeChrome.runtime.onMessage.addListener.secondCall.args[0],
fakeChrome.runtime.onMessage.addListener.firstCall.args[0],
"both the addListener calls received the same wrapped listener");
// Retrieve the wrapped listener and execute it to fake a received message.
const wrappedListener = fakeChrome.runtime.onMessage.addListener.firstCall.args[0];
wrappedListener("msg", {name: "sender"}, function sendResponse() {});
ok(messageListener.calledOnce, "The listener has been called once");
// Remove the listener.
window.browser.runtime.onMessage.removeListener(messageListener);
ok(fakeChrome.runtime.onMessage.removeListener.calledOnce,
"removeListener has been called once");
equal(fakeChrome.runtime.onMessage.addListener.secondCall.args[0],
fakeChrome.runtime.onMessage.removeListener.firstCall.args[0],
"both the addListener and removeListenercalls received the same wrapped listener");
equal(fakeChrome.runtime.onMessage.hasListener(messageListener), false,
"Got hasListener==false once the listener has been removed");
});
});
it("generates different wrappers for different listeners", () => {
const fakeChromeListeners = [];
const fakeChrome = {
runtime: {
lastError: null,
onMessage: {
addListener: sinon.spy((listener, ...args) => {
fakeChromeListeners.push(listener);
}),
hasListener: sinon.spy(),
removeListener: sinon.spy(),
},
},
};
return setupTestDOMWindow(fakeChrome).then(window => {
const firstMessageListener = sinon.spy();
const secondMessageListener = sinon.spy();
window.browser.runtime.onMessage.addListener(firstMessageListener);
window.browser.runtime.onMessage.addListener(secondMessageListener);
equal(fakeChromeListeners.length, 2, "Got two wrapped listeners");
fakeChromeListeners[0]("call first wrapper");
ok(firstMessageListener.calledOnce);
equal(firstMessageListener.firstCall.args[0], "call first wrapper");
fakeChromeListeners[1]("call second wrapper");
ok(secondMessageListener.calledOnce);
equal(secondMessageListener.firstCall.args[0], "call second wrapper");
});
});
it("sends the returned value as a message response", () => {
const fakeChrome = {
runtime: {
lastError: null,
onMessage: {
addListener: sinon.spy(),
},
},
};
// Plain value returned.
const messageListener = sinon.stub();
const firstResponse = "fake reply";
// Resolved Promise returned.
const secondResponse = Promise.resolve("fake reply 2");
// Rejected Promise returned.
const thirdResponse = Promise.reject("fake error 3");
const sendResponseSpy = sinon.spy();
messageListener
.onFirstCall().returns(firstResponse)
.onSecondCall().returns(secondResponse)
.onThirdCall().returns(thirdResponse);
let wrappedListener;
return setupTestDOMWindow(fakeChrome).then(window => {
window.browser.runtime.onMessage.addListener(messageListener);
ok(fakeChrome.runtime.onMessage.addListener.calledOnce);
wrappedListener = fakeChrome.runtime.onMessage.addListener.firstCall.args[0];
wrappedListener("fake message", {name: "fake sender"}, sendResponseSpy);
ok(messageListener.calledOnce, "The unwrapped message listener has been called");
deepEqual(messageListener.firstCall.args,
["fake message", {name: "fake sender"}],
"The unwrapped message listener has received the expected parameters");
ok(sendResponseSpy.calledOnce, "The sendResponse function has been called");
equal(sendResponseSpy.firstCall.args[0], "fake reply",
"sendResponse callback has been called with the expected parameters");
wrappedListener("fake message2", {name: "fake sender2"}, sendResponseSpy);
// Wait the second response promise to be resolved.
return secondResponse;
}).then(() => {
ok(messageListener.calledTwice,
"The unwrapped message listener has been called");
deepEqual(messageListener.secondCall.args,
["fake message2", {name: "fake sender2"}],
"The unwrapped listener has received the expected parameters");
ok(sendResponseSpy.calledTwice, "The sendResponse function has been called");
equal(sendResponseSpy.secondCall.args[0], "fake reply 2",
"sendResponse callback has been called with the expected parameters");
}).then(() => {
wrappedListener("fake message3", {name: "fake sender3"}, sendResponseSpy);
// Wait the third response promise to be rejected.
return thirdResponse.catch(err => {
equal(messageListener.callCount, 3,
"The unwrapped message listener has been called");
deepEqual(messageListener.thirdCall.args,
["fake message3", {name: "fake sender3"}],
"The unwrapped listener has received the expected parameters");
equal(sendResponseSpy.callCount, 3,
"The sendResponse function has been called");
equal(sendResponseSpy.thirdCall.args[0], err,
"sendResponse callback has been called with the expected parameters");
});
});
});
});
});