diff --git a/.eslintignore b/.eslintignore index 9f1953c..3a5b5a0 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ lib/testpilot/*.js +coverage \ No newline at end of file diff --git a/.gitignore b/.gitignore index c745683..ff6bfab 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,7 @@ src/web-ext-artifacts/* # JetBrains IDE files .idea + +# IstanbulJS +.nyc_output +coverage \ No newline at end of file diff --git a/README.md b/README.md index 7574cf0..e85c870 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,15 @@ For more info, see: 2. `./node_modules/.bin/web-ext run -s src/` ### Testing -TBD +`npm run test` + +or + +`npm run lint` + +for just the linter + +There is a timeout test that sometimes fails on certain machines, so make sure to run the tests on your clone before you make any changes to see if you have this problem. ### Distributing #### Make the new version @@ -51,6 +59,6 @@ Finally, we also publish the release to GitHub for those followers. Facebook & Twitter icons CC-Attrib https://fairheadcreative.com. -- [Licence](./LICENSE.txt) +- [License](./LICENSE.txt) - [Contributing](./CONTRIBUTING.md) - [Code Of Conduct](./CODE_OF_CONDUCT.md) diff --git a/package.json b/package.json index 172a4f5..753f292 100644 --- a/package.json +++ b/package.json @@ -9,23 +9,24 @@ }, "dependencies": {}, "devDependencies": { - "ajv": "^6.6.2", "addons-linter": "^1.3.2", - "chai": "^4.1.2", - "eslint": "^3.17.1", + "ajv": "^6.6.2", + "chai": "^4.2.0", + "eslint": "^6.6.0", "eslint-plugin-no-unsanitized": "^2.0.0", "eslint-plugin-promise": "^3.4.0", "htmllint-cli": "0.0.7", - "jsdom": "^11.6.2", "json": "^9.0.6", - "mocha": "^5.0.0", + "mocha": "^6.2.2", "npm-run-all": "^4.0.0", - "sinon": "^4.4.0", - "sinon-chai": "^2.14.0", + "nyc": "^14.1.1", + "sinon": "^7.5.0", + "sinon-chai": "^3.3.0", "stylelint": "^7.9.0", "stylelint-config-standard": "^16.0.0", "stylelint-order": "^0.3.0", - "web-ext": "^2.2.2" + "web-ext": "^2.9.3", + "webextensions-jsdom": "^1.1.0" }, "homepage": "https://github.com/mozilla/multi-account-containers#readme", "license": "MPL-2.0", @@ -42,7 +43,8 @@ "lint:html": "htmllint *.html", "lint:js": "eslint .", "package": "rm -rf src/web-ext-artifacts && npm run build && mv src/web-ext-artifacts/firefox_multi-account_containers-*.zip addon.xpi", - "test": "npm run lint && mocha ./test/setup.js test/**/*.test.js", - "test-watch": "mocha ./test/setup.js test/**/*.test.js --watch" + "test": "npm run lint && npm run coverage", + "test-watch": "mocha ./test/setup.js test/**/*.test.js --watch", + "coverage": "nyc --reporter=html --reporter=text mocha ./test/setup.js test/**/*.test.js --timeout 60000" } } diff --git a/src/css/confirm-page.css b/src/css/confirm-page.css index e00eb1c..113ffef 100644 --- a/src/css/confirm-page.css +++ b/src/css/confirm-page.css @@ -60,6 +60,7 @@ html { @media (prefers-color-scheme: dark) { #redirect-url { background: #38383d; /* Grey 70 */ + color: #eee; /* White 20 */ } } /* stylelint-enable */ diff --git a/src/css/popup.css b/src/css/popup.css index 5f45f10..d5f3295 100644 --- a/src/css/popup.css +++ b/src/css/popup.css @@ -754,21 +754,24 @@ span ~ .panel-header-text { background: var(--primary-action-color); block-size: 100%; color: #fff; - display: flex; - flex: 1; + display: inline-block; justify-content: center; + padding-block-start: 6px; + padding-inline-start: 30%; } -.exit-edit-mode-link::before { - background: url('/img/container-arrow.svg') no-repeat; +.edit-containers-panel-footer { + background: var(--primary-action-color); +} + +.exit-edit-mode-link img { block-size: 16px; - content: ""; - display: block; + display: inline; filter: grayscale(100%) brightness(5); - float: left; inline-size: 16px; margin-inline-end: 5px; transform: scaleX(-1); + vertical-align: bottom; } .delete-container-confirm { diff --git a/src/js/background/assignManager.js b/src/js/background/assignManager.js index dc9e991..b48db75 100644 --- a/src/js/background/assignManager.js +++ b/src/js/background/assignManager.js @@ -4,7 +4,7 @@ const assignManager = { MENU_SEPARATOR_ID: "separator", MENU_HIDE_ID: "hide-container", MENU_MOVE_ID: "move-to-new-window-container", - + OPEN_IN_CONTAINER: "open-bookmark-in-container-tab", storageArea: { area: browser.storage.local, exemptedTabs: {}, @@ -143,7 +143,6 @@ const assignManager = { const userContextId = this.getUserContextIdFromCookieStore(tab); if (!siteSettings || userContextId === siteSettings.userContextId - || tab.incognito || this.storageArea.isExempted(options.url, tab.id)) { return {}; } @@ -221,7 +220,7 @@ const assignManager = { init() { browser.contextMenus.onClicked.addListener((info, tab) => { - 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 @@ -241,7 +240,46 @@ const assignManager = { delete this.canceledRequests[options.tabId]; } },{urls: [""], types: ["main_frame"]}); + this.resetBookmarksMenuItem(); + }, + async resetBookmarksMenuItem() { + const hasPermission = await browser.permissions.contains({permissions: ["bookmarks"]}); + if (this.hadBookmark === hasPermission) { + return; + } + this.hadBookmark = hasPermission; + if (hasPermission) { + this.initBookmarksMenu(); + browser.contextualIdentities.onCreated.addListener(this.contextualIdentityCreated); + browser.contextualIdentities.onUpdated.addListener(this.contextualIdentityUpdated); + browser.contextualIdentities.onRemoved.addListener(this.contextualIdentityRemoved); + } else { + this.removeBookmarksMenu(); + browser.contextualIdentities.onCreated.removeListener(this.contextualIdentityCreated); + browser.contextualIdentities.onUpdated.removeListener(this.contextualIdentityUpdated); + browser.contextualIdentities.onRemoved.removeListener(this.contextualIdentityRemoved); + } + }, + + contextualIdentityCreated(changeInfo) { + browser.contextMenus.create({ + parentId: assignManager.OPEN_IN_CONTAINER, + id: changeInfo.contextualIdentity.cookieStoreId, + title: changeInfo.contextualIdentity.name, + icons: { "16": `img/usercontext.svg#${changeInfo.contextualIdentity.icon}` } + }); + }, + + contextualIdentityUpdated(changeInfo) { + browser.contextMenus.update(changeInfo.contextualIdentity.cookieStoreId, { + title: changeInfo.contextualIdentity.name, + icons: { "16": `img/usercontext.svg#${changeInfo.contextualIdentity.icon}` } + }); + }, + + contextualIdentityRemoved(changeInfo) { + browser.contextMenus.remove(changeInfo.contextualIdentity.cookieStoreId); }, async _onClickedHandler(info, tab) { @@ -275,6 +313,38 @@ const assignManager = { } }, + async _onClickedBookmark(info) { + + async function _getBookmarksFromInfo(info) { + const [bookmarkTreeNode] = await browser.bookmarks.get(info.bookmarkId); + if (bookmarkTreeNode.type === "folder") { + return await browser.bookmarks.getChildren(bookmarkTreeNode.id); + } + return [bookmarkTreeNode]; + } + + const bookmarks = await _getBookmarksFromInfo(info); + for (const bookmark of bookmarks) { + // Some checks on the urls from 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"); + if(openInReaderMode) { + try { + const parsed = new URL(bookmark.url); + bookmark.url = parsed.searchParams.get("url") + parsed.hash; + } catch (err) { + return err.message; + } + } + browser.tabs.create({ + cookieStoreId: info.menuItemId, + url: bookmark.url, + openInReaderMode: openInReaderMode + }); + } + } + }, + deleteContainer(userContextId) { this.storageArea.deleteContainer(userContextId); @@ -289,11 +359,9 @@ const assignManager = { isTabPermittedAssign(tab) { // Ensure we are not an important about url - // Ensure we are not in incognito mode const url = new URL(tab.url); if (url.protocol === "about:" - || url.protocol === "moz-extension:" - || tab.incognito) { + || url.protocol === "moz-extension:") { return false; } return true; @@ -442,6 +510,32 @@ const assignManager = { throw e; }); } + }, + + async initBookmarksMenu() { + browser.contextMenus.create({ + id: this.OPEN_IN_CONTAINER, + title: "Open Bookmark in Container Tab", + contexts: ["bookmark"], + }); + + const identities = await browser.contextualIdentities.query({}); + for (const identity of identities) { + browser.contextMenus.create({ + parentId: this.OPEN_IN_CONTAINER, + id: identity.cookieStoreId, + title: identity.name, + icons: { "16": `img/usercontext.svg#${identity.icon}` } + }); + } + }, + + async removeBookmarksMenu() { + browser.contextMenus.remove(this.OPEN_IN_CONTAINER); + const identities = await browser.contextualIdentities.query({}); + for (const identity of identities) { + browser.contextMenus.remove(identity.cookieStoreId); + } } }; diff --git a/src/js/background/backgroundLogic.js b/src/js/background/backgroundLogic.js index 0aa51fc..f05148b 100644 --- a/src/js/background/backgroundLogic.js +++ b/src/js/background/backgroundLogic.js @@ -46,15 +46,13 @@ const backgroundLogic = { donePromise = browser.contextualIdentities.create(options.params); } await donePromise; - browser.runtime.sendMessage({ - method: "refreshNeeded" - }); }, async openNewTab(options) { let url = options.url || undefined; const userContextId = ("userContextId" in options) ? options.userContextId : 0; const active = ("nofocus" in options) ? options.nofocus : true; + const discarded = ("noload" in options) ? options.noload : false; const cookieStoreId = backgroundLogic.cookieStoreId(userContextId); // Autofocus url bar will happen in 54: https://bugzilla.mozilla.org/show_bug.cgi?id=1295072 @@ -71,6 +69,7 @@ const backgroundLogic = { return browser.tabs.create({ url, active, + discarded, pinned: options.pinned || false, cookieStoreId }); @@ -113,11 +112,12 @@ const backgroundLogic = { return list.concat(containerState.hiddenTabs); }, - async unhideContainer(cookieStoreId) { + async unhideContainer(cookieStoreId, alreadyShowingUrl) { if (!this.unhideQueue.includes(cookieStoreId)) { this.unhideQueue.push(cookieStoreId); await this.showTabs({ - cookieStoreId + cookieStoreId, + alreadyShowingUrl }); this.unhideQueue.splice(this.unhideQueue.indexOf(cookieStoreId), 1); } @@ -369,12 +369,16 @@ const backgroundLogic = { const containerState = await identityState.storageArea.get(options.cookieStoreId); for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const - promises.push(this.openNewTab({ - userContextId: userContextId, - url: object.url, - nofocus: options.nofocus || false, - pinned: object.pinned, - })); + // do not show already opened url + if (object.url !== options.alreadyShowingUrl) { + promises.push(this.openNewTab({ + userContextId: userContextId, + url: object.url, + nofocus: options.nofocus || false, + noload: true, + pinned: object.pinned, + })); + } } containerState.hiddenTabs = []; @@ -386,4 +390,4 @@ const backgroundLogic = { cookieStoreId(userContextId) { return `firefox-container-${userContextId}`; } -}; +}; \ No newline at end of file diff --git a/src/js/background/badge.js b/src/js/background/badge.js index 78cd9f1..7d532ac 100644 --- a/src/js/background/badge.js +++ b/src/js/background/badge.js @@ -2,22 +2,17 @@ const MAJOR_VERSIONS = ["2.3.0", "2.4.0"]; const badge = { async init() { const currentWindow = await browser.windows.getCurrent(); - this.displayBrowserActionBadge(currentWindow.incognito); - }, - - disableAddon(tabId) { - browser.browserAction.disable(tabId); - browser.browserAction.setTitle({ tabId, title: "Containers disabled in Private Browsing Mode" }); + this.displayBrowserActionBadge(currentWindow); }, async displayBrowserActionBadge() { const extensionInfo = await backgroundLogic.getExtensionInfo(); - const storage = await browser.storage.local.get({browserActionBadgesClicked: []}); + const storage = await browser.storage.local.get({ browserActionBadgesClicked: [] }); if (MAJOR_VERSIONS.indexOf(extensionInfo.version) > -1 && - storage.browserActionBadgesClicked.indexOf(extensionInfo.version) < 0) { - browser.browserAction.setBadgeBackgroundColor({color: "rgba(0,217,0,255)"}); - browser.browserAction.setBadgeText({text: "NEW"}); + storage.browserActionBadgesClicked.indexOf(extensionInfo.version) < 0) { + browser.browserAction.setBadgeBackgroundColor({ color: "rgba(0,217,0,255)" }); + browser.browserAction.setBadgeText({ text: "NEW" }); } } }; diff --git a/src/js/background/messageHandler.js b/src/js/background/messageHandler.js index 0314ff7..8d44010 100644 --- a/src/js/background/messageHandler.js +++ b/src/js/background/messageHandler.js @@ -10,6 +10,9 @@ const messageHandler = { let response; switch (m.method) { + case "resetBookmarksContext": + response = assignManager.resetBookmarksMenuItem(); + break; case "deleteContainer": response = backgroundLogic.deleteContainer(m.message.userContextId); break; @@ -85,6 +88,7 @@ const messageHandler = { if (!extensionInfo.permissions.includes("contextualIdentities")) { throw new Error("Missing contextualIdentities permission"); } + // eslint-disable-next-line require-atomic-updates externalExtensionAllowed[sender.id] = true; } let response; @@ -147,9 +151,6 @@ const messageHandler = { }, {urls: [""], types: ["main_frame"]}); browser.tabs.onCreated.addListener((tab) => { - if (tab.incognito) { - badge.disableAddon(tab.id); - } // lets remember the last tab created so we can close it if it looks like a redirect this.lastCreatedTab = tab; if (tab.cookieStoreId) { @@ -159,9 +160,26 @@ const messageHandler = { !tab.url.startsWith("moz-extension")) { // increment the counter of container tabs opened this.incrementCountOfContainerTabsOpened(); - } - backgroundLogic.unhideContainer(tab.cookieStoreId); + this.tabUpdateHandler = (tabId, changeInfo) => { + if (tabId === tab.id && changeInfo.status === "complete") { + // get current tab's url to not open the same one from hidden tabs + browser.tabs.get(tabId).then(loadedTab => { + backgroundLogic.unhideContainer(tab.cookieStoreId, loadedTab.url); + }).catch((e) => { + throw e; + }); + + browser.tabs.onUpdated.removeListener(this.tabUpdateHandler); + } + }; + + // if it's a container tab wait for it to complete and + // unhide other tabs from this container + if (tab.cookieStoreId.startsWith("firefox-container")) { + browser.tabs.onUpdated.addListener(this.tabUpdateHandler); + } + } } setTimeout(() => { this.lastCreatedTab = null; diff --git a/src/js/confirm-page.js b/src/js/confirm-page.js index 9f6eb77..8a14a7b 100644 --- a/src/js/confirm-page.js +++ b/src/js/confirm-page.js @@ -1,6 +1,6 @@ async function load() { const searchParams = new URL(window.location).searchParams; - const redirectUrl = decodeURIComponent(searchParams.get("url")); + const redirectUrl = searchParams.get("url"); const cookieStoreId = searchParams.get("cookieStoreId"); const currentCookieStoreId = searchParams.get("currentCookieStoreId"); const redirectUrlElement = document.getElementById("redirect-url"); diff --git a/src/js/options.js b/src/js/options.js index c738366..6d1d17a 100644 --- a/src/js/options.js +++ b/src/js/options.js @@ -1,4 +1,24 @@ -window.addEventListener("load", () => { + +async function requestPermissions() { + const checkbox = document.querySelector("#bookmarksPermissions"); + if (checkbox.checked) { + const granted = await browser.permissions.request({permissions: ["bookmarks"]}); + if (!granted) { + checkbox.checked = false; + return; + } + } else { + await browser.permissions.remove({permissions: ["bookmarks"]}); + } + browser.runtime.sendMessage({ method: "resetBookmarksContext" }); +} + +async function restoreOptions() { + const hasPermission = await browser.permissions.contains({ permissions: ["bookmarks"] }); + if (hasPermission) { + document.querySelector("#bookmarksPermissions").checked = true; + } + const backupLink = document.getElementById("containers-save-link"); document.getElementById("containers-save-button").addEventListener("click", async () => { const content = JSON.stringify( @@ -26,4 +46,8 @@ window.addEventListener("load", () => { } restoreInput.value = ""; }); -}); +} + + +document.addEventListener("DOMContentLoaded", restoreOptions); +document.querySelector("#bookmarksPermissions").addEventListener( "change", requestPermissions); diff --git a/src/js/popup.js b/src/js/popup.js index 80986a2..64dca45 100644 --- a/src/js/popup.js +++ b/src/js/popup.js @@ -12,15 +12,15 @@ const NEW_CONTAINER_ID = "new"; const ONBOARDING_STORAGE_KEY = "onboarding-stage"; // List of panels -const P_ONBOARDING_1 = "onboarding1"; -const P_ONBOARDING_2 = "onboarding2"; -const P_ONBOARDING_3 = "onboarding3"; -const P_ONBOARDING_4 = "onboarding4"; -const P_ONBOARDING_5 = "onboarding5"; -const P_CONTAINERS_LIST = "containersList"; -const P_CONTAINERS_EDIT = "containersEdit"; -const P_CONTAINER_INFO = "containerInfo"; -const P_CONTAINER_EDIT = "containerEdit"; +const P_ONBOARDING_1 = "onboarding1"; +const P_ONBOARDING_2 = "onboarding2"; +const P_ONBOARDING_3 = "onboarding3"; +const P_ONBOARDING_4 = "onboarding4"; +const P_ONBOARDING_5 = "onboarding5"; +const P_CONTAINERS_LIST = "containersList"; +const P_CONTAINERS_EDIT = "containersEdit"; +const P_CONTAINER_INFO = "containerInfo"; +const P_CONTAINER_EDIT = "containerEdit"; const P_CONTAINER_DELETE = "containerDelete"; const P_CONTAINERS_ACHIEVEMENT = "containersAchievement"; @@ -32,7 +32,7 @@ const P_CONTAINERS_ACHIEVEMENT = "containersAchievement"; * @return {string} The escaped string. */ function escapeXML(str) { - const replacements = {"&": "&", "\"": """, "'": "'", "<": "<", ">": ">", "/": "/"}; + const replacements = { "&": "&", "\"": """, "'": "'", "<": "<", ">": ">", "/": "/" }; return String(str).replace(/[&"'<>/]/g, m => replacements[m]); } @@ -85,7 +85,7 @@ const Logic = { try { await identitiesPromise; - } catch(e) { + } catch (e) { throw new Error("Failed to retrieve the identities or variation. We cannot continue. ", e.message); } @@ -125,7 +125,7 @@ const Logic = { async showAchievementOrContainersListPanel() { // Do we need to show an achievement panel? let showAchievements = false; - const achievementsStorage = await browser.storage.local.get({achievements: []}); + const achievementsStorage = await browser.storage.local.get({ achievements: [] }); for (const achievement of achievementsStorage.achievements) { if (!achievement.done) { showAchievements = true; @@ -142,7 +142,7 @@ const Logic = { // they have to click the "Done" button to stop the panel // from showing async setAchievementDone(achievementName) { - const achievementsStorage = await browser.storage.local.get({achievements: []}); + const achievementsStorage = await browser.storage.local.get({ achievements: [] }); const achievements = achievementsStorage.achievements; achievements.forEach((achievement, index, achievementsArray) => { if (achievement.name === achievementName) { @@ -150,7 +150,7 @@ const Logic = { achievementsArray[index] = achievement; } }); - browser.storage.local.set({achievements}); + browser.storage.local.set({ achievements }); }, setOnboardingStage(stage) { @@ -161,9 +161,9 @@ const Logic = { async clearBrowserActionBadge() { const extensionInfo = await getExtensionInfo(); - const storage = await browser.storage.local.get({browserActionBadgesClicked: []}); - browser.browserAction.setBadgeBackgroundColor({color: null}); - browser.browserAction.setBadgeText({text: ""}); + const storage = await browser.storage.local.get({ browserActionBadgesClicked: [] }); + browser.browserAction.setBadgeBackgroundColor({ color: null }); + browser.browserAction.setBadgeText({ text: "" }); storage.browserActionBadgesClicked.push(extensionInfo.version); // use set and spread to create a unique array const browserActionBadgesClicked = [...new Set(storage.browserActionBadgesClicked)]; @@ -184,7 +184,7 @@ const Logic = { // Handle old style rejection with null and also Promise.reject new style try { return await browser.contextualIdentities.get(cookieStoreId) || defaultContainer; - } catch(e) { + } catch (e) { return defaultContainer; } }, @@ -207,7 +207,7 @@ const Logic = { }, async currentTab() { - const activeTabs = await browser.tabs.query({active: true, windowId: browser.windows.WINDOW_ID_CURRENT}); + const activeTabs = await browser.tabs.query({ active: true, windowId: browser.windows.WINDOW_ID_CURRENT }); if (activeTabs.length > 0) { return activeTabs[0]; } @@ -215,7 +215,7 @@ const Logic = { }, async numTabs() { - const activeTabs = await browser.tabs.query({windowId: browser.windows.WINDOW_ID_CURRENT}); + const activeTabs = await browser.tabs.query({ windowId: browser.windows.WINDOW_ID_CURRENT }); return activeTabs.length; }, @@ -259,7 +259,8 @@ const Logic = { getPanelSelector(panel) { if (this._onboardingVariation === "securityOnboarding" && - panel.hasOwnProperty("securityPanelSelector")) { + // eslint-disable-next-line no-prototype-builtins + panel.hasOwnProperty("securityPanelSelector")) { return panel.securityPanelSelector; } else { return panel.panelSelector; @@ -289,7 +290,13 @@ const Logic = { } } }); - document.querySelector(this.getPanelSelector(this._panels[panel])).classList.remove("hide"); + const panelEl = document.querySelector(this.getPanelSelector(this._panels[panel])); + panelEl.classList.remove("hide"); + + const focusEl = panelEl.querySelector(".firstTabindex"); + if(focusEl) { + focusEl.focus(); + } }, showPreviousPanel() { @@ -333,7 +340,7 @@ const Logic = { return browser.runtime.sendMessage({ method: "deleteContainer", - message: {userContextId} + message: { userContextId } }); }, @@ -347,7 +354,7 @@ const Logic = { getAssignmentObjectByContainer(userContextId) { return browser.runtime.sendMessage({ method: "getAssignmentObjectByContainer", - message: {userContextId} + message: { userContextId } }); }, @@ -376,12 +383,17 @@ const Logic = { }); // Here we find the first valid id. - for (let id = 1;; ++id) { + for (let id = 1; ; ++id) { if (ids.indexOf(id) === -1) { return defaultName + (id < 10 ? "0" : "") + id; } } }, + + getCurrentPanelElement() { + const panelItem = this._panels[this._currentPanel]; + return document.querySelector(this.getPanelSelector(panelItem)); + }, }; // P_ONBOARDING_1: First page for Onboarding. @@ -511,7 +523,7 @@ Logic.registerPanel(P_CONTAINERS_LIST, { }); Logic.addEnterHandler(document.querySelector("#edit-containers-link"), (e) => { - if (!e.target.classList.contains("disable-edit-containers")){ + if (!e.target.classList.contains("disable-edit-containers")) { Logic.showPanel(P_CONTAINERS_EDIT); } }); @@ -550,6 +562,30 @@ Logic.registerPanel(P_CONTAINERS_LIST, { case 38: previous(); break; + case 13: { + const panel = Logic.getCurrentPanelElement(); + const button = panel.getElementsByTagName("A")[0]; + if(button) { + button.click(); + } + break; + } + case 39: + { + const showTabs = element.parentNode.querySelector(".show-tabs"); + if(showTabs) { + showTabs.click(); + } + break; + } + case 37: + { + const hideTabs = document.querySelector(".panel-back-arrow"); + if(hideTabs) { + hideTabs.click(); + } + break; + } default: if ((e.keyCode >= 49 && e.keyCode <= 57) && Logic._currentPanel === "containersList") { @@ -635,11 +671,11 @@ Logic.registerPanel(P_CONTAINERS_LIST, { tr.classList.add("container-panel-row"); - context.classList.add("userContext-wrapper", "open-newtab", "clickable"); + context.classList.add("userContext-wrapper", "open-newtab", "clickable", "firstTabindex"); manage.classList.add("show-tabs", "pop-button"); - manage.title = escaped`View ${identity.name} container`; + manage.setAttribute("title", `View ${identity.name} container`); context.setAttribute("tabindex", "0"); - context.title = escaped`Create ${identity.name} tab`; + context.setAttribute("title", `Create ${identity.name} tab`); context.innerHTML = escaped`
{ if (e.target.matches(".open-newtab") - || e.target.parentNode.matches(".open-newtab") - || e.type === "keydown") { + || e.target.parentNode.matches(".open-newtab") + || e.type === "keydown") { try { browser.tabs.create({ cookieStoreId: identity.cookieStoreId @@ -713,11 +749,15 @@ Logic.registerPanel(P_CONTAINER_INFO, { // This method is called when the object is registered. async initialize() { - Logic.addEnterHandler(document.querySelector("#close-container-info-panel"), () => { + const closeContEl = document.querySelector("#close-container-info-panel"); + closeContEl.setAttribute("tabindex", "0"); + closeContEl.classList.add("firstTabindex"); + Logic.addEnterHandler(closeContEl, () => { Logic.showPreviousPanel(); }); - - Logic.addEnterHandler(document.querySelector("#container-info-hideorshow"), async () => { + const hideContEl = document.querySelector("#container-info-hideorshow"); + hideContEl.setAttribute("tabindex", "0"); + Logic.addEnterHandler(hideContEl, async () => { const identity = Logic.currentIdentity(); try { browser.runtime.sendMessage({ @@ -741,13 +781,14 @@ Logic.registerPanel(P_CONTAINER_INFO, { throw new Error("Could not check for incompatible add-ons."); } const moveTabsEl = document.querySelector("#container-info-movetabs"); + moveTabsEl.setAttribute("tabindex","0"); const numTabs = await Logic.numTabs(); if (incompatible) { Logic._disableMoveTabs("Moving container tabs is incompatible with Pulse, PageShot, and SnoozeTabs."); return; } else if (numTabs === 1) { Logic._disableMoveTabs("Cannot move a tab from a single-tab window."); - return; + return; } Logic.addEnterHandler(moveTabsEl, async () => { await browser.runtime.sendMessage({ @@ -807,8 +848,9 @@ Logic.registerPanel(P_CONTAINER_INFO, {
${tab.title}
`; tr.querySelector("td").appendChild(Utils.createFavIconElement(tab.favIconUrl)); + tr.setAttribute("tabindex", "0"); document.getElementById("container-info-table").appendChild(fragment); - + // On click, we activate this tab. But only if this tab is active. if (!tab.hiddenState) { const closeImage = document.createElement("img"); @@ -830,7 +872,7 @@ Logic.registerPanel(P_CONTAINER_INFO, { tr.classList.add("clickable"); Logic.addEnterHandler(tr, async () => { - await browser.tabs.update(tab.id, {active: true}); + await browser.tabs.update(tab.id, { active: true }); window.close(); }); @@ -842,7 +884,7 @@ Logic.registerPanel(P_CONTAINER_INFO, { }); } } - } + } }, }); @@ -1018,7 +1060,7 @@ Logic.registerPanel(P_CONTAINER_EDIT, { return escaped`
+ + Exit Edit Mode +
diff --git a/test/.eslintrc.js b/test/.eslintrc.js index 0b29fac..d32e265 100644 --- a/test/.eslintrc.js +++ b/test/.eslintrc.js @@ -3,10 +3,14 @@ module.exports = { "node": true, "mocha": true }, + "parserOptions": { + "ecmaVersion": 2018 + }, globals: { "sinon": false, "expect": false, "nextTick": false, + "buildDom": false, "buildBackgroundDom": false, "background": false, "buildPopupDom": false, diff --git a/test/browser.mock.js b/test/browser.mock.js deleted file mode 100644 index d6d38dd..0000000 --- a/test/browser.mock.js +++ /dev/null @@ -1,137 +0,0 @@ -module.exports = () => { - const _storage = {}; - - // could maybe be replaced by https://github.com/acvetkov/sinon-chrome - const browserMock = { - _storage, - runtime: { - onMessage: { - addListener: sinon.stub(), - }, - onMessageExternal: { - addListener: sinon.stub(), - }, - sendMessage: sinon.stub().resolves(), - }, - webRequest: { - onBeforeRequest: { - addListener: sinon.stub() - }, - onCompleted: { - addListener: sinon.stub() - }, - onErrorOccurred: { - addListener: sinon.stub() - } - }, - windows: { - getCurrent: sinon.stub().resolves({}), - onFocusChanged: { - addListener: sinon.stub(), - } - }, - tabs: { - onActivated: { - addListener: sinon.stub() - }, - onCreated: { - addListener: sinon.stub() - }, - onUpdated: { - addListener: sinon.stub() - }, - sendMessage: sinon.stub(), - query: sinon.stub().resolves([{}]), - get: sinon.stub(), - create: sinon.stub().resolves({}), - remove: sinon.stub().resolves() - }, - history: { - deleteUrl: sinon.stub() - }, - storage: { - local: { - get: sinon.stub(), - set: sinon.stub() - } - }, - contextualIdentities: { - create: sinon.stub(), - get: sinon.stub(), - query: sinon.stub().resolves([]) - }, - contextMenus: { - create: sinon.stub(), - remove: sinon.stub(), - onClicked: { - addListener: sinon.stub() - } - }, - browserAction: { - setBadgeBackgroundColor: sinon.stub(), - setBadgeText: sinon.stub() - }, - management: { - get: sinon.stub(), - onInstalled: { - addListener: sinon.stub() - }, - onUninstalled: { - addListener: sinon.stub() - } - }, - extension: { - getURL: sinon.stub().returns("moz-extension://multi-account-containers/confirm-page.html") - } - }; - - // inmemory local storage - browserMock.storage.local = { - get: sinon.spy(async key => { - if (!key) { - return _storage; - } - let result = {}; - if (Array.isArray(key)) { - key.map(akey => { - if (typeof _storage[akey] !== "undefined") { - result[akey] = _storage[akey]; - } - }); - } else if (typeof key === "object") { - // TODO support nested objects - Object.keys(key).map(oKey => { - if (typeof _storage[oKey] !== "undefined") { - result[oKey] = _storage[oKey]; - } else { - result[oKey] = key[oKey]; - } - }); - } else { - result = _storage[key]; - } - return result; - }), - set: sinon.spy(async (key, value) => { - if (typeof key === "object") { - // TODO support nested objects - Object.keys(key).map(oKey => { - _storage[oKey] = key[oKey]; - }); - } else { - _storage[key] = value; - } - }), - remove: sinon.spy(async (key) => { - if (Array.isArray(key)) { - key.map(aKey => { - delete _storage[aKey]; - }); - } else { - delete _storage[key]; - } - }), - }; - - return browserMock; -}; diff --git a/test/features/assignment.test.js b/test/features/assignment.test.js index 632c60b..43bb981 100644 --- a/test/features/assignment.test.js +++ b/test/features/assignment.test.js @@ -1,12 +1,12 @@ describe("Assignment Feature", () => { - const activeTab = { - id: 1, - cookieStoreId: "firefox-container-1", - url: "http://example.com", - index: 0 - }; + const url = "http://example.com"; + + let activeTab; beforeEach(async () => { - await helper.browser.initializeWithTab(activeTab); + activeTab = await helper.browser.initializeWithTab({ + cookieStoreId: "firefox-container-1", + url + }); }); describe("click the 'Always open in' checkbox in the popup", () => { @@ -16,23 +16,24 @@ describe("Assignment Feature", () => { }); describe("open new Tab with the assigned URL in the default container", () => { - const newTab = { - id: 2, - cookieStoreId: "firefox-default", - url: activeTab.url, - index: 1, - active: true - }; + let newTab; beforeEach(async () => { // new Tab opening activeTab.url in default container - await helper.browser.openNewTab(newTab); + newTab = await helper.browser.openNewTab({ + cookieStoreId: "firefox-default", + url + }, { + options: { + webRequestError: true // because request is canceled due to reopening + } + }); }); it("should open the confirm page", async () => { // should have created a new tab with the confirm page - background.browser.tabs.create.should.have.been.calledWith({ - url: "moz-extension://multi-account-containers/confirm-page.html?" + - `url=${encodeURIComponent(activeTab.url)}` + + background.browser.tabs.create.should.have.been.calledWithMatch({ + url: "moz-extension://fake/confirm-page.html?" + + `url=${encodeURIComponent(url)}` + `&cookieStoreId=${activeTab.cookieStoreId}`, cookieStoreId: undefined, openerTabId: null, @@ -53,16 +54,12 @@ describe("Assignment Feature", () => { }); describe("open new Tab with the no longer assigned URL in the default container", () => { - const newTab = { - id: 3, - cookieStoreId: "firefox-default", - url: activeTab.url, - index: 3, - active: true - }; beforeEach(async () => { // new Tab opening activeTab.url in default container - await helper.browser.openNewTab(newTab); + await helper.browser.openNewTab({ + cookieStoreId: "firefox-default", + url + }); }); it("should not open the confirm page", async () => { diff --git a/test/features/containers.test.js b/test/features/containers.test.js new file mode 100644 index 0000000..ac5bccd --- /dev/null +++ b/test/features/containers.test.js @@ -0,0 +1,28 @@ +describe("Containers Management", () => { + beforeEach(async () => { + await helper.browser.initializeWithTab(); + }); + + describe("creating a new container", () => { + beforeEach(async () => { + await helper.popup.clickElementById("container-add-link"); + await helper.popup.clickElementById("edit-container-ok-link"); + }); + + it("should create it in the browser as well", () => { + background.browser.contextualIdentities.create.should.have.been.calledOnce; + }); + + describe("removing it afterwards", () => { + beforeEach(async () => { + await helper.popup.clickElementById("edit-containers-link"); + await helper.popup.clickLastMatchingElementByQuerySelector(".delete-container-icon"); + await helper.popup.clickElementById("delete-container-ok-link"); + }); + + it("should remove it in the browser as well", () => { + background.browser.contextualIdentities.remove.should.have.been.calledOnce; + }); + }); + }); +}); \ No newline at end of file diff --git a/test/features/external-webextensions.test.js b/test/features/external-webextensions.test.js index fe32e4d..30e6b49 100644 --- a/test/features/external-webextensions.test.js +++ b/test/features/external-webextensions.test.js @@ -1,12 +1,11 @@ describe("External Webextensions", () => { - const activeTab = { - id: 1, - cookieStoreId: "firefox-container-1", - url: "http://example.com", - index: 0 - }; + const url = "http://example.com"; + beforeEach(async () => { - await helper.browser.initializeWithTab(activeTab); + await helper.browser.initializeWithTab({ + cookieStoreId: "firefox-container-1", + url + }); await helper.popup.clickElementById("container-page-assigned"); }); @@ -18,25 +17,18 @@ describe("External Webextensions", () => { const message = { method: "getAssignment", - url: "http://example.com" + url }; const sender = { id: "external-webextension" }; - // currently not possible to get the return value of yielding with sinon - // so we expect that if no error is thrown and the storage was called, everything is ok - // maybe i get around to provide a PR https://github.com/sinonjs/sinon/issues/903 - // - // the alternative would be to expose the actual messageHandler and call it directly - // but personally i think that goes against the black-box-ish nature of these feature tests - const rejectionStub = sinon.stub(); - process.on("unhandledRejection", rejectionStub); - background.browser.runtime.onMessageExternal.addListener.yield(message, sender); - await nextTick(); - process.removeListener("unhandledRejection", rejectionStub); - rejectionStub.should.not.have.been.called; - background.browser.storage.local.get.should.have.been.called; + const [promise] = background.browser.runtime.onMessageExternal.addListener.yield(message, sender); + const answer = await promise; + expect(answer).to.deep.equal({ + userContextId: "1", + neverAsk: false + }); }); }); @@ -48,20 +40,16 @@ describe("External Webextensions", () => { const message = { method: "getAssignment", - url: "http://example.com" + url }; const sender = { id: "external-webextension" }; - const rejectionStub = sinon.spy(); - process.on("unhandledRejection", rejectionStub); - background.browser.runtime.onMessageExternal.addListener.yield(message, sender); - await nextTick(); - process.removeListener("unhandledRejection", rejectionStub); - rejectionStub.should.have.been.calledWith(sinon.match({ - message: "Missing contextualIdentities permission" - })); + const [promise] = background.browser.runtime.onMessageExternal.addListener.yield(message, sender); + return promise.catch(error => { + expect(error.message).to.equal("Missing contextualIdentities permission"); + }); }); }); }); diff --git a/test/helper.js b/test/helper.js index 555ee7a..2704bac 100644 --- a/test/helper.js +++ b/test/helper.js @@ -1,47 +1,44 @@ module.exports = { browser: { - async initializeWithTab(tab) { - await buildBackgroundDom({ - beforeParse(window) { - window.browser.tabs.get.resolves(tab); - window.browser.tabs.query.resolves([tab]); - window.browser.contextualIdentities.get.resolves({ - cookieStoreId: tab.cookieStoreId - }); - } - }); - await buildPopupDom({ - beforeParse(window) { - window.browser.tabs.get.resolves(tab); - window.browser.tabs.query.resolves([tab]); + async 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 = {}) { - if (options.resetHistory) { - background.browser.tabs.create.resetHistory(); - background.browser.tabs.remove.resetHistory(); - } - background.browser.tabs.get.resolves(tab); - background.browser.tabs.onCreated.addListener.yield(tab); - const [promise] = background.browser.webRequest.onBeforeRequest.addListener.yield({ - frameId: 0, - tabId: tab.id, - url: tab.url, - requestId: options.requestId - }); - - return promise; + return background.browser.tabs._create(tab, options); } }, popup: { async clickElementById(id) { - const clickEvent = popup.document.createEvent("HTMLEvents"); - clickEvent.initEvent("click"); - popup.document.getElementById(id).dispatchEvent(clickEvent); - await nextTick(); + await popup.helper.clickElementById(id); + }, + + async clickLastMatchingElementByQuerySelector(querySelector) { + await popup.helper.clickElementByQuerySelectorAll(querySelector, "last"); } - }, + } }; diff --git a/test/issues/1168.test.js b/test/issues/1168.test.js new file mode 100644 index 0000000..50f463a --- /dev/null +++ b/test/issues/1168.test.js @@ -0,0 +1,34 @@ +describe("#1168", () => { + describe("when navigation happens too slow after opening new tab to a page which then redirects", () => { + let clock, tab; + + beforeEach(async () => { + await helper.browser.initializeWithTab({ + cookieStoreId: "firefox-container-1", + url: "https://bugzilla.mozilla.org" + }); + await helper.popup.clickElementById("container-page-assigned"); + + clock = sinon.useFakeTimers(); + tab = await helper.browser.openNewTab({}); + + clock.tick(2000); + + await background.browser.tabs._navigate(tab.id, "https://duckduckgo.com/?q=%21bugzilla+thing&t=ffab"); + await background.browser.tabs._redirect(tab.id, [ + "https://bugzilla.mozilla.org" + ]); + }); + + // Not solved yet + // See: https://github.com/mozilla/multi-account-containers/issues/1168#issuecomment-378394091 + it.skip("should remove the old tab", async () => { + expect(background.browser.tabs.create).to.have.been.calledOnce; + expect(background.browser.tabs.remove).to.have.been.calledWith(tab.id); + }); + + afterEach(() => { + clock.restore(); + }); + }); +}); \ No newline at end of file diff --git a/test/issues/940.test.js b/test/issues/940.test.js index b889035..4a7eb0e 100644 --- a/test/issues/940.test.js +++ b/test/issues/940.test.js @@ -1,112 +1,82 @@ describe("#940", () => { describe("when other onBeforeRequestHandlers are faster and redirect with the same requestId", () => { it("should not open two confirm pages", async () => { - // init - const activeTab = { - id: 1, + await helper.browser.initializeWithTab({ cookieStoreId: "firefox-container-1", - url: "http://example.com", - index: 0 - }; - await helper.browser.initializeWithTab(activeTab); - // assign the activeTab.url + url: "http://example.com" + }); await helper.popup.clickElementById("container-page-assigned"); - // start request and don't await the requests at all - // so the second request below is actually comparable to an actual redirect that also fires immediately - const newTab = { - id: 2, - cookieStoreId: "firefox-default", - url: activeTab.url, - index: 1, - active: true - }; - helper.browser.openNewTab(newTab, { - requestId: 1 + const responses = {}; + await helper.browser.openNewTab({ + url: "http://example.com" + }, { + options: { + webRequestRedirects: ["https://example.com"], + webRequestError: true, + instantRedirects: true + }, + responses }); - // other addon sees the same request - // and redirects to the https version of activeTab.url - // since it's a redirect the request has the same requestId - background.browser.webRequest.onBeforeRequest.addListener.yield({ - frameId: 0, - tabId: newTab.id, - url: "https://example.com", - requestId: 1 + const result = await responses.webRequest.onBeforeRequest[1]; + expect(result).to.deep.equal({ + cancel: true }); - await nextTick(); - background.browser.tabs.create.should.have.been.calledOnce; }); }); describe("when redirects change requestId midflight", () => { - let promiseResults; + let newTab; + const newTabResponses = {}; + const redirectedRequest = async (options = {}) => { + 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 () => { - // init - const activeTab = { - id: 1, + await helper.browser.initializeWithTab({ cookieStoreId: "firefox-container-1", - url: "https://www.youtube.com", - index: 0 - }; - await helper.browser.initializeWithTab(activeTab); - // assign the activeTab.url + url: "https://www.youtube.com" + }); await helper.popup.clickElementById("container-page-assigned"); - - // http://youtube.com - const newTab = { - id: 2, - cookieStoreId: "firefox-default", - url: "http://youtube.com", - index: 1, - active: true - }; - const promise1 = helper.browser.openNewTab(newTab, { - requestId: 1 - }); - - // https://youtube.com - const [promise2] = background.browser.webRequest.onBeforeRequest.addListener.yield({ - frameId: 0, - tabId: newTab.id, - url: "https://youtube.com", - requestId: 1 - }); - - // https://www.youtube.com - const [promise3] = background.browser.webRequest.onBeforeRequest.addListener.yield({ - frameId: 0, - tabId: newTab.id, - url: "https://www.youtube.com", - requestId: 1 - }); - - // https://www.youtube.com - const [promise4] = background.browser.webRequest.onBeforeRequest.addListener.yield({ - frameId: 0, - tabId: newTab.id, - url: "https://www.youtube.com", - requestId: 2 - }); - - promiseResults = await Promise.all([promise1, promise2, promise3, promise4]); }); it("should not open two confirm pages", async () => { + await redirectedRequest(); + // http://youtube.com is not assigned, no cancel, no reopening - expect(promiseResults[0]).to.deep.equal({}); + expect(await newTabResponses.webRequest.onBeforeRequest[0]).to.deep.equal({}); // https://youtube.com is not assigned, no cancel, no reopening - expect(promiseResults[1]).to.deep.equal({}); + expect(await newTabResponses.webRequest.onBeforeRequest[1]).to.deep.equal({}); // https://www.youtube.com is assigned, this triggers reopening, cancel - expect(promiseResults[2]).to.deep.equal({ + expect(await newTabResponses.webRequest.onBeforeRequest[2]).to.deep.equal({ cancel: true }); // https://www.youtube.com is assigned, this was a redirect, cancel early, no reopening - expect(promiseResults[3]).to.deep.equal({ + expect(await newTabResponses.webRequest.onBeforeRequest[3]).to.deep.equal({ cancel: true }); @@ -114,67 +84,85 @@ describe("#940", () => { }); it("should uncancel after webRequest.onCompleted", async () => { - const [promise1] = background.browser.webRequest.onCompleted.addListener.yield({ - tabId: 2 + await redirectedRequest(); + // 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 + background.browser.webRequest.onCompleted.addListener = sinon.stub(); + background.browser.tabs.create.resetHistory(); + // we create a tab with the same id and use the same request id to see if uncanceled + await helper.browser.openNewTab({ + id: newTab.id, + url: "https://www.youtube.com" + }, { + options: { + webRequest: { + requestId: newTabResponses.webRequest.request.requestId + } + } }); - await promise1; - const [promise2] = background.browser.webRequest.onBeforeRequest.addListener.yield({ - frameId: 0, - tabId: 2, - url: "https://www.youtube.com", - requestId: 123 - }); - await promise2; - - background.browser.tabs.create.should.have.been.calledTwice; + background.browser.tabs.create.should.have.been.calledOnce; }); it("should uncancel after webRequest.onErrorOccurred", async () => { - const [promise1] = background.browser.webRequest.onErrorOccurred.addListener.yield({ - tabId: 2 + await redirectedRequest(); + background.browser.tabs.create.resetHistory(); + // we create a tab with the same id and use the same request id to see if uncanceled + await helper.browser.openNewTab({ + id: newTab.id, + url: "https://www.youtube.com" + }, { + options: { + webRequest: { + requestId: newTabResponses.webRequest.request.requestId + }, + webRequestError: true + } }); - await promise1; - // request to assigned url in same tab - const [promise2] = background.browser.webRequest.onBeforeRequest.addListener.yield({ - frameId: 0, - tabId: 2, - url: "https://www.youtube.com", - requestId: 123 - }); - await promise2; - - background.browser.tabs.create.should.have.been.calledTwice; + background.browser.tabs.create.should.have.been.calledOnce; }); it("should uncancel after 2 seconds", async () => { - await new Promise(resolve => setTimeout(resolve, 2000)); - // request to assigned url in same tab - const [promise2] = background.browser.webRequest.onBeforeRequest.addListener.yield({ - frameId: 0, - tabId: 2, - url: "https://www.youtube.com", - requestId: 123 + await redirectedRequest({ + webRequestDontYield: ["onCompleted", "onErrorOccurred"] }); - await promise2; + global.clock.tick(2000); - background.browser.tabs.create.should.have.been.calledTwice; - }).timeout(2002); + background.browser.tabs.create.resetHistory(); + // we create a tab with the same id and use the same request id to see if uncanceled + await helper.browser.openNewTab({ + id: newTab.id, + url: "https://www.youtube.com" + }, { + options: { + webRequest: { + requestId: newTabResponses.webRequest.request.requestId + }, + webRequestError: true + } + }); + + background.browser.tabs.create.should.have.been.calledOnce; + }); it("should not influence the canceled url in other tabs", async () => { - const newTab = { - id: 123, + await redirectedRequest(); + background.browser.tabs.create.resetHistory(); + await helper.browser.openNewTab({ cookieStoreId: "firefox-default", - url: "https://www.youtube.com", - index: 10, - active: true - }; - await helper.browser.openNewTab(newTab, { - requestId: 321 + url: "https://www.youtube.com" + }, { + options: { + webRequestError: true + } }); - background.browser.tabs.create.should.have.been.calledTwice; + background.browser.tabs.create.should.have.been.calledOnce; + }); + + afterEach(() => { + global.clock.restore(); }); }); }); diff --git a/test/setup.js b/test/setup.js index b6d563d..0cc45ee 100644 --- a/test/setup.js +++ b/test/setup.js @@ -2,7 +2,6 @@ if (!process.listenerCount("unhandledRejection")) { // eslint-disable-next-line no-console process.on("unhandledRejection", r => console.log(r)); } -const jsdom = require("jsdom"); const path = require("path"); const chai = require("chai"); const sinonChai = require("sinon-chai"); @@ -19,83 +18,64 @@ global.nextTick = () => { }; global.helper = require("./helper"); -const browserMock = require("./browser.mock"); -const srcBasePath = path.resolve(path.join(__dirname, "..", "src")); -const srcJsBackgroundPath = path.join(srcBasePath, "js", "background"); -global.buildBackgroundDom = async (options = {}) => { - const dom = await jsdom.JSDOM.fromFile(path.join(srcJsBackgroundPath, "index.html"), { - runScripts: "dangerously", - resources: "usable", - virtualConsole: (new jsdom.VirtualConsole).sendTo(console), - beforeParse(window) { - window.browser = browserMock(); - window.fetch = sinon.stub().resolves({ - json: sinon.stub().resolves({}) - }); - if (options.beforeParse) { - options.beforeParse(window); +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"]}); } } - }); - await new Promise(resolve => { - dom.window.document.addEventListener("DOMContentLoaded", resolve); - }); - await nextTick(); - - global.background = { - dom, - browser: dom.window.browser }; -}; -global.buildPopupDom = async (options = {}) => { - const dom = await jsdom.JSDOM.fromFile(path.join(srcBasePath, "popup.html"), { - runScripts: "dangerously", - resources: "usable", - virtualConsole: (new jsdom.VirtualConsole).sendTo(console), - beforeParse(window) { - window.browser = browserMock(); - window.browser.storage.local.set("browserActionBadgesClicked", []); - window.browser.storage.local.set("onboarding-stage", 5); - window.browser.storage.local.set("achievements", []); - window.browser.storage.local.set.resetHistory(); - window.fetch = sinon.stub().resolves({ - json: sinon.stub().resolves({}) - }); - - if (options.beforeParse) { - options.beforeParse(window); - } + popup = { + ...popup, + jsdom: { + ...popup.jsdom, + pretendToBeVisual: true } - }); - await new Promise(resolve => { - dom.window.document.addEventListener("DOMContentLoaded", resolve); - }); - await nextTick(); - dom.window.browser.runtime.sendMessage.resetHistory(); - - if (global.background) { - dom.window.browser.runtime.sendMessage = sinon.spy(function() { - global.background.browser.runtime.onMessage.addListener.yield(...arguments); - }); - } - - global.popup = { - dom, - document: dom.window.document, - browser: dom.window.browser }; + + 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.dom.window.close(); - delete global.background; + global.background.destroy(); } if (global.popup) { - global.popup.dom.window.close(); - delete global.popup; + global.popup.destroy(); } });