"use strict"; const { Schemas } = ChromeUtils.importESModule( "resource://gre/modules/Schemas.sys.mjs" ); let { BaseContext, LocalAPIImplementation } = ExtensionCommon; let schemaJson = [ { namespace: "testnamespace", types: [ { id: "Widget", type: "object", properties: { size: { type: "integer" }, colour: { type: "string", optional: true }, }, }, ], functions: [ { name: "one_required", type: "function", parameters: [ { name: "first", type: "function", parameters: [], }, ], }, { name: "one_optional", type: "function", parameters: [ { name: "first", type: "function", parameters: [], optional: true, }, ], }, { name: "async_required", type: "function", async: "first", parameters: [ { name: "first", type: "function", parameters: [], }, ], }, { name: "async_optional", type: "function", async: "first", parameters: [ { name: "first", type: "function", parameters: [], optional: true, }, ], }, { name: "async_result", type: "function", async: "callback", parameters: [ { name: "callback", type: "function", parameters: [ { name: "widget", $ref: "Widget", }, ], }, ], }, ], }, ]; const global = this; class StubContext extends BaseContext { constructor() { let fakeExtension = { id: "test@web.extension" }; super("testEnv", fakeExtension); this.sandbox = Cu.Sandbox(global); } get cloneScope() { return this.sandbox; } get principal() { return Cu.getObjectPrincipal(this.sandbox); } } let context; function generateAPIs(extraWrapper, apiObj) { context = new StubContext(); let localWrapper = { manifestVersion: 2, cloneScope: global, shouldInject() { return true; }, getImplementation(namespace, name) { return new LocalAPIImplementation(apiObj, name, context); }, }; Object.assign(localWrapper, extraWrapper); let root = {}; Schemas.inject(root, localWrapper); return root.testnamespace; } add_task(async function testParameterValidation() { await Schemas.load("data:," + JSON.stringify(schemaJson)); let testnamespace; function assertThrows(name, ...args) { Assert.throws( () => testnamespace[name](...args), /Incorrect argument types/, `Expected testnamespace.${name}(${args.map(String).join(", ")}) to throw.` ); } function assertNoThrows(name, ...args) { try { testnamespace[name](...args); } catch (e) { info( `testnamespace.${name}(${args .map(String) .join(", ")}) unexpectedly threw.` ); throw new Error(e); } } let cb = () => {}; for (let isChromeCompat of [true, false]) { info(`Testing API validation with isChromeCompat=${isChromeCompat}`); testnamespace = generateAPIs( { isChromeCompat, }, { one_required() {}, one_optional() {}, async_required() {}, async_optional() {}, } ); assertThrows("one_required"); assertThrows("one_required", null); assertNoThrows("one_required", cb); assertThrows("one_required", cb, null); assertThrows("one_required", cb, cb); assertNoThrows("one_optional"); assertNoThrows("one_optional", null); assertNoThrows("one_optional", cb); assertThrows("one_optional", cb, null); assertThrows("one_optional", cb, cb); // Schema-based validation happens before an async method is called, so // errors should be thrown synchronously. // The parameter was declared as required, but there was also an "async" // attribute with the same value as the parameter name, so the callback // parameter is actually optional. assertNoThrows("async_required"); assertNoThrows("async_required", null); assertNoThrows("async_required", cb); assertThrows("async_required", cb, null); assertThrows("async_required", cb, cb); assertNoThrows("async_optional"); assertNoThrows("async_optional", null); assertNoThrows("async_optional", cb); assertThrows("async_optional", cb, null); assertThrows("async_optional", cb, cb); } }); add_task(async function testCheckAsyncResults() { await Schemas.load("data:," + JSON.stringify(schemaJson)); const complete = generateAPIs( {}, { async_result: async () => ({ size: 5, colour: "green" }), } ); const optional = generateAPIs( {}, { async_result: async () => ({ size: 6 }), } ); const invalid = generateAPIs( {}, { async_result: async () => ({}), } ); deepEqual(await complete.async_result(), { size: 5, colour: "green" }); deepEqual( await optional.async_result(), { size: 6 }, "Missing optional properties is allowed" ); if (AppConstants.DEBUG) { await Assert.rejects( invalid.async_result(), /Type error for widget value \(Property "size" is required\)/, "Should throw for invalid callback argument in DEBUG builds" ); } else { deepEqual( await invalid.async_result(), {}, "Invalid callback argument doesn't throw in release builds" ); } }); add_task(async function testAsyncResults() { await Schemas.load("data:," + JSON.stringify(schemaJson)); function runWithCallback(func) { info(`Calling testnamespace.${func.name}, expecting callback with result`); return new Promise(resolve => { let result = "uninitialized value"; let returnValue = func(reply => { result = reply; resolve(result); }); // When a callback is given, the return value must be missing. Assert.equal(returnValue, undefined); // Callback must be called asynchronously. Assert.equal(result, "uninitialized value"); }); } function runFailCallback(func) { info(`Calling testnamespace.${func.name}, expecting callback with error`); return new Promise(resolve => { func(reply => { Assert.equal(reply, undefined); resolve(context.lastError.message); // eslint-disable-line no-undef }); }); } for (let isChromeCompat of [true, false]) { info(`Testing API invocation with isChromeCompat=${isChromeCompat}`); let testnamespace = generateAPIs( { isChromeCompat, }, { async_required(cb) { Assert.equal(cb, undefined); return Promise.resolve(1); }, async_optional(cb) { Assert.equal(cb, undefined); return Promise.resolve(2); }, } ); if (!isChromeCompat) { // No promises for chrome. info("testnamespace.async_required should be a Promise"); let promise = testnamespace.async_required(); Assert.ok(promise instanceof context.cloneScope.Promise); Assert.equal(await promise, 1); info("testnamespace.async_optional should be a Promise"); promise = testnamespace.async_optional(); Assert.ok(promise instanceof context.cloneScope.Promise); Assert.equal(await promise, 2); } Assert.equal(await runWithCallback(testnamespace.async_required), 1); Assert.equal(await runWithCallback(testnamespace.async_optional), 2); let otherSandbox = Cu.Sandbox(null, {}); let errorFactories = [ msg => { throw new context.cloneScope.Error(msg); }, msg => context.cloneScope.Promise.reject({ message: msg }), msg => Cu.evalInSandbox(`throw new Error("${msg}")`, otherSandbox), msg => Cu.evalInSandbox(`Promise.reject({message: "${msg}"})`, otherSandbox), ]; for (let makeError of errorFactories) { info(`Testing callback/promise with error caused by: ${makeError}`); testnamespace = generateAPIs( { isChromeCompat, }, { async_required() { return makeError("ONE"); }, async_optional() { return makeError("TWO"); }, } ); if (!isChromeCompat) { // No promises for chrome. await Assert.rejects( testnamespace.async_required(), /ONE/, "should reject testnamespace.async_required()" ); await Assert.rejects( testnamespace.async_optional(), /TWO/, "should reject testnamespace.async_optional()" ); } Assert.equal(await runFailCallback(testnamespace.async_required), "ONE"); Assert.equal(await runFailCallback(testnamespace.async_optional), "TWO"); } } });