summaryrefslogtreecommitdiffstats
path: root/toolkit/components/webextensions/test/xpcshell/test_ext_schemas.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/webextensions/test/xpcshell/test_ext_schemas.js')
-rw-r--r--toolkit/components/webextensions/test/xpcshell/test_ext_schemas.js1427
1 files changed, 1427 insertions, 0 deletions
diff --git a/toolkit/components/webextensions/test/xpcshell/test_ext_schemas.js b/toolkit/components/webextensions/test/xpcshell/test_ext_schemas.js
new file mode 100644
index 000000000..d838be5b5
--- /dev/null
+++ b/toolkit/components/webextensions/test/xpcshell/test_ext_schemas.js
@@ -0,0 +1,1427 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/Schemas.jsm");
+Components.utils.import("resource://gre/modules/BrowserUtils.jsm");
+Components.utils.import("resource://gre/modules/ExtensionCommon.jsm");
+
+let {LocalAPIImplementation, SchemaAPIInterface} = ExtensionCommon;
+
+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: "submodule",
+ type: "object",
+ functions: [
+ {
+ name: "sub_foo",
+ type: "function",
+ parameters: [],
+ returns: "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: {
+ url: {type: "string", "format": "url", "optional": true},
+ relativeUrl: {type: "string", "format": "relativeUrl", "optional": true},
+ strictRelativeUrl: {type: "string", "format": "strictRelativeUrl", "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"},
+ ],
+ },
+ ],
+
+ 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"},
+ },
+ },
+];
+
+let tallied = null;
+
+function tally(kind, ns, name, args) {
+ tallied = [kind, ns, name, args];
+}
+
+function verify(...args) {
+ do_check_eq(JSON.stringify(tallied), JSON.stringify(args));
+ tallied = null;
+}
+
+let talliedErrors = [];
+
+function checkErrors(errors) {
+ do_check_eq(talliedErrors.length, errors.length, "Got expected number of errors");
+ for (let [i, error] of errors.entries()) {
+ do_check_true(i in talliedErrors && talliedErrors[i].includes(error),
+ `${JSON.stringify(error)} is a substring of error ${JSON.stringify(talliedErrors[i])}`);
+ }
+
+ talliedErrors.length = 0;
+}
+
+let permissions = new Set();
+
+class TallyingAPIImplementation extends SchemaAPIInterface {
+ constructor(namespace, name) {
+ super();
+ this.namespace = namespace;
+ this.name = name;
+ }
+
+ callFunction(args) {
+ tally("call", this.namespace, this.name, args);
+ }
+
+ callFunctionNoReturn(args) {
+ tally("call", this.namespace, this.name, args);
+ }
+
+ getProperty() {
+ tally("get", this.namespace, this.name);
+ }
+
+ setProperty(value) {
+ tally("set", this.namespace, this.name, value);
+ }
+
+ addListener(listener, args) {
+ tally("addListener", this.namespace, this.name, [listener, args]);
+ }
+
+ removeListener(listener) {
+ tally("removeListener", this.namespace, this.name, [listener]);
+ }
+
+ hasListener(listener) {
+ tally("hasListener", this.namespace, this.name, [listener]);
+ }
+}
+
+let wrapper = {
+ url: "moz-extension://b66e3509-cdb3-44f6-8eb8-c8b39b3a1d27/",
+
+ checkLoadURL(url) {
+ return !url.startsWith("chrome:");
+ },
+
+ preprocessors: {
+ localize(value, context) {
+ return value.replace(/__MSG_(.*?)__/g, (m0, m1) => `${m1.toUpperCase()}`);
+ },
+ },
+
+ logError(message) {
+ talliedErrors.push(message);
+ },
+
+ hasPermission(permission) {
+ return permissions.has(permission);
+ },
+
+ shouldInject(ns) {
+ return ns != "do-not-inject";
+ },
+
+ getImplementation(namespace, name) {
+ return new TallyingAPIImplementation(namespace, name);
+ },
+};
+
+add_task(function* () {
+ let url = "data:," + JSON.stringify(json);
+ yield Schemas.load(url);
+
+ let root = {};
+ tallied = null;
+ Schemas.inject(root, wrapper);
+ do_check_eq(tallied, null);
+
+ do_check_eq(root.testing.PROP1, 20, "simple value property");
+ do_check_eq(root.testing.type1.VALUE1, "value1", "enum type");
+ do_check_eq(root.testing.type1.VALUE2, "value2", "enum type");
+
+ do_check_eq("inject" in root, true, "namespace 'inject' should be injected");
+ do_check_eq("do-not-inject" in root, false, "namespace 'do-not-inject' should not be injected");
+
+ root.testing.foo(11, true);
+ verify("call", "testing", "foo", [11, true]);
+
+ root.testing.foo(true);
+ verify("call", "testing", "foo", [99, true]);
+
+ root.testing.foo(null, true);
+ verify("call", "testing", "foo", [99, true]);
+
+ root.testing.foo(undefined, true);
+ verify("call", "testing", "foo", [99, true]);
+
+ root.testing.foo(11);
+ 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);
+ verify("call", "testing", "bar", [null, true]);
+
+ root.testing.baz({prop1: "hello", prop2: 22});
+ verify("call", "testing", "baz", [{prop1: "hello", prop2: 22}]);
+
+ root.testing.baz({prop1: "hello"});
+ verify("call", "testing", "baz", [{prop1: "hello", prop2: null}]);
+
+ root.testing.baz({prop1: "hello", prop2: null});
+ 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");
+ 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"]});
+ 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);
+ do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["call", "testing", "quora"]));
+ do_check_eq(tallied[3][0], f);
+ tallied = null;
+
+ let g = () => 0;
+ root.testing.quora(g);
+ do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["call", "testing", "quora"]));
+ do_check_eq(tallied[3][0], g);
+ tallied = null;
+
+ root.testing.quileute(10);
+ 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});
+ 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});
+ do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["call", "testing", "quasar"]));
+ do_check_eq(tallied[3][0].func, f);
+ tallied = null;
+
+ root.testing.quosimodo({a: 10, b: 20, c: 30});
+ verify("call", "testing", "quosimodo", [{a: 10, b: 20, c: 30}]);
+ tallied = null;
+
+ 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"});
+ verify("call", "testing", "patternprop", [{prop1: "12", prop2: "42", Prop3: "43", foo1: "x"}]);
+ tallied = null;
+
+ root.testing.patternprop({prop1: "12"});
+ verify("call", "testing", "patternprop", [{prop1: "12"}]);
+ tallied = null;
+
+ 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");
+ verify("call", "testing", "pattern", ["DEADbeef"]);
+ tallied = null;
+
+ Assert.throws(() => root.testing.pattern("DEADcow"),
+ /String "DEADcow" must match \/\^\[0-9a-f\]\+\$\/i/,
+ "should throw for non-match");
+
+ root.testing.format({url: "http://foo/bar",
+ relativeUrl: "http://foo/bar"});
+ verify("call", "testing", "format", [{url: "http://foo/bar",
+ relativeUrl: "http://foo/bar",
+ strictRelativeUrl: null}]);
+ tallied = null;
+
+ root.testing.format({relativeUrl: "foo.html", strictRelativeUrl: "foo.html"});
+ verify("call", "testing", "format", [{url: null,
+ relativeUrl: `${wrapper.url}foo.html`,
+ strictRelativeUrl: `${wrapper.url}foo.html`}]);
+ tallied = 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");
+ }
+
+ 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});
+ 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"}}]}});
+ verify("call", "testing", "deep", [{foo: {bar: [{baz: {required: 12, optional: "42"}}]}}]);
+ tallied = null;
+
+ 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: {required: 12, optional: 42}}]}}),
+ /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");
+
+
+ talliedErrors.length = 0;
+
+ root.testing.errors({warn: "0123", ignore: "0123", default: "0123"});
+ verify("call", "testing", "errors", [{warn: "0123", ignore: "0123", default: "0123"}]);
+ checkErrors([]);
+
+ root.testing.errors({warn: "0123", ignore: "x123", default: "0123"});
+ verify("call", "testing", "errors", [{warn: "0123", ignore: null, default: "0123"}]);
+ checkErrors([]);
+
+ root.testing.errors({warn: "x123", ignore: "0123", default: "0123"});
+ verify("call", "testing", "errors", [{warn: null, ignore: "0123", default: "0123"}]);
+ checkErrors([
+ 'String "x123" must match /^\\d+$/',
+ ]);
+
+
+ root.testing.onFoo.addListener(f);
+ do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["addListener", "testing", "onFoo"]));
+ do_check_eq(tallied[3][0], f);
+ do_check_eq(JSON.stringify(tallied[3][1]), JSON.stringify([]));
+ tallied = null;
+
+ root.testing.onFoo.removeListener(f);
+ do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["removeListener", "testing", "onFoo"]));
+ do_check_eq(tallied[3][0], f);
+ tallied = null;
+
+ root.testing.onFoo.hasListener(f);
+ do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["hasListener", "testing", "onFoo"]));
+ do_check_eq(tallied[3][0], f);
+ tallied = null;
+
+ Assert.throws(() => root.testing.onFoo.addListener(10),
+ /Invalid listener/,
+ "addListener with non-function should throw");
+
+ root.testing.onBar.addListener(f, 10);
+ do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["addListener", "testing", "onBar"]));
+ do_check_eq(tallied[3][0], f);
+ do_check_eq(JSON.stringify(tallied[3][1]), JSON.stringify([10]));
+ tallied = null;
+
+ root.testing.onBar.addListener(f);
+ do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["addListener", "testing", "onBar"]));
+ do_check_eq(tallied[3][0], f);
+ do_check_eq(JSON.stringify(tallied[3][1]), JSON.stringify([1]));
+ 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/__"});
+ verify("call", "testing", "localize", [{foo: "FOO", bar: "__MSG_foo__", url: "http://example.com/"}]);
+ tallied = null;
+
+
+ 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"});
+ verify("call", "testing", "extended1", [{prop1: "foo", prop2: "bar"}]);
+ tallied = null;
+
+ 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");
+ verify("call", "testing", "extended2", ["foo"]);
+ tallied = null;
+
+ root.testing.extended2(12);
+ verify("call", "testing", "extended2", [12]);
+ tallied = null;
+
+ Assert.throws(() => root.testing.extended2(true),
+ /Incorrect argument types/,
+ "should throw for wrong argument type");
+
+ root.testing.prop3.sub_foo();
+ verify("call", "testing.prop3", "sub_foo", []);
+ tallied = null;
+
+ Assert.throws(() => root.testing.prop4.sub_foo(),
+ /root.testing.prop4 is undefined/,
+ "should throw for unsupported submodule");
+
+ root.foreign.foreignRef.sub_foo();
+ verify("call", "foreign.foreignRef", "sub_foo", []);
+ tallied = null;
+});
+
+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(function* testDeprecation() {
+ let url = "data:," + JSON.stringify(deprecatedJson);
+ yield Schemas.load(url);
+
+ let root = {};
+ Schemas.inject(root, wrapper);
+
+ talliedErrors.length = 0;
+
+
+ root.deprecated.property({foo: "bar", xxx: "any", yyy: "property"});
+ verify("call", "deprecated", "property", [{foo: "bar", xxx: "any", yyy: "property"}]);
+ checkErrors([
+ "Error processing xxx: Unknown property",
+ "Error processing yyy: Unknown property",
+ ]);
+
+ root.deprecated.value(12);
+ verify("call", "deprecated", "value", [12]);
+ checkErrors([]);
+
+ root.deprecated.value("12");
+ verify("call", "deprecated", "value", ["12"]);
+ checkErrors(["Please use an integer, not \"12\""]);
+
+ root.deprecated.choices(12);
+ verify("call", "deprecated", "choices", [12]);
+ checkErrors(["You have no choices"]);
+
+ root.deprecated.ref("12");
+ verify("call", "deprecated", "ref", ["12"]);
+ checkErrors(["Deprecated alias"]);
+
+ root.deprecated.method();
+ verify("call", "deprecated", "method", []);
+ checkErrors(["Do not call this method"]);
+
+
+ void root.deprecated.accessor;
+ verify("get", "deprecated", "accessor", null);
+ checkErrors(["This is not the property you are looking for"]);
+
+ root.deprecated.accessor = "x";
+ verify("set", "deprecated", "accessor", "x");
+ checkErrors(["This is not the property you are looking for"]);
+
+
+ root.deprecated.onDeprecated.addListener(() => {});
+ checkErrors(["This event does not work"]);
+
+ root.deprecated.onDeprecated.removeListener(() => {});
+ checkErrors(["This event does not work"]);
+
+ root.deprecated.onDeprecated.hasListener(() => {});
+ checkErrors(["This event does not work"]);
+});
+
+
+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(function* testChoices() {
+ let url = "data:," + JSON.stringify(choicesJson);
+ yield Schemas.load(url);
+
+ let root = {};
+ Schemas.inject(root, wrapper);
+
+ talliedErrors.length = 0;
+
+ Assert.throws(() => root.choices.meh("frog"),
+ /Value 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(function* testPermissions() {
+ let url = "data:," + JSON.stringify(permissionsJson);
+ yield Schemas.load(url);
+
+ 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");
+
+ ok(!("fooPerm" in root.noPerms), "noPerms.fooPerm should not method exist");
+
+ ok(!("fooPerm" in root), "fooPerm namespace should not exist");
+
+
+ do_print('Add "foo" permission');
+ 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");
+
+ ok(!("fooBarPerm" in root.fooPerm), "fooPerm.fooBarPerm method should not exist");
+
+
+ do_print('Add "foo.bar" permission');
+ 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",
+ },
+ ],
+ "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(function* testNestedNamespace() {
+ let url = "data:," + JSON.stringify(nestedNamespaceJson);
+
+ yield Schemas.load(url);
+
+ let root = {};
+ Schemas.inject(root, wrapper);
+
+ talliedErrors.length = 0;
+
+ 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");
+ do_check_eq(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");
+
+ // TODO: test support events and properties in a SubModuleType defined in the schema,
+ // once implemented, e.g.:
+ //
+ // ok(instanceOfCustomType.url,
+ // "Got the expected property defined in the CustomType instance)
+ //
+ // ok(instanceOfCustomType.onEvent &&
+ // instanceOfCustomType.onEvent.addListener &&
+ // typeof instanceOfCustomType.onEvent.addListener == "function",
+ // "Got the expected event defined in the CustomType instance");
+});
+
+add_task(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 = {
+ shouldInject(ns) {
+ return ns == "testing" || ns == "testing.prop3";
+ },
+ getImplementation(ns, name) {
+ do_check_true(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);
+ do_check_eq(countGet2, 0);
+ do_check_eq(countProp3, 0);
+ do_check_eq(countProp3SubFoo, 0);
+
+ do_check_eq(root.testing.PROP1, 20);
+
+ do_check_eq(root.testing.prop2, "prop2 val");
+ do_check_eq(countGet2, 1);
+
+ do_check_eq(root.testing.prop2, "prop2 val");
+ do_check_eq(countGet2, 2);
+
+ do_print(JSON.stringify(root.testing));
+ do_check_eq(root.testing.prop3.sub_foo(), 1);
+ do_check_eq(countProp3, 1);
+ do_check_eq(countProp3SubFoo, 1);
+
+ do_check_eq(root.testing.prop3.sub_foo(), 2);
+ do_check_eq(countProp3, 2);
+ do_check_eq(countProp3SubFoo, 2);
+
+ root.testing.prop3.sub_foo = () => { return "overwritten"; };
+ do_check_eq(root.testing.prop3.sub_foo(), "overwritten");
+
+ root.testing.prop3 = {sub_foo() { return "overwritten again"; }};
+ do_check_eq(root.testing.prop3.sub_foo(), "overwritten again");
+ do_check_eq(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",
+ },
+ },
+ ]},
+];
+
+add_task(function* testDefaults() {
+ let url = "data:," + JSON.stringify(defaultsJson);
+ yield 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 = {
+ 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});
+});