/* -*- 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 }) { 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 }) { 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({ 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({ 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(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({ 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 }) { 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({ 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({ testLog }) { const api = browser.mockExtensionAPI; let listenerNoReply; let listenerSendResponseReply; return new Promise(resolve => { testLog("addListener and wait for event to be fired"); listenerNoReply = () => { 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" ); });