Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/Promise.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils", "resource://testing-common/PlacesTestUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "TabCrashHandler", "resource:///modules/ContentCrashHandlers.jsm"); /** * Wait for a to be closed then call the specified callback. */ function waitForNotificationClose(notification, cb) { let parent = notification.parentNode; let observer = new MutationObserver(function onMutatations(mutations) { for (let mutation of mutations) { for (let i = 0; i < mutation.removedNodes.length; i++) { let node = mutation.removedNodes.item(i); if (node != notification) { continue; } observer.disconnect(); cb(); } } }); observer.observe(parent, {childList: true}); } function closeAllNotifications() { let notificationBox = document.getElementById("global-notificationbox"); if (!notificationBox || !notificationBox.currentNotification) { return Promise.resolve(); } return new Promise(resolve => { for (let notification of notificationBox.allNotifications) { waitForNotificationClose(notification, function() { if (notificationBox.allNotifications.length === 0) { resolve(); } }); notification.close(); } }); } function whenDelayedStartupFinished(aWindow, aCallback) { Services.obs.addObserver(function observer(aSubject, aTopic) { if (aWindow == aSubject) { Services.obs.removeObserver(observer, aTopic); executeSoon(aCallback); } }, "browser-delayed-startup-finished"); } function updateTabContextMenu(tab, onOpened) { let menu = document.getElementById("tabContextMenu"); if (!tab) tab = gBrowser.selectedTab; var evt = new Event(""); tab.dispatchEvent(evt); menu.openPopup(tab, "end_after", 0, 0, true, false, evt); is(TabContextMenu.contextTab, tab, "TabContextMenu context is the expected tab"); const onFinished = () => menu.hidePopup(); if (onOpened) { return (async function() { await onOpened(); onFinished(); })(); } onFinished(); return Promise.resolve(); } function openToolbarCustomizationUI(aCallback, aBrowserWin) { if (!aBrowserWin) aBrowserWin = window; aBrowserWin.gCustomizeMode.enter(); aBrowserWin.gNavToolbox.addEventListener("customizationready", function() { executeSoon(function() { aCallback(aBrowserWin) }); }, {once: true}); } function closeToolbarCustomizationUI(aCallback, aBrowserWin) { aBrowserWin.gNavToolbox.addEventListener("aftercustomization", function() { executeSoon(aCallback); }, {once: true}); aBrowserWin.gCustomizeMode.exit(); } function waitForCondition(condition, nextTest, errorMsg, retryTimes) { retryTimes = typeof retryTimes !== "undefined" ? retryTimes : 30; var tries = 0; var interval = setInterval(function() { if (tries >= retryTimes) { ok(false, errorMsg); moveOn(); } var conditionPassed; try { conditionPassed = condition(); } catch (e) { ok(false, e + "\n" + e.stack); conditionPassed = false; } if (conditionPassed) { moveOn(); } tries++; }, 100); var moveOn = function() { clearInterval(interval); nextTest(); }; } function promiseWaitForCondition(aConditionFn) { return new Promise(resolve => { waitForCondition(aConditionFn, resolve, "Condition didn't pass."); }); } function promiseWaitForEvent(object, eventName, capturing = false, chrome = false) { return new Promise((resolve) => { function listener(event) { info("Saw " + eventName); object.removeEventListener(eventName, listener, capturing, chrome); resolve(event); } info("Waiting for " + eventName); object.addEventListener(eventName, listener, capturing, chrome); }); } /** * Allows setting focus on a window, and waiting for that window to achieve * focus. * * @param aWindow * The window to focus and wait for. * * @return {Promise} * @resolves When the window is focused. * @rejects Never. */ function promiseWaitForFocus(aWindow) { return new Promise((resolve) => { waitForFocus(resolve, aWindow); }); } function getTestPlugin(aName) { var pluginName = aName || "Test Plug-in"; var ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); var tags = ph.getPluginTags(); // Find the test plugin for (var i = 0; i < tags.length; i++) { if (tags[i].name == pluginName) return tags[i]; } ok(false, "Unable to find plugin"); return null; } // call this to set the test plugin(s) initially expected enabled state. // it will automatically be reset to it's previous value after the test // ends function setTestPluginEnabledState(newEnabledState, pluginName) { var plugin = getTestPlugin(pluginName); var oldEnabledState = plugin.enabledState; plugin.enabledState = newEnabledState; SimpleTest.registerCleanupFunction(function() { getTestPlugin(pluginName).enabledState = oldEnabledState; }); } function pushPrefs(...aPrefs) { return new Promise(resolve => { SpecialPowers.pushPrefEnv({"set": aPrefs}, resolve); }); } function updateBlocklist(aCallback) { var blocklistNotifier = Cc["@mozilla.org/extensions/blocklist;1"] .getService(Ci.nsITimerCallback); var observer = function() { Services.obs.removeObserver(observer, "blocklist-updated"); SimpleTest.executeSoon(aCallback); }; Services.obs.addObserver(observer, "blocklist-updated"); blocklistNotifier.notify(null); } var _originalTestBlocklistURL = null; function setAndUpdateBlocklist(aURL, aCallback) { if (!_originalTestBlocklistURL) _originalTestBlocklistURL = Services.prefs.getCharPref("extensions.blocklist.url"); Services.prefs.setCharPref("extensions.blocklist.url", aURL); updateBlocklist(aCallback); } function resetBlocklist() { Services.prefs.setCharPref("extensions.blocklist.url", _originalTestBlocklistURL); } function whenNewWindowLoaded(aOptions, aCallback) { let win = OpenBrowserWindow(aOptions); win.addEventListener("load", function() { aCallback(win); }, {once: true}); } function promiseWindowWillBeClosed(win) { return new Promise((resolve, reject) => { Services.obs.addObserver(function observe(subject, topic) { if (subject == win) { Services.obs.removeObserver(observe, topic); resolve(); } }, "domwindowclosed"); }); } function promiseWindowClosed(win) { let promise = promiseWindowWillBeClosed(win); win.close(); return promise; } function promiseOpenAndLoadWindow(aOptions, aWaitForDelayedStartup = false) { return new Promise(resolve => { let win = OpenBrowserWindow(aOptions); if (aWaitForDelayedStartup) { Services.obs.addObserver(function onDS(aSubject, aTopic, aData) { if (aSubject != win) { return; } Services.obs.removeObserver(onDS, "browser-delayed-startup-finished"); resolve(win); }, "browser-delayed-startup-finished"); } else { win.addEventListener("load", function() { resolve(win); }, {once: true}); } }); } /** * Waits for all pending async statements on the default connection, before * proceeding with aCallback. * * @param aCallback * Function to be called when done. * @param aScope * Scope for the callback. * @param aArguments * Arguments array for the callback. * * @note The result is achieved by asynchronously executing a query requiring * a write lock. Since all statements on the same connection are * serialized, the end of this write operation means that all writes are * complete. Note that WAL makes so that writers don't block readers, but * this is a problem only across different connections. */ function waitForAsyncUpdates(aCallback, aScope, aArguments) { let scope = aScope || this; let args = aArguments || []; let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) .DBConnection; let begin = db.createAsyncStatement("BEGIN EXCLUSIVE"); begin.executeAsync(); begin.finalize(); let commit = db.createAsyncStatement("COMMIT"); commit.executeAsync({ handleResult() {}, handleError() {}, handleCompletion(aReason) { aCallback.apply(scope, args); } }); commit.finalize(); } /** * Asynchronously check a url is visited. * @param aURI The URI. * @param aExpectedValue The expected value. * @return {Promise} * @resolves When the check has been added successfully. * @rejects JavaScript exception. */ function promiseIsURIVisited(aURI, aExpectedValue) { return new Promise(resolve => { PlacesUtils.asyncHistory.isURIVisited(aURI, function(unused, aIsVisited) { resolve(aIsVisited); }); }); } function whenNewTabLoaded(aWindow, aCallback) { aWindow.BrowserOpenTab(); let browser = aWindow.gBrowser.selectedBrowser; if (browser.contentDocument.readyState === "complete") { aCallback(); return; } whenTabLoaded(aWindow.gBrowser.selectedTab, aCallback); } function whenTabLoaded(aTab, aCallback) { promiseTabLoadEvent(aTab).then(aCallback); } function promiseTabLoaded(aTab) { return new Promise(resolve => { whenTabLoaded(aTab, resolve); }); } /** * Ensures that the specified URIs are either cleared or not. * * @param aURIs * Array of page URIs * @param aShouldBeCleared * True if each visit to the URI should be cleared, false otherwise */ function promiseHistoryClearedState(aURIs, aShouldBeCleared) { return new Promise(resolve => { let callbackCount = 0; let niceStr = aShouldBeCleared ? "no longer" : "still"; function callbackDone() { if (++callbackCount == aURIs.length) resolve(); } aURIs.forEach(function(aURI) { PlacesUtils.asyncHistory.isURIVisited(aURI, function(uri, isVisited) { is(isVisited, !aShouldBeCleared, "history visit " + uri.spec + " should " + niceStr + " exist"); callbackDone(); }); }); }); } /** * Waits for the next top-level document load in the current browser. The URI * of the document is compared against aExpectedURL. The load is then stopped * before it actually starts. * * @param aExpectedURL * The URL of the document that is expected to load. * @param aStopFromProgressListener * Whether to cancel the load directly from the progress listener. Defaults to true. * If you're using this method to avoid hitting the network, you want the default (true). * However, the browser UI will behave differently for loads stopped directly from * the progress listener (effectively in the middle of a call to loadURI) and so there * are cases where you may want to avoid stopping the load directly from within the * progress listener callback. * @return promise */ function waitForDocLoadAndStopIt(aExpectedURL, aBrowser = gBrowser.selectedBrowser, aStopFromProgressListener = true) { function content_script(contentStopFromProgressListener) { let { interfaces: Ci, utils: Cu } = Components; Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); let wp = docShell.QueryInterface(Ci.nsIWebProgress); function stopContent(now, uri) { if (now) { /* Hammer time. */ content.stop(); /* Let the parent know we're done. */ sendAsyncMessage("Test:WaitForDocLoadAndStopIt", { uri }); } else { setTimeout(stopContent.bind(null, true, uri), 0); } } let progressListener = { onStateChange(webProgress, req, flags, status) { dump("waitForDocLoadAndStopIt: onStateChange " + flags.toString(16) + ": " + req.name + "\n"); if (webProgress.isTopLevel && flags & Ci.nsIWebProgressListener.STATE_START) { wp.removeProgressListener(progressListener); let chan = req.QueryInterface(Ci.nsIChannel); dump(`waitForDocLoadAndStopIt: Document start: ${chan.URI.spec}\n`); stopContent(contentStopFromProgressListener, chan.originalURI.spec); } }, QueryInterface: XPCOMUtils.generateQI(["nsISupportsWeakReference"]) }; wp.addProgressListener(progressListener, wp.NOTIFY_STATE_WINDOW); /** * As |this| is undefined and we can't extend |docShell|, adding an unload * event handler is the easiest way to ensure the weakly referenced * progress listener is kept alive as long as necessary. */ addEventListener("unload", function() { try { wp.removeProgressListener(progressListener); } catch (e) { /* Will most likely fail. */ } }); } return new Promise((resolve, reject) => { function complete({ data }) { is(data.uri, aExpectedURL, "waitForDocLoadAndStopIt: The expected URL was loaded"); mm.removeMessageListener("Test:WaitForDocLoadAndStopIt", complete); resolve(); } let mm = aBrowser.messageManager; mm.loadFrameScript("data:,(" + content_script.toString() + ")(" + aStopFromProgressListener + ");", true); mm.addMessageListener("Test:WaitForDocLoadAndStopIt", complete); info("waitForDocLoadAndStopIt: Waiting for URL: " + aExpectedURL); }); } /** * Waits for the next load to complete in any browser or the given browser. * If a is given it waits for a load in any of its browsers. * * @return promise */ function waitForDocLoadComplete(aBrowser = gBrowser) { return new Promise(resolve => { let listener = { onStateChange(webProgress, req, flags, status) { let docStop = Ci.nsIWebProgressListener.STATE_IS_NETWORK | Ci.nsIWebProgressListener.STATE_STOP; info("Saw state " + flags.toString(16) + " and status " + status.toString(16)); // When a load needs to be retargetted to a new process it is cancelled // with NS_BINDING_ABORTED so ignore that case if ((flags & docStop) == docStop && status != Cr.NS_BINDING_ABORTED) { aBrowser.removeProgressListener(this); waitForDocLoadComplete.listeners.delete(this); let chan = req.QueryInterface(Ci.nsIChannel); info("Browser loaded " + chan.originalURI.spec); resolve(); } }, QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]) }; aBrowser.addProgressListener(listener); waitForDocLoadComplete.listeners.add(listener); info("Waiting for browser load"); }); } // Keep a set of progress listeners for waitForDocLoadComplete() to make sure // they're not GC'ed before we saw the page load. waitForDocLoadComplete.listeners = new Set(); registerCleanupFunction(() => waitForDocLoadComplete.listeners.clear()); var FullZoomHelper = { selectTabAndWaitForLocationChange: function selectTabAndWaitForLocationChange(tab) { if (!tab) throw new Error("tab must be given."); if (gBrowser.selectedTab == tab) return Promise.resolve(); return Promise.all([BrowserTestUtils.switchTab(gBrowser, tab), this.waitForLocationChange()]); }, removeTabAndWaitForLocationChange: function removeTabAndWaitForLocationChange(tab) { tab = tab || gBrowser.selectedTab; let selected = gBrowser.selectedTab == tab; gBrowser.removeTab(tab); if (selected) return this.waitForLocationChange(); return Promise.resolve(); }, waitForLocationChange: function waitForLocationChange() { return new Promise(resolve => { Services.obs.addObserver(function obs(subj, topic, data) { Services.obs.removeObserver(obs, topic); resolve(); }, "browser-fullZoom:location-change"); }); }, load: function load(tab, url) { return new Promise(resolve => { let didLoad = false; let didZoom = false; promiseTabLoadEvent(tab).then(event => { didLoad = true; if (didZoom) resolve(); }, true); this.waitForLocationChange().then(function() { didZoom = true; if (didLoad) resolve(); }); tab.linkedBrowser.loadURI(url); }); }, zoomTest: function zoomTest(tab, val, msg) { is(ZoomManager.getZoomForBrowser(tab.linkedBrowser), val, msg); }, enlarge: function enlarge() { return new Promise(resolve => FullZoom.enlarge(resolve)); }, reduce: function reduce() { return new Promise(resolve => FullZoom.reduce(resolve)); }, reset: function reset() { return FullZoom.reset(); }, BACK: 0, FORWARD: 1, navigate: function navigate(direction) { return new Promise(resolve => { let didPs = false; let didZoom = false; gBrowser.addEventListener("pageshow", function(event) { didPs = true; if (didZoom) resolve(); }, {capture: true, once: true}); if (direction == this.BACK) gBrowser.goBack(); else if (direction == this.FORWARD) gBrowser.goForward(); this.waitForLocationChange().then(function() { didZoom = true; if (didPs) resolve(); }); }); }, failAndContinue: function failAndContinue(func) { return function(err) { ok(false, err); func(); }; }, }; /** * Waits for a load (or custom) event to finish in a given tab. If provided * load an uri into the tab. * * @param tab * The tab to load into. * @param [optional] url * The url to load, or the current url. * @return {Promise} resolved when the event is handled. * @resolves to the received event * @rejects if a valid load event is not received within a meaningful interval */ function promiseTabLoadEvent(tab, url) { info("Wait tab event: load"); function handle(loadedUrl) { if (loadedUrl === "about:blank" || (url && loadedUrl !== url)) { info(`Skipping spurious load event for ${loadedUrl}`); return false; } info("Tab event received: load"); return true; } let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, handle); if (url) BrowserTestUtils.loadURI(tab.linkedBrowser, url); return loaded; } /** * Returns a Promise that resolves once a new tab has been opened in * a xul:tabbrowser. * * @param aTabBrowser * The xul:tabbrowser to monitor for a new tab. * @return {Promise} * Resolved when the new tab has been opened. * @resolves to the TabOpen event that was fired. * @rejects Never. */ function waitForNewTabEvent(aTabBrowser) { return promiseWaitForEvent(aTabBrowser.tabContainer, "TabOpen"); } function is_hidden(element) { var style = element.ownerGlobal.getComputedStyle(element); if (style.display == "none") return true; if (style.visibility != "visible") return true; if (style.display == "-moz-popup") return ["hiding", "closed"].indexOf(element.state) != -1; // Hiding a parent element will hide all its children if (element.parentNode != element.ownerDocument) return is_hidden(element.parentNode); return false; } function is_visible(element) { var style = element.ownerGlobal.getComputedStyle(element); if (style.display == "none") return false; if (style.visibility != "visible") return false; if (style.display == "-moz-popup" && element.state != "open") return false; // Hiding a parent element will hide all its children if (element.parentNode != element.ownerDocument) return is_visible(element.parentNode); return true; } function is_element_visible(element, msg) { isnot(element, null, "Element should not be null, when checking visibility"); ok(is_visible(element), msg || "Element should be visible"); } function is_element_hidden(element, msg) { isnot(element, null, "Element should not be null, when checking visibility"); ok(is_hidden(element), msg || "Element should be hidden"); } function promisePopupEvent(popup, eventSuffix) { let endState = {shown: "open", hidden: "closed"}[eventSuffix]; if (popup.state == endState) return Promise.resolve(); let eventType = "popup" + eventSuffix; return new Promise(resolve => { popup.addEventListener(eventType, function(event) { resolve(); }, {once: true}); }); } function promisePopupShown(popup) { return promisePopupEvent(popup, "shown"); } function promisePopupHidden(popup) { return promisePopupEvent(popup, "hidden"); } function promiseNotificationShown(notification) { let win = notification.browser.ownerGlobal; if (win.PopupNotifications.panel.state == "open") { return Promise.resolve(); } let panelPromise = promisePopupShown(win.PopupNotifications.panel); notification.reshow(); return panelPromise; } /** * Allows waiting for an observer notification once. * * @param aTopic * Notification topic to observe. * * @return {Promise} * @resolves An object with subject and data properties from the observed * notification. * @rejects Never. */ function promiseTopicObserved(aTopic) { return new Promise((resolve) => { Services.obs.addObserver( function PTO_observe(aSubject, aTopic2, aData) { Services.obs.removeObserver(PTO_observe, aTopic2); resolve({subject: aSubject, data: aData}); }, aTopic); }); } function promiseNewSearchEngine(basename) { return new Promise((resolve, reject) => { info("Waiting for engine to be added: " + basename); let url = getRootDirectory(gTestPath) + basename; Services.search.addEngine(url, null, "", false, { onSuccess(engine) { info("Search engine added: " + basename); registerCleanupFunction(() => Services.search.removeEngine(engine)); resolve(engine); }, onError(errCode) { Assert.ok(false, "addEngine failed with error code " + errCode); reject(); }, }); }); } /** * Resolves when a bookmark with the given uri is added. */ function promiseOnBookmarkItemAdded(aExpectedURI) { return new Promise((resolve, reject) => { let bookmarksObserver = { onItemAdded(aItemId, aFolderId, aIndex, aItemType, aURI) { info("Added a bookmark to " + aURI.spec); PlacesUtils.bookmarks.removeObserver(bookmarksObserver); if (aURI.equals(aExpectedURI)) { resolve(); } else { reject(new Error("Added an unexpected bookmark")); } }, onBeginUpdateBatch() {}, onEndUpdateBatch() {}, onItemRemoved() {}, onItemChanged() {}, onItemVisited() {}, onItemMoved() {}, QueryInterface: XPCOMUtils.generateQI([ Ci.nsINavBookmarkObserver, ]) }; info("Waiting for a bookmark to be added"); PlacesUtils.bookmarks.addObserver(bookmarksObserver); }); } async function loadBadCertPage(url) { const EXCEPTION_DIALOG_URI = "chrome://pippki/content/exceptionDialog.xul"; let exceptionDialogResolved = new Promise(function(resolve) { // When the certificate exception dialog has opened, click the button to add // an exception. let certExceptionDialogObserver = { observe(aSubject, aTopic, aData) { if (aTopic == "cert-exception-ui-ready") { Services.obs.removeObserver(this, "cert-exception-ui-ready"); let certExceptionDialog = getCertExceptionDialog(EXCEPTION_DIALOG_URI); ok(certExceptionDialog, "found exception dialog"); executeSoon(function() { certExceptionDialog.documentElement.getButton("extra1").click(); resolve(); }); } } }; Services.obs.addObserver(certExceptionDialogObserver, "cert-exception-ui-ready"); }); let loaded = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser); await BrowserTestUtils.loadURI(gBrowser.selectedBrowser, url); await loaded; await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() { content.document.getElementById("exceptionDialogButton").click(); }); await exceptionDialogResolved; await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); } // Utility function to get a handle on the certificate exception dialog. // Modified from toolkit/components/passwordmgr/test/prompt_common.js function getCertExceptionDialog(aLocation) { let enumerator = Services.wm.getXULWindowEnumerator(null); while (enumerator.hasMoreElements()) { let win = enumerator.getNext(); let windowDocShell = win.QueryInterface(Ci.nsIXULWindow).docShell; let containedDocShells = windowDocShell.getDocShellEnumerator( Ci.nsIDocShellTreeItem.typeChrome, Ci.nsIDocShell.ENUMERATE_FORWARDS); while (containedDocShells.hasMoreElements()) { // Get the corresponding document for this docshell let childDocShell = containedDocShells.getNext(); let childDoc = childDocShell.QueryInterface(Ci.nsIDocShell) .contentViewer .DOMDocument; if (childDoc.location.href == aLocation) { return childDoc; } } } return undefined; }