summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/test_ext_schemas_async.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/test_ext_schemas_async.js')
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_schemas_async.js352
1 files changed, 352 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_schemas_async.js b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_async.js
new file mode 100644
index 0000000000..2613593771
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_async.js
@@ -0,0 +1,352 @@
+"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");
+ }
+ }
+});