gecko-dev/testing/marionette/session.js
Henrik Skupin 63313b65f6 Bug 1429338 - Marionette has to honor "moz:useNonSpecCompliantPointerOrigin" capability. r=maja_zf
This flag is used to turn off the WebDriver spec conforming pointer origin
calculation. It has to be kept until all Selenium bindings can successfully
handle the WebDriver spec conforming Pointer Origin calculation.

MozReview-Commit-ID: 3YknXlWoyi1

--HG--
extra : rebase_source : 6df2af027e7458fd29658d7a3bbe99634b6f58f9
2018-01-23 17:31:06 +01:00

550 lines
15 KiB
JavaScript

/* 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";
Cu.importGlobalProperties(["URL"]);
ChromeUtils.import("resource://gre/modules/Preferences.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("chrome://marionette/content/assert.js");
const {
InvalidArgumentError,
} = ChromeUtils.import("chrome://marionette/content/error.js", {});
const {
pprint,
} = ChromeUtils.import("chrome://marionette/content/format.js", {});
this.EXPORTED_SYMBOLS = ["session"];
// Enable testing this module, as Services.appinfo.* is not available
// in xpcshell tests.
const appinfo = {name: "<missing>", version: "<missing>"};
try { appinfo.name = Services.appinfo.name.toLowerCase(); } catch (e) {}
try { appinfo.version = Services.appinfo.version; } catch (e) {}
/**
* State associated with a WebDriver session.
*
* @namespace
*/
this.session = {};
/** Representation of WebDriver session timeouts. */
session.Timeouts = class {
constructor() {
// disabled
this.implicit = 0;
// five mintues
this.pageLoad = 300000;
// 30 seconds
this.script = 30000;
}
toString() { return "[object session.Timeouts]"; }
/** Marshals timeout durations to a JSON Object. */
toJSON() {
return {
implicit: this.implicit,
pageLoad: this.pageLoad,
script: this.script,
};
}
static fromJSON(json) {
assert.object(json);
let t = new session.Timeouts();
for (let [typ, ms] of Object.entries(json)) {
switch (typ) {
case "implicit":
t.implicit = assert.positiveInteger(ms);
break;
case "script":
t.script = assert.positiveInteger(ms);
break;
case "pageLoad":
t.pageLoad = assert.positiveInteger(ms);
break;
default:
throw new InvalidArgumentError("Unrecognised timeout: " + typ);
}
}
return t;
}
};
/**
* Enum of page loading strategies.
*
* @enum
*/
session.PageLoadStrategy = {
/** No page load strategy. Navigation will return immediately. */
None: "none",
/**
* Eager, causing navigation to complete when the document reaches
* the <code>interactive</code> ready state.
*/
Eager: "eager",
/**
* Normal, causing navigation to return when the document reaches the
* <code>complete</code> ready state.
*/
Normal: "normal",
};
/** Proxy configuration object representation. */
session.Proxy = class {
/** @class */
constructor() {
this.proxyType = null;
this.ftpProxy = null;
this.ftpProxyPort = null;
this.httpProxy = null;
this.httpProxyPort = null;
this.noProxy = null;
this.sslProxy = null;
this.sslProxyPort = null;
this.socksProxy = null;
this.socksProxyPort = null;
this.socksVersion = null;
this.proxyAutoconfigUrl = null;
}
/**
* Sets Firefox proxy settings.
*
* @return {boolean}
* True if proxy settings were updated as a result of calling this
* function, or false indicating that this function acted as
* a no-op.
*/
init() {
switch (this.proxyType) {
case "autodetect":
Preferences.set("network.proxy.type", 4);
return true;
case "direct":
Preferences.set("network.proxy.type", 0);
return true;
case "manual":
Preferences.set("network.proxy.type", 1);
if (this.ftpProxy) {
Preferences.set("network.proxy.ftp", this.ftpProxy);
if (Number.isInteger(this.ftpProxyPort)) {
Preferences.set("network.proxy.ftp_port", this.ftpProxyPort);
}
}
if (this.httpProxy) {
Preferences.set("network.proxy.http", this.httpProxy);
if (Number.isInteger(this.httpProxyPort)) {
Preferences.set("network.proxy.http_port", this.httpProxyPort);
}
}
if (this.sslProxy) {
Preferences.set("network.proxy.ssl", this.sslProxy);
if (Number.isInteger(this.sslProxyPort)) {
Preferences.set("network.proxy.ssl_port", this.sslProxyPort);
}
}
if (this.socksProxy) {
Preferences.set("network.proxy.socks", this.socksProxy);
if (Number.isInteger(this.socksProxyPort)) {
Preferences.set("network.proxy.socks_port", this.socksProxyPort);
}
if (this.socksVersion) {
Preferences.set("network.proxy.socks_version", this.socksVersion);
}
}
if (this.noProxy) {
Preferences.set("network.proxy.no_proxies_on", this.noProxy.join(", "));
}
return true;
case "pac":
Preferences.set("network.proxy.type", 2);
Preferences.set(
"network.proxy.autoconfig_url", this.proxyAutoconfigUrl);
return true;
case "system":
Preferences.set("network.proxy.type", 5);
return true;
default:
return false;
}
}
/**
* @param {Object.<string, ?>} json
* JSON Object to unmarshal.
*
* @throws {InvalidArgumentError}
* When proxy configuration is invalid.
*/
static fromJSON(json) {
function stripBracketsFromIpv6Hostname(hostname) {
return hostname.includes(":") ? hostname.replace(/[\[\]]/g, "") : hostname;
}
// Parse hostname and optional port from host
function fromHost(scheme, host) {
assert.string(host,
pprint`Expected proxy "host" to be a string, got ${host}`);
if (host.includes("://")) {
throw new InvalidArgumentError(`${host} contains a scheme`);
}
let url;
try {
// To parse the host a scheme has to be added temporarily.
// If the returned value for the port is an empty string it
// could mean no port or the default port for this scheme was
// specified. In such a case parse again with a different
// scheme to ensure we filter out the default port.
url = new URL("http://" + host);
if (url.port == "") {
url = new URL("https://" + host);
}
} catch (e) {
throw new InvalidArgumentError(e.message);
}
let hostname = stripBracketsFromIpv6Hostname(url.hostname);
// If the port hasn't been set, use the default port of
// the selected scheme (except for socks which doesn't have one).
let port = parseInt(url.port);
if (!Number.isInteger(port)) {
if (scheme === "socks") {
port = null;
} else {
port = Services.io.getProtocolHandler(scheme).defaultPort;
}
}
if (url.username != "" ||
url.password != "" ||
url.pathname != "/" ||
url.search != "" ||
url.hash != "") {
throw new InvalidArgumentError(
`${host} was not of the form host[:port]`);
}
return [hostname, port];
}
let p = new session.Proxy();
if (typeof json == "undefined" || json === null) {
return p;
}
assert.object(json, pprint`Expected "proxy" to be an object, got ${json}`);
assert.in("proxyType", json,
pprint`Expected "proxyType" in "proxy" object, got ${json}`);
p.proxyType = assert.string(json.proxyType,
pprint`Expected "proxyType" to be a string, got ${json.proxyType}`);
switch (p.proxyType) {
case "autodetect":
case "direct":
case "system":
break;
case "pac":
p.proxyAutoconfigUrl = assert.string(json.proxyAutoconfigUrl,
`Expected "proxyAutoconfigUrl" to be a string, ` +
pprint`got ${json.proxyAutoconfigUrl}`);
break;
case "manual":
if (typeof json.ftpProxy != "undefined") {
[p.ftpProxy, p.ftpProxyPort] = fromHost("ftp", json.ftpProxy);
}
if (typeof json.httpProxy != "undefined") {
[p.httpProxy, p.httpProxyPort] = fromHost("http", json.httpProxy);
}
if (typeof json.sslProxy != "undefined") {
[p.sslProxy, p.sslProxyPort] = fromHost("https", json.sslProxy);
}
if (typeof json.socksProxy != "undefined") {
[p.socksProxy, p.socksProxyPort] = fromHost("socks", json.socksProxy);
p.socksVersion = assert.positiveInteger(json.socksVersion);
}
if (typeof json.noProxy != "undefined") {
let entries = assert.array(json.noProxy,
pprint`Expected "noProxy" to be an array, got ${json.noProxy}`);
p.noProxy = entries.map(entry => {
assert.string(entry,
pprint`Expected "noProxy" entry to be a string, got ${entry}`);
return stripBracketsFromIpv6Hostname(entry);
});
}
break;
default:
throw new InvalidArgumentError(
`Invalid type of proxy: ${p.proxyType}`);
}
return p;
}
/**
* @return {Object.<string, (number|string)>}
* JSON serialisation of proxy object.
*/
toJSON() {
function addBracketsToIpv6Hostname(hostname) {
return hostname.includes(":") ? `[${hostname}]` : hostname;
}
function toHost(hostname, port) {
if (!hostname) {
return null;
}
// Add brackets around IPv6 addresses
hostname = addBracketsToIpv6Hostname(hostname);
if (port != null) {
return `${hostname}:${port}`;
}
return hostname;
}
let excludes = this.noProxy;
if (excludes) {
excludes = excludes.map(addBracketsToIpv6Hostname);
}
return marshal({
proxyType: this.proxyType,
ftpProxy: toHost(this.ftpProxy, this.ftpProxyPort),
httpProxy: toHost(this.httpProxy, this.httpProxyPort),
noProxy: excludes,
sslProxy: toHost(this.sslProxy, this.sslProxyPort),
socksProxy: toHost(this.socksProxy, this.socksProxyPort),
socksVersion: this.socksVersion,
proxyAutoconfigUrl: this.proxyAutoconfigUrl,
});
}
toString() { return "[object session.Proxy]"; }
};
/** WebDriver session capabilities representation. */
session.Capabilities = class extends Map {
/** @class */
constructor() {
super([
// webdriver
["browserName", appinfo.name],
["browserVersion", appinfo.version],
["platformName", Services.sysinfo.getProperty("name").toLowerCase()],
["platformVersion", Services.sysinfo.getProperty("version")],
["pageLoadStrategy", session.PageLoadStrategy.Normal],
["acceptInsecureCerts", false],
["timeouts", new session.Timeouts()],
["proxy", new session.Proxy()],
// features
["rotatable", appinfo.name == "B2G"],
// proprietary
["moz:accessibilityChecks", false],
["moz:headless", Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless],
["moz:processID", Services.appinfo.processID],
["moz:profile", maybeProfile()],
["moz:useNonSpecCompliantPointerOrigin", false],
["moz:webdriverClick", true],
]);
}
/**
* @param {string} key
* Capability key.
* @param {(string|number|boolean)} value
* JSON-safe capability value.
*/
set(key, value) {
if (key === "timeouts" && !(value instanceof session.Timeouts)) {
throw new TypeError();
} else if (key === "proxy" && !(value instanceof session.Proxy)) {
throw new TypeError();
}
return super.set(key, value);
}
/** @return {string} */
toString() { return "[object session.Capabilities]"; }
/**
* JSON serialisation of capabilities object.
*
* @return {Object.<string, ?>}
*/
toJSON() {
return marshal(this);
}
/**
* Unmarshal a JSON object representation of WebDriver capabilities.
*
* @param {Object.<string, *>=} json
* WebDriver capabilities.
*
* @return {session.Capabilities}
* Internal representation of WebDriver capabilities.
*/
static fromJSON(json) {
if (typeof json == "undefined" || json === null) {
json = {};
}
assert.object(json);
return session.Capabilities.match_(json);
}
// Matches capabilities as described by WebDriver.
static match_(json = {}) {
let matched = new session.Capabilities();
for (let [k, v] of Object.entries(json)) {
switch (k) {
case "acceptInsecureCerts":
assert.boolean(v);
matched.set("acceptInsecureCerts", v);
break;
case "pageLoadStrategy":
if (v === null) {
matched.set("pageLoadStrategy", session.PageLoadStrategy.Normal);
} else {
assert.string(v);
if (Object.values(session.PageLoadStrategy).includes(v)) {
matched.set("pageLoadStrategy", v);
} else {
throw new InvalidArgumentError(
"Unknown page load strategy: " + v);
}
}
break;
case "proxy":
let proxy = session.Proxy.fromJSON(v);
matched.set("proxy", proxy);
break;
case "timeouts":
let timeouts = session.Timeouts.fromJSON(v);
matched.set("timeouts", timeouts);
break;
case "moz:accessibilityChecks":
assert.boolean(v);
matched.set("moz:accessibilityChecks", v);
break;
case "moz:useNonSpecCompliantPointerOrigin":
assert.boolean(v);
matched.set("moz:useNonSpecCompliantPointerOrigin", v);
break;
case "moz:webdriverClick":
assert.boolean(v);
matched.set("moz:webdriverClick", v);
break;
}
}
return matched;
}
};
// Specialisation of |JSON.stringify| that produces JSON-safe object
// literals, dropping empty objects and entries which values are undefined
// or null. Objects are allowed to produce their own JSON representations
// by implementing a |toJSON| function.
function marshal(obj) {
let rv = Object.create(null);
function* iter(mapOrObject) {
if (mapOrObject instanceof Map) {
for (const [k, v] of mapOrObject) {
yield [k, v];
}
} else {
for (const k of Object.keys(mapOrObject)) {
yield [k, mapOrObject[k]];
}
}
}
for (let [k, v] of iter(obj)) {
// Skip empty values when serialising to JSON.
if (typeof v == "undefined" || v === null) {
continue;
}
// Recursively marshal objects that are able to produce their own
// JSON representation.
if (typeof v.toJSON == "function") {
v = marshal(v.toJSON());
// Or do the same for object literals.
} else if (isObject(v)) {
v = marshal(v);
}
// And finally drop (possibly marshaled) objects which have no
// entries.
if (!isObjectEmpty(v)) {
rv[k] = v;
}
}
return rv;
}
function isObject(obj) {
return Object.prototype.toString.call(obj) == "[object Object]";
}
function isObjectEmpty(obj) {
return isObject(obj) && Object.keys(obj).length === 0;
}
// Services.dirsvc is not accessible from content frame scripts,
// but we should not panic about that.
function maybeProfile() {
try {
return Services.dirsvc.get("ProfD", Ci.nsIFile).path;
} catch (e) {
return "<protected>";
}
}