summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/webidl-api/head_webidl_api.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/webidl-api/head_webidl_api.js')
-rw-r--r--toolkit/components/extensions/test/xpcshell/webidl-api/head_webidl_api.js306
1 files changed, 306 insertions, 0 deletions
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}"`);
+}