summaryrefslogtreecommitdiffstats
path: root/comm/mail/test/browser/shared-modules/WindowHelpers.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/test/browser/shared-modules/WindowHelpers.jsm')
-rw-r--r--comm/mail/test/browser/shared-modules/WindowHelpers.jsm1018
1 files changed, 1018 insertions, 0 deletions
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 <browser>, waits for it to completely load.
+ *
+ * @param aBrowser The <browser> 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 <frame> or <iframe>, waits for it to completely load.
+ *
+ * @param aFrame The element to wait for.
+ * @param aURLOrPredicate The URL that should be loaded (string) or a predicate
+ * for the URL (function).
+ * @returns The frame wrapped in a MozMillController.
+ */
+function wait_for_frame_load(aFrame, aURLOrPredicate) {
+ return _wait_for_generic_load(aFrame, aURLOrPredicate);
+}
+
+/**
+ * Generic function to wait for some sort of document to load. We expect
+ * aDetails to have three fields:
+ * - webProgress: an nsIWebProgress associated with the contentWindow.
+ * - currentURI: the currently loaded page (nsIURI).
+ */
+function _wait_for_generic_load(aDetails, aURLOrPredicate) {
+ let predicate;
+ if (typeof aURLOrPredicate == "string") {
+ let expectedURL = NetUtil.newURI(aURLOrPredicate);
+ predicate = url => expectedURL.equals(url);
+ } else {
+ predicate = aURLOrPredicate;
+ }
+
+ function isLoadedChecker() {
+ if (aDetails.webProgress?.isLoadingDocument) {
+ return false;
+ }
+ if (
+ aDetails.contentDocument &&
+ aDetails.contentDocument.readyState != "complete"
+ ) {
+ return false;
+ }
+
+ return predicate(
+ aDetails.currentURI ||
+ NetUtil.newURI(aDetails.contentWindow.location.href)
+ );
+ }
+
+ try {
+ utils.waitFor(isLoadedChecker);
+ } catch (e) {
+ if (e instanceof utils.TimeoutError) {
+ Assert.report(
+ true,
+ undefined,
+ undefined,
+ `Timeout waiting for content page to load. Current URL is: ${aDetails.currentURI.spec}`
+ );
+ } else {
+ throw e;
+ }
+ }
+
+ // Lie to mozmill to convince it to not explode because these frames never
+ // get a mozmillDocumentLoaded attribute (bug 666438).
+ let contentWindow = aDetails.contentWindow;
+ if (contentWindow) {
+ return new controller.MozMillController(contentWindow);
+ }
+ return null;
+}
+
+/**
+ * Resize given window to new dimensions.
+ *
+ * @param aController window controller
+ * @param aWidth the requested window width
+ * @param aHeight the requested window height
+ */
+function resize_to(aController, aWidth, aHeight) {
+ aController.window.resizeTo(aWidth, aHeight);
+ // Give the event loop a spin in order to let the reality of an asynchronously
+ // interacting window manager have its impact. This still may not be
+ // sufficient.
+ utils.sleep(0);
+ utils.waitFor(
+ () =>
+ aController.window.outerWidth == aWidth &&
+ aController.window.outerHeight == aHeight,
+ "Timeout waiting for resize (current screen size: " +
+ aController.window.screen.availWidth +
+ "X" +
+ aController.window.screen.availHeight +
+ "), Requested width " +
+ aWidth +
+ " but got " +
+ aController.window.outerWidth +
+ ", Request height " +
+ aHeight +
+ " but got " +
+ aController.window.outerHeight,
+ 10000,
+ 50
+ );
+}
+
+/**
+ * Dynamically-built/XBL-defined menus can be hard to work with, this makes it
+ * easier.
+ *
+ * @param aRootPopup The base popup. The caller is expected to activate it
+ * (by clicking/rightclicking the right widget). We will only wait for it
+ * to open if it is in the process.
+ * @param aActions An array of objects where each object has attributes
+ * with a value defined. We pick the menu item whose DOM node matches
+ * all the attributes with the specified names and value. We click whatever
+ * we find. We throw if the element being asked for is not found.
+ * @param aKeepOpen If set to true the popups are not closed after last click.
+ *
+ * @returns An array of popup elements that were left open. It will be
+ * an empty array if aKeepOpen was set to false.
+ */
+async function click_menus_in_sequence(aRootPopup, aActions, aKeepOpen) {
+ if (aRootPopup.state != "open") {
+ await BrowserTestUtils.waitForEvent(aRootPopup, "popupshown");
+ }
+
+ /**
+ * Check if a node's attributes match all those given in actionObj.
+ * Nodes that are obvious containers are skipped, and their children
+ * will be used to recursively find a match instead.
+ *
+ * @param {Element} node - The node to check.
+ * @param {object} actionObj - Contains attribute-value pairs to match.
+ * @returns {Element|null} The matched node or null if no match.
+ */
+ let findMatch = function (node, actionObj) {
+ // Ignore some elements and just use their children instead.
+ if (node.localName == "hbox" || node.localName == "vbox") {
+ for (let i = 0; i < node.children.length; i++) {
+ let childMatch = findMatch(node.children[i]);
+ if (childMatch) {
+ return childMatch;
+ }
+ }
+ return null;
+ }
+
+ let matchedAll = true;
+ for (let name in actionObj) {
+ let value = actionObj[name];
+ if (!node.hasAttribute(name) || node.getAttribute(name) != value) {
+ matchedAll = false;
+ break;
+ }
+ }
+ return matchedAll ? node : null;
+ };
+
+ // These popups sadly do not close themselves, so we need to keep track
+ // of them so we can make sure they end up closed.
+ let closeStack = [aRootPopup];
+
+ let curPopup = aRootPopup;
+ for (let [iAction, actionObj] of aActions.entries()) {
+ let matchingNode = null;
+ let kids = curPopup.children;
+ for (let iKid = 0; iKid < kids.length; iKid++) {
+ let node = kids[iKid];
+ matchingNode = findMatch(node, actionObj);
+ if (matchingNode) {
+ break;
+ }
+ }
+
+ if (!matchingNode) {
+ throw new Error(
+ "Did not find matching menu item for action index " +
+ iAction +
+ ": " +
+ JSON.stringify(actionObj)
+ );
+ }
+
+ if (matchingNode.localName == "menu") {
+ matchingNode.openMenu(true);
+ } else {
+ curPopup.activateItem(matchingNode);
+ }
+ await new Promise(r => matchingNode.ownerGlobal.setTimeout(r, 500));
+
+ let newPopup = null;
+ if ("menupopup" in matchingNode) {
+ newPopup = matchingNode.menupopup;
+ }
+ if (newPopup) {
+ curPopup = newPopup;
+ closeStack.push(curPopup);
+ if (curPopup.state != "open") {
+ await BrowserTestUtils.waitForEvent(curPopup, "popupshown");
+ }
+ }
+ }
+
+ if (!aKeepOpen) {
+ close_popup_sequence(closeStack);
+ return [];
+ }
+ return closeStack;
+}
+
+/**
+ * Close given menupopups.
+ *
+ * @param aCloseStack An array of menupopup elements that are to be closed.
+ * The elements are processed from the end of the array
+ * to the front (a stack).
+ */
+function close_popup_sequence(aCloseStack) {
+ while (aCloseStack.length) {
+ let curPopup = aCloseStack.pop();
+ if (curPopup.state == "open") {
+ curPopup.focus();
+ curPopup.hidePopup();
+ }
+ }
+}
+
+/**
+ * Click through the appmenu. Callers are expected to open the initial
+ * appmenu panelview (e.g. by clicking the appmenu button). We wait for it
+ * to open if it is not open yet. Then we use a recursive style approach
+ * with a sequence of event listeners handling "ViewShown" events. The
+ * `navTargets` parameter specifies items to click to navigate through the
+ * menu. The optional `nonNavTarget` parameter specifies a final item to
+ * click to perform a command after navigating through the menu. If this
+ * argument is omitted, callers can interact with the last view panel that
+ * is returned. Callers will then need to close the appmenu when they are
+ * done with it.
+ *
+ * @param {object[]} navTargets - Array of objects that contain
+ * attribute->value pairs. We pick the menu item whose DOM node matches
+ * all the attribute->value pairs. We click whatever we find. We throw
+ * if the element being asked for is not found.
+ * @param {object} [nonNavTarget] - Contains attribute->value pairs used
+ * to identify a final menu item to click.
+ * @param {Window} win - The window we're using.
+ * @returns {Element} The <vbox class="panel-subview-body"> element inside
+ * the last shown <panelview>.
+ */
+function _click_appmenu_in_sequence(navTargets, nonNavTarget, win) {
+ const rootPopup = win.document.getElementById("appMenu-popup");
+
+ function viewShownListener(navTargets, nonNavTarget, allDone, event) {
+ // Set up the next listener if there are more navigation targets.
+ if (navTargets.length > 0) {
+ rootPopup.addEventListener(
+ "ViewShown",
+ viewShownListener.bind(
+ null,
+ navTargets.slice(1),
+ nonNavTarget,
+ allDone
+ ),
+ { once: true }
+ );
+ }
+
+ const subview = event.target.querySelector(".panel-subview-body");
+
+ // Click a target if there is a target left to click.
+ const clickTarget = navTargets[0] || nonNavTarget;
+
+ if (clickTarget) {
+ const kids = Array.from(subview.children);
+ const findFunction = node => {
+ let selectors = [];
+ for (let name in clickTarget) {
+ let value = clickTarget[name];
+ selectors.push(`[${name}="${value}"]`);
+ }
+ let s = selectors.join(",");
+ return node.matches(s) || node.querySelector(s);
+ };
+
+ // Some views are dynamically populated after ViewShown, so we wait.
+ utils.waitFor(
+ () => kids.find(findFunction),
+ () =>
+ "Waited but did not find matching menu item for target: " +
+ JSON.stringify(clickTarget)
+ );
+
+ const foundNode = kids.find(findFunction);
+
+ EventUtils.synthesizeMouseAtCenter(foundNode, {}, foundNode.ownerGlobal);
+ }
+
+ // We are all done when there are no more navigation targets.
+ if (navTargets.length == 0) {
+ allDone(subview);
+ }
+ }
+
+ let done = false;
+ let subviewToReturn;
+ const allDone = subview => {
+ subviewToReturn = subview;
+ done = true;
+ };
+
+ utils.waitFor(
+ () => rootPopup.getAttribute("panelopen") == "true",
+ "Waited for the appmenu to open, but it never opened."
+ );
+
+ // Because the appmenu button has already been clicked in the calling
+ // code (to match click_menus_in_sequence), we have to call the first
+ // viewShownListener manually, using a fake event argument, to start the
+ // series of event listener calls.
+ const fakeEvent = {
+ target: win.document.getElementById("appMenu-mainView"),
+ };
+ viewShownListener(navTargets, nonNavTarget, allDone, fakeEvent);
+
+ utils.waitFor(() => done, "Timed out in _click_appmenu_in_sequence.");
+ return subviewToReturn;
+}
+
+/**
+ * Utility wrapper function that clicks the main appmenu button to open the
+ * appmenu before calling `click_appmenu_in_sequence`. Makes things simple
+ * and concise for the most common case while still allowing for tests that
+ * open the appmenu via keyboard before calling `_click_appmenu_in_sequence`.
+ *
+ * @param {object[]} navTargets - Array of objects that contain
+ * attribute->value pairs to be used to identify menu items to click.
+ * @param {?object} nonNavTarget - Contains attribute->value pairs used
+ * to identify a final menu item to click.
+ * @param {Window} win - The window we're using.
+ * @returns {Element} The <vbox class="panel-subview-body"> element inside
+ * the last shown <panelview>.
+ */
+function click_through_appmenu(navTargets, nonNavTarget, win) {
+ let appmenu = win.document.getElementById("button-appmenu");
+ EventUtils.synthesizeMouseAtCenter(appmenu, {}, appmenu.ownerGlobal);
+ return _click_appmenu_in_sequence(navTargets, nonNavTarget, win);
+}