Bug 1682632 - part2.8: WebExtensions API request handling unit tests. r=baku,mixedpuppy
☠☠ backed out by 11f571b7506c ☠ ☠
authorLuca Greco <lgreco@mozilla.com>
Wed, 09 Jun 2021 19:20:45 +0000
changeset 582534 3e47d00b7822fd63eb72e8e0e5e26863214644ea
parent 582533 bf010d24288ce7f343e46ab274693b6967735bf6
child 582535 e14034a66620ba25709c06c51f65a4483fe9022c
push id38527
push userncsoregi@mozilla.com
push dateThu, 10 Jun 2021 03:46:06 +0000
treeherdermozilla-central@52f82029a1e5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbaku, mixedpuppy
bugs1682632
milestone91.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1682632 - part2.8: WebExtensions API request handling unit tests. r=baku,mixedpuppy - Added a new set of xpcshell-tests in toolkit/components/extensions/test/xpcshell/webidl-api for unit tests related to the WebIDL bindings API request forwarding Depends on D99886 Differential Revision: https://phabricator.services.mozilla.com/D99887
toolkit/components/extensions/ExtensionProcessScript.jsm
toolkit/components/extensions/moz.build
toolkit/components/extensions/test/xpcshell/webidl-api/.eslintrc.js
toolkit/components/extensions/test/xpcshell/webidl-api/head_webidl_api.js
toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api.js
toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_event_callback.js
toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_runtime_port.js
toolkit/components/extensions/test/xpcshell/webidl-api/xpcshell.ini
--- a/toolkit/components/extensions/ExtensionProcessScript.jsm
+++ b/toolkit/components/extensions/ExtensionProcessScript.jsm
@@ -5,17 +5,17 @@
 
 /**
  * This script contains the minimum, skeleton content process code that we need
  * in order to lazily load other extension modules when they are first
  * necessary. Anything which is not likely to be needed immediately, or shortly
  * after startup, in *every* browser process live outside of this file.
  */
 
-var EXPORTED_SYMBOLS = ["ExtensionProcessScript"];
+var EXPORTED_SYMBOLS = ["ExtensionProcessScript", "ExtensionAPIRequestHandler"];
 
 const { MessageChannel } = ChromeUtils.import(
   "resource://gre/modules/MessageChannel.jsm"
 );
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
@@ -412,9 +412,19 @@ var ExtensionProcessScript = {
 
   loadContentScript(contentScript, window) {
     return ExtensionContent.contentScripts
       .get(contentScript)
       .injectInto(window);
   },
 };
 
+var ExtensionAPIRequestHandler = {
+  handleAPIRequest(policy, request) {
+    // TODO: to be actually implemented in the "part3" patches that follows,
+    // this patch does only contain a placeholder method, which is
+    // replaced with a mock in the set of unit tests defined in this
+    // patch.
+    throw new Error("Not implemented");
+  },
+};
+
 ExtensionManager.init();
--- a/toolkit/components/extensions/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -119,16 +119,17 @@ BROWSER_CHROME_MANIFESTS += [
 
 MOCHITEST_MANIFESTS += [
     "test/mochitest/mochitest-remote.ini",
     "test/mochitest/mochitest.ini",
 ]
 MOCHITEST_CHROME_MANIFESTS += ["test/mochitest/chrome.ini"]
 XPCSHELL_TESTS_MANIFESTS += [
     "test/xpcshell/native_messaging.ini",
+    "test/xpcshell/webidl-api/xpcshell.ini",
     "test/xpcshell/xpcshell-e10s.ini",
     "test/xpcshell/xpcshell-legacy-ep.ini",
     "test/xpcshell/xpcshell-remote.ini",
     "test/xpcshell/xpcshell.ini",
 ]
 
 SPHINX_TREES["webextensions"] = "docs"
 
new file mode 100644
--- /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,
+  },
+};
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/webidl-api/head_webidl_api.js
@@ -0,0 +1,301 @@
+/* import-globals-from ../head.js */
+
+/* exported getBackgroundServiceWorkerRegistration, waitForTerminatedWorkers,
+ *          runExtensionAPITest */
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  TestUtils: "resource://testing-common/TestUtils.jsm",
+  ExtensionTestCommon: "resource://testing-common/ExtensionTestCommon.jsm",
+});
+
+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.import(
+      "resource://gre/modules/ExtensionProcessScript.jsm"
+    );
+
+    // 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.spawn(
+    [ExtensionTestCommon.serializeFunction(mockHandleAPIRequest)],
+    mockFnText => {
+      const { ExtensionAPIRequestHandler } = ChromeUtils.import(
+        "resource://gre/modules/ExtensionProcessScript.jsm"
+      );
+
+      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} 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} 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`
+ * @param {Function} 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) {
+    info(`*** Run test case in an extension service worker`);
+    const result = await page.spawn([], 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);
+  }
+
+  // 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",
+      },
+      applications: {
+        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);
+  await testCleanup();
+  info(`End test case "${testDescription}"`);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api.js
@@ -0,0 +1,489 @@
+/* -*- 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();
+  // Ensure that the profile-after-change message has been notified,
+  // so that ServiceWokerRegistrar is going to be initialized.
+  Services.obs.notifyObservers(
+    null,
+    "profile-after-change",
+    "force-serviceworkerrestart-init"
+  );
+});
+
+add_task(async function test_ext_context_does_have_webidl_bindings() {
+  await runExtensionAPITest("should have a browser global object", {
+    backgroundScript() {
+      const { browser } = self;
+
+      return {
+        hasExtensionAPI: !!browser,
+        hasExtensionMockAPI: !!browser?.mockExtensionAPI,
+      };
+    },
+    assertResults({ testResult, testError }) {
+      Assert.deepEqual(testError, undefined);
+      Assert.deepEqual(
+        testResult,
+        {
+          hasExtensionAPI: true,
+          hasExtensionMockAPI: 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");
+      },
+    }
+  );
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_api_event_callback.js
@@ -0,0 +1,421 @@
+/* -*- 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();
+  // Ensure that the profile-after-change message has been notified,
+  // so that ServiceWokerRegistrar is going to be initialized.
+  Services.obs.notifyObservers(
+    null,
+    "profile-after-change",
+    "force-serviceworkerrestart-init"
+  );
+});
+
+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]);
+          });
+        }
+      }
+    },
+  });
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/webidl-api/test_ext_webidl_runtime_port.js
@@ -0,0 +1,204 @@
+/* -*- 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();
+  // Ensure that the profile-after-change message has been notified,
+  // so that ServiceWokerRegistrar is going to be initialized.
+  Services.obs.notifyObservers(
+    null,
+    "profile-after-change",
+    "force-serviceworkerrestart-init"
+  );
+});
+
+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.RESULT_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;
+        }
+      }
+
+      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 => {
+                if (msg === "test-done") {
+                  testLog("Got a port.onMessage event");
+                  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([], {
+            apiObjectType: Ci.mozIExtensionListenerCallOptions.RUNTIME_PORT,
+            apiObjectDescriptor: { portId: "port-id-2", name: "a-port-name-2" },
+          });
+          return;
+        } else if (
+          request.requestType == "addListener" &&
+          request.apiObjectType == "Port" &&
+          request.apiObjectId == "port-id-2"
+        ) {
+          request.eventListener.callListener(["test-done"]);
+          return;
+        }
+
+        throw new Error(`Unexpected request: ${request}`);
+      },
+    }
+  );
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/webidl-api/xpcshell.ini
@@ -0,0 +1,29 @@
+[DEFAULT]
+head = ../head.js ../head_remote.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
+  # services.settings.server/default_bucket:
+  # 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 default remote settings bucket pref name to
+  # ensure that the IDB database isn't created in the first place.
+  services.settings.server=http://localhost:7777/remote-settings-dummy/v1
+  services.settings.default_bucket=nonexistent-bucket-foo
+
+# 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]
+[test_ext_webidl_runtime_port.js]