forked from mirrors/gecko-dev
Depends on D175553 Differential Revision: https://phabricator.services.mozilla.com/D176005
306 lines
10 KiB
JavaScript
306 lines
10 KiB
JavaScript
/* 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}"`);
|
|
}
|