--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -1694,36 +1694,21 @@ class ProxyAPIImplementation extends Sch
*/
constructor(namespace, name, childApiManager) {
super();
this.path = `${namespace}.${name}`;
this.childApiManager = childApiManager;
}
callFunctionNoReturn(args) {
- this.childApiManager.messageManager.sendAsyncMessage("API:Call", {
- childId: this.childApiManager.id,
- path: this.path,
- args,
- });
+ this.childApiManager.callParentFunctionNoReturn(this.path, args);
}
callAsyncFunction(args, callback) {
- let callId = nextId++;
- let deferred = PromiseUtils.defer();
- this.childApiManager.callPromises.set(callId, deferred);
-
- this.childApiManager.messageManager.sendAsyncMessage("API:Call", {
- childId: this.childApiManager.id,
- callId,
- path: this.path,
- args,
- });
-
- return this.childApiManager.context.wrapPromise(deferred.promise, callback);
+ return this.childApiManager.callParentAsyncFunction(this.path, args, callback);
}
addListener(listener, args) {
let set = this.childApiManager.listeners.get(this.path);
if (!set) {
set = new Set();
this.childApiManager.listeners.set(this.path, set);
}
@@ -1815,16 +1800,56 @@ class ChildAPIManager {
} else {
deferred.resolve(new SpreadArgs(data.args));
}
this.callPromises.delete(data.callId);
break;
}
}
+ /**
+ * Call a function in the parent process and ignores its return value.
+ *
+ * @param {string} path The full name of the method, e.g. "tabs.create".
+ * @param {Array} args The parameters for the function.
+ */
+ callParentFunctionNoReturn(path, args) {
+ this.messageManager.sendAsyncMessage("API:Call", {
+ childId: this.id,
+ path,
+ args,
+ });
+ }
+
+ /**
+ * Calls a function in the parent process and returns its result
+ * asynchronously.
+ *
+ * @param {string} path The full name of the method, e.g. "tabs.create".
+ * @param {Array} args The parameters for the function.
+ * @param {function(*)} [callback] The callback to be called when the function
+ * completes.
+ * @returns {Promise|undefined} Must be void if `callback` is set, and a
+ * promise otherwise. The promise is resolved when the function completes.
+ */
+ callParentAsyncFunction(path, args, callback) {
+ let callId = nextId++;
+ let deferred = PromiseUtils.defer();
+ this.callPromises.set(callId, deferred);
+
+ this.messageManager.sendAsyncMessage("API:Call", {
+ childId: this.id,
+ callId,
+ path,
+ args,
+ });
+
+ return this.context.wrapPromise(deferred.promise, callback);
+ }
+
close() {
this.messageManager.sendAsyncMessage("API:CloseProxyContext", {childId: this.id});
}
get cloneScope() {
return this.context.cloneScope;
}
--- a/toolkit/components/extensions/ext-test.js
+++ b/toolkit/components/extensions/ext-test.js
@@ -49,25 +49,31 @@ function testApiFactory(context) {
extension.emit("test-result", false, msg);
},
succeed: function(msg) {
extension.emit("test-result", true, msg);
},
assertTrue: function(value, msg) {
- extension.emit("test-result", Boolean(value), msg);
+ extension.emit("test-result", Boolean(value), String(msg));
},
assertFalse: function(value, msg) {
- extension.emit("test-result", !value, msg);
+ extension.emit("test-result", !value, String(msg));
},
assertEq: function(expected, actual, msg) {
- extension.emit("test-eq", expected === actual, msg, String(expected), String(actual));
+ let equal = expected === actual;
+ expected += "";
+ actual += "";
+ if (!equal && expected === actual) {
+ actual += " (different)";
+ }
+ extension.emit("test-eq", equal, String(msg), expected, actual);
},
onMessage: new EventManager(context, "test.onMessage", fire => {
let handlers = messageHandlers.get(extension);
handlers.add(fire);
return () => {
handlers.delete(fire);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_test.html
@@ -0,0 +1,190 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Testing test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+
+<script>
+"use strict";
+
+function loadExtensionAndInterceptTest(extensionData) {
+ let results = [];
+ let testResolve;
+ let testDone = new Promise(resolve => { testResolve = resolve; });
+ let handler = {
+ testResult(...result) {
+ results.push(result);
+ SimpleTest.info(`Received test result: ${JSON.stringify(result)}`);
+ },
+
+ testMessage(msg, ...args) {
+ results.push(["test-message", msg, ...args]);
+ SimpleTest.info(`Received message: ${msg} ${JSON.stringify(args)}`);
+ if (msg === "This is the last browser.test call") {
+ testResolve();
+ }
+ },
+ };
+ let extension = SpecialPowers.loadExtension(extensionData, handler);
+ SimpleTest.registerCleanupFunction(() => {
+ if (extension.state == "pending" || extension.state == "running") {
+ SimpleTest.ok(false, "Extension left running at test shutdown");
+ return extension.unload();
+ } else if (extension.state == "unloading") {
+ SimpleTest.ok(false, "Extension not fully unloaded at test shutdown");
+ }
+ });
+ extension.awaitResults = () => testDone.then(() => results);
+ return extension;
+}
+
+function testScript() {
+ // Note: The result of these browser.test calls are intercepted by the test.
+ // See verifyTestResults for the expectations of each browser.test call.
+ browser.test.notifyPass("dot notifyPass");
+ browser.test.notifyFail("dot notifyFail");
+ browser.test.log("dot log");
+ browser.test.fail("dot fail");
+ browser.test.succeed("dot succeed");
+ browser.test.assertTrue(true);
+ browser.test.assertFalse(false);
+ browser.test.assertEq("", "");
+
+ let obj = {};
+ let arr = [];
+ let dom = document.createElement("body");
+ browser.test.assertTrue(obj, "Object truthy");
+ browser.test.assertTrue(arr, "Array truthy");
+ browser.test.assertTrue(dom, "Element truthy");
+ browser.test.assertTrue(true, "True truthy");
+ browser.test.assertTrue(false, "False truthy");
+ browser.test.assertTrue(null, "Null truthy");
+ browser.test.assertTrue(undefined, "Void truthy");
+ browser.test.assertTrue(false, document.createElement("html"));
+
+ browser.test.assertFalse(obj, "Object falsey");
+ browser.test.assertFalse(arr, "Array falsey");
+ browser.test.assertFalse(dom, "Element falsey");
+ browser.test.assertFalse(true, "True falsey");
+ browser.test.assertFalse(false, "False falsey");
+ browser.test.assertFalse(null, "Null falsey");
+ browser.test.assertFalse(undefined, "Void falsey");
+ browser.test.assertFalse(true, document.createElement("head"));
+
+ browser.test.assertEq(obj, obj, "Object equality");
+ browser.test.assertEq(arr, arr, "Array equality");
+ browser.test.assertEq(dom, dom, "Element equality");
+ browser.test.assertEq(null, null, "Null equality");
+ browser.test.assertEq(undefined, undefined, "Void equality");
+
+ browser.test.assertEq({}, {}, "Object reference ineqality");
+ browser.test.assertEq([], [], "Array reference ineqality");
+ browser.test.assertEq(dom, document.createElement("body"), "Element ineqality");
+ browser.test.assertEq(null, undefined, "Null and void ineqality");
+ browser.test.assertEq(true, false, document.createElement("div"));
+
+ obj = {
+ toString() {
+ return "Dynamic toString forbidden";
+ },
+ };
+ browser.test.assertEq(obj, obj, "obj with dynamic toString()");
+ browser.test.sendMessage("Ran test at", location.protocol);
+ browser.test.sendMessage("This is the last browser.test call");
+}
+
+function verifyTestResults(results, shortName, expectedProtocol) {
+ let expectations = [
+ ["test-done", true, "dot notifyPass"],
+ ["test-done", false, "dot notifyFail"],
+ ["test-log", true, "dot log"],
+ ["test-result", false, "dot fail"],
+ ["test-result", true, "dot succeed"],
+ ["test-result", true, "undefined"],
+ ["test-result", true, "undefined"],
+ ["test-eq", true, "undefined", "", ""],
+
+ ["test-result", true, "Object truthy"],
+ ["test-result", true, "Array truthy"],
+ ["test-result", true, "Element truthy"],
+ ["test-result", true, "True truthy"],
+ ["test-result", false, "False truthy"],
+ ["test-result", false, "Null truthy"],
+ ["test-result", false, "Void truthy"],
+ ["test-result", false, "[object HTMLHtmlElement]"],
+
+ ["test-result", false, "Object falsey"],
+ ["test-result", false, "Array falsey"],
+ ["test-result", false, "Element falsey"],
+ ["test-result", false, "True falsey"],
+ ["test-result", true, "False falsey"],
+ ["test-result", true, "Null falsey"],
+ ["test-result", true, "Void falsey"],
+ ["test-result", false, "[object HTMLHeadElement]"],
+
+ ["test-eq", true, "Object equality", "[object Object]", "[object Object]"],
+ ["test-eq", true, "Array equality", "", ""],
+ ["test-eq", true, "Element equality", "[object HTMLBodyElement]", "[object HTMLBodyElement]"],
+ ["test-eq", true, "Null equality", "null", "null"],
+ ["test-eq", true, "Void equality", "undefined", "undefined"],
+
+ ["test-eq", false, "Object reference ineqality", "[object Object]", "[object Object] (different)"],
+ ["test-eq", false, "Array reference ineqality", "", " (different)"],
+ ["test-eq", false, "Element ineqality", "[object HTMLBodyElement]", "[object HTMLBodyElement] (different)"],
+ ["test-eq", false, "Null and void ineqality", "null", "undefined"],
+ ["test-eq", false, "[object HTMLDivElement]", "true", "false"],
+
+ ["test-eq", true, "obj with dynamic toString()", "[object Object]", "[object Object]"],
+
+ ["test-message", "Ran test at", expectedProtocol],
+ ["test-message", "This is the last browser.test call"],
+ ];
+
+ expectations.forEach((expectation, i) => {
+ let msg = expectation.slice(2).join(" - ");
+ isDeeply(results[i], expectation, `${shortName} (${msg})`);
+ });
+ is(results[expectations.length], undefined, "No more results");
+}
+
+add_task(function* test_test_in_background() {
+ let extensionData = {
+ background: `(${testScript})()`,
+ };
+
+ let extension = loadExtensionAndInterceptTest(extensionData);
+ yield extension.startup();
+ let results = yield extension.awaitResults();
+ verifyTestResults(results, "background page", "moz-extension:");
+ yield extension.unload();
+});
+
+add_task(function* test_test_in_content_script() {
+ let extensionData = {
+ manifest: {
+ content_scripts: [{
+ matches: ["http://mochi.test/*/file_sample.html"],
+ js: ["contentscript.js"],
+ }],
+ },
+ files: {
+ "contentscript.js": `(${testScript})()`,
+ },
+ };
+
+ let extension = loadExtensionAndInterceptTest(extensionData);
+ yield extension.startup();
+ let win = window.open("file_sample.html");
+ let results = yield extension.awaitResults();
+ win.close();
+ verifyTestResults(results, "content script", "http:");
+ yield extension.unload();
+});
+</script>
+</body>
+</html>