summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/webidl-api
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/components/extensions/test/xpcshell/webidl-api
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/webidl-api')
-rw-r--r--toolkit/components/extensions/test/xpcshell/webidl-api/.eslintrc.js9
-rw-r--r--toolkit/components/extensions/test/xpcshell/webidl-api/head_webidl_api.js306
-rw-r--r--toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api.js486
-rw-r--r--toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_event_callback.js575
-rw-r--r--toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_request_handler.js443
-rw-r--r--toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_schema_errors.js202
-rw-r--r--toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_schema_formatters.js99
-rw-r--r--toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_runtime_port.js220
-rw-r--r--toolkit/components/extensions/test/xpcshell/webidl-api/xpcshell.ini32
9 files changed, 2372 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/webidl-api/.eslintrc.js b/toolkit/components/extensions/test/xpcshell/webidl-api/.eslintrc.js
new file mode 100644
index 0000000000..3622fff4f6
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/webidl-api/.eslintrc.js
@@ -0,0 +1,9 @@
+"use strict";
+
+module.exports = {
+ env: {
+ // The tests in this folder are testing based on WebExtensions, so lets
+ // just define the webextensions environment here.
+ webextensions: true,
+ },
+};
diff --git a/toolkit/components/extensions/test/xpcshell/webidl-api/head_webidl_api.js b/toolkit/components/extensions/test/xpcshell/webidl-api/head_webidl_api.js
new file mode 100644
index 0000000000..3e8e094a20
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/webidl-api/head_webidl_api.js
@@ -0,0 +1,306 @@
+/* import-globals-from ../head.js */
+
+/* exported getBackgroundServiceWorkerRegistration, waitForTerminatedWorkers,
+ * runExtensionAPITest */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ ExtensionTestCommon: "resource://testing-common/ExtensionTestCommon.sys.mjs",
+ TestUtils: "resource://testing-common/TestUtils.sys.mjs",
+});
+
+add_setup(function checkExtensionsWebIDLEnabled() {
+ equal(
+ AppConstants.MOZ_WEBEXT_WEBIDL_ENABLED,
+ true,
+ "WebExtensions WebIDL bindings build time flag should be enabled"
+ );
+});
+
+function getBackgroundServiceWorkerRegistration(extension) {
+ const swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
+ Ci.nsIServiceWorkerManager
+ );
+
+ const swRegs = swm.getAllRegistrations();
+ const scope = `moz-extension://${extension.uuid}/`;
+
+ for (let i = 0; i < swRegs.length; i++) {
+ let regInfo = swRegs.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
+ if (regInfo.scope === scope) {
+ return regInfo;
+ }
+ }
+}
+
+function waitForTerminatedWorkers(swRegInfo) {
+ info(`Wait all ${swRegInfo.scope} workers to be terminated`);
+ return TestUtils.waitForCondition(() => {
+ const { evaluatingWorker, installingWorker, waitingWorker, activeWorker } =
+ swRegInfo;
+ return !(
+ evaluatingWorker ||
+ installingWorker ||
+ waitingWorker ||
+ activeWorker
+ );
+ }, `wait workers for scope ${swRegInfo.scope} to be terminated`);
+}
+
+function unmockHandleAPIRequest(extPage) {
+ return extPage.spawn([], () => {
+ const { ExtensionAPIRequestHandler } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionProcessScript.sys.mjs"
+ );
+
+ // Unmock ExtensionAPIRequestHandler.
+ if (ExtensionAPIRequestHandler._handleAPIRequest_orig) {
+ ExtensionAPIRequestHandler.handleAPIRequest =
+ ExtensionAPIRequestHandler._handleAPIRequest_orig;
+ delete ExtensionAPIRequestHandler._handleAPIRequest_orig;
+ }
+ });
+}
+
+function mockHandleAPIRequest(extPage, mockHandleAPIRequest) {
+ mockHandleAPIRequest =
+ mockHandleAPIRequest ||
+ ((policy, request) => {
+ const ExtError = request.window?.Error || Error;
+ return {
+ type: Ci.mozIExtensionAPIRequestResult.EXTENSION_ERROR,
+ value: new ExtError(
+ "mockHandleAPIRequest not defined by this test case"
+ ),
+ };
+ });
+
+ return extPage.legacySpawn(
+ [ExtensionTestCommon.serializeFunction(mockHandleAPIRequest)],
+ mockFnText => {
+ const { ExtensionAPIRequestHandler } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionProcessScript.sys.mjs"
+ );
+
+ mockFnText = `(() => {
+ return (${mockFnText});
+ })();`;
+ // eslint-disable-next-line no-eval
+ const mockFn = eval(mockFnText);
+
+ // Mock ExtensionAPIRequestHandler.
+ if (!ExtensionAPIRequestHandler._handleAPIRequest_orig) {
+ ExtensionAPIRequestHandler._handleAPIRequest_orig =
+ ExtensionAPIRequestHandler.handleAPIRequest;
+ }
+
+ ExtensionAPIRequestHandler.handleAPIRequest = function (policy, request) {
+ if (request.apiNamespace === "test") {
+ return this._handleAPIRequest_orig(policy, request);
+ }
+ return mockFn.call(this, policy, request);
+ };
+ }
+ );
+}
+
+/**
+ * An helper function used to run unit test that are meant to test the
+ * Extension API webidl bindings helpers shared by all the webextensions
+ * API namespaces.
+ *
+ * @param {string} testDescription
+ * Brief description of the test.
+ * @param {object} [options]
+ * @param {Function} options.backgroundScript
+ * Test function running in the extension global. This function
+ * does receive a parameter of type object with the following
+ * properties:
+ * - testLog(message): log a message on the terminal
+ * - testAsserts:
+ * - isErrorInstance(err): throw if err is not an Error instance
+ * - isInstanceOf(value, globalContructorName): throws if value
+ * is not an instance of global[globalConstructorName]
+ * - equal(val, exp, msg): throw an error including msg if
+ * val is not strictly equal to exp.
+ * @param {Function} options.assertResults
+ * Function to be provided to assert the result returned by
+ * `backgroundScript`, or assert the error if it did throw.
+ * This function does receive a parameter of type object with
+ * the following properties:
+ * - testResult: the result returned (and resolved if the return
+ * value was a promise) from the call to `backgroundScript`
+ * - testError: the error raised (or rejected if the return value
+ * value was a promise) from the call to `backgroundScript`
+ * - extension: the extension wrapper created by this helper.
+ * @param {Function} options.mockAPIRequestHandler
+ * Function to be used to mock mozIExtensionAPIRequestHandler.handleAPIRequest
+ * for the purpose of the test.
+ * This function received the same parameter that are listed in the idl
+ * definition (mozIExtensionAPIRequestHandling.webidl).
+ * @param {string} [options.extensionId]
+ * Optional extension id for the test extension.
+ */
+async function runExtensionAPITest(
+ testDescription,
+ {
+ backgroundScript,
+ assertResults,
+ mockAPIRequestHandler,
+ extensionId = "test-ext-api-request-forward@mochitest",
+ }
+) {
+ // Wraps the `backgroundScript` function to be execute in the target
+ // extension global (currently only in a background service worker,
+ // in follow-ups the same function should also be execute in
+ // other supported extension globals, e.g. an extension page and
+ // a content script).
+ //
+ // The test wrapper does also provide to `backgroundScript` some
+ // helpers to be used as part of the test, these tests are meant to
+ // only cover internals shared by all webidl API bindings through a
+ // mock API namespace only available in tests (and so none of the tests
+ // written with this helpers should be using the browser.test API namespace).
+ function backgroundScriptWrapper(testParams, testFn) {
+ const testLog = msg => {
+ // console messages emitted by workers are not visible in the test logs if not
+ // explicitly collected, and so this testLog helper method does use dump for now
+ // (this way the logs will be visibile as part of the test logs).
+ dump(`"${testParams.extensionId}": ${msg}\n`);
+ };
+
+ const testAsserts = {
+ isErrorInstance(err) {
+ if (!(err instanceof Error)) {
+ throw new Error("Unexpected error: not an instance of Error");
+ }
+ return true;
+ },
+ isInstanceOf(value, globalConstructorName) {
+ if (!(value instanceof self[globalConstructorName])) {
+ throw new Error(
+ `Unexpected error: expected instance of ${globalConstructorName}`
+ );
+ }
+ return true;
+ },
+ equal(val, exp, msg) {
+ if (val !== exp) {
+ throw new Error(
+ `Unexpected error: expected ${exp} but got ${val}. ${msg}`
+ );
+ }
+ },
+ };
+
+ testLog(`Evaluating - test case "${testParams.testDescription}"`);
+ self.onmessage = async evt => {
+ testLog(`Running test case "${testParams.testDescription}"`);
+
+ let testError = null;
+ let testResult;
+ try {
+ testResult = await testFn({ testLog, testAsserts });
+ } catch (err) {
+ testError = { message: err.message, stack: err.stack };
+ testLog(`Unexpected test error: ${err} :: ${err.stack}\n`);
+ }
+
+ evt.ports[0].postMessage({ success: !testError, testError, testResult });
+
+ testLog(`Test case "${testParams.testDescription}" executed`);
+ };
+ testLog(`Wait onmessage event - test case "${testParams.testDescription}"`);
+ }
+
+ async function assertTestResult(result) {
+ if (assertResults) {
+ await assertResults(result);
+ } else {
+ equal(result.testError, undefined, "Expect no errors");
+ ok(result.success, "Test completed successfully");
+ }
+ }
+
+ async function runTestCaseInWorker({ page, extension }) {
+ info(`*** Run test case in an extension service worker`);
+ const result = await page.legacySpawn([], async () => {
+ const { active } = await content.navigator.serviceWorker.ready;
+ const { port1, port2 } = new MessageChannel();
+
+ return new Promise(resolve => {
+ port1.onmessage = evt => resolve(evt.data);
+ active.postMessage("run-test", [port2]);
+ });
+ });
+ info(`*** Assert test case results got from extension service worker`);
+ await assertTestResult({ ...result, extension });
+ }
+
+ // NOTE: prefixing this with `function ` is needed because backgroundScript
+ // is an object property and so it is going to be stringified as
+ // `backgroundScript() { ... }` (which would be detected as a syntax error
+ // on the worker script evaluation phase).
+ const scriptFnParam = ExtensionTestCommon.serializeFunction(backgroundScript);
+ const testOptsParam = `${JSON.stringify({ testDescription, extensionId })}`;
+
+ const testExtData = {
+ useAddonManager: "temporary",
+ manifest: {
+ version: "1",
+ background: {
+ service_worker: "test-sw.js",
+ },
+ browser_specific_settings: {
+ gecko: { id: extensionId },
+ },
+ },
+ files: {
+ "page.html": `<!DOCTYPE html>
+ <head><meta charset="utf-8"></head>
+ <body>
+ <script src="test-sw.js"></script>
+ </body>`,
+ "test-sw.js": `
+ (${backgroundScriptWrapper})(${testOptsParam}, ${scriptFnParam});
+ `,
+ },
+ };
+
+ let cleanupCalled = false;
+ let extension;
+ let page;
+ let swReg;
+
+ async function testCleanup() {
+ if (cleanupCalled) {
+ return;
+ }
+
+ cleanupCalled = true;
+ await unmockHandleAPIRequest(page);
+ await page.close();
+ await extension.unload();
+ await waitForTerminatedWorkers(swReg);
+ }
+
+ info(`Start test case "${testDescription}"`);
+ extension = ExtensionTestUtils.loadExtension(testExtData);
+ await extension.startup();
+
+ swReg = getBackgroundServiceWorkerRegistration(extension);
+ ok(swReg, "Extension background.service_worker should be registered");
+
+ page = await ExtensionTestUtils.loadContentPage(
+ `moz-extension://${extension.uuid}/page.html`,
+ { extension }
+ );
+
+ registerCleanupFunction(testCleanup);
+
+ await mockHandleAPIRequest(page, mockAPIRequestHandler);
+ await runTestCaseInWorker({ page, extension });
+ await testCleanup();
+ info(`End test case "${testDescription}"`);
+}
diff --git a/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api.js b/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api.js
new file mode 100644
index 0000000000..489cc3a754
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api.js
@@ -0,0 +1,486 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+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_ext_context_does_have_webidl_bindings() {
+ await runExtensionAPITest("should have a browser global object", {
+ backgroundScript() {
+ const { browser, chrome } = self;
+
+ return {
+ hasExtensionAPI: !!browser,
+ hasExtensionMockAPI: !!browser?.mockExtensionAPI,
+ hasChromeCompatGlobal: !!chrome,
+ hasChromeMockAPI: !!chrome?.mockExtensionAPI,
+ };
+ },
+ assertResults({ testResult, testError }) {
+ Assert.deepEqual(testError, undefined);
+ Assert.deepEqual(
+ testResult,
+ {
+ hasExtensionAPI: true,
+ hasExtensionMockAPI: true,
+ hasChromeCompatGlobal: true,
+ hasChromeMockAPI: true,
+ },
+ "browser and browser.test WebIDL API bindings found"
+ );
+ },
+ });
+});
+
+add_task(async function test_propagated_extension_error() {
+ await runExtensionAPITest(
+ "should throw an extension error on ResultType::EXTENSION_ERROR",
+ {
+ backgroundScript({ testAsserts }) {
+ try {
+ const api = self.browser.mockExtensionAPI;
+ api.methodSyncWithReturn("arg0", 1, { value: "arg2" });
+ } catch (err) {
+ testAsserts.isErrorInstance(err);
+ throw err;
+ }
+ },
+ mockAPIRequestHandler(policy, request) {
+ return {
+ type: Ci.mozIExtensionAPIRequestResult.EXTENSION_ERROR,
+ value: new Error("Fake Extension Error"),
+ };
+ },
+ assertResults({ testError }) {
+ Assert.deepEqual(testError?.message, "Fake Extension Error");
+ },
+ }
+ );
+});
+
+add_task(async function test_system_errors_donot_leak() {
+ function assertResults({ testError }) {
+ ok(
+ testError?.message?.match(/An unexpected error occurred/),
+ `Got the general unexpected error as expected: ${testError?.message}`
+ );
+ }
+
+ function mockAPIRequestHandler(policy, request) {
+ throw new Error("Fake handleAPIRequest exception");
+ }
+
+ const msg =
+ "should throw an unexpected error occurred if handleAPIRequest throws";
+
+ await runExtensionAPITest(`sync method ${msg}`, {
+ backgroundScript({ testAsserts }) {
+ try {
+ self.browser.mockExtensionAPI.methodSyncWithReturn("arg0");
+ } catch (err) {
+ testAsserts.isErrorInstance(err);
+ throw err;
+ }
+ },
+ mockAPIRequestHandler,
+ assertResults,
+ });
+
+ await runExtensionAPITest(`async method ${msg}`, {
+ backgroundScript({ testAsserts }) {
+ try {
+ self.browser.mockExtensionAPI.methodAsync("arg0");
+ } catch (err) {
+ testAsserts.isErrorInstance(err);
+ throw err;
+ }
+ },
+ mockAPIRequestHandler,
+ assertResults,
+ });
+
+ await runExtensionAPITest(`no return method ${msg}`, {
+ backgroundScript({ testAsserts }) {
+ try {
+ self.browser.mockExtensionAPI.methodNoReturn("arg0");
+ } catch (err) {
+ testAsserts.isErrorInstance(err);
+ throw err;
+ }
+ },
+ mockAPIRequestHandler,
+ assertResults,
+ });
+});
+
+add_task(async function test_call_sync_function_result() {
+ await runExtensionAPITest(
+ "sync API methods should support structured clonable return values",
+ {
+ backgroundScript({ testAsserts }) {
+ const api = self.browser.mockExtensionAPI;
+ const results = {
+ string: api.methodSyncWithReturn("string-result"),
+ nested_prop: api.methodSyncWithReturn({
+ string: "123",
+ number: 123,
+ date: new Date("2020-09-20"),
+ map: new Map([
+ ["a", 1],
+ ["b", 2],
+ ]),
+ }),
+ };
+
+ testAsserts.isInstanceOf(results.nested_prop.date, "Date");
+ testAsserts.isInstanceOf(results.nested_prop.map, "Map");
+ return results;
+ },
+ mockAPIRequestHandler(policy, request) {
+ if (request.apiName === "methodSyncWithReturn") {
+ // Return the first argument unmodified, which will be checked in the
+ // resultAssertFn above.
+ return {
+ type: Ci.mozIExtensionAPIRequestResult.RETURN_VALUE,
+ value: request.args[0],
+ };
+ }
+ throw new Error("Unexpected API method");
+ },
+ assertResults({ testResult, testError }) {
+ Assert.deepEqual(testError, null, "Got no error as expected");
+ Assert.deepEqual(testResult, {
+ string: "string-result",
+ nested_prop: {
+ string: "123",
+ number: 123,
+ date: new Date("2020-09-20"),
+ map: new Map([
+ ["a", 1],
+ ["b", 2],
+ ]),
+ },
+ });
+ },
+ }
+ );
+});
+
+add_task(async function test_call_sync_fn_missing_return() {
+ await runExtensionAPITest(
+ "should throw an unexpected error occurred on missing return value",
+ {
+ backgroundScript() {
+ self.browser.mockExtensionAPI.methodSyncWithReturn("arg0");
+ },
+ mockAPIRequestHandler(policy, request) {
+ return undefined;
+ },
+ assertResults({ testError }) {
+ ok(
+ testError?.message?.match(/An unexpected error occurred/),
+ `Got the general unexpected error as expected: ${testError?.message}`
+ );
+ },
+ }
+ );
+});
+
+add_task(async function test_call_async_throw_extension_error() {
+ await runExtensionAPITest(
+ "an async function can throw an error occurred for param validation errors",
+ {
+ backgroundScript({ testAsserts }) {
+ try {
+ self.browser.mockExtensionAPI.methodAsync("arg0");
+ } catch (err) {
+ testAsserts.isErrorInstance(err);
+ throw err;
+ }
+ },
+ mockAPIRequestHandler(policy, request) {
+ return {
+ type: Ci.mozIExtensionAPIRequestResult.EXTENSION_ERROR,
+ value: new Error("Fake Param Validation Error"),
+ };
+ },
+ assertResults({ testError }) {
+ Assert.deepEqual(testError?.message, "Fake Param Validation Error");
+ },
+ }
+ );
+});
+
+add_task(async function test_call_async_reject_error() {
+ await runExtensionAPITest(
+ "an async function rejected promise should propagate extension errors",
+ {
+ async backgroundScript({ testAsserts }) {
+ try {
+ await self.browser.mockExtensionAPI.methodAsync("arg0");
+ } catch (err) {
+ testAsserts.isErrorInstance(err);
+ throw err;
+ }
+ },
+ mockAPIRequestHandler(policy, request) {
+ return {
+ type: Ci.mozIExtensionAPIRequestResult.RETURN_VALUE,
+ value: Promise.reject(new Error("Fake API rejected error object")),
+ };
+ },
+ assertResults({ testError }) {
+ Assert.deepEqual(testError?.message, "Fake API rejected error object");
+ },
+ }
+ );
+});
+
+add_task(async function test_call_async_function_result() {
+ await runExtensionAPITest(
+ "async API methods should support structured clonable resolved values",
+ {
+ async backgroundScript({ testAsserts }) {
+ const api = self.browser.mockExtensionAPI;
+ const results = {
+ string: await api.methodAsync("string-result"),
+ nested_prop: await api.methodAsync({
+ string: "123",
+ number: 123,
+ date: new Date("2020-09-20"),
+ map: new Map([
+ ["a", 1],
+ ["b", 2],
+ ]),
+ }),
+ };
+
+ testAsserts.isInstanceOf(results.nested_prop.date, "Date");
+ testAsserts.isInstanceOf(results.nested_prop.map, "Map");
+ return results;
+ },
+ mockAPIRequestHandler(policy, request) {
+ if (request.apiName === "methodAsync") {
+ // Return the first argument unmodified, which will be checked in the
+ // resultAssertFn above.
+ return {
+ type: Ci.mozIExtensionAPIRequestResult.RETURN_VALUE,
+ value: Promise.resolve(request.args[0]),
+ };
+ }
+ throw new Error("Unexpected API method");
+ },
+ assertResults({ testResult, testError }) {
+ Assert.deepEqual(testError, null, "Got no error as expected");
+ Assert.deepEqual(testResult, {
+ string: "string-result",
+ nested_prop: {
+ string: "123",
+ number: 123,
+ date: new Date("2020-09-20"),
+ map: new Map([
+ ["a", 1],
+ ["b", 2],
+ ]),
+ },
+ });
+ },
+ }
+ );
+});
+
+add_task(async function test_call_no_return_throw_extension_error() {
+ await runExtensionAPITest(
+ "no return function call throw an error occurred for param validation errors",
+ {
+ backgroundScript({ testAsserts }) {
+ try {
+ self.browser.mockExtensionAPI.methodNoReturn("arg0");
+ } catch (err) {
+ testAsserts.isErrorInstance(err);
+ throw err;
+ }
+ },
+ mockAPIRequestHandler(policy, request) {
+ return {
+ type: Ci.mozIExtensionAPIRequestResult.EXTENSION_ERROR,
+ value: new Error("Fake Param Validation Error"),
+ };
+ },
+ assertResults({ testError }) {
+ Assert.deepEqual(testError?.message, "Fake Param Validation Error");
+ },
+ }
+ );
+});
+
+add_task(async function test_call_no_return_without_errors() {
+ await runExtensionAPITest(
+ "handleAPIHandler can return undefined on api calls to methods with no return",
+ {
+ backgroundScript() {
+ self.browser.mockExtensionAPI.methodNoReturn("arg0");
+ },
+ mockAPIRequestHandler(policy, request) {
+ return undefined;
+ },
+ assertResults({ testError }) {
+ Assert.deepEqual(testError, null, "Got no error as expected");
+ },
+ }
+ );
+});
+
+add_task(async function test_async_method_chrome_compatible_callback() {
+ function mockAPIRequestHandler(policy, request) {
+ if (request.args[0] === "fake-async-method-failure") {
+ return {
+ type: Ci.mozIExtensionAPIRequestResult.RETURN_VALUE,
+ value: Promise.reject("this-should-not-be-passed-to-cb-as-parameter"),
+ };
+ }
+
+ return {
+ type: Ci.mozIExtensionAPIRequestResult.RETURN_VALUE,
+ value: Promise.resolve(request.args),
+ };
+ }
+
+ await runExtensionAPITest(
+ "async method should support an optional chrome-compatible callback",
+ {
+ mockAPIRequestHandler,
+ async backgroundScript({ testAsserts }) {
+ const api = self.browser.mockExtensionAPI;
+ const success_cb_params = await new Promise(resolve => {
+ const res = api.methodAsync(
+ { prop: "fake-async-method-success" },
+ (...results) => {
+ resolve(results);
+ }
+ );
+ testAsserts.equal(res, undefined, "no promise should be returned");
+ });
+ const error_cb_params = await new Promise(resolve => {
+ const res = api.methodAsync(
+ "fake-async-method-failure",
+ (...results) => {
+ resolve(results);
+ }
+ );
+ testAsserts.equal(res, undefined, "no promise should be returned");
+ });
+ return { success_cb_params, error_cb_params };
+ },
+ assertResults({ testError, testResult }) {
+ Assert.deepEqual(testError, null, "Got no error as expected");
+ Assert.deepEqual(
+ testResult,
+ {
+ success_cb_params: [[{ prop: "fake-async-method-success" }]],
+ error_cb_params: [],
+ },
+ "Got the expected results from the chrome compatible callbacks"
+ );
+ },
+ }
+ );
+
+ await runExtensionAPITest(
+ "async method with ambiguous args called with a chrome-compatible callback",
+ {
+ mockAPIRequestHandler,
+ async backgroundScript({ testAsserts }) {
+ const api = self.browser.mockExtensionAPI;
+ const success_cb_params = await new Promise(resolve => {
+ const res = api.methodAmbiguousArgsAsync(
+ "arg0",
+ { prop: "arg1" },
+ 3,
+ (...results) => {
+ resolve(results);
+ }
+ );
+ testAsserts.equal(res, undefined, "no promise should be returned");
+ });
+ const error_cb_params = await new Promise(resolve => {
+ const res = api.methodAmbiguousArgsAsync(
+ "fake-async-method-failure",
+ (...results) => {
+ resolve(results);
+ }
+ );
+ testAsserts.equal(res, undefined, "no promise should be returned");
+ });
+ return { success_cb_params, error_cb_params };
+ },
+ assertResults({ testError, testResult }) {
+ Assert.deepEqual(testError, null, "Got no error as expected");
+ Assert.deepEqual(
+ testResult,
+ {
+ success_cb_params: [["arg0", { prop: "arg1" }, 3]],
+ error_cb_params: [],
+ },
+ "Got the expected results from the chrome compatible callbacks"
+ );
+ },
+ }
+ );
+});
+
+add_task(async function test_get_property() {
+ await runExtensionAPITest(
+ "getProperty API request does return a value synchrously",
+ {
+ backgroundScript() {
+ return self.browser.mockExtensionAPI.propertyAsString;
+ },
+ mockAPIRequestHandler(policy, request) {
+ return {
+ type: Ci.mozIExtensionAPIRequestResult.RETURN_VALUE,
+ value: "property-value",
+ };
+ },
+ assertResults({ testError, testResult }) {
+ Assert.deepEqual(testError, null, "Got no error as expected");
+ Assert.deepEqual(
+ testResult,
+ "property-value",
+ "Got the expected result"
+ );
+ },
+ }
+ );
+
+ await runExtensionAPITest(
+ "getProperty API request can return an error object",
+ {
+ backgroundScript({ testAsserts }) {
+ const errObj = self.browser.mockExtensionAPI.propertyAsErrorObject;
+ testAsserts.isErrorInstance(errObj);
+ testAsserts.equal(errObj.message, "fake extension error");
+ },
+ mockAPIRequestHandler(policy, request) {
+ let savedFrame = request.calledSavedFrame;
+ return {
+ type: Ci.mozIExtensionAPIRequestResult.RETURN_VALUE,
+ value: ChromeUtils.createError("fake extension error", savedFrame),
+ };
+ },
+ assertResults({ testError, testResult }) {
+ Assert.deepEqual(testError, null, "Got no error as expected");
+ },
+ }
+ );
+});
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"
+ );
+});
diff --git a/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_request_handler.js b/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_request_handler.js
new file mode 100644
index 0000000000..070a45fa95
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_request_handler.js
@@ -0,0 +1,443 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+AddonTestUtils.init(this);
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "42"
+);
+
+add_task(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+});
+
+// Verify ExtensionAPIRequestHandler handling API requests for
+// an ext-*.js API module running in the local process
+// (toolkit/components/extensions/child/ext-test.js).
+add_task(async function test_sw_api_request_handling_local_process_api() {
+ const extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ background: {
+ service_worker: "sw.js",
+ },
+ browser_specific_settings: { gecko: { id: "test-bg-sw@mochi.test" } },
+ },
+ files: {
+ "page.html": "<!DOCTYPE html><body></body>",
+ "sw.js": async function () {
+ browser.test.onMessage.addListener(async msg => {
+ browser.test.succeed("call to test.succeed");
+ browser.test.assertTrue(true, "call to test.assertTrue");
+ browser.test.assertFalse(false, "call to test.assertFalse");
+ // Smoke test assertEq (more complete coverage of the behavior expected
+ // by the test API will be introduced in test_ext_test.html as part of
+ // Bug 1723785).
+ const errorObject = new Error("fake_error_message");
+ browser.test.assertEq(
+ errorObject,
+ errorObject,
+ "call to test.assertEq"
+ );
+
+ // Smoke test for assertThrows/assertRejects.
+ const errorMatchingTestCases = [
+ ["expected error instance", errorObject],
+ ["expected error message string", "fake_error_message"],
+ ["expected regexp", /fake_error/],
+ ["matching function", error => errorObject === error],
+ ["matching Constructor", Error],
+ ];
+
+ browser.test.log("run assertThrows smoke tests");
+
+ const throwFn = () => {
+ throw errorObject;
+ };
+ for (const [msg, expected] of errorMatchingTestCases) {
+ browser.test.assertThrows(
+ throwFn,
+ expected,
+ `call to assertThrow with ${msg}`
+ );
+ }
+
+ browser.test.log("run assertRejects smoke tests");
+
+ const rejectedPromise = Promise.reject(errorObject);
+ for (const [msg, expected] of errorMatchingTestCases) {
+ await browser.test.assertRejects(
+ rejectedPromise,
+ expected,
+ `call to assertRejects with ${msg}`
+ );
+ }
+
+ browser.test.notifyPass("test-completed");
+ });
+ browser.test.sendMessage("bgsw-ready");
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("bgsw-ready");
+ extension.sendMessage("test-message-ok");
+ await extension.awaitFinish();
+ await extension.unload();
+});
+
+// Verify ExtensionAPIRequestHandler handling API requests for
+// an ext-*.js API module running in the main process
+// (toolkit/components/extensions/parent/ext-alarms.js).
+add_task(async function test_sw_api_request_handling_main_process_api() {
+ const extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ background: {
+ service_worker: "sw.js",
+ },
+ permissions: ["alarms"],
+ browser_specific_settings: { gecko: { id: "test-bg-sw@mochi.test" } },
+ },
+ files: {
+ "page.html": "<!DOCTYPE html><body></body>",
+ "sw.js": async function () {
+ browser.alarms.create("test-alarm", { when: Date.now() + 2000000 });
+ const all = await browser.alarms.getAll();
+ if (all.length === 1 && all[0].name === "test-alarm") {
+ browser.test.succeed("Got the expected alarms");
+ } else {
+ browser.test.fail(
+ `browser.alarms.create didn't create the expected alarm: ${JSON.stringify(
+ all
+ )}`
+ );
+ }
+
+ browser.alarms.onAlarm.addListener(alarm => {
+ if (alarm.name === "test-onAlarm") {
+ browser.test.succeed("Got the expected onAlarm event");
+ } else {
+ browser.test.fail(`Got unexpected onAlarm event: ${alarm.name}`);
+ }
+ browser.test.sendMessage("test-completed");
+ });
+
+ browser.alarms.create("test-onAlarm", { when: Date.now() + 1000 });
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("test-completed");
+ await extension.unload();
+});
+
+add_task(async function test_sw_api_request_bgsw_runtime_onMessage() {
+ const extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ background: {
+ service_worker: "sw.js",
+ },
+ permissions: [],
+ browser_specific_settings: {
+ gecko: { id: "test-bg-sw-on-message@mochi.test" },
+ },
+ },
+ files: {
+ "page.html": '<!DOCTYPE html><script src="page.js"></script>',
+ "page.js": async function () {
+ browser.test.onMessage.addListener(msg => {
+ if (msg !== "extpage-send-message") {
+ browser.test.fail(`Unexpected message received: ${msg}`);
+ return;
+ }
+ browser.runtime.sendMessage("extpage-send-message");
+ });
+ },
+ "sw.js": async function () {
+ browser.runtime.onMessage.addListener(msg => {
+ browser.test.sendMessage("bgsw-on-message", msg);
+ });
+ const extURL = browser.runtime.getURL("/");
+ browser.test.sendMessage("ext-url", extURL);
+ },
+ },
+ });
+
+ await extension.startup();
+ const extURL = await extension.awaitMessage("ext-url");
+ equal(
+ extURL,
+ `moz-extension://${extension.uuid}/`,
+ "Got the expected extension url"
+ );
+
+ const extPage = await ExtensionTestUtils.loadContentPage(
+ `${extURL}/page.html`,
+ { extension }
+ );
+ extension.sendMessage("extpage-send-message");
+
+ const msg = await extension.awaitMessage("bgsw-on-message");
+ equal(msg, "extpage-send-message", "Got the expected message");
+ await extPage.close();
+ await extension.unload();
+});
+
+add_task(async function test_sw_api_request_bgsw_runtime_sendMessage() {
+ const extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ background: {
+ service_worker: "sw.js",
+ },
+ permissions: [],
+ browser_specific_settings: {
+ gecko: { id: "test-bg-sw-sendMessage@mochi.test" },
+ },
+ },
+ files: {
+ "page.html": '<!DOCTYPE html><script src="page.js"></script>',
+ "page.js": async function () {
+ browser.runtime.onMessage.addListener(msg => {
+ browser.test.sendMessage("extpage-on-message", msg);
+ });
+
+ browser.test.sendMessage("extpage-ready");
+ },
+ "sw.js": async function () {
+ browser.test.onMessage.addListener(msg => {
+ if (msg !== "bgsw-send-message") {
+ browser.test.fail(`Unexpected message received: ${msg}`);
+ return;
+ }
+ browser.runtime.sendMessage("bgsw-send-message");
+ });
+ const extURL = browser.runtime.getURL("/");
+ browser.test.sendMessage("ext-url", extURL);
+ },
+ },
+ });
+
+ await extension.startup();
+ const extURL = await extension.awaitMessage("ext-url");
+ equal(
+ extURL,
+ `moz-extension://${extension.uuid}/`,
+ "Got the expected extension url"
+ );
+
+ const extPage = await ExtensionTestUtils.loadContentPage(
+ `${extURL}/page.html`,
+ { extension }
+ );
+ await extension.awaitMessage("extpage-ready");
+ extension.sendMessage("bgsw-send-message");
+
+ const msg = await extension.awaitMessage("extpage-on-message");
+ equal(msg, "bgsw-send-message", "Got the expected message");
+ await extPage.close();
+ await extension.unload();
+});
+
+// Verify ExtensionAPIRequestHandler handling API requests that
+// returns a runtinme.Port API object.
+add_task(async function test_sw_api_request_bgsw_connnect_runtime_port() {
+ const extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ background: {
+ service_worker: "sw.js",
+ },
+ permissions: [],
+ browser_specific_settings: { gecko: { id: "test-bg-sw@mochi.test" } },
+ },
+ files: {
+ "page.html": '<!DOCTYPE html><script src="page.js"></script>',
+ "page.js": async function () {
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.sendMessage("page-got-port-from-sw");
+ port.postMessage("page-to-sw");
+ });
+ browser.test.sendMessage("page-waiting-port");
+ },
+ "sw.js": async function () {
+ browser.test.onMessage.addListener(msg => {
+ if (msg !== "connect-port") {
+ return;
+ }
+ const port = browser.runtime.connect();
+ if (!port) {
+ browser.test.fail("Got an undefined port");
+ }
+ port.onMessage.addListener((msg, portArgument) => {
+ browser.test.assertTrue(
+ port === portArgument,
+ "Got the expected runtime.Port instance"
+ );
+ browser.test.sendMessage("test-done", msg);
+ });
+ browser.test.sendMessage("sw-waiting-port-message");
+ });
+
+ const portWithError = browser.runtime.connect();
+ portWithError.onDisconnect.addListener(() => {
+ const portError = portWithError.error;
+ browser.test.sendMessage("port-error", {
+ isError: portError instanceof Error,
+ message: portError?.message,
+ });
+ });
+
+ const extURL = browser.runtime.getURL("/");
+ browser.test.sendMessage("ext-url", extURL);
+ browser.test.sendMessage("ext-id", browser.runtime.id);
+ },
+ },
+ });
+
+ await extension.startup();
+ const extURL = await extension.awaitMessage("ext-url");
+ equal(
+ extURL,
+ `moz-extension://${extension.uuid}/`,
+ "Got the expected extension url"
+ );
+
+ const extId = await extension.awaitMessage("ext-id");
+ equal(extId, extension.id, "Got the expected extension id");
+
+ const lastError = await extension.awaitMessage("port-error");
+ Assert.deepEqual(
+ lastError,
+ {
+ isError: true,
+ message: "Could not establish connection. Receiving end does not exist.",
+ },
+ "Got the expected lastError value"
+ );
+
+ const extPage = await ExtensionTestUtils.loadContentPage(
+ `${extURL}/page.html`,
+ { extension }
+ );
+ await extension.awaitMessage("page-waiting-port");
+
+ info("bgsw connect port");
+ extension.sendMessage("connect-port");
+ await extension.awaitMessage("sw-waiting-port-message");
+ info("bgsw waiting port message");
+ await extension.awaitMessage("page-got-port-from-sw");
+ info("page got port from sw, wait to receive event");
+ const msg = await extension.awaitMessage("test-done");
+ equal(msg, "page-to-sw", "Got the expected message");
+ await extPage.close();
+ await extension.unload();
+});
+
+// Verify ExtensionAPIRequestHandler handling API events that should
+// get a runtinme.Port API object as an event argument.
+add_task(async function test_sw_api_request_bgsw_runtime_onConnect() {
+ const extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ background: {
+ service_worker: "sw.js",
+ },
+ permissions: [],
+ browser_specific_settings: {
+ gecko: { id: "test-bg-sw-onConnect@mochi.test" },
+ },
+ },
+ files: {
+ "page.html": '<!DOCTYPE html><script src="page.js"></script>',
+ "page.js": async function () {
+ browser.test.onMessage.addListener(msg => {
+ if (msg !== "connect-port") {
+ return;
+ }
+ const port = browser.runtime.connect();
+ port.onMessage.addListener(msg => {
+ browser.test.sendMessage("test-done", msg);
+ });
+ browser.test.sendMessage("page-waiting-port-message");
+ });
+ },
+ "sw.js": async function () {
+ try {
+ const extURL = browser.runtime.getURL("/");
+ browser.test.sendMessage("ext-url", extURL);
+
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.sendMessage("bgsw-got-port-from-page");
+ port.postMessage("sw-to-page");
+ });
+ browser.test.sendMessage("bgsw-waiting-port");
+ } catch (err) {
+ browser.test.fail(`Error on runtime.onConnect: ${err}`);
+ }
+ },
+ },
+ });
+
+ await extension.startup();
+ const extURL = await extension.awaitMessage("ext-url");
+ equal(
+ extURL,
+ `moz-extension://${extension.uuid}/`,
+ "Got the expected extension url"
+ );
+ await extension.awaitMessage("bgsw-waiting-port");
+
+ const extPage = await ExtensionTestUtils.loadContentPage(
+ `${extURL}/page.html`,
+ { extension }
+ );
+ info("ext page connect port");
+ extension.sendMessage("connect-port");
+
+ await extension.awaitMessage("page-waiting-port-message");
+ info("page waiting port message");
+ await extension.awaitMessage("bgsw-got-port-from-page");
+ info("bgsw got port from page, page wait to receive event");
+ const msg = await extension.awaitMessage("test-done");
+ equal(msg, "sw-to-page", "Got the expected message");
+ await extPage.close();
+ await extension.unload();
+});
+
+add_task(async function test_sw_runtime_lastError() {
+ const extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ background: {
+ service_worker: "sw.js",
+ },
+ browser_specific_settings: { gecko: { id: "test-bg-sw@mochi.test" } },
+ },
+ files: {
+ "page.html": "<!DOCTYPE html><body></body>",
+ "sw.js": async function () {
+ browser.runtime.sendMessage(() => {
+ const lastError = browser.runtime.lastError;
+ if (!(lastError instanceof Error)) {
+ browser.test.fail(
+ `lastError isn't an Error instance: ${lastError}`
+ );
+ }
+ browser.test.sendMessage("test-lastError-completed");
+ });
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("test-lastError-completed");
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_schema_errors.js b/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_schema_errors.js
new file mode 100644
index 0000000000..d8684c1574
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_schema_errors.js
@@ -0,0 +1,202 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { ExtensionAPI } = ExtensionCommon;
+
+AddonTestUtils.init(this);
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "42"
+);
+
+// Because the `mockExtensionAPI` is currently the only "mock" API that has
+// WebIDL bindings, this is the only namespace we can use in our tests. There
+// is no JSON schema for this namespace so we add one here that is tailored for
+// our testing needs.
+const API = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ mockExtensionAPI: {
+ methodAsync: () => {
+ return "some-value";
+ },
+ },
+ };
+ }
+};
+
+const SCHEMA = [
+ {
+ namespace: "mockExtensionAPI",
+ functions: [
+ {
+ name: "methodAsync",
+ type: "function",
+ async: true,
+ parameters: [
+ {
+ name: "arg",
+ type: "string",
+ enum: ["THE_ONLY_VALUE_ALLOWED"],
+ },
+ ],
+ },
+ ],
+ },
+];
+
+add_setup(async function () {
+ await AddonTestUtils.promiseStartupManager();
+
+ // The blob:-URL registered in `registerModules()` below gets loaded at:
+ // https://searchfox.org/mozilla-central/rev/0fec57c05d3996cc00c55a66f20dd5793a9bfb5d/toolkit/components/extensions/ExtensionCommon.jsm#1649
+ Services.prefs.setBoolPref(
+ "security.allow_parent_unrestricted_js_loads",
+ true
+ );
+
+ ExtensionParent.apiManager.registerModules({
+ mockExtensionAPI: {
+ schema: `data:,${JSON.stringify(SCHEMA)}`,
+ scopes: ["addon_parent"],
+ paths: [["mockExtensionAPI"]],
+ url: URL.createObjectURL(
+ new Blob([`this.mockExtensionAPI = ${API.toString()}`])
+ ),
+ },
+ });
+});
+
+add_task(async function test_schema_error_is_propagated_to_extension() {
+ await runExtensionAPITest("should throw an extension error", {
+ backgroundScript() {
+ return browser.mockExtensionAPI.methodAsync("UNEXPECTED_VALUE");
+ },
+ mockAPIRequestHandler(policy, request) {
+ return this._handleAPIRequest_orig(policy, request);
+ },
+ assertResults({ testError }) {
+ Assert.ok(
+ /Invalid enumeration value "UNEXPECTED_VALUE"/.test(testError.message)
+ );
+ },
+ });
+});
+
+add_task(async function test_schema_error_no_error_with_expected_value() {
+ await runExtensionAPITest("should not throw any error", {
+ backgroundScript() {
+ return browser.mockExtensionAPI.methodAsync("THE_ONLY_VALUE_ALLOWED");
+ },
+ mockAPIRequestHandler(policy, request) {
+ return this._handleAPIRequest_orig(policy, request);
+ },
+ assertResults({ testError, testResult }) {
+ Assert.deepEqual(testError, undefined);
+ Assert.deepEqual(testResult, "some-value");
+ },
+ });
+});
+
+add_task(async function test_schema_data_not_found_or_unexpected_schema_type() {
+ const { Schemas } = ChromeUtils.importESModule(
+ "resource://gre/modules/Schemas.sys.mjs"
+ );
+
+ const mockSchemaExtContext = {};
+
+ const testSchemasErrorOnWebIDLRequest = testCase => {
+ if (testCase.expectedExceptions) {
+ const expectedExceptions = Array.isArray(testCase.expectedExceptions)
+ ? testCase.expectedExceptions
+ : [testCase.expectedExceptions];
+ expectedExceptions.forEach(expectedException =>
+ Assert.throws(
+ () =>
+ Schemas.checkWebIDLRequestParameters(
+ testCase.mockSchemaExtContext,
+ testCase.mockWebIDLAPIRequest
+ ),
+ expectedException,
+ `Got the expected error on ${testCase.description}`
+ )
+ );
+ } else {
+ throw new Error(
+ `Test case ${testCase.description} is missing mandatory expectedExceptions test case property`
+ );
+ }
+ };
+
+ const TEST_CASES = [
+ {
+ description:
+ "callFunction API request for non existing nested API namespace",
+ mockSchemaExtContext,
+ mockWebIDLAPIRequest: {
+ apiNamespace: "browserSettings.unknownNamespace",
+ apiName: "get",
+ requestType: "callFunction",
+ },
+ expectedExceptions:
+ /API Schema not found for browserSettings\.unknownNamespace/,
+ },
+ {
+ description:
+ "addListener API request for non existing API event property",
+ mockSchemaExtContext,
+ mockWebIDLAPIRequest: {
+ apiNamespace: "browserSettings.nonExistingSetting",
+ apiName: "onChange",
+ requestType: "addListener",
+ },
+ expectedExceptions:
+ /API Schema not found for browserSettings\.nonExistingSetting/,
+ },
+ {
+ description:
+ "callFunction on non existing method from existing nested API namespace",
+ mockSchemaExtContext,
+ mockWebIDLAPIRequest: {
+ apiNamespace: "browserSettings.colorManagement.mode",
+ apiName: "nonExistingMethod",
+ requestType: "callFunction",
+ },
+ expectedExceptions: [
+ /API Schema for "nonExistingMethod" not found in browserSettings\.colorManagement\.mode/,
+ /\(browserSettings\.colorManagement\.mode type is SubModuleProperty\)/,
+ ],
+ },
+ {
+ description:
+ "callFunction on non existing method from existing API namespace",
+ mockSchemaExtContext,
+ mockWebIDLAPIRequest: {
+ apiNamespace: "browserSettings",
+ apiName: "nonExistingMethod",
+ requestType: "callFunction",
+ },
+ expectedExceptions:
+ /API Schema not found for browserSettings\.nonExistingMethod/,
+ },
+ {
+ description:
+ "callFunction on existing property but unexpected schema type",
+ mockSchemaExtContext,
+ mockWebIDLAPIRequest: {
+ apiNamespace: "tabs",
+ apiName: "TAB_ID_NONE",
+ requestType: "callFunction",
+ },
+ expectedExceptions: [
+ /Unexpected API Schema type for tabs.TAB_ID_NONE/,
+ /tabs.TAB_ID_NONE type is ValueProperty/,
+ ],
+ },
+ ];
+
+ TEST_CASES.forEach(testSchemasErrorOnWebIDLRequest);
+});
diff --git a/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_schema_formatters.js b/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_schema_formatters.js
new file mode 100644
index 0000000000..a7310f345e
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_schema_formatters.js
@@ -0,0 +1,99 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { ExtensionAPI } = ExtensionCommon;
+
+AddonTestUtils.init(this);
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "42"
+);
+
+// Because the `mockExtensionAPI` is currently the only "mock" API that has
+// WebIDL bindings, this is the only namespace we can use in our tests. There
+// is no JSON schema for this namespace so we add one here that is tailored for
+// our testing needs.
+const API = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ mockExtensionAPI: {
+ methodAsync: files => {
+ return files;
+ },
+ },
+ };
+ }
+};
+
+const SCHEMA = [
+ {
+ namespace: "mockExtensionAPI",
+ functions: [
+ {
+ name: "methodAsync",
+ type: "function",
+ async: true,
+ parameters: [
+ {
+ name: "files",
+ type: "array",
+ items: { $ref: "manifest.ExtensionURL" },
+ },
+ ],
+ },
+ ],
+ },
+];
+
+add_setup(async function () {
+ await AddonTestUtils.promiseStartupManager();
+
+ // The blob:-URL registered in `registerModules()` below gets loaded at:
+ // https://searchfox.org/mozilla-central/rev/0fec57c05d3996cc00c55a66f20dd5793a9bfb5d/toolkit/components/extensions/ExtensionCommon.jsm#1649
+ Services.prefs.setBoolPref(
+ "security.allow_parent_unrestricted_js_loads",
+ true
+ );
+
+ ExtensionParent.apiManager.registerModules({
+ mockExtensionAPI: {
+ schema: `data:,${JSON.stringify(SCHEMA)}`,
+ scopes: ["addon_parent"],
+ paths: [["mockExtensionAPI"]],
+ url: URL.createObjectURL(
+ new Blob([`this.mockExtensionAPI = ${API.toString()}`])
+ ),
+ },
+ });
+});
+
+add_task(async function test_relative_urls() {
+ await runExtensionAPITest(
+ "should format arguments with the relativeUrl formatter",
+ {
+ backgroundScript() {
+ return browser.mockExtensionAPI.methodAsync([
+ "script-1.js",
+ "script-2.js",
+ ]);
+ },
+ mockAPIRequestHandler(policy, request) {
+ return this._handleAPIRequest_orig(policy, request);
+ },
+ assertResults({ testResult, testError, extension }) {
+ Assert.deepEqual(
+ testResult,
+ [
+ `moz-extension://${extension.uuid}/script-1.js`,
+ `moz-extension://${extension.uuid}/script-2.js`,
+ ],
+ "expected correct url"
+ );
+ Assert.deepEqual(testError, undefined, "expected no error");
+ },
+ }
+ );
+});
diff --git a/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_runtime_port.js b/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_runtime_port.js
new file mode 100644
index 0000000000..0d88014f32
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_runtime_port.js
@@ -0,0 +1,220 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+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_method_return_runtime_port() {
+ await runExtensionAPITest("API method returns an ExtensionPort instance", {
+ backgroundScript({ testAsserts, testLog }) {
+ try {
+ browser.mockExtensionAPI.methodReturnsPort("port-create-error");
+ throw new Error("methodReturnsPort should have raised an exception");
+ } catch (err) {
+ testAsserts.equal(
+ err?.message,
+ "An unexpected error occurred",
+ "Got the expected error"
+ );
+ }
+ const port = browser.mockExtensionAPI.methodReturnsPort(
+ "port-create-success"
+ );
+ testAsserts.equal(!!port, true, "Got a port");
+ testAsserts.equal(
+ typeof port.name,
+ "string",
+ "port.name should be a string"
+ );
+ testAsserts.equal(
+ typeof port.sender,
+ "object",
+ "port.sender should be an object"
+ );
+ testAsserts.equal(
+ typeof port.disconnect,
+ "function",
+ "port.disconnect method"
+ );
+ testAsserts.equal(
+ typeof port.postMessage,
+ "function",
+ "port.postMessage method"
+ );
+ testAsserts.equal(
+ typeof port.onDisconnect?.addListener,
+ "function",
+ "port.onDisconnect.addListener method"
+ );
+ testAsserts.equal(
+ typeof port.onMessage?.addListener,
+ "function",
+ "port.onDisconnect.addListener method"
+ );
+ return new Promise(resolve => {
+ let messages = [];
+ port.onDisconnect.addListener(() => resolve(messages));
+ port.onMessage.addListener((...args) => {
+ messages.push(args);
+ });
+ });
+ },
+ assertResults({ testError, testResult }) {
+ Assert.deepEqual(testError, null, "Got no error as expected");
+ Assert.deepEqual(
+ testResult,
+ [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ ],
+ "Got the expected results"
+ );
+ },
+ mockAPIRequestHandler(policy, request) {
+ if (request.apiName == "methodReturnsPort") {
+ if (request.args[0] == "port-create-error") {
+ return {
+ type: Ci.mozIExtensionAPIRequestResult.RETURN_VALUE,
+ value: "not-a-valid-port",
+ };
+ }
+ return {
+ type: Ci.mozIExtensionAPIRequestResult.RETURN_VALUE,
+ value: {
+ portId: "port-id-1",
+ name: "a-port-name",
+ },
+ };
+ } else if (request.requestType == "addListener") {
+ if (request.apiObjectType !== "Port") {
+ throw new Error(`Unexpected objectType ${request}`);
+ }
+
+ switch (request.apiName) {
+ case "onDisconnect":
+ this._onDisconnectCb = request.eventListener;
+ return;
+ case "onMessage":
+ Promise.resolve().then(async () => {
+ await request.eventListener.callListener([1, 2]);
+ await request.eventListener.callListener([3, 4]);
+ await request.eventListener.callListener([5, 6]);
+ this._onDisconnectCb.callListener([]);
+ });
+ return;
+ }
+ } else if (
+ request.requestType == "getProperty" &&
+ request.apiObjectType == "Port" &&
+ request.apiName == "sender"
+ ) {
+ return {
+ type: Ci.mozIExtensionAPIRequestResult.RETURN_VALUE,
+ value: { id: "fake-sender-id-prop" },
+ };
+ }
+
+ throw new Error(`Unexpected request: ${request}`);
+ },
+ });
+});
+
+add_task(async function test_port_as_event_listener_eventListener_param() {
+ await runExtensionAPITest(
+ "API event eventListener received an ExtensionPort parameter",
+ {
+ backgroundScript({ testAsserts, testLog }) {
+ const api = browser.mockExtensionAPI;
+ let listener;
+
+ return new Promise((resolve, reject) => {
+ testLog("addListener and wait for event to be fired");
+ listener = port => {
+ try {
+ testAsserts.equal(!!port, true, "Got a port parameter");
+ testAsserts.equal(
+ port.name,
+ "a-port-name-2",
+ "Got expected port.name value"
+ );
+ testAsserts.equal(
+ typeof port.disconnect,
+ "function",
+ "port.disconnect method"
+ );
+ testAsserts.equal(
+ typeof port.postMessage,
+ "function",
+ "port.disconnect method"
+ );
+ port.onMessage.addListener((msg, portArg) => {
+ if (msg === "test-done") {
+ testLog("Got a port.onMessage event");
+ testAsserts.equal(
+ portArg?.name,
+ "a-port-name-2",
+ "Got port as last argument"
+ );
+ testAsserts.equal(
+ portArg === port,
+ true,
+ "Got the same port instance as expected"
+ );
+ resolve();
+ } else {
+ reject(
+ new Error(
+ `port.onMessage got an unexpected message: ${msg}`
+ )
+ );
+ }
+ });
+ } catch (err) {
+ reject(err);
+ }
+ };
+ api.onTestEvent.addListener(listener);
+ });
+ },
+ assertResults({ testError }) {
+ Assert.deepEqual(testError, null, "Got no error as expected");
+ },
+ mockAPIRequestHandler(policy, request) {
+ if (
+ request.requestType == "addListener" &&
+ request.apiName == "onTestEvent"
+ ) {
+ request.eventListener.callListener(["arg0", "arg1"], {
+ apiObjectType: Ci.mozIExtensionListenerCallOptions.RUNTIME_PORT,
+ apiObjectDescriptor: { portId: "port-id-2", name: "a-port-name-2" },
+ apiObjectPrepended: true,
+ });
+ return;
+ } else if (
+ request.requestType == "addListener" &&
+ request.apiObjectType == "Port" &&
+ request.apiObjectId == "port-id-2"
+ ) {
+ request.eventListener.callListener(["test-done"], {
+ apiObjectType: Ci.mozIExtensionListenerCallOptions.RUNTIME_PORT,
+ apiObjectDescriptor: { portId: "port-id-2", name: "a-port-name-2" },
+ });
+ return;
+ }
+
+ throw new Error(`Unexpected request: ${request}`);
+ },
+ }
+ );
+});
diff --git a/toolkit/components/extensions/test/xpcshell/webidl-api/xpcshell.ini b/toolkit/components/extensions/test/xpcshell/webidl-api/xpcshell.ini
new file mode 100644
index 0000000000..465f913917
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/webidl-api/xpcshell.ini
@@ -0,0 +1,32 @@
+[DEFAULT]
+head = ../head.js ../head_remote.js ../head_service_worker.js head_webidl_api.js
+firefox-appdir = browser
+tags = webextensions webextensions-webidl-api
+
+prefs =
+ # Enable support for the extension background service worker.
+ extensions.backgroundServiceWorker.enabled=true
+ # Enable Extensions API WebIDL bindings for extension windows.
+ extensions.webidl-api.enabled=true
+ # Enable ExtensionMockAPI WebIDL bindings used for unit tests
+ # related to the API request forwarding and not tied to a particular
+ # extension API.
+ extensions.webidl-api.expose_mock_interface=true
+ # Make sure that loading the default settings for url-classifier-skip-urls
+ # doesn't interfere with running our tests while IDB operations are in
+ # flight by overriding the remote settings server URL to
+ # ensure that the IDB database isn't created in the first place.
+ services.settings.server=data:,#remote-settings-dummy/v1
+
+# NOTE: these tests seems to be timing out because it takes too much time to
+# run all tests and then fully exiting the test.
+skip-if = os == "android" && verify
+
+[test_ext_webidl_api.js]
+[test_ext_webidl_api_event_callback.js]
+skip-if =
+ os == "android" && processor == "x86_64" && debug # Bug 1716308
+[test_ext_webidl_api_request_handler.js]
+[test_ext_webidl_api_schema_errors.js]
+[test_ext_webidl_api_schema_formatters.js]
+[test_ext_webidl_runtime_port.js]