Bug 1107706: Part 11: Global modal dialogue support
☠☠ backed out by 4c2c50594967 ☠ ☠
authorAndreas Tolfsen <ato@mozilla.com>
Fri, 20 Mar 2015 15:46:46 +0000
changeset 265683 5dce917aeb925d0e8e0ae412d73414fb97a06140
parent 265682 933d7aa1c709d5b67ae24003f8a4ab45a324a4b1
child 265684 18c48c6a0cd5a040832959351ccf8783c252e19e
push id830
push userraliiev@mozilla.com
push dateFri, 19 Jun 2015 19:24:37 +0000
treeherdermozilla-release@932614382a68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1107706
milestone39.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1107706: Part 11: Global modal dialogue support
testing/marionette/driver.js
testing/marionette/jar.mn
testing/marionette/modal.js
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -21,16 +21,17 @@ this.DevToolsUtils = devtools.require("d
 
 XPCOMUtils.defineLazyServiceGetter(
     this, "cookieManager", "@mozilla.org/cookiemanager;1", "nsICookieManager");
 
 Cu.import("chrome://marionette/content/emulator.js");
 Cu.import("chrome://marionette/content/error.js");
 Cu.import("chrome://marionette/content/marionette-elements.js");
 Cu.import("chrome://marionette/content/marionette-simpletest.js");
+Cu.import("chrome://marionette/content/modal.js");
 
 loader.loadSubScript("chrome://marionette/content/marionette-common.js");
 
 // preserve this import order:
 let utils = {};
 loader.loadSubScript("chrome://marionette/content/EventUtils.js", utils);
 loader.loadSubScript("chrome://marionette/content/ChromeUtils.js", utils);
 loader.loadSubScript("chrome://marionette/content/atoms.js", utils);
@@ -40,18 +41,16 @@ loader.loadSubScript("chrome://marionett
 this.EXPORTED_SYMBOLS = ["GeckoDriver", "Context"];
 
 const FRAME_SCRIPT = "chrome://marionette/content/marionette-listener.js";
 const BROWSER_STARTUP_FINISHED = "browser-delayed-startup-finished";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 const SECURITY_PREF = "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer";
 const CLICK_TO_START_PREF = "marionette.debugging.clicktostart";
 const CONTENT_LISTENER_PREF = "marionette.contentListener";
-const COMMON_DIALOG_LOADED = "common-dialog-loaded";
-const TABMODAL_DIALOG_LOADED = "tabmodal-dialog-loaded";
 
 const logger = Log.repository.getLogger("Marionette");
 const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
 const globalMessageManager = Cc["@mozilla.org/globalmessagemanager;1"]
     .getService(Ci.nsIMessageBroadcaster);
 let specialpowers = {};
 
 // This is used to prevent newSession from returning before the telephony
@@ -100,17 +99,16 @@ this.Context.fromString = function(s) {
  * @param {function(string, Object, number)} sendAsyncFn
  *     Callback for sending async messages to the current listener.
  * @param {function(): BrowserObj} curBrowserFn
  *     Function that returns the current browser.
  */
 let ListenerProxy = function(mmFn, sendAsyncFn, curBrowserFn) {
   this.curCmdId = null;
   this.sendAsync = sendAsyncFn;
-  this.ondialog = d => {};
 
   this.mmFn_ = mmFn;
   this.curBrowserFn_ = curBrowserFn;
 };
 
 Object.defineProperty(ListenerProxy.prototype, "mm", {
   get: function() { return this.mmFn_(); }
 });
@@ -118,92 +116,62 @@ Object.defineProperty(ListenerProxy.prot
 Object.defineProperty(ListenerProxy.prototype, "curBrowser", {
   get: function() { return this.curBrowserFn_(); }
 });
 
 ListenerProxy.prototype.__noSuchMethod__ = function*(name, args) {
   const ok = "Marionette:ok";
   const val = "Marionette:done";
   const err = "Marionette:error";
-  const all = [ok, val, err];
 
   let proxy = new Promise((resolve, reject) => {
+    let removeListeners = (name, fn) => {
+      let rmFn = msg => {
+        if (this.isOutOfSync(msg.json.command_id)) {
+          logger.warn("Skipping out-of-sync response from listener: " +
+              msg.name + msg.json.toSource());
+          return;
+        }
+
+        listeners.remove();
+        modal.removeHandler(handleDialog);
+
+        fn(msg);
+        this.curCmdId = null;
+      };
+
+      listeners.push([name, rmFn]);
+      return rmFn;
+    };
+
     let listeners = [];
-    let obs = new Map();
-    obs.add = function(modalHandler) {
-      if (Services.appinfo.name != "Firefox")
-        return;
-      this.set(COMMON_DIALOG_LOADED, modalHandler);
-      this.set(TABMODAL_DIALOG_LOADED, modalHandler);
-      for (let [t,o] of this) {
-        Services.obs.addObserver(o, t, false);
-      }
+    listeners.add = () => {
+      this.mm.addMessageListener(ok, removeListeners(ok, okListener));
+      this.mm.addMessageListener(val, removeListeners(val, valListener));
+      this.mm.addMessageListener(err, removeListeners(err, errListener));
     };
-    obs.remove = function() {
-      for (let [t,o] of this) {
-        Services.obs.removeObserver(o, t);
-      }
-    };
+    listeners.remove = () =>
+        listeners.map(l => this.mm.removeMessageListener(l[0], l[1]));
 
     let okListener = () => resolve();
     let valListener = msg => resolve(msg.json.value);
     let errListener = msg => reject(
         "error" in msg.objects ? msg.objects.error : msg.json);
 
-    let handleDialogLoad = function(subject, topic) {
-      obs.remove();
-      this.cancelRequest();
-
-      // we shouldn't return to the client due to the modal associated with the
-      // jsdebugger
-      let clickToStart;
-      try {
-        clickToStart = Services.prefs.getBoolPref(CLICK_TO_START_PREF);
-      } catch (e) {}
-      if (clickToStart) {
-        Services.prefs.setBoolPref(CLICK_TO_START_PREF, false);
-        return;
-      }
-
-      let winr;
-      if (topic == COMMON_DIALOG_LOADED)
-        winr = Cu.getWeakReference(subject);
-      let d = new ModalDialog(() => this.curBrowser, winr);
-      this.ondialog(d);
-
-      // shortcut to return a response immediately,
-      // causes next reply from listener to be out-of-sync
+    let handleDialog = function(subject, topic) {
+      listeners.remove();
+      modal.removeHandler(handleDialog);
+      this.sendAsync("cancelRequest");
       resolve();
-    };
-
-    let removeListeners = (name, listenerFn) => {
-      let fn = msg => {
-        if (this.isOutOfSync(msg.json.command_id)) {
-          logger.warn("Skipping out-of-sync response from listener: " +
-              msg.name + msg.json.toSource());
-          return;
-        }
-
-        listeners.map(l => this.mm.removeMessageListener(l[0], l[1]));
-        obs.remove();
-
-        listenerFn(msg);
-        this.curCmdId = null;
-      };
-
-      listeners.push([name, fn]);
-      return fn;
-    };
-
-    this.mm.addMessageListener(ok, removeListeners(ok, okListener));
-    this.mm.addMessageListener(val, removeListeners(val, valListener));
-    this.mm.addMessageListener(err, removeListeners(err, errListener));
-
-    // install observers for global- and tab modal dialogues
-    obs.add(handleDialogLoad.bind(this));
+    }.bind(this);
+
+    // start content process listeners, and install observers for global-
+    // and tab modal dialogues
+    listeners.add();
+    modal.addHandler(handleDialog);
 
     // convert to array if passed arguments
     let msg;
     if (args.length == 1 && typeof args[0] == "object" && args[0] !== null)
       msg = args[0];
     else
       msg = Array.prototype.slice.call(args);
 
@@ -264,17 +232,16 @@ this.GeckoDriver = function(appName, dev
   this.testName = null;
   this.mozBrowserClose = null;
   this.enabled_security_pref = false;
   this.sandbox = null;
   // frame ID of the current remote frame, used for mozbrowserclose events
   this.oopFrameId = null;
   this.observing = null;
   this._browserIds = new WeakMap();
-  this.dialog = null;
 
   this.sessionCapabilities = {
     // Mandated capabilities
     "browserName": this.appName,
     "browserVersion": Services.appinfo.version,
     "platformName": Services.appinfo.OS.toUpperCase(),
     "platformVersion": Services.appinfo.platformVersion,
 
@@ -297,17 +264,25 @@ this.GeckoDriver = function(appName, dev
     "version": Services.appinfo.version
   };
 
   this.mm = globalMessageManager;
   this.listener = new ListenerProxy(
       () => this.mm,
       this.sendAsync.bind(this),
       () => this.curBrowser);
-  this.listener.ondialog = d => this.dialog = d;
+
+  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);
 };
 
 GeckoDriver.prototype.QueryInterface = XPCOMUtils.generateQI([
   Ci.nsIMessageListener,
   Ci.nsIObserver,
   Ci.nsISupportsWeakReference
 ]);
 
@@ -3032,56 +3007,16 @@ GeckoDriver.prototype.commands = {
   "maximizeWindow": GeckoDriver.prototype.maximizeWindow,
   "dismissDialog": GeckoDriver.prototype.dismissDialog,
   "acceptDialog": GeckoDriver.prototype.acceptDialog,
   "getTextFromDialog": GeckoDriver.prototype.getTextFromDialog,
   "sendKeysToDialog": GeckoDriver.prototype.sendKeysToDialog
 };
 
 /**
- * Represents the current modal dialogue.
- *
- * @param {function(): BrowserObj} curBrowserFn
- *     Function that returns the current BrowserObj.
- * @param {?nsIWeakReference} winRef
- *     A weak reference to the current ChromeWindow.
- */
-this.ModalDialog = function(curBrowserFn, winRef=null) {
-  Object.defineProperty(this, "curBrowser", {
-    get() { return curBrowserFn(); }
-  });
-  this.win_ = winRef;
-};
-
-/**
- * Returns the ChromeWindow associated with an open dialog window if it is
- * currently attached to the dom.
- *
- */
-Object.defineProperty(ModalDialog.prototype, "window", {
-  get() {
-    if (this.win_ !== null) {
-      let win = this.win_.get();
-      if (win && win.parent)
-        return win;
-    }
-    return null;
-  }
-});
-
-Object.defineProperty(ModalDialog.prototype, "ui", {
-  get() {
-    let win = this.window;
-    if (win)
-      return win.Dialog.ui;
-    return this.curBrowser.getTabModalUI();
-  }
-});
-
-/**
  * Creates a BrowserObj.  BrowserObjs handle interactions with the
  * browser, according to the current environment (desktop, b2g, etc.).
  *
  * @param {nsIDOMWindow} win
  *     The window whose browser needs to be accessed.
  * @param {GeckoDriver} driver
  *     Reference to the driver the browser is attached to.
  */
--- a/testing/marionette/jar.mn
+++ b/testing/marionette/jar.mn
@@ -14,16 +14,17 @@ marionette.jar:
   content/marionette-simpletest.js  (marionette-simpletest.js)
   content/marionette-frame-manager.js  (marionette-frame-manager.js)
   content/EventUtils.js  (EventUtils.js)
   content/ChromeUtils.js  (ChromeUtils.js)
   content/error.js (error.js)
   content/command.js (command.js)
   content/dispatcher.js (dispatcher.js)
   content/emulator.js (emulator.js)
+  content/modal.js (modal.js)
 #ifdef ENABLE_TESTS
   content/test.xul  (client/marionette/chrome/test.xul)
   content/test2.xul  (client/marionette/chrome/test2.xul)
   content/test_nested_iframe.xul  (client/marionette/chrome/test_nested_iframe.xul)
   content/test_anonymous_content.xul  (client/marionette/chrome/test_anonymous_content.xul)
 #endif
 
 % content specialpowers %content/
new file mode 100644
--- /dev/null
+++ b/testing/marionette/modal.js
@@ -0,0 +1,113 @@
+/* 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 {utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+this.EXPORTED_SYMBOLS = ["modal"];
+
+let isFirefox = () => Services.appinfo.name == "Firefox";
+
+this.modal = {};
+modal = {
+  COMMON_DIALOG_LOADED: "common-dialog-loaded",
+  TABMODAL_DIALOG_LOADED: "tabmodal-dialog-loaded",
+  handlers: {
+    "common-dialog-loaded": new Set(),
+    "tabmodal-dialog-loaded": new Set()
+  }
+};
+
+/**
+ * Add handler that will be called when a global- or tab modal dialogue
+ * appears.
+ *
+ * This is achieved by installing observers for common-
+ * and tab modal loaded events.
+ *
+ * This function is a no-op if called on any other product than Firefox.
+ *
+ * @param {function(Object, string)} handler
+ *     The handler to be called, which is passed the
+ *     subject (e.g. ChromeWindow) and the topic (one of
+ *     {@code modal.COMMON_DIALOG_LOADED} or
+ *     {@code modal.TABMODAL_DIALOG_LOADED}.
+ */
+modal.addHandler = function(handler) {
+  if (!isFirefox()) {
+    return;
+  }
+
+  Object.keys(this.handlers).map(topic => {
+    this.handlers[topic].add(handler);
+    Services.obs.addObserver(handler, topic, false);
+  });
+};
+
+/**
+ * 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.
+ */
+modal.removeHandler = function(toRemove) {
+  if (!isFirefox()) {
+    return;
+  }
+
+  for (let topic of Object.keys(this.handlers)) {
+    let handlers = this.handlers[topic];
+    for (let handler of handlers) {
+      if (handler == toRemove) {
+        Services.obs.removeObserver(handler, topic);
+        handlers.delete(handler);
+      }
+    }
+  }
+};
+
+/**
+ * Represents the current modal dialogue.
+ *
+ * @param {function(): BrowserObj} curBrowserFn
+ *     Function that returns the current BrowserObj.
+ * @param {?nsIWeakReference} winRef
+ *     A weak reference to the current ChromeWindow.
+ */
+modal.Dialog = function(curBrowserFn, winRef=null) {
+  Object.defineProperty(this, "curBrowser", {
+    get() { return curBrowserFn(); }
+  });
+  this.win_ = winRef;
+};
+
+/**
+ * Returns the ChromeWindow associated with an open dialog window if it
+ * is currently attached to the DOM.
+ */
+Object.defineProperty(modal.Dialog.prototype, "window", {
+  get() {
+    if (this.win_ !== null) {
+      let win = this.win_.get();
+      if (win && win.parent)
+        return win;
+    }
+    return null;
+  }
+});
+
+Object.defineProperty(modal.Dialog.prototype, "ui", {
+  get() {
+    let win = this.window;
+    if (win)
+      return win.Dialog.ui;
+    return this.curBrowser.getTabModalUI();
+  }
+});