Bug 1504756 - [marionette] Added "WebDriver:NewWindow" command to open a new top-level browsing context. r=ato
☠☠ backed out by 49bd1dd914a4 ☠ ☠
authorHenrik Skupin <mail@hskupin.info>
Tue, 04 Dec 2018 21:59:43 +0000
changeset 508563 30d345cce5be1236aed9380e7c4d121a99800d70
parent 508562 a8ea6d01fbe1de92009842a4028c9179646a88f5
child 508564 d808b528532ac7ccb48bbcb7447f194a822381e5
push id1905
push userffxbld-merge
push dateMon, 21 Jan 2019 12:33:13 +0000
treeherdermozilla-release@c2fca1944d8c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersato
bugs1504756
milestone65.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 `Create 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
@@ -66,21 +66,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.
@@ -295,16 +295,60 @@ browser.Context = class {
     let unloaded = waitForEvent(this.window, "unload");
 
     this.window.close();
 
     return Promise.all([disconnected, 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();
+
+        // 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);
+
+        let activated = waitForEvent(win, "activate");
+        let focused = waitForEvent(win, "focus", {capture: true});
+        let startup = waitForObserverTopic("browser-delayed-startup-finished",
+            subject => subject == win);
+        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.
    */
@@ -320,42 +364,68 @@ browser.Context = class {
 
     // Create a copy of the messageManager before it is disconnected
     let messageManager = this.messageManager;
     let disconnected = waitForObserverTopic("message-manager-disconnect",
         subject => subject === 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([disconnected, 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.
@@ -379,26 +449,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
@@ -1304,16 +1303,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);
@@ -2720,16 +2720,83 @@ 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`.
+ * @param {boolean=} focus
+ *     Optional flag if the new top-level browsing context should be opened
+ *     in foreground (focused) or background (not focused).
+ *
+ * @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}`);
+  }
+
+  let types = ["tab", "window"];
+  switch (this.appName) {
+    case "firefox":
+      if (typeof type == "undefined" || !types.includes(type)) {
+        type = "window";
+      }
+      break;
+    case "fennec":
+      if (typeof type == "undefined" || !types.includes(type)) {
+        type = "tab";
+      }
+      break;
+  }
+
+  let contentBrowser;
+
+  switch (type) {
+    case "tab":
+      let tab = await this.curBrowser.openTab(focus);
+      contentBrowser = browser.getBrowserForTab(tab);
+      break;
+
+    default:
+      let win = await this.curBrowser.openBrowserWindow(focus);
+      contentBrowser = browser.getTabBrowser(win).selectedBrowser;
+  }
+
+  // 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.
  *
@@ -3559,16 +3626,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;