Bug 1326534 - Deploy WebDriver conforming capabilities in Marionette; r?automatedtester,whimboo,maja_zf draft
authorAndreas Tolfsen <ato@mozilla.com>
Sat, 31 Dec 2016 12:27:13 +0000
changeset 457111 0ab1430892e3fa4fbca53bf5b1cd65e88f481e1a
parent 457110 204c99867038c12b6064662c93dc01b4107dff57
child 457112 6c6c9d713b6bbc60aa7ab498f92a1f88b691f252
push id40670
push userbmo:ato@mozilla.com
push dateFri, 06 Jan 2017 18:52:13 +0000
reviewersautomatedtester, whimboo, maja_zf
bugs1326534
milestone53.0a1
Bug 1326534 - Deploy WebDriver conforming capabilities in Marionette; r?automatedtester,whimboo,maja_zf 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
testing/marionette/driver.js
testing/marionette/harness/marionette_harness/tests/unit/test_capabilities.py
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -29,16 +29,17 @@ Cu.import("chrome://marionette/content/e
 Cu.import("chrome://marionette/content/evaluate.js");
 Cu.import("chrome://marionette/content/event.js");
 Cu.import("chrome://marionette/content/interaction.js");
 Cu.import("chrome://marionette/content/l10n.js");
 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"];
 
 var FRAME_SCRIPT = "chrome://marionette/content/listener.js";
 const BROWSER_STARTUP_FINISHED = "browser-delayed-startup-finished";
 const CLICK_TO_START_PREF = "marionette.debugging.clicktostart";
 const CONTENT_LISTENER_PREF = "marionette.contentListener";
@@ -118,58 +119,32 @@ this.GeckoDriver = function (appName, se
   this.mainContentFrameId = null;
   this.mozBrowserClose = null;
   this.currentFrameElement = null;
   // frame ID of the current remote frame, used for mozbrowserclose events
   this.oopFrameId = null;
   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;
 
   this.importedScripts = new evaluate.ScriptStorageService(
       [Context.CHROME, Context.CONTENT]);
   this.sandboxes = new Sandboxes(() => this.getCurrentWindow());
   this.legacyactions = new legacyaction.Chain();
 
   this.timer = null;
   this.inactivityTimer = null;
 
   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));
 
   // always keep weak reference to current dialogue
   this.dialog = null;
   let handleDialog = (subject, topic) => {
     let winr;
@@ -177,17 +152,19 @@ this.GeckoDriver = function (appName, se
       winr = Cu.getWeakReference(subject);
     }
     this.dialog = new modal.Dialog(() => this.curBrowser, winr);
   };
   modal.addHandler(handleDialog);
 };
 
 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([
   Ci.nsIMessageListener,
   Ci.nsIObserver,
   Ci.nsISupportsWeakReference,
 ]);
 
@@ -461,23 +438,26 @@ GeckoDriver.prototype.registerBrowser = 
   // set to true if we updated mainContentId
   mainContent = mainContent && this.curBrowser.mainContentId !== null;
   if (mainContent) {
     this.mainContentFrameId = this.curBrowser.curFrameId;
   }
 
   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() {
   const li = "Marionette:register";
 
   return new Promise(resolve => {
     let cb = msg => {
       let wid = msg.json.value;
@@ -506,38 +486,68 @@ GeckoDriver.prototype.listeningPromise =
     let cb = () => {
       this.mm.removeMessageListener(li, cb);
       resolve();
     };
     this.mm.addMessageListener(li, cb);
   });
 };
 
+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) {
     throw new SessionNotCreatedError("Maximum number of active sessions");
   }
 
   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.
   if (this.a11yChecks && accessibility.service) {
     logger.info("Preemptively starting accessibility service in Chrome");
   }
 
   let registerBrowsers = this.registerPromise();
@@ -619,138 +629,16 @@ GeckoDriver.prototype.newSession = funct
  * ("capabilities") to values, which may be of types boolean,
  * numerical or string.
  */
 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.
  *
  * @param {string} value
  *     Log message.
  * @param {string} level
  *     Arbitrary log level.
  */
 GeckoDriver.prototype.log = function (cmd, resp) {
@@ -833,17 +721,17 @@ GeckoDriver.prototype.getContext = funct
  * @throws ScriptTimeoutError
  *     If the script was interrupted due to reaching the {@code
  *     scriptTimeout} or default timeout.
  * @throws JavaScriptError
  *     If an Error was thrown whilst evaluating the script.
  */
 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,
     newSandbox: !!(typeof cmd.parameters.newSandbox == "undefined") ||
         cmd.parameters.newSandbox,
     filename: cmd.parameters.filename,
     line: cmd.parameters.line,
     debug: cmd.parameters.debug_script,
@@ -906,17 +794,17 @@ GeckoDriver.prototype.executeScript = fu
  * @throws ScriptTimeoutError
  *     If the script was interrupted due to reaching the {@code
  *     scriptTimeout} or default timeout.
  * @throws JavaScriptError
  *     If an Error was thrown whilst evaluating the script.
  */
 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,
     newSandbox: !!(typeof cmd.parameters.newSandbox == "undefined") ||
         cmd.parameters.newSandbox,
     filename: cmd.parameters.filename,
     line: cmd.parameters.line,
     debug: cmd.parameters.debug_script,
@@ -956,17 +844,17 @@ GeckoDriver.prototype.execute_ = functio
 /**
  * Execute pure JavaScript.  Used to execute simpletest harness tests,
  * which are like mochitests only injected using Marionette.
  *
  * Scripts are expected to call the {@code finish} global when done.
  */
 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,
     line: cmd.parameters.line,
     async: cmd.parameters.async,
   };
 
   switch (this.context) {
@@ -1018,26 +906,26 @@ GeckoDriver.prototype.executeJSScript = 
  * @param {string} url
  *     URL to navigate to.
  */
 GeckoDriver.prototype.get = function*(cmd, resp) {
   assert.content(this.context);
 
   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;
 
   // If a remoteness update interrupts our page load, this will never return
   // We need to re-issue this request to correctly poll for readyState and
   // 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);
   });
 
   yield get;
   this.curBrowser.browserForTab.focus();
 };
@@ -1557,67 +1445,46 @@ GeckoDriver.prototype.switchToFrame = fu
 
       yield registerBrowsers;
       yield browserListening;
     }
   }
 };
 
 GeckoDriver.prototype.getTimeouts = function (cmd, resp) {
-  return {
-    "implicit": this.searchTimeout,
-    "script": this.scriptTimeout,
-    "page load": this.pageTimeout,
-  };
+  return this.timeouts;
 };
 
 /**
  * Set timeout for page loading, searching, and scripts.
  *
  * @param {Object.<string, number>}
  *     Dictionary of timeout types and their new value, where all timeout
  *     types are optional.
  *
  * @throws {InvalidArgumentError}
  *     If timeout type key is unknown, or the value provided with it is
  *     not an integer.
  */
 GeckoDriver.prototype.setTimeouts = function (cmd, resp) {
   // backwards compatibility with old API
   // that accepted a dictionary {type: <string>, ms: <number>}
-  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. */
 GeckoDriver.prototype.singleTap = function*(cmd, resp) {
   let {id, x, y} = cmd.parameters;
 
   switch (this.context) {
     case Context.CHROME:
@@ -1725,17 +1592,17 @@ GeckoDriver.prototype.multiAction = func
  * @param {string} value
  *     Value the client is looking for.
  */
 GeckoDriver.prototype.findElement = function*(cmd, resp) {
   let strategy = cmd.parameters.using;
   let expr = cmd.parameters.value;
   let opts = {
     startNode: cmd.parameters.element,
-    timeout: this.searchTimeout,
+    timeout: this.timeouts.implicit,
     all: false,
   };
 
   switch (this.context) {
     case Context.CHROME:
       if (!SUPPORTED_STRATEGIES.has(strategy)) {
         throw new InvalidSelectorError(`Strategy not supported: ${strategy}`);
       }
@@ -1768,17 +1635,17 @@ GeckoDriver.prototype.findElement = func
  * @param {string} value
  *     Value the client is looking for.
  */
 GeckoDriver.prototype.findElements = function*(cmd, resp) {
   let strategy = cmd.parameters.using;
   let expr = cmd.parameters.value;
   let opts = {
     startNode: cmd.parameters.element,
-    timeout: this.searchTimeout,
+    timeout: this.timeouts.implicit,
     all: true,
   };
 
   switch (this.context) {
     case Context.CHROME:
       if (!SUPPORTED_STRATEGIES.has(strategy)) {
         throw new InvalidSelectorError(`Strategy not supported: ${strategy}`);
       }
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_capabilities.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_capabilities.py
@@ -21,22 +21,27 @@ class TestCapabilities(MarionetteTestCas
                 "return Services.sysinfo.getProperty('version')")
 
     def test_mandated_capabilities(self):
         self.assertIn("browserName", self.caps)
         self.assertIn("browserVersion", self.caps)
         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)
 
     def test_additional_capabilities(self):
         self.assertIn("moz:processID", self.caps)
         self.assertEqual(self.caps["moz:processID"], self.appinfo["processID"])
         self.assertEqual(self.marionette.process_id, self.appinfo["processID"])
@@ -46,54 +51,195 @@ class TestCapabilities(MarionetteTestCas
         self.assertEqual(self.caps["moz:profile"], current_profile)
         self.assertEqual(self.marionette.profile, current_profile)
 
         self.assertIn("moz:accessibilityChecks", self.caps)
         self.assertFalse(self.caps["moz:accessibilityChecks"])
         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()
         capabilities = {"requiredCapabilities": {"browserName": self.appinfo["name"].lower()}}
         self.marionette.start_session(capabilities)
         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)