summaryrefslogtreecommitdiffstats
path: root/browser/components/extensions/test/browser/head.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/extensions/test/browser/head.js')
-rw-r--r--browser/components/extensions/test/browser/head.js1054
1 files changed, 1054 insertions, 0 deletions
diff --git a/browser/components/extensions/test/browser/head.js b/browser/components/extensions/test/browser/head.js
new file mode 100644
index 0000000000..786f183011
--- /dev/null
+++ b/browser/components/extensions/test/browser/head.js
@@ -0,0 +1,1054 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* exported CustomizableUI makeWidgetId focusWindow forceGC
+ * getBrowserActionWidget assertPersistentListeners
+ * clickBrowserAction clickPageAction clickPageActionInPanel
+ * triggerPageActionWithKeyboard triggerPageActionWithKeyboardInPanel
+ * triggerBrowserActionWithKeyboard
+ * getBrowserActionPopup getPageActionPopup getPageActionButton
+ * openBrowserActionPanel
+ * closeBrowserAction closePageAction
+ * promisePopupShown promisePopupHidden promisePopupNotificationShown
+ * toggleBookmarksToolbar
+ * openContextMenu closeContextMenu promiseContextMenuClosed
+ * openContextMenuInSidebar openContextMenuInPopup
+ * openExtensionContextMenu closeExtensionContextMenu
+ * openActionContextMenu openSubmenu closeActionContextMenu
+ * openTabContextMenu closeTabContextMenu
+ * openToolsMenu closeToolsMenu
+ * imageBuffer imageBufferFromDataURI
+ * getInlineOptionsBrowser
+ * getListStyleImage getPanelForNode
+ * awaitExtensionPanel awaitPopupResize
+ * promiseContentDimensions alterContent
+ * promisePrefChangeObserved openContextMenuInFrame
+ * promiseAnimationFrame getCustomizableUIPanelID
+ * awaitEvent BrowserWindowIterator
+ * navigateTab historyPushState promiseWindowRestored
+ * getIncognitoWindow startIncognitoMonitorExtension
+ * loadTestSubscript awaitBrowserLoaded backgroundColorSetOnRoot
+ * getScreenAt roundCssPixcel getCssAvailRect isRectContained
+ */
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// This bug should be fixed, but for the moment all tests in this directory
+// allow various classes of promise rejections.
+//
+// NOTE: Allowing rejections on an entire directory should be avoided.
+// Normally you should use "expectUncaughtRejection" to flag individual
+// failures.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /Message manager disconnected/
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/No matching message handler/);
+PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /Receiving end does not exist/
+);
+
+const { AppUiTestDelegate, AppUiTestInternals } = ChromeUtils.importESModule(
+ "resource://testing-common/AppUiTestDelegate.sys.mjs"
+);
+
+const { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+const { ClientEnvironmentBase } = ChromeUtils.importESModule(
+ "resource://gre/modules/components-utils/ClientEnvironment.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ Management: "resource://gre/modules/Extension.sys.mjs",
+});
+
+var { makeWidgetId, promisePopupShown, getPanelForNode, awaitBrowserLoaded } =
+ AppUiTestInternals;
+
+// The extension tests can run a lot slower under ASAN.
+if (AppConstants.ASAN) {
+ requestLongerTimeout(5);
+}
+
+function loadTestSubscript(filePath) {
+ Services.scriptloader.loadSubScript(new URL(filePath, gTestPath).href, this);
+}
+
+// Ensure when we turn off topsites in the next few lines,
+// we don't hit any remote endpoints.
+Services.prefs
+ .getDefaultBranch("browser.newtabpage.activity-stream.")
+ .setStringPref("discoverystream.endpointSpocsClear", "");
+// Leaving Top Sites enabled during these tests would create site screenshots
+// and update pinned Top Sites unnecessarily.
+Services.prefs
+ .getDefaultBranch("browser.newtabpage.activity-stream.")
+ .setBoolPref("feeds.topsites", false);
+Services.prefs
+ .getDefaultBranch("browser.newtabpage.activity-stream.")
+ .setBoolPref("feeds.system.topsites", false);
+
+{
+ // Touch the recipeParentPromise lazy getter so we don't get
+ // `this._recipeManager is undefined` errors during tests.
+ const { LoginManagerParent } = ChromeUtils.importESModule(
+ "resource://gre/modules/LoginManagerParent.sys.mjs"
+ );
+ void LoginManagerParent.recipeParentPromise;
+}
+
+// Persistent Listener test functionality
+const { assertPersistentListeners } = ExtensionTestUtils.testAssertions;
+
+// Bug 1239884: Our tests occasionally hit a long GC pause at unpredictable
+// times in debug builds, which results in intermittent timeouts. Until we have
+// a better solution, we force a GC after certain strategic tests, which tend to
+// accumulate a high number of unreaped windows.
+function forceGC() {
+ if (AppConstants.DEBUG) {
+ Cu.forceGC();
+ }
+}
+
+var focusWindow = async function focusWindow(win) {
+ if (Services.focus.activeWindow == win) {
+ return;
+ }
+
+ let promise = new Promise(resolve => {
+ win.addEventListener(
+ "focus",
+ function () {
+ resolve();
+ },
+ { capture: true, once: true }
+ );
+ });
+
+ win.focus();
+ await promise;
+};
+
+function imageBufferFromDataURI(encodedImageData) {
+ let decodedImageData = atob(encodedImageData);
+ return Uint8Array.from(decodedImageData, byte => byte.charCodeAt(0)).buffer;
+}
+
+let img =
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==";
+var imageBuffer = imageBufferFromDataURI(img);
+
+function getInlineOptionsBrowser(aboutAddonsBrowser) {
+ let { contentDocument } = aboutAddonsBrowser;
+ return contentDocument.getElementById("addon-inline-options");
+}
+
+function getListStyleImage(button) {
+ // Ensure popups are initialized so that the elements are rendered and
+ // getComputedStyle works.
+ for (
+ let popup = button.closest("panel,menupopup");
+ popup;
+ popup = popup.parentElement?.closest("panel,menupopup")
+ ) {
+ popup.ensureInitialized();
+ }
+
+ let style = button.ownerGlobal.getComputedStyle(button);
+
+ let match = /^url\("(.*)"\)$/.exec(style.listStyleImage);
+
+ return match && match[1];
+}
+
+function promiseAnimationFrame(win = window) {
+ return AppUiTestInternals.promiseAnimationFrame(win);
+}
+
+function promisePopupHidden(popup) {
+ return new Promise(resolve => {
+ let onPopupHidden = event => {
+ popup.removeEventListener("popuphidden", onPopupHidden);
+ resolve();
+ };
+ popup.addEventListener("popuphidden", onPopupHidden);
+ });
+}
+
+/**
+ * Wait for the given PopupNotification to display
+ *
+ * @param {string} name
+ * The name of the notification to wait for.
+ * @param {Window} [win]
+ * The chrome window in which to wait for the notification.
+ *
+ * @returns {Promise}
+ * Resolves with the notification window.
+ */
+function promisePopupNotificationShown(name, win = window) {
+ return new Promise(resolve => {
+ function popupshown() {
+ let notification = win.PopupNotifications.getNotification(name);
+ if (!notification) {
+ return;
+ }
+
+ ok(notification, `${name} notification shown`);
+ ok(win.PopupNotifications.isPanelOpen, "notification panel open");
+
+ win.PopupNotifications.panel.removeEventListener(
+ "popupshown",
+ popupshown
+ );
+ resolve(win.PopupNotifications.panel.firstElementChild);
+ }
+
+ win.PopupNotifications.panel.addEventListener("popupshown", popupshown);
+ });
+}
+
+function promisePossiblyInaccurateContentDimensions(browser) {
+ return SpecialPowers.spawn(browser, [], async function () {
+ function copyProps(obj, props) {
+ let res = {};
+ for (let prop of props) {
+ res[prop] = obj[prop];
+ }
+ return res;
+ }
+
+ return {
+ window: copyProps(content, [
+ "innerWidth",
+ "innerHeight",
+ "outerWidth",
+ "outerHeight",
+ "scrollX",
+ "scrollY",
+ "scrollMaxX",
+ "scrollMaxY",
+ ]),
+ body: copyProps(content.document.body, [
+ "clientWidth",
+ "clientHeight",
+ "scrollWidth",
+ "scrollHeight",
+ ]),
+ root: copyProps(content.document.documentElement, [
+ "clientWidth",
+ "clientHeight",
+ "scrollWidth",
+ "scrollHeight",
+ ]),
+ isStandards: content.document.compatMode !== "BackCompat",
+ };
+ });
+}
+
+function delay(ms = 0) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+/**
+ * Retrieve the content dimensions (and wait until the content gets to the.
+ * size of the browser element they are loaded into, optionally tollerating
+ * size differences to prevent intermittent failures).
+ *
+ * @param {BrowserElement} browser
+ * The browser element where the content has been loaded.
+ * @param {number} [tolleratedWidthSizeDiff]
+ * width size difference to tollerate in pixels (defaults to 1).
+ *
+ * @returns {Promise<object>}
+ * An object with the dims retrieved from the content.
+ */
+async function promiseContentDimensions(browser, tolleratedWidthSizeDiff = 1) {
+ // For remote browsers, each resize operation requires an asynchronous
+ // round-trip to resize the content window. Since there's a certain amount of
+ // unpredictability in the timing, mainly due to the unpredictability of
+ // reflows, we need to wait until the content window dimensions match the
+ // <browser> dimensions before returning data.
+
+ let dims = await promisePossiblyInaccurateContentDimensions(browser);
+ while (
+ Math.abs(browser.clientWidth - dims.window.innerWidth) >
+ tolleratedWidthSizeDiff ||
+ browser.clientHeight !== Math.round(dims.window.innerHeight)
+ ) {
+ const diffWidth = Math.abs(browser.clientWidth - dims.window.innerWidth);
+ const diffHeight = Math.abs(browser.clientHeight - dims.window.innerHeight);
+ info(
+ `Content dimension did not reached the expected size yet (diff: ${diffWidth}x${diffHeight}). Wait further.`
+ );
+ await delay(50);
+ dims = await promisePossiblyInaccurateContentDimensions(browser);
+ }
+
+ return dims;
+}
+
+async function awaitPopupResize(browser) {
+ await BrowserTestUtils.waitForEvent(
+ browser,
+ "WebExtPopupResized",
+ event => event.detail === "delayed"
+ );
+
+ return promiseContentDimensions(browser);
+}
+
+function alterContent(browser, task, arg = null) {
+ return Promise.all([
+ SpecialPowers.spawn(browser, [arg], task),
+ awaitPopupResize(browser),
+ ]).then(([, dims]) => dims);
+}
+
+async function focusButtonAndPressKey(key, elem, modifiers) {
+ let focused = BrowserTestUtils.waitForEvent(elem, "focus", true);
+
+ elem.setAttribute("tabindex", "-1");
+ elem.focus();
+ elem.removeAttribute("tabindex");
+ await focused;
+
+ EventUtils.synthesizeKey(key, modifiers);
+ elem.blur();
+}
+
+var awaitExtensionPanel = function (extension, win = window, awaitLoad = true) {
+ return AppUiTestDelegate.awaitExtensionPanel(win, extension.id, awaitLoad);
+};
+
+function getCustomizableUIPanelID(win = window) {
+ return CustomizableUI.AREA_ADDONS;
+}
+
+function getBrowserActionWidget(extension) {
+ return AppUiTestInternals.getBrowserActionWidget(extension.id);
+}
+
+function getBrowserActionPopup(extension, win = window) {
+ let group = getBrowserActionWidget(extension);
+
+ if (group.areaType == CustomizableUI.TYPE_TOOLBAR) {
+ return win.document.getElementById("customizationui-widget-panel");
+ }
+
+ return win.gUnifiedExtensions.panel;
+}
+
+var showBrowserAction = function (extension, win = window) {
+ return AppUiTestInternals.showBrowserAction(win, extension.id);
+};
+
+function clickBrowserAction(extension, win = window, modifiers) {
+ return AppUiTestDelegate.clickBrowserAction(win, extension.id, modifiers);
+}
+
+async function triggerBrowserActionWithKeyboard(
+ extension,
+ key = "KEY_Enter",
+ modifiers = {},
+ win = window
+) {
+ await promiseAnimationFrame(win);
+ await showBrowserAction(extension, win);
+
+ let group = getBrowserActionWidget(extension);
+ let node = group.forWindow(win).node.firstElementChild;
+
+ if (group.areaType == CustomizableUI.TYPE_TOOLBAR) {
+ await focusButtonAndPressKey(key, node, modifiers);
+ } else if (group.areaType == CustomizableUI.TYPE_PANEL) {
+ // Use key navigation so that the PanelMultiView doesn't ignore key events.
+ let panel = win.gUnifiedExtensions.panel;
+ while (win.document.activeElement != node) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ ok(
+ panel.contains(win.document.activeElement),
+ "Focus is inside the panel"
+ );
+ }
+ EventUtils.synthesizeKey(key, modifiers);
+ }
+}
+
+function closeBrowserAction(extension, win = window) {
+ return AppUiTestDelegate.closeBrowserAction(win, extension.id);
+}
+
+function openBrowserActionPanel(extension, win = window, awaitLoad = false) {
+ clickBrowserAction(extension, win);
+
+ return awaitExtensionPanel(extension, win, awaitLoad);
+}
+
+async function toggleBookmarksToolbar(visible = true) {
+ let bookmarksToolbar = document.getElementById("PersonalToolbar");
+ // Third parameter is 'persist' and true is the default.
+ // Fourth parameter is 'animated' and we want no animation.
+ setToolbarVisibility(bookmarksToolbar, visible, true, false);
+ if (!visible) {
+ return BrowserTestUtils.waitForMutationCondition(
+ bookmarksToolbar,
+ { attributes: true },
+ () => bookmarksToolbar.collapsed
+ );
+ }
+
+ return BrowserTestUtils.waitForEvent(
+ bookmarksToolbar,
+ "BookmarksToolbarVisibilityUpdated"
+ );
+}
+
+async function openContextMenuInPopup(
+ extension,
+ selector = "body",
+ win = window
+) {
+ let doc = win.document;
+ let contentAreaContextMenu = doc.getElementById("contentAreaContextMenu");
+ let browser = await awaitExtensionPanel(extension, win);
+
+ // Ensure that the document layout has been flushed before triggering the mouse event
+ // (See Bug 1519808 for a rationale).
+ await browser.ownerGlobal.promiseDocumentFlushed(() => {});
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ selector,
+ { type: "mousedown", button: 2 },
+ browser
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ selector,
+ { type: "contextmenu" },
+ browser
+ );
+ await popupShownPromise;
+ return contentAreaContextMenu;
+}
+
+async function openContextMenuInSidebar(selector = "body") {
+ let contentAreaContextMenu = SidebarUI.browser.contentDocument.getElementById(
+ "contentAreaContextMenu"
+ );
+ let browser = SidebarUI.browser.contentDocument.getElementById(
+ "webext-panels-browser"
+ );
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popupshown"
+ );
+
+ // Wait for the layout to be flushed, otherwise this test may
+ // fail intermittently if synthesizeMouseAtCenter is being called
+ // while the sidebar is still opening and the browser window layout
+ // being recomputed.
+ await SidebarUI.browser.contentWindow.promiseDocumentFlushed(() => {});
+
+ info("Opening context menu in sidebarAction panel");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ selector,
+ { type: "mousedown", button: 2 },
+ browser
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ selector,
+ { type: "contextmenu" },
+ browser
+ );
+ await popupShownPromise;
+ return contentAreaContextMenu;
+}
+
+// `selector` should refer to the content in the frame. If invalid the test can
+// fail intermittently because the click could inadvertently be registered on
+// the upper-left corner of the frame (instead of inside the frame).
+async function openContextMenuInFrame(selector = "body", frameIndex = 0) {
+ let contentAreaContextMenu = document.getElementById(
+ "contentAreaContextMenu"
+ );
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ selector,
+ { type: "contextmenu" },
+ gBrowser.selectedBrowser.browsingContext.children[frameIndex]
+ );
+ await popupShownPromise;
+ return contentAreaContextMenu;
+}
+
+async function openContextMenu(selector = "#img1", win = window) {
+ let contentAreaContextMenu = win.document.getElementById(
+ "contentAreaContextMenu"
+ );
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ selector,
+ { type: "mousedown", button: 2 },
+ win.gBrowser.selectedBrowser
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ selector,
+ { type: "contextmenu" },
+ win.gBrowser.selectedBrowser
+ );
+ await popupShownPromise;
+ return contentAreaContextMenu;
+}
+
+async function promiseContextMenuClosed(contextMenu) {
+ let contentAreaContextMenu =
+ contextMenu || document.getElementById("contentAreaContextMenu");
+ return BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popuphidden");
+}
+
+async function closeContextMenu(contextMenu, win = window) {
+ let contentAreaContextMenu =
+ contextMenu || win.document.getElementById("contentAreaContextMenu");
+ let closed = promiseContextMenuClosed(contentAreaContextMenu);
+ contentAreaContextMenu.hidePopup();
+ await closed;
+}
+
+async function openExtensionContextMenu(selector = "#img1") {
+ let contextMenu = await openContextMenu(selector);
+ let topLevelMenu = contextMenu.getElementsByAttribute(
+ "ext-type",
+ "top-level-menu"
+ );
+
+ // Return null if the extension only has one item and therefore no extension menu.
+ if (!topLevelMenu.length) {
+ return null;
+ }
+
+ let extensionMenu = topLevelMenu[0];
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ extensionMenu.openMenu(true);
+ await popupShownPromise;
+ return extensionMenu;
+}
+
+async function closeExtensionContextMenu(itemToSelect, modifiers = {}) {
+ let contentAreaContextMenu = document.getElementById(
+ "contentAreaContextMenu"
+ );
+ let popupHiddenPromise = promiseContextMenuClosed(contentAreaContextMenu);
+ if (itemToSelect) {
+ itemToSelect.closest("menupopup").activateItem(itemToSelect, modifiers);
+ } else {
+ contentAreaContextMenu.hidePopup();
+ }
+ await popupHiddenPromise;
+
+ // Bug 1351638: parent menu fails to close intermittently, make sure it does.
+ contentAreaContextMenu.hidePopup();
+}
+
+async function openToolsMenu(win = window) {
+ const node = win.document.getElementById("tools-menu");
+ const menu = win.document.getElementById("menu_ToolsPopup");
+ const shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ if (AppConstants.platform === "macosx") {
+ // We can't open menubar items on OSX, so mocking instead.
+ menu.dispatchEvent(new MouseEvent("popupshowing"));
+ menu.dispatchEvent(new MouseEvent("popupshown"));
+ } else {
+ node.open = true;
+ }
+ await shown;
+ return menu;
+}
+
+function closeToolsMenu(itemToSelect, win = window) {
+ const menu = win.document.getElementById("menu_ToolsPopup");
+ const hidden = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ if (AppConstants.platform === "macosx") {
+ // Mocking on OSX, see above.
+ if (itemToSelect) {
+ itemToSelect.doCommand();
+ }
+ menu.dispatchEvent(new MouseEvent("popuphiding"));
+ menu.dispatchEvent(new MouseEvent("popuphidden"));
+ } else if (itemToSelect) {
+ EventUtils.synthesizeMouseAtCenter(itemToSelect, {}, win);
+ } else {
+ menu.hidePopup();
+ }
+ return hidden;
+}
+
+async function openChromeContextMenu(menuId, target, win = window) {
+ const node = win.document.querySelector(target);
+ const menu = win.document.getElementById(menuId);
+ const shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(node, { type: "contextmenu" }, win);
+ await shown;
+ return menu;
+}
+
+async function openSubmenu(submenuItem, win = window) {
+ const submenu = submenuItem.menupopup;
+ const shown = BrowserTestUtils.waitForEvent(submenu, "popupshown");
+ submenuItem.openMenu(true);
+ await shown;
+ return submenu;
+}
+
+function closeChromeContextMenu(menuId, itemToSelect, win = window) {
+ const menu = win.document.getElementById(menuId);
+ const hidden = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ if (itemToSelect) {
+ itemToSelect.closest("menupopup").activateItem(itemToSelect);
+ } else {
+ menu.hidePopup();
+ }
+ return hidden;
+}
+
+async function openActionContextMenu(extension, kind, win = window) {
+ // See comment from getPageActionButton below.
+ win.gURLBar.setPageProxyState("valid");
+ await promiseAnimationFrame(win);
+ let buttonID;
+ let menuID;
+ if (kind == "page") {
+ buttonID =
+ "#" +
+ BrowserPageActions.urlbarButtonNodeIDForActionID(
+ makeWidgetId(extension.id)
+ );
+ menuID = "pageActionContextMenu";
+ } else {
+ buttonID = `#${makeWidgetId(extension.id)}-${kind}-action`;
+ menuID = "toolbar-context-menu";
+ }
+ return openChromeContextMenu(menuID, buttonID, win);
+}
+
+function closeActionContextMenu(itemToSelect, kind, win = window) {
+ let menuID =
+ kind == "page" ? "pageActionContextMenu" : "toolbar-context-menu";
+ return closeChromeContextMenu(menuID, itemToSelect, win);
+}
+
+function openTabContextMenu(tab = gBrowser.selectedTab) {
+ // The TabContextMenu initializes its strings only on a focus or mouseover event.
+ // Calls focus event on the TabContextMenu before opening.
+ tab.focus();
+ let indexOfTab = Array.prototype.indexOf.call(tab.parentNode.children, tab);
+ return openChromeContextMenu(
+ "tabContextMenu",
+ `.tabbrowser-tab:nth-child(${indexOfTab + 1})`,
+ tab.ownerGlobal
+ );
+}
+
+function closeTabContextMenu(itemToSelect, win = window) {
+ return closeChromeContextMenu("tabContextMenu", itemToSelect, win);
+}
+
+function getPageActionPopup(extension, win = window) {
+ return AppUiTestInternals.getPageActionPopup(win, extension.id);
+}
+
+function getPageActionButton(extension, win = window) {
+ return AppUiTestInternals.getPageActionButton(win, extension.id);
+}
+
+function clickPageAction(extension, win = window, modifiers = {}) {
+ return AppUiTestDelegate.clickPageAction(win, extension.id, modifiers);
+}
+
+// Shows the popup for the page action which for lists
+// all available page actions
+async function showPageActionsPanel(win = window) {
+ // See the comment at getPageActionButton
+ win.gURLBar.setPageProxyState("valid");
+ await promiseAnimationFrame(win);
+
+ let pageActionsPopup = win.document.getElementById("pageActionPanel");
+
+ let popupShownPromise = promisePopupShown(pageActionsPopup);
+ EventUtils.synthesizeMouseAtCenter(
+ win.document.getElementById("pageActionButton"),
+ {},
+ win
+ );
+ await popupShownPromise;
+
+ return pageActionsPopup;
+}
+
+async function clickPageActionInPanel(extension, win = window, modifiers = {}) {
+ let pageActionsPopup = await showPageActionsPanel(win);
+
+ let pageActionId = BrowserPageActions.panelButtonNodeIDForActionID(
+ makeWidgetId(extension.id)
+ );
+
+ let popupHiddenPromise = promisePopupHidden(pageActionsPopup);
+ let widgetButton = win.document.getElementById(pageActionId);
+ EventUtils.synthesizeMouseAtCenter(widgetButton, modifiers, win);
+ if (widgetButton.disabled) {
+ pageActionsPopup.hidePopup();
+ }
+ await popupHiddenPromise;
+
+ return new Promise(SimpleTest.executeSoon);
+}
+
+async function triggerPageActionWithKeyboard(
+ extension,
+ modifiers = {},
+ win = window
+) {
+ let elem = await getPageActionButton(extension, win);
+ await focusButtonAndPressKey("KEY_Enter", elem, modifiers);
+ return new Promise(SimpleTest.executeSoon);
+}
+
+async function triggerPageActionWithKeyboardInPanel(
+ extension,
+ modifiers = {},
+ win = window
+) {
+ let pageActionsPopup = await showPageActionsPanel(win);
+
+ let pageActionId = BrowserPageActions.panelButtonNodeIDForActionID(
+ makeWidgetId(extension.id)
+ );
+
+ let popupHiddenPromise = promisePopupHidden(pageActionsPopup);
+ let widgetButton = win.document.getElementById(pageActionId);
+ if (widgetButton.disabled) {
+ pageActionsPopup.hidePopup();
+ return new Promise(SimpleTest.executeSoon);
+ }
+
+ // Use key navigation so that the PanelMultiView doesn't ignore key events
+ while (win.document.activeElement != widgetButton) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ ok(
+ pageActionsPopup.contains(win.document.activeElement),
+ "Focus is inside of the panel"
+ );
+ }
+ EventUtils.synthesizeKey("KEY_Enter", modifiers);
+ await popupHiddenPromise;
+
+ return new Promise(SimpleTest.executeSoon);
+}
+
+function closePageAction(extension, win = window) {
+ return AppUiTestDelegate.closePageAction(win, extension.id);
+}
+
+function promisePrefChangeObserved(pref) {
+ return new Promise((resolve, reject) =>
+ Preferences.observe(pref, function prefObserver() {
+ Preferences.ignore(pref, prefObserver);
+ resolve();
+ })
+ );
+}
+
+function promiseWindowRestored(window) {
+ return new Promise(resolve =>
+ window.addEventListener("SSWindowRestored", resolve, { once: true })
+ );
+}
+
+function awaitEvent(eventName, id) {
+ return new Promise(resolve => {
+ let listener = (_eventName, ...args) => {
+ let extension = args[0];
+ if (_eventName === eventName && extension.id == id) {
+ Management.off(eventName, listener);
+ resolve();
+ }
+ };
+
+ Management.on(eventName, listener);
+ });
+}
+
+function* BrowserWindowIterator() {
+ for (let currentWindow of Services.wm.getEnumerator("navigator:browser")) {
+ if (!currentWindow.closed) {
+ yield currentWindow;
+ }
+ }
+}
+
+async function locationChange(tab, url, task) {
+ let locationChanged = BrowserTestUtils.waitForLocationChange(gBrowser, url);
+ await SpecialPowers.spawn(tab.linkedBrowser, [url], task);
+ return locationChanged;
+}
+
+function navigateTab(tab, url) {
+ return locationChange(tab, url, url => {
+ content.location.href = url;
+ });
+}
+
+function historyPushState(tab, url) {
+ return locationChange(tab, url, url => {
+ content.history.pushState(null, null, url);
+ });
+}
+
+// This monitor extension runs with incognito: not_allowed, if it receives any
+// events with incognito data it fails.
+async function startIncognitoMonitorExtension() {
+ function background() {
+ // Bug 1513220 - We're unable to get the tab during onRemoved, so we track
+ // valid tabs in "seen" so we can at least validate tabs that we have "seen"
+ // during onRemoved. This means that the monitor extension must be started
+ // prior to creating any tabs that will be removed.
+
+ // Map<tabId -> tab>
+ let seenTabs = new Map();
+ function getTabById(tabId) {
+ return seenTabs.has(tabId)
+ ? seenTabs.get(tabId)
+ : browser.tabs.get(tabId);
+ }
+
+ async function testTab(tabOrId, eventName) {
+ let tab = tabOrId;
+ if (typeof tabOrId == "number") {
+ let tabId = tabOrId;
+ try {
+ tab = await getTabById(tabId);
+ } catch (e) {
+ browser.test.fail(
+ `tabs.${eventName} for id ${tabOrId} unexpected failure ${e}\n`
+ );
+ return;
+ }
+ }
+ browser.test.assertFalse(
+ tab.incognito,
+ `tabs.${eventName} ${tab.id}: monitor extension got expected incognito value`
+ );
+ seenTabs.set(tab.id, tab);
+ }
+ async function testTabInfo(tabInfo, eventName) {
+ if (typeof tabInfo == "number") {
+ await testTab(tabInfo, eventName);
+ } else if (typeof tabInfo == "object") {
+ if (tabInfo.id !== undefined) {
+ await testTab(tabInfo, eventName);
+ } else if (tabInfo.tab !== undefined) {
+ await testTab(tabInfo.tab, eventName);
+ } else if (tabInfo.tabIds !== undefined) {
+ await Promise.all(
+ tabInfo.tabIds.map(tabId => testTab(tabId, eventName))
+ );
+ } else if (tabInfo.tabId !== undefined) {
+ await testTab(tabInfo.tabId, eventName);
+ }
+ }
+ }
+ let tabEvents = [
+ "onUpdated",
+ "onCreated",
+ "onAttached",
+ "onDetached",
+ "onRemoved",
+ "onMoved",
+ "onZoomChange",
+ "onHighlighted",
+ ];
+ for (let eventName of tabEvents) {
+ browser.tabs[eventName].addListener(async details => {
+ await testTabInfo(details, eventName);
+ });
+ }
+ browser.tabs.onReplaced.addListener(async (addedTabId, removedTabId) => {
+ await testTabInfo(addedTabId, "onReplaced (addedTabId)");
+ await testTabInfo(removedTabId, "onReplaced (removedTabId)");
+ });
+
+ // Map<windowId -> window>
+ let seenWindows = new Map();
+ function getWindowById(windowId) {
+ return seenWindows.has(windowId)
+ ? seenWindows.get(windowId)
+ : browser.windows.get(windowId);
+ }
+
+ browser.windows.onCreated.addListener(window => {
+ browser.test.assertFalse(
+ window.incognito,
+ `windows.onCreated monitor extension got expected incognito value`
+ );
+ seenWindows.set(window.id, window);
+ });
+ browser.windows.onRemoved.addListener(async windowId => {
+ let window;
+ try {
+ window = await getWindowById(windowId);
+ } catch (e) {
+ browser.test.fail(
+ `windows.onCreated for id ${windowId} unexpected failure ${e}\n`
+ );
+ return;
+ }
+ browser.test.assertFalse(
+ window.incognito,
+ `windows.onRemoved ${window.id}: monitor extension got expected incognito value`
+ );
+ });
+ browser.windows.onFocusChanged.addListener(async windowId => {
+ if (windowId == browser.windows.WINDOW_ID_NONE) {
+ return;
+ }
+ // onFocusChanged will also fire for blur so check actual window.incognito value.
+ let window;
+ try {
+ window = await getWindowById(windowId);
+ } catch (e) {
+ browser.test.fail(
+ `windows.onFocusChanged for id ${windowId} unexpected failure ${e}\n`
+ );
+ return;
+ }
+ browser.test.assertFalse(
+ window.incognito,
+ `windows.onFocusChanged ${window.id}: monitor extesion got expected incognito value`
+ );
+ seenWindows.set(window.id, window);
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ incognitoOverride: "not_allowed",
+ background,
+ });
+ await extension.startup();
+ return extension;
+}
+
+async function getIncognitoWindow(url = "about:privatebrowsing") {
+ // Since events will be limited based on incognito, we need a
+ // spanning extension to get the tab id so we can test access failure.
+
+ function background(expectUrl) {
+ browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+ if (changeInfo.status === "complete" && tab.url === expectUrl) {
+ browser.test.sendMessage("data", { tabId, windowId: tab.windowId });
+ }
+ });
+ }
+
+ let windowWatcher = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background: `(${background})(${JSON.stringify(url)})`,
+ incognitoOverride: "spanning",
+ });
+
+ await windowWatcher.startup();
+ let data = windowWatcher.awaitMessage("data");
+
+ let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ BrowserTestUtils.loadURIString(win.gBrowser.selectedBrowser, url);
+
+ let details = await data;
+ await windowWatcher.unload();
+ return { win, details };
+}
+
+/**
+ * Windows 7 and 8 set the window's background-color on :root instead of
+ * #navigator-toolbox to avoid bug 1695280. When that bug is fixed, this
+ * function and the assertions it gates can be removed.
+ *
+ * @returns {boolean} True if the window's background-color is set on :root
+ * rather than #navigator-toolbox.
+ */
+function backgroundColorSetOnRoot() {
+ const os = ClientEnvironmentBase.os;
+ if (!os.isWindows) {
+ return false;
+ }
+ return os.windowsVersion < 10;
+}
+
+function getScreenAt(left, top, width, height) {
+ const screenManager = Cc["@mozilla.org/gfx/screenmanager;1"].getService(
+ Ci.nsIScreenManager
+ );
+ return screenManager.screenForRect(left, top, width, height);
+}
+
+function roundCssPixcel(pixel, screen) {
+ return Math.floor(
+ Math.floor(pixel * screen.defaultCSSScaleFactor) /
+ screen.defaultCSSScaleFactor
+ );
+}
+
+function getCssAvailRect(screen) {
+ const availDeviceLeft = {};
+ const availDeviceTop = {};
+ const availDeviceWidth = {};
+ const availDeviceHeight = {};
+ screen.GetAvailRect(
+ availDeviceLeft,
+ availDeviceTop,
+ availDeviceWidth,
+ availDeviceHeight
+ );
+ const factor = screen.defaultCSSScaleFactor;
+ const left = Math.floor(availDeviceLeft.value / factor);
+ const top = Math.floor(availDeviceTop.value / factor);
+ const width = Math.floor(availDeviceWidth.value / factor);
+ const height = Math.floor(availDeviceHeight.value / factor);
+ return {
+ left,
+ top,
+ width,
+ height,
+ right: left + width,
+ bottom: top + height,
+ };
+}
+
+function isRectContained(actualRect, maxRect) {
+ is(
+ `top=${actualRect.top >= maxRect.top},bottom=${
+ actualRect.bottom <= maxRect.bottom
+ },left=${actualRect.left >= maxRect.left},right=${
+ actualRect.right <= maxRect.right
+ }`,
+ "top=true,bottom=true,left=true,right=true",
+ `Dimension must be inside, top:${actualRect.top}>=${maxRect.top}, bottom:${actualRect.bottom}<=${maxRect.bottom}, left:${actualRect.left}>=${maxRect.left}, right:${actualRect.right}<=${maxRect.right}`
+ );
+}