Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

"use strict";
const global = this;
let json = [
{
namespace: "testing",
properties: {
PROP1: { value: 20 },
prop2: { type: "string" },
prop3: {
$ref: "submodule",
},
prop4: {
$ref: "submodule",
unsupported: true,
},
},
types: [
{
id: "type1",
type: "string",
enum: ["value1", "value2", "value3"],
},
{
id: "type2",
type: "object",
properties: {
prop1: { type: "integer" },
prop2: { type: "array", items: { $ref: "type1" } },
},
},
{
id: "basetype1",
type: "object",
properties: {
prop1: { type: "string" },
},
},
{
id: "basetype2",
choices: [{ type: "integer" }],
},
{
$extend: "basetype1",
properties: {
prop2: { type: "string" },
},
},
{
$extend: "basetype2",
choices: [{ type: "string" }],
},
{
id: "basetype3",
type: "object",
properties: {
baseprop: { type: "string" },
},
},
{
id: "derivedtype1",
type: "object",
$import: "basetype3",
properties: {
derivedprop: { type: "string" },
},
},
{
id: "derivedtype2",
type: "object",
$import: "basetype3",
properties: {
derivedprop: { type: "integer" },
},
},
{
id: "submodule",
type: "object",
functions: [
{
name: "sub_foo",
type: "function",
parameters: [],
returns: { type: "integer" },
},
],
},
],
functions: [
{
name: "foo",
type: "function",
parameters: [
{ name: "arg1", type: "integer", optional: true, default: 99 },
{ name: "arg2", type: "boolean", optional: true },
],
},
{
name: "bar",
type: "function",
parameters: [
{ name: "arg1", type: "integer", optional: true },
{ name: "arg2", type: "boolean" },
],
},
{
name: "baz",
type: "function",
parameters: [
{
name: "arg1",
type: "object",
properties: {
prop1: { type: "string" },
prop2: { type: "integer", optional: true },
prop3: { type: "integer", unsupported: true },
},
},
],
},
{
name: "qux",
type: "function",
parameters: [{ name: "arg1", $ref: "type1" }],
},
{
name: "quack",
type: "function",
parameters: [{ name: "arg1", $ref: "type2" }],
},
{
name: "quora",
type: "function",
parameters: [{ name: "arg1", type: "function" }],
},
{
name: "quileute",
type: "function",
parameters: [
{ name: "arg1", type: "integer", optional: true },
{ name: "arg2", type: "integer" },
],
},
{
name: "queets",
type: "function",
unsupported: true,
parameters: [],
},
{
name: "quintuplets",
type: "function",
parameters: [
{
name: "obj",
type: "object",
properties: [],
additionalProperties: { type: "integer" },
},
],
},
{
name: "quasar",
type: "function",
parameters: [
{
name: "abc",
type: "object",
properties: {
func: {
type: "function",
parameters: [{ name: "x", type: "integer" }],
},
},
},
],
},
{
name: "quosimodo",
type: "function",
parameters: [
{
name: "xyz",
type: "object",
additionalProperties: { type: "any" },
},
],
},
{
name: "patternprop",
type: "function",
parameters: [
{
name: "obj",
type: "object",
properties: { prop1: { type: "string", pattern: "^\\d+$" } },
patternProperties: {
"(?i)^prop\\d+$": { type: "string" },
"^foo\\d+$": { type: "string" },
},
},
],
},
{
name: "pattern",
type: "function",
parameters: [
{ name: "arg", type: "string", pattern: "(?i)^[0-9a-f]+$" },
],
},
{
name: "format",
type: "function",
parameters: [
{
name: "arg",
type: "object",
properties: {
hostname: { type: "string", format: "hostname", optional: true },
canonicalDomain: {
type: "string",
format: "canonicalDomain",
optional: "omit-key-if-missing",
},
url: { type: "string", format: "url", optional: true },
origin: { type: "string", format: "origin", optional: true },
relativeUrl: {
type: "string",
format: "relativeUrl",
optional: true,
},
strictRelativeUrl: {
type: "string",
format: "strictRelativeUrl",
optional: true,
},
imageDataOrStrictRelativeUrl: {
type: "string",
format: "imageDataOrStrictRelativeUrl",
optional: true,
},
},
},
],
},
{
name: "formatDate",
type: "function",
parameters: [
{
name: "arg",
type: "object",
properties: {
date: { type: "string", format: "date", optional: true },
},
},
],
},
{
name: "deep",
type: "function",
parameters: [
{
name: "arg",
type: "object",
properties: {
foo: {
type: "object",
properties: {
bar: {
type: "array",
items: {
type: "object",
properties: {
baz: {
type: "object",
properties: {
required: { type: "integer" },
optional: { type: "string", optional: true },
},
},
},
},
},
},
},
},
},
],
},
{
name: "errors",
type: "function",
parameters: [
{
name: "arg",
type: "object",
properties: {
warn: {
type: "string",
pattern: "^\\d+$",
optional: true,
onError: "warn",
},
ignore: {
type: "string",
pattern: "^\\d+$",
optional: true,
onError: "ignore",
},
default: {
type: "string",
pattern: "^\\d+$",
optional: true,
},
},
},
],
},
{
name: "localize",
type: "function",
parameters: [
{
name: "arg",
type: "object",
properties: {
foo: { type: "string", preprocess: "localize", optional: true },
bar: { type: "string", optional: true },
url: {
type: "string",
preprocess: "localize",
format: "url",
optional: true,
},
},
},
],
},
{
name: "extended1",
type: "function",
parameters: [{ name: "val", $ref: "basetype1" }],
},
{
name: "extended2",
type: "function",
parameters: [{ name: "val", $ref: "basetype2" }],
},
{
name: "callderived1",
type: "function",
parameters: [{ name: "value", $ref: "derivedtype1" }],
},
{
name: "callderived2",
type: "function",
parameters: [{ name: "value", $ref: "derivedtype2" }],
},
],
events: [
{
name: "onFoo",
type: "function",
},
{
name: "onBar",
type: "function",
extraParameters: [
{
name: "filter",
type: "integer",
optional: true,
default: 1,
},
],
},
],
},
{
namespace: "foreign",
properties: {
foreignRef: { $ref: "testing.submodule" },
},
},
{
namespace: "inject",
properties: {
PROP1: { value: "should inject" },
},
},
{
namespace: "do-not-inject",
properties: {
PROP1: { value: "should not inject" },
},
},
];
add_task(async function () {
let wrapper = getContextWrapper();
let url = "data:," + JSON.stringify(json);
Schemas._rootSchema = null;
await Schemas.load(url);
let root = {};
Schemas.inject(root, wrapper);
Assert.equal(root.testing.PROP1, 20, "simple value property");
Assert.equal(root.testing.type1.VALUE1, "value1", "enum type");
Assert.equal(root.testing.type1.VALUE2, "value2", "enum type");
Assert.equal("inject" in root, true, "namespace 'inject' should be injected");
Assert.equal(
root["do-not-inject"],
undefined,
"namespace 'do-not-inject' should not be injected"
);
root.testing.foo(11, true);
wrapper.verify("call", "testing", "foo", [11, true]);
root.testing.foo(true);
wrapper.verify("call", "testing", "foo", [99, true]);
root.testing.foo(null, true);
wrapper.verify("call", "testing", "foo", [99, true]);
root.testing.foo(undefined, true);
wrapper.verify("call", "testing", "foo", [99, true]);
root.testing.foo(11);
wrapper.verify("call", "testing", "foo", [11, null]);
Assert.throws(
() => root.testing.bar(11),
/Incorrect argument types/,
"should throw without required arg"
);
Assert.throws(
() => root.testing.bar(11, true, 10),
/Incorrect argument types/,
"should throw with too many arguments"
);
root.testing.bar(true);
wrapper.verify("call", "testing", "bar", [null, true]);
root.testing.baz({ prop1: "hello", prop2: 22 });
wrapper.verify("call", "testing", "baz", [{ prop1: "hello", prop2: 22 }]);
root.testing.baz({ prop1: "hello" });
wrapper.verify("call", "testing", "baz", [{ prop1: "hello", prop2: null }]);
root.testing.baz({ prop1: "hello", prop2: null });
wrapper.verify("call", "testing", "baz", [{ prop1: "hello", prop2: null }]);
Assert.throws(
() => root.testing.baz({ prop2: 12 }),
/Property "prop1" is required/,
"should throw without required property"
);
Assert.throws(
() => root.testing.baz({ prop1: "hi", prop3: 12 }),
/Property "prop3" is unsupported by Firefox/,
"should throw with unsupported property"
);
Assert.throws(
() => root.testing.baz({ prop1: "hi", prop4: 12 }),
/Unexpected property "prop4"/,
"should throw with unexpected property"
);
Assert.throws(
() => root.testing.baz({ prop1: 12 }),
/Expected string instead of 12/,
"should throw with wrong type"
);
root.testing.qux("value2");
wrapper.verify("call", "testing", "qux", ["value2"]);
Assert.throws(
() => root.testing.qux("value4"),
/Invalid enumeration value "value4"/,
"should throw for invalid enum value"
);
root.testing.quack({ prop1: 12, prop2: ["value1", "value3"] });
wrapper.verify("call", "testing", "quack", [
{ prop1: 12, prop2: ["value1", "value3"] },
]);
Assert.throws(
() =>
root.testing.quack({ prop1: 12, prop2: ["value1", "value3", "value4"] }),
/Invalid enumeration value "value4"/,
"should throw for invalid array type"
);
function f() {}
root.testing.quora(f);
Assert.equal(
JSON.stringify(wrapper.tallied.slice(0, -1)),
JSON.stringify(["call", "testing", "quora"])
);
Assert.equal(wrapper.tallied[3][0], f);
wrapper.tallied = null;
let g = () => 0;
root.testing.quora(g);
Assert.equal(
JSON.stringify(wrapper.tallied.slice(0, -1)),
JSON.stringify(["call", "testing", "quora"])
);
Assert.equal(wrapper.tallied[3][0], g);
wrapper.tallied = null;
root.testing.quileute(10);
wrapper.verify("call", "testing", "quileute", [null, 10]);
Assert.throws(
() => root.testing.queets(),
/queets is not a function/,
"should throw for unsupported functions"
);
root.testing.quintuplets({ a: 10, b: 20, c: 30 });
wrapper.verify("call", "testing", "quintuplets", [{ a: 10, b: 20, c: 30 }]);
Assert.throws(
() => root.testing.quintuplets({ a: 10, b: 20, c: 30, d: "hi" }),
/Expected integer instead of "hi"/,
"should throw for wrong additionalProperties type"
);
root.testing.quasar({ func: f });
Assert.equal(
JSON.stringify(wrapper.tallied.slice(0, -1)),
JSON.stringify(["call", "testing", "quasar"])
);
Assert.equal(wrapper.tallied[3][0].func, f);
root.testing.quosimodo({ a: 10, b: 20, c: 30 });
wrapper.verify("call", "testing", "quosimodo", [{ a: 10, b: 20, c: 30 }]);
Assert.throws(
() => root.testing.quosimodo(10),
/Incorrect argument types/,
"should throw for wrong type"
);
root.testing.patternprop({
prop1: "12",
prop2: "42",
Prop3: "43",
foo1: "x",
});
wrapper.verify("call", "testing", "patternprop", [
{ prop1: "12", prop2: "42", Prop3: "43", foo1: "x" },
]);
root.testing.patternprop({ prop1: "12" });
wrapper.verify("call", "testing", "patternprop", [{ prop1: "12" }]);
Assert.throws(
() => root.testing.patternprop({ prop1: "12", foo1: null }),
/Expected string instead of null/,
"should throw for wrong property type"
);
Assert.throws(
() => root.testing.patternprop({ prop1: "xx", prop2: "yy" }),
/String "xx" must match \/\^\\d\+\$\//,
"should throw for wrong property type"
);
Assert.throws(
() => root.testing.patternprop({ prop1: "12", prop2: 42 }),
/Expected string instead of 42/,
"should throw for wrong property type"
);
Assert.throws(
() => root.testing.patternprop({ prop1: "12", prop2: null }),
/Expected string instead of null/,
"should throw for wrong property type"
);
Assert.throws(
() => root.testing.patternprop({ prop1: "12", propx: "42" }),
/Unexpected property "propx"/,
"should throw for unexpected property"
);
Assert.throws(
() => root.testing.patternprop({ prop1: "12", Foo1: "x" }),
/Unexpected property "Foo1"/,
"should throw for unexpected property"
);
root.testing.pattern("DEADbeef");
wrapper.verify("call", "testing", "pattern", ["DEADbeef"]);
Assert.throws(
() => root.testing.pattern("DEADcow"),
/String "DEADcow" must match \/\^\[0-9a-f\]\+\$\/i/,
"should throw for non-match"
);
root.testing.format({ hostname: "foo" });
wrapper.verify("call", "testing", "format", [
{
hostname: "foo",
imageDataOrStrictRelativeUrl: null,
origin: null,
relativeUrl: null,
strictRelativeUrl: null,
url: null,
},
]);
for (let invalid of ["", " ", "http://foo", "foo/bar", "foo.com/", "foo?"]) {
Assert.throws(
() => root.testing.format({ hostname: invalid }),
/Invalid hostname/,
"should throw for invalid hostname"
);
Assert.throws(
() => root.testing.format({ canonicalDomain: invalid }),
/Invalid domain /,
`should throw for invalid canonicalDomain (${invalid})`
);
}
for (let invalid of [
"%61", // ASCII should not be URL-encoded.
"foo:12345", // It is a common mistake to use .host instead of .hostname.
"2", // Single digit is an IPv4 address, but should be written as 0.0.0.2.
"::1", // IPv6 addresses should have brackets.
"[::1A]", // not lowercase.
"[::ffff:127.0.0.1]", // not a canonical IPv6 representation.
"UPPERCASE", // not lowercase.
"straß.de", // not punycode.
]) {
Assert.throws(
() => root.testing.format({ canonicalDomain: invalid }),
/Invalid domain /,
`should throw for invalid canonicalDomain (${invalid})`
);
}
for (let valid of ["0.0.0.2", "[::1]", "[::1a]", "lowercase", "."]) {
root.testing.format({ canonicalDomain: valid });
wrapper.verify("call", "testing", "format", [
{
canonicalDomain: valid,
hostname: null,
imageDataOrStrictRelativeUrl: null,
origin: null,
relativeUrl: null,
strictRelativeUrl: null,
url: null,
},
]);
}
for (let valid of [
]) {
root.testing.format({ origin: valid });
}
for (let invalid of [
"file:/foo/bar",
"",
" ",
]) {
Assert.throws(
() => root.testing.format({ origin: invalid }),
/Invalid origin/,
"should throw for invalid origin"
);
}
root.testing.format({ url: "http://foo/bar", relativeUrl: "http://foo/bar" });
wrapper.verify("call", "testing", "format", [
{
hostname: null,
imageDataOrStrictRelativeUrl: null,
origin: null,
relativeUrl: "http://foo/bar",
strictRelativeUrl: null,
url: "http://foo/bar",
},
]);
root.testing.format({
relativeUrl: "foo.html",
strictRelativeUrl: "foo.html",
});
wrapper.verify("call", "testing", "format", [
{
hostname: null,
imageDataOrStrictRelativeUrl: null,
origin: null,
relativeUrl: `${wrapper.url}foo.html`,
strictRelativeUrl: `${wrapper.url}foo.html`,
url: null,
},
]);
root.testing.format({
imageDataOrStrictRelativeUrl: "data:image/png;base64,A",
});
wrapper.verify("call", "testing", "format", [
{
hostname: null,
imageDataOrStrictRelativeUrl: "data:image/png;base64,A",
origin: null,
relativeUrl: null,
strictRelativeUrl: null,
url: null,
},
]);
root.testing.format({
imageDataOrStrictRelativeUrl: "data:image/jpeg;base64,A",
});
wrapper.verify("call", "testing", "format", [
{
hostname: null,
imageDataOrStrictRelativeUrl: "data:image/jpeg;base64,A",
origin: null,
relativeUrl: null,
strictRelativeUrl: null,
url: null,
},
]);
root.testing.format({ imageDataOrStrictRelativeUrl: "foo.html" });
wrapper.verify("call", "testing", "format", [
{
hostname: null,
imageDataOrStrictRelativeUrl: `${wrapper.url}foo.html`,
origin: null,
relativeUrl: null,
strictRelativeUrl: null,
url: null,
},
]);
for (let format of ["url", "relativeUrl"]) {
Assert.throws(
() => root.testing.format({ [format]: "chrome://foo/content/" }),
/Access denied/,
"should throw for access denied"
);
}
for (let urlString of ["//foo.html", "http://foo/bar.html"]) {
Assert.throws(
() => root.testing.format({ strictRelativeUrl: urlString }),
/must be a relative URL/,
"should throw for non-relative URL"
);
}
Assert.throws(
() =>
root.testing.format({
imageDataOrStrictRelativeUrl: "data:image/svg+xml;utf8,A",
}),
/must be a relative or PNG or JPG data:image URL/,
"should throw for non-relative or non PNG/JPG data URL"
);
const dates = [
"2016-03-04",
"2016-03-04T08:00:00Z",
"2016-03-04T08:00:00.000Z",
"2016-03-04T08:00:00-08:00",
"2016-03-04T08:00:00.000-08:00",
"2016-03-04T08:00:00+08:00",
"2016-03-04T08:00:00.000+08:00",
"2016-03-04T08:00:00+0800",
"2016-03-04T08:00:00-0800",
];
dates.forEach(str => {
root.testing.formatDate({ date: str });
wrapper.verify("call", "testing", "formatDate", [{ date: str }]);
});
// Make sure that a trivial change to a valid date invalidates it.
dates.forEach(str => {
Assert.throws(
() => root.testing.formatDate({ date: "0" + str }),
/Invalid date string/,
"should throw for invalid iso date string"
);
Assert.throws(
() => root.testing.formatDate({ date: str + "0" }),
/Invalid date string/,
"should throw for invalid iso date string"
);
});
const badDates = [
"I do not look anything like a date string",
"2016-99-99",
"2016-03-04T25:00:00Z",
];
badDates.forEach(str => {
Assert.throws(
() => root.testing.formatDate({ date: str }),
/Invalid date string/,
"should throw for invalid iso date string"
);
});
root.testing.deep({
foo: { bar: [{ baz: { required: 12, optional: "42" } }] },
});
wrapper.verify("call", "testing", "deep", [
{ foo: { bar: [{ baz: { optional: "42", required: 12 } }] } },
]);
Assert.throws(
() => root.testing.deep({ foo: { bar: [{ baz: { optional: "42" } }] } }),
/Type error for parameter arg \(Error processing foo\.bar\.0\.baz: Property "required" is required\) for testing\.deep/,
"should throw with the correct object path"
);
Assert.throws(
() =>
root.testing.deep({
foo: { bar: [{ baz: { optional: 42, required: 12 } }] },
}),
/Type error for parameter arg \(Error processing foo\.bar\.0\.baz\.optional: Expected string instead of 42\) for testing\.deep/,
"should throw with the correct object path"
);
wrapper.talliedErrors.length = 0;
root.testing.errors({ default: "0123", ignore: "0123", warn: "0123" });
wrapper.verify("call", "testing", "errors", [
{ default: "0123", ignore: "0123", warn: "0123" },
]);
wrapper.checkErrors([]);
root.testing.errors({ default: "0123", ignore: "x123", warn: "0123" });
wrapper.verify("call", "testing", "errors", [
{ default: "0123", ignore: null, warn: "0123" },
]);
wrapper.checkErrors([]);
ExtensionTestUtils.failOnSchemaWarnings(false);
root.testing.errors({ default: "0123", ignore: "0123", warn: "x123" });
ExtensionTestUtils.failOnSchemaWarnings(true);
wrapper.verify("call", "testing", "errors", [
{ default: "0123", ignore: "0123", warn: null },
]);
wrapper.checkErrors(['String "x123" must match /^\\d+$/']);
root.testing.onFoo.addListener(f);
Assert.equal(
JSON.stringify(wrapper.tallied.slice(0, -1)),
JSON.stringify(["addListener", "testing", "onFoo"])
);
Assert.equal(wrapper.tallied[3][0], f);
Assert.equal(JSON.stringify(wrapper.tallied[3][1]), JSON.stringify([]));
wrapper.tallied = null;
root.testing.onFoo.removeListener(f);
Assert.equal(
JSON.stringify(wrapper.tallied.slice(0, -1)),
JSON.stringify(["removeListener", "testing", "onFoo"])
);
Assert.equal(wrapper.tallied[3][0], f);
wrapper.tallied = null;
root.testing.onFoo.hasListener(f);
Assert.equal(
JSON.stringify(wrapper.tallied.slice(0, -1)),
JSON.stringify(["hasListener", "testing", "onFoo"])
);
Assert.equal(wrapper.tallied[3][0], f);
wrapper.tallied = null;
Assert.throws(
() => root.testing.onFoo.addListener(10),
/Invalid listener/,
"addListener with non-function should throw"
);
root.testing.onBar.addListener(f, 10);
Assert.equal(
JSON.stringify(wrapper.tallied.slice(0, -1)),
JSON.stringify(["addListener", "testing", "onBar"])
);
Assert.equal(wrapper.tallied[3][0], f);
Assert.equal(JSON.stringify(wrapper.tallied[3][1]), JSON.stringify([10]));
wrapper.tallied = null;
root.testing.onBar.addListener(f);
Assert.equal(
JSON.stringify(wrapper.tallied.slice(0, -1)),
JSON.stringify(["addListener", "testing", "onBar"])
);
Assert.equal(wrapper.tallied[3][0], f);
Assert.equal(JSON.stringify(wrapper.tallied[3][1]), JSON.stringify([1]));
wrapper.tallied = null;
Assert.throws(
() => root.testing.onBar.addListener(f, "hi"),
/Incorrect argument types/,
"addListener with wrong extra parameter should throw"
);
let target = { prop1: 12, prop2: ["value1", "value3"] };
let proxy = new Proxy(target, {});
Assert.throws(
() => root.testing.quack(proxy),
/Expected a plain JavaScript object, got a Proxy/,
"should throw when passing a Proxy"
);
if (Symbol.toStringTag) {
let stringTarget = { prop1: 12, prop2: ["value1", "value3"] };
stringTarget[Symbol.toStringTag] = () => "[object Object]";
let stringProxy = new Proxy(stringTarget, {});
Assert.throws(
() => root.testing.quack(stringProxy),
/Expected a plain JavaScript object, got a Proxy/,
"should throw when passing a Proxy"
);
}
root.testing.localize({
foo: "__MSG_foo__",
bar: "__MSG_foo__",
url: "__MSG_http://example.com/__",
});
wrapper.verify("call", "testing", "localize", [
{ bar: "__MSG_foo__", foo: "FOO", url: "http://example.com/" },
]);
Assert.throws(
() => root.testing.localize({ url: "__MSG_/foo/bar__" }),
/\/FOO\/BAR is not a valid URL\./,
"should throw for invalid URL"
);
root.testing.extended1({ prop1: "foo", prop2: "bar" });
wrapper.verify("call", "testing", "extended1", [
{ prop1: "foo", prop2: "bar" },
]);
Assert.throws(
() => root.testing.extended1({ prop1: "foo", prop2: 12 }),
/Expected string instead of 12/,
"should throw for wrong property type"
);
Assert.throws(
() => root.testing.extended1({ prop1: "foo" }),
/Property "prop2" is required/,
"should throw for missing property"
);
Assert.throws(
() => root.testing.extended1({ prop1: "foo", prop2: "bar", prop3: "xxx" }),
/Unexpected property "prop3"/,
"should throw for extra property"
);
root.testing.extended2("foo");
wrapper.verify("call", "testing", "extended2", ["foo"]);
root.testing.extended2(12);
wrapper.verify("call", "testing", "extended2", [12]);
Assert.throws(
() => root.testing.extended2(true),
/Incorrect argument types/,
"should throw for wrong argument type"
);
root.testing.prop3.sub_foo();
wrapper.verify("call", "testing.prop3", "sub_foo", []);
Assert.throws(
() => root.testing.prop4.sub_foo(),
/root.testing.prop4 is undefined/,
"should throw for unsupported submodule"
);
root.foreign.foreignRef.sub_foo();
wrapper.verify("call", "foreign.foreignRef", "sub_foo", []);
root.testing.callderived1({ baseprop: "s1", derivedprop: "s2" });
wrapper.verify("call", "testing", "callderived1", [
{ baseprop: "s1", derivedprop: "s2" },
]);
Assert.throws(
() => root.testing.callderived1({ baseprop: "s1", derivedprop: 42 }),
/Error processing derivedprop: Expected string/,
"Two different objects may $import the same base object"
);
Assert.throws(
() => root.testing.callderived1({ baseprop: "s1" }),
/Property "derivedprop" is required/,
"Object using $import has its local properites"
);
Assert.throws(
() => root.testing.callderived1({ derivedprop: "s2" }),
/Property "baseprop" is required/,
"Object using $import has imported properites"
);
root.testing.callderived2({ baseprop: "s1", derivedprop: 42 });
wrapper.verify("call", "testing", "callderived2", [
{ baseprop: "s1", derivedprop: 42 },
]);
Assert.throws(
() => root.testing.callderived2({ baseprop: "s1", derivedprop: "s2" }),
/Error processing derivedprop: Expected integer/,
"Two different objects may $import the same base object"
);
Assert.throws(
() => root.testing.callderived2({ baseprop: "s1" }),
/Property "derivedprop" is required/,
"Object using $import has its local properites"
);
Assert.throws(
() => root.testing.callderived2({ derivedprop: 42 }),
/Property "baseprop" is required/,
"Object using $import has imported properites"
);
});
let deprecatedJson = [
{
namespace: "deprecated",
properties: {
accessor: {
type: "string",
writable: true,
deprecated: "This is not the property you are looking for",
},
},
types: [
{
id: "Type",
type: "string",
},
],
functions: [
{
name: "property",
type: "function",
parameters: [
{
name: "arg",
type: "object",
properties: {
foo: {
type: "string",
},
},
additionalProperties: {
type: "any",
deprecated: "Unknown property",
},
},
],
},
{
name: "value",
type: "function",
parameters: [
{
name: "arg",
choices: [
{
type: "integer",
},
{
type: "string",
deprecated: "Please use an integer, not ${value}",
},
],
},
],
},
{
name: "choices",
type: "function",
parameters: [
{
name: "arg",
deprecated: "You have no choices",
choices: [
{
type: "integer",
},
],
},
],
},
{
name: "ref",
type: "function",
parameters: [
{
name: "arg",
choices: [
{
$ref: "Type",
deprecated: "Deprecated alias",
},
],
},
],
},
{
name: "method",
type: "function",
deprecated: "Do not call this method",
parameters: [],
},
],
events: [
{
name: "onDeprecated",
type: "function",
deprecated: "This event does not work",
},
],
},
];
add_task(async function testDeprecation() {
let wrapper = getContextWrapper();
// This whole test expects deprecation warnings.
ExtensionTestUtils.failOnSchemaWarnings(false);
let url = "data:," + JSON.stringify(deprecatedJson);
Schemas._rootSchema = null;
await Schemas.load(url);
let root = {};
Schemas.inject(root, wrapper);
root.deprecated.property({ foo: "bar", xxx: "any", yyy: "property" });
wrapper.verify("call", "deprecated", "property", [
{ foo: "bar", xxx: "any", yyy: "property" },
]);
wrapper.checkErrors([
"Warning processing xxx: Unknown property",
"Warning processing yyy: Unknown property",
]);
root.deprecated.value(12);
wrapper.verify("call", "deprecated", "value", [12]);
wrapper.checkErrors([]);
root.deprecated.value("12");
wrapper.verify("call", "deprecated", "value", ["12"]);
wrapper.checkErrors(['Please use an integer, not "12"']);
root.deprecated.choices(12);
wrapper.verify("call", "deprecated", "choices", [12]);
wrapper.checkErrors(["You have no choices"]);
root.deprecated.ref("12");
wrapper.verify("call", "deprecated", "ref", ["12"]);
wrapper.checkErrors(["Deprecated alias"]);
root.deprecated.method();
wrapper.verify("call", "deprecated", "method", []);
wrapper.checkErrors(["Do not call this method"]);
void root.deprecated.accessor;
wrapper.verify("get", "deprecated", "accessor", null);
wrapper.checkErrors(["This is not the property you are looking for"]);
root.deprecated.accessor = "x";
wrapper.verify("set", "deprecated", "accessor", "x");
wrapper.checkErrors(["This is not the property you are looking for"]);
root.deprecated.onDeprecated.addListener(() => {});
wrapper.checkErrors(["This event does not work"]);
root.deprecated.onDeprecated.removeListener(() => {});
wrapper.checkErrors(["This event does not work"]);
root.deprecated.onDeprecated.hasListener(() => {});
wrapper.checkErrors(["This event does not work"]);
ExtensionTestUtils.failOnSchemaWarnings(true);
Assert.throws(
() => root.deprecated.onDeprecated.hasListener(() => {}),
/This event does not work/,
"Deprecation warning with extensions.webextensions.warnings-as-errors=true"
);
});
let choicesJson = [
{
namespace: "choices",
types: [],
functions: [
{
name: "meh",
type: "function",
parameters: [
{
name: "arg",
choices: [
{
type: "string",
enum: ["foo", "bar", "baz"],
},
{
type: "string",
pattern: "florg.*meh",
},
{
type: "integer",
minimum: 12,
maximum: 42,
},
],
},
],
},
{
name: "foo",
type: "function",
parameters: [
{
name: "arg",
choices: [
{
type: "object",
properties: {
blurg: {
type: "string",
unsupported: true,
optional: true,
},
},
additionalProperties: {
type: "string",
},
},
{
type: "string",
},
{
type: "array",
minItems: 2,
maxItems: 3,
items: {
type: "integer",
},
},
],
},
],
},
{
name: "bar",
type: "function",
parameters: [
{
name: "arg",
choices: [
{
type: "object",
properties: {
baz: {
type: "string",
},
},
},
{
type: "array",
items: {
type: "integer",
},
},
],
},
],
},
],
},
];
add_task(async function testChoices() {
let wrapper = getContextWrapper();
let url = "data:," + JSON.stringify(choicesJson);
Schemas._rootSchema = null;
await Schemas.load(url);
let root = {};
Schemas.inject(root, wrapper);
Assert.throws(
() => root.choices.meh("frog"),
/Value "frog" must either: be one of \["foo", "bar", "baz"\], match the pattern \/florg\.\*meh\/, or be an integer value/
);
Assert.throws(
() => root.choices.meh(4),
/be a string value, or be at least 12/
);
Assert.throws(
() => root.choices.meh(43),
/be a string value, or be no greater than 42/
);
Assert.throws(
() => root.choices.foo([]),
/be an object value, be a string value, or have at least 2 items/
);
Assert.throws(
() => root.choices.foo([1, 2, 3, 4]),
/be an object value, be a string value, or have at most 3 items/
);
Assert.throws(
() => root.choices.foo({ foo: 12 }),
/.foo must be a string value, be a string value, or be an array value/
);
Assert.throws(
() => root.choices.foo({ blurg: "foo" }),
/not contain an unsupported "blurg" property, be a string value, or be an array value/
);
Assert.throws(
() => root.choices.bar({}),
/contain the required "baz" property, or be an array value/
);
Assert.throws(
() => root.choices.bar({ baz: "x", quux: "y" }),
/not contain an unexpected "quux" property, or be an array value/
);
Assert.throws(
() => root.choices.bar({ baz: "x", quux: "y", foo: "z" }),
/not contain the unexpected properties \[foo, quux\], or be an array value/
);
});
let permissionsJson = [
{
namespace: "noPerms",
types: [],
functions: [
{
name: "noPerms",
type: "function",
parameters: [],
},
{
name: "fooPerm",
type: "function",
permissions: ["foo"],
parameters: [],
},
],
},
{
namespace: "fooPerm",
permissions: ["foo"],
types: [],
functions: [
{
name: "noPerms",
type: "function",
parameters: [],
},
{
name: "fooBarPerm",
type: "function",
permissions: ["foo.bar"],
parameters: [],
},
],
},
];
add_task(async function testPermissions() {
let url = "data:," + JSON.stringify(permissionsJson);
Schemas._rootSchema = null;
await Schemas.load(url);
let wrapper = getContextWrapper();
let root = {};
Schemas.inject(root, wrapper);
equal(typeof root.noPerms, "object", "noPerms namespace should exist");
equal(
typeof root.noPerms.noPerms,
"function",
"noPerms.noPerms method should exist"
);
equal(
root.noPerms.fooPerm,
undefined,
"noPerms.fooPerm should not method exist"
);
equal(root.fooPerm, undefined, "fooPerm namespace should not exist");
info('Add "foo" permission');
wrapper.permissions.add("foo");
root = {};
Schemas.inject(root, wrapper);
equal(typeof root.noPerms, "object", "noPerms namespace should exist");
equal(
typeof root.noPerms.noPerms,
"function",
"noPerms.noPerms method should exist"
);
equal(
typeof root.noPerms.fooPerm,
"function",
"noPerms.fooPerm method should exist"
);
equal(typeof root.fooPerm, "object", "fooPerm namespace should exist");
equal(
typeof root.fooPerm.noPerms,
"function",
"noPerms.noPerms method should exist"
);
equal(
root.fooPerm.fooBarPerm,
undefined,
"fooPerm.fooBarPerm method should not exist"
);
info('Add "foo.bar" permission');
wrapper.permissions.add("foo.bar");
root = {};
Schemas.inject(root, wrapper);
equal(typeof root.noPerms, "object", "noPerms namespace should exist");
equal(
typeof root.noPerms.noPerms,
"function",
"noPerms.noPerms method should exist"
);
equal(
typeof root.noPerms.fooPerm,
"function",
"noPerms.fooPerm method should exist"
);
equal(typeof root.fooPerm, "object", "fooPerm namespace should exist");
equal(
typeof root.fooPerm.noPerms,
"function",
"noPerms.noPerms method should exist"
);
equal(
typeof root.fooPerm.fooBarPerm,
"function",
"noPerms.fooBarPerm method should exist"
);
});
let nestedNamespaceJson = [
{
namespace: "nested.namespace",
types: [
{
id: "CustomType",
type: "object",
events: [
{
name: "onEvent",
type: "function",
},
],
properties: {
url: {
type: "string",
},
},
functions: [
{
name: "functionOnCustomType",
type: "function",
parameters: [
{
name: "title",
type: "string",
},
],
},
],
},
],
properties: {
instanceOfCustomType: {
$ref: "CustomType",
},
},
functions: [
{
name: "create",
type: "function",
parameters: [
{
name: "title",
type: "string",
},
],
},
],
},
];
add_task(async function testNestedNamespace() {
let url = "data:," + JSON.stringify(nestedNamespaceJson);
let wrapper = getContextWrapper();
Schemas._rootSchema = null;
await Schemas.load(url);
let root = {};
Schemas.inject(root, wrapper);
ok(root.nested, "The root object contains the first namespace level");
ok(
root.nested.namespace,
"The first level object contains the second namespace level"
);
ok(
root.nested.namespace.create,
"Got the expected function in the nested namespace"
);
equal(
typeof root.nested.namespace.create,
"function",
"The property is a function as expected"
);
let { instanceOfCustomType } = root.nested.namespace;
ok(
instanceOfCustomType,
"Got the expected instance of the CustomType defined in the schema"
);
ok(
instanceOfCustomType.functionOnCustomType,
"Got the expected method in the CustomType instance"
);
ok(
instanceOfCustomType.onEvent &&
instanceOfCustomType.onEvent.addListener &&
typeof instanceOfCustomType.onEvent.addListener == "function",
"Got the expected event defined in the CustomType instance"
);
instanceOfCustomType.functionOnCustomType("param_value");
wrapper.verify(
"call",
"nested.namespace.instanceOfCustomType",
"functionOnCustomType",
["param_value"]
);
let fakeListener = () => {};
instanceOfCustomType.onEvent.addListener(fakeListener);
wrapper.verify(
"addListener",
"nested.namespace.instanceOfCustomType",
"onEvent",
[fakeListener, []]
);
instanceOfCustomType.onEvent.removeListener(fakeListener);
wrapper.verify(
"removeListener",
"nested.namespace.instanceOfCustomType",
"onEvent",
[fakeListener]
);
// TODO: test support properties in a SubModuleType defined in the schema,
// once implemented, e.g.:
// ok("url" in instanceOfCustomType,
// "Got the expected property defined in the CustomType instance");
});
let $importJson = [
{
namespace: "from_the",
$import: "future",
},
{
namespace: "future",
properties: {
PROP1: { value: "original value" },
PROP2: { value: "second original" },
},
types: [
{
id: "Colour",
type: "string",
enum: ["red", "white", "blue"],
},
],
functions: [
{
name: "dye",
type: "function",
parameters: [{ name: "arg", $ref: "Colour" }],
},
],
},
{
namespace: "embrace",
$import: "future",
properties: {
PROP2: { value: "overridden value" },
},
types: [
{
id: "Colour",
type: "string",
enum: ["blue", "orange"],
},
],
},
];
add_task(async function test_$import() {
let wrapper = getContextWrapper();
let url = "data:," + JSON.stringify($importJson);
Schemas._rootSchema = null;
await Schemas.load(url);
let root = {};
Schemas.inject(root, wrapper);
equal(root.from_the.PROP1, "original value", "imported property");
equal(root.from_the.PROP2, "second original", "second imported property");
equal(root.from_the.Colour.RED, "red", "imported enum type");
equal(typeof root.from_the.dye, "function", "imported function");
root.from_the.dye("white");
wrapper.verify("call", "from_the", "dye", ["white"]);
Assert.throws(
() => root.from_the.dye("orange"),
/Invalid enumeration value/,
"original imported argument type Colour doesn't include 'orange'"
);
equal(root.embrace.PROP1, "original value", "imported property");
equal(root.embrace.PROP2, "overridden value", "overridden property");
equal(root.embrace.Colour.ORANGE, "orange", "overridden enum type");
equal(typeof root.embrace.dye, "function", "imported function");
root.embrace.dye("orange");
wrapper.verify("call", "embrace", "dye", ["orange"]);
Assert.throws(
() => root.embrace.dye("white"),
/Invalid enumeration value/,
"overridden argument type Colour doesn't include 'white'"
);
});
add_task(async function testLocalAPIImplementation() {
let countGet2 = 0;
let countProp3 = 0;
let countProp3SubFoo = 0;
let testingApiObj = {
get PROP1() {
// PROP1 is a schema-defined constant.
throw new Error("Unexpected get PROP1");
},
get prop2() {
++countGet2;
return "prop2 val";
},
get prop3() {
throw new Error("Unexpected get prop3");
},
set prop3(v) {
// prop3 is a submodule, defined as a function, so the API should not pass
// through assignment to prop3.
throw new Error("Unexpected set prop3");
},
};
let submoduleApiObj = {
get sub_foo() {
++countProp3;
return () => {
return ++countProp3SubFoo;
};
},
};
let localWrapper = {
manifestVersion: 2,
cloneScope: global,
shouldInject(ns, name) {
return name == "testing" || ns == "testing" || ns == "testing.prop3";
},
getImplementation(ns, name) {
Assert.ok(ns == "testing" || ns == "testing.prop3");
if (ns == "testing.prop3" && name == "sub_foo") {
// It is fine to use `null` here because we don't call async functions.
return new LocalAPIImplementation(submoduleApiObj, name, null);
}
// It is fine to use `null` here because we don't call async functions.
return new LocalAPIImplementation(testingApiObj, name, null);
},
};
let root = {};
Schemas.inject(root, localWrapper);
Assert.equal(countGet2, 0);
Assert.equal(countProp3, 0);
Assert.equal(countProp3SubFoo, 0);
Assert.equal(root.testing.PROP1, 20);
Assert.equal(root.testing.prop2, "prop2 val");
Assert.equal(countGet2, 1);
Assert.equal(root.testing.prop2, "prop2 val");
Assert.equal(countGet2, 2);
info(JSON.stringify(root.testing));
Assert.equal(root.testing.prop3.sub_foo(), 1);
Assert.equal(countProp3, 1);
Assert.equal(countProp3SubFoo, 1);
Assert.equal(root.testing.prop3.sub_foo(), 2);
Assert.equal(countProp3, 2);
Assert.equal(countProp3SubFoo, 2);
root.testing.prop3.sub_foo = () => {
return "overwritten";
};
Assert.equal(root.testing.prop3.sub_foo(), "overwritten");
root.testing.prop3 = {
sub_foo() {
return "overwritten again";
},
};
Assert.equal(root.testing.prop3.sub_foo(), "overwritten again");
Assert.equal(countProp3SubFoo, 2);
});
let defaultsJson = [
{
namespace: "defaultsJson",
types: [],
functions: [
{
name: "defaultFoo",
type: "function",
parameters: [
{
name: "arg",
type: "object",
optional: true,
properties: {
prop1: { type: "integer", optional: true },
},
default: { prop1: 1 },
},
],
returns: {
type: "object",
additionalProperties: true,
},
},
],
},
];
add_task(async function testDefaults() {
let url = "data:," + JSON.stringify(defaultsJson);
Schemas._rootSchema = null;
await Schemas.load(url);
let testingApiObj = {
defaultFoo: function (arg) {
if (Object.keys(arg) != "prop1") {
throw new Error(
`Received the expected default object, default: ${JSON.stringify(
arg
)}`
);
}
arg.newProp = 1;
return arg;
},
};
let localWrapper = {
manifestVersion: 2,
cloneScope: global,
shouldInject() {
return true;
},
getImplementation(ns, name) {
return new LocalAPIImplementation(testingApiObj, name, null);
},
};
let root = {};
Schemas.inject(root, localWrapper);
deepEqual(root.defaultsJson.defaultFoo(), { prop1: 1, newProp: 1 });
deepEqual(root.defaultsJson.defaultFoo({ prop1: 2 }), {
prop1: 2,
newProp: 1,
});
deepEqual(root.defaultsJson.defaultFoo(), { prop1: 1, newProp: 1 });
});
let returnsJson = [
{
namespace: "returns",
types: [
{
id: "Widget",
type: "object",
properties: {
size: { type: "integer" },
colour: { type: "string", optional: true },
},
},
],
functions: [
{
name: "complete",
type: "function",
returns: { $ref: "Widget" },
parameters: [],
},
{
name: "optional",
type: "function",
returns: { $ref: "Widget" },
parameters: [],
},
{
name: "invalid",
type: "function",
returns: { $ref: "Widget" },
parameters: [],
},
],
},
];
add_task(async function testReturns() {
const url = "data:," + JSON.stringify(returnsJson);
Schemas._rootSchema = null;
await Schemas.load(url);
const apiObject = {
complete() {
return { size: 3, colour: "orange" };
},
optional() {
return { size: 4 };
},
invalid() {
return {};
},
};
const localWrapper = {
manifestVersion: 2,
cloneScope: global,
shouldInject() {
return true;
},
getImplementation(ns, name) {
return new LocalAPIImplementation(apiObject, name, null);
},
};
const root = {};
Schemas.inject(root, localWrapper);
deepEqual(root.returns.complete(), { size: 3, colour: "orange" });
deepEqual(
root.returns.optional(),
{ size: 4 },
"Missing optional properties is allowed"
);
if (AppConstants.DEBUG) {
Assert.throws(
() => root.returns.invalid(),
/Type error for result value \(Property "size" is required\)/,
"Should throw for invalid result in DEBUG builds"
);
} else {
deepEqual(
root.returns.invalid(),
{},
"Doesn't throw for invalid result value in release builds"
);
}
});
let booleanEnumJson = [
{
namespace: "booleanEnum",
types: [
{
id: "enumTrue",
type: "boolean",
enum: [true],
},
],
functions: [
{
name: "paramMustBeTrue",
type: "function",
parameters: [{ name: "arg", $ref: "enumTrue" }],
},
],
},
];
add_task(async function testBooleanEnum() {
let wrapper = getContextWrapper();
let url = "data:," + JSON.stringify(booleanEnumJson);
Schemas._rootSchema = null;
await Schemas.load(url);
let root = {};
Schemas.inject(root, wrapper);
ok(root.booleanEnum, "namespace exists");
root.booleanEnum.paramMustBeTrue(true);
wrapper.verify("call", "booleanEnum", "paramMustBeTrue", [true]);
Assert.throws(
() => root.booleanEnum.paramMustBeTrue(false),
/Type error for parameter arg \(Invalid value false\) for booleanEnum\.paramMustBeTrue\./,
"should throw because enum of the type restricts parameter to true"
);
});
let xoriginJson = [
{
namespace: "xorigin",
types: [],
functions: [
{
name: "foo",
type: "function",
parameters: [
{
name: "arg",
type: "any",
},
],
},
{
name: "crossFoo",
type: "function",
allowCrossOriginArguments: true,
parameters: [
{
name: "arg",
type: "any",
},
],
},
],
},
];
add_task(async function testCrossOriginArguments() {
let url = "data:," + JSON.stringify(xoriginJson);
Schemas._rootSchema = null;
await Schemas.load(url);
let sandbox = new Cu.Sandbox("http://test.com");
let testingApiObj = {
foo(arg) {
sandbox.result = JSON.stringify(arg);
},
crossFoo(arg) {
sandbox.xResult = JSON.stringify(arg);
},
};
let localWrapper = {
manifestVersion: 2,
cloneScope: sandbox,
shouldInject() {
return true;
},
getImplementation(ns, name) {
return new LocalAPIImplementation(testingApiObj, name, null);
},
};
let root = {};
Schemas.inject(root, localWrapper);
Assert.throws(
() => root.xorigin.foo({ key: 13 }),
/Permission denied to pass object/
);
equal(sandbox.result, undefined, "Foo can't read cross origin object.");
root.xorigin.crossFoo({ answer: 42 });
equal(sandbox.xResult, '{"answer":42}', "Can read cross origin object.");
});