diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/components/extensions/test/xpcshell/test_ext_schemas.js | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/test_ext_schemas.js')
-rw-r--r-- | toolkit/components/extensions/test/xpcshell/test_ext_schemas.js | 2118 |
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."); +}); |