Source code
Revision control
Copy as Markdown
Other Tools
Test Info:
"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");
}
}
});