From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../test/browser/shared-modules/WindowHelpers.jsm | 1018 ++++++++++++++++++++ 1 file changed, 1018 insertions(+) create mode 100644 comm/mail/test/browser/shared-modules/WindowHelpers.jsm (limited to 'comm/mail/test/browser/shared-modules/WindowHelpers.jsm') diff --git a/comm/mail/test/browser/shared-modules/WindowHelpers.jsm b/comm/mail/test/browser/shared-modules/WindowHelpers.jsm new file mode 100644 index 0000000000..9a8d16fbae --- /dev/null +++ b/comm/mail/test/browser/shared-modules/WindowHelpers.jsm @@ -0,0 +1,1018 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const EXPORTED_SYMBOLS = [ + "click_menus_in_sequence", + "close_popup_sequence", + "click_through_appmenu", + "plan_for_new_window", + "wait_for_new_window", + "async_plan_for_new_window", + "plan_for_modal_dialog", + "wait_for_modal_dialog", + "plan_for_window_close", + "wait_for_window_close", + "close_window", + "wait_for_existing_window", + "wait_for_window_focused", + "wait_for_browser_load", + "wait_for_frame_load", + "resize_to", +]; + +var controller = ChromeUtils.import( + "resource://testing-common/mozmill/controller.jsm" +); +var utils = ChromeUtils.import("resource://testing-common/mozmill/utils.jsm"); + +var { Assert } = ChromeUtils.importESModule( + "resource://testing-common/Assert.sys.mjs" +); +var { BrowserTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" +); +var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +var EventUtils = ChromeUtils.import( + "resource://testing-common/mozmill/EventUtils.jsm" +); + +/** + * Timeout to use when waiting for the first window ever to load. This is + * long because we are basically waiting for the entire app startup process. + */ +var FIRST_WINDOW_EVER_TIMEOUT_MS = 30000; +/** + * Interval to check if the window has shown up for the first window ever to + * load. The check interval is longer because it's less likely the window + * is going to show up quickly and there is a cost to the check. + */ +var FIRST_WINDOW_CHECK_INTERVAL_MS = 300; + +/** + * Timeout for opening a window. + */ +var WINDOW_OPEN_TIMEOUT_MS = 10000; +/** + * Check interval for opening a window. + */ +var WINDOW_OPEN_CHECK_INTERVAL_MS = 100; + +/** + * Timeout for closing a window. + */ +var WINDOW_CLOSE_TIMEOUT_MS = 10000; +/** + * Check interval for closing a window. + */ +var WINDOW_CLOSE_CHECK_INTERVAL_MS = 100; + +/** + * Timeout for focusing a window. Only really an issue on linux. + */ +var WINDOW_FOCUS_TIMEOUT_MS = 10000; + +function getWindowTypeOrId(aWindowElem) { + let windowType = aWindowElem.getAttribute("windowtype"); + // Ignore types that start with "prompt:". This prefix gets added in + // toolkit/components/prompts/src/CommonDialog.jsm since bug 1388238. + if (windowType && !windowType.startsWith("prompt:")) { + return windowType; + } + + return aWindowElem.getAttribute("id"); +} + +/** + * Return the "windowtype" or "id" for the given app window if it is available. + * If not, return null. + */ +function getWindowTypeForAppWindow(aAppWindow, aBusyOk) { + // Sometimes we are given HTML windows, for which the logic below will + // bail. So we use a fast-path here that should work for HTML and should + // maybe also work with XUL. I'm not going to go into it... + if ( + aAppWindow.document && + aAppWindow.document.documentElement && + aAppWindow.document.documentElement.hasAttribute("windowtype") + ) { + return getWindowTypeOrId(aAppWindow.document.documentElement); + } + + let docshell = aAppWindow.docShell; + // we need the docshell to exist... + if (!docshell) { + return null; + } + + // we can't know if it's the right document until it's not busy + if (!aBusyOk && docshell.busyFlags) { + return null; + } + + // it also needs to have content loaded (it starts out not busy with no + // content viewer.) + if (docshell.contentViewer == null) { + return null; + } + + // now we're cooking! let's get the document... + let outerDoc = docshell.contentViewer.DOMDocument; + // and make sure it's not blank. that's also an intermediate state. + if (outerDoc.location.href == "about:blank") { + return null; + } + + // finally, we can now have a windowtype! + let windowType = getWindowTypeOrId(outerDoc.documentElement); + + if (windowType) { + return windowType; + } + + // As a last resort, use the name given to the DOM window. + let domWindow = aAppWindow.docShell.domWindow; + + return domWindow.name; +} + +var WindowWatcher = { + _inited: false, + _firstWindowOpened: false, + ensureInited() { + if (this._inited) { + return; + } + + // Add ourselves as an nsIWindowMediatorListener so we can here about when + // windows get registered with the window mediator. Because this + // generally happens + // Another possible means of getting this info would be to observe + // "xul-window-visible", but it provides no context and may still require + // polling anyways. + Services.wm.addListener(this); + + // Clean up any references to windows at the end of each test, and clean + // up the listeners/observers as the end of the session. + let observer = { + observe(subject, topic) { + WindowWatcher.monitoringList.length = 0; + WindowWatcher.waitingList.clear(); + if (topic == "quit-application-granted") { + Services.wm.removeListener(this); + Services.obs.removeObserver(this, "test-complete"); + Services.obs.removeObserver(this, "quit-application-granted"); + } + }, + }; + Services.obs.addObserver(observer, "test-complete"); + Services.obs.addObserver(observer, "quit-application-granted"); + + this._inited = true; + }, + + /** + * Track the windowtypes we are waiting on. Keys are windowtypes. When + * watching for new windows, values are initially null, and are set to an + * nsIAppWindow when we actually find the window. When watching for closing + * windows, values are nsIAppWindows. This symmetry lets us have windows + * that appear and dis-appear do so without dangerously confusing us (as + * long as another one comes along...) + */ + waitingList: new Map(), + /** + * Note that we will be looking for a window with the given window type + * (ex: "mailnews:search"). This allows us to be ready if an event shows + * up before waitForWindow is called. + */ + planForWindowOpen(aWindowType) { + this.waitingList.set(aWindowType, null); + }, + + /** + * Like planForWindowOpen but we check for already-existing windows. + */ + planForAlreadyOpenWindow(aWindowType) { + this.waitingList.set(aWindowType, null); + // We need to iterate over all the app windows and consider them all. + // We can't pass the window type because the window might not have a + // window type yet. + // because this iterates from old to new, this does the right thing in that + // side-effects of consider will pick the most recent window. + for (let appWindow of Services.wm.getAppWindowEnumerator(null)) { + if (!this.consider(appWindow)) { + this.monitoringList.push(appWindow); + } + } + }, + + /** + * The current windowType we are waiting to open. This is mainly a means of + * communicating the desired window type to monitorize without having to + * put the argument in the eval string. + */ + waitingForOpen: null, + /** + * Wait for the given windowType to open and finish loading. + * + * @returns The window wrapped in a MozMillController. + */ + waitForWindowOpen(aWindowType) { + this.waitingForOpen = aWindowType; + utils.waitFor( + () => this.monitorizeOpen(), + "Timed out waiting for window open!", + this._firstWindowOpened + ? WINDOW_OPEN_TIMEOUT_MS + : FIRST_WINDOW_EVER_TIMEOUT_MS, + this._firstWindowOpened + ? WINDOW_OPEN_CHECK_INTERVAL_MS + : FIRST_WINDOW_CHECK_INTERVAL_MS + ); + + this.waitingForOpen = null; + let appWindow = this.waitingList.get(aWindowType); + let domWindow = appWindow.docShell.domWindow; + this.waitingList.delete(aWindowType); + // spin the event loop to make sure any setTimeout 0 calls have gotten their + // time in the sun. + utils.sleep(0); + this._firstWindowOpened = true; + return new controller.MozMillController(domWindow); + }, + + /** + * Because the modal dialog spins its own event loop, the mozmill idiom of + * spinning your own event-loop as performed by waitFor is no good. We use + * this timer to generate our events so that we can have a waitFor + * equivalent. + * + * We only have one timer right now because modal dialogs that spawn modal + * dialogs are not tremendously likely. + */ + _timer: null, + _timerRuntimeSoFar: 0, + /** + * The test function to run when the modal dialog opens. + */ + subTestFunc: null, + planForModalDialog(aWindowType, aSubTestFunc) { + if (this._timer == null) { + this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + } + this.waitingForOpen = aWindowType; + this.subTestFunc = aSubTestFunc; + this.waitingList.set(aWindowType, null); + + this._timerRuntimeSoFar = 0; + this._timer.initWithCallback( + this, + WINDOW_OPEN_CHECK_INTERVAL_MS, + Ci.nsITimer.TYPE_REPEATING_SLACK + ); + }, + + /** + * This is the nsITimer notification we receive... + */ + notify() { + if (this.monitorizeOpen()) { + // okay, the window is opened, and we should be in its event loop now. + let appWindow = this.waitingList.get(this.waitingForOpen); + let domWindow = appWindow.docShell.domWindow; + let troller = new controller.MozMillController(domWindow); + + this._timer.cancel(); + + let self = this; + async function startTest() { + self.planForWindowClose(troller.window); + try { + await self.subTestFunc(troller); + } finally { + self.subTestFunc = null; + } + + // if the test failed, make sure we force the window closed... + // except I'm not sure how to easily figure that out... + // so just close it no matter what. + troller.window.close(); + self.waitForWindowClose(); + + self.waitingList.delete(self.waitingForOpen); + // now we are waiting for it to close... + self.waitingForClose = self.waitingForOpen; + self.waitingForOpen = null; + } + + let targetFocusedWindow = {}; + Services.focus.getFocusedElementForWindow( + domWindow, + true, + targetFocusedWindow + ); + targetFocusedWindow = targetFocusedWindow.value; + + let focusedWindow = {}; + if (Services.focus.activeWindow) { + Services.focus.getFocusedElementForWindow( + Services.focus.activeWindow, + true, + focusedWindow + ); + + focusedWindow = focusedWindow.value; + } + + if (focusedWindow == targetFocusedWindow) { + startTest(); + } else { + function onFocus(event) { + targetFocusedWindow.setTimeout(startTest, 0); + } + targetFocusedWindow.addEventListener("focus", onFocus, { + capture: true, + once: true, + }); + targetFocusedWindow.focus(); + } + } + // notify is only used for modal dialogs, which are never the first window, + // so we can always just use this set of timeouts/intervals. + this._timerRuntimeSoFar += WINDOW_OPEN_CHECK_INTERVAL_MS; + if (this._timerRuntimeSoFar >= WINDOW_OPEN_TIMEOUT_MS) { + this._timer.cancel(); + throw new Error("Timeout while waiting for modal dialog.\n"); + } + }, + + /** + * Symmetry for planForModalDialog; conceptually provides the waiting. In + * reality, all we do is potentially soak up the event loop a little to + */ + waitForModalDialog(aWindowType, aTimeout) { + // did the window already come and go? + if (this.subTestFunc == null) { + return; + } + // spin the event loop until we the window has come and gone. + utils.waitFor( + () => { + return this.waitingForOpen == null && this.monitorizeClose(); + }, + "Timeout waiting for modal dialog to open.", + aTimeout || WINDOW_OPEN_TIMEOUT_MS, + WINDOW_OPEN_CHECK_INTERVAL_MS + ); + this.waitingForClose = null; + }, + + planForWindowClose(aAppWindow) { + let windowType = getWindowTypeOrId(aAppWindow.document.documentElement); + this.waitingList.set(windowType, aAppWindow); + this.waitingForClose = windowType; + }, + + /** + * The current windowType we are waiting to close. Same deal as + * waitingForOpen, this makes the eval less crazy. + */ + waitingForClose: null, + waitForWindowClose() { + utils.waitFor( + () => this.monitorizeClose(), + "Timeout waiting for window to close!", + WINDOW_CLOSE_TIMEOUT_MS, + WINDOW_CLOSE_CHECK_INTERVAL_MS + ); + let didDisappear = this.waitingList.get(this.waitingForClose) == null; + let windowType = this.waitingForClose; + this.waitingList.delete(windowType); + this.waitingForClose = null; + if (!didDisappear) { + throw new Error(windowType + " window did not disappear!"); + } + }, + + /** + * Used by waitForWindowOpen to check all of the windows we are monitoring and + * then check if we have any results. + * + * @returns true if we found what we were |waitingForOpen|, false otherwise. + */ + monitorizeOpen() { + for (let iWin = this.monitoringList.length - 1; iWin >= 0; iWin--) { + let appWindow = this.monitoringList[iWin]; + if (this.consider(appWindow)) { + this.monitoringList.splice(iWin, 1); + } + } + + return ( + this.waitingList.has(this.waitingForOpen) && + this.waitingList.get(this.waitingForOpen) != null + ); + }, + + /** + * Used by waitForWindowClose to check if the window we are waiting to close + * actually closed yet. + * + * @returns true if it closed. + */ + monitorizeClose() { + return this.waitingList.get(this.waitingForClose) == null; + }, + + /** + * A list of app windows to monitor because they are loading and it's not yet + * possible to tell whether they are something we are looking for. + */ + monitoringList: [], + /** + * Monitor the given window's loading process until we can determine whether + * it is what we are looking for. + */ + monitorWindowLoad(aAppWindow) { + this.monitoringList.push(aAppWindow); + }, + + /** + * nsIWindowMediatorListener notification that a app window was opened. We + * check out the window, and if we were not able to fully consider it, we + * add it to our monitoring list. + */ + onOpenWindow(aAppWindow) { + // note: we would love to add our window activation/deactivation listeners + // and poke our unique id, but there is no contentViewer at this point + // and so there's no place to poke our unique id. (aAppWindow does not + // let us put expandos on; it's an XPCWrappedNative and explodes.) + // There may be nuances about outer window/inner window that make it + // feasible, but I have forgotten any such nuances I once knew. + if (!this.consider(aAppWindow)) { + this.monitorWindowLoad(aAppWindow); + } + }, + + /** + * Consider if the given window is something in our |waitingList|. + * + * @returns true if we were able to fully consider the object, false if we were + * not and need to be called again on the window later. This has no + * relation to whether the window was one in our waitingList or not. + * Check the waitingList structure for that. + */ + consider(aAppWindow) { + let windowType = getWindowTypeForAppWindow(aAppWindow); + if (windowType == null) { + return false; + } + + // stash the window if we were watching for it + if (this.waitingList.has(windowType)) { + this.waitingList.set(windowType, aAppWindow); + } + + return true; + }, + + /** + * Closing windows have the advantage of having to already have been loaded, + * so things like their windowtype are immediately available. + */ + onCloseWindow(aAppWindow) { + let domWindow = aAppWindow.docShell.domWindow; + let windowType = getWindowTypeOrId(domWindow.document.documentElement); + if (this.waitingList.has(windowType)) { + this.waitingList.set(windowType, null); + } + }, +}; + +/** + * Call this if the window you want to get may already be open. What we + * provide above just directly grabbing the window yourself is: + * - We wait for it to finish loading. + * + * @param aWindowType the window type that will be created. This is literally + * the value of the "windowtype" attribute on the window. The values tend + * to look like "app:windowname", for example "mailnews:search". + * + * @returns {MozmillController} + */ +function wait_for_existing_window(aWindowType) { + WindowWatcher.ensureInited(); + WindowWatcher.planForAlreadyOpenWindow(aWindowType); + return WindowWatcher.waitForWindowOpen(aWindowType); +} + +/** + * Call this just before you trigger the event that will cause a window to be + * displayed. + * In theory, we don't need this and could just do a sweep of existing windows + * when you call wait_for_new_window, or we could always just keep track of + * the most recently seen window of each type, but this is arguably more + * resilient in the face of multiple windows of the same type as long as you + * don't try and open them all at the same time. + * + * @param aWindowType the window type that will be created. This is literally + * the value of the "windowtype" attribute on the window. The values tend + * to look like "app:windowname", for example "mailnews:search". + */ +function plan_for_new_window(aWindowType) { + WindowWatcher.ensureInited(); + WindowWatcher.planForWindowOpen(aWindowType); +} + +/** + * Wait for the loading of the given window type to complete (that you + * previously told us about via |plan_for_new_window|), returning it wrapped + * in a MozmillController. + * + * @returns {MozmillController} + */ +function wait_for_new_window(aWindowType) { + let c = WindowWatcher.waitForWindowOpen(aWindowType); + // A nested event loop can get spun inside the Controller's constructor + // (which is arguably not a great idea), so it's important that we denote + // when we're actually leaving this function in case something crazy + // happens. + return c; +} + +async function async_plan_for_new_window(aWindowType) { + let domWindow = await BrowserTestUtils.domWindowOpened(null, async win => { + await BrowserTestUtils.waitForEvent(win, "load"); + return ( + win.document.documentElement.getAttribute("windowtype") == aWindowType + ); + }); + + await new Promise(r => domWindow.setTimeout(r)); + await new Promise(r => domWindow.setTimeout(r)); + + let domWindowController = new controller.MozMillController(domWindow); + return domWindowController; +} + +/** + * Plan for the imminent display of a modal dialog. Modal dialogs spin their + * own event loop which means that either that control flow will not return + * to the caller until the modal dialog finishes running. This means that + * you need to provide a sub-test function to be run inside the modal dialog + * (and it should not start with "test" or mozmill will also try and run it.) + * + * @param aWindowType The window type that you expect the modal dialog to have + * or the id of the window if there is no window type + * available. + * @param aSubTestFunction The sub-test function that will be run once the modal + * dialog appears and is loaded. This function should take one argument, + * a MozmillController against the modal dialog. + */ +function plan_for_modal_dialog(aWindowType, aSubTestFunction) { + WindowWatcher.ensureInited(); + WindowWatcher.planForModalDialog(aWindowType, aSubTestFunction); +} +/** + * In case the dialog might be stuck for a long time, you can pass an optional + * timeout. + * + * @param aTimeout Your custom timeout (default is WINDOW_OPEN_TIMEOUT_MS) + */ +function wait_for_modal_dialog(aWindowType, aTimeout) { + WindowWatcher.waitForModalDialog(aWindowType, aTimeout); +} + +/** + * Call this just before you trigger the event that will cause the provided + * controller's window to disappear. You then follow this with a call to + * |wait_for_window_close| when you want to block on verifying the close. + * + * @param aController The MozmillController, potentially returned from a call to + * wait_for_new_window, whose window should be disappearing. + */ +function plan_for_window_close(aController) { + WindowWatcher.ensureInited(); + WindowWatcher.planForWindowClose(aController.window); +} + +/** + * Wait for the closure of the window you noted you would listen for its close + * in plan_for_window_close. + */ +function wait_for_window_close() { + WindowWatcher.waitForWindowClose(); +} + +/** + * Close a window by calling window.close() on the controller. + * + * @param aController the controller whose window is to be closed. + */ +function close_window(aController) { + plan_for_window_close(aController); + aController.window.close(); + wait_for_window_close(); +} + +/** + * Wait for the window to be focused. + * + * @param aWindow the window to be focused. + */ +function wait_for_window_focused(aWindow) { + let targetWindow = {}; + + Services.focus.getFocusedElementForWindow(aWindow, true, targetWindow); + targetWindow = targetWindow.value; + + let focusedWindow = {}; + if (Services.focus.activeWindow) { + Services.focus.getFocusedElementForWindow( + Services.focus.activeWindow, + true, + focusedWindow + ); + focusedWindow = focusedWindow.value; + } + + let focused = false; + if (focusedWindow == targetWindow) { + focused = true; + } else { + targetWindow.addEventListener("focus", () => (focused = true), { + capture: true, + once: true, + }); + targetWindow.focus(); + } + + utils.waitFor( + () => focused, + "Timeout waiting for window to be focused.", + WINDOW_FOCUS_TIMEOUT_MS, + 100, + this + ); +} + +/** + * Given a , waits for it to completely load. + * + * @param aBrowser The element to wait for. + * @param aURLOrPredicate The URL that should be loaded (string) or a predicate + * for the URL (function). + * @returns The browser's content window wrapped in a MozMillController. + */ +function wait_for_browser_load(aBrowser, aURLOrPredicate) { + // aBrowser has all the fields we need already. + return _wait_for_generic_load(aBrowser, aURLOrPredicate); +} + +/** + * Given an HTML or