Bug 1315872: Add browser.test.assertRejects and assertThrows. r?aswan
MozReview-Commit-ID: DKUlSVS2EvA
--- 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",