summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/test_ext_userScripts_exports.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/extensions/test/xpcshell/test_ext_userScripts_exports.js
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/test_ext_userScripts_exports.js')
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_userScripts_exports.js1108
1 files changed, 1108 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_userScripts_exports.js b/toolkit/components/extensions/test/xpcshell/test_ext_userScripts_exports.js
new file mode 100644
index 0000000000..5950377f85
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_userScripts_exports.js
@@ -0,0 +1,1108 @@
+"use strict";
+
+const { createAppInfo } = AddonTestUtils;
+
+AddonTestUtils.init(this);
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "49");
+
+const server = createHttpServer();
+server.registerDirectory("/data/", do_get_file("data"));
+
+const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`;
+
+// A small utility function used to test the expected behaviors of the userScripts API method
+// wrapper.
+async function test_userScript_APIMethod({
+ apiScript,
+ userScript,
+ userScriptMetadata,
+ testFn,
+ runtimeMessageListener,
+}) {
+ async function backgroundScript(
+ userScriptFn,
+ scriptMetadata,
+ messageListener
+ ) {
+ await browser.userScripts.register({
+ js: [
+ {
+ code: `(${userScriptFn})();`,
+ },
+ ],
+ runAt: "document_end",
+ matches: ["http://localhost/*/file_sample.html"],
+ scriptMetadata,
+ });
+
+ if (messageListener) {
+ browser.runtime.onMessage.addListener(messageListener);
+ }
+
+ browser.test.sendMessage("background-ready");
+ }
+
+ function notifyFinish(failureReason) {
+ browser.test.assertEq(
+ undefined,
+ failureReason,
+ "should be completed without errors"
+ );
+ browser.test.sendMessage("test_userScript_APIMethod:done");
+ }
+
+ function assertTrue(val, message) {
+ browser.test.assertTrue(val, message);
+ if (!val) {
+ browser.test.sendMessage("test_userScript_APIMethod:done");
+ throw message;
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["http://localhost/*/file_sample.html"],
+ user_scripts: {
+ api_script: "api-script.js",
+ },
+ },
+ // Defines a background script that receives all the needed test parameters.
+ background: `
+ const metadata = ${JSON.stringify(userScriptMetadata)};
+ (${backgroundScript})(${userScript}, metadata, ${runtimeMessageListener})
+ `,
+ files: {
+ "api-script.js": `(${apiScript})({
+ assertTrue: ${assertTrue},
+ notifyFinish: ${notifyFinish}
+ })`,
+ },
+ });
+
+ // Load a page in a content process, register the user script and then load a
+ // new page in the existing content process.
+ let url = `${BASE_URL}/file_sample.html`;
+ let contentPage = await ExtensionTestUtils.loadContentPage(`about:blank`);
+
+ await extension.startup();
+ await extension.awaitMessage("background-ready");
+ await contentPage.loadURL(url);
+
+ // Run any additional test-specific assertions.
+ if (testFn) {
+ await testFn({ extension, contentPage, url });
+ }
+
+ await extension.awaitMessage("test_userScript_APIMethod:done");
+
+ await extension.unload();
+ await contentPage.close();
+}
+
+add_task(async function test_apiScript_exports_simple_sync_method() {
+ function apiScript(sharedTestAPIMethods) {
+ browser.userScripts.onBeforeScript.addListener(script => {
+ const scriptMetadata = script.metadata;
+
+ script.defineGlobals({
+ ...sharedTestAPIMethods,
+ testAPIMethod(
+ stringParam,
+ numberParam,
+ boolParam,
+ nullParam,
+ undefinedParam,
+ arrayParam
+ ) {
+ browser.test.assertEq(
+ "test-user-script-exported-apis",
+ scriptMetadata.name,
+ "Got the expected value for a string scriptMetadata property"
+ );
+ browser.test.assertEq(
+ null,
+ scriptMetadata.nullProperty,
+ "Got the expected value for a null scriptMetadata property"
+ );
+ browser.test.assertTrue(
+ scriptMetadata.arrayProperty &&
+ scriptMetadata.arrayProperty.length === 1 &&
+ scriptMetadata.arrayProperty[0] === "el1",
+ "Got the expected value for an array scriptMetadata property"
+ );
+ browser.test.assertTrue(
+ scriptMetadata.objectProperty &&
+ scriptMetadata.objectProperty.nestedProp === "nestedValue",
+ "Got the expected value for an object scriptMetadata property"
+ );
+
+ browser.test.assertEq(
+ "param1",
+ stringParam,
+ "Got the expected string parameter value"
+ );
+ browser.test.assertEq(
+ 123,
+ numberParam,
+ "Got the expected number parameter value"
+ );
+ browser.test.assertEq(
+ true,
+ boolParam,
+ "Got the expected boolean parameter value"
+ );
+ browser.test.assertEq(
+ null,
+ nullParam,
+ "Got the expected null parameter value"
+ );
+ browser.test.assertEq(
+ undefined,
+ undefinedParam,
+ "Got the expected undefined parameter value"
+ );
+
+ browser.test.assertEq(
+ 3,
+ arrayParam.length,
+ "Got the expected length on the array param"
+ );
+ browser.test.assertTrue(
+ arrayParam.includes(1),
+ "Got the expected result when calling arrayParam.includes"
+ );
+
+ return "returned_value";
+ },
+ });
+ });
+ }
+
+ function userScript() {
+ const { assertTrue, notifyFinish, testAPIMethod } = this;
+
+ // Redefine the includes method on the Array prototype, to explicitly verify that the method
+ // redefined in the userScript is not used when accessing arrayParam.includes from the API script.
+ // eslint-disable-next-line no-extend-native
+ Array.prototype.includes = () => {
+ throw new Error("Unexpected prototype leakage");
+ };
+ const arrayParam = new Array(1, 2, 3); // eslint-disable-line no-array-constructor
+ const result = testAPIMethod(
+ "param1",
+ 123,
+ true,
+ null,
+ undefined,
+ arrayParam
+ );
+
+ assertTrue(
+ result === "returned_value",
+ `userScript got an unexpected result value: ${result}`
+ );
+
+ notifyFinish();
+ }
+
+ const userScriptMetadata = {
+ name: "test-user-script-exported-apis",
+ arrayProperty: ["el1"],
+ objectProperty: { nestedProp: "nestedValue" },
+ nullProperty: null,
+ };
+
+ await test_userScript_APIMethod({
+ userScript,
+ apiScript,
+ userScriptMetadata,
+ });
+});
+
+add_task(async function test_apiScript_async_method() {
+ function apiScript(sharedTestAPIMethods) {
+ browser.userScripts.onBeforeScript.addListener(script => {
+ script.defineGlobals({
+ ...sharedTestAPIMethods,
+ testAPIMethod(param, cb, cb2, objWithCb) {
+ browser.test.assertEq(
+ "function",
+ typeof cb,
+ "Got a callback function parameter"
+ );
+ browser.test.assertTrue(
+ cb === cb2,
+ "Got the same cloned function for the same function parameter"
+ );
+
+ browser.runtime.sendMessage(param).then(bgPageRes => {
+ const cbResult = cb(script.export(bgPageRes));
+ browser.test.sendMessage("user-script-callback-return", cbResult);
+ });
+
+ return "resolved_value";
+ },
+ });
+ });
+ }
+
+ async function userScript() {
+ // Redefine Promise to verify that it doesn't break the WebExtensions internals
+ // that are going to use them.
+ const { Promise } = this;
+ Promise.resolve = function () {
+ throw new Error("Promise.resolve poisoning");
+ };
+ this.Promise = function () {
+ throw new Error("Promise constructor poisoning");
+ };
+
+ const { assertTrue, notifyFinish, testAPIMethod } = this;
+
+ const cb = cbParam => {
+ return `callback param: ${JSON.stringify(cbParam)}`;
+ };
+ const cb2 = cb;
+ const asyncAPIResult = await testAPIMethod("param3", cb, cb2);
+
+ assertTrue(
+ asyncAPIResult === "resolved_value",
+ `userScript got an unexpected resolved value: ${asyncAPIResult}`
+ );
+
+ notifyFinish();
+ }
+
+ async function runtimeMessageListener(param) {
+ if (param !== "param3") {
+ browser.test.fail(`Got an unexpected message: ${param}`);
+ }
+
+ return { bgPageReply: true };
+ }
+
+ await test_userScript_APIMethod({
+ userScript,
+ apiScript,
+ runtimeMessageListener,
+ async testFn({ extension }) {
+ const res = await extension.awaitMessage("user-script-callback-return");
+ equal(
+ res,
+ `callback param: ${JSON.stringify({ bgPageReply: true })}`,
+ "Got the expected userScript callback return value"
+ );
+ },
+ });
+});
+
+add_task(async function test_apiScript_method_with_webpage_objects_params() {
+ function apiScript(sharedTestAPIMethods) {
+ browser.userScripts.onBeforeScript.addListener(script => {
+ script.defineGlobals({
+ ...sharedTestAPIMethods,
+ testAPIMethod(windowParam, documentParam) {
+ browser.test.assertEq(
+ window,
+ windowParam,
+ "Got a reference to the native window as first param"
+ );
+ browser.test.assertEq(
+ window.document,
+ documentParam,
+ "Got a reference to the native document as second param"
+ );
+
+ // Return an uncloneable webpage object, which checks that if the returned object is from a principal
+ // that is subsumed by the userScript sandbox principal, it is returned without being cloned.
+ return windowParam;
+ },
+ });
+ });
+ }
+
+ async function userScript() {
+ const { assertTrue, notifyFinish, testAPIMethod } = this;
+
+ const result = testAPIMethod(window, document);
+
+ // We expect the returned value to be the uncloneable window object.
+ assertTrue(
+ result === window,
+ `userScript got an unexpected returned value: ${result}`
+ );
+ notifyFinish();
+ }
+
+ await test_userScript_APIMethod({
+ userScript,
+ apiScript,
+ });
+});
+
+add_task(async function test_apiScript_method_got_param_with_methods() {
+ function apiScript(sharedTestAPIMethods) {
+ browser.userScripts.onBeforeScript.addListener(script => {
+ const scriptGlobal = script.global;
+ const ScriptFunction = scriptGlobal.Function;
+
+ script.defineGlobals({
+ ...sharedTestAPIMethods,
+ testAPIMethod(objWithMethods) {
+ browser.test.assertEq(
+ "objPropertyValue",
+ objWithMethods && objWithMethods.objProperty,
+ "Got the expected property on the object passed as a parameter"
+ );
+ browser.test.assertEq(
+ undefined,
+ objWithMethods?.objMethod,
+ "XrayWrapper should deny access to a callable property"
+ );
+
+ browser.test.assertTrue(
+ objWithMethods &&
+ objWithMethods.wrappedJSObject &&
+ objWithMethods.wrappedJSObject.objMethod instanceof
+ ScriptFunction.wrappedJSObject,
+ "The callable property is accessible on the wrappedJSObject"
+ );
+
+ browser.test.assertEq(
+ "objMethodResult: p1",
+ objWithMethods &&
+ objWithMethods.wrappedJSObject &&
+ objWithMethods.wrappedJSObject.objMethod("p1"),
+ "Got the expected result when calling the method on the wrappedJSObject"
+ );
+ return true;
+ },
+ });
+ });
+ }
+
+ async function userScript() {
+ const { assertTrue, notifyFinish, testAPIMethod } = this;
+
+ let result = testAPIMethod({
+ objProperty: "objPropertyValue",
+ objMethod(param) {
+ return `objMethodResult: ${param}`;
+ },
+ });
+
+ assertTrue(
+ result === true,
+ `userScript got an unexpected returned value: ${result}`
+ );
+ notifyFinish();
+ }
+
+ await test_userScript_APIMethod({
+ userScript,
+ apiScript,
+ });
+});
+
+add_task(async function test_apiScript_method_throws_errors() {
+ function apiScript({ notifyFinish }) {
+ let proxyTrapsCount = 0;
+
+ browser.userScripts.onBeforeScript.addListener(script => {
+ const scriptGlobals = {
+ Error: script.global.Error,
+ TypeError: script.global.TypeError,
+ Proxy: script.global.Proxy,
+ };
+
+ script.defineGlobals({
+ notifyFinish,
+ testAPIMethod(errorTestName, returnRejectedPromise) {
+ let err;
+
+ switch (errorTestName) {
+ case "apiScriptError":
+ err = new Error(`${errorTestName} message`);
+ break;
+ case "apiScriptThrowsPlainString":
+ err = `${errorTestName} message`;
+ break;
+ case "apiScriptThrowsNull":
+ err = null;
+ break;
+ case "userScriptError":
+ err = new scriptGlobals.Error(`${errorTestName} message`);
+ break;
+ case "userScriptTypeError":
+ err = new scriptGlobals.TypeError(`${errorTestName} message`);
+ break;
+ case "userScriptProxyObject":
+ let proxyTarget = script.export({
+ name: "ProxyObject",
+ message: "ProxyObject message",
+ });
+ let proxyHandlers = script.export({
+ get(target, prop) {
+ proxyTrapsCount++;
+ switch (prop) {
+ case "name":
+ return "ProxyObjectGetName";
+ case "message":
+ return "ProxyObjectGetMessage";
+ }
+ return undefined;
+ },
+ getPrototypeOf() {
+ proxyTrapsCount++;
+ return scriptGlobals.TypeError;
+ },
+ });
+ err = new scriptGlobals.Proxy(proxyTarget, proxyHandlers);
+ break;
+ default:
+ browser.test.fail(`Unknown ${errorTestName} error testname`);
+ return undefined;
+ }
+
+ if (returnRejectedPromise) {
+ return Promise.reject(err);
+ }
+
+ throw err;
+ },
+ assertNoProxyTrapTriggered() {
+ browser.test.assertEq(
+ 0,
+ proxyTrapsCount,
+ "Proxy traps should not be triggered"
+ );
+ },
+ resetProxyTrapCounter() {
+ proxyTrapsCount = 0;
+ },
+ sendResults(results) {
+ browser.test.sendMessage("test-results", results);
+ },
+ });
+ });
+ }
+
+ async function userScript() {
+ const {
+ assertNoProxyTrapTriggered,
+ notifyFinish,
+ resetProxyTrapCounter,
+ sendResults,
+ testAPIMethod,
+ } = this;
+
+ let apiThrowResults = {};
+ let apiThrowTestCases = [
+ "apiScriptError",
+ "apiScriptThrowsPlainString",
+ "apiScriptThrowsNull",
+ "userScriptError",
+ "userScriptTypeError",
+ "userScriptProxyObject",
+ ];
+ for (let errorTestName of apiThrowTestCases) {
+ try {
+ testAPIMethod(errorTestName);
+ } catch (err) {
+ // We expect that no proxy traps have been triggered by the WebExtensions internals.
+ if (errorTestName === "userScriptProxyObject") {
+ assertNoProxyTrapTriggered();
+ }
+
+ if (err instanceof Error) {
+ apiThrowResults[errorTestName] = {
+ name: err.name,
+ message: err.message,
+ };
+ } else {
+ apiThrowResults[errorTestName] = {
+ name: err && err.name,
+ message: err && err.message,
+ typeOf: typeof err,
+ value: err,
+ };
+ }
+ }
+ }
+
+ sendResults(apiThrowResults);
+
+ resetProxyTrapCounter();
+
+ let apiRejectsResults = {};
+ for (let errorTestName of apiThrowTestCases) {
+ try {
+ await testAPIMethod(errorTestName, true);
+ } catch (err) {
+ // We expect that no proxy traps have been triggered by the WebExtensions internals.
+ if (errorTestName === "userScriptProxyObject") {
+ assertNoProxyTrapTriggered();
+ }
+
+ if (err instanceof Error) {
+ apiRejectsResults[errorTestName] = {
+ name: err.name,
+ message: err.message,
+ };
+ } else {
+ apiRejectsResults[errorTestName] = {
+ name: err && err.name,
+ message: err && err.message,
+ typeOf: typeof err,
+ value: err,
+ };
+ }
+ }
+ }
+
+ sendResults(apiRejectsResults);
+
+ notifyFinish();
+ }
+
+ await test_userScript_APIMethod({
+ userScript,
+ apiScript,
+ async testFn({ extension }) {
+ const expectedResults = {
+ // Any error not explicitly raised as a userScript objects or error instance is
+ // expected to be turned into a generic error message.
+ apiScriptError: {
+ name: "Error",
+ message: "An unexpected apiScript error occurred",
+ },
+
+ // When the api script throws a primitive value, we expect to receive it unmodified on
+ // the userScript side.
+ apiScriptThrowsPlainString: {
+ typeOf: "string",
+ value: "apiScriptThrowsPlainString message",
+ name: undefined,
+ message: undefined,
+ },
+ apiScriptThrowsNull: {
+ typeOf: "object",
+ value: null,
+ name: undefined,
+ message: undefined,
+ },
+
+ // Error messages that the apiScript has explicitly created as userScript's Error
+ // global instances are expected to be passing through unmodified.
+ userScriptError: { name: "Error", message: "userScriptError message" },
+ userScriptTypeError: {
+ name: "TypeError",
+ message: "userScriptTypeError message",
+ },
+
+ // Error raised from the apiScript as userScript proxy objects are expected to
+ // be passing through unmodified.
+ userScriptProxyObject: {
+ typeOf: "object",
+ name: "ProxyObjectGetName",
+ message: "ProxyObjectGetMessage",
+ },
+ };
+
+ info(
+ "Checking results from errors raised from an apiScript exported function"
+ );
+
+ const apiThrowResults = await extension.awaitMessage("test-results");
+
+ for (let [key, expected] of Object.entries(expectedResults)) {
+ Assert.deepEqual(
+ apiThrowResults[key],
+ expected,
+ `Got the expected error object for test case "${key}"`
+ );
+ }
+
+ Assert.deepEqual(
+ Object.keys(expectedResults).sort(),
+ Object.keys(apiThrowResults).sort(),
+ "the expected and actual test case names matches"
+ );
+
+ info(
+ "Checking expected results from errors raised from an apiScript exported function"
+ );
+
+ // Verify expected results from rejected promises returned from an apiScript exported function.
+ const apiThrowRejections = await extension.awaitMessage("test-results");
+
+ for (let [key, expected] of Object.entries(expectedResults)) {
+ Assert.deepEqual(
+ apiThrowRejections[key],
+ expected,
+ `Got the expected rejected object for test case "${key}"`
+ );
+ }
+
+ Assert.deepEqual(
+ Object.keys(expectedResults).sort(),
+ Object.keys(apiThrowRejections).sort(),
+ "the expected and actual test case names matches"
+ );
+ },
+ });
+});
+
+add_task(
+ async function test_apiScript_method_ensure_xraywrapped_proxy_in_params() {
+ function apiScript(sharedTestAPIMethods) {
+ browser.userScripts.onBeforeScript.addListener(script => {
+ script.defineGlobals({
+ ...sharedTestAPIMethods,
+ testAPIMethod(...args) {
+ // Proxies are opaque when wrapped in Xrays, and the proto of an opaque object
+ // is supposed to be Object.prototype.
+ browser.test.assertEq(
+ script.global.Object.prototype,
+ Object.getPrototypeOf(args[0]),
+ "Calling getPrototypeOf on the XrayWrapped proxy object doesn't run the proxy trap"
+ );
+
+ browser.test.assertTrue(
+ Array.isArray(args[0]),
+ "Got an array object for the XrayWrapped proxy object param"
+ );
+ browser.test.assertEq(
+ undefined,
+ args[0].length,
+ "XrayWrappers deny access to the length property"
+ );
+ browser.test.assertEq(
+ undefined,
+ args[0][0],
+ "Got the expected item in the array object"
+ );
+ return true;
+ },
+ });
+ });
+ }
+
+ async function userScript() {
+ const { assertTrue, notifyFinish, testAPIMethod } = this;
+
+ let proxy = new Proxy(["expectedArrayValue"], {
+ getPrototypeOf() {
+ throw new Error("Proxy's getPrototypeOf trap");
+ },
+ get(target, prop, receiver) {
+ throw new Error("Proxy's get trap");
+ },
+ });
+
+ let result = testAPIMethod(proxy);
+
+ assertTrue(
+ result,
+ `userScript got an unexpected returned value: ${result}`
+ );
+ notifyFinish();
+ }
+
+ await test_userScript_APIMethod({
+ userScript,
+ apiScript,
+ });
+ }
+);
+
+add_task(async function test_apiScript_method_return_proxy_object() {
+ function apiScript(sharedTestAPIMethods) {
+ let proxyTrapsCount = 0;
+ let scriptTrapsCount = 0;
+
+ browser.userScripts.onBeforeScript.addListener(script => {
+ script.defineGlobals({
+ ...sharedTestAPIMethods,
+ testAPIMethodError() {
+ return new Proxy(["expectedArrayValue"], {
+ getPrototypeOf(target) {
+ proxyTrapsCount++;
+ return Object.getPrototypeOf(target);
+ },
+ });
+ },
+ testAPIMethodOk() {
+ return new script.global.Proxy(
+ script.export(["expectedArrayValue"]),
+ script.export({
+ getPrototypeOf(target) {
+ scriptTrapsCount++;
+ return script.global.Object.getPrototypeOf(target);
+ },
+ })
+ );
+ },
+ assertNoProxyTrapTriggered() {
+ browser.test.assertEq(
+ 0,
+ proxyTrapsCount,
+ "Proxy traps should not be triggered"
+ );
+ },
+ assertScriptProxyTrapsCount(expected) {
+ browser.test.assertEq(
+ expected,
+ scriptTrapsCount,
+ "Script Proxy traps should have been triggered"
+ );
+ },
+ });
+ });
+ }
+
+ async function userScript() {
+ const {
+ assertTrue,
+ assertNoProxyTrapTriggered,
+ assertScriptProxyTrapsCount,
+ notifyFinish,
+ testAPIMethodError,
+ testAPIMethodOk,
+ } = this;
+
+ let error;
+ try {
+ let result = testAPIMethodError();
+ notifyFinish(
+ `Unexpected returned value while expecting error: ${result}`
+ );
+ return;
+ } catch (err) {
+ error = err;
+ }
+
+ assertTrue(
+ error &&
+ error.message.includes("Return value not accessible to the userScript"),
+ `Got an unexpected error message: ${error}`
+ );
+
+ error = undefined;
+ try {
+ let result = testAPIMethodOk();
+ assertScriptProxyTrapsCount(0);
+ if (!(result instanceof Array)) {
+ notifyFinish(`Got an unexpected result: ${result}`);
+ return;
+ }
+ assertScriptProxyTrapsCount(1);
+ } catch (err) {
+ error = err;
+ }
+
+ assertTrue(!error, `Got an unexpected error: ${error}`);
+
+ assertNoProxyTrapTriggered();
+
+ notifyFinish();
+ }
+
+ await test_userScript_APIMethod({
+ userScript,
+ apiScript,
+ });
+});
+
+add_task(async function test_apiScript_returns_functions() {
+ function apiScript(sharedTestAPIMethods) {
+ browser.userScripts.onBeforeScript.addListener(script => {
+ script.defineGlobals({
+ ...sharedTestAPIMethods,
+ testAPIReturnsFunction() {
+ // Return a function with provides the same kind of behavior
+ // of the API methods exported as globals.
+ return script.export(() => window);
+ },
+ testAPIReturnsObjWithMethod() {
+ return script.export({
+ getWindow() {
+ return window;
+ },
+ });
+ },
+ });
+ });
+ }
+
+ async function userScript() {
+ const {
+ assertTrue,
+ notifyFinish,
+ testAPIReturnsFunction,
+ testAPIReturnsObjWithMethod,
+ } = this;
+
+ let resultFn = testAPIReturnsFunction();
+ assertTrue(
+ typeof resultFn === "function",
+ `userScript got an unexpected returned value: ${typeof resultFn}`
+ );
+
+ let fnRes = resultFn();
+ assertTrue(
+ fnRes === window,
+ `Got an unexpected value from the returned function: ${fnRes}`
+ );
+
+ let resultObj = testAPIReturnsObjWithMethod();
+ let actualTypeof = resultObj && typeof resultObj.getWindow;
+ assertTrue(
+ actualTypeof === "function",
+ `Returned object does not have the expected getWindow method: ${actualTypeof}`
+ );
+
+ let methodRes = resultObj.getWindow();
+ assertTrue(
+ methodRes === window,
+ `Got an unexpected value from the returned method: ${methodRes}`
+ );
+
+ notifyFinish();
+ }
+
+ await test_userScript_APIMethod({
+ userScript,
+ apiScript,
+ });
+});
+
+add_task(
+ async function test_apiScript_method_clone_non_subsumed_returned_values() {
+ function apiScript(sharedTestAPIMethods) {
+ browser.userScripts.onBeforeScript.addListener(script => {
+ script.defineGlobals({
+ ...sharedTestAPIMethods,
+ testAPIMethodReturnOk() {
+ return script.export({
+ objKey1: {
+ nestedProp: "nestedvalue",
+ },
+ window,
+ });
+ },
+ testAPIMethodExplicitlyClonedError() {
+ let result = script.export({ apiScopeObject: undefined });
+
+ browser.test.assertThrows(
+ () => {
+ result.apiScopeObject = { disallowedProp: "disallowedValue" };
+ },
+ /Not allowed to define cross-origin object as property on .* XrayWrapper/,
+ "Assigning a property to a xRayWrapper is expected to throw"
+ );
+
+ // Let the exception to be raised, so that we check that the actual underlying
+ // error message is not leaking in the userScript (replaced by the generic
+ // "An unexpected apiScript error occurred" error message).
+ result.apiScopeObject = { disallowedProp: "disallowedValue" };
+ },
+ });
+ });
+ }
+
+ async function userScript() {
+ const {
+ assertTrue,
+ notifyFinish,
+ testAPIMethodReturnOk,
+ testAPIMethodExplicitlyClonedError,
+ } = this;
+
+ let result = testAPIMethodReturnOk();
+
+ assertTrue(
+ result &&
+ "objKey1" in result &&
+ result.objKey1.nestedProp === "nestedvalue",
+ `userScript got an unexpected returned value: ${result}`
+ );
+
+ assertTrue(
+ result.window === window,
+ `userScript should have access to the window property: ${result.window}`
+ );
+
+ let error;
+ try {
+ result = testAPIMethodExplicitlyClonedError();
+ notifyFinish(
+ `Unexpected returned value while expecting error: ${result}`
+ );
+ return;
+ } catch (err) {
+ error = err;
+ }
+
+ // We expect the generic "unexpected apiScript error occurred" to be raised to the
+ // userScript code.
+ assertTrue(
+ error &&
+ error.message.includes("An unexpected apiScript error occurred"),
+ `Got an unexpected error message: ${error}`
+ );
+
+ notifyFinish();
+ }
+
+ await test_userScript_APIMethod({
+ userScript,
+ apiScript,
+ });
+ }
+);
+
+add_task(async function test_apiScript_method_export_primitive_types() {
+ function apiScript(sharedTestAPIMethods) {
+ browser.userScripts.onBeforeScript.addListener(script => {
+ script.defineGlobals({
+ ...sharedTestAPIMethods,
+ testAPIMethod(typeToExport) {
+ switch (typeToExport) {
+ case "boolean":
+ return script.export(true);
+ case "number":
+ return script.export(123);
+ case "string":
+ return script.export("a string");
+ case "symbol":
+ return script.export(Symbol("a symbol"));
+ }
+ return undefined;
+ },
+ });
+ });
+ }
+
+ async function userScript() {
+ const { assertTrue, notifyFinish, testAPIMethod } = this;
+
+ let v = testAPIMethod("boolean");
+ assertTrue(v === true, `Should export a boolean`);
+
+ v = testAPIMethod("number");
+ assertTrue(v === 123, `Should export a number`);
+
+ v = testAPIMethod("string");
+ assertTrue(v === "a string", `Should export a string`);
+
+ v = testAPIMethod("symbol");
+ assertTrue(typeof v === "symbol", `Should export a symbol`);
+
+ notifyFinish();
+ }
+
+ await test_userScript_APIMethod({
+ userScript,
+ apiScript,
+ });
+});
+
+add_task(
+ async function test_apiScript_method_avoid_unnecessary_params_cloning() {
+ function apiScript(sharedTestAPIMethods) {
+ browser.userScripts.onBeforeScript.addListener(script => {
+ script.defineGlobals({
+ ...sharedTestAPIMethods,
+ testAPIMethodReturnsParam(param) {
+ return param;
+ },
+ testAPIMethodReturnsUnwrappedParam(param) {
+ return param.wrappedJSObject;
+ },
+ });
+ });
+ }
+
+ async function userScript() {
+ const {
+ assertTrue,
+ notifyFinish,
+ testAPIMethodReturnsParam,
+ testAPIMethodReturnsUnwrappedParam,
+ } = this;
+
+ let obj = {};
+
+ let result = testAPIMethodReturnsParam(obj);
+
+ assertTrue(
+ result === obj,
+ `Expect returned value to be strictly equal to the API method parameter`
+ );
+
+ result = testAPIMethodReturnsUnwrappedParam(obj);
+
+ assertTrue(
+ result === obj,
+ `Expect returned value to be strictly equal to the unwrapped API method parameter`
+ );
+
+ notifyFinish();
+ }
+
+ await test_userScript_APIMethod({
+ userScript,
+ apiScript,
+ });
+ }
+);
+
+add_task(async function test_apiScript_method_export_sparse_arrays() {
+ function apiScript(sharedTestAPIMethods) {
+ browser.userScripts.onBeforeScript.addListener(script => {
+ script.defineGlobals({
+ ...sharedTestAPIMethods,
+ testAPIMethod() {
+ const sparseArray = [];
+ sparseArray[3] = "third-element";
+ sparseArray[5] = "fifth-element";
+ return script.export(sparseArray);
+ },
+ });
+ });
+ }
+
+ async function userScript() {
+ const { assertTrue, notifyFinish, testAPIMethod } = this;
+
+ const result = testAPIMethod(window, document);
+
+ // We expect the returned value to be the uncloneable window object.
+ assertTrue(
+ result && result.length === 6,
+ `the returned value should be an array of the expected length: ${result}`
+ );
+ assertTrue(
+ result[3] === "third-element",
+ `the third array element should have the expected value: ${result[3]}`
+ );
+ assertTrue(
+ result[5] === "fifth-element",
+ `the fifth array element should have the expected value: ${result[5]}`
+ );
+ assertTrue(
+ result[0] === undefined,
+ `the first array element should have the expected value: ${result[0]}`
+ );
+ assertTrue(!("0" in result), "Holey array should still be holey");
+
+ notifyFinish();
+ }
+
+ await test_userScript_APIMethod({
+ userScript,
+ apiScript,
+ });
+});