Bug 1504756 - [marionette] Added "WebDriver:NewWindow" command to open a new top-level browsing context. r=ato
☠☠ backed out by 4ff41b70cbd8 ☠ ☠
authorHenrik Skupin <mail@hskupin.info>
Wed, 09 Jan 2019 18:27:28 +0000
changeset 510236 8f9d90979825a6e3be54dac5aa64b47f15889d43
parent 510235 9715660f8c07d7e31aaf2dbb02c37103942c2206
child 510237 9d80f662ad2bda4696f525d8c12f3a3ce0addf44
push id10547
push userffxbld-merge
push dateMon, 21 Jan 2019 13:03:58 +0000
treeherdermozilla-beta@24ec1916bffe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersato
bugs1504756
milestone66.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 1504756 - [marionette] Added "WebDriver:NewWindow" command to open a new top-level browsing context. r=ato The patch adds the end-point for the recently defined `New Window` command (https://github.com/w3c/webdriver/issues/1138). It allows to open a new top-level browsing context as tab or as window. Depends on D13662 Differential Revision: https://phabricator.services.mozilla.com/D13663
testing/marionette/browser.js
testing/marionette/driver.js
testing/marionette/server.js
--- a/testing/marionette/browser.js
+++ b/testing/marionette/browser.js
@@ -9,16 +9,17 @@ const {WebElementEventTarget} = ChromeUt
 ChromeUtils.import("chrome://marionette/content/element.js");
 const {
   NoSuchWindowError,
   UnsupportedOperationError,
 } = ChromeUtils.import("chrome://marionette/content/error.js", {});
 const {
   MessageManagerDestroyedPromise,
   waitForEvent,
+  waitForObserverTopic,
 } = ChromeUtils.import("chrome://marionette/content/sync.js", {});
 
 this.EXPORTED_SYMBOLS = ["browser", "Context", "WindowState"];
 
 /** @namespace */
 this.browser = {};
 
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
@@ -66,21 +67,21 @@ this.Context = Context;
  * @param {Tab} tab
  *     The tab whose browser needs to be returned.
  *
  * @return {Browser}
  *     The linked browser for the tab or null if no browser can be found.
  */
 browser.getBrowserForTab = function(tab) {
   // Fennec
-  if ("browser" in tab) {
+  if (tab && "browser" in tab) {
     return tab.browser;
 
   // Firefox
-  } else if ("linkedBrowser" in tab) {
+  } else if (tab && "linkedBrowser" in tab) {
     return tab.linkedBrowser;
   }
 
   return null;
 };
 
 /**
  * Return the tab browser for the specified chrome window.
@@ -293,16 +294,61 @@ browser.Context = class {
     let unloaded = waitForEvent(this.window, "unload");
 
     this.window.close();
 
     return Promise.all([destroyed, unloaded]);
   }
 
   /**
+   * Open a new browser window.
+   *
+   * @return {Promise}
+   *     A promise resolving to the newly created chrome window.
+   */
+  async openBrowserWindow(focus = false) {
+    switch (this.driver.appName) {
+      case "firefox":
+        // Open new browser window, and wait until it is fully loaded.
+        // Also wait for the window to be focused and activated to prevent a
+        // race condition when promptly focusing to the original window again.
+        let win = this.window.OpenBrowserWindow();
+
+        let activated = waitForEvent(win, "activate");
+        let focused = waitForEvent(win, "focus", {capture: true});
+        let startup = waitForObserverTopic("browser-delayed-startup-finished",
+            subject => subject == win);
+
+        // Bug 1509380 - Missing focus/activate event when Firefox is not
+        // the top-most application. As such wait for the next tick, and
+        // manually focus the newly opened window.
+        win.setTimeout(() => win.focus(), 0);
+
+        await Promise.all([activated, focused, startup]);
+
+        if (!focus) {
+          // The new window shouldn't get focused. As such set the
+          // focus back to the currently selected window.
+          activated = waitForEvent(this.window, "activate");
+          focused = waitForEvent(this.window, "focus", {capture: true});
+
+          this.window.focus();
+
+          await Promise.all([activated, focused]);
+        }
+
+        return win;
+
+      default:
+        throw new UnsupportedOperationError(
+            `openWindow() not supported in ${this.driver.appName}`);
+    }
+  }
+
+  /**
    * Close the current tab.
    *
    * @return {Promise}
    *     A promise which is resolved when the current tab has been closed.
    *
    * @throws UnsupportedOperationError
    *     If tab handling for the current application isn't supported.
    */
@@ -314,42 +360,68 @@ browser.Context = class {
         this.tabBrowser.tabs.length === 1 ||
         !this.tab) {
       return this.closeWindow();
     }
 
     let destroyed = new MessageManagerDestroyedPromise(this.messageManager);
     let tabClosed;
 
-    if (this.tabBrowser.closeTab) {
-      // Fennec
-      tabClosed = waitForEvent(this.tabBrowser.deck, "TabClose");
-      this.tabBrowser.closeTab(this.tab);
+    switch (this.driver.appName) {
+      case "fennec":
+        // Fennec
+        tabClosed = waitForEvent(this.tabBrowser.deck, "TabClose");
+        this.tabBrowser.closeTab(this.tab);
+        break;
 
-    } else if (this.tabBrowser.removeTab) {
-      // Firefox
-      tabClosed = waitForEvent(this.tab, "TabClose");
-      this.tabBrowser.removeTab(this.tab);
+      case "firefox":
+        tabClosed = waitForEvent(this.tab, "TabClose");
+        this.tabBrowser.removeTab(this.tab);
+        break;
 
-    } else {
-      throw new UnsupportedOperationError(
-        `closeTab() not supported in ${this.driver.appName}`);
+      default:
+        throw new UnsupportedOperationError(
+          `closeTab() not supported in ${this.driver.appName}`);
     }
 
     return Promise.all([destroyed, tabClosed]);
   }
 
   /**
-   * Opens a tab with given URI.
-   *
-   * @param {string} uri
-   *      URI to open.
+   * Open a new tab in the currently selected chrome window.
    */
-  addTab(uri) {
-    return this.tabBrowser.addTab(uri, true);
+  async openTab(focus = false) {
+    let tab = null;
+    let tabOpened = waitForEvent(this.window, "TabOpen");
+
+    switch (this.driver.appName) {
+      case "fennec":
+        tab = this.tabBrowser.addTab(null, {selected: focus});
+        break;
+
+      case "firefox":
+        this.window.BrowserOpenTab();
+        tab = this.tabBrowser.selectedTab;
+
+        // The new tab is always selected by default. If focus is not wanted,
+        // the previously tab needs to be selected again.
+        if (!focus) {
+          this.tabBrowser.selectedTab = this.tab;
+        }
+
+        break;
+
+      default:
+        throw new UnsupportedOperationError(
+          `openTab() not supported in ${this.driver.appName}`);
+    }
+
+    await tabOpened;
+
+    return tab;
   }
 
   /**
    * Set the current tab.
    *
    * @param {number=} index
    *     Tab index to switch to. If the parameter is undefined,
    *     the currently selected tab will be used.
@@ -373,26 +445,28 @@ browser.Context = class {
     }
 
     if (typeof index == "undefined") {
       this.tab = this.tabBrowser.selectedTab;
     } else {
       this.tab = this.tabBrowser.tabs[index];
 
       if (focus) {
-        if (this.tabBrowser.selectTab) {
-          // Fennec
-          this.tabBrowser.selectTab(this.tab);
+        switch (this.driver.appName) {
+          case "fennec":
+            this.tabBrowser.selectTab(this.tab);
+            break;
 
-        } else if ("selectedTab" in this.tabBrowser) {
-          // Firefox
-          this.tabBrowser.selectedTab = this.tab;
+          case "firefox":
+            this.tabBrowser.selectedTab = this.tab;
+            break;
 
-        } else {
-          throw new UnsupportedOperationError("switchToTab() not supported");
+          default:
+            throw new UnsupportedOperationError(
+              `switchToTab() not supported in ${this.driver.appName}`);
         }
       }
     }
 
     // TODO(ato): Currently tied to curBrowser, but should be moved to
     // WebElement when introduced by https://bugzil.la/1400256.
     this.eventObserver = new WebElementEventTarget(this.messageManager);
   }
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -103,23 +103,22 @@ const globalMessageManager = Services.mm
  * browsing context's content frame message listener via ListenerProxy.
  *
  * Throughout this prototype, functions with the argument <var>cmd</var>'s
  * documentation refers to the contents of the <code>cmd.parameter</code>
  * object.
  *
  * @class GeckoDriver
  *
- * @param {string} appId
- *     Unique identifier of the application.
  * @param {MarionetteServer} server
  *     The instance of Marionette server.
  */
-this.GeckoDriver = function(appId, server) {
-  this.appId = appId;
+this.GeckoDriver = function(server) {
+  this.appId = Services.appinfo.ID;
+  this.appName = Services.appinfo.name.toLowerCase();
   this._server = server;
 
   this.sessionID = null;
   this.wins = new browser.Windows();
   this.browsers = {};
   // points to current browser
   this.curBrowser = null;
   // top-most chrome window
@@ -1289,16 +1288,17 @@ GeckoDriver.prototype.updateIdForBrowser
  * Retrieves a listener id for the given xul browser element. In case
  * the browser is not known, an attempt is made to retrieve the id from
  * a CPOW, and null is returned if this fails.
  */
 GeckoDriver.prototype.getIdForBrowser = function(browser) {
   if (browser === null) {
     return null;
   }
+
   let permKey = browser.permanentKey;
   if (this._browserIds.has(permKey)) {
     return this._browserIds.get(permKey);
   }
 
   let winId = browser.outerWindowID;
   if (winId) {
     this._browserIds.set(permKey, winId);
@@ -2705,16 +2705,76 @@ GeckoDriver.prototype.deleteCookie = asy
   for (let c of cookie.iter(hostname, pathname)) {
     if (c.name === name) {
       cookie.remove(c);
     }
   }
 };
 
 /**
+ * Open a new top-level browsing context.
+ *
+ * @param {string=} type
+ *     Optional type of the new top-level browsing context. Can be one of
+ *     `tab` or `window`. Defaults to `tab`.
+ * @param {boolean=} focus
+ *     Optional flag if the new top-level browsing context should be opened
+ *     in foreground (focused) or background (not focused). Defaults to false.
+ *
+ * @return {Object.<string, string>}
+ *     Handle and type of the new browsing context.
+ */
+GeckoDriver.prototype.newWindow = async function(cmd) {
+  assert.open(this.getCurrentWindow(Context.Content));
+  await this._handleUserPrompts();
+
+  let focus = false;
+  if (typeof cmd.parameters.focus != "undefined") {
+    focus = assert.boolean(cmd.parameters.focus,
+        pprint`Expected "focus" to be a boolean, got ${cmd.parameters.focus}`);
+  }
+
+  let type;
+  if (typeof cmd.parameters.type != "undefined") {
+    type = assert.string(cmd.parameters.type,
+        pprint`Expected "type" to be a string, got ${cmd.parameters.type}`);
+  }
+
+  // If an invalid or no type has been specified default to a tab.
+  if (typeof type == "undefined" || !["tab", "window"].includes(type)) {
+    type = "tab";
+  }
+
+  let contentBrowser;
+
+  switch (type) {
+    case "window":
+      let win = await this.curBrowser.openBrowserWindow(focus);
+      contentBrowser = browser.getTabBrowser(win).selectedBrowser;
+      break;
+
+    default:
+      // To not fail if a new type gets added in the future, make opening
+      // a new tab the default action.
+      let tab = await this.curBrowser.openTab(focus);
+      contentBrowser = browser.getBrowserForTab(tab);
+  }
+
+  // Even with the framescript registered, the browser might not be known to
+  // the parent process yet. Wait until it is available.
+  // TODO: Fix by using `Browser:Init` or equivalent on bug 1311041
+  let windowId = await new PollPromise((resolve, reject) => {
+    let id = this.getIdForBrowser(contentBrowser);
+    this.windowHandles.includes(id) ? resolve(id) : reject();
+  });
+
+  return {handle: windowId.toString(), type};
+};
+
+/**
  * Close the currently selected tab/window.
  *
  * With multiple open tabs present the currently selected tab will
  * be closed.  Otherwise the window itself will be closed. If it is the
  * last window currently open, the window will not be closed to prevent
  * a shutdown of the application. Instead the returned list of window
  * handles is empty.
  *
@@ -3544,16 +3604,17 @@ GeckoDriver.prototype.commands = {
   "WebDriver:GetWindowRect": GeckoDriver.prototype.getWindowRect,
   "WebDriver:IsElementDisplayed": GeckoDriver.prototype.isElementDisplayed,
   "WebDriver:IsElementEnabled": GeckoDriver.prototype.isElementEnabled,
   "WebDriver:IsElementSelected": GeckoDriver.prototype.isElementSelected,
   "WebDriver:MinimizeWindow": GeckoDriver.prototype.minimizeWindow,
   "WebDriver:MaximizeWindow": GeckoDriver.prototype.maximizeWindow,
   "WebDriver:Navigate": GeckoDriver.prototype.get,
   "WebDriver:NewSession": GeckoDriver.prototype.newSession,
+  "WebDriver:NewWindow": GeckoDriver.prototype.newWindow,
   "WebDriver:PerformActions": GeckoDriver.prototype.performActions,
   "WebDriver:Refresh":  GeckoDriver.prototype.refresh,
   "WebDriver:ReleaseActions": GeckoDriver.prototype.releaseActions,
   "WebDriver:SendAlertText": GeckoDriver.prototype.sendKeysToDialog,
   "WebDriver:SetTimeouts": GeckoDriver.prototype.setTimeouts,
   "WebDriver:SetWindowRect": GeckoDriver.prototype.setWindowRect,
   "WebDriver:SwitchToFrame": GeckoDriver.prototype.switchToFrame,
   "WebDriver:SwitchToParentFrame": GeckoDriver.prototype.switchToParentFrame,
--- a/testing/marionette/server.js
+++ b/testing/marionette/server.js
@@ -6,17 +6,16 @@
 
 const CC = Components.Constructor;
 
 const ServerSocket = CC(
     "@mozilla.org/network/server-socket;1",
     "nsIServerSocket",
     "initSpecialConnection");
 
-ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 ChromeUtils.import("chrome://marionette/content/assert.js");
 const {GeckoDriver} = ChromeUtils.import("chrome://marionette/content/driver.js", {});
 const {WebElement} = ChromeUtils.import("chrome://marionette/content/element.js", {});
 const {
   error,
   UnknownCommandError,
@@ -69,17 +68,17 @@ class TCPListener {
    *
    * Determines the application to initialise the driver with.
    *
    * @return {GeckoDriver}
    *     A driver instance.
    */
   driverFactory() {
     MarionettePrefs.contentListener = false;
-    return new GeckoDriver(Services.appinfo.ID, this);
+    return new GeckoDriver(this);
   }
 
   set acceptConnections(value) {
     if (value) {
       if (!this.socket) {
         try {
           const flags = KeepWhenOffline | LoopbackOnly;
           const backlog = 1;