Compare commits

...

38 Commits

Author SHA1 Message Date
luke crouch ee6a54ffa2 Merge pull request #1116 from mozilla/testing-6.0.0
Version 6.0.0
2018-02-22 14:44:12 -06:00
Jonathan Kingston 601056406a Merge pull request #1118 from stoically/tests-6.0.0
Add and fix tests for 6.0.0
2018-02-22 16:57:08 +00:00
stoically fd72ce12b4 Fix assignment test to check for the new openerTabId property 2018-02-10 00:51:13 +01:00
stoically 6e45532f58 Add tests for external webextensions feature 2018-02-10 00:50:29 +01:00
Jonathan Kingston 61da6b5e99 Merge pull request #1117 from mozilla/jonathanKingston-patch-1
Adding an s://
2018-02-09 18:37:45 +00:00
Jonathan Kingston e0156388e8 Adding an s:// 2018-02-09 18:37:29 +00:00
Jonathan Kingston 16f1d47bf2 Merge pull request #1115 from jonathanKingston/6.0.0
Bumping version to 6.0.0 to account for latest fixes
2018-02-09 18:34:54 +00:00
Jonathan Kingston ee647344a1 Bumping version to 6.0.0 to account for latest fixes 2018-02-09 18:33:54 +00:00
Jonathan Kingston 40426ca936 Merge pull request #1107 from stoically/tests
Adding assignment feature tests
2018-02-09 18:07:20 +00:00
Jonathan Kingston d1e9c2d1e3 Merge pull request #1066 from LoveIsGrief/1065_-_Feature_Request]_Pass_openerTabId_when_creating_tabs_in_order_to_know_the_parent
Pass opener tab id when creating tabs in order to know the parent
2018-02-09 18:06:56 +00:00
Jonathan Kingston 3bd33cda99 Merge branch 'testing-6.0.0' into 1065_-_Feature_Request]_Pass_openerTabId_when_creating_tabs_in_order_to_know_the_parent 2018-02-09 18:04:58 +00:00
stoically 609f62ac7a Allow webextensions with contextualIdentities permission to get assignment
Closes #1095
2018-02-09 18:00:26 +00:00
stoically ce84665e3a Add management permission to manifest.json
Part of #1095
2018-02-09 18:00:26 +00:00
stoically 7dceaf6679 Cancel requests with the same requestId
Prevents potential redirects from opening two tabs
Closes #1114
2018-02-09 17:59:23 +00:00
stoically b6bcd99dc8 Add test for issue #940 2018-02-09 12:02:41 +01:00
stoically 9bc9509316 Added assignment feature test
Part of #1107
2018-02-02 19:05:40 +01:00
luke crouch 22ec01d565 Merge pull request #1106 from timendum/master
Fix for siteSettings is null
2018-01-30 14:03:06 -06:00
Timendum a16cae0342 Fix for siteSettings is null
siteSettings can be null, with this change we can avoid an exception.
2018-01-30 10:51:38 +01:00
luke crouch f17ff7168f Merge pull request #1055 from ericlathrop/fix-moving-pinned-tabs-to-new-window
Fix #1053.
2018-01-29 11:02:14 -06:00
luke crouch d3b22faf65 Merge pull request #1097 from crenwick/fix-unwanted-shortcut-bug
Fixes #1084.
2018-01-29 10:27:16 -06:00
Charles Renwick 30e5a27eb4 Fixes #1084.
Before opening a container with the `1-9` shortcut, it ensures that the current pannel is the "containersList"
2018-01-26 13:44:46 -05:00
luke crouch 0f720ec11d Merge pull request #1073 from tiagonbotelho/tb-fix-readme-code-wrapping
Fixes code markdown wrapping under Development in the README section
2018-01-18 14:19:57 -06:00
groovecoder 0ddee7f9d0 id:@testpilot-containers to manifest.json; 5.0.1 2018-01-08 22:59:52 +00:00
Tiago Botelho 1e16e203dc Fixes code markdown wrapping under Development in the README section 2018-01-08 20:37:35 +00:00
LoveIsGrief af986e8880 Set the openerTabId to a tab that won't be removed/closed
The tab might be removed before we can create the tab making the parent
 invalid.

#1065 - [Feature Request] Pass openerTabId when creating tabs in order to know the parent
2018-01-05 13:51:47 +01:00
LoveIsGrief 7e04c46070 Pass the openerTabId when automatically opening tabs in containers
the `openerTabId` can also be seen as the tab's parent.
This is useful for extensions like https://github.com/piroor/treestyletab

#1065 - [Feature Request] Pass openerTabId when creating tabs in order to know the parent
2018-01-05 13:05:32 +01:00
LoveIsGrief 166420dd86 Ignore JetBrains IDE files
#1065 - [Feature Request] Pass openerTabId when creating tabs in order to know the parent
2018-01-05 13:01:43 +01:00
Eric Lathrop d7a2b43b07 Fix #1053.
Create the new window with the default tab rather than an existing tab,
then pin the default tab. This allows any pinned tabs to be moved to the
new window because they're not allowed to be moved after a un-pinned
tab. The default tab is automatically closed later in `moveTabsToWindow`
because it's not a part of the container.
2017-12-22 11:51:11 -05:00
groovecoder bea201a389 update README & package.json to match web-ext flow 2017-12-05 19:06:13 +00:00
Jonathan Kingston d944116e3e Move to a Web Extension only. Fixes #1005 2017-12-05 19:06:13 +00:00
luke crouch 4a1597c87f Merge pull request #1021 from gaborluk/maintain-tab-active-state
Maintain the active state of the redirected tab
2017-12-04 09:20:02 -06:00
Gabor Luk f87bf2a861 Maintain the active state of the redirected tab 2017-12-02 15:25:32 +01:00
luke crouch ef45cde290 Merge pull request #992 from crenwick/open-containers-shortcut
Adds shortcut to open containers.
2017-11-28 15:59:19 -06:00
Charles Renwick 752b1c3b27 Removes shortcut for the tenth container. 2017-11-28 16:17:48 -05:00
Charles Renwick cf26d8547a Consolidates switch pattern for shortcut keys.
Applies @groovecoder suggestions to consolidate a long switch statement.
2017-11-28 11:40:10 -05:00
Charles Renwick 1d78febafc fixes keyCodes 2017-11-21 10:51:29 -05:00
Charles Renwick f483119a40 Adds shortcut to open containers.
Makes it more effective to open the first ten containers in the
containers list. Keys 1-0 will open containers 1-10, if that container
exists.

Note: the plugin's popup panel does not autofocus when opened. This
requires the user to focus on the panel by either clicking or pressing
TAB before using shortcut keys. This behavior is consistent with the previous shortcuts for
this addon.
2017-11-21 10:26:30 -05:00
groovecoder abd2b73fca ignore all *.sw* files for jpm and git 2017-11-17 09:59:29 -06:00
61 changed files with 607 additions and 332 deletions
+5 -3
View File
@@ -3,10 +3,12 @@ package-lock.json
node_modules
README.html
*.xpi
*.swp
*.swo
*.sw*
.vimrc
.env
addon.env
webextension/web-ext-artifacts/*
src/web-ext-artifacts/*
# JetBrains IDE files
.idea
+1
View File
@@ -19,3 +19,4 @@ bin/
.vimrc
.DS_Store
.gdb_history
*.sw*
+1 -1
View File
@@ -5,7 +5,7 @@
"extends": "stylelint-config-standard",
"ignoreFiles": ["webextension/css/*.min.css"],
"ignoreFiles": ["src/css/*.min.css"],
"rules": {
"declaration-block-no-duplicate-properties": true,
+1 -1
View File
@@ -1,6 +1,6 @@
language: node_js
node_js:
- "6.1"
- "lts/*"
notifications:
irc:
+9 -74
View File
@@ -1,12 +1,9 @@
# Multi-Account Containers
The Firefox Multi-Account Containers extension lets you carve out a separate box for each of your online lives no more opening a different browser just to check your work email! [Learn More Here](https://blog.mozilla.org/firefox/introducing-firefox-multi-account-containers/)
[Available on addons.mozilla.org](https://addons.mozilla.org/en-GB/firefox/addon/multi-account-containers/)
**Note:** Firefox 57 + 58 users should Install from our [latest GitHub Release](https://github.com/mozilla/testpilot-containers/releases/latest)
For more info, see:
* [Test Pilot Product Hypothesis Document](https://docs.google.com/document/d/1WQdHTVXROk7dYkSFluc6_hS44tqZjIrG9I-uPyzevE8/edit#)
@@ -16,67 +13,13 @@ For more info, see:
## Requirements
* node 7+ (for jpm)
* Firefox 53+
* Firefox 57+
## Development
### Web Extension Development
Since Firefox 57, this extension can now be run without any of the legacy components that were previously needed.
1. Install web-ext with npm
2. cd webextension; web-ext run -f Nightly
This will work in other builds of Firefox however certain features won't work and you will need to manually flip preferences to enable containers. All other sections of this guide talk about using the legacy setup with jpm.
## Legacy Development
### Development Environment
Add-on development is better with [a particular environment](https://developer.mozilla.org/en-US/Add-ons/Setting_up_extension_development_environment). One simple way to get that environment set up is to install the [DevPrefs add-on](https://addons.mozilla.org/en-US/firefox/addon/devprefs/). You can make a custom Firefox profile that includes the DevPrefs add-on, and use that profile when you run the code in this repository.
1. Make a new profile by running `/path/to/firefox -P`, which launches the profile editor. "Create Profile" -- name it whatever you wish (e.g. 'addon_dev') and store it in the default location. It's probably best to deselect the option to "Use without asking," since you probably don't want to use this as your default profile.
2. Once you've created your profile, click "Start Firefox". A new instance of Firefox should launch. Go to Tools->Add-ons and search for "DevPrefs". Install it. Quit Firefox.
3. Now you have a new, vanilla Firefox profile with the DevPrefs add-on installed. You can use your new profile with the code in _this_ repository like so:
#### Run the `.xpi` file in an unbranded build
Release & Beta channels do not allow un-signed add-ons, even with the DevPrefs. So, you must run the add-on in an [unbranded build](https://wiki.mozilla.org/Add-ons/Extension_Signing#Unbranded_Builds):
1. Download and install an un-branded build of Firefox
2. Download the latest `.xpi` from this repository's releases
3. Run the un-branded build of Firefox with your DevPrefs profile
4. Go to `about:addons`
5. Click the gear, and select "Install Add-on From File..."
6. Select the `.xpi` file
#### Correct prefs
Whilst this is still using legacy code to test you will need the following in your profile:
Change the following prefs in about:config:
- extensions.legacy.enabled = true
- xpinstall.signatures.required = false
#### Run the TxP experiment with `jpm`
1. `git clone git@github.com:mozilla/testpilot-containers.git`
2. `cd testpilot-containers`
3. `npm install`
4. `./node_modules/.bin/jpm run -p /Path/To/Firefox/Profiles/{junk}.addon_dev -b FirefoxBeta` (where FirefoxBeta might be: ~/<reponame>/obj-x86_64-pc-linux-gnu/dist/bin/firefox or ~/<downloadedFirefoxBeta>/firefox)
Check out the [Browser Toolbox](https://developer.mozilla.org/en-US/docs/Tools/Browser_Toolbox) for more information about debugging add-on code.
### Building .xpi
To build a local testpilot-containers.xpi, use the plain [`jpm
xpi`](https://developer.mozilla.org/en-US/Add-ons/SDK/Tools/jpm#jpm_xpi) command,
or run `npm run build`.
1. `npm install`
2. `./node_modules/.bin/web-ext run -s src/`
### Testing
TBD
@@ -84,29 +27,21 @@ TBD
### Distributing
#### Make the new version
1. Bump the version number in `package.json`, `install.rdf`, and
`manifest.json`
1. Bump the version number in `package.json` and `manifest.json`
2. Commit the version number bump
3. Create a git tag for the version: `git tag <version>`
4. Push the tag up to GitHub: `git push --tags`
#### Publish to AMO
While the add-on is an Embedded Web Extension, we have to use the [Mozilla
Internal Signing
Service](https://mana.mozilla.org/wiki/display/FIREFOX/Internal+Extension+Signing)
to sign it as a Mozilla extension exempt from AMO's Web Extension restrictions.
So, to distribute the add-on to AMO:
1. Use `jpm xpi` to build the `.xpi` file
2. [Submit the `.xpi` to the Internal Signing Service and download the signed `.xpi`](https://mana.mozilla.org/wiki/display/SVCOPS/Sign+a+Mozilla+Internal+Extension)
3. [Upload the signed `.xpi` file to
AMO](https://addons.mozilla.org/en-US/developers/addon/multi-account-containers/versions/submit/)
1. `npm run-script build`
2. [Upload the `.zip` to AMO](https://addons.mozilla.org/en-US/developers/addon/multi-account-containers/versions/submit/)
#### Publish to GitHub
Finally, we also publish the release to GitHub for those followers.
1. [Make the new release on
1. Download the signed `.xpi` from [the addon versions page](https://addons.mozilla.org/en-US/developers/addon/multi-account-containers/versions)
2. [Make the new release on
GitHub](https://github.com/mozilla/multi-account-containers/releases/new)
* Use the version number for "Tag version" and "Release title"
* Release notes: copy the output of `git log --no-merges --pretty=format:"%h %s" <previous-version>..<new-version>`
@@ -114,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)
-187
View File
@@ -1,187 +0,0 @@
"use strict";
const PREFS = [
{
name: "privacy.userContext.enabled",
value: true,
type: "bool",
default: false
},
{
name: "privacy.userContext.longPressBehavior",
value: 2,
type: "int",
default: 0
},
{
name: "privacy.userContext.ui.enabled",
value: true, // Post web ext we will be setting this true
type: "bool",
default: true
},
{
name: "privacy.usercontext.about_newtab_segregation.enabled",
value: true,
type: "bool",
default: false
},
];
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cc = Components.classes;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.importGlobalProperties(["TextEncoder", "TextDecoder"]);
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
const JETPACK_DIR_BASENAME = "jetpack";
const EXTENSION_ID = "@testpilot-containers";
function loadStyles(resourceURI) {
const styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"]
.getService(Ci.nsIStyleSheetService);
const styleURI = styleSheet(resourceURI);
const sheetType = styleSheetService.AGENT_SHEET;
styleSheetService.loadAndRegisterSheet(styleURI, sheetType);
}
function styleSheet(resourceURI) {
return Services.io.newURI("data/usercontext.css", null, resourceURI);
}
function unloadStyles(resourceURI) {
const styleURI = styleSheet(resourceURI);
const styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"]
.getService(Ci.nsIStyleSheetService);
const sheetType = styleSheetService.AGENT_SHEET;
if (styleSheetService.sheetRegistered(styleURI, sheetType)) {
styleSheetService.unregisterSheet(styleURI, sheetType);
}
}
function filename() {
const storeFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
storeFile.append(JETPACK_DIR_BASENAME);
storeFile.append(EXTENSION_ID);
storeFile.append("simple-storage");
storeFile.append("store.json");
return storeFile.path;
}
async function makeFilepath() {
const storeFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
storeFile.append(JETPACK_DIR_BASENAME);
await OS.File.makeDir(storeFile.path, { ignoreExisting: true });
storeFile.append(EXTENSION_ID);
await OS.File.makeDir(storeFile.path, { ignoreExisting: true });
storeFile.append("simple-storage");
await OS.File.makeDir(storeFile.path, { ignoreExisting: true });
}
async function getConfig() {
let savedConfig = {savedConfiguration: {}};
try {
const bytes = await OS.File.read(filename());
const raw = new TextDecoder().decode(bytes) || "";
if (raw) {
savedConfig = JSON.parse(raw);
}
} catch (e) {
// ignore file read errors, sometimes they happen and I'm not sure if we can fix
}
return savedConfig;
}
async function initConfig() {
const savedConfig = await getConfig();
savedConfig.savedConfiguration.version = 2;
if (!("prefs" in savedConfig.savedConfiguration)) {
savedConfig.savedConfiguration.prefs = {};
PREFS.forEach((pref) => {
if ("int" === pref.type) {
savedConfig.savedConfiguration.prefs[pref.name] = Services.prefs.getIntPref(pref.name);
} else {
savedConfig.savedConfiguration.prefs[pref.name] = Services.prefs.getBoolPref(pref.name);
}
});
}
const serialized = JSON.stringify(savedConfig);
const bytes = new TextEncoder().encode(serialized) || "";
await makeFilepath();
await OS.File.writeAtomic(filename(), bytes, { });
}
function setPrefs() {
PREFS.forEach((pref) => {
if ("int" === pref.type) {
Services.prefs.setIntPref(pref.name, pref.value);
} else {
Services.prefs.setBoolPref(pref.name, pref.value);
}
});
}
// eslint-disable-next-line no-unused-vars
async function install() {
await initConfig();
setPrefs();
}
// eslint-disable-next-line no-unused-vars
async function uninstall({resourceURI}, aReason) {
if (checkLegacyFirefox()) {
if (aReason === ADDON_UNINSTALL) {
unloadStyles(resourceURI);
await removeChanges();
}
}
}
async function removeChanges() {
const config = await getConfig();
const storedPrefs = config.savedConfiguration.prefs || {};
PREFS.forEach((pref) => {
let value = pref.default;
if (pref.name in storedPrefs) {
value = storedPrefs[pref.name];
}
if ("int" === pref.type) {
Services.prefs.setIntPref(pref.name, value);
} else {
Services.prefs.setBoolPref(pref.name, value);
}
});
}
function checkLegacyFirefox() {
const version = Services.appinfo.version;
const versionMatch = version.match(/^([0-9]+)\./)[1];
if (Number(versionMatch) <= 56) {
return true;
}
return false;
}
// eslint-disable-next-line no-unused-vars
function startup({webExtension, resourceURI}) {
if (checkLegacyFirefox()) {
loadStyles(resourceURI);
// Reset prefs that may have changed, or are legacy
install();
}
// Start the embedded webextension.
webExtension.startup();
}
// eslint-disable-next-line no-unused-vars
function shutdown({resourceURI}, aReason) {
if (checkLegacyFirefox()) {
unloadStyles(resourceURI);
if (aReason === ADDON_DISABLE) {
removeChanges();
}
}
}
-23
View File
@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>@testpilot-containers</em:id>
<em:type>2</em:type>
<em:bootstrap>true</em:bootstrap>
<em:multiprocessCompatible>true</em:multiprocessCompatible>
<em:hasEmbeddedWebExtension>true</em:hasEmbeddedWebExtension>
<em:name>Multi-Account Containers</em:name>
<em: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.</em:description>
<em:targetApplication>
<Description>
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!--Firefox-->
<em:minVersion>53.0</em:minVersion>
<em:maxVersion>*</em:maxVersion>
</Description>
</em:targetApplication>
<em:version>4.1.0</em:version>
<em:unpack>false</em:unpack>
</Description>
</RDF>
+15 -9
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": "4.1.0",
"version": "6.0.0",
"author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston",
"bugs": {
"url": "https://github.com/mozilla/testpilot-containers/issues"
@@ -10,17 +10,22 @@
"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",
"jpm": "^1.2.2",
"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"
"stylelint-order": "^0.3.0",
"web-ext": "^2.2.2"
},
"homepage": "https://github.com/mozilla/testpilot-containers#readme",
"license": "MPL-2.0",
@@ -30,14 +35,15 @@
"url": "git+https://github.com/mozilla/testpilot-containers.git"
},
"scripts": {
"build": "npm test && jpm xpi",
"build": "npm test && cd src && web-ext build --overwrite-dest",
"deploy": "deploy-txp",
"lint": "npm-run-all lint:*",
"lint:addon": "addons-linter webextension --self-hosted",
"lint:css": "stylelint webextension/css/*.css",
"lint:html": "htmllint webextension/*.html",
"lint:addon": "addons-linter src --self-hosted",
"lint:css": "stylelint src/css/*.css",
"lint:html": "htmllint *.html",
"lint:js": "eslint .",
"package": "npm run build && mv testpilot-containers.xpi addon.xpi",
"test": "npm run lint"
"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 && mocha ./test/setup.js test/**/*.test.js",
"test-watch": "mocha ./test/setup.js test/**/*.test.js --watch"
}
}

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 307 B

After

Width:  |  Height:  |  Size: 307 B

Before

Width:  |  Height:  |  Size: 595 B

After

Width:  |  Height:  |  Size: 595 B

Before

Width:  |  Height:  |  Size: 520 B

After

Width:  |  Height:  |  Size: 520 B

Before

Width:  |  Height:  |  Size: 626 B

After

Width:  |  Height:  |  Size: 626 B

Before

Width:  |  Height:  |  Size: 603 B

After

Width:  |  Height:  |  Size: 603 B

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before

Width:  |  Height:  |  Size: 883 B

After

Width:  |  Height:  |  Size: 883 B

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before

Width:  |  Height:  |  Size: 342 B

After

Width:  |  Height:  |  Size: 342 B

Before

Width:  |  Height:  |  Size: 578 B

After

Width:  |  Height:  |  Size: 578 B

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before

Width:  |  Height:  |  Size: 307 B

After

Width:  |  Height:  |  Size: 307 B

Before

Width:  |  Height:  |  Size: 534 B

After

Width:  |  Height:  |  Size: 534 B

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before

Width:  |  Height:  |  Size: 755 B

After

Width:  |  Height:  |  Size: 755 B

Before

Width:  |  Height:  |  Size: 399 B

After

Width:  |  Height:  |  Size: 399 B

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -132,7 +132,7 @@ const assignManager = {
// The container we have in the assignment map isn't present any more so lets remove it
// then continue the existing load
if (!container) {
if (siteSettings && !container) {
this.deleteContainer(siteSettings.userContextId);
return {};
}
@@ -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, 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, 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});
browser.tabs.create({url, cookieStoreId, index, active, openerTabId});
} else {
let confirmUrl = `${loadPage}?url=${this.encodeURLProperty(url)}&cookieStoreId=${cookieStoreId}`;
let currentCookieStoreId;
@@ -367,7 +393,9 @@ const assignManager = {
browser.tabs.create({
url: confirmUrl,
cookieStoreId: currentCookieStoreId,
index
openerTabId,
index,
active
}).then(() => {
// We don't want to sync this URL ever nor clutter the users history
browser.history.deleteUrl({url: confirmUrl});
@@ -131,9 +131,13 @@ const backgroundLogic = {
let newWindowObj;
let hiddenDefaultTabToClose;
if (list.length) {
newWindowObj = await browser.windows.create({
tabId: list.shift().id
});
newWindowObj = await browser.windows.create();
// Pin the default tab in the new window so existing pinned tabs can be moved after it.
// From the docs (https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/tabs/move):
// Note that you can't move pinned tabs to a position after any unpinned tabs in a window, or move any unpinned tabs to a position before any pinned tabs.
await browser.tabs.update(newWindowObj.tabs[0].id, { pinned: true });
browser.tabs.move(list.map((tab) => tab.id), {
windowId: newWindowObj.id,
index: -1
@@ -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);
@@ -523,6 +523,15 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
case 38:
previous();
break;
default:
if ((e.keyCode >= 49 && e.keyCode <= 57) &&
Logic._currentPanel === "containersList") {
const element = selectables[e.keyCode - 48];
if (element) {
element.click();
}
}
break;
}
});
@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Firefox Multi-Account Containers",
"version": "4.1.0",
"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": {
@@ -11,7 +11,8 @@
"applications": {
"gecko": {
"strict_min_version": "53.0"
"id": "@testpilot-containers",
"strict_min_version": "57.0"
}
},
@@ -25,6 +26,7 @@
"contextualIdentities",
"history",
"idle",
"management",
"storage",
"tabs",
"webRequestBlocking",
+1 -1
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
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
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;
};
+75
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;
});
});
});
});
});
@@ -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
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
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
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;
}
});
-19
View File
@@ -1,19 +0,0 @@
const main = require("../");
exports["test main"] = function(assert) {
assert.pass("Unit test running!");
};
exports["test main async"] = function(assert, done) {
assert.pass("async Unit test running!");
done();
};
exports["test dummy"] = function(assert, done) {
main.dummy("foo", function(text) {
assert.ok((text === "foo"), "Is the text actually 'foo'");
done();
});
};
require("sdk/test").run(exports);