summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_event_callback.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_event_callback.js')
-rw-r--r--toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_event_callback.js575
1 files changed, 575 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_event_callback.js b/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_event_callback.js
new file mode 100644
index 0000000000..576ec760d3
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_event_callback.js
@@ -0,0 +1,575 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* import-globals-from ../head_service_worker.js */
+
+AddonTestUtils.init(this);
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "42"
+);
+
+add_task(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+});
+
+add_task(async function test_api_event_manager_methods() {
+ await runExtensionAPITest("extension event manager methods", {
+ backgroundScript({ testAsserts, testLog }) {
+ const api = browser.mockExtensionAPI;
+ const listener = () => {};
+
+ function assertHasListener(expect) {
+ testAsserts.equal(
+ api.onTestEvent.hasListeners(),
+ expect,
+ `onTestEvent.hasListeners should return {expect}`
+ );
+ testAsserts.equal(
+ api.onTestEvent.hasListener(listener),
+ expect,
+ `onTestEvent.hasListeners should return {expect}`
+ );
+ }
+
+ assertHasListener(false);
+ api.onTestEvent.addListener(listener);
+ assertHasListener(true);
+ api.onTestEvent.removeListener(listener);
+ assertHasListener(false);
+ },
+ mockAPIRequestHandler(policy, request) {
+ if (!request.eventListener) {
+ throw new Error(
+ "Unexpected Error: missing ExtensionAPIRequest.eventListener"
+ );
+ }
+ },
+ assertResults({ testError, testResult }) {
+ Assert.deepEqual(testError, null, "Got no error as expected");
+ },
+ });
+});
+
+add_task(async function test_api_event_eventListener_call() {
+ await runExtensionAPITest(
+ "extension event eventListener wrapper does forward calls parameters",
+ {
+ backgroundScript({ testAsserts, testLog }) {
+ const api = browser.mockExtensionAPI;
+ let listener;
+
+ return new Promise((resolve, reject) => {
+ testLog("addListener and wait for event to be fired");
+ listener = (...args) => {
+ testLog("onTestEvent");
+ // Make sure the extension code can access the arguments.
+ try {
+ testAsserts.equal(args[1], "arg1");
+ resolve(args);
+ } catch (err) {
+ reject(err);
+ }
+ };
+ api.onTestEvent.addListener(listener);
+ });
+ },
+ mockAPIRequestHandler(policy, request) {
+ if (!request.eventListener) {
+ throw new Error(
+ "Unexpected Error: missing ExtensionAPIRequest.eventListener"
+ );
+ }
+ if (request.requestType === "addListener") {
+ let args = [{ arg: 0 }, "arg1"];
+ request.eventListener.callListener(args);
+ }
+ },
+ assertResults({ testError, testResult }) {
+ Assert.deepEqual(testError, null, "Got no error as expected");
+ Assert.deepEqual(
+ testResult,
+ [{ arg: 0 }, "arg1"],
+ "Got the expected result"
+ );
+ },
+ }
+ );
+});
+
+add_task(async function test_api_event_eventListener_call_with_result() {
+ await runExtensionAPITest(
+ "extension event eventListener wrapper forwarded call result",
+ {
+ backgroundScript({ testAsserts, testLog }) {
+ const api = browser.mockExtensionAPI;
+ let listener;
+
+ return new Promise((resolve, reject) => {
+ testLog("addListener and wait for event to be fired");
+ listener = (msg, value) => {
+ testLog(`onTestEvent received: ${msg}`);
+ switch (msg) {
+ case "test-result-value":
+ return value;
+ case "test-promise-resolve":
+ return Promise.resolve(value);
+ case "test-promise-reject":
+ return Promise.reject(new Error("test-reject"));
+ case "test-done":
+ resolve(value);
+ break;
+ default:
+ reject(new Error(`Unexpected onTestEvent message: ${msg}`));
+ }
+ };
+ api.onTestEvent.addListener(listener);
+ });
+ },
+ assertResults({ testError, testResult }) {
+ Assert.deepEqual(testError, null, "Got no error as expected");
+ Assert.deepEqual(
+ testResult?.resSync,
+ { prop: "retval" },
+ "Got result from eventListener returning a plain return value"
+ );
+ Assert.deepEqual(
+ testResult?.resAsync,
+ { prop: "promise" },
+ "Got result from eventListener returning a resolved promise"
+ );
+ Assert.deepEqual(
+ testResult?.resAsyncReject,
+ {
+ isInstanceOfError: true,
+ errorMessage: "test-reject",
+ },
+ "got result from eventListener returning a rejected promise"
+ );
+ },
+ mockAPIRequestHandler(policy, request) {
+ if (!request.eventListener) {
+ throw new Error(
+ "Unexpected Error: missing ExtensionAPIRequest.eventListener"
+ );
+ }
+
+ if (request.requestType === "addListener") {
+ Promise.resolve().then(async () => {
+ try {
+ dump(`calling listener, expect a plain return value\n`);
+ const resSync = await request.eventListener.callListener([
+ "test-result-value",
+ { prop: "retval" },
+ ]);
+
+ dump(
+ `calling listener, expect a resolved promise return value\n`
+ );
+ const resAsync = await request.eventListener.callListener([
+ "test-promise-resolve",
+ { prop: "promise" },
+ ]);
+
+ dump(
+ `calling listener, expect a rejected promise return value\n`
+ );
+ const resAsyncReject = await request.eventListener
+ .callListener(["test-promise-reject"])
+ .catch(err => err);
+
+ // call API listeners once more to complete the test
+ let args = {
+ resSync,
+ resAsync,
+ resAsyncReject: {
+ isInstanceOfError: resAsyncReject instanceof Error,
+ errorMessage: resAsyncReject?.message,
+ },
+ };
+ request.eventListener.callListener(["test-done", args]);
+ } catch (err) {
+ dump(`Unexpected error: ${err} :: ${err.stack}\n`);
+ throw err;
+ }
+ });
+ }
+ },
+ }
+ );
+});
+
+add_task(async function test_api_event_eventListener_result_rejected() {
+ await runExtensionAPITest(
+ "extension event eventListener throws (mozIExtensionCallback.call)",
+ {
+ backgroundScript({ testAsserts, testLog }) {
+ const api = browser.mockExtensionAPI;
+ let listener;
+
+ return new Promise((resolve, reject) => {
+ testLog("addListener and wait for event to be fired");
+ listener = (msg, arg1) => {
+ if (msg === "test-done") {
+ testLog(`Resolving result: ${JSON.stringify(arg1)}`);
+ resolve(arg1);
+ return;
+ }
+ throw new Error("FAKE eventListener exception");
+ };
+ api.onTestEvent.addListener(listener);
+ });
+ },
+ assertResults({ testError, testResult }) {
+ Assert.deepEqual(testError, null, "Got no error as expected");
+ Assert.deepEqual(
+ testResult,
+ {
+ isPromise: true,
+ rejectIsError: true,
+ errorMessage: "FAKE eventListener exception",
+ },
+ "Got the expected rejected promise"
+ );
+ },
+ mockAPIRequestHandler(policy, request) {
+ if (!request.eventListener) {
+ throw new Error(
+ "Unexpected Error: missing ExtensionAPIRequest.eventListener"
+ );
+ }
+
+ if (request.requestType === "addListener") {
+ Promise.resolve().then(async () => {
+ const promiseResult = request.eventListener.callListener([]);
+ const isPromise = promiseResult instanceof Promise;
+ const err = await promiseResult.catch(e => e);
+ const rejectIsError = err instanceof Error;
+ request.eventListener.callListener([
+ "test-done",
+ { isPromise, rejectIsError, errorMessage: err?.message },
+ ]);
+ });
+ }
+ },
+ }
+ );
+});
+
+add_task(async function test_api_event_eventListener_throws_on_call() {
+ await runExtensionAPITest(
+ "extension event eventListener throws (mozIExtensionCallback.call)",
+ {
+ backgroundScript({ testAsserts, testLog }) {
+ const api = browser.mockExtensionAPI;
+ let listener;
+
+ return new Promise(resolve => {
+ testLog("addListener and wait for event to be fired");
+ listener = (msg, arg1) => {
+ if (msg === "test-done") {
+ testLog(`Resolving result: ${JSON.stringify(arg1)}`);
+ resolve();
+ return;
+ }
+ throw new Error("FAKE eventListener exception");
+ };
+ api.onTestEvent.addListener(listener);
+ });
+ },
+ assertResults({ testError, testResult }) {
+ Assert.deepEqual(testError, null, "Got no error as expected");
+ },
+ mockAPIRequestHandler(policy, request) {
+ if (!request.eventListener) {
+ throw new Error(
+ "Unexpected Error: missing ExtensionAPIRequest.eventListener"
+ );
+ }
+
+ if (request.requestType === "addListener") {
+ Promise.resolve().then(async () => {
+ request.eventListener.callListener([]);
+ request.eventListener.callListener(["test-done"]);
+ });
+ }
+ },
+ }
+ );
+});
+
+add_task(async function test_send_response_eventListener() {
+ await runExtensionAPITest(
+ "extension event eventListener sendResponse eventListener argument",
+ {
+ backgroundScript({ testAsserts, testLog }) {
+ const api = browser.mockExtensionAPI;
+ let listener;
+
+ return new Promise(resolve => {
+ testLog("addListener and wait for event to be fired");
+ listener = (msg, sendResponse) => {
+ if (msg === "call-sendResponse") {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(() => sendResponse("sendResponse-value"), 20);
+ return true;
+ }
+
+ resolve(msg);
+ };
+ api.onTestEvent.addListener(listener);
+ });
+ },
+ assertResults({ testError, testResult }) {
+ Assert.deepEqual(testError, null, "Got no error as expected");
+ Assert.equal(testResult, "sendResponse-value", "Got expected value");
+ },
+ mockAPIRequestHandler(policy, request) {
+ if (!request.eventListener) {
+ throw new Error(
+ "Unexpected Error: missing ExtensionAPIRequest.eventListener"
+ );
+ }
+
+ if (request.requestType === "addListener") {
+ Promise.resolve().then(async () => {
+ const res = await request.eventListener.callListener(
+ ["call-sendResponse"],
+ {
+ callbackType:
+ Ci.mozIExtensionListenerCallOptions.CALLBACK_SEND_RESPONSE,
+ }
+ );
+ request.eventListener.callListener([res]);
+ });
+ }
+ },
+ }
+ );
+});
+
+add_task(async function test_send_response_multiple_eventListener() {
+ await runExtensionAPITest("multiple extension event eventListeners", {
+ backgroundScript({ testAsserts, testLog }) {
+ const api = browser.mockExtensionAPI;
+ let listenerNoReply;
+ let listenerSendResponseReply;
+
+ return new Promise(resolve => {
+ testLog("addListener and wait for event to be fired");
+ listenerNoReply = (msg, sendResponse) => {
+ return false;
+ };
+ listenerSendResponseReply = (msg, sendResponse) => {
+ if (msg === "call-sendResponse") {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(() => sendResponse("sendResponse-value"), 20);
+ return true;
+ }
+
+ resolve(msg);
+ };
+ api.onTestEvent.addListener(listenerNoReply);
+ api.onTestEvent.addListener(listenerSendResponseReply);
+ });
+ },
+ assertResults({ testError, testResult }) {
+ Assert.deepEqual(testError, null, "Got no error as expected");
+ Assert.equal(testResult, "sendResponse-value", "Got expected value");
+ },
+ mockAPIRequestHandler(policy, request) {
+ if (!request.eventListener) {
+ throw new Error(
+ "Unexpected Error: missing ExtensionAPIRequest.eventListener"
+ );
+ }
+
+ if (request.requestType === "addListener") {
+ this._listeners = this._listeners || [];
+ this._listeners.push(request.eventListener);
+ if (this._listeners.length === 2) {
+ Promise.resolve().then(async () => {
+ const { _listeners } = this;
+ this._listeners = undefined;
+
+ // Reference to the listener to which we should send the
+ // final message to complete the test.
+ const replyListener = _listeners[1];
+
+ const res = await Promise.race(
+ _listeners.map(l =>
+ l.callListener(["call-sendResponse"], {
+ callbackType:
+ Ci.mozIExtensionListenerCallOptions.CALLBACK_SEND_RESPONSE,
+ })
+ )
+ );
+ replyListener.callListener([res]);
+ });
+ }
+ }
+ },
+ });
+});
+
+// Unit test nsIServiceWorkerManager.wakeForExtensionAPIEvent method.
+add_task(async function test_serviceworkermanager_wake_for_api_event_helper() {
+ const extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ version: "1.0",
+ background: {
+ service_worker: "sw.js",
+ },
+ browser_specific_settings: {
+ gecko: { id: "test-bg-sw-wakeup@mochi.test" },
+ },
+ },
+ files: {
+ "sw.js": `
+ dump("Background ServiceWorker - executing\\n");
+ const lifecycleEvents = [];
+ self.oninstall = () => {
+ dump('Background ServiceWorker - oninstall\\n');
+ lifecycleEvents.push("install");
+ };
+ self.onactivate = () => {
+ dump('Background ServiceWorker - onactivate\\n');
+ lifecycleEvents.push("activate");
+ };
+ browser.test.onMessage.addListener(msg => {
+ if (msg === "bgsw-getSWEvents") {
+ browser.test.sendMessage("bgsw-gotSWEvents", lifecycleEvents);
+ return;
+ }
+
+ browser.test.fail("Got unexpected test message: " + msg);
+ });
+
+ const fakeListener01 = () => {};
+ const fakeListener02 = () => {};
+
+ // Adding and removing the same listener, and so we expect
+ // ExtensionEventWakeupMap to not have any wakeup listener
+ // for the runtime.onInstalled event.
+ browser.runtime.onInstalled.addListener(fakeListener01);
+ browser.runtime.onInstalled.removeListener(fakeListener01);
+ // Removing the same listener more than ones should make any
+ // difference, and it shouldn't trigger any assertion in
+ // debug builds.
+ browser.runtime.onInstalled.removeListener(fakeListener01);
+
+ browser.runtime.onStartup.addListener(fakeListener02);
+ // Removing an unrelated listener, runtime.onStartup is expected to
+ // still have one wakeup listener tracked by ExtensionEventWakeupMap.
+ browser.runtime.onStartup.removeListener(fakeListener01);
+
+ browser.test.sendMessage("bgsw-executed");
+ dump("Background ServiceWorker - executed\\n");
+ `,
+ },
+ });
+
+ const testWorkerWatcher = new TestWorkerWatcher("../data");
+ let watcher = await testWorkerWatcher.watchExtensionServiceWorker(extension);
+
+ await extension.startup();
+
+ info("Wait for the background service worker to be spawned");
+ ok(
+ await watcher.promiseWorkerSpawned,
+ "The extension service worker has been spawned as expected"
+ );
+
+ await extension.awaitMessage("bgsw-executed");
+
+ extension.sendMessage("bgsw-getSWEvents");
+ let lifecycleEvents = await extension.awaitMessage("bgsw-gotSWEvents");
+ Assert.deepEqual(
+ lifecycleEvents,
+ ["install", "activate"],
+ "Got install and activate lifecycle events as expected"
+ );
+
+ info("Wait for the background service worker to be terminated");
+ ok(
+ await watcher.terminate(),
+ "The extension service worker has been terminated as expected"
+ );
+
+ const swReg = testWorkerWatcher.getRegistration(extension);
+ ok(swReg, "Got a service worker registration");
+ ok(swReg?.activeWorker, "Got an active worker");
+
+ watcher = await testWorkerWatcher.watchExtensionServiceWorker(extension);
+
+ const extensionBaseURL = extension.extension.baseURI.spec;
+
+ async function testWakeupOnAPIEvent(eventName, expectedResult) {
+ const result = await testWorkerWatcher.swm.wakeForExtensionAPIEvent(
+ extensionBaseURL,
+ "runtime",
+ eventName
+ );
+ equal(
+ result,
+ expectedResult,
+ `Got expected result from wakeForExtensionAPIEvent for ${eventName}`
+ );
+ info(
+ `Wait for the background service worker to be spawned for ${eventName}`
+ );
+ ok(
+ await watcher.promiseWorkerSpawned,
+ "The extension service worker has been spawned as expected"
+ );
+ await extension.awaitMessage("bgsw-executed");
+ }
+
+ info("Wake up active worker for API event");
+ // Extension API event listener has been added and removed synchronously by
+ // the worker script, and so we expect the promise to resolve successfully
+ // to `false`.
+ await testWakeupOnAPIEvent("onInstalled", false);
+
+ extension.sendMessage("bgsw-getSWEvents");
+ lifecycleEvents = await extension.awaitMessage("bgsw-gotSWEvents");
+ Assert.deepEqual(
+ lifecycleEvents,
+ [],
+ "No install and activate lifecycle events expected on spawning active worker"
+ );
+
+ info("Wait for the background service worker to be terminated");
+ ok(
+ await watcher.terminate(),
+ "The extension service worker has been terminated as expected"
+ );
+
+ info("Wakeup again with an API event that has been subscribed");
+ // Extension API event listener has been added synchronously (and not removed)
+ // by the worker script, and so we expect the promise to resolve successfully
+ // to `true`.
+ await testWakeupOnAPIEvent("onStartup", true);
+
+ info("Wait for the background service worker to be terminated");
+ ok(
+ await watcher.terminate(),
+ "The extension service worker has been terminated as expected"
+ );
+
+ await extension.unload();
+
+ await Assert.rejects(
+ testWorkerWatcher.swm.wakeForExtensionAPIEvent(
+ extensionBaseURL,
+ "runtime",
+ "onStartup"
+ ),
+ /Not an extension principal or extension disabled/,
+ "Got the expected rejection on wakeForExtensionAPIEvent called for an uninstalled extension"
+ );
+});