Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a2995b6c66 | |||
| ed383c8dfc | |||
| df9b900db6 | |||
| 8e611de605 | |||
| 0a437ff303 | |||
| f7f4c320a6 | |||
| 5813621fb9 | |||
| 56fc7407da | |||
| 220b902144 | |||
| dcc3b76cda | |||
| 752d18ffca | |||
| 97559dd08a | |||
| 0e7363a87f | |||
| 6c62c2f599 | |||
| 884e419a7c | |||
| d7586dd4c2 | |||
| aada0419eb | |||
| 3d1dcd33d1 | |||
| fe0810b048 | |||
| e1c1ac4bd9 | |||
| 7f7f221a79 | |||
| e57c556427 | |||
| dd57158ab5 | |||
| 99db192792 | |||
| fcbee854d0 | |||
| fae1336467 | |||
| 655d8f3791 | |||
| dcc852bf17 | |||
| dab3005c6f | |||
| ee6a54ffa2 | |||
| 601056406a | |||
| fd72ce12b4 | |||
| 6e45532f58 | |||
| 61da6b5e99 | |||
| e0156388e8 | |||
| 16f1d47bf2 | |||
| ee647344a1 | |||
| 40426ca936 | |||
| d1e9c2d1e3 | |||
| 3bd33cda99 | |||
| 609f62ac7a | |||
| ce84665e3a | |||
| 7dceaf6679 | |||
| b6bcd99dc8 | |||
| 9bc9509316 | |||
| 22ec01d565 | |||
| a16cae0342 | |||
| f17ff7168f | |||
| d3b22faf65 | |||
| 30e5a27eb4 | |||
| 0f720ec11d | |||
| 0ddee7f9d0 | |||
| 1e16e203dc | |||
| af986e8880 | |||
| 7e04c46070 | |||
| 166420dd86 | |||
| d7a2b43b07 | |||
| bea201a389 | |||
| d944116e3e | |||
| 4a1597c87f | |||
| f87bf2a861 | |||
| ef45cde290 | |||
| 752b1c3b27 | |||
| cf26d8547a | |||
| 1d78febafc | |||
| f483119a40 | |||
| abd2b73fca |
@@ -0,0 +1,28 @@
|
|||||||
|
<!--
|
||||||
|
Feel free to ignore this Issue template if you just want to ask or suggest something. If you experience an Issue then please provide all asked informations.
|
||||||
|
|
||||||
|
Note: If "Firefox will: Never remember history" in the Firefox Preferences/Options under "Privacy & Security > History" is selected, then Multi-Account Containers will not work, since Containers aren't available in Private Windows.
|
||||||
|
-->
|
||||||
|
- Is "Firefox will: Never remember history" in the Firefox Preferences/Options under "Privacy & Security > History" selected? Yes/No:
|
||||||
|
- Are you using Firefox in a Private Window? Yes/No:
|
||||||
|
- Can you see a grayed out but ticked Checkbox with the description "Enable Container Tabs" in the Firefox Preferences/Options under "Tabs"? Yes/No:
|
||||||
|
- Multi-Account Containers Version:
|
||||||
|
- Operating System + Version:
|
||||||
|
- Firefox Version:
|
||||||
|
- Other installed Add-ons + Version + Enabled/Disabled-Status:
|
||||||
|
<!-- To be able to Copy&Paste the full list of your Add-ons navigate to "about:support" and scroll down to "Extensions" -->
|
||||||
|
|
||||||
|
|
||||||
|
### Actual behavior
|
||||||
|
..
|
||||||
|
|
||||||
|
### Expected behavior
|
||||||
|
..
|
||||||
|
|
||||||
|
### Steps to reproduce
|
||||||
|
1. ..
|
||||||
|
2. ..
|
||||||
|
3. ..
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
..
|
||||||
@@ -3,10 +3,12 @@ package-lock.json
|
|||||||
node_modules
|
node_modules
|
||||||
README.html
|
README.html
|
||||||
*.xpi
|
*.xpi
|
||||||
*.swp
|
*.sw*
|
||||||
*.swo
|
|
||||||
.vimrc
|
.vimrc
|
||||||
.env
|
.env
|
||||||
addon.env
|
addon.env
|
||||||
|
|
||||||
webextension/web-ext-artifacts/*
|
src/web-ext-artifacts/*
|
||||||
|
|
||||||
|
# JetBrains IDE files
|
||||||
|
.idea
|
||||||
|
|||||||
@@ -19,3 +19,4 @@ bin/
|
|||||||
.vimrc
|
.vimrc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.gdb_history
|
.gdb_history
|
||||||
|
*.sw*
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
"extends": "stylelint-config-standard",
|
"extends": "stylelint-config-standard",
|
||||||
|
|
||||||
"ignoreFiles": ["webextension/css/*.min.css"],
|
"ignoreFiles": ["src/css/*.min.css"],
|
||||||
|
|
||||||
"rules": {
|
"rules": {
|
||||||
"declaration-block-no-duplicate-properties": true,
|
"declaration-block-no-duplicate-properties": true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- "6.1"
|
- "lts/*"
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
irc:
|
irc:
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ Everyone is welcome to contribute to containers. Reach out to team members if yo
|
|||||||
|
|
||||||
If you find a bug with containers, please file a issue.
|
If you find a bug with containers, please file a issue.
|
||||||
|
|
||||||
Check first if the bug might already exist: https://github.com/mozilla/testpilot-containers/issues
|
Check first if the bug might already exist: https://github.com/mozilla/multi-account-containers/issues
|
||||||
|
|
||||||
[Open an issue](https://github.com/mozilla/testpilot-containers/issues/new)
|
[Open an issue](https://github.com/mozilla/multi-account-containers/issues/new)
|
||||||
|
|
||||||
1. Visit about:support
|
1. Visit about:support
|
||||||
2. Click "Copy raw data to clipboard" and paste into the bug. Alternatively copy the following sections into the issue:
|
2. Click "Copy raw data to clipboard" and paste into the bug. Alternatively copy the following sections into the issue:
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
# Multi-Account Containers
|
# 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/)
|
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/)
|
[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:
|
For more info, see:
|
||||||
|
|
||||||
* [Test Pilot Product Hypothesis Document](https://docs.google.com/document/d/1WQdHTVXROk7dYkSFluc6_hS44tqZjIrG9I-uPyzevE8/edit#)
|
* [Test Pilot Product Hypothesis Document](https://docs.google.com/document/d/1WQdHTVXROk7dYkSFluc6_hS44tqZjIrG9I-uPyzevE8/edit#)
|
||||||
@@ -16,67 +13,13 @@ For more info, see:
|
|||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
* node 7+ (for jpm)
|
* node 7+ (for jpm)
|
||||||
* Firefox 53+
|
* Firefox 57+
|
||||||
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
### Web Extension Development
|
1. `npm install`
|
||||||
|
2. `./node_modules/.bin/web-ext run -s src/`
|
||||||
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`.
|
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
TBD
|
TBD
|
||||||
@@ -84,29 +27,21 @@ TBD
|
|||||||
### Distributing
|
### Distributing
|
||||||
#### Make the new version
|
#### Make the new version
|
||||||
|
|
||||||
1. Bump the version number in `package.json`, `install.rdf`, and
|
1. Bump the version number in `package.json` and `manifest.json`
|
||||||
`manifest.json`
|
|
||||||
2. Commit the version number bump
|
2. Commit the version number bump
|
||||||
3. Create a git tag for the version: `git tag <version>`
|
3. Create a git tag for the version: `git tag <version>`
|
||||||
4. Push the tag up to GitHub: `git push --tags`
|
4. Push the tag up to GitHub: `git push --tags`
|
||||||
|
|
||||||
#### Publish to AMO
|
#### 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. `npm run-script build`
|
||||||
|
2. [Upload the `.zip` to AMO](https://addons.mozilla.org/en-US/developers/addon/multi-account-containers/versions/submit/)
|
||||||
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/)
|
|
||||||
|
|
||||||
#### Publish to GitHub
|
#### Publish to GitHub
|
||||||
Finally, we also publish the release to GitHub for those followers.
|
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)
|
GitHub](https://github.com/mozilla/multi-account-containers/releases/new)
|
||||||
* Use the version number for "Tag version" and "Release title"
|
* 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>`
|
* 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
|
### Links
|
||||||
|
|
||||||
Facebook & Twitter icons CC-Attrib http://fairheadcreative.com.
|
Facebook & Twitter icons CC-Attrib https://fairheadcreative.com.
|
||||||
|
|
||||||
- [Licence](./LICENSE.txt)
|
- [Licence](./LICENSE.txt)
|
||||||
- [Contributing](./CONTRIBUTING.md)
|
- [Contributing](./CONTRIBUTING.md)
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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>
|
|
||||||
|
|
||||||
@@ -2,42 +2,46 @@
|
|||||||
"name": "testpilot-containers",
|
"name": "testpilot-containers",
|
||||||
"title": "Multi-Account 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.",
|
"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.1",
|
||||||
"author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston",
|
"author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/mozilla/testpilot-containers/issues"
|
"url": "https://github.com/mozilla/multi-account-containers/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"addons-linter": "^0.15.14",
|
"addons-linter": "^1.3.2",
|
||||||
"deploy-txp": "^1.0.7",
|
"chai": "^4.1.2",
|
||||||
"eslint": "^3.17.1",
|
"eslint": "^3.17.1",
|
||||||
"eslint-plugin-no-unsanitized": "^2.0.0",
|
"eslint-plugin-no-unsanitized": "^2.0.0",
|
||||||
"eslint-plugin-promise": "^3.4.0",
|
"eslint-plugin-promise": "^3.4.0",
|
||||||
"htmllint-cli": "^0.0.5",
|
"htmllint-cli": "0.0.7",
|
||||||
"jpm": "^1.2.2",
|
"jsdom": "^11.6.2",
|
||||||
"json": "^9.0.6",
|
"json": "^9.0.6",
|
||||||
|
"mocha": "^5.0.0",
|
||||||
"npm-run-all": "^4.0.0",
|
"npm-run-all": "^4.0.0",
|
||||||
|
"sinon": "^4.4.0",
|
||||||
|
"sinon-chai": "^2.14.0",
|
||||||
"stylelint": "^7.9.0",
|
"stylelint": "^7.9.0",
|
||||||
"stylelint-config-standard": "^16.0.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",
|
"homepage": "https://github.com/mozilla/multi-account-containers#readme",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/mozilla/testpilot-containers.git"
|
"url": "git+https://github.com/mozilla/multi-account-containers.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"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": "npm-run-all lint:*",
|
||||||
"lint:addon": "addons-linter webextension --self-hosted",
|
"lint:addon": "addons-linter src --self-hosted",
|
||||||
"lint:css": "stylelint webextension/css/*.css",
|
"lint:css": "stylelint src/css/*.css",
|
||||||
"lint:html": "htmllint webextension/*.html",
|
"lint:html": "htmllint *.html",
|
||||||
"lint:js": "eslint .",
|
"lint:js": "eslint .",
|
||||||
"package": "npm run build && mv testpilot-containers.xpi addon.xpi",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ body {
|
|||||||
--small-text-size: 0.833rem; /* 10px */
|
--small-text-size: 0.833rem; /* 10px */
|
||||||
--small-radius: 3px;
|
--small-radius: 3px;
|
||||||
--icon-button-size: calc(calc(var(--block-line-separation-size) * 2) + 1.66rem); /* 20px */
|
--icon-button-size: calc(calc(var(--block-line-separation-size) * 2) + 1.66rem); /* 20px */
|
||||||
|
--inactive-opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-resolution: 1dppx) {
|
@media (min-resolution: 1dppx) {
|
||||||
@@ -537,7 +538,7 @@ span ~ .panel-header-text {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#current-tab > label > input:checked {
|
#current-tab > label > input:checked {
|
||||||
background-image: url("chrome://global/skin/in-content/check.svg#check-native");
|
background-image: url("/img/check.svg");
|
||||||
background-position: -1px -1px;
|
background-position: -1px -1px;
|
||||||
background-size: var(--icon-size);
|
background-size: var(--icon-size);
|
||||||
}
|
}
|
||||||
@@ -578,6 +579,11 @@ span ~ .panel-header-text {
|
|||||||
max-inline-size: 204px;
|
max-inline-size: 204px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disable-edit-containers {
|
||||||
|
opacity: var(--inactive-opacity);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.userContext-wrapper {
|
.userContext-wrapper {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 307 B After Width: | Height: | Size: 307 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path fill="#3c3c3c" d="M6 14a1 1 0 0 1-.707-.293l-3-3a1 1 0 0 1 1.414-1.414l2.157 2.157 6.316-9.023a1 1 0 0 1 1.639 1.146l-7 10a1 1 0 0 1-.732.427A.863.863 0 0 1 6 14z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 477 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
|
// The container we have in the assignment map isn't present any more so lets remove it
|
||||||
// then continue the existing load
|
// then continue the existing load
|
||||||
if (!container) {
|
if (siteSettings && !container) {
|
||||||
this.deleteContainer(siteSettings.userContextId);
|
this.deleteContainer(siteSettings.userContextId);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -143,8 +143,57 @@ const assignManager = {
|
|||||||
|| this.storageArea.isExempted(options.url, tab.id)) {
|
|| this.storageArea.isExempted(options.url, tab.id)) {
|
||||||
return {};
|
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);
|
if (!this.canceledRequests[tab.id]) {
|
||||||
|
// we decided to cancel the request at this point, register canceled request
|
||||||
|
this.canceledRequests[tab.id] = {
|
||||||
|
requestIds: {
|
||||||
|
[options.requestId]: true
|
||||||
|
},
|
||||||
|
urls: {
|
||||||
|
[options.url]: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// since webRequest onCompleted and onErrorOccurred are not 100% reliable (see #1120)
|
||||||
|
// we register a timer here to cleanup canceled requests, just to make sure we don't
|
||||||
|
// end up in a situation where certain urls in a tab.id stay canceled
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.canceledRequests[tab.id]) {
|
||||||
|
delete this.canceledRequests[tab.id];
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
let cancelEarly = false;
|
||||||
|
if (this.canceledRequests[tab.id].requestIds[options.requestId] ||
|
||||||
|
this.canceledRequests[tab.id].urls[options.url]) {
|
||||||
|
// same requestId or url from the same tab
|
||||||
|
// this is a redirect that we have to cancel early to prevent opening two tabs
|
||||||
|
cancelEarly = true;
|
||||||
|
}
|
||||||
|
// we decided to cancel the request at this point, register canceled request
|
||||||
|
this.canceledRequests[tab.id].requestIds[options.requestId] = true;
|
||||||
|
this.canceledRequests[tab.id].urls[options.url] = true;
|
||||||
|
if (cancelEarly) {
|
||||||
|
return {
|
||||||
|
cancel: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reloadPageInContainer(
|
||||||
|
options.url,
|
||||||
|
userContextId,
|
||||||
|
siteSettings.userContextId,
|
||||||
|
tab.index + 1,
|
||||||
|
tab.active,
|
||||||
|
siteSettings.neverAsk,
|
||||||
|
openTabId
|
||||||
|
);
|
||||||
this.calculateContextMenu(tab);
|
this.calculateContextMenu(tab);
|
||||||
|
|
||||||
/* Removal of existing tabs:
|
/* Removal of existing tabs:
|
||||||
@@ -158,9 +207,7 @@ const assignManager = {
|
|||||||
however they don't run on about:blank so this would likely be just as hacky.
|
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.
|
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)
|
if (removeTab) {
|
||||||
|| (messageHandler.lastCreatedTab
|
|
||||||
&& messageHandler.lastCreatedTab.id === tab.id)) {
|
|
||||||
browser.tabs.remove(tab.id);
|
browser.tabs.remove(tab.id);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -174,9 +221,23 @@ const assignManager = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Before a request is handled by the browser we decide if we should route through a different container
|
// 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) => {
|
browser.webRequest.onBeforeRequest.addListener((options) => {
|
||||||
return this.onBeforeRequest(options);
|
return this.onBeforeRequest(options);
|
||||||
},{urls: ["<all_urls>"], types: ["main_frame"]}, ["blocking"]);
|
},{urls: ["<all_urls>"], types: ["main_frame"]}, ["blocking"]);
|
||||||
|
|
||||||
|
// Clean up canceled requests
|
||||||
|
browser.webRequest.onCompleted.addListener((options) => {
|
||||||
|
if (this.canceledRequests[options.tabId]) {
|
||||||
|
delete this.canceledRequests[options.tabId];
|
||||||
|
}
|
||||||
|
},{urls: ["<all_urls>"], types: ["main_frame"]});
|
||||||
|
browser.webRequest.onErrorOccurred.addListener((options) => {
|
||||||
|
if (this.canceledRequests[options.tabId]) {
|
||||||
|
delete this.canceledRequests[options.tabId];
|
||||||
|
}
|
||||||
|
},{urls: ["<all_urls>"], types: ["main_frame"]});
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async _onClickedHandler(info, tab) {
|
async _onClickedHandler(info, tab) {
|
||||||
@@ -350,13 +411,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 cookieStoreId = backgroundLogic.cookieStoreId(userContextId);
|
||||||
const loadPage = browser.extension.getURL("confirm-page.html");
|
const loadPage = browser.extension.getURL("confirm-page.html");
|
||||||
// False represents assignment is not permitted
|
// 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 the user has explicitly checked "Never Ask Again" on the warning page we will send them straight there
|
||||||
if (neverAsk) {
|
if (neverAsk) {
|
||||||
browser.tabs.create({url, cookieStoreId, index});
|
browser.tabs.create({url, cookieStoreId, index, active, openerTabId});
|
||||||
} else {
|
} else {
|
||||||
let confirmUrl = `${loadPage}?url=${this.encodeURLProperty(url)}&cookieStoreId=${cookieStoreId}`;
|
let confirmUrl = `${loadPage}?url=${this.encodeURLProperty(url)}&cookieStoreId=${cookieStoreId}`;
|
||||||
let currentCookieStoreId;
|
let currentCookieStoreId;
|
||||||
@@ -367,7 +428,9 @@ const assignManager = {
|
|||||||
browser.tabs.create({
|
browser.tabs.create({
|
||||||
url: confirmUrl,
|
url: confirmUrl,
|
||||||
cookieStoreId: currentCookieStoreId,
|
cookieStoreId: currentCookieStoreId,
|
||||||
index
|
openerTabId,
|
||||||
|
index,
|
||||||
|
active
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
// We don't want to sync this URL ever nor clutter the users history
|
// We don't want to sync this URL ever nor clutter the users history
|
||||||
browser.history.deleteUrl({url: confirmUrl});
|
browser.history.deleteUrl({url: confirmUrl});
|
||||||
@@ -131,9 +131,13 @@ const backgroundLogic = {
|
|||||||
let newWindowObj;
|
let newWindowObj;
|
||||||
let hiddenDefaultTabToClose;
|
let hiddenDefaultTabToClose;
|
||||||
if (list.length) {
|
if (list.length) {
|
||||||
newWindowObj = await browser.windows.create({
|
newWindowObj = await browser.windows.create();
|
||||||
tabId: list.shift().id
|
|
||||||
});
|
// 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), {
|
browser.tabs.move(list.map((tab) => tab.id), {
|
||||||
windowId: newWindowObj.id,
|
windowId: newWindowObj.id,
|
||||||
index: -1
|
index: -1
|
||||||
@@ -72,6 +72,42 @@ const messageHandler = {
|
|||||||
return response;
|
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) {
|
if (browser.contextualIdentities.onRemoved) {
|
||||||
browser.contextualIdentities.onRemoved.addListener(({contextualIdentity}) => {
|
browser.contextualIdentities.onRemoved.addListener(({contextualIdentity}) => {
|
||||||
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(contextualIdentity.cookieStoreId);
|
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(contextualIdentity.cookieStoreId);
|
||||||
@@ -162,7 +162,7 @@ const Logic = {
|
|||||||
async clearBrowserActionBadge() {
|
async clearBrowserActionBadge() {
|
||||||
const extensionInfo = await getExtensionInfo();
|
const extensionInfo = await getExtensionInfo();
|
||||||
const storage = await browser.storage.local.get({browserActionBadgesClicked: []});
|
const storage = await browser.storage.local.get({browserActionBadgesClicked: []});
|
||||||
browser.browserAction.setBadgeBackgroundColor({color: ""});
|
browser.browserAction.setBadgeBackgroundColor({color: null});
|
||||||
browser.browserAction.setBadgeText({text: ""});
|
browser.browserAction.setBadgeText({text: ""});
|
||||||
storage.browserActionBadgesClicked.push(extensionInfo.version);
|
storage.browserActionBadgesClicked.push(extensionInfo.version);
|
||||||
// use set and spread to create a unique array
|
// use set and spread to create a unique array
|
||||||
@@ -212,6 +212,27 @@ const Logic = {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async numTabs() {
|
||||||
|
const activeTabs = await browser.tabs.query({windowId: browser.windows.WINDOW_ID_CURRENT});
|
||||||
|
return activeTabs.length;
|
||||||
|
},
|
||||||
|
|
||||||
|
_disableMoveTabs(message) {
|
||||||
|
const moveTabsEl = document.querySelector("#container-info-movetabs");
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
const incompatEl = document.createElement("div");
|
||||||
|
|
||||||
|
moveTabsEl.classList.remove("clickable");
|
||||||
|
moveTabsEl.setAttribute("title", message);
|
||||||
|
|
||||||
|
fragment.appendChild(incompatEl);
|
||||||
|
incompatEl.setAttribute("id", "container-info-movetabs-incompat");
|
||||||
|
incompatEl.textContent = message;
|
||||||
|
incompatEl.classList.add("container-info-tab-row");
|
||||||
|
|
||||||
|
moveTabsEl.parentNode.insertBefore(fragment, moveTabsEl.nextSibling);
|
||||||
|
},
|
||||||
|
|
||||||
async refreshIdentities() {
|
async refreshIdentities() {
|
||||||
const [identities, state] = await Promise.all([
|
const [identities, state] = await Promise.all([
|
||||||
browser.contextualIdentities.query({}),
|
browser.contextualIdentities.query({}),
|
||||||
@@ -485,8 +506,10 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
|||||||
Logic.showPanel(P_CONTAINER_EDIT, { name: Logic.generateIdentityName() });
|
Logic.showPanel(P_CONTAINER_EDIT, { name: Logic.generateIdentityName() });
|
||||||
});
|
});
|
||||||
|
|
||||||
Logic.addEnterHandler(document.querySelector("#edit-containers-link"), () => {
|
Logic.addEnterHandler(document.querySelector("#edit-containers-link"), (e) => {
|
||||||
Logic.showPanel(P_CONTAINERS_EDIT);
|
if (!e.target.classList.contains("disable-edit-containers")){
|
||||||
|
Logic.showPanel(P_CONTAINERS_EDIT);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Logic.addEnterHandler(document.querySelector("#sort-containers-link"), async function () {
|
Logic.addEnterHandler(document.querySelector("#sort-containers-link"), async function () {
|
||||||
@@ -523,6 +546,15 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
|||||||
case 38:
|
case 38:
|
||||||
previous();
|
previous();
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
if ((e.keyCode >= 49 && e.keyCode <= 57) &&
|
||||||
|
Logic._currentPanel === "containersList") {
|
||||||
|
const element = selectables[e.keyCode - 48];
|
||||||
|
if (element) {
|
||||||
|
element.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -657,6 +689,13 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
|||||||
document.addEventListener("mousedown", () => {
|
document.addEventListener("mousedown", () => {
|
||||||
document.removeEventListener("focus", focusHandler);
|
document.removeEventListener("focus", focusHandler);
|
||||||
});
|
});
|
||||||
|
/* If no container is present disable the Edit Containers button */
|
||||||
|
const editContainer = document.querySelector("#edit-containers-link");
|
||||||
|
if (Logic.identities().length === 0) {
|
||||||
|
editContainer.classList.add("disable-edit-containers");
|
||||||
|
} else {
|
||||||
|
editContainer.classList.remove("disable-edit-containers");
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
@@ -689,37 +728,31 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Check if the user has incompatible add-ons installed
|
// Check if the user has incompatible add-ons installed
|
||||||
|
let incompatible = false;
|
||||||
try {
|
try {
|
||||||
const incompatible = await browser.runtime.sendMessage({
|
incompatible = await browser.runtime.sendMessage({
|
||||||
method: "checkIncompatibleAddons"
|
method: "checkIncompatibleAddons"
|
||||||
});
|
});
|
||||||
const moveTabsEl = document.querySelector("#container-info-movetabs");
|
|
||||||
if (incompatible) {
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
const incompatEl = document.createElement("div");
|
|
||||||
|
|
||||||
moveTabsEl.classList.remove("clickable");
|
|
||||||
moveTabsEl.setAttribute("title", "Moving container tabs is incompatible with Pulse, PageShot, and SnoozeTabs.");
|
|
||||||
|
|
||||||
fragment.appendChild(incompatEl);
|
|
||||||
incompatEl.setAttribute("id", "container-info-movetabs-incompat");
|
|
||||||
incompatEl.textContent = "Incompatible with other Experiments.";
|
|
||||||
incompatEl.classList.add("container-info-tab-row");
|
|
||||||
|
|
||||||
moveTabsEl.parentNode.insertBefore(fragment, moveTabsEl.nextSibling);
|
|
||||||
} else {
|
|
||||||
Logic.addEnterHandler(moveTabsEl, async function () {
|
|
||||||
await browser.runtime.sendMessage({
|
|
||||||
method: "moveTabsToWindow",
|
|
||||||
windowId: browser.windows.WINDOW_ID_CURRENT,
|
|
||||||
cookieStoreId: Logic.currentIdentity().cookieStoreId,
|
|
||||||
});
|
|
||||||
window.close();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error("Could not check for incompatible add-ons.");
|
throw new Error("Could not check for incompatible add-ons.");
|
||||||
}
|
}
|
||||||
|
const moveTabsEl = document.querySelector("#container-info-movetabs");
|
||||||
|
const numTabs = await Logic.numTabs();
|
||||||
|
if (incompatible) {
|
||||||
|
Logic._disableMoveTabs("Moving container tabs is incompatible with Pulse, PageShot, and SnoozeTabs.");
|
||||||
|
return;
|
||||||
|
} else if (numTabs === 1) {
|
||||||
|
Logic._disableMoveTabs("Cannot move a tab from a single-tab window.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Logic.addEnterHandler(moveTabsEl, async function () {
|
||||||
|
await browser.runtime.sendMessage({
|
||||||
|
method: "moveTabsToWindow",
|
||||||
|
windowId: browser.windows.WINDOW_ID_CURRENT,
|
||||||
|
cookieStoreId: Logic.currentIdentity().cookieStoreId,
|
||||||
|
});
|
||||||
|
window.close();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// This method is called when the panel is shown.
|
// This method is called when the panel is shown.
|
||||||
@@ -992,6 +1025,11 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
|||||||
|
|
||||||
document.querySelector("#edit-container-panel-name-input").value = identity.name || "";
|
document.querySelector("#edit-container-panel-name-input").value = identity.name || "";
|
||||||
document.querySelector("#edit-container-panel-usercontext-input").value = userContextId || NEW_CONTAINER_ID;
|
document.querySelector("#edit-container-panel-usercontext-input").value = userContextId || NEW_CONTAINER_ID;
|
||||||
|
const containerName = document.querySelector("#edit-container-panel-name-input");
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
containerName.select();
|
||||||
|
containerName.focus();
|
||||||
|
});
|
||||||
[...document.querySelectorAll("[name='container-color']")].forEach(colorInput => {
|
[...document.querySelectorAll("[name='container-color']")].forEach(colorInput => {
|
||||||
colorInput.checked = colorInput.value === identity.color;
|
colorInput.checked = colorInput.value === identity.color;
|
||||||
});
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
const DEFAULT_FAVICON = "moz-icon://goat?size=16";
|
const DEFAULT_FAVICON = "/img/blank-favicon.svg";
|
||||||
|
|
||||||
// TODO use export here instead of globals
|
// TODO use export here instead of globals
|
||||||
window.Utils = {
|
window.Utils = {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "Firefox Multi-Account Containers",
|
"name": "Firefox Multi-Account Containers",
|
||||||
"version": "4.1.0",
|
"version": "6.0.1",
|
||||||
|
|
||||||
"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.",
|
"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": {
|
"icons": {
|
||||||
@@ -11,11 +11,12 @@
|
|||||||
|
|
||||||
"applications": {
|
"applications": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
"strict_min_version": "53.0"
|
"id": "@testpilot-containers",
|
||||||
|
"strict_min_version": "57.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"homepage_url": "https://testpilot.firefox.com/",
|
"homepage_url": "https://github.com/mozilla/multi-account-containers#readme",
|
||||||
|
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"<all_urls>",
|
"<all_urls>",
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
"contextualIdentities",
|
"contextualIdentities",
|
||||||
"history",
|
"history",
|
||||||
"idle",
|
"idle",
|
||||||
|
"management",
|
||||||
"storage",
|
"storage",
|
||||||
"tabs",
|
"tabs",
|
||||||
"webRequestBlocking",
|
"webRequestBlocking",
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||||
<title>Multi-Account Containers</title>
|
<title>Multi-Account Containers</title>
|
||||||
<link rel="stylesheet" href="/css/popup.css">
|
<link rel="stylesheet" href="css/popup.css">
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
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()
|
||||||
|
},
|
||||||
|
onErrorOccurred: {
|
||||||
|
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;
|
||||||
|
};
|
||||||
@@ -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"
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
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 = {}) {
|
||||||
|
if (options.resetHistory) {
|
||||||
|
background.browser.tabs.create.resetHistory();
|
||||||
|
background.browser.tabs.remove.resetHistory();
|
||||||
|
}
|
||||||
|
background.browser.tabs.get.resolves(tab);
|
||||||
|
background.browser.tabs.onCreated.addListener.yield(tab);
|
||||||
|
const [promise] = background.browser.webRequest.onBeforeRequest.addListener.yield({
|
||||||
|
frameId: 0,
|
||||||
|
tabId: tab.id,
|
||||||
|
url: tab.url,
|
||||||
|
requestId: options.requestId
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
popup: {
|
||||||
|
async clickElementById(id) {
|
||||||
|
const clickEvent = popup.document.createEvent("HTMLEvents");
|
||||||
|
clickEvent.initEvent("click");
|
||||||
|
popup.document.getElementById(id).dispatchEvent(clickEvent);
|
||||||
|
await nextTick();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when redirects change requestId midflight", () => {
|
||||||
|
let promiseResults;
|
||||||
|
beforeEach(async () => {
|
||||||
|
// init
|
||||||
|
const activeTab = {
|
||||||
|
id: 1,
|
||||||
|
cookieStoreId: "firefox-container-1",
|
||||||
|
url: "https://www.youtube.com",
|
||||||
|
index: 0
|
||||||
|
};
|
||||||
|
await helper.browser.initializeWithTab(activeTab);
|
||||||
|
// assign the activeTab.url
|
||||||
|
await helper.popup.clickElementById("container-page-assigned");
|
||||||
|
|
||||||
|
// http://youtube.com
|
||||||
|
const newTab = {
|
||||||
|
id: 2,
|
||||||
|
cookieStoreId: "firefox-default",
|
||||||
|
url: "http://youtube.com",
|
||||||
|
index: 1,
|
||||||
|
active: true
|
||||||
|
};
|
||||||
|
const promise1 = helper.browser.openNewTab(newTab, {
|
||||||
|
requestId: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://youtube.com
|
||||||
|
const [promise2] = background.browser.webRequest.onBeforeRequest.addListener.yield({
|
||||||
|
frameId: 0,
|
||||||
|
tabId: newTab.id,
|
||||||
|
url: "https://youtube.com",
|
||||||
|
requestId: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://www.youtube.com
|
||||||
|
const [promise3] = background.browser.webRequest.onBeforeRequest.addListener.yield({
|
||||||
|
frameId: 0,
|
||||||
|
tabId: newTab.id,
|
||||||
|
url: "https://www.youtube.com",
|
||||||
|
requestId: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://www.youtube.com
|
||||||
|
const [promise4] = background.browser.webRequest.onBeforeRequest.addListener.yield({
|
||||||
|
frameId: 0,
|
||||||
|
tabId: newTab.id,
|
||||||
|
url: "https://www.youtube.com",
|
||||||
|
requestId: 2
|
||||||
|
});
|
||||||
|
|
||||||
|
promiseResults = await Promise.all([promise1, promise2, promise3, promise4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not open two confirm pages", async () => {
|
||||||
|
// http://youtube.com is not assigned, no cancel, no reopening
|
||||||
|
expect(promiseResults[0]).to.deep.equal({});
|
||||||
|
|
||||||
|
// https://youtube.com is not assigned, no cancel, no reopening
|
||||||
|
expect(promiseResults[1]).to.deep.equal({});
|
||||||
|
|
||||||
|
// https://www.youtube.com is assigned, this triggers reopening, cancel
|
||||||
|
expect(promiseResults[2]).to.deep.equal({
|
||||||
|
cancel: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://www.youtube.com is assigned, this was a redirect, cancel early, no reopening
|
||||||
|
expect(promiseResults[3]).to.deep.equal({
|
||||||
|
cancel: true
|
||||||
|
});
|
||||||
|
|
||||||
|
background.browser.tabs.create.should.have.been.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should uncancel after webRequest.onCompleted", async () => {
|
||||||
|
const [promise1] = background.browser.webRequest.onCompleted.addListener.yield({
|
||||||
|
tabId: 2
|
||||||
|
});
|
||||||
|
await promise1;
|
||||||
|
|
||||||
|
const [promise2] = background.browser.webRequest.onBeforeRequest.addListener.yield({
|
||||||
|
frameId: 0,
|
||||||
|
tabId: 2,
|
||||||
|
url: "https://www.youtube.com",
|
||||||
|
requestId: 123
|
||||||
|
});
|
||||||
|
await promise2;
|
||||||
|
|
||||||
|
background.browser.tabs.create.should.have.been.calledTwice;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should uncancel after webRequest.onErrorOccurred", async () => {
|
||||||
|
const [promise1] = background.browser.webRequest.onErrorOccurred.addListener.yield({
|
||||||
|
tabId: 2
|
||||||
|
});
|
||||||
|
await promise1;
|
||||||
|
|
||||||
|
// request to assigned url in same tab
|
||||||
|
const [promise2] = background.browser.webRequest.onBeforeRequest.addListener.yield({
|
||||||
|
frameId: 0,
|
||||||
|
tabId: 2,
|
||||||
|
url: "https://www.youtube.com",
|
||||||
|
requestId: 123
|
||||||
|
});
|
||||||
|
await promise2;
|
||||||
|
|
||||||
|
background.browser.tabs.create.should.have.been.calledTwice;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should uncancel after 2 seconds", async () => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
// request to assigned url in same tab
|
||||||
|
const [promise2] = background.browser.webRequest.onBeforeRequest.addListener.yield({
|
||||||
|
frameId: 0,
|
||||||
|
tabId: 2,
|
||||||
|
url: "https://www.youtube.com",
|
||||||
|
requestId: 123
|
||||||
|
});
|
||||||
|
await promise2;
|
||||||
|
|
||||||
|
background.browser.tabs.create.should.have.been.calledTwice;
|
||||||
|
}).timeout(2002);
|
||||||
|
|
||||||
|
it("should not influence the canceled url in other tabs", async () => {
|
||||||
|
const newTab = {
|
||||||
|
id: 123,
|
||||||
|
cookieStoreId: "firefox-default",
|
||||||
|
url: "https://www.youtube.com",
|
||||||
|
index: 10,
|
||||||
|
active: true
|
||||||
|
};
|
||||||
|
await helper.browser.openNewTab(newTab, {
|
||||||
|
requestId: 321
|
||||||
|
});
|
||||||
|
|
||||||
|
background.browser.tabs.create.should.have.been.calledTwice;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (global.popup) {
|
||||||
|
global.popup.dom.window.close();
|
||||||
|
delete global.popup;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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);
|
|
||||||