forked from mirrors/gecko-dev
And added "TODO bug 1797376" to the hostname format in Schemas.jsm, because the implementation does not match the name. Because of that, this patch introduces the new "canonicalDomain" schema format. Differential Revision: https://phabricator.services.mozilla.com/D160316
2118 lines
51 KiB
JavaScript
2118 lines
51 KiB
JavaScript
"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 [
|
|
"https://example.com",
|
|
"http://example.com",
|
|
"https://foo.bar.栃木.jp",
|
|
]) {
|
|
root.testing.format({ origin: valid });
|
|
}
|
|
|
|
for (let invalid of [
|
|
"https://example.com/testing",
|
|
"file:/foo/bar",
|
|
"file:///foo/bar",
|
|
"",
|
|
" ",
|
|
"https://foo.bar.栃木.jp/",
|
|
"https://user:pass@example.com",
|
|
"https://*.example.com",
|
|
"https://example.com#test",
|
|
"https://example.com?test",
|
|
]) {
|
|
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(ns) {
|
|
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(ns) {
|
|
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(ns) {
|
|
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.");
|
|
});
|