From c67cbf8a8c637e428b11a812823aaee63eb5f02e Mon Sep 17 00:00:00 2001 From: Andreas Tolfsen Date: Sat, 31 Dec 2016 12:27:13 +0000 Subject: [PATCH] Bug 1326534 - Deploy WebDriver conforming capabilities in Marionette; r=automatedtester,maja_zf,whimboo This change removes session capability processing from testing/marionette/driver.js and replaces it with testing/marionette/session.js and `session.Capabilities`. Session timeout durations used to be stored in properties exposed directly on the `GeckoDriver` prototype, but these are now represented by `GeckoDriver#timeouts`, which is a pointer (getter) of `GeckoDriver#sessionCapabilities#timeouts`. The same is true for other session-scoped state. Since capabilities parsing is not unique to starting a new session, the errors thrown by `session.Capabilities.fromJSON` are re-thrown in `GeckoDriver#newSession` since it is required that we return a `SessionNotCreatedError` on parsing them during session creation. MozReview-Commit-ID: I3Xu2v71n4S --HG-- extra : rebase_source : 40cef31adf238bef021a7c7c2713016a34f35920 --- testing/marionette/driver.js | 249 ++++-------------- .../tests/unit/test_capabilities.py | 194 ++++++++++++-- 2 files changed, 228 insertions(+), 215 deletions(-) diff --git a/testing/marionette/driver.js b/testing/marionette/driver.js index 047ade803503..db37ce7de279 100644 --- a/testing/marionette/driver.js +++ b/testing/marionette/driver.js @@ -34,6 +34,7 @@ Cu.import("chrome://marionette/content/legacyaction.js"); Cu.import("chrome://marionette/content/logging.js"); Cu.import("chrome://marionette/content/modal.js"); Cu.import("chrome://marionette/content/proxy.js"); +Cu.import("chrome://marionette/content/session.js"); Cu.import("chrome://marionette/content/simpletest.js"); this.EXPORTED_SYMBOLS = ["GeckoDriver", "Context"]; @@ -123,15 +124,6 @@ this.GeckoDriver = function (appName, server) { this.observing = null; this._browserIds = new WeakMap(); - // user-defined timeouts - this.scriptTimeout = 30000; // 30 seconds - this.searchTimeout = null; - this.pageTimeout = 300000; // five minutes - - // Unsigned or invalid TLS certificates will be ignored if secureTLS - // is set to false. - this.secureTLS = true; - // The curent context decides if commands should affect chrome- or // content space. this.context = Context.CONTENT; @@ -147,24 +139,7 @@ this.GeckoDriver = function (appName, server) { this.marionetteLog = new logging.ContentLogger(); this.testName = null; - this.sessionCapabilities = { - // mandated capabilities - "browserName": Services.appinfo.name.toLowerCase(), - "browserVersion": Services.appinfo.version, - "platformName": Services.sysinfo.getProperty("name").toLowerCase(), - "platformVersion": Services.sysinfo.getProperty("version"), - "acceptInsecureCerts": !this.secureTLS, - - // supported features - "rotatable": this.appName == "B2G", - "proxy": {}, - - // proprietary extensions - "specificationLevel": 0, - "moz:processID": Services.appinfo.processID, - "moz:profile": Services.dirsvc.get("ProfD", Ci.nsIFile).path, - "moz:accessibilityChecks": false, - }; + this.sessionCapabilities = new session.Capabilities(); this.mm = globalMessageManager; this.listener = proxy.toListener(() => this.mm, this.sendAsync.bind(this)); @@ -182,7 +157,9 @@ this.GeckoDriver = function (appName, server) { }; Object.defineProperty(GeckoDriver.prototype, "a11yChecks", { - get: function () { return this.sessionCapabilities["moz:accessibilityChecks"]; } + get: function () { + return this.sessionCapabilities.get("moz:accessibilityChecks"); + } }); GeckoDriver.prototype.QueryInterface = XPCOMUtils.generateQI([ @@ -466,13 +443,16 @@ GeckoDriver.prototype.registerBrowser = function (id, be) { this.wins.set(reg.id, listenerWindow); if (nullPrevious && (this.curBrowser.curFrameId !== null)) { - this.sendAsync("newSession", this.sessionCapabilities, this.newSessionCommandId); + this.sendAsync( + "newSession", + this.sessionCapabilities.toJSON(), + this.newSessionCommandId); if (this.curBrowser.isNewSession) { this.newSessionCommandId = null; } } - return [reg, mainContent, this.sessionCapabilities]; + return [reg, mainContent, this.sessionCapabilities.toJSON()]; }; GeckoDriver.prototype.registerPromise = function() { @@ -511,6 +491,28 @@ GeckoDriver.prototype.listeningPromise = function() { }); }; +Object.defineProperty(GeckoDriver.prototype, "timeouts", { + get: function () { + return this.sessionCapabilities.get("timeouts"); + }, + + set: function (newTimeouts) { + this.sessionCapabilities.set("timeouts", newTimeouts); + }, +}); + +Object.defineProperty(GeckoDriver.prototype, "secureTLS", { + get: function () { + return !this.sessionCapabilities.get("acceptInsecureCerts"); + } +}); + +Object.defineProperty(GeckoDriver.prototype, "proxy", { + get: function () { + return this.sessionCapabilities.get("proxy"); + } +}); + /** Create a new session. */ GeckoDriver.prototype.newSession = function*(cmd, resp) { if (this.sessionId) { @@ -520,19 +522,27 @@ GeckoDriver.prototype.newSession = function*(cmd, resp) { this.sessionId = cmd.parameters.sessionId || cmd.parameters.session_id || element.generateUUID(); - this.newSessionCommandId = cmd.id; - this.setSessionCapabilities(cmd.parameters.capabilities); - this.scriptTimeout = 10000; + try { + this.sessionCapabilities = session.Capabilities.fromJSON( + cmd.parameters.capabilities, {merge: true}); + logger.config("Matched capabilities: " + + JSON.stringify(this.sessionCapabilities)); + } catch (e) { + throw new SessionNotCreatedError(e); + } - this.secureTLS = !this.sessionCapabilities.acceptInsecureCerts; if (!this.secureTLS) { logger.warn("TLS certificate errors will be ignored for this session"); let acceptAllCerts = new cert.InsecureSweepingOverride(); cert.installOverride(acceptAllCerts); } + if (this.proxy.init()) { + logger.info("Proxy settings initialised: " + JSON.stringify(this.proxy)); + } + // If we are testing accessibility with marionette, start a11y service in // chrome first. This will ensure that we do not have any content-only // services hanging around. @@ -623,128 +633,6 @@ GeckoDriver.prototype.getSessionCapabilities = function (cmd, resp) { resp.body.capabilities = this.sessionCapabilities; }; -/** - * Update the sessionCapabilities object with the keys that have been - * passed in when a new session is created. - * - * This is not a public API, only available when a new session is - * created. - * - * @param {Object} newCaps - * Key/value dictionary to overwrite session's current capabilities. - */ -GeckoDriver.prototype.setSessionCapabilities = function (newCaps) { - const copy = (from, to={}) => { - let errors = []; - - // Remove any duplicates between required and desired in favour of the - // required capabilities - if (from !== null && from.desiredCapabilities) { - for (let cap in from.requiredCapabilities) { - if (from.desiredCapabilities[cap]) { - delete from.desiredCapabilities[cap]; - } - } - - // Let's remove the sessionCapabilities from desired capabilities - for (let cap in this.sessionCapabilities) { - if (from.desiredCapabilities && from.desiredCapabilities[cap]) { - delete from.desiredCapabilities[cap]; - } - } - } - - for (let key in from) { - switch (key) { - case "desiredCapabilities": - to = copy(from[key], to); - break; - - case "requiredCapabilities": - if (from[key].proxy) { - this.setUpProxy(from[key].proxy); - to.proxy = from[key].proxy; - delete from[key].proxy; - } - for (let caps in from[key]) { - if (from[key][caps] !== this.sessionCapabilities[caps]) { - errors.push(from[key][caps] + " does not equal " + - this.sessionCapabilities[caps]); - } - } - break; - - default: - to[key] = from[key]; - } - } - - if (Object.keys(errors).length == 0) { - return to; - } - - throw new SessionNotCreatedError( - `Not all requiredCapabilities could be met: ${JSON.stringify(errors)}`); - }; - - // clone, overwrite, and set - let caps = copy(this.sessionCapabilities); - caps = copy(newCaps, caps); - logger.config("Changing capabilities: " + JSON.stringify(caps)); - this.sessionCapabilities = caps; -}; - -GeckoDriver.prototype.setUpProxy = function (proxy) { - logger.config("User-provided proxy settings: " + JSON.stringify(proxy)); - - assert.object(proxy); - if (!proxy.hasOwnProperty("proxyType")) { - throw new InvalidArgumentError(); - } - switch (proxy.proxyType.toUpperCase()) { - case "MANUAL": - Preferences.set("network.proxy.type", 1); - if (proxy.httpProxy && proxy.httpProxyPort){ - Preferences.set("network.proxy.http", proxy.httpProxy); - Preferences.set("network.proxy.http_port", proxy.httpProxyPort); - } - if (proxy.sslProxy && proxy.sslProxyPort){ - Preferences.set("network.proxy.ssl", proxy.sslProxy); - Preferences.set("network.proxy.ssl_port", proxy.sslProxyPort); - } - if (proxy.ftpProxy && proxy.ftpProxyPort) { - Preferences.set("network.proxy.ftp", proxy.ftpProxy); - Preferences.set("network.proxy.ftp_port", proxy.ftpProxyPort); - } - if (proxy.socksProxy) { - Preferences.set("network.proxy.socks", proxy.socksProxy); - Preferences.set("network.proxy.socks_port", proxy.socksProxyPort); - if (proxy.socksVersion) { - Preferences.set("network.proxy.socks_version", proxy.socksVersion); - } - } - break; - - case "PAC": - Preferences.set("network.proxy.type", 2); - Preferences.set("network.proxy.autoconfig_url", proxy.proxyAutoconfigUrl); - break; - - case "AUTODETECT": - Preferences.set("network.proxy.type", 4); - break; - - case "SYSTEM": - Preferences.set("network.proxy.type", 5); - break; - - case "NOPROXY": - default: - Preferences.set("network.proxy.type", 0); - break; - } -}; - /** * Log message. Accepts user defined log-level. * @@ -838,7 +726,7 @@ GeckoDriver.prototype.getContext = function (cmd, resp) { */ GeckoDriver.prototype.executeScript = function*(cmd, resp) { let {script, args, scriptTimeout} = cmd.parameters; - scriptTimeout = scriptTimeout || this.scriptTimeout; + scriptTimeout = scriptTimeout || this.timeouts.script; let opts = { sandboxName: cmd.parameters.sandbox, @@ -911,7 +799,7 @@ GeckoDriver.prototype.executeScript = function*(cmd, resp) { */ GeckoDriver.prototype.executeAsyncScript = function* (cmd, resp) { let {script, args, scriptTimeout} = cmd.parameters; - scriptTimeout = scriptTimeout || this.scriptTimeout; + scriptTimeout = scriptTimeout || this.timeouts.script; let opts = { sandboxName: cmd.parameters.sandbox, @@ -961,7 +849,7 @@ GeckoDriver.prototype.execute_ = function (script, args, timeout, opts = {}) { */ GeckoDriver.prototype.executeJSScript = function* (cmd, resp) { let {script, args, scriptTimeout} = cmd.parameters; - scriptTimeout = scriptTimeout || this.scriptTimeout; + scriptTimeout = scriptTimeout || this.timeouts.script; let opts = { filename: cmd.parameters.filename, @@ -1023,7 +911,7 @@ GeckoDriver.prototype.get = function*(cmd, resp) { let url = cmd.parameters.url; - let get = this.listener.get({url: url, pageTimeout: this.pageTimeout}); + let get = this.listener.get({url: url, pageTimeout: this.timeouts.pageLoad}); // TODO(ato): Bug 1242595 let id = this.listener.activeMessageId; @@ -1032,7 +920,7 @@ GeckoDriver.prototype.get = function*(cmd, resp) { // send errors. this.curBrowser.pendingCommands.push(() => { cmd.parameters.command_id = id; - cmd.parameters.pageTimeout = this.pageTimeout; + cmd.parameters.pageTimeout = this.timeouts.pageLoad; this.mm.broadcastAsyncMessage( "Marionette:pollForReadyState" + this.curBrowser.curFrameId, cmd.parameters); @@ -1562,11 +1450,7 @@ GeckoDriver.prototype.switchToFrame = function* (cmd, resp) { }; GeckoDriver.prototype.getTimeouts = function (cmd, resp) { - return { - "implicit": this.searchTimeout, - "script": this.scriptTimeout, - "page load": this.pageTimeout, - }; + return this.timeouts; }; /** @@ -1583,36 +1467,19 @@ GeckoDriver.prototype.getTimeouts = function (cmd, resp) { GeckoDriver.prototype.setTimeouts = function (cmd, resp) { // backwards compatibility with old API // that accepted a dictionary {type: , ms: } - let timeouts = {}; + let json = {}; if (typeof cmd.parameters == "object" && "type" in cmd.parameters && "ms" in cmd.parameters) { logger.warn("Using deprecated data structure for setting timeouts"); - timeouts = {[cmd.parameters.type]: parseInt(cmd.parameters.ms)}; + json = {[cmd.parameters.type]: parseInt(cmd.parameters.ms)}; } else { - timeouts = cmd.parameters; + json = cmd.parameters; } - for (let [typ, ms] of Object.entries(timeouts)) { - assert.positiveInteger(ms); - - switch (typ) { - case "implicit": - this.searchTimeout = ms; - break; - - case "script": - this.scriptTimeout = ms; - break; - - case "page load": - this.pageTimeout = ms; - break; - - default: - throw new InvalidArgumentError(); - } - } + // merge with existing timeouts + let merged = Object.assign(this.timeouts.toJSON(), json); + this.timeouts = session.Timeouts.fromJSON(merged); }; /** Single tap. */ @@ -1730,7 +1597,7 @@ GeckoDriver.prototype.findElement = function*(cmd, resp) { let expr = cmd.parameters.value; let opts = { startNode: cmd.parameters.element, - timeout: this.searchTimeout, + timeout: this.timeouts.implicit, all: false, }; @@ -1773,7 +1640,7 @@ GeckoDriver.prototype.findElements = function*(cmd, resp) { let expr = cmd.parameters.value; let opts = { startNode: cmd.parameters.element, - timeout: this.searchTimeout, + timeout: this.timeouts.implicit, all: true, }; diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_capabilities.py b/testing/marionette/harness/marionette_harness/tests/unit/test_capabilities.py index 23a0bab58f3c..c596432e4e00 100644 --- a/testing/marionette/harness/marionette_harness/tests/unit/test_capabilities.py +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_capabilities.py @@ -26,12 +26,17 @@ class TestCapabilities(MarionetteTestCase): self.assertIn("platformName", self.caps) self.assertIn("platformVersion", self.caps) self.assertIn("acceptInsecureCerts", self.caps) + self.assertIn("timeouts", self.caps) self.assertEqual(self.caps["browserName"], self.appinfo["name"].lower()) self.assertEqual(self.caps["browserVersion"], self.appinfo["version"]) self.assertEqual(self.caps["platformName"], self.os_name) self.assertEqual(self.caps["platformVersion"], self.os_version) self.assertFalse(self.caps["acceptInsecureCerts"]) + self.assertDictEqual(self.caps["timeouts"], + {"implicit": 0, + "page load": 300000, + "script": 30000}) def test_supported_features(self): self.assertIn("rotatable", self.caps) @@ -52,26 +57,16 @@ class TestCapabilities(MarionetteTestCase): self.assertIn("specificationLevel", self.caps) self.assertEqual(self.caps["specificationLevel"], 0) - def test_we_can_pass_in_capabilities_on_session_start(self): - self.marionette.delete_session() - capabilities = {"desiredCapabilities": {"somethingAwesome": "cake"}} - self.marionette.start_session(capabilities) - caps = self.marionette.session_capabilities - self.assertIn("somethingAwesome", caps) - def test_set_specification_level(self): self.marionette.delete_session() - self.marionette.start_session({"specificationLevel": 1}) + self.marionette.start_session({"desiredCapabilities": {"specificationLevel": 2}}) caps = self.marionette.session_capabilities - self.assertEqual(1, caps["specificationLevel"]) + self.assertEqual(2, caps["specificationLevel"]) - def test_we_dont_overwrite_server_capabilities(self): self.marionette.delete_session() - capabilities = {"desiredCapabilities": {"browserName": "ChocolateCake"}} - self.marionette.start_session(capabilities) + self.marionette.start_session({"requiredCapabilities": {"specificationLevel": 3}}) caps = self.marionette.session_capabilities - self.assertEqual(caps["browserName"], self.appinfo["name"].lower(), - "This should have appname not ChocolateCake.") + self.assertEqual(3, caps["specificationLevel"]) def test_we_can_pass_in_required_capabilities_on_session_start(self): self.marionette.delete_session() @@ -80,21 +75,172 @@ class TestCapabilities(MarionetteTestCase): caps = self.marionette.session_capabilities self.assertIn("browserName", caps) - def test_we_pass_in_required_capability_we_cant_fulfil_raises_exception(self): - self.marionette.delete_session() - capabilities = {"requiredCapabilities": {"browserName": "CookiesAndCream"}} - try: - self.marionette.start_session(capabilities) - self.fail("Marionette Should have throw an exception") - except SessionNotCreatedException as e: - # We want an exception - self.assertIn("CookiesAndCream does not equal", str(e)) - # Start a new session just to make sure we leave the browser in the # same state it was before it started the test self.marionette.start_session() + def test_capability_types(self): + for value in ["", "invalid", True, 42, []]: + print("testing value {}".format(value)) + with self.assertRaises(SessionNotCreatedException): + print(" with desiredCapabilities") + self.marionette.delete_session() + self.marionette.start_session({"desiredCapabilities": value}) + with self.assertRaises(SessionNotCreatedException): + print(" with requiredCapabilities") + self.marionette.delete_session() + self.marionette.start_session({"requiredCapabilities": value}) + def test_we_get_valid_uuid4_when_creating_a_session(self): self.assertNotIn("{", self.marionette.session_id, "Session ID has {{}} in it: {}".format( self.marionette.session_id)) + + +class TestCapabilityMatching(MarionetteTestCase): + allowed = [None, "*"] + disallowed = ["", 42, True, {}, []] + + def setUp(self): + MarionetteTestCase.setUp(self) + self.browser_name = self.marionette.session_capabilities["browserName"] + self.platform_name = self.marionette.session_capabilities["platformName"] + self.marionette.delete_session() + + def test_browser_name_desired(self): + self.marionette.start_session({"desiredCapabilities": {"browserName": self.browser_name}}) + self.assertEqual(self.marionette.session_capabilities["browserName"], self.browser_name) + + def test_browser_name_required(self): + self.marionette.start_session({"requiredCapabilities": {"browserName": self.browser_name}}) + self.assertEqual(self.marionette.session_capabilities["browserName"], self.browser_name) + + def test_browser_name_desired_allowed_types(self): + for typ in self.allowed: + self.marionette.delete_session() + self.marionette.start_session({"desiredCapabilities": {"browserName": typ}}) + self.assertEqual(self.marionette.session_capabilities["browserName"], self.browser_name) + + def test_browser_name_desired_disallowed_types(self): + for typ in self.disallowed: + with self.assertRaises(SessionNotCreatedException): + self.marionette.start_session({"desiredCapabilities": {"browserName": typ}}) + + def test_browser_name_required_allowed_types(self): + for typ in self.allowed: + self.marionette.delete_session() + self.marionette.start_session({"requiredCapabilities": {"browserName": typ}}) + self.assertEqual(self.marionette.session_capabilities["browserName"], self.browser_name) + + def test_browser_name_requried_disallowed_types(self): + for typ in self.disallowed: + with self.assertRaises(SessionNotCreatedException): + self.marionette.start_session({"requiredCapabilities": {"browserName": typ}}) + + def test_browser_name_prefers_required(self): + caps = {"desiredCapabilities": {"browserName": "invalid"}, + "requiredCapabilities": {"browserName": "*"}} + self.marionette.start_session(caps) + + def test_browser_name_error_on_invalid_required(self): + with self.assertRaises(SessionNotCreatedException): + caps = {"desiredCapabilities": {"browserName": "*"}, + "requiredCapabilities": {"browserName": "invalid"}} + self.marionette.start_session(caps) + + # TODO(ato): browser version comparison not implemented yet + + def test_platform_name_desired(self): + self.marionette.start_session({"desiredCapabilities": {"platformName": self.platform_name}}) + self.assertEqual(self.marionette.session_capabilities["platformName"], self.platform_name) + + def test_platform_name_required(self): + self.marionette.start_session({"requiredCapabilities": {"platformName": self.platform_name}}) + self.assertEqual(self.marionette.session_capabilities["platformName"], self.platform_name) + + def test_platform_name_desired_allowed_types(self): + for typ in self.allowed: + self.marionette.delete_session() + self.marionette.start_session({"desiredCapabilities": {"platformName": typ}}) + self.assertEqual(self.marionette.session_capabilities["platformName"], self.platform_name) + + def test_platform_name_desired_disallowed_types(self): + for typ in self.disallowed: + with self.assertRaises(SessionNotCreatedException): + self.marionette.start_session({"desiredCapabilities": {"platformName": typ}}) + + def test_platform_name_required_allowed_types(self): + for typ in self.allowed: + self.marionette.delete_session() + self.marionette.start_session({"requiredCapabilities": {"platformName": typ}}) + self.assertEqual(self.marionette.session_capabilities["platformName"], self.platform_name) + + def test_platform_name_requried_disallowed_types(self): + for typ in self.disallowed: + with self.assertRaises(SessionNotCreatedException): + self.marionette.start_session({"requiredCapabilities": {"platformName": typ}}) + + def test_platform_name_prefers_required(self): + caps = {"desiredCapabilities": {"platformName": "invalid"}, + "requiredCapabilities": {"platformName": "*"}} + self.marionette.start_session(caps) + + def test_platform_name_error_on_invalid_required(self): + with self.assertRaises(SessionNotCreatedException): + caps = {"desiredCapabilities": {"platformName": "*"}, + "requiredCapabilities": {"platformName": "invalid"}} + self.marionette.start_session(caps) + + # TODO(ato): platform version comparison not imlpemented yet + + def test_accept_insecure_certs(self): + for capability_type in ["desiredCapabilities", "requiredCapabilities"]: + print("testing {}".format(capability_type)) + for value in ["", 42, {}, []]: + print(" type {}".format(type(value))) + with self.assertRaises(SessionNotCreatedException): + self.marionette.start_session({capability_type: {"acceptInsecureCerts": value}}) + + self.marionette.delete_session() + self.marionette.start_session({"desiredCapabilities": {"acceptInsecureCerts": True}}) + self.assertTrue(self.marionette.session_capabilities["acceptInsecureCerts"]) + self.marionette.delete_session() + self.marionette.start_session({"requiredCapabilities": {"acceptInsecureCerts": True}}) + + self.assertTrue(self.marionette.session_capabilities["acceptInsecureCerts"]) + + def test_page_load_strategy(self): + for strategy in ["none", "eager", "normal"]: + print("valid strategy {}".format(strategy)) + self.marionette.delete_session() + self.marionette.start_session({"desiredCapabilities": {"pageLoadStrategy": strategy}}) + self.assertEqual(self.marionette.session_capabilities["pageLoadStrategy"], strategy) + + for value in ["", "EAGER", True, 42, {}, []]: + print("invalid strategy {}".format(value)) + with self.assertRaises(SessionNotCreatedException): + self.marionette.start_session({"desiredCapabilities": {"pageLoadStrategy": value}}) + + def test_proxy_default(self): + self.marionette.start_session() + self.assertNotIn("proxy", self.marionette.session_capabilities) + + def test_proxy_desired(self): + self.marionette.start_session({"desiredCapabilities": {"proxy": {"proxyType": "manual"}}}) + self.assertIn("proxy", self.marionette.session_capabilities) + self.assertEqual(self.marionette.session_capabilities["proxy"]["proxyType"], "manual") + self.assertEqual(self.marionette.get_pref("network.proxy.type"), 1) + + def test_proxy_required(self): + self.marionette.start_session({"requiredCapabilities": {"proxy": {"proxyType": "manual"}}}) + self.assertIn("proxy", self.marionette.session_capabilities) + self.assertEqual(self.marionette.session_capabilities["proxy"]["proxyType"], "manual") + self.assertEqual(self.marionette.get_pref("network.proxy.type"), 1) + + def test_timeouts(self): + timeouts = {u"implicit": 123, u"page load": 456, u"script": 789} + caps = {"desiredCapabilities": {"timeouts": timeouts}} + self.marionette.start_session(caps) + self.assertIn("timeouts", self.marionette.session_capabilities) + self.assertDictEqual(self.marionette.session_capabilities["timeouts"], timeouts) + self.assertDictEqual(self.marionette._send_message("getTimeouts"), timeouts)