/* -*- 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} * 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 // 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 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 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}` ); }