forked from mirrors/gecko-dev
Depends on D175553 Differential Revision: https://phabricator.services.mozilla.com/D176005
352 lines
8.9 KiB
JavaScript
352 lines
8.9 KiB
JavaScript
"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");
|
|
}
|
|
}
|
|
});
|