From e5010028a2f85838a5208e3b9776bb1ba316033f Mon Sep 17 00:00:00 2001 From: Andreas Tolfsen Date: Mon, 31 Oct 2016 22:00:21 +0000 Subject: [PATCH] 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 --HG-- extra : rebase_source : 1d35c10e29d4fd536829e9714ae65bcd14ad21f8 --- testing/marionette/assert.js | 220 ++++++++++++++++++++++++++++++ testing/marionette/error.js | 26 ++++ testing/marionette/jar.mn | 1 + testing/marionette/test_assert.js | 85 ++++++++++++ testing/marionette/unit.ini | 1 + 5 files changed, 333 insertions(+) create mode 100644 testing/marionette/assert.js create mode 100644 testing/marionette/test_assert.js diff --git a/testing/marionette/assert.js b/testing/marionette/assert.js new file mode 100644 index 000000000000..7a68aebeda94 --- /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; + }; +}; diff --git a/testing/marionette/error.js b/testing/marionette/error.js index a567645ed6f7..e8321be695e6 100644 --- a/testing/marionette/error.js +++ b/testing/marionette/error.js @@ -128,6 +128,32 @@ error.stringify = function(err) { } }; +/** + * 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. * diff --git a/testing/marionette/jar.mn b/testing/marionette/jar.mn index 2fbbdde91950..1aed2c351ac1 100644 --- a/testing/marionette/jar.mn +++ b/testing/marionette/jar.mn @@ -27,6 +27,7 @@ marionette.jar: 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) diff --git a/testing/marionette/test_assert.js b/testing/marionette/test_assert.js new file mode 100644 index 000000000000..adabe7d60f39 --- /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(); +}); diff --git a/testing/marionette/unit.ini b/testing/marionette/unit.ini index 3f246f82987c..38e83e464c01 100644 --- a/testing/marionette/unit.ini +++ b/testing/marionette/unit.ini @@ -8,6 +8,7 @@ skip-if = appname == "thunderbird" [test_action.js] +[test_assert.js] [test_element.js] [test_error.js] [test_message.js]