From c08b8af9822af3f26c679243284cd1cea01948d5 Mon Sep 17 00:00:00 2001 From: Luca Greco Date: Mon, 10 Oct 2016 02:48:35 +0200 Subject: [PATCH] test: introduced a test suite for unit testing. --- .gitignore | 4 ++ .travis.yml | 18 ++++++ Gruntfile.js | 8 +++ package.json | 19 +++++- test/.eslintrc | 11 ++++ test/setup.js | 75 ++++++++++++++++++++++ test/test-async-functions.js | 31 +++++++++ test/test-browser-global.js | 14 +++++ test/test-proxied-properties.js | 48 ++++++++++++++ test/test-runtime-onMessage.js | 108 ++++++++++++++++++++++++++++++++ 10 files changed, 335 insertions(+), 1 deletion(-) create mode 100644 .travis.yml create mode 100644 test/.eslintrc create mode 100644 test/setup.js create mode 100644 test/test-async-functions.js create mode 100644 test/test-browser-global.js create mode 100644 test/test-proxied-properties.js create mode 100644 test/test-runtime-onMessage.js diff --git a/.gitignore b/.gitignore index 3659f1a..cb52d64 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ node_modules/* dist/* + +## code coverage +coverage/ +.nyc_output/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..8fe055f --- /dev/null +++ b/.travis.yml @@ -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#amo-bots + on_success: change + on_failure: always diff --git a/Gruntfile.js b/Gruntfile.js index 2b73734..4a2d074 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -12,8 +12,15 @@ module.exports = function(grunt) { grunt.initConfig({ pkg: grunt.file.readJSON("package.json"), + coveralls: { + all: { + src: "coverage/lcov.info", + }, + }, + eslint: { src: ["browser-polyfill.in.js", "Gruntfile.js"], + tests: ["tests/**/*.js"], }, replace: { @@ -81,6 +88,7 @@ module.exports = function(grunt) { grunt.loadNpmTasks("gruntify-eslint"); grunt.loadNpmTasks("grunt-replace"); + grunt.loadNpmTasks("grunt-coveralls"); require("google-closure-compiler").grunt(grunt); grunt.registerTask("default", ["eslint", "replace", "closure-compiler"]); diff --git a/package.json b/package.json index bf58da6..6809d80 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,26 @@ }, "homepage": "https://github.com/mozilla/webextension-polyfill", "devDependencies": { + "chai": "^3.5.0", "google-closure-compiler": "^20160911.0.0", "grunt": "^1.0.1", + "grunt-coveralls": "^1.0.1", "grunt-replace": "*", - "gruntify-eslint": "*" + "gruntify-eslint": "*", + "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" } } diff --git a/test/.eslintrc b/test/.eslintrc new file mode 100644 index 0000000..e275d67 --- /dev/null +++ b/test/.eslintrc @@ -0,0 +1,11 @@ +{ + "extends": "../.eslintrc", + "env": { + "mocha": true, + "node": true, + "browser": true, + "webextensions": true + }, + "globals": { + } +} \ No newline at end of file diff --git a/test/setup.js b/test/setup.js new file mode 100644 index 0000000..8f4b4c8 --- /dev/null +++ b/test/setup.js @@ -0,0 +1,75 @@ +"use strict"; + +const {createInstrumenter} = require("istanbul-lib-instrument"); +const fs = require("fs"); +const {jsdom, createVirtualConsole} = require("jsdom"); + +var virtualConsole = createVirtualConsole().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"; + +function setupTestDOMWindow(chromeObject) { + return new Promise((resolve, reject) => { + let window; + + // If a jsdom window has already been created, reuse it so that + // we can retrieve the final code coverage data, which has been + // collected in the jsdom window (where the instrumented browser-polyfill + // is running). + if (global.window) { + window = global.window; + window.browser = undefined; + } else { + window = jsdom({virtualConsole}).defaultView; + global.window = window; + } + + // Inject the fake chrome object used as a fixture for the particular + // browser-polyfill test scenario. + window.chrome = chromeObject; + + 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; + } + + // Prepare to listen for script loading errors (which results in a rejection), + // and to detect when the browser-polyfill has been executed (which resolves + // to the jsdom window where the loading has been completed). + window.__browserPolyfillLoaded__ = {resolve, reject}; + window.addEventListener("error", (evt) => reject(evt)); + const loadedScriptEl = window.document.createElement("script"); + loadedScriptEl.textContent = ` + window.removeEventListener("error", window.__browserPolyfillLoaded__.reject); + window.__browserPolyfillLoaded__.resolve(window); + `; + + window.document.body.appendChild(scriptEl); + window.document.body.appendChild(loadedScriptEl); + }); +} + +// 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 (global.window && process.env.COVERAGE == "y") { + global.__coverage__ = global.window.__coverage__; + } +}); + +module.exports = { + BROWSER_POLYFILL_PATH, + setupTestDOMWindow, +}; diff --git a/test/test-async-functions.js b/test/test-async-functions.js new file mode 100644 index 0000000..cf47816 --- /dev/null +++ b/test/test-async-functions.js @@ -0,0 +1,31 @@ +"use strict"; + +const {assert} = require("chai"); +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: undefined}, + }; + return setupTestDOMWindow(fakeChrome).then(window => { + fakeChrome.alarms.clear + .onFirstCall().callsArgWith(1, "res1") + .onSecondCall().callsArgWith(1, "res1", "res2", "res3"); + + return Promise.all([ + window.browser.alarms.clear("test1"), + window.browser.alarms.clear("test2"), + ]); + }).then(results => { + assert.equal(results[0], "res1", "The first call resolved to a single value"); + assert.deepEqual(results[1], ["res1", "res2", "res3"], + "The second call resolved to an array of the expected values"); + }); + }); + }); +}); diff --git a/test/test-browser-global.js b/test/test-browser-global.js new file mode 100644 index 0000000..97ca4e3 --- /dev/null +++ b/test/test-browser-global.js @@ -0,0 +1,14 @@ +"use strict"; + +const {assert} = require("chai"); + +const {setupTestDOMWindow} = require("./setup"); + +describe("browser-polyfill", () => { + it("automatically wrapps chrome into a browser object", () => { + const fakeChrome = {}; + return setupTestDOMWindow(fakeChrome).then(window => { + assert.equal(typeof window.browser, "object", "Got the window.browser object"); + }); + }); +}); diff --git a/test/test-proxied-properties.js b/test/test-proxied-properties.js new file mode 100644 index 0000000..98f0f6a --- /dev/null +++ b/test/test-proxied-properties.js @@ -0,0 +1,48 @@ +"use strict"; + +const {assert} = require("chai"); +const sinon = require("sinon"); + +const {setupTestDOMWindow} = require("./setup"); + +describe("browser-polyfill", () => { + describe("proxies non-wrapped functions", () => { + it("should proxy getters and setters", () => { + const fakeChrome = { + runtime: {myprop: "previous-value"}, + nowrapns: {nowrapkey: "previous-value"}, + }; + return setupTestDOMWindow(fakeChrome).then(window => { + const setResult = window.browser.runtime.myprop = "new-value"; + const setResult2 = window.browser.nowrapns.nowrapkey = "new-value"; + + assert.equal(setResult, "new-value", + "Got the expected result from setting a wrapped property name"); + assert.equal(setResult2, "new-value", + "Got the expected result from setting a wrapped property name"); + }); + }); + + it("delete proxy getter/setter that are not wrapped", () => { + const fakeChrome = {}; + return setupTestDOMWindow(fakeChrome).then(window => { + window.browser.newns = {newkey: "test-value"}; + assert.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"}; + assert.equal(window.browser.newns.newkey2, "new-value", + "The new non-wrapped getter is cached"); + assert.deepEqual(setRes, {newkey2: "new-value"}, + "Got the expected result from setting a new wrapped property name"); + assert.deepEqual(window.browser.newns, window.chrome.newns, + "chrome.newns and browser.newns are the same"); + + + delete window.browser.newns.newkey2; + assert.equal(window.browser.newns.newkey2, undefined, + "Got the expected result from setting a wrapped property name"); + }); + }); + }); +}); diff --git a/test/test-runtime-onMessage.js b/test/test-runtime-onMessage.js new file mode 100644 index 0000000..08f9a03 --- /dev/null +++ b/test/test-runtime-onMessage.js @@ -0,0 +1,108 @@ +"use strict"; + +const {assert} = require("chai"); +const sinon = require("sinon"); + +const {setupTestDOMWindow} = require("./setup"); + +describe("browser-polyfill", () => { + describe("wrapped runtime.onMessage listener", () => { + it("keep track of the listeners added", () => { + const messageListener = sinon.spy(); + + const fakeChrome = { + runtime: { + lastError: undefined, + onMessage: { + addListener: sinon.spy(), + hasListener: sinon.stub(), + removeListener: sinon.spy(), + }, + }, + }; + + return setupTestDOMWindow(fakeChrome).then(window => { + fakeChrome.runtime.onMessage.hasListener + .onFirstCall().returns(false) + .onSecondCall().returns(true); + + assert.equal(window.browser.runtime.onMessage.hasListener(messageListener), + false, "Got hasListener==false before the listener has been added"); + window.browser.runtime.onMessage.addListener(messageListener); + assert.equal(window.browser.runtime.onMessage.hasListener(messageListener), + true, "Got hasListener=true once the listener has been added"); + window.browser.runtime.onMessage.addListener(messageListener); + + assert.ok(fakeChrome.runtime.onMessage.addListener.calledTwice, + "addListener has been called twice"); + assert.equal(fakeChrome.runtime.onMessage.addListener.secondCall.args[0], + fakeChrome.runtime.onMessage.addListener.firstCall.args[0], + "both the addListener calls received the same wrapped listener"); + + const wrappedListener = fakeChrome.runtime.onMessage.addListener.firstCall.args[0]; + wrappedListener("msg", {name: "sender"}, function sendResponse() {}); + assert.ok(messageListener.calledOnce, "The listener has been called once"); + + window.browser.runtime.onMessage.removeListener(messageListener); + assert.ok(fakeChrome.runtime.onMessage.removeListener.calledOnce, + "removeListener has been called once"); + assert.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"); + }); + }); + + it("sends the returned value as a message response", () => { + const fakeChrome = { + runtime: { + lastError: undefined, + onMessage: { + addListener: sinon.spy(), + }, + }, + }; + + const messageListener = sinon.stub(); + const firstResponse = "fake reply"; + const secondResponse = Promise.resolve("fake reply2"); + const sendResponseSpy = sinon.spy(); + + messageListener.onFirstCall().returns(firstResponse) + .onSecondCall().returns(secondResponse); + + return setupTestDOMWindow(fakeChrome).then(window => { + window.browser.runtime.onMessage.addListener(messageListener); + + assert.ok(fakeChrome.runtime.onMessage.addListener.calledOnce); + + const wrappedListener = fakeChrome.runtime.onMessage.addListener.firstCall.args[0]; + + wrappedListener("fake message", {name: "fake sender"}, sendResponseSpy); + + assert.ok(messageListener.calledOnce, "The unwrapped message listener has been called"); + assert.deepEqual(messageListener.firstCall.args, + ["fake message", {name: "fake sender"}], + "The unwrapped message listener has received the expected parameters"); + + assert.ok(sendResponseSpy.calledOnce, "The sendResponse function has been called"); + assert.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(() => { + assert.ok(messageListener.calledTwice, + "The unwrapped message listener has been called"); + assert.deepEqual(messageListener.secondCall.args, + ["fake message2", {name: "fake sender2"}], + "The unwrapped message listener has received the expected parameters"); + + assert.ok(sendResponseSpy.calledTwice, "The sendResponse function has been called"); + assert.equal(sendResponseSpy.secondCall.args[0], "fake reply2", + "sendResponse callback has been called with the expected parameters"); + }); + }); + }); +});