/* 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": ` `, "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}"`); }