forked from mirrors/gecko-dev
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:
parent
0ac177013f
commit
3687348b50
12 changed files with 436 additions and 161 deletions
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
];
|
||||
|
||||
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],
|
||||
]);
|
||||
["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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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,23 +143,7 @@ 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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
126
remote/webdriver-bidi/test/browser/browser_WebDriverBiDi.js
Normal file
126
remote/webdriver-bidi/test/browser/browser_WebDriverBiDi.js
Normal 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 }
|
||||
);
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue