Merge pull request #1116 from mozilla/testing-6.0.0

Version 6.0.0
This commit is contained in:
luke crouch 2018-02-22 14:44:12 -06:00 committed by GitHub
commit ee6a54ffa2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 567 additions and 13 deletions

3
.gitignore vendored
View file

@ -9,3 +9,6 @@ README.html
addon.env
src/web-ext-artifacts/*
# JetBrains IDE files
.idea

View file

@ -1,6 +1,6 @@
language: node_js
node_js:
- "6.1"
- "lts/*"
notifications:
irc:

View file

@ -49,7 +49,7 @@ Finally, we also publish the release to GitHub for those followers.
### Links
Facebook & Twitter icons CC-Attrib http://fairheadcreative.com.
Facebook & Twitter icons CC-Attrib https://fairheadcreative.com.
- [Licence](./LICENSE.txt)
- [Contributing](./CONTRIBUTING.md)

View file

@ -2,7 +2,7 @@
"name": "testpilot-containers",
"title": "Multi-Account Containers",
"description": "Containers helps you keep all the parts of your online life contained in different tabs. Custom labels and color-coded tabs help keep different activities — like online shopping, travel planning, or checking work email — separate.",
"version": "5.0.1",
"version": "6.0.0",
"author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston",
"bugs": {
"url": "https://github.com/mozilla/testpilot-containers/issues"
@ -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"
}
}

View file

@ -143,8 +143,35 @@ const assignManager = {
|| this.storageArea.isExempted(options.url, tab.id)) {
return {};
}
const removeTab = backgroundLogic.NEW_TAB_PAGES.has(tab.url)
|| (messageHandler.lastCreatedTab
&& messageHandler.lastCreatedTab.id === tab.id);
const openTabId = removeTab ? tab.openerTabId : tab.id;
this.reloadPageInContainer(options.url, userContextId, siteSettings.userContextId, tab.index + 1, tab.active, siteSettings.neverAsk);
// we decided to cancel the request at this point, register it as canceled request as early as possible
if (!this.canceledRequests[options.requestId]) {
this.canceledRequests[options.requestId] = true;
// register a cleanup for handled requestIds
// all relevant requests that come in that timeframe with the same requestId will be canceled
setTimeout(() => {
delete this.canceledRequests[options.requestId];
}, 2000);
} else {
// if we see a request for the same requestId at this point then this is a redirect that we have to cancel to prevent opening two tabs
return {
cancel: true
};
}
this.reloadPageInContainer(
options.url,
userContextId,
siteSettings.userContextId,
tab.index + 1,
tab.active,
siteSettings.neverAsk,
openTabId
);
this.calculateContextMenu(tab);
/* Removal of existing tabs:
@ -158,9 +185,7 @@ const assignManager = {
however they don't run on about:blank so this would likely be just as hacky.
We capture the time the tab was created and close if it was within the timeout to try to capture pages which haven't had user interaction or history.
*/
if (backgroundLogic.NEW_TAB_PAGES.has(tab.url)
|| (messageHandler.lastCreatedTab
&& messageHandler.lastCreatedTab.id === tab.id)) {
if (removeTab) {
browser.tabs.remove(tab.id);
}
return {
@ -174,6 +199,7 @@ const assignManager = {
});
// Before a request is handled by the browser we decide if we should route through a different container
this.canceledRequests = {};
browser.webRequest.onBeforeRequest.addListener((options) => {
return this.onBeforeRequest(options);
},{urls: ["<all_urls>"], types: ["main_frame"]}, ["blocking"]);
@ -350,13 +376,13 @@ const assignManager = {
});
},
reloadPageInContainer(url, currentUserContextId, userContextId, index, active, neverAsk = false) {
reloadPageInContainer(url, currentUserContextId, userContextId, index, active, neverAsk = false, openerTabId = null) {
const cookieStoreId = backgroundLogic.cookieStoreId(userContextId);
const loadPage = browser.extension.getURL("confirm-page.html");
// False represents assignment is not permitted
// If the user has explicitly checked "Never Ask Again" on the warning page we will send them straight there
if (neverAsk) {
browser.tabs.create({url, cookieStoreId, index, active});
browser.tabs.create({url, cookieStoreId, index, active, openerTabId});
} else {
let confirmUrl = `${loadPage}?url=${this.encodeURLProperty(url)}&cookieStoreId=${cookieStoreId}`;
let currentCookieStoreId;
@ -367,6 +393,7 @@ const assignManager = {
browser.tabs.create({
url: confirmUrl,
cookieStoreId: currentCookieStoreId,
openerTabId,
index,
active
}).then(() => {

View file

@ -72,6 +72,42 @@ const messageHandler = {
return response;
});
// Handles external messages from webextensions
const externalExtensionAllowed = {};
browser.runtime.onMessageExternal.addListener(async (message, sender) => {
if (!externalExtensionAllowed[sender.id]) {
const extensionInfo = await browser.management.get(sender.id);
if (!extensionInfo.permissions.includes("contextualIdentities")) {
throw new Error("Missing contextualIdentities permission");
}
externalExtensionAllowed[sender.id] = true;
}
let response;
switch (message.method) {
case "getAssignment":
if (typeof message.url === "undefined") {
throw new Error("Missing message.url");
}
response = assignManager.storageArea.get(message.url);
break;
default:
throw new Error("Unknown message.method");
}
return response;
});
// Delete externalExtensionAllowed if add-on installs/updates; permissions might change
browser.management.onInstalled.addListener(extensionInfo => {
if (externalExtensionAllowed[extensionInfo.id]) {
delete externalExtensionAllowed[extensionInfo.id];
}
});
// Delete externalExtensionAllowed if add-on uninstalls; not needed anymore
browser.management.onUninstalled.addListener(extensionInfo => {
if (externalExtensionAllowed[extensionInfo.id]) {
delete externalExtensionAllowed[extensionInfo.id];
}
});
if (browser.contextualIdentities.onRemoved) {
browser.contextualIdentities.onRemoved.addListener(({contextualIdentity}) => {
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(contextualIdentity.cookieStoreId);

View file

@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Firefox Multi-Account Containers",
"version": "5.0.1",
"version": "6.0.0",
"description": "Multi-Account Containers helps you keep all the parts of your online life contained in different tabs. Custom labels and color-coded tabs help keep different activities — like online shopping, travel planning, or checking work email — separate.",
"icons": {
@ -26,6 +26,7 @@
"contextualIdentities",
"history",
"idle",
"management",
"storage",
"tabs",
"webRequestBlocking",

View file

@ -2,7 +2,7 @@
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Multi-Account Containers</title>
<link rel="stylesheet" href="/css/popup.css">
<link rel="stylesheet" href="css/popup.css">
</head>
<body>

16
test/.eslintrc.js Normal file
View file

@ -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
}
}

134
test/browser.mock.js Normal file
View file

@ -0,0 +1,134 @@
module.exports = () => {
const _storage = {};
// could maybe be replaced by https://github.com/acvetkov/sinon-chrome
const browserMock = {
_storage,
runtime: {
onMessage: {
addListener: sinon.stub(),
},
onMessageExternal: {
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()
},
management: {
get: sinon.stub(),
onInstalled: {
addListener: sinon.stub()
},
onUninstalled: {
addListener: 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;
};

View file

@ -0,0 +1,75 @@
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,
openerTabId: null,
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;
});
});
});
});
});

View file

@ -0,0 +1,67 @@
describe("External Webextensions", () => {
const activeTab = {
id: 1,
cookieStoreId: "firefox-container-1",
url: "http://example.com",
index: 0
};
beforeEach(async () => {
await helper.browser.initializeWithTab(activeTab);
await helper.popup.clickElementById("container-page-assigned");
});
describe("with contextualIdentities permissions", () => {
it("should be able to get assignments", async () => {
background.browser.management.get.resolves({
permissions: ["contextualIdentities"]
});
const message = {
method: "getAssignment",
url: "http://example.com"
};
const sender = {
id: "external-webextension"
};
// currently not possible to get the return value of yielding with sinon
// so we expect that if no error is thrown and the storage was called, everything is ok
// maybe i get around to provide a PR https://github.com/sinonjs/sinon/issues/903
//
// the alternative would be to expose the actual messageHandler and call it directly
// but personally i think that goes against the black-box-ish nature of these feature tests
const rejectionStub = sinon.stub();
process.on("unhandledRejection", rejectionStub);
background.browser.runtime.onMessageExternal.addListener.yield(message, sender);
await nextTick();
process.removeListener("unhandledRejection", rejectionStub);
rejectionStub.should.not.have.been.called;
background.browser.storage.local.get.should.have.been.called;
});
});
describe("without contextualIdentities permissions", () => {
it("should throw an error", async () => {
background.browser.management.get.resolves({
permissions: []
});
const message = {
method: "getAssignment",
url: "http://example.com"
};
const sender = {
id: "external-webextension"
};
const rejectionStub = sinon.spy();
process.on("unhandledRejection", rejectionStub);
background.browser.runtime.onMessageExternal.addListener.yield(message, sender);
await nextTick();
process.removeListener("unhandledRejection", rejectionStub);
rejectionStub.should.have.been.calledWith(sinon.match({
message: "Missing contextualIdentities permission"
}));
});
});
});

45
test/helper.js Normal file
View file

@ -0,0 +1,45 @@
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, options = {isAsync: true}) {
background.browser.tabs.get.resolves(tab);
background.browser.webRequest.onBeforeRequest.addListener.yield({
frameId: 0,
tabId: tab.id,
url: tab.url,
requestId: options.requestId
});
background.browser.tabs.onCreated.addListener.yield(tab);
if (!options.isAsync) {
return;
}
await nextTick();
}
},
popup: {
async clickElementById(id) {
const clickEvent = popup.document.createEvent("HTMLEvents");
clickEvent.initEvent("click");
popup.document.getElementById(id).dispatchEvent(clickEvent);
await nextTick();
}
},
};

43
test/issues/940.test.js Normal file
View file

@ -0,0 +1,43 @@
describe("#940", () => {
describe("when other onBeforeRequestHandlers are faster and redirect with the same requestId", () => {
it("should not open two confirm pages", async () => {
// init
const activeTab = {
id: 1,
cookieStoreId: "firefox-container-1",
url: "http://example.com",
index: 0
};
await helper.browser.initializeWithTab(activeTab);
// assign the activeTab.url
await helper.popup.clickElementById("container-page-assigned");
// start request and don't await the requests at all
// so the second request below is actually comparable to an actual redirect that also fires immediately
const newTab = {
id: 2,
cookieStoreId: "firefox-default",
url: activeTab.url,
index: 1,
active: true
};
helper.browser.openNewTab(newTab, {
requestId: 1,
isAsync: false
});
// other addon sees the same request
// and redirects to the https version of activeTab.url
// since it's a redirect the request has the same requestId
background.browser.webRequest.onBeforeRequest.addListener.yield({
frameId: 0,
tabId: newTab.id,
url: "https://example.com",
requestId: 1
});
await nextTick();
background.browser.tabs.create.should.have.been.calledOnce;
});
});
});

101
test/setup.js Normal file
View file

@ -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;
}
});