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