Bug 1691446 - [remote] Add support for handling WebDriverSession to WebDriver BiDi. r=webdriver-reviewers,jdescottes,jgraham
authorHenrik Skupin <mail@hskupin.info>
Fri, 16 Jul 2021 11:56:48 +0000
changeset 585681 5bdb798c3b70a0c1099c0736cc59e5d31f181f58
parent 585680 57d00cc40a5603b7063cad14109b72ec6700c954
child 585682 fd61434c223f4cb98319a0918980eef240085d35
push id38618
push userapavel@mozilla.com
push dateFri, 16 Jul 2021 21:43:02 +0000
treeherdermozilla-central@38aa248ef576 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerswebdriver-reviewers, jdescottes, jgraham
bugs1691446
milestone92.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 1691446 - [remote] Add support for handling WebDriverSession to WebDriver BiDi. r=webdriver-reviewers,jdescottes,jgraham Differential Revision: https://phabricator.services.mozilla.com/D119552
remote/cdp/CDPConnection.jsm
remote/marionette/driver.js
remote/shared/WebSocketConnection.jsm
remote/shared/WindowManager.jsm
remote/shared/webdriver/Session.jsm
remote/webdriver-bidi/WebDriverBiDi.jsm
remote/webdriver-bidi/WebDriverBiDiConnection.jsm
--- a/remote/cdp/CDPConnection.jsm
+++ b/remote/cdp/CDPConnection.jsm
@@ -18,17 +18,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
 });
 
 XPCOMUtils.defineLazyGetter(this, "logger", () => Log.get(Log.TYPES.CDP));
 
 class CDPConnection extends WebSocketConnection {
   /**
    * @param {WebSocket} webSocket
    *     The WebSocket server connection to wrap.
-   * @param {HttpServer} httpdConnection
+   * @param {Connection} httpdConnection
    *     Reference to the httpd.js's connection needed for clean-up.
    */
   constructor(webSocket, httpdConnection) {
     super(webSocket, httpdConnection);
 
     this.sessions = new Map();
     this.defaultSession = null;
   }
--- a/remote/marionette/driver.js
+++ b/remote/marionette/driver.js
@@ -15,16 +15,18 @@ XPCOMUtils.defineLazyModuleGetters(this,
   Services: "resource://gre/modules/Services.jsm",
 
   Addon: "chrome://remote/content/marionette/addon.js",
   AppInfo: "chrome://remote/content/marionette/appinfo.js",
   assert: "chrome://remote/content/marionette/assert.js",
   atom: "chrome://remote/content/marionette/atom.js",
   browser: "chrome://remote/content/marionette/browser.js",
   capture: "chrome://remote/content/marionette/capture.js",
+  clearActionInputState:
+    "chrome://remote/content/marionette/actors/MarionetteCommandsChild.jsm",
   clearElementIdCache:
     "chrome://remote/content/marionette/actors/MarionetteCommandsParent.jsm",
   Context: "chrome://remote/content/marionette/browser.js",
   cookie: "chrome://remote/content/marionette/cookie.js",
   DebounceCallback: "chrome://remote/content/marionette/sync.js",
   element: "chrome://remote/content/marionette/element.js",
   error: "chrome://remote/content/shared/webdriver/Errors.jsm",
   getMarionetteCommandsActorProxy:
@@ -39,16 +41,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
   PollPromise: "chrome://remote/content/marionette/sync.js",
   pprint: "chrome://remote/content/marionette/format.js",
   print: "chrome://remote/content/marionette/print.js",
   reftest: "chrome://remote/content/marionette/reftest.js",
   registerCommandsActor:
     "chrome://remote/content/marionette/actors/MarionetteCommandsParent.jsm",
   registerEventsActor:
     "chrome://remote/content/marionette/actors/MarionetteEventsParent.jsm",
+  RemoteAgent: "chrome://remote/content/components/RemoteAgent.jsm",
   TimedPromise: "chrome://remote/content/marionette/sync.js",
   Timeouts: "chrome://remote/content/shared/webdriver/Capabilities.jsm",
   UnhandledPromptBehavior:
     "chrome://remote/content/shared/webdriver/Capabilities.jsm",
   unregisterCommandsActor:
     "chrome://remote/content/marionette/actors/MarionetteCommandsParent.jsm",
   unregisterEventsActor:
     "chrome://remote/content/marionette/actors/MarionetteEventsParent.jsm",
@@ -103,17 +106,17 @@ const TIMEOUT_NO_WINDOW_MANAGER = 5000;
  *
  * @param {MarionetteServer} server
  *     The instance of Marionette server.
  */
 this.GeckoDriver = function(server) {
   this._server = server;
 
   // WebDriver Session
-  this.currentSession = null;
+  this._currentSession = null;
 
   this.browsers = {};
 
   // points to current browser
   this.curBrowser = null;
   // top-most chrome window
   this.mainFrame = null;
 
@@ -135,16 +138,29 @@ Object.defineProperty(GeckoDriver.protot
   },
 
   set(context) {
     this._context = Context.fromString(context);
   },
 });
 
 /**
+ * The current WebDriver Session.
+ */
+Object.defineProperty(GeckoDriver.prototype, "currentSession", {
+  get() {
+    if (RemoteAgent.webdriverBiDi) {
+      return RemoteAgent.webdriverBiDi.session;
+    }
+
+    return this._currentSession;
+  },
+});
+
+/**
  * Returns the current URL of the ChromeWindow or content browser,
  * depending on context.
  *
  * @return {URL}
  *     Read-only property containing the currently loaded URL.
  */
 Object.defineProperty(GeckoDriver.prototype, "currentURL", {
   get() {
@@ -370,224 +386,95 @@ GeckoDriver.prototype.registerBrowser = 
   ) {
     this.curBrowser.register(browserElement);
   }
 };
 
 /**
  * Create a new WebDriver session.
  *
- * It is expected that the caller performs the necessary checks on
- * the requested capabilities to be WebDriver conforming.  The WebDriver
- * service offered by Marionette does not match or negotiate capabilities
- * beyond type- and bounds checks.
- *
- * <h3>Capabilities</h3>
- *
- * <dl>
- *  <dt><code>pageLoadStrategy</code> (string)
- *  <dd>The page load strategy to use for the current session.  Must be
- *   one of "<tt>none</tt>", "<tt>eager</tt>", and "<tt>normal</tt>".
- *
- *  <dt><code>acceptInsecureCerts</code> (boolean)
- *  <dd>Indicates whether untrusted and self-signed TLS certificates
- *   are implicitly trusted on navigation for the duration of the session.
- *
- *  <dt><code>timeouts</code> (Timeouts object)
- *  <dd>Describes the timeouts imposed on certian session operations.
- *
- *  <dt><code>proxy</code> (Proxy object)
- *  <dd>Defines the proxy configuration.
- *
- *  <dt><code>moz:accessibilityChecks</code> (boolean)
- *  <dd>Run a11y checks when clicking elements.
- *
- *  <dt><code>moz:useNonSpecCompliantPointerOrigin</code> (boolean)
- *  <dd>Use the not WebDriver conforming calculation of the pointer origin
- *   when the origin is an element, and the element center point is used.
- *
- *  <dt><code>moz:webdriverClick</code> (boolean)
- *  <dd>Use a WebDriver conforming <i>WebDriver::ElementClick</i>.
- * </dl>
- *
- * <h4>Timeouts object</h4>
- *
- * <dl>
- *  <dt><code>script</code> (number)
- *  <dd>Determines when to interrupt a script that is being evaluates.
- *
- *  <dt><code>pageLoad</code> (number)
- *  <dd>Provides the timeout limit used to interrupt navigation of the
- *   browsing context.
- *
- *  <dt><code>implicit</code> (number)
- *  <dd>Gives the timeout of when to abort when locating an element.
- * </dl>
- *
- * <h4>Proxy object</h4>
- *
- * <dl>
- *  <dt><code>proxyType</code> (string)
- *  <dd>Indicates the type of proxy configuration.  Must be one
- *   of "<tt>pac</tt>", "<tt>direct</tt>", "<tt>autodetect</tt>",
- *   "<tt>system</tt>", or "<tt>manual</tt>".
- *
- *  <dt><code>proxyAutoconfigUrl</code> (string)
- *  <dd>Defines the URL for a proxy auto-config file if
- *   <code>proxyType</code> is equal to "<tt>pac</tt>".
- *
- *  <dt><code>httpProxy</code> (string)
- *  <dd>Defines the proxy host for HTTP traffic when the
- *   <code>proxyType</code> is "<tt>manual</tt>".
- *
- *  <dt><code>noProxy</code> (string)
- *  <dd>Lists the adress for which the proxy should be bypassed when
- *   the <code>proxyType</code> is "<tt>manual</tt>".  Must be a JSON
- *   List containing any number of any of domains, IPv4 addresses, or IPv6
- *   addresses.
- *
- *  <dt><code>sslProxy</code> (string)
- *  <dd>Defines the proxy host for encrypted TLS traffic when the
- *   <code>proxyType</code> is "<tt>manual</tt>".
- *
- *  <dt><code>socksProxy</code> (string)
- *  <dd>Defines the proxy host for a SOCKS proxy traffic when the
- *   <code>proxyType</code> is "<tt>manual</tt>".
- *
- *  <dt><code>socksVersion</code> (string)
- *  <dd>Defines the SOCKS proxy version when the <code>proxyType</code> is
- *   "<tt>manual</tt>".  It must be any integer between 0 and 255
- *   inclusive.
- * </dl>
- *
- * <h3>Example</h3>
- *
- * Input:
- *
- * <pre><code>
- *     {"capabilities": {"acceptInsecureCerts": true}}
- * </code></pre>
- *
- * @param {Object.<string, *>=} capabilities
- *     JSON Object containing any of the recognised capabilities listed
- *     above.
+ * @param {Object} cmd
+ * @param {Object.<string, *>=} cmd.parameters
+ *     JSON Object containing any of the recognised capabilities as listed
+ *     on the `WebDriverSession` class.
  *
  * @return {Object}
  *     Session ID and capabilities offered by the WebDriver service.
  *
  * @throws {SessionNotCreatedError}
  *     If, for whatever reason, a session could not be created.
  */
 GeckoDriver.prototype.newSession = async function(cmd) {
   if (this.currentSession) {
     throw new error.SessionNotCreatedError("Maximum number of active sessions");
   }
 
-  this.currentSession = new WebDriverSession(cmd.parameters);
-
-  registerCommandsActor();
-  registerEventsActor();
-
-  // Wait until the initial application window has been loaded
-  await new TimedPromise(
-    resolve => {
-      const waitForWindow = () => {
-        let windowTypes;
-        if (AppInfo.isThunderbird) {
-          windowTypes = ["mail:3pane"];
-        } else {
-          // We assume that an app either has GeckoView windows, or
-          // Firefox/Fennec windows, but not both.
-          windowTypes = ["navigator:browser", "navigator:geckoview"];
-        }
-
-        let win;
-        for (const windowType of windowTypes) {
-          win = Services.wm.getMostRecentWindow(windowType);
-          if (win) {
-            break;
-          }
+  const { parameters: capabilities } = cmd;
+
+  try {
+    const win = await windowManager.waitForInitialApplicationWindow();
+
+    if (MarionettePrefs.clickToStart) {
+      Services.prompt.alert(
+        win,
+        "",
+        "Click to start execution of marionette tests"
+      );
+    }
+
+    this.addBrowser(win);
+    this.mainFrame = win;
+
+    // If the WebDriver BiDi protocol is active always use the Remote Agent
+    // to handle the WebDriver session. If it's not the case Marionette itself
+    // needs to handle it.
+    if (RemoteAgent.webdriverBiDi) {
+      RemoteAgent.webdriverBiDi.createSession(capabilities);
+    } else {
+      this._currentSession = new WebDriverSession(capabilities);
+    }
+
+    registerCommandsActor();
+    registerEventsActor();
+
+    for (let win of windowManager.windows) {
+      const tabBrowser = browser.getTabBrowser(win);
+
+      if (tabBrowser) {
+        for (const tab of tabBrowser.tabs) {
+          const contentBrowser = browser.getBrowserForTab(tab);
+          this.registerBrowser(contentBrowser);
         }
-
-        if (!win) {
-          // if the window isn't even created, just poll wait for it
-          let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(
-            Ci.nsITimer
-          );
-          checkTimer.initWithCallback(
-            waitForWindow,
-            100,
-            Ci.nsITimer.TYPE_ONE_SHOT
-          );
-        } else if (win.document.readyState != "complete") {
-          // otherwise, wait for it to be fully loaded before proceeding
-          let listener = ev => {
-            // ensure that we proceed, on the top level document load event
-            // (not an iframe one...)
-            if (ev.target != win.document) {
-              return;
-            }
-            win.removeEventListener("load", listener);
-            waitForWindow();
-          };
-          win.addEventListener("load", listener, true);
-        } else {
-          if (MarionettePrefs.clickToStart) {
-            Services.prompt.alert(
-              win,
-              "",
-              "Click to start execution of marionette tests"
-            );
-          }
-          this.addBrowser(win);
-          this.mainFrame = win;
-          resolve();
-        }
-      };
-
-      waitForWindow();
-    },
-    {
-      throws: error.SessionNotCreatedError,
-      errorMessage: "No applicable application windows found",
+      }
+
+      this.registerListenersForWindow(win);
+    }
+
+    if (this.mainFrame) {
+      this.currentSession.chromeBrowsingContext = this.mainFrame.browsingContext;
+      this.mainFrame.focus();
     }
-  );
-
-  for (let win of windowManager.windows) {
-    const tabBrowser = browser.getTabBrowser(win);
-
-    if (tabBrowser) {
-      for (const tab of tabBrowser.tabs) {
-        const contentBrowser = browser.getBrowserForTab(tab);
-        this.registerBrowser(contentBrowser);
-      }
+
+    if (this.curBrowser.tab) {
+      this.currentSession.contentBrowsingContext = this.curBrowser.contentBrowser.browsingContext;
+      this.curBrowser.contentBrowser.focus();
     }
 
-    this.registerListenersForWindow(win);
+    // Setup observer for modal dialogs
+    this.dialogObserver = new modal.DialogObserver(() => this.curBrowser);
+    this.dialogObserver.add(this.handleModalDialog.bind(this));
+
+    // Check if there is already an open dialog for the selected browser window.
+    this.dialog = modal.findModalDialogs(this.curBrowser);
+
+    Services.obs.addObserver(this, "browser-delayed-startup-finished");
+  } catch (e) {
+    throw new error.SessionNotCreatedError(e);
   }
 
-  if (this.mainFrame) {
-    this.currentSession.chromeBrowsingContext = this.mainFrame.browsingContext;
-    this.mainFrame.focus();
-  }
-
-  if (this.curBrowser.tab) {
-    this.currentSession.contentBrowsingContext = this.curBrowser.contentBrowser.browsingContext;
-    this.curBrowser.contentBrowser.focus();
-  }
-
-  // Setup observer for modal dialogs
-  this.dialogObserver = new modal.DialogObserver(() => this.curBrowser);
-  this.dialogObserver.add(this.handleModalDialog.bind(this));
-
-  // Check if there is already an open dialog for the selected browser window.
-  this.dialog = modal.findModalDialogs(this.curBrowser);
-
-  Services.obs.addObserver(this, "browser-delayed-startup-finished");
-
   return {
     sessionId: this.currentSession.id,
     capabilities: this.currentSession.capabilities,
   };
 };
 
 /**
  * Register event listeners for the specified window.
@@ -1427,16 +1314,17 @@ GeckoDriver.prototype.getTimeouts = func
  *     not an integer.
  */
 GeckoDriver.prototype.setTimeouts = function(cmd) {
   // merge with existing timeouts
   let merged = Object.assign(
     this.currentSession.timeouts.toJSON(),
     cmd.parameters
   );
+
   this.currentSession.timeouts = Timeouts.fromJSON(merged);
 };
 
 /** Single tap. */
 GeckoDriver.prototype.singleTap = async function(cmd) {
   assert.open(this.getBrowsingContext());
 
   let { id, x, y } = cmd.parameters;
@@ -2239,25 +2127,30 @@ GeckoDriver.prototype.deleteSession = fu
 
   if (this.dialogObserver) {
     this.dialogObserver.cleanup();
     this.dialogObserver = null;
   }
 
   Services.obs.removeObserver(this, "browser-delayed-startup-finished");
 
+  clearActionInputState();
   clearElementIdCache();
 
   // Always unregister actors after all other observers
   // and listeners have been removed.
   unregisterCommandsActor();
   unregisterEventsActor();
 
-  this.currentSession.destroy();
-  this.currentSession = null;
+  if (RemoteAgent.webdriverBiDi) {
+    RemoteAgent.webdriverBiDi.deleteSession();
+  } else {
+    this.currentSession.destroy();
+    this._currentSession = null;
+  }
 };
 
 /**
  * Takes a screenshot of a web element, current frame, or viewport.
  *
  * The screen capture is returned as a lossless PNG image encoded as
  * a base 64 string.
  *
--- a/remote/shared/WebSocketConnection.jsm
+++ b/remote/shared/WebSocketConnection.jsm
@@ -24,17 +24,17 @@ XPCOMUtils.defineLazyServiceGetter(
   "@mozilla.org/uuid-generator;1",
   "nsIUUIDGenerator"
 );
 
 class WebSocketConnection {
   /**
    * @param {WebSocket} webSocket
    *     The WebSocket server connection to wrap.
-   * @param {HttpServer} httpdConnection
+   * @param {Connection} httpdConnection
    *     Reference to the httpd.js's connection needed for clean-up.
    */
   constructor(webSocket, httpdConnection) {
     this.id = UUIDGen.generateUUID().toString();
     this.httpdConnection = httpdConnection;
 
     this.transport = new WebSocketTransport(webSocket);
     this.transport.hooks = this;
--- a/remote/shared/WindowManager.jsm
+++ b/remote/shared/WindowManager.jsm
@@ -7,19 +7,21 @@
 const EXPORTED_SYMBOLS = ["windowManager"];
 
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   Services: "resource://gre/modules/Services.jsm",
+
   AppInfo: "chrome://remote/content/marionette/appinfo.js",
   browser: "chrome://remote/content/marionette/browser.js",
   error: "chrome://remote/content/shared/webdriver/Errors.jsm",
+  TimedPromise: "chrome://remote/content/marionette/sync.js",
   waitForEvent: "chrome://remote/content/marionette/sync.js",
   waitForObserverTopic: "chrome://remote/content/marionette/sync.js",
 });
 
 XPCOMUtils.defineLazyServiceGetter(
   this,
   "uuidGen",
   "@mozilla.org/uuid-generator;1",
@@ -263,12 +265,74 @@ class WindowManager {
         return win;
 
       default:
         throw new error.UnsupportedOperationError(
           `openWindow() not supported in ${AppInfo.name}`
         );
     }
   }
+
+  /**
+   * Wait until the initial application window has been opened and loaded.
+   *
+   * @return {Promise<WindowProxy>}
+   *     A promise that resolved to the application window.
+   */
+  waitForInitialApplicationWindow() {
+    return new TimedPromise(
+      resolve => {
+        const waitForWindow = () => {
+          let windowTypes;
+          if (AppInfo.isThunderbird) {
+            windowTypes = ["mail:3pane"];
+          } else {
+            // We assume that an app either has GeckoView windows, or
+            // Firefox/Fennec windows, but not both.
+            windowTypes = ["navigator:browser", "navigator:geckoview"];
+          }
+
+          let win;
+          for (const windowType of windowTypes) {
+            win = Services.wm.getMostRecentWindow(windowType);
+            if (win) {
+              break;
+            }
+          }
+
+          if (!win) {
+            // if the window isn't even created, just poll wait for it
+            let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(
+              Ci.nsITimer
+            );
+            checkTimer.initWithCallback(
+              waitForWindow,
+              100,
+              Ci.nsITimer.TYPE_ONE_SHOT
+            );
+          } else if (win.document.readyState != "complete") {
+            // otherwise, wait for it to be fully loaded before proceeding
+            let listener = ev => {
+              // ensure that we proceed, on the top level document load event
+              // (not an iframe one...)
+              if (ev.target != win.document) {
+                return;
+              }
+              win.removeEventListener("load", listener);
+              waitForWindow();
+            };
+            win.addEventListener("load", listener, true);
+          } else {
+            resolve(win);
+          }
+        };
+
+        waitForWindow();
+      },
+      {
+        errorMessage: "No applicable application windows found",
+      }
+    );
+  }
 }
 
 // Expose a shared singleton.
 const windowManager = new WindowManager();
--- a/remote/shared/webdriver/Session.jsm
+++ b/remote/shared/webdriver/Session.jsm
@@ -9,43 +9,173 @@ const EXPORTED_SYMBOLS = ["WebDriverSess
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   accessibility: "chrome://remote/content/marionette/accessibility.js",
   allowAllCerts: "chrome://remote/content/marionette/cert.js",
   Capabilities: "chrome://remote/content/shared/webdriver/Capabilities.jsm",
-  clearActionInputState:
-    "chrome://remote/content/marionette/actors/MarionetteCommandsChild.jsm",
   error: "chrome://remote/content/shared/webdriver/Errors.jsm",
   Log: "chrome://remote/content/shared/Log.jsm",
+  WebDriverBiDiConnection:
+    "chrome://remote/content/webdriver-bidi/WebDriverBiDiConnection.jsm",
+  WebSocketHandshake: "chrome://remote/content/server/WebSocketHandshake.jsm",
 });
 
 XPCOMUtils.defineLazyServiceGetter(
   this,
   "uuidGen",
   "@mozilla.org/uuid-generator;1",
   "nsIUUIDGenerator"
 );
 
 XPCOMUtils.defineLazyGetter(this, "logger", () => Log.get());
 
-/** Representation of WebDriver session. */
+/**
+ * Representation of WebDriver session.
+ */
 class WebDriverSession {
+  /**
+   * Construct a new WebDriver session.
+   *
+   * It is expected that the caller performs the necessary checks on
+   * the requested capabilities to be WebDriver conforming.  The WebDriver
+   * service offered by Marionette does not match or negotiate capabilities
+   * beyond type- and bounds checks.
+   *
+   * <h3>Capabilities</h3>
+   *
+   * <dl>
+   *  <dt><code>acceptInsecureCerts</code> (boolean)
+   *  <dd>Indicates whether untrusted and self-signed TLS certificates
+   *   are implicitly trusted on navigation for the duration of the session.
+   *
+   *  <dt><code>pageLoadStrategy</code> (string)
+   *  <dd>The page load strategy to use for the current session.  Must be
+   *   one of "<tt>none</tt>", "<tt>eager</tt>", and "<tt>normal</tt>".
+   *
+   *  <dt><code>proxy</code> (Proxy object)
+   *  <dd>Defines the proxy configuration.
+   *
+   *  <dt><code>setWindowRect</code> (boolean)
+   *  <dd>Indicates whether the remote end supports all of the resizing
+   *   and repositioning commands.
+   *
+   *  <dt><code>timeouts</code> (Timeouts object)
+   *  <dd>Describes the timeouts imposed on certian session operations.
+   *
+   *  <dt><code>strictFileInteractability</code> (boolean)
+   *  <dd>Defines the current session’s strict file interactability.
+   *
+   *  <dt><code>unhandledPromptBehavior</code> (string)
+   *  <dd>Describes the current session’s user prompt handler.  Must be one of
+   *   "<tt>accept</tt>", "<tt>accept and notify</tt>", "<tt>dismiss</tt>",
+   *   "<tt>dismiss and notify</tt>", and "<tt>ignore</tt>".  Defaults to the
+   *   "<tt>dismiss and notify</tt>" state.
+   *
+   *  <dt><code>moz:accessibilityChecks</code> (boolean)
+   *  <dd>Run a11y checks when clicking elements.
+   *
+   *  <dt><code>moz:debuggerAddress</code> (boolean)
+   *  <dd>Indicate that the Chrome DevTools Protocol (CDP) has to be enabled.
+   *
+   *  <dt><code>moz:useNonSpecCompliantPointerOrigin</code> (boolean)
+   *  <dd>Use the not WebDriver conforming calculation of the pointer origin
+   *   when the origin is an element, and the element center point is used.
+   *
+   *  <dt><code>moz:webdriverClick</code> (boolean)
+   *  <dd>Use a WebDriver conforming <i>WebDriver::ElementClick</i>.
+   * </dl>
+   *
+   * <h4>Timeouts object</h4>
+   *
+   * <dl>
+   *  <dt><code>script</code> (number)
+   *  <dd>Determines when to interrupt a script that is being evaluates.
+   *
+   *  <dt><code>pageLoad</code> (number)
+   *  <dd>Provides the timeout limit used to interrupt navigation of the
+   *   browsing context.
+   *
+   *  <dt><code>implicit</code> (number)
+   *  <dd>Gives the timeout of when to abort when locating an element.
+   * </dl>
+   *
+   * <h4>Proxy object</h4>
+   *
+   * <dl>
+   *  <dt><code>proxyType</code> (string)
+   *  <dd>Indicates the type of proxy configuration.  Must be one
+   *   of "<tt>pac</tt>", "<tt>direct</tt>", "<tt>autodetect</tt>",
+   *   "<tt>system</tt>", or "<tt>manual</tt>".
+   *
+   *  <dt><code>proxyAutoconfigUrl</code> (string)
+   *  <dd>Defines the URL for a proxy auto-config file if
+   *   <code>proxyType</code> is equal to "<tt>pac</tt>".
+   *
+   *  <dt><code>httpProxy</code> (string)
+   *  <dd>Defines the proxy host for HTTP traffic when the
+   *   <code>proxyType</code> is "<tt>manual</tt>".
+   *
+   *  <dt><code>noProxy</code> (string)
+   *  <dd>Lists the adress for which the proxy should be bypassed when
+   *   the <code>proxyType</code> is "<tt>manual</tt>".  Must be a JSON
+   *   List containing any number of any of domains, IPv4 addresses, or IPv6
+   *   addresses.
+   *
+   *  <dt><code>sslProxy</code> (string)
+   *  <dd>Defines the proxy host for encrypted TLS traffic when the
+   *   <code>proxyType</code> is "<tt>manual</tt>".
+   *
+   *  <dt><code>socksProxy</code> (string)
+   *  <dd>Defines the proxy host for a SOCKS proxy traffic when the
+   *   <code>proxyType</code> is "<tt>manual</tt>".
+   *
+   *  <dt><code>socksVersion</code> (string)
+   *  <dd>Defines the SOCKS proxy version when the <code>proxyType</code> is
+   *   "<tt>manual</tt>".  It must be any integer between 0 and 255
+   *   inclusive.
+   * </dl>
+   *
+   * <h3>Example</h3>
+   *
+   * Input:
+   *
+   * <pre><code>
+   *     {"capabilities": {"acceptInsecureCerts": true}}
+   * </code></pre>
+   *
+   * @param {Object.<string, *>=} capabilities
+   *     JSON Object containing any of the recognised capabilities listed
+   *     above.
+   *
+   * @throws {SessionNotCreatedError}
+   *     If, for whatever reason, a session could not be created.
+   */
   constructor(capabilities) {
+    // WebSocket connections that use this session. This also accounts for
+    // possible disconnects due to network outages, which require clients
+    // to reconnect.
+    this._connections = new Set();
+
+    this.id = uuidGen
+      .generateUUID()
+      .toString()
+      .slice(1, -1);
+
+    // Define the HTTP path to query this session via WebDriver BiDi
+    this.path = `/session/${this.id}`;
+
     try {
       this.capabilities = Capabilities.fromJSON(capabilities);
     } catch (e) {
       throw new error.SessionNotCreatedError(e);
     }
 
-    const uuid = uuidGen.generateUUID().toString();
-    this.id = uuid.substring(1, uuid.length - 1);
-
     if (this.capabilities.get("acceptInsecureCerts")) {
       logger.warn("TLS certificate errors will be ignored for this session");
       allowAllCerts.enable();
     }
 
     if (this.proxy.init()) {
       logger.info(`Proxy settings initialised: ${JSON.stringify(this.proxy)}`);
     }
@@ -56,17 +186,19 @@ class WebDriverSession {
     if (this.a11yChecks && accessibility.service) {
       logger.info("Preemptively starting accessibility service in Chrome");
     }
   }
 
   destroy() {
     allowAllCerts.disable();
 
-    clearActionInputState();
+    // Close all open connections
+    this._connections.forEach(connection => connection.close());
+    this._connections.clear();
   }
 
   get a11yChecks() {
     return this.capabilities.get("moz:accessibilityChecks");
   }
 
   get pageLoadStrategy() {
     return this.capabilities.get("pageLoadStrategy");
@@ -86,9 +218,36 @@ class WebDriverSession {
 
   set timeouts(timeouts) {
     this.capabilities.set("timeouts", timeouts);
   }
 
   get unhandledPromptBehavior() {
     return this.capabilities.get("unhandledPromptBehavior");
   }
+
+  // nsIHttpRequestHandler
+
+  /**
+   * Handle new WebSocket connection requests.
+   *
+   * WebSocket clients will attempt to connect to this session at
+   * `/session/:id`.  Hereby a WebSocket upgrade will automatically
+   * be performed.
+   *
+   * @param {Request} request
+   *     HTTP request (httpd.js)
+   * @param {Response} response
+   *     Response to an HTTP request (httpd.js)
+   */
+  async handle(request, response) {
+    const webSocket = await WebSocketHandshake.upgrade(request, response);
+    const conn = new WebDriverBiDiConnection(webSocket, response._connection);
+    conn.registerSession(this);
+    this._connections.add(conn);
+  }
+
+  // XPCOM
+
+  get QueryInterface() {
+    return ChromeUtils.generateQI(["nsIHttpRequestHandler"]);
+  }
 }
--- a/remote/webdriver-bidi/WebDriverBiDi.jsm
+++ b/remote/webdriver-bidi/WebDriverBiDi.jsm
@@ -7,39 +7,105 @@
 var EXPORTED_SYMBOLS = ["WebDriverBiDi"];
 
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   Services: "resource://gre/modules/Services.jsm",
+
+  error: "chrome://remote/content/shared/webdriver/Errors.jsm",
+  Log: "chrome://remote/content/shared/Log.jsm",
+  WebDriverSession: "chrome://remote/content/shared/webdriver/Session.jsm",
 });
 
+XPCOMUtils.defineLazyGetter(this, "logger", () =>
+  Log.get(Log.TYPES.WEBDRIVER_BIDI)
+);
+
 /**
  * Entry class for the WebDriver BiDi support.
  *
  * @see https://w3c.github.io/webdriver-bidi
  */
 class WebDriverBiDi {
   /**
    * Creates a new instance of the WebDriverBiDi class.
    *
    * @param {RemoteAgent} agent
    *     Reference to the Remote Agent instance.
    */
   constructor(agent) {
     this.agent = agent;
     this._running = false;
+
+    this._session = null;
   }
 
   get address() {
     return `ws://${this.agent.host}:${this.agent.port}`;
   }
 
+  get session() {
+    return this._session;
+  }
+
+  /**
+   * Create a new WebDriver session.
+   *
+   * @param {Object.<string, *>=} capabilities
+   *     JSON Object containing any of the recognised capabilities as listed
+   *     on the `WebDriverSession` class.
+   *
+   * @return {Object<String, Capabilities>}
+   *     Object containing the current session ID, and all its capabilities.
+   *
+   * @throws {SessionNotCreatedError}
+   *     If, for whatever reason, a session could not be created.
+   */
+  createSession(capabilities) {
+    if (this.session) {
+      throw new error.SessionNotCreatedError(
+        "Maximum number of active sessions"
+      );
+    }
+
+    this._session = new WebDriverSession(capabilities);
+
+    // Only register the path handler when the Remote Agent is active.
+    if (this.agent.listening) {
+      this.agent.server.registerPathHandler(this.session.path, this.session);
+      logger.debug(`Registered session handler: ${this.session.path}`);
+    }
+
+    return {
+      sessionId: this.session.id,
+      capabilities: this.session.capabilities,
+    };
+  }
+
+  /**
+   * Delete the current WebDriver session.
+   */
+  deleteSession() {
+    if (!this.session) {
+      return;
+    }
+
+    // Only unregister the path handler when the Remote Agent is active.
+    if (this.agent.listening) {
+      this.agent.server.registerPathHandler(this.session.path, null);
+      logger.debug(`Unregistered session handler: ${this.session.path}`);
+    }
+
+    this.session.destroy();
+    this._session = null;
+  }
+
   /**
    * Starts the WebDriver BiDi support.
    */
   start() {
     if (this._running) {
       return;
     }
 
@@ -55,11 +121,15 @@ class WebDriverBiDi {
   /**
    * Stops the WebDriver BiDi support.
    */
   stop() {
     if (!this._running) {
       return;
     }
 
-    this._running = false;
+    try {
+      this.deleteSession();
+    } finally {
+      this._running = false;
+    }
   }
 }
--- a/remote/webdriver-bidi/WebDriverBiDiConnection.jsm
+++ b/remote/webdriver-bidi/WebDriverBiDiConnection.jsm
@@ -20,23 +20,23 @@ XPCOMUtils.defineLazyModuleGetters(this,
 XPCOMUtils.defineLazyGetter(this, "logger", () =>
   Log.get(Log.TYPES.WEBDRIVER_BIDI)
 );
 
 class WebDriverBiDiConnection extends WebSocketConnection {
   /**
    * @param {WebSocket} webSocket
    *     The WebSocket server connection to wrap.
-   * @param {HttpServer} httpdConnection
+   * @param {Connection} httpdConnection
    *     Reference to the httpd.js's connection needed for clean-up.
    */
   constructor(webSocket, httpdConnection) {
     super(webSocket, httpdConnection);
 
-    // Each connection has only a single WebDriver session.
+    // Each connection has only a single associated WebDriver session.
     this.session = null;
   }
 
   /**
    * Register a new Session to forward the messages to.
    *
    * @param {Session} session
    *     The WebDriverSession to register.