From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../test_ext_webidl_api_event_callback.js | 575 +++++++++++++++++++++ 1 file changed, 575 insertions(+) create mode 100644 toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_event_callback.js (limited to 'toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_event_callback.js') 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" + ); +}); -- cgit v1.2.3