Merge pull request #1611 from kendallcorner/new-sync-file

Sync Feature
This commit is contained in:
luke crouch 2020-02-11 08:36:02 -06:00 committed by GitHub
commit da79841201
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1778 additions and 363 deletions

View file

@ -19,7 +19,7 @@ For more info, see:
## Development ## Development
1. `npm install` 1. `npm install`
2. `./node_modules/.bin/web-ext run -s src/` 2. `./node_modules/web-ext/bin/web-ext run -s src/`
### Testing ### Testing
`npm run test` `npm run test`

View file

@ -2,7 +2,7 @@
"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": "6.1.1", "version": "6.2.0",
"author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston", "author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston",
"bugs": { "bugs": {
"url": "https://github.com/mozilla/multi-account-containers/issues" "url": "https://github.com/mozilla/multi-account-containers/issues"
@ -10,7 +10,7 @@
"dependencies": {}, "dependencies": {},
"devDependencies": { "devDependencies": {
"addons-linter": "^1.3.2", "addons-linter": "^1.3.2",
"ajv": "^6.6.2", "ajv": "^6.6.3",
"chai": "^4.2.0", "chai": "^4.2.0",
"eslint": "^6.6.0", "eslint": "^6.6.0",
"eslint-plugin-no-unsanitized": "^2.0.0", "eslint-plugin-no-unsanitized": "^2.0.0",
@ -19,14 +19,14 @@
"json": "^9.0.6", "json": "^9.0.6",
"mocha": "^6.2.2", "mocha": "^6.2.2",
"npm-run-all": "^4.0.0", "npm-run-all": "^4.0.0",
"nyc": "^14.1.1", "nyc": "^15.0.0",
"sinon": "^7.5.0", "sinon": "^7.5.0",
"sinon-chai": "^3.3.0", "sinon-chai": "^3.3.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.9.3", "web-ext": "^2.9.3",
"webextensions-jsdom": "^1.1.0" "webextensions-jsdom": "^1.2.1"
}, },
"homepage": "https://github.com/mozilla/multi-account-containers#readme", "homepage": "https://github.com/mozilla/multi-account-containers#readme",
"license": "MPL-2.0", "license": "MPL-2.0",
@ -44,7 +44,8 @@
"lint:js": "eslint .", "lint:js": "eslint .",
"package": "rm -rf src/web-ext-artifacts && npm run build && mv src/web-ext-artifacts/firefox_multi-account_containers-*.zip addon.xpi", "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 && npm run coverage", "test": "npm run lint && npm run coverage",
"test-watch": "mocha ./test/setup.js test/**/*.test.js --watch", "test:once": "mocha test/**/*.test.js",
"coverage": "nyc --reporter=html --reporter=text mocha ./test/setup.js test/**/*.test.js --timeout 60000" "test:watch": "npm run test:once -- --watch",
"coverage": "nyc --reporter=html --reporter=text mocha test/**/*.test.js --timeout 60000"
} }
} }

View file

@ -356,6 +356,35 @@ table {
transition: background-color 75ms; transition: background-color 75ms;
} }
.half-button-wrapper {
align-items: center;
display: flex;
flex-direction: row;
height: 44px;
inline-size: 100%;
}
.half-onboarding-button {
align-items: center;
background-color: #0996f8;
border-radius: 3px;
color: white;
display: flex;
flex: 1 0 auto;
font-size: 14px;
height: 44px;
inline-size: 50%;
justify-content: center;
margin-inline-end: 4px;
text-decoration: none;
transition: background-color 75ms;
}
.grey-button {
background-color: #e3e3e3;
color: #000;
}
.onboarding-button:hover, .onboarding-button:hover,
.onboarding-button:active { .onboarding-button:active {
background-color: #0675d3; background-color: #0675d3;

1
src/img/Account.svg Normal file
View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 160"><defs><style>.cls-1{fill:#6a57a5;}.cls-2{fill:#5a4a9e;}.cls-3{fill:#e7dfff;}</style></defs><title>account</title><path class="cls-1" d="M110,138.89A58.89,58.89,0,1,1,168.89,80,59,59,0,0,1,110,138.89Z"/><path class="cls-2" d="M110,130.27A50.27,50.27,0,1,1,160.27,80,50.33,50.33,0,0,1,110,130.27Z"/><circle class="cls-3" cx="110.39" cy="65.12" r="23.27" transform="translate(-12.01 27.1) rotate(-13.28)"/><path class="cls-3" d="M141.78,92.87c-8.2-9.46-19.58,3.28-31.39,3.28S87.2,83.41,79,92.87a7.83,7.83,0,0,0-.53,9.53,38.43,38.43,0,0,0,63.83,0A7.83,7.83,0,0,0,141.78,92.87Z"/></svg>

After

Width:  |  Height:  |  Size: 676 B

1
src/img/Sync.svg Normal file
View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 160"><defs><style>.cls-1{fill:#9f9fad;}.cls-2{fill:#5a4a9e;}.cls-3{fill:#6a57a5;}.cls-4{fill:#8f8f9d;}.cls-5{fill:none;stroke:#80808e;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.6px;}.cls-6{fill:#231f20;opacity:0.4;}.cls-7{fill:#ee3389;}.cls-8{fill:#7661aa;}</style></defs><title>Sync</title><path class="cls-1" d="M119.16,122.69v4.81H19.76v-4.81l12.83-3.21h72.15Z"/><rect class="cls-1" x="24.57" y="55.35" width="89.79" height="67.34" rx="3"/><path class="cls-2" d="M79.08,65l-49.7,49.7a1.61,1.61,0,0,0,1.6,1.61h77a1.62,1.62,0,0,0,1.61-1.61V65Z"/><polygon class="cls-3" points="29.38 64.97 29.38 114.67 79.08 64.97 29.38 64.97"/><path class="cls-2" d="M107.94,60.16H31a1.6,1.6,0,0,0-1.6,1.6V65h80.17V61.76A1.61,1.61,0,0,0,107.94,60.16Z"/><path class="cls-4" d="M108.74,121.09H30.18a.81.81,0,0,1,0-1.61h78.56a.81.81,0,1,1,0,1.61Z"/><line class="cls-5" x1="63.61" y1="124.18" x2="74.83" y2="124.18"/><path class="cls-6" d="M114.35,127.35H102.2V71.64a5.53,5.53,0,0,1,5.52-5.53h6.63Z"/><path class="cls-1" d="M200.24,134.72v4.81h-99.4v-4.81l12.82-3.21h72.15Z"/><rect class="cls-1" x="105.65" y="67.38" width="89.79" height="67.34" rx="3"/><path class="cls-2" d="M160.16,77l-49.71,49.7a1.61,1.61,0,0,0,1.61,1.6h77a1.6,1.6,0,0,0,1.6-1.6V77Z"/><polygon class="cls-3" points="110.45 77 110.45 126.7 160.16 77 110.45 77"/><path class="cls-2" d="M189,72.19h-77a1.61,1.61,0,0,0-1.61,1.6V77h80.17V73.79A1.6,1.6,0,0,0,189,72.19Z"/><path class="cls-4" d="M189.82,133.11H111.26a.8.8,0,1,1,0-1.6h78.56a.8.8,0,0,1,0,1.6Z"/><line class="cls-5" x1="144.69" y1="136.2" x2="155.91" y2="136.2"/><path class="cls-7" d="M136.85,50l-3-.55a3,3,0,0,0-3.51,2.37l-.27,1.45c-1.59,8.36-9.86,14.42-19.66,14.42a21,21,0,0,1-15.93-6.89H103a3,3,0,0,0,3-3v-3a3,3,0,0,0-3-3H84.86a3,3,0,0,0-3,3V73.64a3,3,0,0,0,3,3h3a3,3,0,0,0,3-3V69.72a30.8,30.8,0,0,0,19.57,6.87c14.15,0,26.15-9.11,28.54-21.66l.27-1.45A2.94,2.94,0,0,0,136.85,50Z"/><path class="cls-8" d="M84.06,47l3,.54a3.41,3.41,0,0,0,.55,0,3,3,0,0,0,3-2.41l.27-1.45h0c1.59-8.36,9.86-14.42,19.65-14.42a21,21,0,0,1,15.94,6.89H117.9a3,3,0,0,0-3,3v3a3,3,0,0,0,3,3h18.15a3,3,0,0,0,3-3V23.43a3,3,0,0,0-3-3h-3a3,3,0,0,0-3,3v3.92a30.82,30.82,0,0,0-19.58-6.88c-14.14,0-26.14,9.11-28.53,21.67l-.27,1.45A3,3,0,0,0,84.06,47Z"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -7,6 +7,7 @@ module.exports = {
"badge": true, "badge": true,
"backgroundLogic": true, "backgroundLogic": true,
"identityState": true, "identityState": true,
"messageHandler": true "messageHandler": true,
"sync": true
} }
}; };

View file

@ -1,4 +1,4 @@
const assignManager = { window.assignManager = {
MENU_ASSIGN_ID: "open-in-this-container", MENU_ASSIGN_ID: "open-in-this-container",
MENU_REMOVE_ID: "remove-open-in-this-container", MENU_REMOVE_ID: "remove-open-in-this-container",
MENU_SEPARATOR_ID: "separator", MENU_SEPARATOR_ID: "separator",
@ -9,8 +9,9 @@ const assignManager = {
area: browser.storage.local, area: browser.storage.local,
exemptedTabs: {}, exemptedTabs: {},
getSiteStoreKey(pageUrl) { getSiteStoreKey(pageUrlorUrlKey) {
const url = new window.URL(pageUrl); if (pageUrlorUrlKey.includes("siteContainerMap@@_")) return pageUrlorUrlKey;
const url = new window.URL(pageUrlorUrlKey);
const storagePrefix = "siteContainerMap@@_"; const storagePrefix = "siteContainerMap@@_";
if (url.port === "80" || url.port === "443") { if (url.port === "80" || url.port === "443") {
return `${storagePrefix}${url.hostname}`; return `${storagePrefix}${url.hostname}`;
@ -19,29 +20,38 @@ const assignManager = {
} }
}, },
setExempted(pageUrl, tabId) { setExempted(pageUrlorUrlKey, tabId) {
const siteStoreKey = this.getSiteStoreKey(pageUrl); const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
if (!(siteStoreKey in this.exemptedTabs)) { if (!(siteStoreKey in this.exemptedTabs)) {
this.exemptedTabs[siteStoreKey] = []; this.exemptedTabs[siteStoreKey] = [];
} }
this.exemptedTabs[siteStoreKey].push(tabId); this.exemptedTabs[siteStoreKey].push(tabId);
}, },
removeExempted(pageUrl) { removeExempted(pageUrlorUrlKey) {
const siteStoreKey = this.getSiteStoreKey(pageUrl); const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
this.exemptedTabs[siteStoreKey] = []; this.exemptedTabs[siteStoreKey] = [];
}, },
isExempted(pageUrl, tabId) { isExempted(pageUrlorUrlKey, tabId) {
const siteStoreKey = this.getSiteStoreKey(pageUrl); const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
if (!(siteStoreKey in this.exemptedTabs)) { if (!(siteStoreKey in this.exemptedTabs)) {
return false; return false;
} }
return this.exemptedTabs[siteStoreKey].includes(tabId); return this.exemptedTabs[siteStoreKey].includes(tabId);
}, },
get(pageUrl) { get(pageUrlorUrlKey) {
const siteStoreKey = this.getSiteStoreKey(pageUrl); const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
return this.getByUrlKey(siteStoreKey);
},
async getSyncEnabled() {
const { syncEnabled } = await browser.storage.local.get("syncEnabled");
return !!syncEnabled;
},
getByUrlKey(siteStoreKey) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.area.get([siteStoreKey]).then((storageResponse) => { this.area.get([siteStoreKey]).then((storageResponse) => {
if (storageResponse && siteStoreKey in storageResponse) { if (storageResponse && siteStoreKey in storageResponse) {
@ -54,51 +64,103 @@ const assignManager = {
}); });
}, },
set(pageUrl, data, exemptedTabIds) { async set(pageUrlorUrlKey, data, exemptedTabIds, backup = true) {
const siteStoreKey = this.getSiteStoreKey(pageUrl); const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
if (exemptedTabIds) { if (exemptedTabIds) {
exemptedTabIds.forEach((tabId) => { exemptedTabIds.forEach((tabId) => {
this.setExempted(pageUrl, tabId); this.setExempted(pageUrlorUrlKey, tabId);
}); });
} }
return this.area.set({ // eslint-disable-next-line require-atomic-updates
data.identityMacAddonUUID =
await identityState.lookupMACaddonUUID(data.userContextId);
await this.area.set({
[siteStoreKey]: data [siteStoreKey]: data
}); });
const syncEnabled = await this.getSyncEnabled();
if (backup && syncEnabled) {
await sync.storageArea.backup({undeleteSiteStoreKey: siteStoreKey});
}
return;
}, },
remove(pageUrl) { async remove(pageUrlorUrlKey) {
const siteStoreKey = this.getSiteStoreKey(pageUrl); const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
// When we remove an assignment we should clear all the exemptions // When we remove an assignment we should clear all the exemptions
this.removeExempted(pageUrl); this.removeExempted(pageUrlorUrlKey);
return this.area.remove([siteStoreKey]); await this.area.remove([siteStoreKey]);
const syncEnabled = await this.getSyncEnabled();
if (syncEnabled) await sync.storageArea.backup({siteStoreKey});
return;
}, },
async deleteContainer(userContextId) { async deleteContainer(userContextId) {
const sitesByContainer = await this.getByContainer(userContextId); const sitesByContainer = await this.getAssignedSites(userContextId);
this.area.remove(Object.keys(sitesByContainer)); this.area.remove(Object.keys(sitesByContainer));
}, },
async getByContainer(userContextId) { async getAssignedSites(userContextId = null) {
const sites = {}; const sites = {};
const siteConfigs = await this.area.get(); const siteConfigs = await this.area.get();
Object.keys(siteConfigs).forEach((key) => { for(const urlKey of Object.keys(siteConfigs)) {
// For some reason this is stored as string... lets check them both as that if (urlKey.includes("siteContainerMap@@_")) {
if (String(siteConfigs[key].userContextId) === String(userContextId)) { // For some reason this is stored as string... lets check
const site = siteConfigs[key]; // them both as that
if (!!userContextId &&
String(siteConfigs[urlKey].userContextId)
!== String(userContextId)) {
continue;
}
const site = siteConfigs[urlKey];
// In hindsight we should have stored this // In hindsight we should have stored this
// TODO file a follow up to clean the storage onLoad // TODO file a follow up to clean the storage onLoad
site.hostname = key.replace(/^siteContainerMap@@_/, ""); site.hostname = urlKey.replace(/^siteContainerMap@@_/, "");
sites[key] = site; sites[urlKey] = site;
} }
}); }
return sites; return sites;
},
/*
* Looks for abandoned site assignments. If there is no identity with
* the site assignment's userContextId (cookieStoreId), then the assignment
* is removed.
*/
async upgradeData() {
const identitiesList = await browser.contextualIdentities.query({});
const macConfigs = await this.area.get();
for(const configKey of Object.keys(macConfigs)) {
if (configKey.includes("siteContainerMap@@_")) {
const cookieStoreId =
"firefox-container-" + macConfigs[configKey].userContextId;
const match = identitiesList.find(
localIdentity => localIdentity.cookieStoreId === cookieStoreId
);
if (!match) {
await this.remove(configKey);
continue;
}
const updatedSiteAssignment = macConfigs[configKey];
updatedSiteAssignment.identityMacAddonUUID =
await identityState.lookupMACaddonUUID(match.cookieStoreId);
await this.set(
configKey,
updatedSiteAssignment,
false,
false
);
}
}
} }
}, },
_neverAsk(m) { _neverAsk(m) {
const pageUrl = m.pageUrl; const pageUrl = m.pageUrl;
if (m.neverAsk === true) { if (m.neverAsk === true) {
// If we have existing data and for some reason it hasn't been deleted etc lets update it // If we have existing data and for some reason it hasn't been
// deleted etc lets update it
this.storageArea.get(pageUrl).then((siteSettings) => { this.storageArea.get(pageUrl).then((siteSettings) => {
if (siteSettings) { if (siteSettings) {
siteSettings.neverAsk = true; siteSettings.neverAsk = true;
@ -113,11 +175,12 @@ const assignManager = {
// We return here so the confirm page can load the tab when exempted // We return here so the confirm page can load the tab when exempted
async _exemptTab(m) { async _exemptTab(m) {
const pageUrl = m.pageUrl; const pageUrl = m.pageUrl;
this.storageArea.setExempted(pageUrl, m.tabId); await this.storageArea.setExempted(pageUrl, m.tabId);
return true; return true;
}, },
// 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
async onBeforeRequest(options) { async onBeforeRequest(options) {
if (options.frameId !== 0 || options.tabId === -1) { if (options.frameId !== 0 || options.tabId === -1) {
return {}; return {};
@ -129,13 +192,14 @@ const assignManager = {
]); ]);
let container; let container;
try { try {
container = await browser.contextualIdentities.get(backgroundLogic.cookieStoreId(siteSettings.userContextId)); container = await browser.contextualIdentities
.get(backgroundLogic.cookieStoreId(siteSettings.userContextId));
} catch (e) { } catch (e) {
container = false; container = false;
} }
// 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
// then continue the existing load // more so lets remove it then continue the existing load
if (siteSettings && !container) { if (siteSettings && !container) {
this.deleteContainer(siteSettings.userContextId); this.deleteContainer(siteSettings.userContextId);
return {}; return {};
@ -152,7 +216,8 @@ const assignManager = {
const openTabId = removeTab ? tab.openerTabId : tab.id; const openTabId = removeTab ? tab.openerTabId : tab.id;
if (!this.canceledRequests[tab.id]) { if (!this.canceledRequests[tab.id]) {
// we decided to cancel the request at this point, register canceled request // we decided to cancel the request at this point, register
// canceled request
this.canceledRequests[tab.id] = { this.canceledRequests[tab.id] = {
requestIds: { requestIds: {
[options.requestId]: true [options.requestId]: true
@ -162,8 +227,10 @@ const assignManager = {
} }
}; };
// since webRequest onCompleted and onErrorOccurred are not 100% reliable (see #1120) // since webRequest onCompleted and onErrorOccurred are not 100%
// we register a timer here to cleanup canceled requests, just to make sure we don't // 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 // end up in a situation where certain urls in a tab.id stay canceled
setTimeout(() => { setTimeout(() => {
if (this.canceledRequests[tab.id]) { if (this.canceledRequests[tab.id]) {
@ -175,10 +242,12 @@ const assignManager = {
if (this.canceledRequests[tab.id].requestIds[options.requestId] || if (this.canceledRequests[tab.id].requestIds[options.requestId] ||
this.canceledRequests[tab.id].urls[options.url]) { this.canceledRequests[tab.id].urls[options.url]) {
// same requestId or url from the same tab // same requestId or url from the same tab
// this is a redirect that we have to cancel early to prevent opening two tabs // this is a redirect that we have to cancel early to prevent
// opening two tabs
cancelEarly = true; cancelEarly = true;
} }
// we decided to cancel the request at this point, register canceled request // we decided to cancel the request at this point, register canceled
// request
this.canceledRequests[tab.id].requestIds[options.requestId] = true; this.canceledRequests[tab.id].requestIds[options.requestId] = true;
this.canceledRequests[tab.id].urls[options.url] = true; this.canceledRequests[tab.id].urls[options.url] = true;
if (cancelEarly) { if (cancelEarly) {
@ -200,15 +269,27 @@ const assignManager = {
this.calculateContextMenu(tab); this.calculateContextMenu(tab);
/* Removal of existing tabs: /* Removal of existing tabs:
We aim to open the new assigned container tab / warning prompt in it's own tab: We aim to open the new assigned container tab / warning prompt in
- As the history won't span from one container to another it seems most sane to not try and reopen a tab on history.back() it's own tab:
- When users open a new tab themselves we want to make sure we don't end up with three tabs as per: https://github.com/mozilla/testpilot-containers/issues/421 - As the history won't span from one container to another it
If we are coming from an internal url that are used for the new tab page (NEW_TAB_PAGES), we can safely close as user is unlikely losing history seems most sane to not try and reopen a tab on history.back()
Detecting redirects on "new tab" opening actions is pretty hard as we don't get tab history: - When users open a new tab themselves we want to make sure we
- Redirects happen from Short URLs and tracking links that act as a gateway don't end up with three tabs as per:
- Extensions don't provide a way to history crawl for tabs, we could inject content scripts to do this https://github.com/mozilla/testpilot-containers/issues/421
however they don't run on about:blank so this would likely be just as hacky. If we are coming from an internal url that are used for the new
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. tab page (NEW_TAB_PAGES), we can safely close as user is unlikely
losing history
Detecting redirects on "new tab" opening actions is pretty hard
as we don't get tab history:
- Redirects happen from Short URLs and tracking links that act as
a gateway
- Extensions don't provide a way to history crawl for tabs, we
could inject content scripts to do this
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 (removeTab) { if (removeTab) {
browser.tabs.remove(tab.id); browser.tabs.remove(tab.id);
@ -220,10 +301,13 @@ const assignManager = {
init() { init() {
browser.contextMenus.onClicked.addListener((info, tab) => { browser.contextMenus.onClicked.addListener((info, tab) => {
info.bookmarkId ? this._onClickedBookmark(info) : this._onClickedHandler(info, tab); info.bookmarkId ?
this._onClickedBookmark(info) :
this._onClickedHandler(info, tab);
}); });
// 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 = {}; this.canceledRequests = {};
browser.webRequest.onBeforeRequest.addListener((options) => { browser.webRequest.onBeforeRequest.addListener((options) => {
return this.onBeforeRequest(options); return this.onBeforeRequest(options);
@ -240,25 +324,34 @@ const assignManager = {
delete this.canceledRequests[options.tabId]; delete this.canceledRequests[options.tabId];
} }
},{urls: ["<all_urls>"], types: ["main_frame"]}); },{urls: ["<all_urls>"], types: ["main_frame"]});
this.resetBookmarksMenuItem(); this.resetBookmarksMenuItem();
}, },
async resetBookmarksMenuItem() { async resetBookmarksMenuItem() {
const hasPermission = await browser.permissions.contains({permissions: ["bookmarks"]}); const hasPermission = await browser.permissions.contains({
permissions: ["bookmarks"]
});
if (this.hadBookmark === hasPermission) { if (this.hadBookmark === hasPermission) {
return; return;
} }
this.hadBookmark = hasPermission; this.hadBookmark = hasPermission;
if (hasPermission) { if (hasPermission) {
this.initBookmarksMenu(); this.initBookmarksMenu();
browser.contextualIdentities.onCreated.addListener(this.contextualIdentityCreated); browser.contextualIdentities.onCreated
browser.contextualIdentities.onUpdated.addListener(this.contextualIdentityUpdated); .addListener(this.contextualIdentityCreated);
browser.contextualIdentities.onRemoved.addListener(this.contextualIdentityRemoved); browser.contextualIdentities.onUpdated
.addListener(this.contextualIdentityUpdated);
browser.contextualIdentities.onRemoved
.addListener(this.contextualIdentityRemoved);
} else { } else {
this.removeBookmarksMenu(); this.removeBookmarksMenu();
browser.contextualIdentities.onCreated.removeListener(this.contextualIdentityCreated); browser.contextualIdentities.onCreated
browser.contextualIdentities.onUpdated.removeListener(this.contextualIdentityUpdated); .removeListener(this.contextualIdentityCreated);
browser.contextualIdentities.onRemoved.removeListener(this.contextualIdentityRemoved); browser.contextualIdentities.onUpdated
.removeListener(this.contextualIdentityUpdated);
browser.contextualIdentities.onRemoved
.removeListener(this.contextualIdentityRemoved);
} }
}, },
@ -267,19 +360,25 @@ const assignManager = {
parentId: assignManager.OPEN_IN_CONTAINER, parentId: assignManager.OPEN_IN_CONTAINER,
id: changeInfo.contextualIdentity.cookieStoreId, id: changeInfo.contextualIdentity.cookieStoreId,
title: changeInfo.contextualIdentity.name, title: changeInfo.contextualIdentity.name,
icons: { "16": `img/usercontext.svg#${changeInfo.contextualIdentity.icon}` } icons: { "16": `img/usercontext.svg#${
changeInfo.contextualIdentity.icon
}` }
}); });
}, },
contextualIdentityUpdated(changeInfo) { contextualIdentityUpdated(changeInfo) {
browser.contextMenus.update(changeInfo.contextualIdentity.cookieStoreId, { browser.contextMenus.update(
title: changeInfo.contextualIdentity.name, changeInfo.contextualIdentity.cookieStoreId, {
icons: { "16": `img/usercontext.svg#${changeInfo.contextualIdentity.icon}` } title: changeInfo.contextualIdentity.name,
}); icons: { "16": `img/usercontext.svg#${
changeInfo.contextualIdentity.icon}` }
});
}, },
contextualIdentityRemoved(changeInfo) { contextualIdentityRemoved(changeInfo) {
browser.contextMenus.remove(changeInfo.contextualIdentity.cookieStoreId); browser.contextMenus.remove(
changeInfo.contextualIdentity.cookieStoreId
);
}, },
async _onClickedHandler(info, tab) { async _onClickedHandler(info, tab) {
@ -295,7 +394,9 @@ const assignManager = {
} else { } else {
remove = true; remove = true;
} }
await this._setOrRemoveAssignment(tab.id, info.pageUrl, userContextId, remove); await this._setOrRemoveAssignment(
tab.id, info.pageUrl, userContextId, remove
);
break; break;
case this.MENU_MOVE_ID: case this.MENU_MOVE_ID:
backgroundLogic.moveTabsToWindow({ backgroundLogic.moveTabsToWindow({
@ -316,17 +417,20 @@ const assignManager = {
async _onClickedBookmark(info) { async _onClickedBookmark(info) {
async function _getBookmarksFromInfo(info) { async function _getBookmarksFromInfo(info) {
const [bookmarkTreeNode] = await browser.bookmarks.get(info.bookmarkId); const [bookmarkTreeNode] =
await browser.bookmarks.get(info.bookmarkId);
if (bookmarkTreeNode.type === "folder") { if (bookmarkTreeNode.type === "folder") {
return await browser.bookmarks.getChildren(bookmarkTreeNode.id); return browser.bookmarks.getChildren(bookmarkTreeNode.id);
} }
return [bookmarkTreeNode]; return [bookmarkTreeNode];
} }
const bookmarks = await _getBookmarksFromInfo(info); const bookmarks = await _getBookmarksFromInfo(info);
for (const bookmark of bookmarks) { for (const bookmark of bookmarks) {
// Some checks on the urls from https://github.com/Rob--W/bookmark-container-tab/ thanks! // Some checks on the urls from
if ( !/^(javascript|place):/i.test(bookmark.url) && bookmark.type !== "folder") { // https://github.com/Rob--W/bookmark-container-tab/ thanks!
if ( !/^(javascript|place):/i.test(bookmark.url) &&
bookmark.type !== "folder") {
const openInReaderMode = bookmark.url.startsWith("about:reader"); const openInReaderMode = bookmark.url.startsWith("about:reader");
if(openInReaderMode) { if(openInReaderMode) {
try { try {
@ -354,7 +458,9 @@ const assignManager = {
if (!("cookieStoreId" in tab)) { if (!("cookieStoreId" in tab)) {
return false; return false;
} }
return backgroundLogic.getUserContextIdFromCookieStoreId(tab.cookieStoreId); return backgroundLogic.getUserContextIdFromCookieStoreId(
tab.cookieStoreId
);
}, },
isTabPermittedAssign(tab) { isTabPermittedAssign(tab) {
@ -411,13 +517,13 @@ const assignManager = {
// Ensure we have a cookieStore to assign to // Ensure we have a cookieStore to assign to
if (cookieStore if (cookieStore
&& this.isTabPermittedAssign(tab)) { && this.isTabPermittedAssign(tab)) {
return await this.storageArea.get(tab.url); return this.storageArea.get(tab.url);
} }
return false; return false;
}, },
_getByContainer(userContextId) { _getByContainer(userContextId) {
return this.storageArea.getByContainer(userContextId); return this.storageArea.getAssignedSites(userContextId);
}, },
removeContextMenu() { removeContextMenu() {
@ -536,7 +642,7 @@ const assignManager = {
for (const identity of identities) { for (const identity of identities) {
browser.contextMenus.remove(identity.cookieStoreId); browser.contextMenus.remove(identity.cookieStoreId);
} }
} },
}; };
assignManager.init(); assignManager.init();

View file

@ -324,7 +324,7 @@ const backgroundLogic = {
containerState.hiddenTabs = []; containerState.hiddenTabs = [];
await Promise.all(promises); await Promise.all(promises);
return await identityState.storageArea.set(options.cookieStoreId, containerState); return identityState.storageArea.set(options.cookieStoreId, containerState);
}, },
cookieStoreId(userContextId) { cookieStoreId(userContextId) {

View file

@ -1,4 +1,4 @@
const MAJOR_VERSIONS = ["2.3.0", "2.4.0"]; const MAJOR_VERSIONS = ["2.3.0", "2.4.0", "6.2.0"];
const badge = { const badge = {
async init() { async init() {
const currentWindow = await browser.windows.getCurrent(); const currentWindow = await browser.windows.getCurrent();

View file

@ -1,4 +1,4 @@
const identityState = { window.identityState = {
storageArea: { storageArea: {
area: browser.storage.local, area: browser.storage.local,
@ -11,12 +11,23 @@ const identityState = {
const storeKey = this.getContainerStoreKey(cookieStoreId); const storeKey = this.getContainerStoreKey(cookieStoreId);
const storageResponse = await this.area.get([storeKey]); const storageResponse = await this.area.get([storeKey]);
if (storageResponse && storeKey in storageResponse) { if (storageResponse && storeKey in storageResponse) {
if (!storageResponse[storeKey].macAddonUUID){
storageResponse[storeKey].macAddonUUID = uuidv4();
await this.set(cookieStoreId, storageResponse[storeKey]);
}
return storageResponse[storeKey]; return storageResponse[storeKey];
} }
const defaultContainerState = identityState._createIdentityState(); // If local storage doesn't have an entry, look it up to make sure it's
await this.set(cookieStoreId, defaultContainerState); // an in-use identity.
const identities = await browser.contextualIdentities.query({});
return defaultContainerState; const match = identities.find(
(identity) => identity.cookieStoreId === cookieStoreId);
if (match) {
const defaultContainerState = identityState._createIdentityState();
await this.set(cookieStoreId, defaultContainerState);
return defaultContainerState;
}
throw new Error (`${cookieStoreId} not found`);
}, },
set(cookieStoreId, data) { set(cookieStoreId, data) {
@ -26,9 +37,41 @@ const identityState = {
}); });
}, },
remove(cookieStoreId) { async remove(cookieStoreId) {
const storeKey = this.getContainerStoreKey(cookieStoreId); const storeKey = this.getContainerStoreKey(cookieStoreId);
return this.area.remove([storeKey]); return this.area.remove([storeKey]);
},
/*
* Looks for abandoned identity keys in local storage, and makes sure all
* identities registered in the browser are also in local storage. (this
* appears to not always be the case based on how this.get() is written)
*/
async upgradeData() {
const identitiesList = await browser.contextualIdentities.query({});
for (const identity of identitiesList) {
// ensure all identities have an entry in local storage
await identityState.addUUID(identity.cookieStoreId);
}
const macConfigs = await this.area.get();
for(const configKey of Object.keys(macConfigs)) {
if (configKey.includes("identitiesState@@_")) {
const cookieStoreId = String(configKey).replace(/^identitiesState@@_/, "");
const match = identitiesList.find(
localIdentity => localIdentity.cookieStoreId === cookieStoreId
);
if (cookieStoreId === "firefox-default") continue;
if (!match) {
await this.remove(cookieStoreId);
continue;
}
if (!macConfigs[configKey].macAddonUUID) {
await identityState.storageArea.get(cookieStoreId);
}
}
}
} }
}, },
@ -36,6 +79,16 @@ const identityState = {
return Object.assign({}, tab); return Object.assign({}, tab);
}, },
async getCookieStoreIDuuidMap() {
const containers = {};
const identities = await browser.contextualIdentities.query({});
for(const identity of identities) {
const containerInfo = await this.storageArea.get(identity.cookieStoreId);
containers[identity.cookieStoreId] = containerInfo.macAddonUUID;
}
return containers;
},
async storeHidden(cookieStoreId, windowId) { async storeHidden(cookieStoreId, windowId) {
const containerState = await this.storageArea.get(cookieStoreId); const containerState = await this.storageArea.get(cookieStoreId);
const tabsByContainer = await browser.tabs.query({cookieStoreId, windowId}); const tabsByContainer = await browser.tabs.query({cookieStoreId, windowId});
@ -54,9 +107,57 @@ const identityState = {
return this.storageArea.set(cookieStoreId, containerState); return this.storageArea.set(cookieStoreId, containerState);
}, },
async updateUUID(cookieStoreId, uuid) {
if (!cookieStoreId || !uuid) {
throw new Error ("cookieStoreId or uuid missing");
}
const containerState = await this.storageArea.get(cookieStoreId);
containerState.macAddonUUID = uuid;
await this.storageArea.set(cookieStoreId, containerState);
return uuid;
},
async addUUID(cookieStoreId) {
await this.storageArea.get(cookieStoreId);
},
async lookupMACaddonUUID(cookieStoreId) {
// This stays a lookup, because if the cookieStoreId doesn't
// exist, this.get() will create it, which is not what we want.
const cookieStoreIdKey = cookieStoreId.includes("firefox-container-") ?
cookieStoreId : "firefox-container-" + cookieStoreId;
const macConfigs = await this.storageArea.area.get();
for(const configKey of Object.keys(macConfigs)) {
if (configKey === this.storageArea.getContainerStoreKey(cookieStoreIdKey)) {
return macConfigs[configKey].macAddonUUID;
}
}
return false;
},
async lookupCookieStoreId(macAddonUUID) {
const macConfigs = await this.storageArea.area.get();
for(const configKey of Object.keys(macConfigs)) {
if (configKey.includes("identitiesState@@_")) {
if(macConfigs[configKey].macAddonUUID === macAddonUUID) {
return String(configKey).replace(/^identitiesState@@_/, "");
}
}
}
return false;
},
_createIdentityState() { _createIdentityState() {
return { return {
hiddenTabs: [] hiddenTabs: [],
macAddonUUID: uuidv4()
}; };
}, },
}; };
function uuidv4() {
// https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}

View file

@ -18,5 +18,6 @@
<script type="text/javascript" src="badge.js"></script> <script type="text/javascript" src="badge.js"></script>
<script type="text/javascript" src="identityState.js"></script> <script type="text/javascript" src="identityState.js"></script>
<script type="text/javascript" src="messageHandler.js"></script> <script type="text/javascript" src="messageHandler.js"></script>
<script type="text/javascript" src="sync.js"></script>
</body> </body>
</html> </html>

View file

@ -10,6 +10,9 @@ const messageHandler = {
let response; let response;
switch (m.method) { switch (m.method) {
case "resetSync":
response = sync.resetSync();
break;
case "resetBookmarksContext": case "resetBookmarksContext":
response = assignManager.resetBookmarksMenuItem(); response = assignManager.resetBookmarksMenuItem();
break; break;

580
src/js/background/sync.js Normal file
View file

@ -0,0 +1,580 @@
const SYNC_DEBUG = false;
const sync = {
storageArea: {
area: browser.storage.sync,
async get(){
return this.area.get();
},
async set(options) {
return this.area.set(options);
},
async deleteIdentity(deletedIdentityUUID) {
const deletedIdentityList =
await sync.storageArea.getDeletedIdentityList();
if (
! deletedIdentityList.find(element => element === deletedIdentityUUID)
) {
deletedIdentityList.push(deletedIdentityUUID);
await sync.storageArea.set({ deletedIdentityList });
}
await this.removeIdentityKeyFromSync(deletedIdentityUUID);
},
async removeIdentityKeyFromSync(deletedIdentityUUID) {
await sync.storageArea.area.remove( "identity@@_" + deletedIdentityUUID);
},
async deleteSite(siteStoreKey) {
const deletedSiteList =
await sync.storageArea.getDeletedSiteList();
if (deletedSiteList.find(element => element === siteStoreKey)) return;
deletedSiteList.push(siteStoreKey);
await sync.storageArea.set({ deletedSiteList });
await sync.storageArea.area.remove(siteStoreKey);
},
async getDeletedIdentityList() {
const storedArray = await this.getStoredItem("deletedIdentityList");
return storedArray || [];
},
async getIdentities() {
const allSyncStorage = await this.get();
const identities = [];
for (const storageKey of Object.keys(allSyncStorage)) {
if (storageKey.includes("identity@@_")) {
identities.push(allSyncStorage[storageKey]);
}
}
return identities;
},
async getDeletedSiteList() {
const storedArray = await this.getStoredItem("deletedSiteList");
return (storedArray) ? storedArray : [];
},
async getAssignedSites() {
const allSyncStorage = await this.get();
const sites = {};
for (const storageKey of Object.keys(allSyncStorage)) {
if (storageKey.includes("siteContainerMap@@_")) {
sites[storageKey] = allSyncStorage[storageKey];
}
}
return sites;
},
async getStoredItem(objectKey) {
const outputObject = await this.get(objectKey);
if (outputObject && outputObject[objectKey])
return outputObject[objectKey];
return false;
},
async getAllInstanceInfo() {
const instanceList = {};
const allSyncInfo = await this.get();
for (const objectKey of Object.keys(allSyncInfo)) {
if (objectKey.includes("MACinstance")) {
instanceList[objectKey] = allSyncInfo[objectKey]; }
}
return instanceList;
},
getInstanceKey() {
return browser.runtime.getURL("")
.replace(/moz-extension:\/\//, "MACinstance:")
.replace(/\//, "");
},
async removeInstance(installUUID) {
if (SYNC_DEBUG) console.log("removing", installUUID);
await this.area.remove(installUUID);
return;
},
async removeThisInstanceFromSync() {
const installUUID = this.getInstanceKey();
await this.removeInstance(installUUID);
return;
},
async hasSyncStorage(){
const inSync = await this.get();
return !(Object.entries(inSync).length === 0);
},
async backup(options) {
// remove listeners to avoid an infinite loop!
await sync.checkForListenersMaybeRemove();
const identities = await updateSyncIdentities();
const siteAssignments = await updateSyncSiteAssignments();
await updateInstanceInfo(identities, siteAssignments);
if (options && options.uuid)
await this.deleteIdentity(options.uuid);
if (options && options.undeleteUUID)
await removeFromDeletedIdentityList(options.undeleteUUID);
if (options && options.siteStoreKey)
await this.deleteSite(options.siteStoreKey);
if (options && options.undeleteSiteStoreKey)
await removeFromDeletedSitesList(options.undeleteSiteStoreKey);
if (SYNC_DEBUG) console.log("Backed up!");
await sync.checkForListenersMaybeAdd();
async function updateSyncIdentities() {
const identities = await browser.contextualIdentities.query({});
for (const identity of identities) {
delete identity.colorCode;
delete identity.iconUrl;
identity.macAddonUUID = await identityState.lookupMACaddonUUID(identity.cookieStoreId);
if(identity.macAddonUUID) {
const storageKey = "identity@@_" + identity.macAddonUUID;
await sync.storageArea.set({ [storageKey]: identity });
}
}
//await sync.storageArea.set({ identities });
return identities;
}
async function updateSyncSiteAssignments() {
const assignedSites =
await assignManager.storageArea.getAssignedSites();
for (const siteKey of Object.keys(assignedSites)) {
await sync.storageArea.set({ [siteKey]: assignedSites[siteKey] });
}
return assignedSites;
}
async function updateInstanceInfo(identitiesInput, siteAssignmentsInput) {
const date = new Date();
const timestamp = date.getTime();
const installUUID = sync.storageArea.getInstanceKey();
if (SYNC_DEBUG) console.log("adding", installUUID);
const identities = [];
const siteAssignments = [];
for (const identity of identitiesInput) {
identities.push(identity.macAddonUUID);
}
for (const siteAssignmentKey of Object.keys(siteAssignmentsInput)) {
siteAssignments.push(siteAssignmentKey);
}
await sync.storageArea.set({ [installUUID]: { timestamp, identities, siteAssignments } });
}
async function removeFromDeletedIdentityList(identityUUID) {
const deletedIdentityList =
await sync.storageArea.getDeletedIdentityList();
const newDeletedIdentityList = deletedIdentityList
.filter(element => element !== identityUUID);
await sync.storageArea.set({ deletedIdentityList: newDeletedIdentityList });
}
async function removeFromDeletedSitesList(siteStoreKey) {
const deletedSiteList =
await sync.storageArea.getDeletedSiteList();
const newDeletedSiteList = deletedSiteList
.filter(element => element !== siteStoreKey);
await sync.storageArea.set({ deletedSiteList: newDeletedSiteList });
}
},
onChangedListener(changes, areaName) {
if (areaName === "sync") sync.errorHandledRunSync();
},
async addToDeletedList(changeInfo) {
const identity = changeInfo.contextualIdentity;
const deletedUUID =
await identityState.lookupMACaddonUUID(identity.cookieStoreId);
await identityState.storageArea.remove(identity.cookieStoreId);
sync.storageArea.backup({uuid: deletedUUID});
}
},
async init() {
const syncEnabled = await assignManager.storageArea.getSyncEnabled();
if (syncEnabled) {
// Add listener to sync storage and containers.
// Works for all installs that have any sync storage.
// Waits for sync storage change before kicking off the restore/backup
// initial sync must be kicked off by user.
this.checkForListenersMaybeAdd();
return;
}
this.checkForListenersMaybeRemove();
},
async errorHandledRunSync () {
await sync.runSync().catch( async (error)=> {
if (SYNC_DEBUG) console.error("Error from runSync", error);
await sync.checkForListenersMaybeAdd();
});
},
async checkForListenersMaybeAdd() {
const hasStorageListener =
await browser.storage.onChanged.hasListener(
sync.storageArea.onChangedListener
);
const hasCIListener = await sync.hasContextualIdentityListeners();
if (!hasCIListener) {
await sync.addContextualIdentityListeners();
}
if (!hasStorageListener) {
await browser.storage.onChanged.addListener(
sync.storageArea.onChangedListener);
}
},
async checkForListenersMaybeRemove() {
const hasStorageListener =
await browser.storage.onChanged.hasListener(
sync.storageArea.onChangedListener
);
const hasCIListener = await sync.hasContextualIdentityListeners();
if (hasCIListener) {
await sync.removeContextualIdentityListeners();
}
if (hasStorageListener) {
await browser.storage.onChanged.removeListener(
sync.storageArea.onChangedListener);
}
},
async runSync() {
if (SYNC_DEBUG) {
const syncInfo = await sync.storageArea.get();
const localInfo = await browser.storage.local.get();
const idents = await browser.contextualIdentities.query({});
console.log("Initial State:", {syncInfo, localInfo, idents});
}
await sync.checkForListenersMaybeRemove();
if (SYNC_DEBUG) console.log("runSync");
await identityState.storageArea.upgradeData();
await assignManager.storageArea.upgradeData();
const hasSyncStorage = await sync.storageArea.hasSyncStorage();
if (hasSyncStorage) await restore();
await sync.storageArea.backup();
await removeOldDeletedItems();
return;
},
async addContextualIdentityListeners() {
await browser.contextualIdentities.onCreated.addListener(sync.storageArea.backup);
await browser.contextualIdentities.onRemoved.addListener(sync.storageArea.addToDeletedList);
await browser.contextualIdentities.onUpdated.addListener(sync.storageArea.backup);
},
async removeContextualIdentityListeners() {
await browser.contextualIdentities.onCreated.removeListener(sync.storageArea.backup);
await browser.contextualIdentities.onRemoved.removeListener(sync.storageArea.addToDeletedList);
await browser.contextualIdentities.onUpdated.removeListener(sync.storageArea.backup);
},
async hasContextualIdentityListeners() {
return (
await browser.contextualIdentities.onCreated.hasListener(sync.storageArea.backup) &&
await browser.contextualIdentities.onRemoved.hasListener(sync.storageArea.addToDeletedList) &&
await browser.contextualIdentities.onUpdated.hasListener(sync.storageArea.backup)
);
},
async resetSync() {
const syncEnabled = await assignManager.storageArea.getSyncEnabled();
if (syncEnabled) {
this.errorHandledRunSync();
return;
}
await this.checkForListenersMaybeRemove();
await this.storageArea.removeThisInstanceFromSync();
}
};
// attaching to window for use in mocha tests
window.sync = sync;
sync.init();
async function restore() {
if (SYNC_DEBUG) console.log("restore");
await reconcileIdentities();
await reconcileSiteAssignments();
return;
}
/*
* Checks for the container name. If it exists, they are assumed to be the
* same container, and the color and icon are overwritten from sync, if
* different.
*/
async function reconcileIdentities(){
if (SYNC_DEBUG) console.log("reconcileIdentities");
// first delete any from the deleted list
const deletedIdentityList =
await sync.storageArea.getDeletedIdentityList();
// first remove any deleted identities
for (const deletedUUID of deletedIdentityList) {
const deletedCookieStoreId =
await identityState.lookupCookieStoreId(deletedUUID);
if (deletedCookieStoreId){
try{
await browser.contextualIdentities.remove(deletedCookieStoreId);
} catch (error) {
// if the identity we are deleting is not there, that's fine.
console.error("Error deleting contextualIdentity", deletedCookieStoreId);
continue;
}
}
}
const localIdentities = await browser.contextualIdentities.query({});
const syncIdentitiesRemoveDupes =
await sync.storageArea.getIdentities();
// find any local dupes created on sync storage and delete from sync storage
for (const localIdentity of localIdentities) {
const syncIdentitiesOfName = syncIdentitiesRemoveDupes
.filter(identity => identity.name === localIdentity.name);
if (syncIdentitiesOfName.length > 1) {
const identityMatchingContextId = syncIdentitiesOfName
.find(identity => identity.cookieStoreId === localIdentity.cookieStoreId);
if (identityMatchingContextId)
await sync.storageArea.removeIdentityKeyFromSync(identityMatchingContextId.macAddonUUID);
}
}
const syncIdentities =
await sync.storageArea.getIdentities();
// now compare all containers for matching names.
for (const syncIdentity of syncIdentities) {
if (syncIdentity.macAddonUUID){
const localMatch = localIdentities.find(
localIdentity => localIdentity.name === syncIdentity.name
);
if (!localMatch) {
// if there's no name match found, check on uuid,
const localCookieStoreID =
await identityState.lookupCookieStoreId(syncIdentity.macAddonUUID);
if (localCookieStoreID) {
await ifUUIDMatch(syncIdentity, localCookieStoreID);
continue;
}
await ifNoMatch(syncIdentity);
continue;
}
// Names match, so use the info from Sync
await updateIdentityWithSyncInfo(syncIdentity, localMatch);
continue;
}
// if no macAddonUUID, there is a problem with the sync info and it needs to be ignored.
}
await updateSiteAssignmentUUIDs();
async function updateSiteAssignmentUUIDs(){
const sites = assignManager.storageArea.getAssignedSites();
for (const siteKey of Object.keys(sites)) {
await assignManager.storageArea.set(siteKey, sites[siteKey]);
}
}
}
async function updateIdentityWithSyncInfo(syncIdentity, localMatch) {
// Sync is truth. if there is a match, compare data and update as needed
if (syncIdentity.color !== localMatch.color
|| syncIdentity.icon !== localMatch.icon) {
await browser.contextualIdentities.update(
localMatch.cookieStoreId, {
name: syncIdentity.name,
color: syncIdentity.color,
icon: syncIdentity.icon
});
if (SYNC_DEBUG) {
if (localMatch.color !== syncIdentity.color) {
console.log(localMatch.name, "Change color: ", syncIdentity.color);
}
if (localMatch.icon !== syncIdentity.icon) {
console.log(localMatch.name, "Change icon: ", syncIdentity.icon);
}
}
}
// Sync is truth. If all is the same, update the local uuid to match sync
if (localMatch.macAddonUUID !== syncIdentity.macAddonUUID) {
await identityState.updateUUID(
localMatch.cookieStoreId,
syncIdentity.macAddonUUID
);
}
// TODOkmw: update any site assignment UUIDs
}
async function ifUUIDMatch(syncIdentity, localCookieStoreID) {
// if there's an identical local uuid, it's the same container. Sync is truth
const identityInfo = {
name: syncIdentity.name,
color: syncIdentity.color,
icon: syncIdentity.icon
};
if (SYNC_DEBUG) {
try {
const getIdent =
await browser.contextualIdentities.get(localCookieStoreID);
if (getIdent.name !== identityInfo.name) {
console.log(getIdent.name, "Change name: ", identityInfo.name);
}
if (getIdent.color !== identityInfo.color) {
console.log(getIdent.name, "Change color: ", identityInfo.color);
}
if (getIdent.icon !== identityInfo.icon) {
console.log(getIdent.name, "Change icon: ", identityInfo.icon);
}
} catch (error) {
//if this fails, there is probably differing sync info.
console.error("Error getting info on CI", error);
}
}
try {
// update the local container with the sync data
await browser.contextualIdentities
.update(localCookieStoreID, identityInfo);
return;
} catch (error) {
// If this fails, sync info is off.
console.error("Error udpating CI", error);
}
}
async function ifNoMatch(syncIdentity){
// if no uuid match either, make new identity
if (SYNC_DEBUG) console.log("create new ident: ", syncIdentity.name);
const newIdentity =
await browser.contextualIdentities.create({
name: syncIdentity.name,
color: syncIdentity.color,
icon: syncIdentity.icon
});
await identityState.updateUUID(
newIdentity.cookieStoreId,
syncIdentity.macAddonUUID
);
return;
}
/*
* Checks for site previously assigned. If it exists, and has the same
* container assignment, the assignment is kept. If it exists, but has
* a different assignment, the user is prompted (not yet implemented).
* If it does not exist, it is created.
*/
async function reconcileSiteAssignments() {
if (SYNC_DEBUG) console.log("reconcileSiteAssignments");
const assignedSitesLocal =
await assignManager.storageArea.getAssignedSites();
const assignedSitesFromSync =
await sync.storageArea.getAssignedSites();
const deletedSiteList =
await sync.storageArea.getDeletedSiteList();
for(const siteStoreKey of deletedSiteList) {
if (Object.prototype.hasOwnProperty.call(assignedSitesLocal,siteStoreKey)) {
assignManager
.storageArea
.remove(siteStoreKey);
}
}
for(const urlKey of Object.keys(assignedSitesFromSync)) {
const assignedSite = assignedSitesFromSync[urlKey];
try{
if (assignedSite.identityMacAddonUUID) {
// Sync is truth.
// Not even looking it up. Just overwrite
if (SYNC_DEBUG){
const isInStorage = await assignManager.storageArea.getByUrlKey(urlKey);
if (!isInStorage)
console.log("new assignment ", assignedSite);
}
await setAssignmentWithUUID(assignedSite, urlKey);
continue;
}
} catch (error) {
// this is probably old or incorrect site info in Sync
// skip and move on.
}
}
}
const MILISECONDS_IN_THIRTY_DAYS = 2592000000;
async function removeOldDeletedItems() {
const instanceList = await sync.storageArea.getAllInstanceInfo();
const deletedSiteList = await sync.storageArea.getDeletedSiteList();
const deletedIdentityList = await sync.storageArea.getDeletedIdentityList();
for (const instanceKey of Object.keys(instanceList)) {
const date = new Date();
const currentTimestamp = date.getTime();
if (instanceList[instanceKey].timestamp < currentTimestamp - MILISECONDS_IN_THIRTY_DAYS) {
delete instanceList[instanceKey];
sync.storageArea.removeInstance(instanceKey);
continue;
}
}
for (const siteStoreKey of deletedSiteList) {
let hasMatch = false;
for (const instance of Object.values(instanceList)) {
const match = instance.siteAssignments.find(element => element === siteStoreKey);
if (!match) continue;
hasMatch = true;
}
if (!hasMatch) {
await sync.storageArea.backup({undeleteSiteStoreKey: siteStoreKey});
}
}
for (const identityUUID of deletedIdentityList) {
let hasMatch = false;
for (const instance of Object.values(instanceList)) {
const match = instance.identities.find(element => element === identityUUID);
if (!match) continue;
hasMatch = true;
}
if (!hasMatch) {
await sync.storageArea.backup({undeleteUUID: identityUUID});
}
}
}
async function setAssignmentWithUUID(assignedSite, urlKey) {
const uuid = assignedSite.identityMacAddonUUID;
const cookieStoreId = await identityState.lookupCookieStoreId(uuid);
if (cookieStoreId) {
// eslint-disable-next-line require-atomic-updates
assignedSite.userContextId = cookieStoreId
.replace(/^firefox-container-/, "");
await assignManager.storageArea.set(
urlKey,
assignedSite,
false,
false
);
return;
}
throw new Error (`No cookieStoreId found for: ${uuid}, ${urlKey}`);
}

View file

@ -1,4 +1,3 @@
async function requestPermissions() { async function requestPermissions() {
const checkbox = document.querySelector("#bookmarksPermissions"); const checkbox = document.querySelector("#bookmarksPermissions");
if (checkbox.checked) { if (checkbox.checked) {
@ -13,13 +12,30 @@ async function requestPermissions() {
browser.runtime.sendMessage({ method: "resetBookmarksContext" }); browser.runtime.sendMessage({ method: "resetBookmarksContext" });
} }
async function enableDisableSync() {
const checkbox = document.querySelector("#syncCheck");
if (checkbox.checked) {
await browser.storage.local.set({syncEnabled: true});
} else {
await browser.storage.local.set({syncEnabled: false});
}
browser.runtime.sendMessage({ method: "resetSync" });
}
async function restoreOptions() { async function restoreOptions() {
const hasPermission = await browser.permissions.contains({permissions: ["bookmarks"]}); const hasPermission = await browser.permissions.contains({permissions: ["bookmarks"]});
const { syncEnabled } = await browser.storage.local.get("syncEnabled");
if (hasPermission) { if (hasPermission) {
document.querySelector("#bookmarksPermissions").checked = true; document.querySelector("#bookmarksPermissions").checked = true;
} }
if (syncEnabled) {
document.querySelector("#syncCheck").checked = true;
} else {
document.querySelector("#syncCheck").checked = false;
}
} }
document.addEventListener("DOMContentLoaded", restoreOptions); document.addEventListener("DOMContentLoaded", restoreOptions);
document.querySelector("#bookmarksPermissions").addEventListener( "change", requestPermissions); document.querySelector("#bookmarksPermissions").addEventListener( "change", requestPermissions);
document.querySelector("#syncCheck").addEventListener( "change", enableDisableSync);

View file

@ -17,6 +17,8 @@ const P_ONBOARDING_2 = "onboarding2";
const P_ONBOARDING_3 = "onboarding3"; const P_ONBOARDING_3 = "onboarding3";
const P_ONBOARDING_4 = "onboarding4"; const P_ONBOARDING_4 = "onboarding4";
const P_ONBOARDING_5 = "onboarding5"; const P_ONBOARDING_5 = "onboarding5";
const P_ONBOARDING_6 = "onboarding6";
const P_ONBOARDING_7 = "onboarding7";
const P_CONTAINERS_LIST = "containersList"; const P_CONTAINERS_LIST = "containersList";
const P_CONTAINERS_EDIT = "containersEdit"; const P_CONTAINERS_EDIT = "containersEdit";
const P_CONTAINER_INFO = "containerInfo"; const P_CONTAINER_INFO = "containerInfo";
@ -99,9 +101,15 @@ const Logic = {
} }
switch (onboarded) { switch (onboarded) {
case 5: case 7:
this.showAchievementOrContainersListPanel(); this.showAchievementOrContainersListPanel();
break; break;
case 6:
this.showPanel(P_ONBOARDING_7);
break;
case 5:
this.showPanel(P_ONBOARDING_6);
break;
case 4: case 4:
this.showPanel(P_ONBOARDING_5); this.showPanel(P_ONBOARDING_5);
break; break;
@ -352,6 +360,9 @@ const Logic = {
}, },
getAssignmentObjectByContainer(userContextId) { getAssignmentObjectByContainer(userContextId) {
if (!userContextId) {
return {};
}
return browser.runtime.sendMessage({ return browser.runtime.sendMessage({
method: "getAssignmentObjectByContainer", method: "getAssignmentObjectByContainer",
message: { userContextId } message: { userContextId }
@ -500,6 +511,39 @@ Logic.registerPanel(P_ONBOARDING_5, {
// Let's move to the containers list panel. // Let's move to the containers list panel.
Logic.addEnterHandler(document.querySelector("#onboarding-longpress-button"), async () => { Logic.addEnterHandler(document.querySelector("#onboarding-longpress-button"), async () => {
await Logic.setOnboardingStage(5); await Logic.setOnboardingStage(5);
Logic.showPanel(P_ONBOARDING_6);
});
},
// This method is called when the panel is shown.
prepare() {
return Promise.resolve(null);
},
});
// P_ONBOARDING_6: Sixth page for Onboarding: new tab long-press behavior
// ----------------------------------------------------------------------------
Logic.registerPanel(P_ONBOARDING_6, {
panelSelector: ".onboarding-panel-6",
// This method is called when the object is registered.
initialize() {
// Let's move to the containers list panel.
Logic.addEnterHandler(document.querySelector("#start-sync-button"), async () => {
await Logic.setOnboardingStage(6);
await browser.storage.local.set({syncEnabled: true});
await browser.runtime.sendMessage({
method: "resetSync"
});
Logic.showPanel(P_ONBOARDING_7);
});
Logic.addEnterHandler(document.querySelector("#no-sync"), async () => {
await Logic.setOnboardingStage(7);
await browser.storage.local.set({syncEnabled: false});
await browser.runtime.sendMessage({
method: "resetSync"
});
Logic.showPanel(P_CONTAINERS_LIST); Logic.showPanel(P_CONTAINERS_LIST);
}); });
}, },
@ -510,6 +554,33 @@ Logic.registerPanel(P_ONBOARDING_5, {
}, },
}); });
// P_ONBOARDING_6: Sixth page for Onboarding: new tab long-press behavior
// ----------------------------------------------------------------------------
Logic.registerPanel(P_ONBOARDING_7, {
panelSelector: ".onboarding-panel-7",
// This method is called when the object is registered.
initialize() {
// Let's move to the containers list panel.
Logic.addEnterHandler(document.querySelector("#sign-in"), async () => {
browser.tabs.create({
url: "https://accounts.firefox.com/?service=sync&action=email&context=fx_desktop_v3&entrypoint=multi-account-containers&utm_source=addon&utm_medium=panel&utm_campaign=container-sync",
});
await Logic.setOnboardingStage(7);
Logic.showPanel(P_CONTAINERS_LIST);
});
Logic.addEnterHandler(document.querySelector("#no-sign-in"), async () => {
await Logic.setOnboardingStage(7);
Logic.showPanel(P_CONTAINERS_LIST);
});
},
// This method is called when the panel is shown.
prepare() {
return Promise.resolve(null);
},
});
// P_CONTAINERS_LIST: The list of containers. The main page. // P_CONTAINERS_LIST: The list of containers. The main page.
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -721,8 +792,12 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
however it allows us to have a tabindex before the first selected item however it allows us to have a tabindex before the first selected item
*/ */
const focusHandler = () => { const focusHandler = () => {
list.querySelector("tr .clickable").focus(); const identityList = list.querySelector("tr .clickable");
document.removeEventListener("focus", focusHandler); if (identityList) {
// otherwise this throws an error when there are no containers present.
identityList.focus();
document.removeEventListener("focus", focusHandler);
}
}; };
document.addEventListener("focus", focusHandler); document.addEventListener("focus", focusHandler);
/* If the user mousedown's first then remove the focus handler */ /* If the user mousedown's first then remove the focus handler */
@ -1022,6 +1097,7 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
while (tableElement.firstChild) { while (tableElement.firstChild) {
tableElement.firstChild.remove(); tableElement.firstChild.remove();
} }
assignmentKeys.forEach((siteKey) => { assignmentKeys.forEach((siteKey) => {
const site = assignments[siteKey]; const site = assignments[siteKey];
const trElement = document.createElement("div"); const trElement = document.createElement("div");

View file

@ -1,7 +1,7 @@
{ {
"manifest_version": 2, "manifest_version": 2,
"name": "Firefox Multi-Account Containers", "name": "Firefox Multi-Account Containers",
"version": "6.1.1", "version": "6.2.0",
"incognito": "not_allowed", "incognito": "not_allowed",
"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": {

View file

@ -12,6 +12,11 @@
Enable Bookmark Menus Enable Bookmark Menus
</label> </label>
<p>This setting allows you to open a bookmark or folder of bookmarks in a container.</p> <p>This setting allows you to open a bookmark or folder of bookmarks in a container.</p>
<label>
<input type="checkbox" id="syncCheck">
Enable Sync
</label>
<p>This setting allows you to sync your containers and site assignments across devices.</p>
</form> </form>
<script src="js/options.js"></script> <script src="js/options.js"></script>
</body> </body>

View file

@ -64,7 +64,27 @@
<img class="onboarding-img" alt="Long-press the New Tab button to create a new container tab." src="/img/onboarding-3.png" /> <img class="onboarding-img" alt="Long-press the New Tab button to create a new container tab." src="/img/onboarding-3.png" />
<h3 class="onboarding-title">Container tabs when you need them.</h3> <h3 class="onboarding-title">Container tabs when you need them.</h3>
<p>Long-press the New Tab button to create a new container tab.</p> <p>Long-press the New Tab button to create a new container tab.</p>
<a href="#" id="onboarding-longpress-button" class="onboarding-button">Done</a> <a href="#" id="onboarding-longpress-button" class="onboarding-button">Next</a>
</div>
<div class="panel onboarding onboarding-panel-6 hide" id="onboarding-panel-6">
<img class="onboarding-img" alt="Syncing Containers is now Available!" src="/img/Sync.svg" />
<h3 class="onboarding-title">Syncing Containers is now Available!</h3>
<p>Turn on Sync to share container and site assignments with any computer connected to your Firefox Account.</p>
<div class="half-button-wrapper">
<a herf="#" id="no-sync" class="half-onboarding-button grey-button">Not Now</a>
<a href="#" id="start-sync-button" class="half-onboarding-button">Start Syncing</a>
</div>
</div>
<div class="panel onboarding onboarding-panel-7 hide" id="onboarding-panel-7">
<img class="onboarding-img" alt="Firefox Account is required to sync" src="/img/Account.svg" />
<h3 class="onboarding-title">Firefox Account is required to sync.</h3>
<p>Click Sign In to confirm that your Firefox Account is active.</p>
<div class="half-button-wrapper">
<a herf="#" id="no-sign-in" class="half-onboarding-button grey-button">Not Now</a>
<a href="#" id="sign-in" class="half-onboarding-button">Sign In</a>
</div>
</div> </div>
<div class="panel achievement-panel hide" id="achievement-panel"> <div class="panel achievement-panel hide" id="achievement-panel">

View file

@ -6,15 +6,7 @@ module.exports = {
"parserOptions": { "parserOptions": {
"ecmaVersion": 2018 "ecmaVersion": 2018
}, },
globals: { "rules": {
"sinon": false, "no-restricted-globals": ["error", "browser"]
"expect": false,
"nextTick": false,
"buildDom": false,
"buildBackgroundDom": false,
"background": false,
"buildPopupDom": false,
"popup": false,
"helper": false
} }
} }

111
test/common.js Normal file
View file

@ -0,0 +1,111 @@
if (!process.listenerCount("unhandledRejection")) {
// eslint-disable-next-line no-console
process.on("unhandledRejection", r => console.log(r));
}
const path = require("path");
const chai = require("chai");
const sinonChai = require("sinon-chai");
const crypto = require("crypto");
const sinon = require("sinon");
const expect = chai.expect;
chai.should();
chai.use(sinonChai);
const nextTick = () => {
return new Promise(resolve => {
setTimeout(() => {
process.nextTick(resolve);
});
});
};
const webExtensionsJSDOM = require("webextensions-jsdom");
const manifestPath = path.resolve(path.join(__dirname, "../src/manifest.json"));
const buildDom = async ({background = {}, popup = {}}) => {
background = {
...background,
jsdom: {
...background.jsom,
beforeParse(window) {
window.browser.permissions.getAll.resolves({permissions: ["bookmarks"]});
window.crypto = {
getRandomValues: arr => crypto.randomBytes(arr.length),
};
}
}
};
popup = {
...popup,
jsdom: {
...popup.jsdom,
pretendToBeVisual: true
}
};
const webExtension = await webExtensionsJSDOM.fromManifest(manifestPath, {
apiFake: true,
wiring: true,
sinon: global.sinon,
background,
popup
});
webExtension.browser = webExtension.background.browser;
return webExtension;
};
const buildBackgroundDom = background => {
return buildDom({
background,
popup: false
});
};
const buildPopupDom = popup => {
return buildDom({
popup,
background: false
});
};
const initializeWithTab = async (details = {
cookieStoreId: "firefox-default"
}) => {
let tab;
const webExtension = await buildDom({
background: {
async afterBuild(background) {
tab = await background.browser.tabs._create(details);
}
},
popup: {
jsdom: {
beforeParse(window) {
window.browser.storage.local.set({
"browserActionBadgesClicked": [],
"onboarding-stage": 7,
"achievements": [],
"syncEnabled": true
});
window.browser.storage.local.set.resetHistory();
window.browser.storage.sync.clear();
}
}
}
});
webExtension.tab = tab;
return webExtension;
};
module.exports = {
buildDom,
buildBackgroundDom,
buildPopupDom,
initializeWithTab,
sinon,
expect,
nextTick,
};

View file

@ -1,25 +1,30 @@
describe("Assignment Feature", () => { const {initializeWithTab} = require("../common");
describe("Assignment Feature", function () {
const url = "http://example.com"; const url = "http://example.com";
let activeTab; beforeEach(async function () {
beforeEach(async () => { this.webExt = await initializeWithTab({
activeTab = await helper.browser.initializeWithTab({
cookieStoreId: "firefox-container-1", cookieStoreId: "firefox-container-1",
url url
}); });
}); });
describe("click the 'Always open in' checkbox in the popup", () => { afterEach(function () {
beforeEach(async () => { this.webExt.destroy();
});
describe("click the 'Always open in' checkbox in the popup", function () {
beforeEach(async function () {
// popup click to set assignment for activeTab.url // popup click to set assignment for activeTab.url
await helper.popup.clickElementById("container-page-assigned"); await this.webExt.popup.helper.clickElementById("container-page-assigned");
}); });
describe("open new Tab with the assigned URL in the default container", () => { describe("open new Tab with the assigned URL in the default container", function () {
let newTab; let newTab;
beforeEach(async () => { beforeEach(async function () {
// new Tab opening activeTab.url in default container // new Tab opening activeTab.url in default container
newTab = await helper.browser.openNewTab({ newTab = await this.webExt.background.browser.tabs._create({
cookieStoreId: "firefox-default", cookieStoreId: "firefox-default",
url url
}, { }, {
@ -29,12 +34,12 @@ describe("Assignment Feature", () => {
}); });
}); });
it("should open the confirm page", async () => { it("should open the confirm page", async function () {
// should have created a new tab with the confirm page // should have created a new tab with the confirm page
background.browser.tabs.create.should.have.been.calledWithMatch({ this.webExt.background.browser.tabs.create.should.have.been.calledWithMatch({
url: "moz-extension://fake/confirm-page.html?" + url: "moz-extension://fake/confirm-page.html?" +
`url=${encodeURIComponent(url)}` + `url=${encodeURIComponent(url)}` +
`&cookieStoreId=${activeTab.cookieStoreId}`, `&cookieStoreId=${this.webExt.tab.cookieStoreId}`,
cookieStoreId: undefined, cookieStoreId: undefined,
openerTabId: null, openerTabId: null,
index: 2, index: 2,
@ -42,29 +47,29 @@ describe("Assignment Feature", () => {
}); });
}); });
it("should remove the new Tab that got opened in the default container", () => { it("should remove the new Tab that got opened in the default container", function () {
background.browser.tabs.remove.should.have.been.calledWith(newTab.id); this.webExt.background.browser.tabs.remove.should.have.been.calledWith(newTab.id);
}); });
}); });
describe("click the 'Always open in' checkbox in the popup again", () => { describe("click the 'Always open in' checkbox in the popup again", function () {
beforeEach(async () => { beforeEach(async function () {
// popup click to remove assignment for activeTab.url // popup click to remove assignment for activeTab.url
await helper.popup.clickElementById("container-page-assigned"); await this.webExt.popup.helper.clickElementById("container-page-assigned");
}); });
describe("open new Tab with the no longer assigned URL in the default container", () => { describe("open new Tab with the no longer assigned URL in the default container", function () {
beforeEach(async () => { beforeEach(async function () {
// new Tab opening activeTab.url in default container // new Tab opening activeTab.url in default container
await helper.browser.openNewTab({ await this.webExt.background.browser.tabs._create({
cookieStoreId: "firefox-default", cookieStoreId: "firefox-default",
url url
}); });
}); });
it("should not open the confirm page", async () => { it("should not open the confirm page", async function () {
// should not have created a new tab // should not have created a new tab
background.browser.tabs.create.should.not.have.been.called; this.webExt.background.browser.tabs.create.should.not.have.been.called;
}); });
}); });
}); });

View file

@ -1,27 +1,33 @@
describe("Containers Management", () => { const {initializeWithTab} = require("../common");
beforeEach(async () => {
await helper.browser.initializeWithTab(); describe("Containers Management", function () {
beforeEach(async function () {
this.webExt = await initializeWithTab();
}); });
describe("creating a new container", () => { afterEach(function () {
beforeEach(async () => { this.webExt.destroy();
await helper.popup.clickElementById("container-add-link"); });
await helper.popup.clickElementById("edit-container-ok-link");
describe("creating a new container", function () {
beforeEach(async function () {
await this.webExt.popup.helper.clickElementById("container-add-link");
await this.webExt.popup.helper.clickElementById("edit-container-ok-link");
}); });
it("should create it in the browser as well", () => { it("should create it in the browser as well", function () {
background.browser.contextualIdentities.create.should.have.been.calledOnce; this.webExt.background.browser.contextualIdentities.create.should.have.been.calledOnce;
}); });
describe("removing it afterwards", () => { describe("removing it afterwards", function () {
beforeEach(async () => { beforeEach(async function () {
await helper.popup.clickElementById("edit-containers-link"); await this.webExt.popup.helper.clickElementById("edit-containers-link");
await helper.popup.clickLastMatchingElementByQuerySelector(".delete-container-icon"); await this.webExt.popup.helper.clickElementByQuerySelectorAll(".delete-container-icon", "last");
await helper.popup.clickElementById("delete-container-ok-link"); await this.webExt.popup.helper.clickElementById("delete-container-ok-link");
}); });
it("should remove it in the browser as well", () => { it("should remove it in the browser as well", function () {
background.browser.contextualIdentities.remove.should.have.been.calledOnce; this.webExt.background.browser.contextualIdentities.remove.should.have.been.calledOnce;
}); });
}); });
}); });

View file

@ -1,17 +1,24 @@
describe("External Webextensions", () => { const {expect, initializeWithTab} = require("../common");
describe("External Webextensions", function () {
const url = "http://example.com"; const url = "http://example.com";
beforeEach(async () => { beforeEach(async function () {
await helper.browser.initializeWithTab({ this.webExt = await initializeWithTab({
cookieStoreId: "firefox-container-1", cookieStoreId: "firefox-container-1",
url url
}); });
await helper.popup.clickElementById("container-page-assigned");
await this.webExt.popup.helper.clickElementById("container-page-assigned");
}); });
describe("with contextualIdentities permissions", () => { afterEach(function () {
it("should be able to get assignments", async () => { this.webExt.destroy();
background.browser.management.get.resolves({ });
describe("with contextualIdentities permissions", function () {
it("should be able to get assignments", async function () {
this.webExt.background.browser.management.get.resolves({
permissions: ["contextualIdentities"] permissions: ["contextualIdentities"]
}); });
@ -23,18 +30,19 @@ describe("External Webextensions", () => {
id: "external-webextension" id: "external-webextension"
}; };
const [promise] = background.browser.runtime.onMessageExternal.addListener.yield(message, sender); const [promise] = this.webExt.background.browser.runtime.onMessageExternal.addListener.yield(message, sender);
const answer = await promise; const answer = await promise;
expect(answer).to.deep.equal({ expect(answer.userContextId === "1").to.be.true;
userContextId: "1", expect(answer.neverAsk === false).to.be.true;
neverAsk: false expect(
}); Object.prototype.hasOwnProperty.call(
answer, "identityMacAddonUUID")).to.be.true;
}); });
}); });
describe("without contextualIdentities permissions", () => { describe("without contextualIdentities permissions", function () {
it("should throw an error", async () => { it("should throw an error", async function () {
background.browser.management.get.resolves({ this.webExt.background.browser.management.get.resolves({
permissions: [] permissions: []
}); });
@ -46,7 +54,7 @@ describe("External Webextensions", () => {
id: "external-webextension" id: "external-webextension"
}; };
const [promise] = background.browser.runtime.onMessageExternal.addListener.yield(message, sender); const [promise] = this.webExt.background.browser.runtime.onMessageExternal.addListener.yield(message, sender);
return promise.catch(error => { return promise.catch(error => {
expect(error.message).to.equal("Missing contextualIdentities permission"); expect(error.message).to.equal("Missing contextualIdentities permission");
}); });

465
test/features/sync.test.js Normal file
View file

@ -0,0 +1,465 @@
const {initializeWithTab} = require("../common");
describe("Sync", function() {
beforeEach(async function() {
this.webExt = await initializeWithTab();
this.syncHelper = new SyncTestHelper(this.webExt);
});
afterEach(function() {
this.webExt.destroy();
delete this.syncHelper;
});
it("testIdentityStateCleanup", async function() {
await this.syncHelper.stopSyncListeners();
await this.syncHelper.setState({}, LOCAL_DATA, TEST_CONTAINERS, []);
await this.webExt.browser.storage.local.set({
"identitiesState@@_firefox-container-5": {
"hiddenTabs": []
}
});
await this.webExt.background.window.identityState.storageArea.upgradeData();
const macConfigs = await this.webExt.browser.storage.local.get();
const identities = [];
for(const configKey of Object.keys(macConfigs)) {
if (configKey.includes("identitiesState@@_") && !configKey.includes("default")) {
identities.push(macConfigs[configKey]);
}
}
identities.should.have.lengthOf(5, "There should be 5 identity entries");
for (const identity of identities) {
(!!identity.macAddonUUID).should.be.true;
}
});
it("testAssignManagerCleanup", async function() {
await this.syncHelper.stopSyncListeners();
await this.syncHelper.setState({}, LOCAL_DATA, TEST_CONTAINERS, TEST_ASSIGNMENTS);
await this.webExt.browser.storage.local.set({
"siteContainerMap@@_www.goop.com": {
"userContextId": "999",
"neverAsk": true
}
});
await this.webExt.background.window.identityState.storageArea.upgradeData();
await this.webExt.background.window.assignManager.storageArea.upgradeData();
const macConfigs = await this.webExt.browser.storage.local.get();
const assignments = [];
for(const configKey of Object.keys(macConfigs)) {
if (configKey.includes("siteContainerMap@@_")) {
macConfigs[configKey].configKey = configKey;
assignments.push(macConfigs[configKey]);
}
}
assignments.should.have.lengthOf(5, "There should be 5 site assignments");
for (const assignment of assignments) {
(!!assignment.identityMacAddonUUID).should.be.true;
}
});
it("testReconcileSiteAssignments", async function() {
await this.syncHelper.stopSyncListeners();
await this.syncHelper.setState(
DUPE_TEST_SYNC,
LOCAL_DATA,
TEST_CONTAINERS,
SITE_ASSIGNMENT_TEST
);
// add 200ok (bad data).
const testSites = {
"siteContainerMap@@_developer.mozilla.org": {
"userContextId": "588",
"neverAsk": true,
"identityMacAddonUUID": "d20d7af2-9866-468e-bb43-541efe8c2c2e",
"hostname": "developer.mozilla.org"
},
"siteContainerMap@@_reddit.com": {
"userContextId": "592",
"neverAsk": true,
"identityMacAddonUUID": "3dc916fb-8c0a-4538-9758-73ef819a45f7",
"hostname": "reddit.com"
},
"siteContainerMap@@_twitter.com": {
"userContextId": "589",
"neverAsk": true,
"identityMacAddonUUID": "cdd73c20-c26a-4c06-9b17-735c1f5e9187",
"hostname": "twitter.com"
},
"siteContainerMap@@_www.facebook.com": {
"userContextId": "590",
"neverAsk": true,
"identityMacAddonUUID": "32cc4a9b-05ed-4e54-8e11-732468de62f4",
"hostname": "www.facebook.com"
},
"siteContainerMap@@_www.linkedin.com": {
"userContextId": "591",
"neverAsk": true,
"identityMacAddonUUID": "9ff381e3-4c11-420d-8e12-e352a3318be1",
"hostname": "www.linkedin.com"
},
"siteContainerMap@@_200ok.us": {
"userContextId": "1",
"neverAsk": true,
"identityMacAddonUUID": "b5f5f794-b37e-4cec-9f4e-6490df620336",
"hostname": "www.linkedin.com"
}
};
for (const site of Object.keys(testSites)) {
await this.webExt.browser.storage.sync.set({[site]:testSites[site]});
}
await this.webExt.browser.storage.sync.set({
deletedSiteList: ["siteContainerMap@@_www.google.com"]
});
await this.webExt.background.window.sync.runSync();
const assignedSites = await this.webExt.background.window.assignManager.storageArea.getAssignedSites();
Object.keys(assignedSites).should.have.lengthOf(6);
});
it("testInitialSync", async function() {
await this.syncHelper.stopSyncListeners();
await this.syncHelper.setState({}, LOCAL_DATA, TEST_CONTAINERS, []);
await this.webExt.background.window.sync.runSync();
const getAssignedSites =
await this.webExt.background.window.assignManager.storageArea.getAssignedSites();
const identities = await this.webExt.browser.contextualIdentities.query({});
identities.should.have.lengthOf(5, "There should be 5 identity entries");
Object.keys(getAssignedSites).should.have.lengthOf(0, "There should be no site assignments");
});
it("test2", async function() {
await this.syncHelper.stopSyncListeners();
await this.syncHelper.setState(SYNC_DATA, LOCAL_DATA, TEST_CONTAINERS, TEST_ASSIGNMENTS);
await this.webExt.background.window.sync.runSync();
const getAssignedSites =
await this.webExt.background.window.assignManager.storageArea.getAssignedSites();
const identities = await this.webExt.browser.contextualIdentities.query({});
identities.should.have.lengthOf(6, "There should be 6 identity entries");
Object.keys(getAssignedSites).should.have.lengthOf(5, "There should be 5 site assignments");
});
it("dupeTest", async function() {
await this.syncHelper.stopSyncListeners();
await this.syncHelper.setState(
DUPE_TEST_SYNC,
DUPE_TEST_LOCAL,
DUPE_TEST_IDENTS,
DUPE_TEST_ASSIGNMENTS
);
await this.webExt.background.window.sync.runSync();
const getAssignedSites =
await this.webExt.background.window.assignManager.storageArea.getAssignedSites();
const identities = await this.webExt.browser.contextualIdentities.query({});
identities.should.have.lengthOf(7, "There should be 7 identity entries");
Object.keys(getAssignedSites).should.have.lengthOf(5, "There should be 5 identity entries");
const personalContainer =
this.syncHelper.lookupIdentityBy(identities, {name: "Personal"});
(personalContainer.color === "red").should.be.true;
const mozillaContainer =
this.syncHelper.lookupIdentityBy(identities, {name: "Mozilla"});
(mozillaContainer.icon === "pet").should.be.true;
});
});
class SyncTestHelper {
constructor(webExt) {
this.webExt = webExt;
}
async stopSyncListeners() {
await this.webExt.browser.storage.onChanged.removeListener(this.webExt.background.window.sync.storageArea.onChangedListener);
await this.webExt.background.window.sync.removeContextualIdentityListeners();
}
async setState(syncData, localData, identityData, assignmentData){
await this.removeAllContainers();
await this.webExt.browser.storage.sync.clear();
await this.webExt.browser.storage.sync.set(syncData);
await this.webExt.browser.storage.local.clear();
await this.webExt.browser.storage.local.set(localData);
for (let i=0; i < identityData.length; i++) {
//build identities
const newIdentity =
await this.webExt.browser.contextualIdentities.create(identityData[i]);
// fill identies with site assignments
if (assignmentData && assignmentData[i]) {
const data = {
"userContextId":
String(
newIdentity.cookieStoreId.replace(/^firefox-container-/, "")
),
"neverAsk": true
};
await this.webExt.browser.storage.local.set({[assignmentData[i]]: data});
}
}
return;
}
async removeAllContainers() {
const identities = await this.webExt.browser.contextualIdentities.query({});
for (const identity of identities) {
await this.webExt.browser.contextualIdentities.remove(identity.cookieStoreId);
}
}
lookupIdentityBy(identities, options) {
for (const identity of identities) {
if (options && options.name) {
if (identity.name === options.name) return identity;
}
if (options && options.color) {
if (identity.color === options.color) return identity;
}
if (options && options.color) {
if (identity.color === options.color) return identity;
}
}
return false;
}
}
const TEST_CONTAINERS = [
{
name: "Personal",
color: "blue",
icon: "fingerprint"
},
{
name: "Banking",
color: "green",
icon: "dollar"
},
{
name: "Mozilla",
color: "red",
icon: "briefcase"
},
{
name: "Groceries, obviously",
color: "yellow",
icon: "cart"
},
{
name: "Facebook",
color: "toolbar",
icon: "fence"
},
];
const TEST_ASSIGNMENTS = [
"siteContainerMap@@_developer.mozilla.org",
"siteContainerMap@@_twitter.com",
"siteContainerMap@@_www.facebook.com",
"siteContainerMap@@_www.linkedin.com",
"siteContainerMap@@_reddit.com"
];
const LOCAL_DATA = {
"browserActionBadgesClicked": [ "6.2.0" ],
"containerTabsOpened": 7,
"identitiesState@@_firefox-default": { "hiddenTabs": [] },
"onboarding-stage": 5
};
const SYNC_DATA = {
"identity@@_22ded543-5173-44a5-a47a-8813535945ca": {
"name": "Personal",
"icon": "fingerprint",
"color": "red",
"cookieStoreId": "firefox-container-146",
"macAddonUUID": "22ded543-5173-44a5-a47a-8813535945ca"
},
"identity@@_63e5212f-0858-418e-b5a3-09c2dea61fcd": {
"name": "Oscar",
"icon": "dollar",
"color": "green",
"cookieStoreId": "firefox-container-147",
"macAddonUUID": "3e5212f-0858-418e-b5a3-09c2dea61fcd"
},
"identity@@_71335417-158e-4d74-a55b-e9e9081601ec": {
"name": "Mozilla",
"icon": "pet",
"color": "red",
"cookieStoreId": "firefox-container-148",
"macAddonUUID": "71335417-158e-4d74-a55b-e9e9081601ec"
},
"identity@@_59c4e5f7-fe3b-435a-ae60-1340db31a91b": {
"name": "Groceries, obviously",
"icon": "cart",
"color": "pink",
"cookieStoreId": "firefox-container-149",
"macAddonUUID": "59c4e5f7-fe3b-435a-ae60-1340db31a91b"
},
"identity@@_3dc916fb-8c0a-4538-9758-73ef819a45f7": {
"name": "Facebook",
"icon": "fence",
"color": "toolbar",
"cookieStoreId": "firefox-container-150",
"macAddonUUID": "3dc916fb-8c0a-4538-9758-73ef819a45f7"
}
};
const DUPE_TEST_SYNC = {
"identity@@_d20d7af2-9866-468e-bb43-541efe8c2c2e": {
"name": "Personal",
"icon": "fingerprint",
"color": "red",
"cookieStoreId": "firefox-container-588",
"macAddonUUID": "d20d7af2-9866-468e-bb43-541efe8c2c2e"
},
"identity@@_cdd73c20-c26a-4c06-9b17-735c1f5e9187": {
"name": "Big Bird",
"icon": "pet",
"color": "yellow",
"cookieStoreId": "firefox-container-589",
"macAddonUUID": "cdd73c20-c26a-4c06-9b17-735c1f5e9187"
},
"identity@@_32cc4a9b-05ed-4e54-8e11-732468de62f4": {
"name": "Mozilla",
"icon": "pet",
"color": "red",
"cookieStoreId": "firefox-container-590",
"macAddonUUID": "32cc4a9b-05ed-4e54-8e11-732468de62f4"
},
"identity@@_9ff381e3-4c11-420d-8e12-e352a3318be1": {
"name": "Groceries, obviously",
"icon": "cart",
"color": "pink",
"cookieStoreId": "firefox-container-591",
"macAddonUUID": "9ff381e3-4c11-420d-8e12-e352a3318be1"
},
"identity@@_3dc916fb-8c0a-4538-9758-73ef819a45f7": {
"name": "Facebook",
"icon": "fence",
"color": "toolbar",
"cookieStoreId": "firefox-container-592",
"macAddonUUID": "3dc916fb-8c0a-4538-9758-73ef819a45f7"
},
"identity@@_63e5212f-0858-418e-b5a3-09c2dea61fcd": {
"name": "Oscar",
"icon": "dollar",
"color": "green",
"cookieStoreId": "firefox-container-593",
"macAddonUUID": "63e5212f-0858-418e-b5a3-09c2dea61fcd"
},
"siteContainerMap@@_developer.mozilla.org": {
"userContextId": "588",
"neverAsk": true,
"identityMacAddonUUID": "d20d7af2-9866-468e-bb43-541efe8c2c2e",
"hostname": "developer.mozilla.org"
},
"siteContainerMap@@_reddit.com": {
"userContextId": "592",
"neverAsk": true,
"identityMacAddonUUID": "3dc916fb-8c0a-4538-9758-73ef819a45f7",
"hostname": "reddit.com"
},
"siteContainerMap@@_twitter.com": {
"userContextId": "589",
"neverAsk": true,
"identityMacAddonUUID": "cdd73c20-c26a-4c06-9b17-735c1f5e9187",
"hostname": "twitter.com"
},
"siteContainerMap@@_www.facebook.com": {
"userContextId": "590",
"neverAsk": true,
"identityMacAddonUUID": "32cc4a9b-05ed-4e54-8e11-732468de62f4",
"hostname": "www.facebook.com"
},
"siteContainerMap@@_www.linkedin.com": {
"userContextId": "591",
"neverAsk": true,
"identityMacAddonUUID": "9ff381e3-4c11-420d-8e12-e352a3318be1",
"hostname": "www.linkedin.com"
}
};
const DUPE_TEST_LOCAL = {
"beenSynced": true,
"browserActionBadgesClicked": [
"6.2.0"
],
"containerTabsOpened": 7,
"identitiesState@@_firefox-default": {
"hiddenTabs": []
},
"onboarding-stage": 5,
};
const DUPE_TEST_ASSIGNMENTS = [
"siteContainerMap@@_developer.mozilla.org",
"siteContainerMap@@_reddit.com",
"siteContainerMap@@_twitter.com",
"siteContainerMap@@_www.facebook.com",
"siteContainerMap@@_www.linkedin.com"
];
const SITE_ASSIGNMENT_TEST = [
"siteContainerMap@@_developer.mozilla.org",
"siteContainerMap@@_www.facebook.com",
"siteContainerMap@@_www.google.com",
"siteContainerMap@@_bugzilla.mozilla.org"
];
const DUPE_TEST_IDENTS = [
{
"name": "Personal",
"icon": "fingerprint",
"color": "blue",
},
{
"name": "Banking",
"icon": "pet",
"color": "green",
},
{
"name": "Mozilla",
"icon": "briefcase",
"color": "red",
},
{
"name": "Groceries, obviously",
"icon": "cart",
"color": "orange",
},
{
"name": "Facebook",
"icon": "fence",
"color": "toolbar",
},
{
"name": "Big Bird",
"icon": "dollar",
"color": "yellow",
}
];

View file

@ -1,44 +0,0 @@
module.exports = {
browser: {
async initializeWithTab(details = {
cookieStoreId: "firefox-default"
}) {
let tab;
await buildDom({
background: {
async afterBuild(background) {
tab = await background.browser.tabs._create(details);
}
},
popup: {
jsdom: {
beforeParse(window) {
window.browser.storage.local.set({
"browserActionBadgesClicked": [],
"onboarding-stage": 5,
"achievements": []
});
window.browser.storage.local.set.resetHistory();
}
}
}
});
return tab;
},
async openNewTab(tab, options = {}) {
return background.browser.tabs._create(tab, options);
}
},
popup: {
async clickElementById(id) {
await popup.helper.clickElementById(id);
},
async clickLastMatchingElementByQuerySelector(querySelector) {
await popup.helper.clickElementByQuerySelectorAll(querySelector, "last");
}
}
};

View file

@ -1,16 +1,19 @@
describe("#1168", () => { const {expect, sinon, initializeWithTab} = require("../common");
describe("when navigation happens too slow after opening new tab to a page which then redirects", () => {
let clock, tab;
beforeEach(async () => { describe("#1168", function () {
await helper.browser.initializeWithTab({ describe("when navigation happens too slow after opening new tab to a page which then redirects", function () {
let clock, tab, background;
beforeEach(async function () {
this.webExt = await initializeWithTab({
cookieStoreId: "firefox-container-1", cookieStoreId: "firefox-container-1",
url: "https://bugzilla.mozilla.org" url: "https://bugzilla.mozilla.org"
}); });
await helper.popup.clickElementById("container-page-assigned");
await this.webExt.popup.helper.clickElementById("container-page-assigned");
clock = sinon.useFakeTimers(); clock = sinon.useFakeTimers();
tab = await helper.browser.openNewTab({}); tab = await this.webExt.browser.tabs._create({});
clock.tick(2000); clock.tick(2000);
@ -20,15 +23,16 @@ describe("#1168", () => {
]); ]);
}); });
afterEach(function () {
this.webExt.destroy();
clock.restore();
});
// Not solved yet // Not solved yet
// See: https://github.com/mozilla/multi-account-containers/issues/1168#issuecomment-378394091 // See: https://github.com/mozilla/multi-account-containers/issues/1168#issuecomment-378394091
it.skip("should remove the old tab", async () => { it.skip("should remove the old tab", async function () {
expect(background.browser.tabs.create).to.have.been.calledOnce; expect(background.browser.tabs.create).to.have.been.calledOnce;
expect(background.browser.tabs.remove).to.have.been.calledWith(tab.id); expect(background.browser.tabs.remove).to.have.been.calledWith(tab.id);
}); });
afterEach(() => {
clock.restore();
});
}); });
}); });

View file

@ -1,15 +1,18 @@
describe("#940", () => { const {expect, sinon, initializeWithTab} = require("../common");
describe("when other onBeforeRequestHandlers are faster and redirect with the same requestId", () => {
it("should not open two confirm pages", async () => { describe("#940", function () {
await helper.browser.initializeWithTab({ describe("when other onBeforeRequestHandlers are faster and redirect with the same requestId", function () {
it("should not open two confirm pages", async function () {
const webExtension = await initializeWithTab({
cookieStoreId: "firefox-container-1", cookieStoreId: "firefox-container-1",
url: "http://example.com" url: "http://example.com"
}); });
await helper.popup.clickElementById("container-page-assigned");
await webExtension.popup.helper.clickElementById("container-page-assigned");
const responses = {}; const responses = {};
await helper.browser.openNewTab({ await webExtension.background.browser.tabs._create({
url: "http://example.com" url: "https://example.com"
}, { }, {
options: { options: {
webRequestRedirects: ["https://example.com"], webRequestRedirects: ["https://example.com"],
@ -23,46 +26,55 @@ describe("#940", () => {
expect(result).to.deep.equal({ expect(result).to.deep.equal({
cancel: true cancel: true
}); });
background.browser.tabs.create.should.have.been.calledOnce; webExtension.browser.tabs.create.should.have.been.calledOnce;
webExtension.destroy();
}); });
}); });
describe("when redirects change requestId midflight", () => { describe("when redirects change requestId midflight", function () {
let newTab; beforeEach(async function () {
const newTabResponses = {};
const redirectedRequest = async (options = {}) => { this.webExt = await initializeWithTab({
global.clock = sinon.useFakeTimers();
newTab = await helper.browser.openNewTab({
url: "http://youtube.com"
}, {
options: Object.assign({
webRequestRedirects: [
"https://youtube.com",
"https://www.youtube.com",
{
url: "https://www.youtube.com",
webRequest: {
requestId: 2
}
}
],
webRequestError: true,
instantRedirects: true
}, options),
responses: newTabResponses
});
};
beforeEach(async () => {
await helper.browser.initializeWithTab({
cookieStoreId: "firefox-container-1", cookieStoreId: "firefox-container-1",
url: "https://www.youtube.com" url: "https://www.youtube.com"
}); });
await helper.popup.clickElementById("container-page-assigned"); await this.webExt.popup.helper.clickElementById("container-page-assigned");
global.clock = sinon.useFakeTimers();
this.redirectedRequest = async (options = {}) => {
const newTabResponses = {};
const newTab = await this.webExt.browser.tabs._create({
url: "http://youtube.com"
}, {
options: Object.assign({
webRequestRedirects: [
"https://youtube.com",
"https://www.youtube.com",
{
url: "https://www.youtube.com",
webRequest: {
requestId: 2
}
}
],
webRequestError: true,
instantRedirects: true
}, options),
responses: newTabResponses
});
return [newTabResponses, newTab];
};
}); });
it("should not open two confirm pages", async () => { afterEach(function () {
await redirectedRequest(); this.webExt.destroy();
global.clock.restore();
});
it("should not open two confirm pages", async function () {
const [newTabResponses] = await this.redirectedRequest();
// http://youtube.com is not assigned, no cancel, no reopening // http://youtube.com is not assigned, no cancel, no reopening
expect(await newTabResponses.webRequest.onBeforeRequest[0]).to.deep.equal({}); expect(await newTabResponses.webRequest.onBeforeRequest[0]).to.deep.equal({});
@ -80,17 +92,17 @@ describe("#940", () => {
cancel: true cancel: true
}); });
background.browser.tabs.create.should.have.been.calledOnce; this.webExt.background.browser.tabs.create.should.have.been.calledOnce;
}); });
it("should uncancel after webRequest.onCompleted", async () => { it("should uncancel after webRequest.onCompleted", async function () {
await redirectedRequest(); const [newTabResponses, newTab] = await this.redirectedRequest();
// remove onCompleted listeners because in the real world this request would never complete // remove onCompleted listeners because in the real world this request would never complete
// and thus might trigger unexpected behavior because the tab gets removed when reopening // and thus might trigger unexpected behavior because the tab gets removed when reopening
background.browser.webRequest.onCompleted.addListener = sinon.stub(); this.webExt.background.browser.webRequest.onCompleted.addListener = sinon.stub();
background.browser.tabs.create.resetHistory(); this.webExt.background.browser.tabs.create.resetHistory();
// we create a tab with the same id and use the same request id to see if uncanceled // we create a tab with the same id and use the same request id to see if uncanceled
await helper.browser.openNewTab({ await this.webExt.browser.tabs._create({
id: newTab.id, id: newTab.id,
url: "https://www.youtube.com" url: "https://www.youtube.com"
}, { }, {
@ -101,14 +113,14 @@ describe("#940", () => {
} }
}); });
background.browser.tabs.create.should.have.been.calledOnce; this.webExt.background.browser.tabs.create.should.have.been.calledOnce;
}); });
it("should uncancel after webRequest.onErrorOccurred", async () => { it("should uncancel after webRequest.onErrorOccurred", async function () {
await redirectedRequest(); const [newTabResponses, newTab] = await this.redirectedRequest();
background.browser.tabs.create.resetHistory(); this.webExt.background.browser.tabs.create.resetHistory();
// we create a tab with the same id and use the same request id to see if uncanceled // we create a tab with the same id and use the same request id to see if uncanceled
await helper.browser.openNewTab({ await this.webExt.browser.tabs._create({
id: newTab.id, id: newTab.id,
url: "https://www.youtube.com" url: "https://www.youtube.com"
}, { }, {
@ -120,18 +132,18 @@ describe("#940", () => {
} }
}); });
background.browser.tabs.create.should.have.been.calledOnce; this.webExt.background.browser.tabs.create.should.have.been.calledOnce;
}); });
it("should uncancel after 2 seconds", async () => { it("should uncancel after 2 seconds", async function () {
await redirectedRequest({ const [newTabResponses, newTab] = await this.redirectedRequest({
webRequestDontYield: ["onCompleted", "onErrorOccurred"] webRequestDontYield: ["onCompleted", "onErrorOccurred"]
}); });
global.clock.tick(2000); global.clock.tick(2000);
background.browser.tabs.create.resetHistory(); this.webExt.background.browser.tabs.create.resetHistory();
// we create a tab with the same id and use the same request id to see if uncanceled // we create a tab with the same id and use the same request id to see if uncanceled
await helper.browser.openNewTab({ await this.webExt.browser.tabs._create({
id: newTab.id, id: newTab.id,
url: "https://www.youtube.com" url: "https://www.youtube.com"
}, { }, {
@ -143,13 +155,13 @@ describe("#940", () => {
} }
}); });
background.browser.tabs.create.should.have.been.calledOnce; this.webExt.background.browser.tabs.create.should.have.been.calledOnce;
}); });
it("should not influence the canceled url in other tabs", async () => { it("should not influence the canceled url in other tabs", async function () {
await redirectedRequest(); await this.redirectedRequest();
background.browser.tabs.create.resetHistory(); this.webExt.background.browser.tabs.create.resetHistory();
await helper.browser.openNewTab({ await this.webExt.browser.tabs._create({
cookieStoreId: "firefox-default", cookieStoreId: "firefox-default",
url: "https://www.youtube.com" url: "https://www.youtube.com"
}, { }, {
@ -158,11 +170,7 @@ describe("#940", () => {
} }
}); });
background.browser.tabs.create.should.have.been.calledOnce; this.webExt.background.browser.tabs.create.should.have.been.calledOnce;
});
afterEach(() => {
global.clock.restore();
}); });
}); });
}); });

View file

@ -1,81 +0,0 @@
if (!process.listenerCount("unhandledRejection")) {
// eslint-disable-next-line no-console
process.on("unhandledRejection", r => console.log(r));
}
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 webExtensionsJSDOM = require("webextensions-jsdom");
const manifestPath = path.resolve(path.join(__dirname, "../src/manifest.json"));
global.buildDom = async ({background = {}, popup = {}}) => {
background = {
...background,
jsdom: {
...background.jsom,
beforeParse(window) {
window.browser.permissions.getAll.resolves({permissions: ["bookmarks"]});
}
}
};
popup = {
...popup,
jsdom: {
...popup.jsdom,
pretendToBeVisual: true
}
};
const webExtension = await webExtensionsJSDOM.fromManifest(manifestPath, {
apiFake: true,
wiring: true,
sinon: global.sinon,
background,
popup
});
// eslint-disable-next-line require-atomic-updates
global.background = webExtension.background;
// eslint-disable-next-line require-atomic-updates
global.popup = webExtension.popup;
};
global.buildBackgroundDom = async background => {
await global.buildDom({
background,
popup: false
});
};
global.buildPopupDom = async popup => {
await global.buildDom({
popup,
background: false
});
};
global.afterEach(() => {
if (global.background) {
global.background.destroy();
}
if (global.popup) {
global.popup.destroy();
}
});