diff --git a/.travis.yml b/.travis.yml index 4614306..2642444 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: - - "6.1" + - "lts/*" notifications: irc: diff --git a/package.json b/package.json index ca8eb7d..39ecde3 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,18 @@ "dependencies": {}, "devDependencies": { "addons-linter": "^0.15.14", + "chai": "^4.1.2", "deploy-txp": "^1.0.7", "eslint": "^3.17.1", "eslint-plugin-no-unsanitized": "^2.0.0", "eslint-plugin-promise": "^3.4.0", "htmllint-cli": "^0.0.5", + "jsdom": "^11.6.2", "json": "^9.0.6", + "mocha": "^5.0.0", "npm-run-all": "^4.0.0", + "sinon": "^4.2.2", + "sinon-chai": "^2.14.0", "stylelint": "^7.9.0", "stylelint-config-standard": "^16.0.0", "stylelint-order": "^0.3.0", @@ -38,6 +43,7 @@ "lint:html": "htmllint *.html", "lint:js": "eslint .", "package": "rm -rf src/web-ext-artifacts && npm run build && mv src/web-ext-artifacts/firefox_multi-account_containers-*.zip addon.xpi", - "test": "npm run lint" + "test": "npm run lint && mocha ./test/setup.js test/**/*.test.js", + "test-watch": "mocha ./test/setup.js test/**/*.test.js --watch" } } diff --git a/src/popup.html b/src/popup.html index 8cde498..a28dd32 100644 --- a/src/popup.html +++ b/src/popup.html @@ -2,7 +2,7 @@ Multi-Account Containers - + diff --git a/test/.eslintrc.js b/test/.eslintrc.js new file mode 100644 index 0000000..0b29fac --- /dev/null +++ b/test/.eslintrc.js @@ -0,0 +1,16 @@ +module.exports = { + env: { + "node": true, + "mocha": true + }, + globals: { + "sinon": false, + "expect": false, + "nextTick": false, + "buildBackgroundDom": false, + "background": false, + "buildPopupDom": false, + "popup": false, + "helper": false + } +} diff --git a/test/browser.mock.js b/test/browser.mock.js new file mode 100644 index 0000000..1c71598 --- /dev/null +++ b/test/browser.mock.js @@ -0,0 +1,122 @@ +module.exports = () => { + const _storage = {}; + + // could maybe be replaced by https://github.com/acvetkov/sinon-chrome + const browserMock = { + _storage, + runtime: { + onMessage: { + addListener: sinon.stub(), + }, + sendMessage: sinon.stub().resolves(), + }, + webRequest: { + onBeforeRequest: { + addListener: sinon.stub() + }, + onCompleted: { + addListener: sinon.stub() + } + }, + windows: { + getCurrent: sinon.stub().resolves({}), + onFocusChanged: { + addListener: sinon.stub(), + } + }, + tabs: { + onActivated: { + addListener: sinon.stub() + }, + onCreated: { + addListener: sinon.stub() + }, + onUpdated: { + addListener: sinon.stub() + }, + sendMessage: sinon.stub(), + query: sinon.stub().resolves([{}]), + get: sinon.stub(), + create: sinon.stub().resolves({}), + remove: sinon.stub().resolves() + }, + history: { + deleteUrl: sinon.stub() + }, + storage: { + local: { + get: sinon.stub(), + set: sinon.stub() + } + }, + contextualIdentities: { + create: sinon.stub(), + get: sinon.stub(), + query: sinon.stub().resolves([]) + }, + contextMenus: { + create: sinon.stub(), + remove: sinon.stub(), + onClicked: { + addListener: sinon.stub() + } + }, + browserAction: { + setBadgeBackgroundColor: sinon.stub(), + setBadgeText: sinon.stub() + }, + extension: { + getURL: sinon.stub().returns("moz-extension://multi-account-containers/confirm-page.html") + } + }; + + // inmemory local storage + browserMock.storage.local = { + get: sinon.spy(async key => { + if (!key) { + return _storage; + } + let result = {}; + if (Array.isArray(key)) { + key.map(akey => { + if (typeof _storage[akey] !== "undefined") { + result[akey] = _storage[akey]; + } + }); + } else if (typeof key === "object") { + // TODO support nested objects + Object.keys(key).map(oKey => { + if (typeof _storage[oKey] !== "undefined") { + result[oKey] = _storage[oKey]; + } else { + result[oKey] = key[oKey]; + } + }); + } else { + result = _storage[key]; + } + return result; + }), + set: sinon.spy(async (key, value) => { + if (typeof key === "object") { + // TODO support nested objects + Object.keys(key).map(oKey => { + _storage[oKey] = key[oKey]; + }); + } else { + _storage[key] = value; + } + }), + remove: sinon.spy(async (key) => { + if (Array.isArray(key)) { + key.map(aKey => { + delete _storage[aKey]; + }); + } else { + delete _storage[key]; + } + }), + }; + + return browserMock; +}; diff --git a/test/features/assignment.test.js b/test/features/assignment.test.js new file mode 100644 index 0000000..f336962 --- /dev/null +++ b/test/features/assignment.test.js @@ -0,0 +1,74 @@ +describe("Assignment Feature", () => { + const activeTab = { + id: 1, + cookieStoreId: "firefox-container-1", + url: "http://example.com", + index: 0 + }; + beforeEach(async () => { + await helper.browser.initializeWithTab(activeTab); + }); + + describe("click the 'Always open in' checkbox in the popup", () => { + beforeEach(async () => { + // popup click to set assignment for activeTab.url + await helper.popup.clickElementById("container-page-assigned"); + }); + + describe("open new Tab with the assigned URL in the default container", () => { + const newTab = { + id: 2, + cookieStoreId: "firefox-default", + url: activeTab.url, + index: 1, + active: true + }; + beforeEach(async () => { + // new Tab opening activeTab.url in default container + await helper.browser.openNewTab(newTab); + }); + + it("should open the confirm page", async () => { + // should have created a new tab with the confirm page + background.browser.tabs.create.should.have.been.calledWith({ + url: "moz-extension://multi-account-containers/confirm-page.html?" + + `url=${encodeURIComponent(activeTab.url)}` + + `&cookieStoreId=${activeTab.cookieStoreId}`, + cookieStoreId: undefined, + index: 2, + active: true + }); + }); + + it("should remove the new Tab that got opened in the default container", () => { + background.browser.tabs.remove.should.have.been.calledWith(newTab.id); + }); + }); + + describe("click the 'Always open in' checkbox in the popup again", () => { + beforeEach(async () => { + // popup click to remove assignment for activeTab.url + await helper.popup.clickElementById("container-page-assigned"); + }); + + describe("open new Tab with the no longer assigned URL in the default container", () => { + const newTab = { + id: 3, + cookieStoreId: "firefox-default", + url: activeTab.url, + index: 3, + active: true + }; + beforeEach(async () => { + // new Tab opening activeTab.url in default container + await helper.browser.openNewTab(newTab); + }); + + it("should not open the confirm page", async () => { + // should not have created a new tab + background.browser.tabs.create.should.not.have.been.called; + }); + }); + }); + }); +}); diff --git a/test/helper.js b/test/helper.js new file mode 100644 index 0000000..a19750b --- /dev/null +++ b/test/helper.js @@ -0,0 +1,41 @@ +module.exports = { + browser: { + async initializeWithTab(tab) { + await buildBackgroundDom({ + beforeParse(window) { + window.browser.tabs.get.resolves(tab); + window.browser.tabs.query.resolves([tab]); + window.browser.contextualIdentities.get.resolves({ + cookieStoreId: tab.cookieStoreId + }); + } + }); + await buildPopupDom({ + beforeParse(window) { + window.browser.tabs.get.resolves(tab); + window.browser.tabs.query.resolves([tab]); + } + }); + }, + + async openNewTab(tab) { + background.browser.tabs.get.resolves(tab); + background.browser.webRequest.onBeforeRequest.addListener.yield({ + frameId: 0, + tabId: tab.id, + url: tab.url + }); + background.browser.tabs.onCreated.addListener.yield(tab); + await nextTick(); + } + }, + + popup: { + async clickElementById(id) { + const clickEvent = popup.document.createEvent("HTMLEvents"); + clickEvent.initEvent("click"); + popup.document.getElementById(id).dispatchEvent(clickEvent); + await nextTick(); + } + }, +}; diff --git a/test/setup.js b/test/setup.js new file mode 100644 index 0000000..9672bfc --- /dev/null +++ b/test/setup.js @@ -0,0 +1,101 @@ +if (!process.listenerCount("unhandledRejection")) { + // eslint-disable-next-line no-console + process.on("unhandledRejection", r => console.log(r)); +} +const jsdom = require("jsdom"); +const path = require("path"); +const chai = require("chai"); +const sinonChai = require("sinon-chai"); +global.sinon = require("sinon"); +global.expect = chai.expect; +chai.should(); +chai.use(sinonChai); +global.nextTick = () => { + return new Promise(resolve => { + setTimeout(() => { + process.nextTick(resolve); + }); + }); +}; + +global.helper = require("./helper"); +const browserMock = require("./browser.mock"); +const srcBasePath = path.resolve(path.join(__dirname, "..", "src")); +const srcJsBackgroundPath = path.join(srcBasePath, "js", "background"); +global.buildBackgroundDom = async (options = {}) => { + const dom = await jsdom.JSDOM.fromFile(path.join(srcJsBackgroundPath, "index.html"), { + runScripts: "dangerously", + resources: "usable", + virtualConsole: (new jsdom.VirtualConsole).sendTo(console), + beforeParse(window) { + window.browser = browserMock(); + window.fetch = sinon.stub().resolves({ + json: sinon.stub().resolves({}) + }); + + if (options.beforeParse) { + options.beforeParse(window); + } + } + }); + await new Promise(resolve => { + dom.window.document.addEventListener("DOMContentLoaded", resolve); + }); + await nextTick(); + + global.background = { + dom, + browser: dom.window.browser + }; +}; + +global.buildPopupDom = async (options = {}) => { + const dom = await jsdom.JSDOM.fromFile(path.join(srcBasePath, "popup.html"), { + runScripts: "dangerously", + resources: "usable", + virtualConsole: (new jsdom.VirtualConsole).sendTo(console), + beforeParse(window) { + window.browser = browserMock(); + window.browser.storage.local.set("browserActionBadgesClicked", []); + window.browser.storage.local.set("onboarding-stage", 5); + window.browser.storage.local.set("achievements", []); + window.browser.storage.local.set.resetHistory(); + window.fetch = sinon.stub().resolves({ + json: sinon.stub().resolves({}) + }); + + if (options.beforeParse) { + options.beforeParse(window); + } + } + }); + await new Promise(resolve => { + dom.window.document.addEventListener("DOMContentLoaded", resolve); + }); + await nextTick(); + dom.window.browser.runtime.sendMessage.resetHistory(); + + if (global.background) { + dom.window.browser.runtime.sendMessage = sinon.spy(function() { + global.background.browser.runtime.onMessage.addListener.yield(...arguments); + }); + } + + global.popup = { + dom, + document: dom.window.document, + browser: dom.window.browser + }; +}; + +global.afterEach(() => { + if (global.background) { + global.background.dom.window.close(); + delete global.background.dom; + } + + if (global.popup) { + global.popup.dom.window.close(); + delete global.popup.dom; + } +});