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

Differential Revision: https://phabricator.services.mozilla.com/D212307
This commit is contained in:
Henrik Skupin 2024-06-06 07:37:26 +00:00
parent 0ac177013f
commit 3687348b50
12 changed files with 436 additions and 161 deletions

View file

@ -116,7 +116,7 @@ export function GeckoDriver(server) {
this._currentSession = null;
// Flag to indicate a WebDriver HTTP session
this._flags = new Set([lazy.WebDriverSession.SESSION_FLAG_HTTP]);
this._sessionConfigFlags = new Set([lazy.WebDriverSession.SESSION_FLAG_HTTP]);
// Flag to indicate that the application is shutting down
this._isShuttingDown = false;
@ -418,14 +418,14 @@ GeckoDriver.prototype.newSession = async function (cmd) {
// to handle the WebDriver session.
await lazy.RemoteAgent.webDriverBiDi.createSession(
capabilities,
this._flags
this._sessionConfigFlags
);
} else {
// 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._sessionConfigFlags
);
this._currentSession.capabilities.delete("webSocketUrl");
}

View file

@ -12,7 +12,7 @@ const { error } = ChromeUtils.importESModule(
);
add_task(async function test_execute_missing_command_error() {
const session = new WebDriverSession({}, new Set());
const session = new WebDriverSession({}, new Set(["bidi"]));
info("Attempt to execute an unknown protocol command");
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() {
const session = new WebDriverSession({}, new Set());
const session = new WebDriverSession({}, new Set(["bidi"]));
info(
"Attempt to execute a protocol command which relies on an unknown internal method"

View file

@ -51,10 +51,18 @@ export const WEBDRIVER_CLASSIC_CAPABILITIES = [
"webSocketUrl",
// Gecko specific capabilities
"moz:accessibilityChecks",
"moz:debuggerAddress",
"moz:firefoxOptions",
"moz:useNonSpecCompliantPointerOrigin",
"moz:webdriverClick",
// Extension capabilities
"webauthn:extension:credBlob",
"webauthn:extension:largeBlob",
"webauthn:extension:prf",
"webauthn:extension:uvm",
"webauthn:virtualAuthenticators",
];
/** Representation of WebDriver session timeouts. */
@ -436,36 +444,46 @@ export class Proxy {
export class Capabilities extends Map {
/**
* WebDriver session capabilities representation.
*
* @param {boolean} isBidi
* Flag indicating that it is a WebDriver BiDi session. Defaults to false.
*/
constructor() {
constructor(isBidi = false) {
// Default values for capabilities supported by both WebDriver protocols
super([
const defaults = [
["acceptInsecureCerts", false],
["browserName", getWebDriverBrowserName()],
["browserVersion", lazy.AppInfo.version],
["platformName", getWebDriverPlatformName()],
["acceptInsecureCerts", false],
["proxy", new Proxy()],
["unhandledPromptBehavior", new lazy.UserPromptHandler()],
["userAgent", lazy.userAgent],
// HTTP only capabilities
["pageLoadStrategy", PageLoadStrategy.Normal],
["timeouts", new Timeouts()],
["setWindowRect", !lazy.AppInfo.isAndroid],
["strictFileInteractability", false],
// Gecko specific capabilities
["moz:accessibilityChecks", false],
["moz:buildID", lazy.AppInfo.appBuildID],
["moz:debuggerAddress", lazy.debuggerAddress],
["moz:headless", lazy.isHeadless],
["moz:platformVersion", Services.sysinfo.getProperty("version")],
["moz:processID", lazy.AppInfo.processID],
["moz:profile", maybeProfile()],
["moz:shutdownTimeout", lazy.shutdownTimeout],
["moz:webdriverClick", true],
["moz:windowless", false],
]);
];
if (!isBidi) {
// HTTP-only capabilities
defaults.push(
["pageLoadStrategy", PageLoadStrategy.Normal],
["timeouts", new Timeouts()],
["setWindowRect", !lazy.AppInfo.isAndroid],
["strictFileInteractability", false],
["moz:accessibilityChecks", false],
["moz:debuggerAddress", lazy.debuggerAddress],
["moz:webdriverClick", true],
["moz:windowless", false]
);
}
super(defaults);
}
/**
@ -512,13 +530,13 @@ export class Capabilities extends Map {
*
* @param {Object<string, *>=} json
* WebDriver capabilities.
* @param {boolean=} isHttp
* Flag indicating that it is a WebDriver classic session. Defaults to false.
* @param {boolean=} isBidi
* Flag indicating that it is a WebDriver BiDi session. Defaults to false.
*
* @returns {Capabilities}
* Internal representation of WebDriver capabilities.
*/
static fromJSON(json, isHttp = false) {
static fromJSON(json, isBidi = false) {
if (typeof json == "undefined" || json === null) {
json = {};
}
@ -527,11 +545,12 @@ export class Capabilities extends Map {
lazy.pprint`Expected "capabilities" to be an object, got ${json}"`
);
const capabilities = new Capabilities();
const capabilities = new Capabilities(isBidi);
// TODO: Bug 1823907. We can start using here spec compliant method `validate`,
// as soon as `desiredCapabilities` and `requiredCapabilities` are not supported.
for (let [k, v] of Object.entries(json)) {
if (!isHttp && WEBDRIVER_CLASSIC_CAPABILITIES.includes(k)) {
if (isBidi && WEBDRIVER_CLASSIC_CAPABILITIES.includes(k)) {
// Ignore any WebDriver classic capability for a WebDriver BiDi session.
continue;
}

View file

@ -39,6 +39,8 @@ const webDriverSessions = new Map();
* define additional flags, or create sessions without the HTTP flag.
*
* <dl>
* <dt><code>"bidi"</code> (string)
* <dd>Flag indicating a WebDriver BiDi session.
* <dt><code>"http"</code> (string)
* <dd>Flag indicating a WebDriver classic (HTTP) session.
* </dl>
@ -48,6 +50,7 @@ const webDriverSessions = new Map();
* Representation of WebDriver session.
*/
export class WebDriverSession {
#bidi;
#capabilities;
#connections;
#http;
@ -55,6 +58,7 @@ export class WebDriverSession {
#messageHandler;
#path;
static SESSION_FLAG_BIDI = "bidi";
static SESSION_FLAG_HTTP = "http";
/**
@ -206,13 +210,26 @@ export class WebDriverSession {
this.#connections = new Set();
this.#id = lazy.generateUUID();
// Flags for WebDriver session features
this.#bidi = flags.has(WebDriverSession.SESSION_FLAG_BIDI);
this.#http = flags.has(WebDriverSession.SESSION_FLAG_HTTP);
if (this.#bidi == this.#http) {
// Initially a WebDriver session can either be HTTP or BiDi. An upgrade of a
// HTTP session to offer BiDi features is done after the constructor is run.
throw new lazy.error.SessionNotCreatedError(
`Initially the WebDriver session needs to be either HTTP or BiDi (bidi=${
this.#bidi
}, http=${this.#http})`
);
}
// Define the HTTP path to query this session via WebDriver BiDi
this.#path = `/session/${this.#id}`;
try {
this.#capabilities = lazy.Capabilities.fromJSON(capabilities, this.#http);
this.#capabilities = lazy.Capabilities.fromJSON(capabilities, this.#bidi);
} catch (e) {
throw new lazy.error.SessionNotCreatedError(e);
}
@ -280,29 +297,6 @@ export class WebDriverSession {
}
}
async execute(module, command, params) {
// XXX: At the moment, commands do not describe consistently their destination,
// so we will need a translation step based on a specific command and its params
// in order to extract a destination that can be understood by the MessageHandler.
//
// For now, an option is to send all commands to ROOT, and all BiDi MessageHandler
// modules will therefore need to implement this translation step in the root
// implementation of their module.
const destination = {
type: lazy.RootMessageHandler.type,
};
if (!this.messageHandler.supportsCommand(module, command, destination)) {
throw new lazy.error.UnknownCommandError(`${module}.${command}`);
}
return this.messageHandler.handleCommand({
moduleName: module,
commandName: command,
params,
destination,
});
}
get a11yChecks() {
return this.#capabilities.get("moz:accessibilityChecks");
}
@ -311,6 +305,14 @@ export class WebDriverSession {
return this.#capabilities.get("acceptInsecureCerts");
}
get bidi() {
return this.#bidi;
}
set bidi(value) {
this.#bidi = value;
}
get capabilities() {
return this.#capabilities;
}
@ -366,6 +368,33 @@ export class WebDriverSession {
return this.#capabilities.get("unhandledPromptBehavior");
}
get webSocketUrl() {
return this.#capabilities.get("webSocketUrl");
}
async execute(module, command, params) {
// XXX: At the moment, commands do not describe consistently their destination,
// so we will need a translation step based on a specific command and its params
// in order to extract a destination that can be understood by the MessageHandler.
//
// For now, an option is to send all commands to ROOT, and all BiDi MessageHandler
// modules will therefore need to implement this translation step in the root
// implementation of their module.
const destination = {
type: lazy.RootMessageHandler.type,
};
if (!this.messageHandler.supportsCommand(module, command, destination)) {
throw new lazy.error.UnknownCommandError(`${module}.${command}`);
}
return this.messageHandler.handleCommand({
moduleName: module,
commandName: command,
params,
destination,
});
}
/**
* Remove the specified WebDriver BiDi connection.
*

View file

@ -369,8 +369,15 @@ add_task(function test_Proxy_fromJSON() {
deepEqual(p, Proxy.fromJSON(manual));
});
add_task(function test_Capabilities_ctor() {
let caps = new Capabilities();
add_task(function test_Capabilities_ctor_http_default() {
const caps = new Capabilities();
equal(true, caps.get("moz:webdriverClick"));
});
add_task(function test_Capabilities_ctor_http() {
const caps = new Capabilities(false);
ok(caps.has("browserName"));
ok(caps.has("browserVersion"));
ok(caps.has("platformName"));
@ -390,6 +397,30 @@ add_task(function test_Capabilities_ctor() {
ok(caps.has("moz:processID"));
ok(caps.has("moz:profile"));
equal(true, caps.get("moz:webdriverClick"));
});
add_task(function test_Capabilities_ctor_bidi() {
const caps = new Capabilities(true);
ok(caps.has("browserName"));
ok(caps.has("browserVersion"));
ok(caps.has("platformName"));
ok(["linux", "mac", "windows", "android"].includes(caps.get("platformName")));
equal(undefined, caps.get("pageLoadStrategy"));
equal(false, caps.get("acceptInsecureCerts"));
ok(!caps.has("timeouts"));
ok(caps.get("proxy") instanceof Proxy);
ok(!caps.has("setWindowRect"));
ok(!caps.has("strictFileInteractability"));
ok(!caps.has("webSocketUrl"));
ok(!caps.has("moz:accessibilityChecks"));
ok(caps.has("moz:buildID"));
ok(!caps.has("moz:debuggerAddress"));
ok(caps.has("moz:platformVersion"));
ok(caps.has("moz:processID"));
ok(caps.has("moz:profile"));
ok(!caps.has("moz:webdriverClick"));
// No longer supported capabilities
ok(!caps.has("moz:useNonSpecCompliantPointerOrigin"));
@ -423,125 +454,164 @@ add_task(function test_Capabilities_toJSON() {
equal(caps.get("moz:webdriverClick"), json["moz:webdriverClick"]);
});
add_task(function test_Capabilities_fromJSON() {
add_task(function test_Capabilities_fromJSON_http() {
const { fromJSON } = Capabilities;
// plain
for (let typ of [{}, null, undefined]) {
ok(fromJSON(typ).has("browserName"));
for (const type of [{}, null, undefined]) {
ok(fromJSON(type, false).has("browserName"));
}
// matching
let caps = new Capabilities();
let caps;
caps = fromJSON({ acceptInsecureCerts: true });
// Capabilities supported by HTTP and BiDi
caps = fromJSON({ acceptInsecureCerts: true }, false);
equal(true, caps.get("acceptInsecureCerts"));
caps = fromJSON({ acceptInsecureCerts: false });
equal(false, caps.get("acceptInsecureCerts"));
let proxyConfig = { proxyType: "manual" };
caps = fromJSON({ proxy: proxyConfig });
caps = fromJSON({ proxy: proxyConfig }, false);
equal("manual", caps.get("proxy").proxyType);
// HTTP only capabilities
// WebDriver HTTP-only capabilities
for (let strategy of Object.values(PageLoadStrategy)) {
caps = fromJSON({ pageLoadStrategy: strategy }, true);
caps = fromJSON({ pageLoadStrategy: strategy }, false);
equal(strategy, caps.get("pageLoadStrategy"));
caps = fromJSON({ pageLoadStrategy: strategy });
equal("normal", caps.get("pageLoadStrategy"));
}
let timeoutsConfig = { implicit: 123 };
caps = fromJSON({ timeouts: timeoutsConfig });
equal(0, caps.get("timeouts").implicit);
caps = fromJSON({ timeouts: timeoutsConfig }, true);
caps = fromJSON({ timeouts: timeoutsConfig }, false);
equal(123, caps.get("timeouts").implicit);
caps = fromJSON({ strictFileInteractability: false }, true);
equal(false, caps.get("strictFileInteractability"));
caps = fromJSON({ strictFileInteractability: true }, true);
caps = fromJSON({ strictFileInteractability: true }, false);
equal(true, caps.get("strictFileInteractability"));
caps = fromJSON({ webSocketUrl: true }, true);
caps = fromJSON({ webSocketUrl: true }, false);
equal(true, caps.get("webSocketUrl"));
// Mozilla specific capabilities
caps = fromJSON({ "moz:accessibilityChecks": true });
caps = fromJSON({ "moz:accessibilityChecks": true }, false);
equal(true, caps.get("moz:accessibilityChecks"));
caps = fromJSON({ "moz:accessibilityChecks": false });
equal(false, caps.get("moz:accessibilityChecks"));
caps = fromJSON({ "moz:webdriverClick": true }, true);
caps = fromJSON({ "moz:webdriverClick": true }, false);
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({});
caps = fromJSON({}, false);
equal(null, caps.get("moz:debuggerAddress"));
caps = fromJSON({ "moz:debuggerAddress": "foo" });
caps = fromJSON({ "moz:debuggerAddress": "foo" }, false);
equal(null, caps.get("moz:debuggerAddress"));
caps = fromJSON({ "moz:debuggerAddress": true });
caps = fromJSON({ "moz:debuggerAddress": true }, false);
equal(null, caps.get("moz:debuggerAddress"));
// Extension capabilities
caps = fromJSON({ "webauthn:virtualAuthenticators": true });
caps = fromJSON({ "webauthn:virtualAuthenticators": true }, false);
equal(true, caps.get("webauthn:virtualAuthenticators"));
caps = fromJSON({ "webauthn:virtualAuthenticators": false });
equal(false, caps.get("webauthn:virtualAuthenticators"));
Assert.throws(
() => fromJSON({ "webauthn:virtualAuthenticators": "foo" }),
() => fromJSON({ "webauthn:virtualAuthenticators": "foo" }, false),
/InvalidArgumentError/
);
caps = fromJSON({ "webauthn:extension:uvm": true });
caps = fromJSON({ "webauthn:extension:uvm": true }, false);
equal(true, caps.get("webauthn:extension:uvm"));
caps = fromJSON({ "webauthn:extension:uvm": false });
equal(false, caps.get("webauthn:extension:uvm"));
Assert.throws(
() => fromJSON({ "webauthn:extension:uvm": "foo" }),
() => fromJSON({ "webauthn:extension:uvm": "foo" }, false),
/InvalidArgumentError/
);
caps = fromJSON({ "webauthn:extension:prf": true });
caps = fromJSON({ "webauthn:extension:prf": true }, false);
equal(true, caps.get("webauthn:extension:prf"));
caps = fromJSON({ "webauthn:extension:prf": false });
equal(false, caps.get("webauthn:extension:prf"));
Assert.throws(
() => fromJSON({ "webauthn:extension:prf": "foo" }),
() => fromJSON({ "webauthn:extension:prf": "foo" }, false),
/InvalidArgumentError/
);
caps = fromJSON({ "webauthn:extension:largeBlob": true });
caps = fromJSON({ "webauthn:extension:largeBlob": true }, false);
equal(true, caps.get("webauthn:extension:largeBlob"));
caps = fromJSON({ "webauthn:extension:largeBlob": false });
equal(false, caps.get("webauthn:extension:largeBlob"));
Assert.throws(
() => fromJSON({ "webauthn:extension:largeBlob": "foo" }),
() => fromJSON({ "webauthn:extension:largeBlob": "foo" }, false),
/InvalidArgumentError/
);
caps = fromJSON({ "webauthn:extension:credBlob": true });
caps = fromJSON({ "webauthn:extension:credBlob": true }, false);
equal(true, caps.get("webauthn:extension:credBlob"));
caps = fromJSON({ "webauthn:extension:credBlob": false });
equal(false, caps.get("webauthn:extension:credBlob"));
Assert.throws(
() => fromJSON({ "webauthn:extension:credBlob": "foo" }),
() => fromJSON({ "webauthn:extension:credBlob": "foo" }, false),
/InvalidArgumentError/
);
// No longer supported capabilities
Assert.throws(
() => fromJSON({ "moz:useNonSpecCompliantPointerOrigin": false }, true),
/InvalidArgumentError/
);
Assert.throws(
() => fromJSON({ "moz:useNonSpecCompliantPointerOrigin": true }, true),
() => fromJSON({ "moz:useNonSpecCompliantPointerOrigin": true }, false),
/InvalidArgumentError/
);
});
add_task(function test_Capabilities_fromJSON_bidi() {
const { fromJSON } = Capabilities;
// plain
for (const type of [{}, null, undefined]) {
ok(fromJSON(type, true).has("browserName"));
}
let caps;
// Capabilities supported by HTTP and BiDi
caps = fromJSON({ acceptInsecureCerts: true }, true);
equal(true, caps.get("acceptInsecureCerts"));
let proxyConfig = { proxyType: "manual" };
caps = fromJSON({ proxy: proxyConfig }, true);
equal("manual", caps.get("proxy").proxyType);
// HTTP capabilities are ignored for BiDi-only sessions
for (let strategy of Object.values(PageLoadStrategy)) {
caps = fromJSON({ pageLoadStrategy: strategy }, true);
ok(!caps.has("pageLoadStrategy"));
}
let timeoutsConfig = { implicit: 123 };
caps = fromJSON({ timeouts: timeoutsConfig }, true);
ok(!caps.has("timeouts"));
caps = fromJSON({ strictFileInteractability: true }, true);
ok(!caps.has("strictFileInteractability"));
caps = fromJSON({ webSocketUrl: true }, true);
ok(!caps.has("webSocketUrl"));
// Mozilla specific capabilities
caps = fromJSON({ "moz:accessibilityChecks": true }, true);
ok(!caps.has("moz:accessibilityChecks"));
caps = fromJSON({ "moz:webdriverClick": true }, true);
equal(undefined, caps.get("moz:webdriverClick"));
// capability is always populated with null if remote agent is not listening
caps = fromJSON({}, true);
equal(null, caps.get("moz:debuggerAddress"));
caps = fromJSON({ "moz:debuggerAddress": "foo" }, true);
equal(null, caps.get("moz:debuggerAddress"));
caps = fromJSON({ "moz:debuggerAddress": true }, true);
equal(null, caps.get("moz:debuggerAddress"));
// Extension capabilities
caps = fromJSON({ "webauthn:virtualAuthenticators": true }, true);
ok(!caps.has("webauthn:virtualAuthenticators"));
caps = fromJSON({ "webauthn:extension:uvm": true }, true);
ok(!caps.has("webauthn:extension:uvm"));
caps = fromJSON({ "webauthn:extension:prf": true }, true);
ok(!caps.has("webauthn:extension:prf"));
caps = fromJSON({ "webauthn:extension:largeBlob": true }, true);
ok(!caps.has("webauthn:extension:largeBlob"));
caps = fromJSON({ "webauthn:extension:credBlob": true }, true);
ok(!caps.has("webauthn:extension:credBlob"));
});
add_task(function test_mergeCapabilities() {
// Shadowed values.
Assert.throws(

View file

@ -13,10 +13,12 @@ const { getWebDriverSessionById, WebDriverSession } =
);
function createSession(options = {}) {
const { capabilities = {}, connection, isHttp = false } = options;
const { capabilities = {}, connection, isBidi = false } = options;
const flags = new Set();
if (isHttp) {
if (isBidi) {
flags.add("bidi");
} else {
flags.add("http");
}
@ -24,17 +26,28 @@ function createSession(options = {}) {
}
add_task(function test_WebDriverSession_ctor() {
// Missing WebDriver session flags
Assert.throws(() => new WebDriverSession({}), /TypeError/);
// Session needs to be either HTTP or BiDi
for (const flags of [[], ["bidi", "http"]]) {
Assert.throws(
() => new WebDriverSession({}, new Set(flags)),
/SessionNotCreatedError:/
);
}
// Session id and path
let session = createSession();
equal(typeof session.id, "string");
equal(session.path, `/session/${session.id}`);
// Sets HTTP flag
session = createSession({ isHttp: true });
// Sets HTTP and BiDi flags correctly.
session = createSession({ isBidi: false });
equal(session.bidi, false);
equal(session.http, true);
session = createSession({ isHttp: false });
session = createSession({ isBidi: true });
equal(session.bidi, true);
equal(session.http, false);
// Sets capabilities based on session configuration flag.
@ -49,20 +62,21 @@ add_task(function test_WebDriverSession_ctor() {
};
// HTTP session
session = createSession({ isHttp: true, capabilities });
session = createSession({ capabilities, isBidi: false });
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 });
// BiDi session
session = createSession({ capabilities, isBidi: true });
equal(session.acceptInsecureCerts, true);
equal(session.pageLoadStrategy, "normal");
equal(session.strictFileInteractability, false);
equal(session.timeouts.script, 30000);
equal(session.userPromptHandler.toJSON(), "dismiss and notify");
equal(session.pageLoadStrategy, undefined);
equal(session.strictFileInteractability, undefined);
equal(session.timeouts, undefined);
});
add_task(function test_WebDriverSession_destroy() {

View file

@ -60,11 +60,37 @@ export class WebDriverBiDi {
return this.#session;
}
#newSessionAlgorithm(session, flags) {
if (!this.#agent.running) {
// With the Remote Agent not running WebDriver BiDi is not supported.
return;
}
if (flags.has(lazy.WebDriverSession.SESSION_FLAG_BIDI)) {
// It's already a WebDriver BiDi session.
return;
}
const webSocketUrl = session.capabilities.get("webSocketUrl");
if (webSocketUrl === undefined) {
return;
}
// Start listening for BiDi connections.
this.#agent.server.registerPathHandler(session.path, session);
lazy.logger.debug(`Registered session handler: ${session.path}`);
session.capabilities.set("webSocketUrl", `${this.address}${session.path}`);
session.bidi = true;
flags.add("bidi");
}
/**
* Add a new connection that is not yet attached to a WebDriver session.
*
* @param {WebDriverBiDiConnection} connection
* The connection without an accociated WebDriver session.
* The connection without an associated WebDriver session.
*/
addSessionlessConnection(connection) {
this.#sessionlessConnections.add(connection);
@ -79,7 +105,7 @@ export class WebDriverBiDi {
* @param {Set} flags
* Session configuration flags.
* @param {WebDriverBiDiConnection=} sessionlessConnection
* Optional connection that is not yet accociated with a WebDriver
* Optional connection that is not yet associated with a WebDriver
* session, and has to be associated with the new WebDriver session.
*
* @returns {Object<string, Capabilities>}
@ -95,19 +121,21 @@ export class WebDriverBiDi {
);
}
const session = new lazy.WebDriverSession(
this.#session = new lazy.WebDriverSession(
capabilities,
flags,
sessionlessConnection
);
// When the Remote Agent is listening, and a BiDi WebSocket connection
// has been requested, register a path handler for the session.
let webSocketUrl = null;
if (
this.#agent.running &&
(session.capabilities.get("webSocketUrl") || sessionlessConnection)
) {
// Run new session steps for WebDriver BiDi.
this.#newSessionAlgorithm(this.#session, flags);
if (sessionlessConnection) {
// Connection is now registered with a WebDriver session
this.#sessionlessConnections.delete(sessionlessConnection);
}
if (this.#session.bidi) {
// Creating a WebDriver BiDi session too early can cause issues with
// clients in not being able to find any available browsing context.
// Also when closing the application while it's still starting up can
@ -115,24 +143,8 @@ export class WebDriverBiDi {
// once the initial application window has finished initializing.
lazy.logger.debug(`Waiting for initial application window`);
await this.#agent.browserStartupFinished;
this.#agent.server.registerPathHandler(session.path, session);
webSocketUrl = `${this.address}${session.path}`;
lazy.logger.debug(`Registered session handler: ${session.path}`);
if (sessionlessConnection) {
// Remove temporary session-less connection
this.#sessionlessConnections.delete(sessionlessConnection);
}
}
// Also update the webSocketUrl capability to contain the session URL if
// a path handler has been registered. Otherwise set its value to null.
session.capabilities.set("webSocketUrl", webSocketUrl);
this.#session = session;
return {
sessionId: this.#session.id,
capabilities: this.#session.capabilities,

View file

@ -14,8 +14,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
"chrome://remote/content/shared/webdriver/Capabilities.sys.mjs",
quit: "chrome://remote/content/shared/Browser.sys.mjs",
RemoteAgent: "chrome://remote/content/components/RemoteAgent.sys.mjs",
WEBDRIVER_CLASSIC_CAPABILITIES:
"chrome://remote/content/shared/webdriver/Capabilities.sys.mjs",
WebDriverSession: "chrome://remote/content/shared/webdriver/Session.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "logger", () =>
@ -23,6 +22,8 @@ ChromeUtils.defineLazyGetter(lazy, "logger", () =>
);
export class WebDriverBiDiConnection extends WebSocketConnection {
#sessionConfigFlags;
/**
* @param {WebSocket} webSocket
* The WebSocket server connection to wrap.
@ -34,6 +35,10 @@ export class WebDriverBiDiConnection extends WebSocketConnection {
// Each connection has only a single associated WebDriver session.
this.session = null;
this.#sessionConfigFlags = new Set([
lazy.WebDriverSession.SESSION_FLAG_BIDI,
]);
}
/**
@ -180,25 +185,11 @@ export class WebDriverBiDiConnection extends WebSocketConnection {
if (module === "session" && command === "new") {
const processedCapabilities = lazy.processCapabilities(params);
const flags = new Set();
result = await lazy.RemoteAgent.webDriverBiDi.createSession(
processedCapabilities,
flags,
this.#sessionConfigFlags,
this
);
// The Capabilities class sets up default values also for capabilities
// which are not relevant for BiDi, we want to remove those from the payload.
result.capabilities = Array.from(result.capabilities.entries()).reduce(
(object, [key, value]) => {
if (!lazy.WEBDRIVER_CLASSIC_CAPABILITIES.includes(key)) {
object[key] = value;
}
return object;
},
{}
);
} else if (module === "session" && command === "status") {
result = lazy.RemoteAgent.webDriverBiDi.getSessionReadinessStatus();
} else {

View file

@ -1,8 +1,13 @@
[DEFAULT]
tags = "wd"
subsuite = "remote"
args = [
"--remote-debugging-port",
]
support-files = ["head.js"]
["browser_RemoteValue.js"]
["browser_RemoteValueDOM.js"]
["browser_WebDriverBiDi.js"]

View file

@ -0,0 +1,126 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { RemoteAgent: RemoteAgentModule } = ChromeUtils.importESModule(
"chrome://remote/content/components/RemoteAgent.sys.mjs"
);
const { WebDriverBiDi } = ChromeUtils.importESModule(
"chrome://remote/content/webdriver-bidi/WebDriverBiDi.sys.mjs"
);
async function runBiDiTest(test, options = {}) {
const { capabilities = {}, connection, isBidi = false } = options;
const flags = new Set();
if (isBidi) {
flags.add("bidi");
} else {
flags.add("http");
}
const wdBiDi = new WebDriverBiDi(RemoteAgentModule);
await wdBiDi.createSession(capabilities, flags, connection);
await test(wdBiDi);
wdBiDi.deleteSession();
}
add_task(async function test_createSession() {
// Missing WebDriver session flags
const bidi = new WebDriverBiDi(RemoteAgent);
await Assert.rejects(bidi.createSession({}), /TypeError/);
// Session needs to be either HTTP or BiDi
for (const flags of [[], ["bidi", "http"]]) {
await Assert.rejects(
bidi.createSession({}, new Set(flags)),
/SessionNotCreatedError:/
);
}
// Session id and path
await runBiDiTest(wdBiDi => {
is(typeof wdBiDi.session.id, "string");
is(wdBiDi.session.path, `/session/${wdBiDi.session.id}`);
});
// Sets HTTP and BiDi flags correctly.
await runBiDiTest(
wdBiDi => {
is(wdBiDi.session.bidi, false);
is(wdBiDi.session.http, true);
},
{ isBidi: false }
);
await runBiDiTest(
wdBiDi => {
is(wdBiDi.session.bidi, true);
is(wdBiDi.session.http, false);
},
{ isBidi: true }
);
// Sets capabilities based on session configuration flag.
const capabilities = {
acceptInsecureCerts: true,
unhandledPromptBehavior: "ignore",
// HTTP only
pageLoadStrategy: "eager",
strictFileInteractability: true,
timeouts: { script: 1000 },
// Bug 1731730: Requires matching for session.new command.
// webSocketUrl: false,
};
// HTTP session (no webSocketUrl)
await runBiDiTest(
wdBiDi => {
is(wdBiDi.session.bidi, false);
is(wdBiDi.session.acceptInsecureCerts, true);
is(wdBiDi.session.pageLoadStrategy, "eager");
is(wdBiDi.session.strictFileInteractability, true);
is(wdBiDi.session.timeouts.script, 1000);
is(wdBiDi.session.userPromptHandler.toJSON(), "ignore");
is(wdBiDi.session.webSocketUrl, undefined);
},
{ capabilities, isBidi: false }
);
// HTTP session (with webSocketUrl)
capabilities.webSocketUrl = true;
await runBiDiTest(
wdBiDi => {
is(wdBiDi.session.bidi, true);
is(wdBiDi.session.acceptInsecureCerts, true);
is(wdBiDi.session.pageLoadStrategy, "eager");
is(wdBiDi.session.strictFileInteractability, true);
is(wdBiDi.session.timeouts.script, 1000);
is(wdBiDi.session.userPromptHandler.toJSON(), "ignore");
is(
wdBiDi.session.webSocketUrl,
`ws://127.0.0.1:9222/session/${wdBiDi.session.id}`
);
},
{ capabilities, isBidi: false }
);
// BiDi session
await runBiDiTest(
wdBiDi => {
is(wdBiDi.session.bidi, true);
is(wdBiDi.session.acceptInsecureCerts, true);
is(wdBiDi.session.userPromptHandler.toJSON(), "dismiss and notify");
is(wdBiDi.session.pageLoadStrategy, undefined);
is(wdBiDi.session.strictFileInteractability, undefined);
is(wdBiDi.session.timeouts, undefined);
is(wdBiDi.session.webSocketUrl, undefined);
},
{ capabilities, isBidi: true }
);
});

View file

@ -49,3 +49,12 @@ async def test_proxy(
)
assert response == {"type": "string", "value": page_content}
@pytest.mark.parametrize("match_type", ["alwaysMatch", "firstMatch"])
async def test_websocket_url(new_session, match_capabilities, match_type):
capabilities = match_capabilities(match_type, "webSocketUrl", True)
bidi_session = await new_session(capabilities=capabilities)
assert bidi_session.capabilities.get("webSocketUrl") is None

View file

@ -26,7 +26,8 @@ async def test_capability_type(new_session, add_browser_capabilities):
("browserVersion", str),
("platformName", str),
("proxy", dict),
("setWindowRect", bool),
("unhandledPromptBehavior", str),
("userAgent", str),
]
assert isinstance(bidi_session.capabilities, dict)
@ -70,7 +71,6 @@ async def test_ignore_non_spec_fields_in_capabilities(
("pageLoadStrategy", "none"),
("strictFileInteractability", True),
("timeouts", {"script": 500}),
("unhandledPromptBehavior", "accept"),
],
)
async def test_with_webdriver_classic_capabilities(