Bug 1884090 - [remote] Add support for "HTTP flag" in WebDriver Session. r=webdriver-reviewers,jdescottes

Differential Revision: https://phabricator.services.mozilla.com/D211815
This commit is contained in:
Henrik Skupin 2024-05-29 08:40:49 +00:00
parent 9406e9ecd1
commit 9c60f55dee
8 changed files with 263 additions and 129 deletions

View file

@ -115,6 +115,9 @@ export function GeckoDriver(server) {
// WebDriver Session // WebDriver Session
this._currentSession = null; this._currentSession = null;
// Flag to indicate a WebDriver HTTP session
this._flags = new Set([lazy.WebDriverSession.SESSION_FLAG_HTTP]);
// Flag to indicate that the application is shutting down // Flag to indicate that the application is shutting down
this._isShuttingDown = false; this._isShuttingDown = false;
@ -401,14 +404,20 @@ GeckoDriver.prototype.newSession = async function (cmd) {
const { parameters: capabilities } = cmd; const { parameters: capabilities } = cmd;
try { try {
// If the WebDriver BiDi protocol is active always use the Remote Agent
// to handle the WebDriver session. If it's not the case then Marionette
// itself needs to handle it, and has to nullify the "webSocketUrl"
// capability.
if (lazy.RemoteAgent.webDriverBiDi) { if (lazy.RemoteAgent.webDriverBiDi) {
await lazy.RemoteAgent.webDriverBiDi.createSession(capabilities); // If the WebDriver BiDi protocol is active always use the Remote Agent
// to handle the WebDriver session.
await lazy.RemoteAgent.webDriverBiDi.createSession(
capabilities,
this._flags
);
} else { } else {
this._currentSession = new lazy.WebDriverSession(capabilities); // If it's not the case then Marionette itself needs to handle it, and
// has to nullify the "webSocketUrl" capability.
this._currentSession = new lazy.WebDriverSession(
capabilities,
this._flags
);
this._currentSession.capabilities.delete("webSocketUrl"); this._currentSession.capabilities.delete("webSocketUrl");
} }

View file

@ -12,7 +12,7 @@ const { error } = ChromeUtils.importESModule(
); );
add_task(async function test_execute_missing_command_error() { add_task(async function test_execute_missing_command_error() {
const session = new WebDriverSession(); const session = new WebDriverSession({}, new Set());
info("Attempt to execute an unknown protocol command"); info("Attempt to execute an unknown protocol command");
await Assert.rejects( await Assert.rejects(
@ -24,7 +24,7 @@ add_task(async function test_execute_missing_command_error() {
}); });
add_task(async function test_execute_missing_internal_command_error() { add_task(async function test_execute_missing_internal_command_error() {
const session = new WebDriverSession(); const session = new WebDriverSession({}, new Set());
info( info(
"Attempt to execute a protocol command which relies on an unknown internal method" "Attempt to execute a protocol command which relies on an unknown internal method"

View file

@ -2,6 +2,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file, * 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/. */ * You can obtain one at http://mozilla.org/MPL/2.0/. */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {}; const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, { ChromeUtils.defineESModuleGetters(lazy, {
@ -14,21 +16,45 @@ ChromeUtils.defineESModuleGetters(lazy, {
"chrome://remote/content/shared/webdriver/UserPromptHandler.sys.mjs", "chrome://remote/content/shared/webdriver/UserPromptHandler.sys.mjs",
}); });
ChromeUtils.defineLazyGetter(lazy, "debuggerAddress", () => {
return lazy.RemoteAgent.running && lazy.RemoteAgent.cdp
? lazy.remoteAgent.debuggerAddress
: null;
});
ChromeUtils.defineLazyGetter(lazy, "isHeadless", () => {
return Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless;
});
ChromeUtils.defineLazyGetter(lazy, "remoteAgent", () => { ChromeUtils.defineLazyGetter(lazy, "remoteAgent", () => {
return Cc["@mozilla.org/remote/agent;1"].createInstance(Ci.nsIRemoteAgent); return Cc["@mozilla.org/remote/agent;1"].createInstance(Ci.nsIRemoteAgent);
}); });
ChromeUtils.defineLazyGetter(lazy, "userAgent", () => {
return Cc["@mozilla.org/network/protocol;1?name=http"].getService(
Ci.nsIHttpProtocolHandler
).userAgent;
});
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"shutdownTimeout",
"toolkit.asyncshutdown.crash_timeout"
);
// List of capabilities which are only relevant for Webdriver Classic. // List of capabilities which are only relevant for Webdriver Classic.
export const WEBDRIVER_CLASSIC_CAPABILITIES = [ export const WEBDRIVER_CLASSIC_CAPABILITIES = [
"pageLoadStrategy", "pageLoadStrategy",
"timeouts",
"strictFileInteractability", "strictFileInteractability",
"timeouts",
"unhandledPromptBehavior", "unhandledPromptBehavior",
"webSocketUrl", "webSocketUrl",
"moz:useNonSpecCompliantPointerOrigin",
"moz:webdriverClick", // Gecko specific capabilities
"moz:debuggerAddress", "moz:debuggerAddress",
"moz:firefoxOptions", "moz:firefoxOptions",
"moz:useNonSpecCompliantPointerOrigin",
"moz:webdriverClick",
]; ];
/** Representation of WebDriver session timeouts. */ /** Representation of WebDriver session timeouts. */
@ -90,7 +116,7 @@ export class Timeouts {
default: default:
throw new lazy.error.InvalidArgumentError( throw new lazy.error.InvalidArgumentError(
"Unrecognised timeout: " + type `Unrecognized timeout: ${type}`
); );
} }
} }
@ -407,51 +433,36 @@ export class Proxy {
} }
} }
/** WebDriver session capabilities representation. */
export class Capabilities extends Map { export class Capabilities extends Map {
/** @class */ /**
* WebDriver session capabilities representation.
*/
constructor() { constructor() {
// Default values for capabilities supported by both WebDriver protocols
super([ super([
// webdriver
["browserName", getWebDriverBrowserName()], ["browserName", getWebDriverBrowserName()],
["browserVersion", lazy.AppInfo.version], ["browserVersion", lazy.AppInfo.version],
["platformName", getWebDriverPlatformName()], ["platformName", getWebDriverPlatformName()],
["acceptInsecureCerts", false], ["acceptInsecureCerts", false],
["pageLoadStrategy", PageLoadStrategy.Normal],
["proxy", new Proxy()], ["proxy", new Proxy()],
["setWindowRect", !lazy.AppInfo.isAndroid],
["timeouts", new Timeouts()],
["strictFileInteractability", false],
["unhandledPromptBehavior", new lazy.UserPromptHandler()], ["unhandledPromptBehavior", new lazy.UserPromptHandler()],
[ ["userAgent", lazy.userAgent],
"userAgent",
Cc["@mozilla.org/network/protocol;1?name=http"].getService(
Ci.nsIHttpProtocolHandler
).userAgent,
],
["webSocketUrl", null],
// proprietary // HTTP only capabilities
["pageLoadStrategy", PageLoadStrategy.Normal],
["timeouts", new Timeouts()],
["setWindowRect", !lazy.AppInfo.isAndroid],
["strictFileInteractability", false],
// Gecko specific capabilities
["moz:accessibilityChecks", false], ["moz:accessibilityChecks", false],
["moz:buildID", lazy.AppInfo.appBuildID], ["moz:buildID", lazy.AppInfo.appBuildID],
[ ["moz:debuggerAddress", lazy.debuggerAddress],
"moz:debuggerAddress", ["moz:headless", lazy.isHeadless],
// With bug 1715481 fixed always use the Remote Agent instance
lazy.RemoteAgent.running && lazy.RemoteAgent.cdp
? lazy.remoteAgent.debuggerAddress
: null,
],
[
"moz:headless",
Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless,
],
["moz:platformVersion", Services.sysinfo.getProperty("version")], ["moz:platformVersion", Services.sysinfo.getProperty("version")],
["moz:processID", lazy.AppInfo.processID], ["moz:processID", lazy.AppInfo.processID],
["moz:profile", maybeProfile()], ["moz:profile", maybeProfile()],
[ ["moz:shutdownTimeout", lazy.shutdownTimeout],
"moz:shutdownTimeout",
Services.prefs.getIntPref("toolkit.asyncshutdown.crash_timeout"),
],
["moz:webdriverClick", true], ["moz:webdriverClick", true],
["moz:windowless", false], ["moz:windowless", false],
]); ]);
@ -478,7 +489,7 @@ export class Capabilities extends Map {
} }
/** /**
* JSON serialisation of capabilities object. * JSON serialization of capabilities object.
* *
* @returns {Object<string, ?>} * @returns {Object<string, ?>}
*/ */
@ -501,11 +512,13 @@ export class Capabilities extends Map {
* *
* @param {Object<string, *>=} json * @param {Object<string, *>=} json
* WebDriver capabilities. * WebDriver capabilities.
* @param {boolean=} isHttp
* Flag indicating that it is a WebDriver classic session. Defaults to false.
* *
* @returns {Capabilities} * @returns {Capabilities}
* Internal representation of WebDriver capabilities. * Internal representation of WebDriver capabilities.
*/ */
static fromJSON(json) { static fromJSON(json, isHttp = false) {
if (typeof json == "undefined" || json === null) { if (typeof json == "undefined" || json === null) {
json = {}; json = {};
} }
@ -518,6 +531,11 @@ export class Capabilities extends Map {
// TODO: Bug 1823907. We can start using here spec compliant method `validate`, // TODO: Bug 1823907. We can start using here spec compliant method `validate`,
// as soon as `desiredCapabilities` and `requiredCapabilities` are not supported. // as soon as `desiredCapabilities` and `requiredCapabilities` are not supported.
for (let [k, v] of Object.entries(json)) { for (let [k, v] of Object.entries(json)) {
if (!isHttp && WEBDRIVER_CLASSIC_CAPABILITIES.includes(k)) {
// Ignore any WebDriver classic capability for a WebDriver BiDi session.
continue;
}
switch (k) { switch (k) {
case "acceptInsecureCerts": case "acceptInsecureCerts":
lazy.assert.boolean( lazy.assert.boolean(

View file

@ -32,10 +32,31 @@ ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
// Global singleton that holds active WebDriver sessions // Global singleton that holds active WebDriver sessions
const webDriverSessions = new Map(); const webDriverSessions = new Map();
/**
* @typedef {Set} SessionConfigurationFlags
* A set of flags defining the features of a WebDriver session. It can be
* empty or contain entries as listed below. External specifications may
* define additional flags, or create sessions without the HTTP flag.
*
* <dl>
* <dt><code>"http"</code> (string)
* <dd>Flag indicating a WebDriver classic (HTTP) session.
* </dl>
*/
/** /**
* Representation of WebDriver session. * Representation of WebDriver session.
*/ */
export class WebDriverSession { export class WebDriverSession {
#capabilities;
#connections;
#http;
#id;
#messageHandler;
#path;
static SESSION_FLAG_HTTP = "http";
/** /**
* Construct a new WebDriver session. * Construct a new WebDriver session.
* *
@ -52,23 +73,22 @@ export class WebDriverSession {
* are implicitly trusted on navigation for the duration of the session. * are implicitly trusted on navigation for the duration of the session.
* *
* <dt><code>pageLoadStrategy</code> (string) * <dt><code>pageLoadStrategy</code> (string)
* <dd>The page load strategy to use for the current session. Must be * <dd>(HTTP only) The page load strategy to use for the current session. Must be
* one of "<tt>none</tt>", "<tt>eager</tt>", and "<tt>normal</tt>". * one of "<tt>none</tt>", "<tt>eager</tt>", and "<tt>normal</tt>".
* *
* <dt><code>proxy</code> (Proxy object) * <dt><code>proxy</code> (Proxy object)
* <dd>Defines the proxy configuration. * <dd>Defines the proxy configuration.
* *
* <dt><code>setWindowRect</code> (boolean) * <dt><code>setWindowRect</code> (boolean)
* <dd>Indicates whether the remote end supports all of the resizing * <dd>(HTTP only) Indicates whether the remote end supports all of the resizing
* and repositioning commands. * and repositioning commands.
* *
* <dt><code>strictFileInteractability</code> (boolean) * <dt><code>strictFileInteractability</code> (boolean)
* <dd>Defines the current sessions strict file interactability. * <dd>(HTTP only) Defines the current sessions strict file interactability.
* *
* <dt><code>timeouts</code> (Timeouts object) * <dt><code>timeouts</code> (Timeouts object)
* <dd>Describes the timeouts imposed on certian session operations. * <dd>(HTTP only) Describes the timeouts imposed on certain session operations.
* *
* TODO: update for WebDriver BiDi type
* <dt><code>unhandledPromptBehavior</code> (string) * <dt><code>unhandledPromptBehavior</code> (string)
* <dd>Describes the current sessions user prompt handler. Must be one of * <dd>Describes the current sessions user prompt handler. Must be one of
* "<tt>accept</tt>", "<tt>accept and notify</tt>", "<tt>dismiss</tt>", * "<tt>accept</tt>", "<tt>accept and notify</tt>", "<tt>dismiss</tt>",
@ -76,13 +96,13 @@ export class WebDriverSession {
* "<tt>dismiss and notify</tt>" state. * "<tt>dismiss and notify</tt>" state.
* *
* <dt><code>moz:accessibilityChecks</code> (boolean) * <dt><code>moz:accessibilityChecks</code> (boolean)
* <dd>Run a11y checks when clicking elements. * <dd>(HTTP only) Run a11y checks when clicking elements.
* *
* <dt><code>moz:debuggerAddress</code> (boolean) * <dt><code>moz:debuggerAddress</code> (boolean)
* <dd>Indicate that the Chrome DevTools Protocol (CDP) has to be enabled. * <dd>Indicate that the Chrome DevTools Protocol (CDP) has to be enabled.
* *
* <dt><code>moz:webdriverClick</code> (boolean) * <dt><code>moz:webdriverClick</code> (boolean)
* <dd>Use a WebDriver conforming <i>WebDriver::ElementClick</i>. * <dd>(HTTP only) Use a WebDriver conforming <i>WebDriver::ElementClick</i>.
* </dl> * </dl>
* *
* <h4>WebAuthn</h4> * <h4>WebAuthn</h4>
@ -140,7 +160,7 @@ export class WebDriverSession {
* <code>proxyType</code> is "<tt>manual</tt>". * <code>proxyType</code> is "<tt>manual</tt>".
* *
* <dt><code>noProxy</code> (string) * <dt><code>noProxy</code> (string)
* <dd>Lists the adress for which the proxy should be bypassed when * <dd>Lists the address for which the proxy should be bypassed when
* the <code>proxyType</code> is "<tt>manual</tt>". Must be a JSON * the <code>proxyType</code> is "<tt>manual</tt>". Must be a JSON
* List containing any number of any of domains, IPv4 addresses, or IPv6 * List containing any number of any of domains, IPv4 addresses, or IPv6
* addresses. * addresses.
@ -168,9 +188,10 @@ export class WebDriverSession {
* </code></pre> * </code></pre>
* *
* @param {Object<string, *>=} capabilities * @param {Object<string, *>=} capabilities
* JSON Object containing any of the recognised capabilities listed * JSON Object containing any of the recognized capabilities listed
* above. * above.
* * @param {SessionConfigurationFlags} flags
* Session configuration flags.
* @param {WebDriverBiDiConnection=} connection * @param {WebDriverBiDiConnection=} connection
* An optional existing WebDriver BiDi connection to associate with the * An optional existing WebDriver BiDi connection to associate with the
* new session. * new session.
@ -178,19 +199,20 @@ export class WebDriverSession {
* @throws {SessionNotCreatedError} * @throws {SessionNotCreatedError}
* If, for whatever reason, a session could not be created. * If, for whatever reason, a session could not be created.
*/ */
constructor(capabilities, connection) { constructor(capabilities, flags, connection) {
// WebSocket connections that use this session. This also accounts for // WebSocket connections that use this session. This also accounts for
// possible disconnects due to network outages, which require clients // possible disconnects due to network outages, which require clients
// to reconnect. // to reconnect.
this._connections = new Set(); this.#connections = new Set();
this.id = lazy.generateUUID(); this.#id = lazy.generateUUID();
this.#http = flags.has(WebDriverSession.SESSION_FLAG_HTTP);
// Define the HTTP path to query this session via WebDriver BiDi // Define the HTTP path to query this session via WebDriver BiDi
this.path = `/session/${this.id}`; this.#path = `/session/${this.#id}`;
try { try {
this.capabilities = lazy.Capabilities.fromJSON(capabilities, this.path); this.#capabilities = lazy.Capabilities.fromJSON(capabilities, this.#http);
} catch (e) { } catch (e) {
throw new lazy.error.SessionNotCreatedError(e); throw new lazy.error.SessionNotCreatedError(e);
} }
@ -201,7 +223,7 @@ export class WebDriverSession {
); );
} }
if (this.capabilities.get("acceptInsecureCerts")) { if (this.acceptInsecureCerts) {
lazy.logger.warn( lazy.logger.warn(
"TLS certificate errors will be ignored for this session" "TLS certificate errors will be ignored for this session"
); );
@ -219,7 +241,7 @@ export class WebDriverSession {
// immediately register the newly created session for it. // immediately register the newly created session for it.
if (connection) { if (connection) {
connection.registerSession(this); connection.registerSession(this);
this._connections.add(connection); this.#connections.add(connection);
} }
// Maps a Navigable (browsing context or content browser for top-level // Maps a Navigable (browsing context or content browser for top-level
@ -228,11 +250,11 @@ export class WebDriverSession {
lazy.registerProcessDataActor(); lazy.registerProcessDataActor();
webDriverSessions.set(this.id, this); webDriverSessions.set(this.#id, this);
} }
destroy() { destroy() {
webDriverSessions.delete(this.id); webDriverSessions.delete(this.#id);
lazy.unregisterProcessDataActor(); lazy.unregisterProcessDataActor();
@ -241,20 +263,20 @@ export class WebDriverSession {
lazy.allowAllCerts.disable(); lazy.allowAllCerts.disable();
// Close all open connections which unregister themselves. // Close all open connections which unregister themselves.
this._connections.forEach(connection => connection.close()); this.#connections.forEach(connection => connection.close());
if (this._connections.size > 0) { if (this.#connections.size > 0) {
lazy.logger.warn( lazy.logger.warn(
`Failed to close ${this._connections.size} WebSocket connections` `Failed to close ${this.#connections.size} WebSocket connections`
); );
} }
// Destroy the dedicated MessageHandler instance if we created one. // Destroy the dedicated MessageHandler instance if we created one.
if (this._messageHandler) { if (this.#messageHandler) {
this._messageHandler.off( this.#messageHandler.off(
"message-handler-protocol-event", "message-handler-protocol-event",
this._onMessageHandlerProtocolEvent this._onMessageHandlerProtocolEvent
); );
this._messageHandler.destroy(); this.#messageHandler.destroy();
} }
} }
@ -282,46 +304,66 @@ export class WebDriverSession {
} }
get a11yChecks() { get a11yChecks() {
return this.capabilities.get("moz:accessibilityChecks"); return this.#capabilities.get("moz:accessibilityChecks");
}
get acceptInsecureCerts() {
return this.#capabilities.get("acceptInsecureCerts");
}
get capabilities() {
return this.#capabilities;
}
get http() {
return this.#http;
}
get id() {
return this.#id;
} }
get messageHandler() { get messageHandler() {
if (!this._messageHandler) { if (!this.#messageHandler) {
this._messageHandler = this.#messageHandler =
lazy.RootMessageHandlerRegistry.getOrCreateMessageHandler(this.id); lazy.RootMessageHandlerRegistry.getOrCreateMessageHandler(this.#id);
this._onMessageHandlerProtocolEvent = this._onMessageHandlerProtocolEvent =
this._onMessageHandlerProtocolEvent.bind(this); this._onMessageHandlerProtocolEvent.bind(this);
this._messageHandler.on( this.#messageHandler.on(
"message-handler-protocol-event", "message-handler-protocol-event",
this._onMessageHandlerProtocolEvent this._onMessageHandlerProtocolEvent
); );
} }
return this._messageHandler; return this.#messageHandler;
} }
get pageLoadStrategy() { get pageLoadStrategy() {
return this.capabilities.get("pageLoadStrategy"); return this.#capabilities.get("pageLoadStrategy");
}
get path() {
return this.#path;
} }
get proxy() { get proxy() {
return this.capabilities.get("proxy"); return this.#capabilities.get("proxy");
} }
get strictFileInteractability() { get strictFileInteractability() {
return this.capabilities.get("strictFileInteractability"); return this.#capabilities.get("strictFileInteractability");
} }
get timeouts() { get timeouts() {
return this.capabilities.get("timeouts"); return this.#capabilities.get("timeouts");
} }
set timeouts(timeouts) { set timeouts(timeouts) {
this.capabilities.set("timeouts", timeouts); this.#capabilities.set("timeouts", timeouts);
} }
get userPromptHandler() { get userPromptHandler() {
return this.capabilities.get("unhandledPromptBehavior"); return this.#capabilities.get("unhandledPromptBehavior");
} }
/** /**
@ -330,15 +372,15 @@ export class WebDriverSession {
* @param {WebDriverBiDiConnection} connection * @param {WebDriverBiDiConnection} connection
*/ */
removeConnection(connection) { removeConnection(connection) {
if (this._connections.has(connection)) { if (this.#connections.has(connection)) {
this._connections.delete(connection); this.#connections.delete(connection);
} else { } else {
lazy.logger.warn("Trying to remove a connection that doesn't exist."); lazy.logger.warn("Trying to remove a connection that doesn't exist.");
} }
} }
toString() { toString() {
return `[object ${this.constructor.name} ${this.id}]`; return `[object ${this.constructor.name} ${this.#id}]`;
} }
// nsIHttpRequestHandler // nsIHttpRequestHandler
@ -362,12 +404,12 @@ export class WebDriverSession {
response._connection response._connection
); );
conn.registerSession(this); conn.registerSession(this);
this._connections.add(conn); this.#connections.add(conn);
} }
_onMessageHandlerProtocolEvent(eventName, messageHandlerEvent) { _onMessageHandlerProtocolEvent(eventName, messageHandlerEvent) {
const { name, data } = messageHandlerEvent; const { name, data } = messageHandlerEvent;
this._connections.forEach(connection => connection.sendEvent(name, data)); this.#connections.forEach(connection => connection.sendEvent(name, data));
} }
// XPCOM // XPCOM

View file

@ -50,7 +50,7 @@ add_task(function test_Timeouts_fromJSON() {
equal(ts.script, json.script); equal(ts.script, json.script);
}); });
add_task(function test_Timeouts_fromJSON_unrecognised_field() { add_task(function test_Timeouts_fromJSON_unrecognized_field() {
let json = { let json = {
sessionId: "foobar", sessionId: "foobar",
}; };
@ -58,7 +58,7 @@ add_task(function test_Timeouts_fromJSON_unrecognised_field() {
Timeouts.fromJSON(json); Timeouts.fromJSON(json);
} catch (e) { } catch (e) {
equal(e.name, error.InvalidArgumentError.name); equal(e.name, error.InvalidArgumentError.name);
equal(e.message, "Unrecognised timeout: sessionId"); equal(e.message, "Unrecognized timeout: sessionId");
} }
}); });
@ -439,27 +439,53 @@ add_task(function test_Capabilities_fromJSON() {
caps = fromJSON({ acceptInsecureCerts: false }); caps = fromJSON({ acceptInsecureCerts: false });
equal(false, caps.get("acceptInsecureCerts")); equal(false, caps.get("acceptInsecureCerts"));
for (let strategy of Object.values(PageLoadStrategy)) {
caps = fromJSON({ pageLoadStrategy: strategy });
equal(strategy, caps.get("pageLoadStrategy"));
}
let proxyConfig = { proxyType: "manual" }; let proxyConfig = { proxyType: "manual" };
caps = fromJSON({ proxy: proxyConfig }); caps = fromJSON({ proxy: proxyConfig });
equal("manual", caps.get("proxy").proxyType); equal("manual", caps.get("proxy").proxyType);
// HTTP only capabilities
for (let strategy of Object.values(PageLoadStrategy)) {
caps = fromJSON({ pageLoadStrategy: strategy }, true);
equal(strategy, caps.get("pageLoadStrategy"));
caps = fromJSON({ pageLoadStrategy: strategy });
equal("normal", caps.get("pageLoadStrategy"));
}
let timeoutsConfig = { implicit: 123 }; let timeoutsConfig = { implicit: 123 };
caps = fromJSON({ timeouts: timeoutsConfig }); caps = fromJSON({ timeouts: timeoutsConfig });
equal(0, caps.get("timeouts").implicit);
caps = fromJSON({ timeouts: timeoutsConfig }, true);
equal(123, caps.get("timeouts").implicit); equal(123, caps.get("timeouts").implicit);
caps = fromJSON({ strictFileInteractability: false }); caps = fromJSON({ strictFileInteractability: false }, true);
equal(false, caps.get("strictFileInteractability")); equal(false, caps.get("strictFileInteractability"));
caps = fromJSON({ strictFileInteractability: true }); caps = fromJSON({ strictFileInteractability: true }, true);
equal(true, caps.get("strictFileInteractability")); equal(true, caps.get("strictFileInteractability"));
caps = fromJSON({ webSocketUrl: true }); caps = fromJSON({ webSocketUrl: true }, true);
equal(true, caps.get("webSocketUrl")); equal(true, caps.get("webSocketUrl"));
// Mozilla specific capabilities
caps = fromJSON({ "moz:accessibilityChecks": true });
equal(true, caps.get("moz:accessibilityChecks"));
caps = fromJSON({ "moz:accessibilityChecks": false });
equal(false, caps.get("moz:accessibilityChecks"));
caps = fromJSON({ "moz:webdriverClick": true }, true);
equal(true, caps.get("moz:webdriverClick"));
caps = fromJSON({ "moz:webdriverClick": false }, true);
equal(false, caps.get("moz:webdriverClick"));
// capability is always populated with null if remote agent is not listening
caps = fromJSON({});
equal(null, caps.get("moz:debuggerAddress"));
caps = fromJSON({ "moz:debuggerAddress": "foo" });
equal(null, caps.get("moz:debuggerAddress"));
caps = fromJSON({ "moz:debuggerAddress": true });
equal(null, caps.get("moz:debuggerAddress"));
// Extension capabilities
caps = fromJSON({ "webauthn:virtualAuthenticators": true }); caps = fromJSON({ "webauthn:virtualAuthenticators": true });
equal(true, caps.get("webauthn:virtualAuthenticators")); equal(true, caps.get("webauthn:virtualAuthenticators"));
caps = fromJSON({ "webauthn:virtualAuthenticators": false }); caps = fromJSON({ "webauthn:virtualAuthenticators": false });
@ -505,31 +531,13 @@ add_task(function test_Capabilities_fromJSON() {
/InvalidArgumentError/ /InvalidArgumentError/
); );
caps = fromJSON({ "moz:accessibilityChecks": true });
equal(true, caps.get("moz:accessibilityChecks"));
caps = fromJSON({ "moz:accessibilityChecks": false });
equal(false, caps.get("moz:accessibilityChecks"));
// capability is always populated with null if remote agent is not listening
caps = fromJSON({});
equal(null, caps.get("moz:debuggerAddress"));
caps = fromJSON({ "moz:debuggerAddress": "foo" });
equal(null, caps.get("moz:debuggerAddress"));
caps = fromJSON({ "moz:debuggerAddress": true });
equal(null, caps.get("moz:debuggerAddress"));
caps = fromJSON({ "moz:webdriverClick": true });
equal(true, caps.get("moz:webdriverClick"));
caps = fromJSON({ "moz:webdriverClick": false });
equal(false, caps.get("moz:webdriverClick"));
// No longer supported capabilities // No longer supported capabilities
Assert.throws( Assert.throws(
() => fromJSON({ "moz:useNonSpecCompliantPointerOrigin": false }), () => fromJSON({ "moz:useNonSpecCompliantPointerOrigin": false }, true),
/InvalidArgumentError/ /InvalidArgumentError/
); );
Assert.throws( Assert.throws(
() => fromJSON({ "moz:useNonSpecCompliantPointerOrigin": true }), () => fromJSON({ "moz:useNonSpecCompliantPointerOrigin": true }, true),
/InvalidArgumentError/ /InvalidArgumentError/
); );
}); });

View file

@ -4,7 +4,7 @@
"use strict"; "use strict";
const { Capabilities, Timeouts } = ChromeUtils.importESModule( const { Timeouts } = ChromeUtils.importESModule(
"chrome://remote/content/shared/webdriver/Capabilities.sys.mjs" "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs"
); );
const { getWebDriverSessionById, WebDriverSession } = const { getWebDriverSessionById, WebDriverSession } =
@ -12,26 +12,79 @@ const { getWebDriverSessionById, WebDriverSession } =
"chrome://remote/content/shared/webdriver/Session.sys.mjs" "chrome://remote/content/shared/webdriver/Session.sys.mjs"
); );
add_task(function test_WebDriverSession_ctor() { function createSession(options = {}) {
const session = new WebDriverSession(); const { capabilities = {}, connection, isHttp = false } = options;
const flags = new Set();
if (isHttp) {
flags.add("http");
}
return new WebDriverSession(capabilities, flags, connection);
}
add_task(function test_WebDriverSession_ctor() {
Assert.throws(() => new WebDriverSession({}), /TypeError/);
// Session id and path
let session = createSession();
equal(typeof session.id, "string"); equal(typeof session.id, "string");
ok(session.capabilities instanceof Capabilities); equal(session.path, `/session/${session.id}`);
// Sets HTTP flag
session = createSession({ isHttp: true });
equal(session.http, true);
session = createSession({ isHttp: false });
equal(session.http, false);
// Sets capabilities based on session configuration flag.
const capabilities = {
acceptInsecureCerts: true,
unhandledPromptBehavior: "ignore",
// HTTP only
pageLoadStrategy: "eager",
strictFileInteractability: true,
timeouts: { script: 1000 },
};
// HTTP session
session = createSession({ isHttp: true, capabilities });
equal(session.acceptInsecureCerts, true);
equal(session.pageLoadStrategy, "eager");
equal(session.strictFileInteractability, true);
equal(session.timeouts.script, 1000);
equal(session.userPromptHandler.toJSON(), "ignore");
// BiDi session (uses default values for HTTP only capabilities)
session = createSession({ isHttp: false, capabilities });
equal(session.acceptInsecureCerts, true);
equal(session.pageLoadStrategy, "normal");
equal(session.strictFileInteractability, false);
equal(session.timeouts.script, 30000);
equal(session.userPromptHandler.toJSON(), "dismiss and notify");
}); });
add_task(function test_WebDriverSession_destroy() { add_task(function test_WebDriverSession_destroy() {
const session = new WebDriverSession(); const session = createSession();
session.destroy(); session.destroy();
// Calling twice doesn't raise error.
session.destroy();
}); });
add_task(function test_WebDriverSession_getters() { add_task(function test_WebDriverSession_getters() {
const session = new WebDriverSession(); const session = createSession();
equal( equal(
session.a11yChecks, session.a11yChecks,
session.capabilities.get("moz:accessibilityChecks") session.capabilities.get("moz:accessibilityChecks")
); );
equal(
session.acceptInsecureCerts,
session.capabilities.get("acceptInsecureCerts")
);
equal(session.pageLoadStrategy, session.capabilities.get("pageLoadStrategy")); equal(session.pageLoadStrategy, session.capabilities.get("pageLoadStrategy"));
equal(session.proxy, session.capabilities.get("proxy")); equal(session.proxy, session.capabilities.get("proxy"));
equal( equal(
@ -46,7 +99,7 @@ add_task(function test_WebDriverSession_getters() {
}); });
add_task(function test_WebDriverSession_setters() { add_task(function test_WebDriverSession_setters() {
const session = new WebDriverSession(); const session = createSession();
const timeouts = new Timeouts(); const timeouts = new Timeouts();
timeouts.pageLoad = 45; timeouts.pageLoad = 45;
@ -56,8 +109,8 @@ add_task(function test_WebDriverSession_setters() {
}); });
add_task(function test_getWebDriverSessionById() { add_task(function test_getWebDriverSessionById() {
const session1 = new WebDriverSession(); const session1 = createSession();
const session2 = new WebDriverSession(); const session2 = createSession();
equal(getWebDriverSessionById(session1.id), session1); equal(getWebDriverSessionById(session1.id), session1);
equal(getWebDriverSessionById(session2.id), session2); equal(getWebDriverSessionById(session2.id), session2);

View file

@ -69,7 +69,8 @@ export class WebDriverBiDi {
* @param {Object<string, *>=} capabilities * @param {Object<string, *>=} capabilities
* JSON Object containing any of the recognised capabilities as listed * JSON Object containing any of the recognised capabilities as listed
* on the `WebDriverSession` class. * on the `WebDriverSession` class.
* * @param {Set} flags
* Session configuration flags.
* @param {WebDriverBiDiConnection=} sessionlessConnection * @param {WebDriverBiDiConnection=} sessionlessConnection
* Optional connection that is not yet accociated with a WebDriver * Optional connection that is not yet accociated with a WebDriver
* session, and has to be associated with the new WebDriver session. * session, and has to be associated with the new WebDriver session.
@ -80,7 +81,7 @@ export class WebDriverBiDi {
* @throws {SessionNotCreatedError} * @throws {SessionNotCreatedError}
* If, for whatever reason, a session could not be created. * If, for whatever reason, a session could not be created.
*/ */
async createSession(capabilities, sessionlessConnection) { async createSession(capabilities, flags, sessionlessConnection) {
if (this.session) { if (this.session) {
throw new lazy.error.SessionNotCreatedError( throw new lazy.error.SessionNotCreatedError(
"Maximum number of active sessions" "Maximum number of active sessions"
@ -89,6 +90,7 @@ export class WebDriverBiDi {
const session = new lazy.WebDriverSession( const session = new lazy.WebDriverSession(
capabilities, capabilities,
flags,
sessionlessConnection sessionlessConnection
); );

View file

@ -180,13 +180,15 @@ export class WebDriverBiDiConnection extends WebSocketConnection {
if (module === "session" && command === "new") { if (module === "session" && command === "new") {
const processedCapabilities = lazy.processCapabilities(params); const processedCapabilities = lazy.processCapabilities(params);
const flags = new Set();
result = await lazy.RemoteAgent.webDriverBiDi.createSession( result = await lazy.RemoteAgent.webDriverBiDi.createSession(
processedCapabilities, processedCapabilities,
flags,
this this
); );
// Since in Capabilities class we setup default values also for capabilities which are // The Capabilities class sets up default values also for capabilities
// not relevant for bidi, we want to remove them from the payload before returning to a client. // which are not relevant for BiDi, we want to remove those from the payload.
result.capabilities = Array.from(result.capabilities.entries()).reduce( result.capabilities = Array.from(result.capabilities.entries()).reduce(
(object, [key, value]) => { (object, [key, value]) => {
if (!lazy.WEBDRIVER_CLASSIC_CAPABILITIES.includes(key)) { if (!lazy.WEBDRIVER_CLASSIC_CAPABILITIES.includes(key)) {