Bug 1313865 - Centralise common Marionette assertions; r=automatedtester,maja_zf
authorAndreas Tolfsen <ato@mozilla.com>
Mon, 31 Oct 2016 22:00:21 +0000
changeset 321980 84038d989111bed1e54038095eb4fbb75d0eeaa0
parent 321979 f3491aaeac27d308357af664b14f66f02426b520
child 321981 9455d3577d5be5e010424f06b6c6435bf347f39c
push id34118
push useratolfsen@mozilla.com
push dateThu, 10 Nov 2016 16:18:41 +0000
treeherderautoland@f8081bbfdf4e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersautomatedtester, maja_zf
bugs1313865
milestone52.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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]