"use strict"; Components.utils.import("resource://gre/modules/ExtensionCommon.jsm"); Components.utils.import("resource://gre/modules/Schemas.jsm"); let {BaseContext, LocalAPIImplementation} = ExtensionCommon; let schemaJson = [ { namespace: "testnamespace", 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, }], }], }, ]; 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 = { 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(function* testParameterValidation() { yield 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) { do_print(`testnamespace.${name}(${args.map(String).join(", ")}) unexpectedly threw.`); throw new Error(e); } } let cb = () => {}; for (let isChromeCompat of [true, false]) { do_print(`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(function* testAsyncResults() { yield Schemas.load("data:," + JSON.stringify(schemaJson)); function* runWithCallback(func) { do_print(`Calling testnamespace.${func.name}, expecting callback with result`); return yield 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. do_check_eq(returnValue, undefined); // Callback must be called asynchronously. do_check_eq(result, "uninitialized value"); }); } function* runFailCallback(func) { do_print(`Calling testnamespace.${func.name}, expecting callback with error`); return yield new Promise(resolve => { func(reply => { do_check_eq(reply, undefined); resolve(context.lastError.message); // eslint-disable-line no-undef }); }); } for (let isChromeCompat of [true, false]) { do_print(`Testing API invocation with isChromeCompat=${isChromeCompat}`); let testnamespace = generateAPIs({ isChromeCompat, }, { async_required(cb) { do_check_eq(cb, undefined); return Promise.resolve(1); }, async_optional(cb) { do_check_eq(cb, undefined); return Promise.resolve(2); }, }); if (!isChromeCompat) { // No promises for chrome. do_print("testnamespace.async_required should be a Promise"); let promise = testnamespace.async_required(); do_check_true(promise instanceof context.cloneScope.Promise); do_check_eq(yield promise, 1); do_print("testnamespace.async_optional should be a Promise"); promise = testnamespace.async_optional(); do_check_true(promise instanceof context.cloneScope.Promise); do_check_eq(yield promise, 2); } do_check_eq(yield* runWithCallback(testnamespace.async_required), 1); do_check_eq(yield* 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) { do_print(`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. yield Assert.rejects(testnamespace.async_required(), /ONE/, "should reject testnamespace.async_required()").catch(() => {}); yield Assert.rejects(testnamespace.async_optional(), /TWO/, "should reject testnamespace.async_optional()").catch(() => {}); } do_check_eq(yield* runFailCallback(testnamespace.async_required), "ONE"); do_check_eq(yield* runFailCallback(testnamespace.async_optional), "TWO"); } } });