Bug 1313865 - Centralise common Marionette assertions; r?automatedtester,maja_zf draft
authorAndreas Tolfsen <ato@mozilla.com>
Mon, 31 Oct 2016 22:00:21 +0000
changeset 437154 20faa46f2c43fbc2fa8b9a31c5e247acd6555097
parent 437137 d2e33428e45718bbeff192e80fd3a75ad264210d
child 437155 2a4e1b3f3a669e8d11a0e49b2d3973f411a9c780
push id35357
push userbmo:ato@mozilla.com
push dateThu, 10 Nov 2016 15:56:31 +0000
reviewersautomatedtester, maja_zf
bugs1313865
milestone52.0a1
Bug 1313865 - Centralise common Marionette assertions; r?automatedtester,maja_zf Many tests that result in throwing errors, amongst those many type- and platform checks, are repeated throughout the Marionette code base. This patch centralises the most common of these, typically reducing consumer calls from three to one line. Example usage: assert.defined(cmd.parameters.value); assert.postiveInteger(cmd.parameters.value, error.pprint`Expected 'value' (${value}) to be a signed integer`); // InvalidArgumentError: Expected 'value' ([object Object] {"foo": "bar"}) to be a positive integer MozReview-Commit-ID: BHOaDazeGer
testing/marionette/assert.js
testing/marionette/error.js
testing/marionette/jar.mn
testing/marionette/test_assert.js
testing/marionette/unit.ini
new file mode 100644
--- /dev/null
+++ b/testing/marionette/assert.js
@@ -0,0 +1,220 @@
+/* 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 {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+Cu.import("chrome://marionette/content/error.js");
+
+this.EXPORTED_SYMBOLS = ["assert"];
+
+const isFennec = () => AppConstants.platform == "android";
+const isB2G = () => AppConstants.MOZ_B2G;
+const isFirefox = () => Services.appinfo.name == "Firefox";
+
+/** Shorthands for common assertions made in Marionette. */
+this.assert = {};
+
+/**
+ * Asserts that the current browser is Firefox Desktop.
+ *
+ * @param {string=} msg
+ *     Custom error message.
+ *
+ * @throws {UnsupportedOperationError}
+ *     If current browser is not Firefox.
+ */
+assert.firefox = function(msg = "") {
+  msg = msg || "Expected Firefox";
+  assert.that(isFirefox, msg, UnsupportedOperationError)();
+};
+
+/**
+ * Asserts that the current browser is Fennec, or Firefox for Android.
+ *
+ * @param {string=} msg
+ *     Custom error message.
+ *
+ * @throws {UnsupportedOperationError}
+ *     If current browser is not Fennec.
+ */
+assert.fennec = function(msg = "") {
+  msg = msg || "Expected Fennec";
+  assert.that(isFennec, msg, UnsupportedOperationError)();
+};
+
+/**
+ * Asserts that the current browser is B2G.
+ *
+ * @param {string=} msg
+ *     Custom error message.
+ *
+ * @throws {UnsupportedOperationError}
+ *     If the current browser is not B2G.
+ */
+assert.b2g = function(msg = "") {
+  msg = msg || "Expected B2G"
+  assert.that(isB2G, msg, UnsupportedOperationError)();
+};
+
+/**
+ * Asserts that the current browser is a mobile browser, that is either
+ * B2G or Fennec.
+ *
+ * @param {string=} msg
+ *     Custom error message.
+ *
+ * @throws {UnsupportedOperationError}
+ *     If the current browser is not B2G or Fennec.
+ */
+assert.mobile = function(msg = "") {
+  msg = msg || "Expected Fennec or B2G";
+  assert.that(() => isFennec() || isB2G(), msg, UnsupportedOperationError)();
+};
+
+/**
+ * Asserts that |obj| is defined.
+ *
+ * @param {?} obj
+ *     Value to test.
+ * @param {string=} msg
+ *     Custom error message.
+ *
+ * @return {?}
+ *     |obj| is returned unaltered.
+ *
+ * @throws {InvalidArgumentError}
+ *     If |obj| is not defined.
+ */
+assert.defined = function(obj, msg = "") {
+  msg = msg || error.pprint`Expected ${obj} to be defined`;
+  return assert.that(o => typeof o != "undefined", msg)(obj);
+};
+
+/**
+ * Asserts that |obj| is an integer.
+ *
+ * @param {?} obj
+ *     Value to test.
+ * @param {string=} msg
+ *     Custom error message.
+ *
+ * @return {number}
+ *     |obj| is returned unaltered.
+ *
+ * @throws {InvalidArgumentError}
+ *     If |obj| is not an integer.
+ */
+assert.integer = function(obj, msg = "") {
+  msg = msg || error.pprint`Expected ${obj} to be an integer`;
+  return assert.that(Number.isInteger, msg)(obj);
+};
+
+/**
+ * Asserts that |obj| is a positive integer.
+ *
+ * @param {?} obj
+ *     Value to test.
+ * @param {string=} msg
+ *     Custom error message.
+ *
+ * @return {number}
+ *     |obj| is returned unaltered.
+ *
+ * @throws {InvalidArgumentError}
+ *     If |obj| is not a positive integer.
+ */
+assert.positiveInteger = function(obj, msg = "") {
+  assert.integer(obj, msg);
+  msg = msg || error.pprint`Expected ${obj} to be >= 0`;
+  return assert.that(n => n >= 0, msg)(obj);
+};
+
+/**
+ * Asserts that |obj| is a boolean.
+ *
+ * @param {?} obj
+ *     Value to test.
+ * @param {string=} msg
+ *     Custom error message.
+ *
+ * @return {boolean}
+ *     |obj| is returned unaltered.
+ *
+ * @throws {InvalidArgumentError}
+ *     If |obj| is not a boolean.
+ */
+assert.boolean = function(obj, msg = "") {
+  msg = msg || error.pprint`Expected ${obj} to be boolean`;
+  return assert.that(b => typeof b == "boolean", msg)(obj);
+};
+
+/**
+ * Asserts that |obj| is a string.
+ *
+ * @param {?} obj
+ *     Value to test.
+ * @param {string=} msg
+ *     Custom error message.
+ *
+ * @return {string}
+ *     |obj| is returned unaltered.
+ *
+ * @throws {InvalidArgumentError}
+ *     If |obj| is not a string.
+ */
+assert.string = function(obj, msg = "") {
+  msg = msg || error.pprint`Expected ${obj} to be a string`;
+  return assert.that(s => typeof s == "string", msg)(obj);
+};
+
+/**
+ * Asserts that |obj| is an object.
+ *
+ * @param {?} obj
+ *     Value to test.
+ * @param {string=} msg
+ *     Custom error message.
+ *
+ * @return {Object}
+ *     |obj| is returned unaltered.
+ *
+ * @throws {InvalidArgumentError}
+ *     If |obj| is not an object.
+ */
+assert.object = function(obj, msg = "") {
+  msg = msg || error.pprint`Expected ${obj} to be an object`;
+  return assert.that(o => typeof o == "object", msg)(obj);
+};
+
+/**
+ * Returns a function that is used to assert the |predicate|.
+ *
+ * @param {function(?): boolean} predicate
+ *     Evaluated on calling the return value of this function.  If its
+ *     return value of the inner function is false, |error| is thrown
+ *     with |message|.
+ * @param {string=} message
+ *     Custom error message.
+ * @param {Error=} error
+ *     Custom error type by its class.
+ *
+ * @return {function(?): ?}
+ *     Function that takes and returns the passed in value unaltered, and
+ *     which may throw |error| with |message| if |predicate| evaluates
+ *     to false.
+ */
+assert.that = function(
+    predicate, message = "", error = InvalidArgumentError) {
+  return obj => {
+    if (!predicate(obj)) {
+      throw new error(message);
+    }
+    return obj;
+  };
+};
--- a/testing/marionette/error.js
+++ b/testing/marionette/error.js
@@ -124,16 +124,42 @@ error.stringify = function(err) {
     }
     return s;
   } catch (e) {
     return "<unprintable error>";
   }
 };
 
 /**
+ * Pretty-print values passed to template strings.
+ *
+ * Usage:
+ *
+ *     let input = {value: true};
+ *     error.pprint`Expected boolean, got ${input}`;
+ *     => "Expected boolean, got [object Object] {"value": true}"
+ */
+error.pprint = function(strings, ...values) {
+  let res = [];
+  for (let i = 0; i < strings.length; i++) {
+    res.push(strings[i]);
+    if (i < values.length) {
+      let val = values[i];
+      res.push(Object.prototype.toString.call(val));
+      let s = JSON.stringify(val);
+      if (s && s.length > 0) {
+        res.push(" ");
+        res.push(s);
+      }
+    }
+  }
+  return res.join("");
+};
+
+/**
  * Marshal a WebDriverError prototype to a JSON dictionary.
  *
  * @param {WebDriverError} err
  *     Error to serialise.
  *
  * @return {Object.<string, Object>}
  *     JSON dictionary with the keys "error", "message", and "stacktrace".
  * @throws {TypeError}
--- a/testing/marionette/jar.mn
+++ b/testing/marionette/jar.mn
@@ -22,16 +22,17 @@ marionette.jar:
   content/modal.js (modal.js)
   content/proxy.js (proxy.js)
   content/capture.js (capture.js)
   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/assert.js (assert.js)
 #ifdef ENABLE_TESTS
   content/test.xul  (harness/marionette/chrome/test.xul)
   content/test2.xul  (harness/marionette/chrome/test2.xul)
   content/test_dialog.xul  (harness/marionette/chrome/test_dialog.xul)
   content/test_nested_iframe.xul  (harness/marionette/chrome/test_nested_iframe.xul)
   content/test_anonymous_content.xul  (harness/marionette/chrome/test_anonymous_content.xul)
 #endif
 
new file mode 100644
--- /dev/null
+++ b/testing/marionette/test_assert.js
@@ -0,0 +1,85 @@
+/* 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("chrome://marionette/content/assert.js");
+Cu.import("chrome://marionette/content/error.js");
+
+add_test(function test_platforms() {
+  // at least one will fail
+  let raised;
+  for (let fn of [assert.firefox, assert.fennec, assert.b2g, assert.mobile]) {
+    try {
+      fn();
+    } catch (e) {
+      raised = e;
+    }
+  }
+  ok(raised instanceof UnsupportedOperationError);
+
+  run_next_test();
+});
+
+add_test(function test_defined() {
+  assert.defined({});
+  Assert.throws(() => assert.defined(undefined), InvalidArgumentError);
+
+  run_next_test();
+});
+
+add_test(function test_integer() {
+  assert.integer(1);
+  assert.integer(0);
+  assert.integer(-1);
+  Assert.throws(() => assert.integer("foo"), InvalidArgumentError);
+
+  run_next_test();
+});
+
+add_test(function test_positiveInteger() {
+  assert.positiveInteger(1);
+  assert.positiveInteger(0);
+  Assert.throws(() => assert.positiveInteger(-1), InvalidArgumentError);
+  Assert.throws(() => assert.positiveInteger("foo"), InvalidArgumentError);
+
+  run_next_test();
+});
+
+add_test(function test_boolean() {
+  assert.boolean(true);
+  assert.boolean(false);
+  Assert.throws(() => assert.boolean("false"), InvalidArgumentError);
+  Assert.throws(() => assert.boolean(undefined), InvalidArgumentError);
+
+  run_next_test();
+});
+
+add_test(function test_string() {
+  assert.string("foo");
+  assert.string(`bar`);
+  Assert.throws(() => assert.string(42), InvalidArgumentError);
+
+  run_next_test();
+});
+
+add_test(function test_object() {
+  assert.object({});
+  assert.object(new Object());
+  Assert.throws(() => assert.object(42), InvalidArgumentError);
+
+  run_next_test();
+});
+
+add_test(function test_that() {
+  equal(1, assert.that(n => n + 1)(1));
+  Assert.throws(() => assert.that(() => false)());
+  Assert.throws(() => assert.that(val => val)(false));
+  Assert.throws(() => assert.that(val => val, "foo", SessionNotCreatedError)(false),
+      SessionNotCreatedError);
+
+  run_next_test();
+});
--- a/testing/marionette/unit.ini
+++ b/testing/marionette/unit.ini
@@ -3,12 +3,13 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 # xpcshell unit tests for Marionette
 
 [DEFAULT]
 skip-if = appname == "thunderbird"
 
 [test_action.js]
+[test_assert.js]
 [test_element.js]
 [test_error.js]
 [test_message.js]
 [test_navigate.js]