Bug 1315872: Add browser.test.assertRejects and assertThrows. r?aswan draft
authorKris Maglione <maglione.k@gmail.com>
Mon, 07 Nov 2016 22:03:15 -0800
changeset 435220 f961ab661da5728b1e0afab4d486f2d69256bbb1
parent 435219 70c8f0c17094e2afbd448653138d90294d1806ba
child 435221 f6ebc5d9665d33993d1d627c654e6f2484eb21c2
push id34962
push usermaglione.k@gmail.com
push dateTue, 08 Nov 2016 06:10:49 +0000
reviewersaswan
bugs1315872
milestone52.0a1
Bug 1315872: Add browser.test.assertRejects and assertThrows. r?aswan MozReview-Commit-ID: DKUlSVS2EvA
toolkit/components/extensions/Schemas.jsm
toolkit/components/extensions/ext-c-test.js
toolkit/components/extensions/ext-test.js
toolkit/components/extensions/schemas/test.json
toolkit/components/extensions/test/mochitest/test_ext_all_apis.js
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -994,17 +994,22 @@ class ObjectType extends Type {
         pattern,
         type: parseProperty(schema.patternProperties[propName]),
       });
     }
 
     // Parse "additionalProperties" schema.
     let additionalProperties = null;
     if (schema.additionalProperties) {
-      additionalProperties = Schemas.parseSchema(schema.additionalProperties, path);
+      let type = schema.additionalProperties;
+      if (type === true) {
+        type = {"type": "any"};
+      }
+
+      additionalProperties = Schemas.parseSchema(type, path);
     }
 
     return new this(schema, properties, additionalProperties, patternProperties, schema.isInstanceOf || null);
   }
 
   constructor(schema, properties, additionalProperties, patternProperties, isInstanceOf) {
     super(schema);
     this.properties = properties;
@@ -1343,17 +1348,17 @@ class FunctionType extends Type {
   static get EXTRA_PROPERTIES() {
     return ["parameters", "async", "returns", ...super.EXTRA_PROPERTIES];
   }
 
   static parseSchema(schema, path, extraProperties = []) {
     this.checkSchemaProperties(schema, path, extraProperties);
 
     let isAsync = !!schema.async;
-    let isExpectingCallback = isAsync;
+    let isExpectingCallback = typeof schema.async === "string";
     let parameters = null;
     if ("parameters" in schema) {
       parameters = [];
       for (let param of schema.parameters) {
         // Callbacks default to optional for now, because of promise
         // handling.
         let isCallback = isAsync && param.name == schema.async;
         if (isCallback) {
@@ -1369,19 +1374,20 @@ class FunctionType extends Type {
       }
     }
     if (isExpectingCallback) {
       throw new Error(`Internal error: Expected a callback parameter with name ${schema.async}`);
     }
 
     let hasAsyncCallback = false;
     if (isAsync) {
-      if (parameters && parameters.length && parameters[parameters.length - 1].name == schema.async) {
-        hasAsyncCallback = true;
-      }
+      hasAsyncCallback = (parameters &&
+                          parameters.length &&
+                          parameters[parameters.length - 1].name == schema.async);
+
       if (schema.returns) {
         throw new Error("Internal error: Async functions must not have return values.");
       }
       if (schema.allowAmbiguousOptionalArguments && !hasAsyncCallback) {
         throw new Error("Internal error: Async functions with ambiguous arguments must declare the callback as the last parameter");
       }
     }
 
@@ -1912,17 +1918,21 @@ this.Schemas = {
   parseSchemas() {
     Object.defineProperty(this, "namespaces", {
       enumerable: true,
       configurable: true,
       value: new Map(),
     });
 
     for (let json of this.schemaJSON.values()) {
-      this.loadSchema(json);
+      try {
+        this.loadSchema(json);
+      } catch (e) {
+        Cu.reportError(e);
+      }
     }
 
     return this.namespaces;
   },
 
   loadSchema(json) {
     for (let namespace of json) {
       let name = namespace.namespace;
--- a/toolkit/components/extensions/ext-c-test.js
+++ b/toolkit/components/extensions/ext-c-test.js
@@ -1,11 +1,85 @@
 "use strict";
 
-function testApiFactory(context) {
+/**
+ * Checks whether the given error matches the given expectations.
+ *
+ * @param {*} error
+ *        The error to check.
+ * @param {string|RegExp|function|null} expectedError
+ *        The expectation to check against. If this parameter is:
+ *
+ *        - a string, the error message must exactly equal the string.
+ *        - a regular expression, it must match the error message.
+ *        - a function, it is called with the error object and its
+ *          return value is returned.
+ *        - null, the function always returns true.
+ * @param {BaseContext} context
+ *
+ * @returns {boolean}
+ *        True if the error matches the expected error.
+ */
+function errorMatches(error, expectedError, context) {
+  if (expectedError === null) {
+    return true;
+  }
+
+  if (typeof expectedError === "function") {
+    return context.runSafeWithoutClone(expectedError, error);
+  }
+
+  if (typeof error !== "object" || error == null ||
+      typeof error.message !== "string") {
+    return false;
+  }
+
+  if (typeof expectedError === "string") {
+    return error.message === expectedError;
+  }
+
+  try {
+    return expectedError.test(error.message);
+  } catch (e) {
+    Cu.reportError(e);
+  }
+
+  return false;
+}
+
+/**
+ * Calls .toSource() on the given value, but handles null, undefined,
+ * and errors.
+ *
+ * @param {*} value
+ * @returns {string}
+ */
+function toSource(value) {
+  if (value === null) {
+    return null;
+  }
+  if (value === undefined) {
+    return null;
+  }
+  if (typeof value === "string") {
+    return JSON.stringify(value);
+  }
+
+  try {
+    return String(value.toSource());
+  } catch (e) {
+    return "<unknown>";
+  }
+}
+
+function makeTestAPI(context) {
+  function assertTrue(...args) {
+    context.childManager.callParentFunctionNoReturn("test.assertTrue", args);
+  }
+
   return {
     test: {
       // These functions accept arbitrary values. Convert the parameters to
       // make sure that the values can be cloned structurally for IPC.
 
       sendMessage(...args) {
         args = Cu.cloneInto(args, context.cloneScope);
         context.childManager.callParentFunctionNoReturn("test.sendMessage", args);
@@ -35,14 +109,51 @@ function testApiFactory(context) {
           actual += " (different)";
         }
         context.childManager.callParentFunctionNoReturn("test.assertEq", [
           expected,
           actual,
           String(msg),
         ]);
       },
+
+      assertRejects(promise, expectedError, msg) {
+        // Wrap in a native promise for consistency.
+        promise = Promise.resolve(promise);
+
+        if (msg) {
+          msg = `: ${msg}`;
+        }
+
+        return promise.then(result => {
+          assertTrue(false, `Promise resolved, expected rejection${msg}`);
+        }, error => {
+          let errorMessage = toSource(error && error.message);
+
+          assertTrue(errorMatches(error, expectedError, context),
+                     `Promise rejected, expecting rejection to match ${toSource(expectedError)}, ` +
+                     `got ${errorMessage}${msg}`);
+        });
+      },
+
+      assertThrows(func, expectedError, msg) {
+        if (msg) {
+          msg = `: ${msg}`;
+        }
+
+        try {
+          func();
+
+          assertTrue(false, `Function did not throw, expected error${msg}`);
+        } catch (error) {
+          let errorMessage = toSource(error && error.message);
+
+          assertTrue(errorMatches(error, expectedError, context),
+                     `Promise rejected, expecting rejection to match ${toSource(expectedError)}` +
+                     `got ${errorMessage}${msg}`);
+        }
+      },
     },
   };
 }
-extensions.registerSchemaAPI("test", "addon_child", testApiFactory);
-extensions.registerSchemaAPI("test", "content_child", testApiFactory);
+extensions.registerSchemaAPI("test", "addon_child", makeTestAPI);
+extensions.registerSchemaAPI("test", "content_child", makeTestAPI);
 
--- a/toolkit/components/extensions/ext-test.js
+++ b/toolkit/components/extensions/ext-test.js
@@ -20,17 +20,17 @@ extensions.on("shutdown", (type, extensi
 extensions.on("test-message", (type, extension, ...args) => {
   let handlers = messageHandlers.get(extension);
   for (let handler of handlers) {
     handler(...args);
   }
 });
 /* eslint-enable mozilla/balanced-listeners */
 
-function testApiFactory(context) {
+function makeTestAPI(context) {
   let {extension} = context;
   return {
     test: {
       sendMessage: function(...args) {
         extension.emit("test-message", ...args);
       },
 
       notifyPass: function(msg) {
@@ -77,10 +77,10 @@ function testApiFactory(context) {
 
         return () => {
           handlers.delete(fire);
         };
       }).api(),
     },
   };
 }
-extensions.registerSchemaAPI("test", "addon_parent", testApiFactory);
-extensions.registerSchemaAPI("test", "content_parent", testApiFactory);
+extensions.registerSchemaAPI("test", "addon_parent", makeTestAPI);
+extensions.registerSchemaAPI("test", "content_parent", makeTestAPI);
--- a/toolkit/components/extensions/schemas/test.json
+++ b/toolkit/components/extensions/schemas/test.json
@@ -121,29 +121,81 @@
         "name": "assertLastError",
         "type": "function",
         "unsupported": true,
         "parameters": [
           {"type": "string", "name": "expectedError"}
         ]
       },
       {
+        "name": "assertRejects",
+        "type": "function",
+        "async": true,
+        "parameters": [
+          {
+            "name": "promise",
+            "$ref": "Promise"
+          },
+          {
+            "name": "expectedError",
+            "$ref": "ExpectedError",
+            "optional": true
+          },
+          {
+            "name": "message",
+            "type": "string",
+            "optional": true
+          }
+        ]
+      },
+      {
         "name": "assertThrows",
         "type": "function",
-        "unsupported": true,
         "parameters": [
-          {"type": "function", "name": "fn"},
+          {
+            "name": "func",
+            "type": "function"
+          },
+          {
+            "name": "expectedError",
+            "$ref": "ExpectedError",
+            "optional": true
+          },
+          {
+            "name": "message",
+            "type": "string",
+            "optional": true
+          }
+        ]
+      }
+    ],
+    "types": [
+      {
+        "id": "ExpectedError",
+        "choices": [
+          {"type": "string"},
+          {"type": "object", "isInstanceOf": "RegExp", "additionalProperties": true},
+          {"type": "function"}
+        ]
+      },
+      {
+        "id": "Promise",
+        "choices": [
           {
             "type": "object",
-            "name": "self",
-            "additionalProperties": {"type": "any"},
-            "optional": true
+            "properties": {
+              "then": {"type": "function"}
+            },
+            "additionalProperties": true
           },
-          {"type": "array", "items": {"type": "any"}, "name": "args", "optional": true},
-          {"choices": [ {"type": "string"}, {"type": "object", "isInstanceOf": "RegExp"} ], "name": "message", "optional": true}
+          {
+            "type": "object",
+            "isInstanceOf": "Promise",
+            "additionalProperties": true
+          }
         ]
       }
     ],
     "events": [
       {
         "name": "onMessage",
         "type": "function",
         "description": "Used to test sending messages to extensions.",
--- a/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js
+++ b/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js
@@ -34,16 +34,18 @@ let expectedCommonApis = [
   "runtime.id",
   "runtime.lastError",
   "runtime.onConnect",
   "runtime.onMessage",
   "runtime.sendMessage",
   // If you want to add a new powerful test API, please see bug 1287233.
   "test.assertEq",
   "test.assertFalse",
+  "test.assertRejects",
+  "test.assertThrows",
   "test.assertTrue",
   "test.fail",
   "test.log",
   "test.notifyFail",
   "test.notifyPass",
   "test.onMessage",
   "test.sendMessage",
   "test.succeed",