Bug 1348872 - Check for existent modal dialogs for new Marionette sessions. draft
authorHenrik Skupin <mail@hskupin.info>
Tue, 28 Mar 2017 22:47:57 +0200
changeset 552701 f7c32dfbeb88fc2c2da03d3154442f2fd59ab9ea
parent 552700 17510b4defd7ebcacb66e2a153d047fbdd598935
child 621879 4777085c40c8b8e7d2102f28afa09f822ced4a38
push id51424
push userbmo:hskupin@gmail.com
push dateTue, 28 Mar 2017 20:52:34 +0000
bugs1348872
milestone55.0a1
Bug 1348872 - Check for existent modal dialogs for new Marionette sessions. To ensure that Marionette can also detect modal dialogs opened right after the application starts, and before Marionette has been initialized, the modal dialog detection code has to be delayed until a new session actually gets started. Then it's not enough to only register the observer notification, but it should also be checked for open modal or tab modal dialogs. MozReview-Commit-ID: ChYcR3I59DW
testing/marionette/client/marionette_driver/marionette.py
testing/marionette/driver.js
testing/marionette/harness/marionette_harness/runner/mixins/window_manager.py
testing/marionette/harness/marionette_harness/tests/unit/test_modal_dialogs.py
testing/marionette/modal.js
--- a/testing/marionette/client/marionette_driver/marionette.py
+++ b/testing/marionette/client/marionette_driver/marionette.py
@@ -508,17 +508,17 @@ class MultiActions(object):
 
 
 class Alert(object):
     """A class for interacting with alerts.
 
     ::
 
         Alert(marionette).accept()
-        Alert(merionette).dismiss()
+        Alert(marionette).dismiss()
     """
 
     def __init__(self, marionette):
         self.marionette = marionette
 
     def accept(self):
         """Accept a currently displayed modal dialog."""
         self.marionette._send_message("acceptDialog")
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -133,26 +133,19 @@ this.GeckoDriver = function (appName, se
   this.marionetteLog = new logging.ContentLogger();
   this.testName = null;
 
   this.capabilities = new session.Capabilities();
 
   this.mm = globalMessageManager;
   this.listener = proxy.toListener(() => this.mm, this.sendAsync.bind(this));
 
-  // always keep weak reference to current dialogue
+  // points to an alert instance if a modal dialog is present
   this.dialog = null;
-  let handleDialog = (subject, topic) => {
-    let winr;
-    if (topic == modal.COMMON_DIALOG_LOADED) {
-      winr = Cu.getWeakReference(subject);
-    }
-    this.dialog = new modal.Dialog(() => this.curBrowser, winr);
-  };
-  modal.addHandler(handleDialog);
+  this.dialogHandler = this.globalModalDialogHandler.bind(this);
 };
 
 Object.defineProperty(GeckoDriver.prototype, "a11yChecks", {
   get: function () {
     return this.capabilities.get("moz:accessibilityChecks");
   }
 });
 
@@ -219,16 +212,29 @@ Object.defineProperty(GeckoDriver.protot
 
 GeckoDriver.prototype.QueryInterface = XPCOMUtils.generateQI([
   Ci.nsIMessageListener,
   Ci.nsIObserver,
   Ci.nsISupportsWeakReference,
 ]);
 
 /**
+ * Callback used to observe the creation of new modal or tab modal dialogs
+ * during the session's lifetime.
+ */
+GeckoDriver.prototype.globalModalDialogHandler = function (subject, topic) {
+  let winr;
+  if (topic === modal.COMMON_DIALOG_LOADED) {
+    // Always keep a weak reference to the current dialog
+    winr = Cu.getWeakReference(subject);
+  }
+  this.dialog = new modal.Dialog(() => this.curBrowser, winr);
+};
+
+/**
  * Switches to the global ChromeMessageBroadcaster, potentially replacing
  * a frame-specific ChromeMessageSender.  Has no effect if the global
  * ChromeMessageBroadcaster is already in use.  If this replaces a
  * frame-specific ChromeMessageSender, it removes the message listeners
  * from that sender, and then puts the corresponding frame script "to
  * sleep", which removes most of the message listeners from it as well.
  */
 GeckoDriver.prototype.switchToGlobalMessageManager = function() {
@@ -657,16 +663,21 @@ GeckoDriver.prototype.newSession = funct
 
   yield registerBrowsers;
   yield browserListening;
 
   if (this.curBrowser.tab) {
     this.curBrowser.contentBrowser.focus();
   }
 
+  // Setup global listener for modal dialogs, and check if there is already
+  // one open for the currently selected browser window.
+  modal.addHandler(this.dialogHandler);
+  this.dialog = modal.findModalDialogs(this.curBrowser);
+
   return {
     sessionId: this.sessionId,
     capabilities: this.capabilities,
   };
 };
 
 /**
  * Send the current session's capabilities to the client.
@@ -2368,16 +2379,18 @@ GeckoDriver.prototype.deleteSession = fu
 
   if (this.observing !== null) {
     for (let topic in this.observing) {
       Services.obs.removeObserver(this.observing[topic], topic);
     }
     this.observing = null;
   }
 
+  modal.removeHandler(this.dialogHandler);
+
   this.sandboxes.clear();
   cert.uninstallOverride();
 
   this.sessionId = null;
   this.capabilities = new session.Capabilities();
 };
 
 /** Returns the current status of the Application Cache. */
@@ -2675,20 +2688,19 @@ GeckoDriver.prototype.sendKeysToDialog =
 
   event.sendKeysToElement(
       cmd.parameters.value,
       loginTextbox,
       {ignoreVisibility: true},
       this.dialog.window ? this.dialog.window : win);
 };
 
-GeckoDriver.prototype._checkIfAlertIsPresent = function() {
+GeckoDriver.prototype._checkIfAlertIsPresent = function () {
   if (!this.dialog || !this.dialog.ui) {
-    throw new NoAlertOpenError(
-        "No tab modal was open when attempting to get the dialog text");
+    throw new NoAlertOpenError("No modal dialog is currently open");
   }
 };
 
 /**
  * Enables or disables accepting new socket connections.
  *
  * By calling this method with `false` the server will not accept any further
  * connections, but existing connections will not be forcible closed. Use `true`
--- a/testing/marionette/harness/marionette_harness/runner/mixins/window_manager.py
+++ b/testing/marionette/harness/marionette_harness/runner/mixins/window_manager.py
@@ -16,20 +16,20 @@ class WindowManagerMixin(object):
 
         self.start_window = self.marionette.current_chrome_window_handle
         self.start_windows = self.marionette.chrome_window_handles
 
         self.start_tab = self.marionette.current_window_handle
         self.start_tabs = self.marionette.window_handles
 
     def tearDown(self):
-        if len(self.marionette.chrome_window_handles) != len(self.start_windows):
+        if len(self.marionette.chrome_window_handles) > len(self.start_windows):
             raise Exception("Not all windows as opened by the test have been closed")
 
-        if len(self.marionette.window_handles) != len(self.start_tabs):
+        if len(self.marionette.window_handles) > len(self.start_tabs):
             raise Exception("Not all tabs as opened by the test have been closed")
 
         super(WindowManagerMixin, self).tearDown()
 
     def close_all_tabs(self):
         current_window_handles = self.marionette.window_handles
 
         # If the start tab is not present anymore, use the next one of the list
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_modal_dialogs.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_modal_dialogs.py
@@ -1,31 +1,33 @@
 # 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/.
 
 from marionette_driver.by import By
 from marionette_driver.errors import NoAlertPresentException, ElementNotInteractableException
+from marionette_driver.expected import element_present
 from marionette_driver.marionette import Alert
 from marionette_driver.wait import Wait
 
 from marionette_harness import MarionetteTestCase, skip_if_e10s, WindowManagerMixin
 
 
 class BaseAlertTestCase(WindowManagerMixin, MarionetteTestCase):
 
     def alert_present(self):
         try:
             Alert(self.marionette).text
             return True
         except NoAlertPresentException:
             return False
 
-    def wait_for_alert(self):
-        Wait(self.marionette).until(lambda _: self.alert_present())
+    def wait_for_alert(self, timeout=None):
+        Wait(self.marionette, timeout=timeout).until(
+            lambda _: self.alert_present())
 
     def wait_for_alert_closed(self, timeout=None):
         Wait(self.marionette, timeout=timeout).until(
             lambda _: not self.alert_present())
 
 
 class TestTabModalAlerts(BaseAlertTestCase):
 
@@ -93,16 +95,27 @@ class TestTabModalAlerts(BaseAlertTestCa
     def test_prompt_dismiss(self):
         self.marionette.find_element(By.ID, "tab-modal-prompt").click()
         self.wait_for_alert()
         alert = self.marionette.switch_to_alert()
         alert.dismiss()
         self.wait_for_condition(
             lambda mn: mn.find_element(By.ID, "prompt-result").text == "null")
 
+    def test_alert_opened_before_session_starts(self):
+        self.marionette.find_element(By.ID, "tab-modal-alert").click()
+        self.wait_for_alert()
+
+        # Restart the session to ensure we still find the formerly left-open dialog.
+        self.marionette.delete_session()
+        self.marionette.start_session()
+
+        alert = self.marionette.switch_to_alert()
+        alert.dismiss()
+
     def test_alert_text(self):
         with self.assertRaises(NoAlertPresentException):
             alert = self.marionette.switch_to_alert()
             alert.text
         self.marionette.find_element(By.ID, "tab-modal-alert").click()
         self.wait_for_alert()
         alert = self.marionette.switch_to_alert()
         self.assertEqual(alert.text, "Marionette alert")
@@ -227,8 +240,21 @@ class TestModalAlerts(BaseAlertTestCase)
         alert.dismiss()
 
         self.wait_for_alert_closed()
 
         status = Wait(self.marionette, timeout=self.marionette.timeout.page_load).until(
             element_present(By.ID, "status")
         )
         self.assertEqual(status.text, "restricted")
+
+    def test_alert_opened_before_session_starts(self):
+        self.marionette.navigate(self.marionette.absolute_url("http_auth"))
+        self.wait_for_alert(timeout=self.marionette.timeout.page_load)
+
+        # Restart the session to ensure we still find the formerly left-open dialog.
+        self.marionette.delete_session()
+        self.marionette.start_session()
+
+        alert = self.marionette.switch_to_alert()
+        alert.dismiss()
+
+        self.wait_for_alert_closed()
--- a/testing/marionette/modal.js
+++ b/testing/marionette/modal.js
@@ -44,16 +44,55 @@ modal.addHandler = function (handler) {
 
   Object.keys(this.handlers).map(topic => {
     this.handlers[topic].add(handler);
     Services.obs.addObserver(handler, topic, false);
   });
 };
 
 /**
+ * Check for already existing modal or tab modal dialogs
+ *
+ * @param {browser.Context} context
+ *     Reference to the browser context to check for existent dialogs.
+ *
+ * @return {modal.Dialog}
+ *     Returns instance of the Dialog class, or `null` if no modal dialog is present.
+ */
+modal.findModalDialogs = function (context) {
+  // First check if there is a modal dialog already present for the current browser window.
+  let winEn = Services.wm.getEnumerator(null);
+  while (winEn.hasMoreElements()) {
+    let win = winEn.getNext();
+
+    // Modal dialogs which do not have an opener set, we cannot detect as long
+    // as GetZOrderDOMWindowEnumerator doesn't work on Linux (Bug 156333).
+    if (win.document.documentURI === "chrome://global/content/commonDialog.xul" &&
+        win.opener && win.opener === context.window) {
+      return new modal.Dialog(() => context, Cu.getWeakReference(win));
+    }
+  }
+
+  // If no modal dialog has been found, also check if there is an open tab modal
+  // dialog present for the current tab.
+  // TODO: Find an adequate implementation for Fennec.
+  if (context.tab && context.tabBrowser.getTabModalPromptBox) {
+    let contentBrowser = context.contentBrowser;
+    let promptManager = context.tabBrowser.getTabModalPromptBox(contentBrowser);
+    let prompts = promptManager.listPrompts();
+
+    if (prompts.length) {
+      return new modal.Dialog(() => context, null);
+    }
+  }
+
+  return null;
+};
+
+/**
  * Remove modal dialogue handler by function reference.
  *
  * This function is a no-op if called on any other product than Firefox.
  *
  * @param {function} toRemove
  *     The handler previously passed to modal.addHandler which will now
  *     be removed.
  */