Bug 1326534 - Rewrite capabilities parsing in Marionette; r?automatedtester
This patch provides a (nearly) WebDriver conforming implementation
of capabilities in Marionette. The work remaining is pending further
clarification in the specification.
Capabilities are represented internally as a complex object provided
by `session.Capabilities`. Timeouts and proxy configuration are also
represented by the similar complex objects `session.Timeouts` and
`session.Proxy`, respectively.
The capabilities stored in `GeckoDriver#sessionCapabilities` are the
result of parsing user-provided desired- and required capabilities.
WebDriver now uses `firstMatch` and `alwaysMatch` primitives for
capabilities, but as this is considered a wider breaking change, the
move to these primitives will be done at a later stage. It’s prudent
to point out that the base techniques used with the new primitives
are similar to those implemented for `desiredCapabilities` and
`requiredCapabilities` in this patch, and that the work needed to adapt
them is considered trivial.
When capabilities are presented back to the user (the so called processed
capabilities), we call the `toJSON` implementation on the complex objects.
`session.Capabilities#toJSON` calls the internal function `marshal`
which ensures empty fields are dropped. `marshal` can be considered to
be a specialisation of the standard library `JSON.stringify`, which also
calls `toJSON` on entry values if they provide this function.
The changeset overall also provides a much deeper level of testing of
WebDriver capabilities.
MozReview-Commit-ID: 97xGt3cnMys
--- a/testing/marionette/jar.mn
+++ b/testing/marionette/jar.mn
@@ -26,15 +26,16 @@ marionette.jar:
content/cookies.js (cookies.js)
content/atom.js (atom.js)
content/evaluate.js (evaluate.js)
content/logging.js (logging.js)
content/navigate.js (navigate.js)
content/l10n.js (l10n.js)
content/assert.js (assert.js)
content/addon.js (addon.js)
+ content/session.js (session.js)
#ifdef ENABLE_TESTS
content/test.xul (chrome/test.xul)
content/test2.xul (chrome/test2.xul)
content/test_dialog.xul (chrome/test_dialog.xul)
content/test_nested_iframe.xul (chrome/test_nested_iframe.xul)
content/test_anonymous_content.xul (chrome/test_anonymous_content.xul)
#endif
new file mode 100644
--- /dev/null
+++ b/testing/marionette/session.js
@@ -0,0 +1,457 @@
+/* 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 {interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+Cu.import("chrome://marionette/content/assert.js");
+Cu.import("chrome://marionette/content/error.js");
+
+this.EXPORTED_SYMBOLS = ["session"];
+
+const logger = Log.repository.getLogger("Marionette");
+const {pprint} = error;
+
+// 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. */
+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]"; }
+
+ toJSON () {
+ return {
+ "implicit": this.implicit,
+ "page load": this.pageLoad,
+ "script": this.script,
+ };
+ }
+
+ static fromJSON (json) {
+ assert.object(json);
+ let t = new session.Timeouts();
+
+ for (let [typ, ms] of Object.entries(json)) {
+ assert.positiveInteger(ms);
+
+ switch (typ) {
+ case "implicit":
+ t.implicit = ms;
+ break;
+
+ case "script":
+ t.script = ms;
+ break;
+
+ case "page load":
+ t.pageLoad = ms;
+ break;
+
+ default:
+ throw new InvalidArgumentError();
+ }
+ }
+
+ return t;
+ }
+};
+
+/** Enum of page loading strategies. */
+session.PageLoadStrategy = {
+ None: "none",
+ Eager: "eager",
+ Normal: "normal",
+};
+
+/** Proxy configuration object representation. */
+session.Proxy = class {
+ constructor() {
+ this.proxyType = null;
+ this.httpProxy = null;
+ this.httpProxyPort = null;
+ this.sslProxy = null;
+ this.sslProxyPort = null;
+ this.ftpProxy = null;
+ this.ftpProxyPort = 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 "manual":
+ Preferences.set("network.proxy.type", 1);
+ if (this.httpProxy && this.httpProxyPort) {
+ Preferences.set("network.proxy.http", this.httpProxy);
+ Preferences.set("network.proxy.http_port", this.httpProxyPort);
+ }
+ if (this.sslProxy && this.sslProxyPort) {
+ Preferences.set("network.proxy.ssl", this.sslProxy);
+ Preferences.set("network.proxy.ssl_port", this.sslProxyPort);
+ }
+ if (this.ftpProxy && this.ftpProxyPort) {
+ Preferences.set("network.proxy.ftp", this.ftpProxy);
+ Preferences.set("network.proxy.ftp_port", this.ftpProxyPort);
+ }
+ if (this.socksProxy) {
+ Preferences.set("network.proxy.socks", this.socksProxy);
+ Preferences.set("network.proxy.socks_port", this.socksProxyPort);
+ if (this.socksVersion) {
+ Preferences.set("network.proxy.socks_version", this.socksVersion);
+ }
+ }
+ return true;
+
+ case "pac":
+ Preferences.set("network.proxy.type", 2);
+ Preferences.set("network.proxy.autoconfig_url", this.proxyAutoconfigUrl);
+ return true;
+
+ case "autodetect":
+ Preferences.set("network.proxy.type", 4);
+ return true;
+
+ case "system":
+ Preferences.set("network.proxy.type", 5);
+ return true;
+
+ case "noproxy":
+ Preferences.set("network.proxy.type", 0);
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ toString () { return "[object session.Proxy]"; }
+
+ toJSON () {
+ return marshal({
+ proxyType: this.proxyType,
+ httpProxy: this.httpProxy,
+ httpProxyPort: this.httpProxyPort ,
+ sslProxy: this.sslProxy,
+ sslProxyPort: this.sslProxyPort,
+ ftpProxy: this.ftpProxy,
+ ftpProxyPort: this.ftpProxyPort,
+ socksProxy: this.socksProxy,
+ socksProxyPort: this.socksProxyPort,
+ socksProxyVersion: this.socksProxyVersion,
+ proxyAutoconfigUrl: this.proxyAutoconfigUrl,
+ });
+ }
+
+ static fromJSON (json) {
+ let p = new session.Proxy();
+ if (typeof json == "undefined" || json === null) {
+ return p;
+ }
+
+ assert.object(json);
+
+ assert.in("proxyType", json);
+ p.proxyType = json.proxyType;
+
+ if (json.proxyType == "manual") {
+ if (typeof json.httpProxy != "undefined") {
+ p.httpProxy = assert.string(json.httpProxy);
+ p.httpProxyPort = assert.positiveInteger(json.httpProxyPort);
+ }
+
+ if (typeof json.sslProxy != "undefined") {
+ p.sslProxy = assert.string(json.sslProxy);
+ p.sslProxyPort = assert.positiveInteger(json.sslProxyPort);
+ }
+
+ if (typeof json.ftpProxy != "undefined") {
+ p.ftpProxy = assert.string(json.ftpProxy);
+ p.ftpProxyPort = assert.positiveInteger(json.ftpProxyPort);
+ }
+
+ if (typeof json.socksProxy != "undefined") {
+ p.socksProxy = assert.string(json.socksProxy);
+ p.socksProxyPort = assert.positiveInteger(json.socksProxyPort);
+ p.socksProxyVersion = assert.positiveInteger(json.socksProxyVersion);
+ }
+ }
+
+ if (typeof json.proxyAutoconfigUrl != "undefined") {
+ p.proxyAutoconfigUrl = assert.string(json.proxyAutoconfigUrl);
+ }
+
+ return p;
+ }
+};
+
+/** WebDriver session capabilities representation. */
+session.Capabilities = class extends Map {
+ 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
+ ["specificationLevel", 0],
+ ["moz:processID", Services.appinfo.processID],
+ ["moz:profile", maybeProfile()],
+ ["moz:accessibilityChecks", false],
+ ]);
+ }
+
+ 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);
+ }
+
+ toString() { return "[object session.Capabilities]"; }
+
+ toJSON() {
+ return marshal(this);
+ }
+
+ /**
+ * Unmarshal a JSON object representation of WebDriver capabilities.
+ *
+ * @param {Object.<string, ?>=} json
+ * WebDriver capabilities.
+ * @param {boolean=} merge
+ * If providing |json| with |desiredCapabilities| or
+ * |requiredCapabilities| fields, or both, it should be set to
+ * true to merge these before parsing. This indicates
+ * that the input provided is from a client and not from
+ * |session.Capabilities#toJSON|.
+ *
+ * @return {session.Capabilities}
+ * Internal representation of WebDriver capabilities.
+ */
+ static fromJSON (json, {merge = false} = {}) {
+ if (typeof json == "undefined" || json === null) {
+ json = {};
+ }
+ assert.object(json);
+
+ if (merge) {
+ json = session.Capabilities.merge_(json);
+ }
+ return session.Capabilities.match_(json);
+ }
+
+ // Processes capabilities as described by WebDriver.
+ static merge_ (json) {
+ for (let entry of [json.desiredCapabilities, json.requiredCapabilities]) {
+ if (typeof entry == "undefined" || entry === null) {
+ continue;
+ }
+ assert.object(entry, error.pprint`Expected ${entry} to be a capabilities object`);
+ }
+
+ let desired = json.desiredCapabilities || {};
+ let required = json.requiredCapabilities || {};
+
+ // One level deep union merge of desired- and required capabilities
+ // with preference on required
+ return Object.assign({}, desired, required);
+ }
+
+ // Matches capabilities as described by WebDriver.
+ static match_ (caps = {}) {
+ let matched = new session.Capabilities();
+
+ const defined = v => typeof v != "undefined" && v !== null;
+ const wildcard = v => v === "*";
+
+ // Iff |actual| provides some value, or is a wildcard or an exact
+ // match of |expected|. This means it can be null or undefined,
+ // or "*", or "firefox".
+ function stringMatch (actual, expected) {
+ return !defined(actual) || (wildcard(actual) || actual === expected);
+ }
+
+ for (let [k,v] of Object.entries(caps)) {
+ switch (k) {
+ case "browserName":
+ let bname = matched.get("browserName");
+ if (!stringMatch(v, bname)) {
+ throw new TypeError(
+ pprint`Given browserName ${v}, but my name is ${bname}`);
+ }
+ break;
+
+ // TODO(ato): bug 1326397
+ case "browserVersion":
+ let bversion = matched.get("browserVersion");
+ if (!stringMatch(v, bversion)) {
+ throw new TypeError(
+ pprint`Given browserVersion ${v}, ` +
+ pprint`but current version is ${bversion}`);
+ }
+ break;
+
+ case "platformName":
+ let pname = matched.get("platformName");
+ if (!stringMatch(v, pname)) {
+ throw new TypeError(
+ pprint`Given platformName ${v}, ` +
+ pprint`but current platform is ${pname}`);
+ }
+ break;
+
+ // TODO(ato): bug 1326397
+ case "platformVersion":
+ let pversion = matched.get("platformVersion");
+ if (!stringMatch(v, pversion)) {
+ throw new TypeError(
+ pprint`Given platformVersion ${v}, ` +
+ pprint`but current platform version is ${pversion}`);
+ }
+ break;
+
+ case "acceptInsecureCerts":
+ assert.boolean(v);
+ matched.set("acceptInsecureCerts", v);
+ break;
+
+ case "pageLoadStrategy":
+ if (Object.values(session.PageLoadStrategy).includes(v)) {
+ matched.set("pageLoadStrategy", v);
+ } else {
+ throw new TypeError("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 "specificationLevel":
+ assert.positiveInteger(v);
+ matched.set("specificationLevel", v);
+ break;
+
+ case "moz:accessibilityChecks":
+ assert.boolean(v);
+ matched.set("moz:accessibilityChecks", 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>";
+ }
+}
new file mode 100644
--- /dev/null
+++ b/testing/marionette/test_session.js
@@ -0,0 +1,370 @@
+/* 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 {utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+Cu.import("chrome://marionette/content/error.js");
+Cu.import("chrome://marionette/content/session.js");
+
+add_test(function test_Timeouts_ctor() {
+ let ts = new session.Timeouts();
+ equal(ts.implicit, 0);
+ equal(ts.pageLoad, 300000);
+ equal(ts.script, 30000);
+
+ run_next_test();
+});
+
+add_test(function test_Timeouts_toString() {
+ equal(new session.Timeouts().toString(), "[object session.Timeouts]");
+
+ run_next_test();
+});
+
+add_test(function test_Timeouts_toJSON() {
+ let ts = new session.Timeouts();
+ deepEqual(ts.toJSON(), {"implicit": 0, "page load": 300000, "script": 30000});
+
+ run_next_test();
+});
+
+add_test(function test_Timeouts_fromJSON() {
+ let json = {
+ "implicit": 10,
+ "page load": 20,
+ "script": 30,
+ };
+ let ts = session.Timeouts.fromJSON(json);
+ equal(ts.implicit, json["implicit"]);
+ equal(ts.pageLoad, json["page load"]);
+ equal(ts.script, json["script"]);
+
+ run_next_test();
+});
+
+add_test(function test_PageLoadStrategy() {
+ equal(session.PageLoadStrategy.None, "none");
+ equal(session.PageLoadStrategy.Eager, "eager");
+ equal(session.PageLoadStrategy.Normal, "normal");
+
+ run_next_test();
+});
+
+add_test(function test_Proxy_ctor() {
+ let p = new session.Proxy();
+ let props = [
+ "proxyType",
+ "httpProxy",
+ "httpProxyPort",
+ "sslProxy",
+ "sslProxyPort",
+ "ftpProxy",
+ "ftpProxyPort",
+ "socksProxy",
+ "socksProxyPort",
+ "socksVersion",
+ "proxyAutoconfigUrl",
+ ];
+ for (let prop of props) {
+ ok(prop in p, `${prop} in ${JSON.stringify(props)}`);
+ equal(p[prop], null);
+ }
+
+ run_next_test();
+});
+
+add_test(function test_Proxy_init() {
+ let p = new session.Proxy();
+
+ // no changed made, and 5 (system) is default
+ equal(p.init(), false);
+ equal(Preferences.get("network.proxy.type"), 5);
+
+ // pac
+ p.proxyType = "pac";
+ p.proxyAutoconfigUrl = "http://localhost:1234";
+ ok(p.init());
+
+ equal(Preferences.get("network.proxy.type"), 2);
+ equal(Preferences.get("network.proxy.autoconfig_url"),
+ "http://localhost:1234");
+
+ // autodetect
+ p = new session.Proxy();
+ p.proxyType = "autodetect";
+ ok(p.init());
+ equal(Preferences.get("network.proxy.type"), 4);
+
+ // system
+ p = new session.Proxy();
+ p.proxyType = "system";
+ ok(p.init());
+ equal(Preferences.get("network.proxy.type"), 5);
+
+ // noproxy
+ p = new session.Proxy();
+ p.proxyType = "noproxy";
+ ok(p.init());
+ equal(Preferences.get("network.proxy.type"), 0);
+
+ run_next_test();
+});
+
+add_test(function test_Proxy_toString() {
+ equal(new session.Proxy().toString(), "[object session.Proxy]");
+
+ run_next_test();
+});
+
+add_test(function test_Proxy_toJSON() {
+ let p = new session.Proxy();
+ deepEqual(p.toJSON(), {});
+
+ p = new session.Proxy();
+ p.proxyType = "manual";
+ deepEqual(p.toJSON(), {proxyType: "manual"});
+
+ run_next_test();
+});
+
+add_test(function test_Proxy_fromJSON() {
+ deepEqual({}, session.Proxy.fromJSON(undefined).toJSON());
+ deepEqual({}, session.Proxy.fromJSON(null).toJSON());
+
+ for (let typ of [true, 42, "foo", []]) {
+ Assert.throws(() => session.Proxy.fromJSON(typ), InvalidArgumentError);
+ }
+
+ // must contain proxyType
+ Assert.throws(() => session.Proxy.fromJSON({}), InvalidArgumentError);
+ deepEqual({proxyType: "foo"},
+ session.Proxy.fromJSON({proxyType: "foo"}).toJSON());
+
+ // manual
+ session.Proxy.fromJSON({proxyType: "manual"});
+
+ for (let proxy of ["httpProxy", "sslProxy", "ftpProxy", "socksProxy"]) {
+ let manual = {proxyType: "manual"};
+
+ for (let typ of [true, 42, [], {}, null]) {
+ manual[proxy] = typ;
+ Assert.throws(() => session.Proxy.fromJSON(manual),
+ InvalidArgumentError);
+ }
+
+ manual[proxy] = "foo";
+ Assert.throws(() => session.Proxy.fromJSON(manual),
+ InvalidArgumentError);
+
+ for (let typ of ["bar", true, [], {}, null, undefined]) {
+ manual[proxy + "Port"] = typ;
+ Assert.throws(() => session.Proxy.fromJSON(manual),
+ InvalidArgumentError);
+ }
+
+ manual[proxy] = "foo";
+ manual[proxy + "Port"] = 1234;
+
+ let expected = {
+ "proxyType": "manual",
+ [proxy]: "foo",
+ [proxy + "Port"]: 1234,
+ };
+
+ if (proxy == "socksProxy") {
+ manual.socksProxyVersion = 42;
+ expected.socksProxyVersion = 42;
+ }
+ deepEqual(expected, session.Proxy.fromJSON(manual).toJSON());
+ }
+
+ Assert.throws(() => session.Proxy.fromJSON(
+ {proxyType: "manual", socksProxy: "foo", socksProxyPort: 1234}),
+ InvalidArgumentError);
+
+ run_next_test();
+});
+
+add_test(function test_Capabilities_ctor() {
+ let caps = new session.Capabilities();
+ ok(caps.has("browserName"));
+ ok(caps.has("browserVersion"));
+ ok(caps.has("platformName"));
+ ok(caps.has("platformVersion"));
+ equal(session.PageLoadStrategy.Normal, caps.get("pageLoadStrategy"));
+ equal(false, caps.get("acceptInsecureCerts"));
+ ok(caps.get("timeouts") instanceof session.Timeouts);
+ ok(caps.get("proxy") instanceof session.Proxy);
+
+ ok(caps.has("rotatable"));
+
+ equal(0, caps.get("specificationLevel"));
+ ok(caps.has("moz:processID"));
+ ok(caps.has("moz:profile"));
+ equal(false, caps.get("moz:accessibilityChecks"));
+
+ run_next_test();
+});
+
+add_test(function test_Capabilities_toString() {
+ equal("[object session.Capabilities]", new session.Capabilities().toString());
+
+ run_next_test();
+});
+
+add_test(function test_Capabilities_toJSON() {
+ let caps = new session.Capabilities();
+ let json = caps.toJSON();
+
+ equal(caps.get("browserName"), json.browserName);
+ equal(caps.get("browserVersion"), json.browserVersion);
+ equal(caps.get("platformName"), json.platformName);
+ equal(caps.get("platformVersion"), json.platformVersion);
+ equal(caps.get("pageLoadStrategy"), json.pageLoadStrategy);
+ equal(caps.get("acceptInsecureCerts"), json.acceptInsecureCerts);
+ deepEqual(caps.get("timeouts").toJSON(), json.timeouts);
+ equal(undefined, json.proxy);
+
+ equal(caps.get("rotatable"), json.rotatable);
+
+ equal(caps.get("specificationLevel"), json.specificationLevel);
+ equal(caps.get("moz:processID"), json["moz:processID"]);
+ equal(caps.get("moz:profile"), json["moz:profile"]);
+ equal(caps.get("moz:accessibilityChecks"), json["moz:accessibilityChecks"]);
+
+ run_next_test();
+});
+
+add_test(function test_Capabilities_fromJSON() {
+ const {fromJSON} = session.Capabilities;
+
+ // plain
+ for (let typ of [{}, null, undefined]) {
+ ok(fromJSON(typ, {merge: true}).has("browserName"));
+ ok(fromJSON(typ, {merge: false}).has("browserName"));
+ }
+ for (let typ of [true, 42, "foo", []]) {
+ Assert.throws(() =>
+ fromJSON(typ, {merge: true}), InvalidArgumentError);
+ Assert.throws(() =>
+ fromJSON(typ, {merge: false}), InvalidArgumentError);
+ }
+
+ // merging
+ let desired = {"moz:accessibilityChecks": false};
+ let required = {"moz:accessibilityChecks": true};
+ let matched = fromJSON(
+ {desiredCapabilities: desired, requiredCapabilities: required},
+ {merge: true});
+ ok(matched.has("moz:accessibilityChecks"));
+ equal(true, matched.get("moz:accessibilityChecks"));
+
+ // desiredCapabilities/requriedCapabilities types
+ for (let typ of [undefined, null, {}]) {
+ ok(fromJSON({desiredCapabilities: typ}, {merge: true}));
+ ok(fromJSON({requiredCapabilities: typ}, {merge: true}));
+ }
+ for (let typ of [true, 42, "foo", []]) {
+ Assert.throws(() => fromJSON({desiredCapabilities: typ}, {merge: true}));
+ Assert.throws(() => fromJSON({requiredCapabilities: typ}, {merge: true}));
+ }
+
+ // matching
+ let caps = new session.Capabilities();
+
+ ok(fromJSON({browserName: caps.get("browserName")}));
+ ok(fromJSON({browserName: null}));
+ ok(fromJSON({browserName: undefined}));
+ ok(fromJSON({browserName: "*"}));
+ Assert.throws(() => fromJSON({browserName: "foo"}));
+
+ ok(fromJSON({browserVersion: caps.get("browserVersion")}));
+ ok(fromJSON({browserVersion: null}));
+ ok(fromJSON({browserVersion: undefined}));
+ ok(fromJSON({browserVersion: "*"}));
+ Assert.throws(() => fromJSON({browserVersion: "foo"}));
+
+ ok(fromJSON({platformName: caps.get("platformName")}));
+ ok(fromJSON({platformName: null}));
+ ok(fromJSON({platformName: undefined}));
+ ok(fromJSON({platformName: "*"}));
+ Assert.throws(() => fromJSON({platformName: "foo"}));
+
+ ok(fromJSON({platformVersion: caps.get("platformVersion")}));
+ ok(fromJSON({platformVersion: null}));
+ ok(fromJSON({platformVersion: undefined}));
+ ok(fromJSON({platformVersion: "*"}));
+ Assert.throws(() => fromJSON({platformVersion: "foo"}));
+
+ caps = fromJSON({acceptInsecureCerts: true});
+ equal(true, caps.get("acceptInsecureCerts"));
+ caps = fromJSON({acceptInsecureCerts: false});
+ equal(false, caps.get("acceptInsecureCerts"));
+ Assert.throws(() => fromJSON({acceptInsecureCerts: "foo"}));
+
+ for (let strategy of Object.values(session.PageLoadStrategy)) {
+ caps = fromJSON({pageLoadStrategy: strategy});
+ equal(strategy, caps.get("pageLoadStrategy"));
+ }
+ Assert.throws(() => fromJSON({pageLoadStrategy: "foo"}));
+
+ let proxyConfig = {proxyType: "manual"};
+ caps = fromJSON({proxy: proxyConfig});
+ equal("manual", caps.get("proxy").proxyType);
+
+ let timeoutsConfig = {implicit: 123};
+ caps = fromJSON({timeouts: timeoutsConfig});
+ equal(123, caps.get("timeouts").implicit);
+
+ equal(0, caps.get("specificationLevel"));
+ caps = fromJSON({specificationLevel: 123});
+ equal(123, caps.get("specificationLevel"));
+ Assert.throws(() => fromJSON({specificationLevel: "foo"}));
+ Assert.throws(() => fromJSON({specificationLevel: -1}));
+
+ caps = fromJSON({"moz:accessibilityChecks": true});
+ equal(true, caps.get("moz:accessibilityChecks"));
+ caps = fromJSON({"moz:accessibilityChecks": false});
+ equal(false, caps.get("moz:accessibilityChecks"));
+ Assert.throws(() => fromJSON({"moz:accessibilityChecks": "foo"}));
+
+ run_next_test();
+});
+
+// use session.Proxy.toJSON to test marshal
+add_test(function test_marshal() {
+ let proxy = new session.Proxy();
+
+ // drop empty fields
+ deepEqual({}, proxy.toJSON());
+ proxy.proxyType = "manual";
+ deepEqual({proxyType: "manual"}, proxy.toJSON());
+ proxy.proxyType = null;
+ deepEqual({}, proxy.toJSON());
+ proxy.proxyType = undefined;
+ deepEqual({}, proxy.toJSON());
+
+ // iterate over object literals
+ proxy.proxyType = {foo: "bar"};
+ deepEqual({proxyType: {foo: "bar"}}, proxy.toJSON());
+
+ // iterate over complex object that implement toJSON
+ proxy.proxyType = new session.Proxy();
+ deepEqual({}, proxy.toJSON());
+ proxy.proxyType.proxyType = "manual";
+ deepEqual({proxyType: {proxyType: "manual"}}, proxy.toJSON());
+
+ // drop objects with no entries
+ proxy.proxyType = {foo: {}};
+ deepEqual({}, proxy.toJSON());
+ proxy.proxyType = {foo: new session.Proxy()};
+ deepEqual({}, proxy.toJSON());
+
+ run_next_test();
+});
--- a/testing/marionette/unit.ini
+++ b/testing/marionette/unit.ini
@@ -8,8 +8,9 @@
skip-if = appname == "thunderbird"
[test_action.js]
[test_assert.js]
[test_element.js]
[test_error.js]
[test_message.js]
[test_navigate.js]
+[test_session.js]