Bug 1733104 - Implement runtime.getFrameId available from content scripts r=robwu
[gecko.git] / toolkit / components / extensions / test / xpcshell / test_ext_schemas.js
blob1aa0146631baf61ef8b7c3559ff4b29641e4ac73
1 "use strict";
3 const global = this;
5 let json = [
6   {
7     namespace: "testing",
9     properties: {
10       PROP1: { value: 20 },
11       prop2: { type: "string" },
12       prop3: {
13         $ref: "submodule",
14       },
15       prop4: {
16         $ref: "submodule",
17         unsupported: true,
18       },
19     },
21     types: [
22       {
23         id: "type1",
24         type: "string",
25         enum: ["value1", "value2", "value3"],
26       },
28       {
29         id: "type2",
30         type: "object",
31         properties: {
32           prop1: { type: "integer" },
33           prop2: { type: "array", items: { $ref: "type1" } },
34         },
35       },
37       {
38         id: "basetype1",
39         type: "object",
40         properties: {
41           prop1: { type: "string" },
42         },
43       },
45       {
46         id: "basetype2",
47         choices: [{ type: "integer" }],
48       },
50       {
51         $extend: "basetype1",
52         properties: {
53           prop2: { type: "string" },
54         },
55       },
57       {
58         $extend: "basetype2",
59         choices: [{ type: "string" }],
60       },
62       {
63         id: "basetype3",
64         type: "object",
65         properties: {
66           baseprop: { type: "string" },
67         },
68       },
70       {
71         id: "derivedtype1",
72         type: "object",
73         $import: "basetype3",
74         properties: {
75           derivedprop: { type: "string" },
76         },
77       },
79       {
80         id: "derivedtype2",
81         type: "object",
82         $import: "basetype3",
83         properties: {
84           derivedprop: { type: "integer" },
85         },
86       },
88       {
89         id: "submodule",
90         type: "object",
91         functions: [
92           {
93             name: "sub_foo",
94             type: "function",
95             parameters: [],
96             returns: { type: "integer" },
97           },
98         ],
99       },
100     ],
102     functions: [
103       {
104         name: "foo",
105         type: "function",
106         parameters: [
107           { name: "arg1", type: "integer", optional: true, default: 99 },
108           { name: "arg2", type: "boolean", optional: true },
109         ],
110       },
112       {
113         name: "bar",
114         type: "function",
115         parameters: [
116           { name: "arg1", type: "integer", optional: true },
117           { name: "arg2", type: "boolean" },
118         ],
119       },
121       {
122         name: "baz",
123         type: "function",
124         parameters: [
125           {
126             name: "arg1",
127             type: "object",
128             properties: {
129               prop1: { type: "string" },
130               prop2: { type: "integer", optional: true },
131               prop3: { type: "integer", unsupported: true },
132             },
133           },
134         ],
135       },
137       {
138         name: "qux",
139         type: "function",
140         parameters: [{ name: "arg1", $ref: "type1" }],
141       },
143       {
144         name: "quack",
145         type: "function",
146         parameters: [{ name: "arg1", $ref: "type2" }],
147       },
149       {
150         name: "quora",
151         type: "function",
152         parameters: [{ name: "arg1", type: "function" }],
153       },
155       {
156         name: "quileute",
157         type: "function",
158         parameters: [
159           { name: "arg1", type: "integer", optional: true },
160           { name: "arg2", type: "integer" },
161         ],
162       },
164       {
165         name: "queets",
166         type: "function",
167         unsupported: true,
168         parameters: [],
169       },
171       {
172         name: "quintuplets",
173         type: "function",
174         parameters: [
175           {
176             name: "obj",
177             type: "object",
178             properties: [],
179             additionalProperties: { type: "integer" },
180           },
181         ],
182       },
184       {
185         name: "quasar",
186         type: "function",
187         parameters: [
188           {
189             name: "abc",
190             type: "object",
191             properties: {
192               func: {
193                 type: "function",
194                 parameters: [{ name: "x", type: "integer" }],
195               },
196             },
197           },
198         ],
199       },
201       {
202         name: "quosimodo",
203         type: "function",
204         parameters: [
205           {
206             name: "xyz",
207             type: "object",
208             additionalProperties: { type: "any" },
209           },
210         ],
211       },
213       {
214         name: "patternprop",
215         type: "function",
216         parameters: [
217           {
218             name: "obj",
219             type: "object",
220             properties: { prop1: { type: "string", pattern: "^\\d+$" } },
221             patternProperties: {
222               "(?i)^prop\\d+$": { type: "string" },
223               "^foo\\d+$": { type: "string" },
224             },
225           },
226         ],
227       },
229       {
230         name: "pattern",
231         type: "function",
232         parameters: [
233           { name: "arg", type: "string", pattern: "(?i)^[0-9a-f]+$" },
234         ],
235       },
237       {
238         name: "format",
239         type: "function",
240         parameters: [
241           {
242             name: "arg",
243             type: "object",
244             properties: {
245               hostname: { type: "string", format: "hostname", optional: true },
246               url: { type: "string", format: "url", optional: true },
247               origin: { type: "string", format: "origin", optional: true },
248               relativeUrl: {
249                 type: "string",
250                 format: "relativeUrl",
251                 optional: true,
252               },
253               strictRelativeUrl: {
254                 type: "string",
255                 format: "strictRelativeUrl",
256                 optional: true,
257               },
258               imageDataOrStrictRelativeUrl: {
259                 type: "string",
260                 format: "imageDataOrStrictRelativeUrl",
261                 optional: true,
262               },
263             },
264           },
265         ],
266       },
268       {
269         name: "formatDate",
270         type: "function",
271         parameters: [
272           {
273             name: "arg",
274             type: "object",
275             properties: {
276               date: { type: "string", format: "date", optional: true },
277             },
278           },
279         ],
280       },
282       {
283         name: "deep",
284         type: "function",
285         parameters: [
286           {
287             name: "arg",
288             type: "object",
289             properties: {
290               foo: {
291                 type: "object",
292                 properties: {
293                   bar: {
294                     type: "array",
295                     items: {
296                       type: "object",
297                       properties: {
298                         baz: {
299                           type: "object",
300                           properties: {
301                             required: { type: "integer" },
302                             optional: { type: "string", optional: true },
303                           },
304                         },
305                       },
306                     },
307                   },
308                 },
309               },
310             },
311           },
312         ],
313       },
315       {
316         name: "errors",
317         type: "function",
318         parameters: [
319           {
320             name: "arg",
321             type: "object",
322             properties: {
323               warn: {
324                 type: "string",
325                 pattern: "^\\d+$",
326                 optional: true,
327                 onError: "warn",
328               },
329               ignore: {
330                 type: "string",
331                 pattern: "^\\d+$",
332                 optional: true,
333                 onError: "ignore",
334               },
335               default: {
336                 type: "string",
337                 pattern: "^\\d+$",
338                 optional: true,
339               },
340             },
341           },
342         ],
343       },
345       {
346         name: "localize",
347         type: "function",
348         parameters: [
349           {
350             name: "arg",
351             type: "object",
352             properties: {
353               foo: { type: "string", preprocess: "localize", optional: true },
354               bar: { type: "string", optional: true },
355               url: {
356                 type: "string",
357                 preprocess: "localize",
358                 format: "url",
359                 optional: true,
360               },
361             },
362           },
363         ],
364       },
366       {
367         name: "extended1",
368         type: "function",
369         parameters: [{ name: "val", $ref: "basetype1" }],
370       },
372       {
373         name: "extended2",
374         type: "function",
375         parameters: [{ name: "val", $ref: "basetype2" }],
376       },
378       {
379         name: "callderived1",
380         type: "function",
381         parameters: [{ name: "value", $ref: "derivedtype1" }],
382       },
384       {
385         name: "callderived2",
386         type: "function",
387         parameters: [{ name: "value", $ref: "derivedtype2" }],
388       },
389     ],
391     events: [
392       {
393         name: "onFoo",
394         type: "function",
395       },
397       {
398         name: "onBar",
399         type: "function",
400         extraParameters: [
401           {
402             name: "filter",
403             type: "integer",
404             optional: true,
405             default: 1,
406           },
407         ],
408       },
409     ],
410   },
411   {
412     namespace: "foreign",
413     properties: {
414       foreignRef: { $ref: "testing.submodule" },
415     },
416   },
417   {
418     namespace: "inject",
419     properties: {
420       PROP1: { value: "should inject" },
421     },
422   },
423   {
424     namespace: "do-not-inject",
425     properties: {
426       PROP1: { value: "should not inject" },
427     },
428   },
431 add_task(async function() {
432   let wrapper = getContextWrapper();
433   let url = "data:," + JSON.stringify(json);
434   Schemas._rootSchema = null;
435   await Schemas.load(url);
437   let root = {};
438   Schemas.inject(root, wrapper);
440   Assert.equal(root.testing.PROP1, 20, "simple value property");
441   Assert.equal(root.testing.type1.VALUE1, "value1", "enum type");
442   Assert.equal(root.testing.type1.VALUE2, "value2", "enum type");
444   Assert.equal("inject" in root, true, "namespace 'inject' should be injected");
445   Assert.equal(
446     root["do-not-inject"],
447     undefined,
448     "namespace 'do-not-inject' should not be injected"
449   );
451   root.testing.foo(11, true);
452   wrapper.verify("call", "testing", "foo", [11, true]);
454   root.testing.foo(true);
455   wrapper.verify("call", "testing", "foo", [99, true]);
457   root.testing.foo(null, true);
458   wrapper.verify("call", "testing", "foo", [99, true]);
460   root.testing.foo(undefined, true);
461   wrapper.verify("call", "testing", "foo", [99, true]);
463   root.testing.foo(11);
464   wrapper.verify("call", "testing", "foo", [11, null]);
466   Assert.throws(
467     () => root.testing.bar(11),
468     /Incorrect argument types/,
469     "should throw without required arg"
470   );
472   Assert.throws(
473     () => root.testing.bar(11, true, 10),
474     /Incorrect argument types/,
475     "should throw with too many arguments"
476   );
478   root.testing.bar(true);
479   wrapper.verify("call", "testing", "bar", [null, true]);
481   root.testing.baz({ prop1: "hello", prop2: 22 });
482   wrapper.verify("call", "testing", "baz", [{ prop1: "hello", prop2: 22 }]);
484   root.testing.baz({ prop1: "hello" });
485   wrapper.verify("call", "testing", "baz", [{ prop1: "hello", prop2: null }]);
487   root.testing.baz({ prop1: "hello", prop2: null });
488   wrapper.verify("call", "testing", "baz", [{ prop1: "hello", prop2: null }]);
490   Assert.throws(
491     () => root.testing.baz({ prop2: 12 }),
492     /Property "prop1" is required/,
493     "should throw without required property"
494   );
496   Assert.throws(
497     () => root.testing.baz({ prop1: "hi", prop3: 12 }),
498     /Property "prop3" is unsupported by Firefox/,
499     "should throw with unsupported property"
500   );
502   Assert.throws(
503     () => root.testing.baz({ prop1: "hi", prop4: 12 }),
504     /Unexpected property "prop4"/,
505     "should throw with unexpected property"
506   );
508   Assert.throws(
509     () => root.testing.baz({ prop1: 12 }),
510     /Expected string instead of 12/,
511     "should throw with wrong type"
512   );
514   root.testing.qux("value2");
515   wrapper.verify("call", "testing", "qux", ["value2"]);
517   Assert.throws(
518     () => root.testing.qux("value4"),
519     /Invalid enumeration value "value4"/,
520     "should throw for invalid enum value"
521   );
523   root.testing.quack({ prop1: 12, prop2: ["value1", "value3"] });
524   wrapper.verify("call", "testing", "quack", [
525     { prop1: 12, prop2: ["value1", "value3"] },
526   ]);
528   Assert.throws(
529     () =>
530       root.testing.quack({ prop1: 12, prop2: ["value1", "value3", "value4"] }),
531     /Invalid enumeration value "value4"/,
532     "should throw for invalid array type"
533   );
535   function f() {}
536   root.testing.quora(f);
537   Assert.equal(
538     JSON.stringify(wrapper.tallied.slice(0, -1)),
539     JSON.stringify(["call", "testing", "quora"])
540   );
541   Assert.equal(wrapper.tallied[3][0], f);
542   wrapper.tallied = null;
544   let g = () => 0;
545   root.testing.quora(g);
546   Assert.equal(
547     JSON.stringify(wrapper.tallied.slice(0, -1)),
548     JSON.stringify(["call", "testing", "quora"])
549   );
550   Assert.equal(wrapper.tallied[3][0], g);
551   wrapper.tallied = null;
553   root.testing.quileute(10);
554   wrapper.verify("call", "testing", "quileute", [null, 10]);
556   Assert.throws(
557     () => root.testing.queets(),
558     /queets is not a function/,
559     "should throw for unsupported functions"
560   );
562   root.testing.quintuplets({ a: 10, b: 20, c: 30 });
563   wrapper.verify("call", "testing", "quintuplets", [{ a: 10, b: 20, c: 30 }]);
565   Assert.throws(
566     () => root.testing.quintuplets({ a: 10, b: 20, c: 30, d: "hi" }),
567     /Expected integer instead of "hi"/,
568     "should throw for wrong additionalProperties type"
569   );
571   root.testing.quasar({ func: f });
572   Assert.equal(
573     JSON.stringify(wrapper.tallied.slice(0, -1)),
574     JSON.stringify(["call", "testing", "quasar"])
575   );
576   Assert.equal(wrapper.tallied[3][0].func, f);
578   root.testing.quosimodo({ a: 10, b: 20, c: 30 });
579   wrapper.verify("call", "testing", "quosimodo", [{ a: 10, b: 20, c: 30 }]);
581   Assert.throws(
582     () => root.testing.quosimodo(10),
583     /Incorrect argument types/,
584     "should throw for wrong type"
585   );
587   root.testing.patternprop({
588     prop1: "12",
589     prop2: "42",
590     Prop3: "43",
591     foo1: "x",
592   });
593   wrapper.verify("call", "testing", "patternprop", [
594     { prop1: "12", prop2: "42", Prop3: "43", foo1: "x" },
595   ]);
597   root.testing.patternprop({ prop1: "12" });
598   wrapper.verify("call", "testing", "patternprop", [{ prop1: "12" }]);
600   Assert.throws(
601     () => root.testing.patternprop({ prop1: "12", foo1: null }),
602     /Expected string instead of null/,
603     "should throw for wrong property type"
604   );
606   Assert.throws(
607     () => root.testing.patternprop({ prop1: "xx", prop2: "yy" }),
608     /String "xx" must match \/\^\\d\+\$\//,
609     "should throw for wrong property type"
610   );
612   Assert.throws(
613     () => root.testing.patternprop({ prop1: "12", prop2: 42 }),
614     /Expected string instead of 42/,
615     "should throw for wrong property type"
616   );
618   Assert.throws(
619     () => root.testing.patternprop({ prop1: "12", prop2: null }),
620     /Expected string instead of null/,
621     "should throw for wrong property type"
622   );
624   Assert.throws(
625     () => root.testing.patternprop({ prop1: "12", propx: "42" }),
626     /Unexpected property "propx"/,
627     "should throw for unexpected property"
628   );
630   Assert.throws(
631     () => root.testing.patternprop({ prop1: "12", Foo1: "x" }),
632     /Unexpected property "Foo1"/,
633     "should throw for unexpected property"
634   );
636   root.testing.pattern("DEADbeef");
637   wrapper.verify("call", "testing", "pattern", ["DEADbeef"]);
639   Assert.throws(
640     () => root.testing.pattern("DEADcow"),
641     /String "DEADcow" must match \/\^\[0-9a-f\]\+\$\/i/,
642     "should throw for non-match"
643   );
645   root.testing.format({ hostname: "foo" });
646   wrapper.verify("call", "testing", "format", [
647     {
648       hostname: "foo",
649       imageDataOrStrictRelativeUrl: null,
650       origin: null,
651       relativeUrl: null,
652       strictRelativeUrl: null,
653       url: null,
654     },
655   ]);
657   for (let invalid of ["", " ", "http://foo", "foo/bar", "foo.com/", "foo?"]) {
658     Assert.throws(
659       () => root.testing.format({ hostname: invalid }),
660       /Invalid hostname/,
661       "should throw for invalid hostname"
662     );
663   }
665   for (let valid of [
666     "https://example.com",
667     "http://example.com",
668     "https://foo.bar.栃木.jp",
669   ]) {
670     root.testing.format({ origin: valid });
671   }
673   for (let invalid of [
674     "https://example.com/testing",
675     "file:/foo/bar",
676     "file:///foo/bar",
677     "",
678     " ",
679     "https://foo.bar.栃木.jp/",
680     "https://user:pass@example.com",
681     "https://*.example.com",
682     "https://example.com#test",
683     "https://example.com?test",
684   ]) {
685     Assert.throws(
686       () => root.testing.format({ origin: invalid }),
687       /Invalid origin/,
688       "should throw for invalid origin"
689     );
690   }
692   root.testing.format({ url: "http://foo/bar", relativeUrl: "http://foo/bar" });
693   wrapper.verify("call", "testing", "format", [
694     {
695       hostname: null,
696       imageDataOrStrictRelativeUrl: null,
697       origin: null,
698       relativeUrl: "http://foo/bar",
699       strictRelativeUrl: null,
700       url: "http://foo/bar",
701     },
702   ]);
704   root.testing.format({
705     relativeUrl: "foo.html",
706     strictRelativeUrl: "foo.html",
707   });
708   wrapper.verify("call", "testing", "format", [
709     {
710       hostname: null,
711       imageDataOrStrictRelativeUrl: null,
712       origin: null,
713       relativeUrl: `${wrapper.url}foo.html`,
714       strictRelativeUrl: `${wrapper.url}foo.html`,
715       url: null,
716     },
717   ]);
719   root.testing.format({
720     imageDataOrStrictRelativeUrl: "data:image/png;base64,A",
721   });
722   wrapper.verify("call", "testing", "format", [
723     {
724       hostname: null,
725       imageDataOrStrictRelativeUrl: "data:image/png;base64,A",
726       origin: null,
727       relativeUrl: null,
728       strictRelativeUrl: null,
729       url: null,
730     },
731   ]);
733   root.testing.format({
734     imageDataOrStrictRelativeUrl: "data:image/jpeg;base64,A",
735   });
736   wrapper.verify("call", "testing", "format", [
737     {
738       hostname: null,
739       imageDataOrStrictRelativeUrl: "data:image/jpeg;base64,A",
740       origin: null,
741       relativeUrl: null,
742       strictRelativeUrl: null,
743       url: null,
744     },
745   ]);
747   root.testing.format({ imageDataOrStrictRelativeUrl: "foo.html" });
748   wrapper.verify("call", "testing", "format", [
749     {
750       hostname: null,
751       imageDataOrStrictRelativeUrl: `${wrapper.url}foo.html`,
752       origin: null,
753       relativeUrl: null,
754       strictRelativeUrl: null,
755       url: null,
756     },
757   ]);
759   for (let format of ["url", "relativeUrl"]) {
760     Assert.throws(
761       () => root.testing.format({ [format]: "chrome://foo/content/" }),
762       /Access denied/,
763       "should throw for access denied"
764     );
765   }
767   for (let urlString of ["//foo.html", "http://foo/bar.html"]) {
768     Assert.throws(
769       () => root.testing.format({ strictRelativeUrl: urlString }),
770       /must be a relative URL/,
771       "should throw for non-relative URL"
772     );
773   }
775   Assert.throws(
776     () =>
777       root.testing.format({
778         imageDataOrStrictRelativeUrl: "data:image/svg+xml;utf8,A",
779       }),
780     /must be a relative or PNG or JPG data:image URL/,
781     "should throw for non-relative or non PNG/JPG data URL"
782   );
784   const dates = [
785     "2016-03-04",
786     "2016-03-04T08:00:00Z",
787     "2016-03-04T08:00:00.000Z",
788     "2016-03-04T08:00:00-08:00",
789     "2016-03-04T08:00:00.000-08:00",
790     "2016-03-04T08:00:00+08:00",
791     "2016-03-04T08:00:00.000+08:00",
792     "2016-03-04T08:00:00+0800",
793     "2016-03-04T08:00:00-0800",
794   ];
795   dates.forEach(str => {
796     root.testing.formatDate({ date: str });
797     wrapper.verify("call", "testing", "formatDate", [{ date: str }]);
798   });
800   // Make sure that a trivial change to a valid date invalidates it.
801   dates.forEach(str => {
802     Assert.throws(
803       () => root.testing.formatDate({ date: "0" + str }),
804       /Invalid date string/,
805       "should throw for invalid iso date string"
806     );
807     Assert.throws(
808       () => root.testing.formatDate({ date: str + "0" }),
809       /Invalid date string/,
810       "should throw for invalid iso date string"
811     );
812   });
814   const badDates = [
815     "I do not look anything like a date string",
816     "2016-99-99",
817     "2016-03-04T25:00:00Z",
818   ];
819   badDates.forEach(str => {
820     Assert.throws(
821       () => root.testing.formatDate({ date: str }),
822       /Invalid date string/,
823       "should throw for invalid iso date string"
824     );
825   });
827   root.testing.deep({
828     foo: { bar: [{ baz: { required: 12, optional: "42" } }] },
829   });
830   wrapper.verify("call", "testing", "deep", [
831     { foo: { bar: [{ baz: { optional: "42", required: 12 } }] } },
832   ]);
834   Assert.throws(
835     () => root.testing.deep({ foo: { bar: [{ baz: { optional: "42" } }] } }),
836     /Type error for parameter arg \(Error processing foo\.bar\.0\.baz: Property "required" is required\) for testing\.deep/,
837     "should throw with the correct object path"
838   );
840   Assert.throws(
841     () =>
842       root.testing.deep({
843         foo: { bar: [{ baz: { optional: 42, required: 12 } }] },
844       }),
845     /Type error for parameter arg \(Error processing foo\.bar\.0\.baz\.optional: Expected string instead of 42\) for testing\.deep/,
846     "should throw with the correct object path"
847   );
849   wrapper.talliedErrors.length = 0;
851   root.testing.errors({ default: "0123", ignore: "0123", warn: "0123" });
852   wrapper.verify("call", "testing", "errors", [
853     { default: "0123", ignore: "0123", warn: "0123" },
854   ]);
855   wrapper.checkErrors([]);
857   root.testing.errors({ default: "0123", ignore: "x123", warn: "0123" });
858   wrapper.verify("call", "testing", "errors", [
859     { default: "0123", ignore: null, warn: "0123" },
860   ]);
861   wrapper.checkErrors([]);
863   ExtensionTestUtils.failOnSchemaWarnings(false);
864   root.testing.errors({ default: "0123", ignore: "0123", warn: "x123" });
865   ExtensionTestUtils.failOnSchemaWarnings(true);
866   wrapper.verify("call", "testing", "errors", [
867     { default: "0123", ignore: "0123", warn: null },
868   ]);
869   wrapper.checkErrors(['String "x123" must match /^\\d+$/']);
871   root.testing.onFoo.addListener(f);
872   Assert.equal(
873     JSON.stringify(wrapper.tallied.slice(0, -1)),
874     JSON.stringify(["addListener", "testing", "onFoo"])
875   );
876   Assert.equal(wrapper.tallied[3][0], f);
877   Assert.equal(JSON.stringify(wrapper.tallied[3][1]), JSON.stringify([]));
878   wrapper.tallied = null;
880   root.testing.onFoo.removeListener(f);
881   Assert.equal(
882     JSON.stringify(wrapper.tallied.slice(0, -1)),
883     JSON.stringify(["removeListener", "testing", "onFoo"])
884   );
885   Assert.equal(wrapper.tallied[3][0], f);
886   wrapper.tallied = null;
888   root.testing.onFoo.hasListener(f);
889   Assert.equal(
890     JSON.stringify(wrapper.tallied.slice(0, -1)),
891     JSON.stringify(["hasListener", "testing", "onFoo"])
892   );
893   Assert.equal(wrapper.tallied[3][0], f);
894   wrapper.tallied = null;
896   Assert.throws(
897     () => root.testing.onFoo.addListener(10),
898     /Invalid listener/,
899     "addListener with non-function should throw"
900   );
902   root.testing.onBar.addListener(f, 10);
903   Assert.equal(
904     JSON.stringify(wrapper.tallied.slice(0, -1)),
905     JSON.stringify(["addListener", "testing", "onBar"])
906   );
907   Assert.equal(wrapper.tallied[3][0], f);
908   Assert.equal(JSON.stringify(wrapper.tallied[3][1]), JSON.stringify([10]));
909   wrapper.tallied = null;
911   root.testing.onBar.addListener(f);
912   Assert.equal(
913     JSON.stringify(wrapper.tallied.slice(0, -1)),
914     JSON.stringify(["addListener", "testing", "onBar"])
915   );
916   Assert.equal(wrapper.tallied[3][0], f);
917   Assert.equal(JSON.stringify(wrapper.tallied[3][1]), JSON.stringify([1]));
918   wrapper.tallied = null;
920   Assert.throws(
921     () => root.testing.onBar.addListener(f, "hi"),
922     /Incorrect argument types/,
923     "addListener with wrong extra parameter should throw"
924   );
926   let target = { prop1: 12, prop2: ["value1", "value3"] };
927   let proxy = new Proxy(target, {});
928   Assert.throws(
929     () => root.testing.quack(proxy),
930     /Expected a plain JavaScript object, got a Proxy/,
931     "should throw when passing a Proxy"
932   );
934   if (Symbol.toStringTag) {
935     let stringTarget = { prop1: 12, prop2: ["value1", "value3"] };
936     stringTarget[Symbol.toStringTag] = () => "[object Object]";
937     let stringProxy = new Proxy(stringTarget, {});
938     Assert.throws(
939       () => root.testing.quack(stringProxy),
940       /Expected a plain JavaScript object, got a Proxy/,
941       "should throw when passing a Proxy"
942     );
943   }
945   root.testing.localize({
946     foo: "__MSG_foo__",
947     bar: "__MSG_foo__",
948     url: "__MSG_http://example.com/__",
949   });
950   wrapper.verify("call", "testing", "localize", [
951     { bar: "__MSG_foo__", foo: "FOO", url: "http://example.com/" },
952   ]);
954   Assert.throws(
955     () => root.testing.localize({ url: "__MSG_/foo/bar__" }),
956     /\/FOO\/BAR is not a valid URL\./,
957     "should throw for invalid URL"
958   );
960   root.testing.extended1({ prop1: "foo", prop2: "bar" });
961   wrapper.verify("call", "testing", "extended1", [
962     { prop1: "foo", prop2: "bar" },
963   ]);
965   Assert.throws(
966     () => root.testing.extended1({ prop1: "foo", prop2: 12 }),
967     /Expected string instead of 12/,
968     "should throw for wrong property type"
969   );
971   Assert.throws(
972     () => root.testing.extended1({ prop1: "foo" }),
973     /Property "prop2" is required/,
974     "should throw for missing property"
975   );
977   Assert.throws(
978     () => root.testing.extended1({ prop1: "foo", prop2: "bar", prop3: "xxx" }),
979     /Unexpected property "prop3"/,
980     "should throw for extra property"
981   );
983   root.testing.extended2("foo");
984   wrapper.verify("call", "testing", "extended2", ["foo"]);
986   root.testing.extended2(12);
987   wrapper.verify("call", "testing", "extended2", [12]);
989   Assert.throws(
990     () => root.testing.extended2(true),
991     /Incorrect argument types/,
992     "should throw for wrong argument type"
993   );
995   root.testing.prop3.sub_foo();
996   wrapper.verify("call", "testing.prop3", "sub_foo", []);
998   Assert.throws(
999     () => root.testing.prop4.sub_foo(),
1000     /root.testing.prop4 is undefined/,
1001     "should throw for unsupported submodule"
1002   );
1004   root.foreign.foreignRef.sub_foo();
1005   wrapper.verify("call", "foreign.foreignRef", "sub_foo", []);
1007   root.testing.callderived1({ baseprop: "s1", derivedprop: "s2" });
1008   wrapper.verify("call", "testing", "callderived1", [
1009     { baseprop: "s1", derivedprop: "s2" },
1010   ]);
1012   Assert.throws(
1013     () => root.testing.callderived1({ baseprop: "s1", derivedprop: 42 }),
1014     /Error processing derivedprop: Expected string/,
1015     "Two different objects may $import the same base object"
1016   );
1017   Assert.throws(
1018     () => root.testing.callderived1({ baseprop: "s1" }),
1019     /Property "derivedprop" is required/,
1020     "Object using $import has its local properites"
1021   );
1022   Assert.throws(
1023     () => root.testing.callderived1({ derivedprop: "s2" }),
1024     /Property "baseprop" is required/,
1025     "Object using $import has imported properites"
1026   );
1028   root.testing.callderived2({ baseprop: "s1", derivedprop: 42 });
1029   wrapper.verify("call", "testing", "callderived2", [
1030     { baseprop: "s1", derivedprop: 42 },
1031   ]);
1033   Assert.throws(
1034     () => root.testing.callderived2({ baseprop: "s1", derivedprop: "s2" }),
1035     /Error processing derivedprop: Expected integer/,
1036     "Two different objects may $import the same base object"
1037   );
1038   Assert.throws(
1039     () => root.testing.callderived2({ baseprop: "s1" }),
1040     /Property "derivedprop" is required/,
1041     "Object using $import has its local properites"
1042   );
1043   Assert.throws(
1044     () => root.testing.callderived2({ derivedprop: 42 }),
1045     /Property "baseprop" is required/,
1046     "Object using $import has imported properites"
1047   );
1050 let deprecatedJson = [
1051   {
1052     namespace: "deprecated",
1054     properties: {
1055       accessor: {
1056         type: "string",
1057         writable: true,
1058         deprecated: "This is not the property you are looking for",
1059       },
1060     },
1062     types: [
1063       {
1064         id: "Type",
1065         type: "string",
1066       },
1067     ],
1069     functions: [
1070       {
1071         name: "property",
1072         type: "function",
1073         parameters: [
1074           {
1075             name: "arg",
1076             type: "object",
1077             properties: {
1078               foo: {
1079                 type: "string",
1080               },
1081             },
1082             additionalProperties: {
1083               type: "any",
1084               deprecated: "Unknown property",
1085             },
1086           },
1087         ],
1088       },
1090       {
1091         name: "value",
1092         type: "function",
1093         parameters: [
1094           {
1095             name: "arg",
1096             choices: [
1097               {
1098                 type: "integer",
1099               },
1100               {
1101                 type: "string",
1102                 deprecated: "Please use an integer, not ${value}",
1103               },
1104             ],
1105           },
1106         ],
1107       },
1109       {
1110         name: "choices",
1111         type: "function",
1112         parameters: [
1113           {
1114             name: "arg",
1115             deprecated: "You have no choices",
1116             choices: [
1117               {
1118                 type: "integer",
1119               },
1120             ],
1121           },
1122         ],
1123       },
1125       {
1126         name: "ref",
1127         type: "function",
1128         parameters: [
1129           {
1130             name: "arg",
1131             choices: [
1132               {
1133                 $ref: "Type",
1134                 deprecated: "Deprecated alias",
1135               },
1136             ],
1137           },
1138         ],
1139       },
1141       {
1142         name: "method",
1143         type: "function",
1144         deprecated: "Do not call this method",
1145         parameters: [],
1146       },
1147     ],
1149     events: [
1150       {
1151         name: "onDeprecated",
1152         type: "function",
1153         deprecated: "This event does not work",
1154       },
1155     ],
1156   },
1159 add_task(async function testDeprecation() {
1160   let wrapper = getContextWrapper();
1161   // This whole test expects deprecation warnings.
1162   ExtensionTestUtils.failOnSchemaWarnings(false);
1164   let url = "data:," + JSON.stringify(deprecatedJson);
1165   Schemas._rootSchema = null;
1166   await Schemas.load(url);
1168   let root = {};
1169   Schemas.inject(root, wrapper);
1171   root.deprecated.property({ foo: "bar", xxx: "any", yyy: "property" });
1172   wrapper.verify("call", "deprecated", "property", [
1173     { foo: "bar", xxx: "any", yyy: "property" },
1174   ]);
1175   wrapper.checkErrors([
1176     "Warning processing xxx: Unknown property",
1177     "Warning processing yyy: Unknown property",
1178   ]);
1180   root.deprecated.value(12);
1181   wrapper.verify("call", "deprecated", "value", [12]);
1182   wrapper.checkErrors([]);
1184   root.deprecated.value("12");
1185   wrapper.verify("call", "deprecated", "value", ["12"]);
1186   wrapper.checkErrors(['Please use an integer, not "12"']);
1188   root.deprecated.choices(12);
1189   wrapper.verify("call", "deprecated", "choices", [12]);
1190   wrapper.checkErrors(["You have no choices"]);
1192   root.deprecated.ref("12");
1193   wrapper.verify("call", "deprecated", "ref", ["12"]);
1194   wrapper.checkErrors(["Deprecated alias"]);
1196   root.deprecated.method();
1197   wrapper.verify("call", "deprecated", "method", []);
1198   wrapper.checkErrors(["Do not call this method"]);
1200   void root.deprecated.accessor;
1201   wrapper.verify("get", "deprecated", "accessor", null);
1202   wrapper.checkErrors(["This is not the property you are looking for"]);
1204   root.deprecated.accessor = "x";
1205   wrapper.verify("set", "deprecated", "accessor", "x");
1206   wrapper.checkErrors(["This is not the property you are looking for"]);
1208   root.deprecated.onDeprecated.addListener(() => {});
1209   wrapper.checkErrors(["This event does not work"]);
1211   root.deprecated.onDeprecated.removeListener(() => {});
1212   wrapper.checkErrors(["This event does not work"]);
1214   root.deprecated.onDeprecated.hasListener(() => {});
1215   wrapper.checkErrors(["This event does not work"]);
1217   ExtensionTestUtils.failOnSchemaWarnings(true);
1219   Assert.throws(
1220     () => root.deprecated.onDeprecated.hasListener(() => {}),
1221     /This event does not work/,
1222     "Deprecation warning with extensions.webextensions.warnings-as-errors=true"
1223   );
1226 let choicesJson = [
1227   {
1228     namespace: "choices",
1230     types: [],
1232     functions: [
1233       {
1234         name: "meh",
1235         type: "function",
1236         parameters: [
1237           {
1238             name: "arg",
1239             choices: [
1240               {
1241                 type: "string",
1242                 enum: ["foo", "bar", "baz"],
1243               },
1244               {
1245                 type: "string",
1246                 pattern: "florg.*meh",
1247               },
1248               {
1249                 type: "integer",
1250                 minimum: 12,
1251                 maximum: 42,
1252               },
1253             ],
1254           },
1255         ],
1256       },
1258       {
1259         name: "foo",
1260         type: "function",
1261         parameters: [
1262           {
1263             name: "arg",
1264             choices: [
1265               {
1266                 type: "object",
1267                 properties: {
1268                   blurg: {
1269                     type: "string",
1270                     unsupported: true,
1271                     optional: true,
1272                   },
1273                 },
1274                 additionalProperties: {
1275                   type: "string",
1276                 },
1277               },
1278               {
1279                 type: "string",
1280               },
1281               {
1282                 type: "array",
1283                 minItems: 2,
1284                 maxItems: 3,
1285                 items: {
1286                   type: "integer",
1287                 },
1288               },
1289             ],
1290           },
1291         ],
1292       },
1294       {
1295         name: "bar",
1296         type: "function",
1297         parameters: [
1298           {
1299             name: "arg",
1300             choices: [
1301               {
1302                 type: "object",
1303                 properties: {
1304                   baz: {
1305                     type: "string",
1306                   },
1307                 },
1308               },
1309               {
1310                 type: "array",
1311                 items: {
1312                   type: "integer",
1313                 },
1314               },
1315             ],
1316           },
1317         ],
1318       },
1319     ],
1320   },
1323 add_task(async function testChoices() {
1324   let wrapper = getContextWrapper();
1325   let url = "data:," + JSON.stringify(choicesJson);
1326   Schemas._rootSchema = null;
1327   await Schemas.load(url);
1329   let root = {};
1330   Schemas.inject(root, wrapper);
1332   Assert.throws(
1333     () => root.choices.meh("frog"),
1334     /Value "frog" must either: be one of \["foo", "bar", "baz"\], match the pattern \/florg\.\*meh\/, or be an integer value/
1335   );
1337   Assert.throws(
1338     () => root.choices.meh(4),
1339     /be a string value, or be at least 12/
1340   );
1342   Assert.throws(
1343     () => root.choices.meh(43),
1344     /be a string value, or be no greater than 42/
1345   );
1347   Assert.throws(
1348     () => root.choices.foo([]),
1349     /be an object value, be a string value, or have at least 2 items/
1350   );
1352   Assert.throws(
1353     () => root.choices.foo([1, 2, 3, 4]),
1354     /be an object value, be a string value, or have at most 3 items/
1355   );
1357   Assert.throws(
1358     () => root.choices.foo({ foo: 12 }),
1359     /.foo must be a string value, be a string value, or be an array value/
1360   );
1362   Assert.throws(
1363     () => root.choices.foo({ blurg: "foo" }),
1364     /not contain an unsupported "blurg" property, be a string value, or be an array value/
1365   );
1367   Assert.throws(
1368     () => root.choices.bar({}),
1369     /contain the required "baz" property, or be an array value/
1370   );
1372   Assert.throws(
1373     () => root.choices.bar({ baz: "x", quux: "y" }),
1374     /not contain an unexpected "quux" property, or be an array value/
1375   );
1377   Assert.throws(
1378     () => root.choices.bar({ baz: "x", quux: "y", foo: "z" }),
1379     /not contain the unexpected properties \[foo, quux\], or be an array value/
1380   );
1383 let permissionsJson = [
1384   {
1385     namespace: "noPerms",
1387     types: [],
1389     functions: [
1390       {
1391         name: "noPerms",
1392         type: "function",
1393         parameters: [],
1394       },
1396       {
1397         name: "fooPerm",
1398         type: "function",
1399         permissions: ["foo"],
1400         parameters: [],
1401       },
1402     ],
1403   },
1405   {
1406     namespace: "fooPerm",
1408     permissions: ["foo"],
1410     types: [],
1412     functions: [
1413       {
1414         name: "noPerms",
1415         type: "function",
1416         parameters: [],
1417       },
1419       {
1420         name: "fooBarPerm",
1421         type: "function",
1422         permissions: ["foo.bar"],
1423         parameters: [],
1424       },
1425     ],
1426   },
1429 add_task(async function testPermissions() {
1430   let url = "data:," + JSON.stringify(permissionsJson);
1431   Schemas._rootSchema = null;
1432   await Schemas.load(url);
1434   let wrapper = getContextWrapper();
1436   let root = {};
1437   Schemas.inject(root, wrapper);
1439   equal(typeof root.noPerms, "object", "noPerms namespace should exist");
1440   equal(
1441     typeof root.noPerms.noPerms,
1442     "function",
1443     "noPerms.noPerms method should exist"
1444   );
1446   equal(
1447     root.noPerms.fooPerm,
1448     undefined,
1449     "noPerms.fooPerm should not method exist"
1450   );
1452   equal(root.fooPerm, undefined, "fooPerm namespace should not exist");
1454   info('Add "foo" permission');
1455   wrapper.permissions.add("foo");
1457   root = {};
1458   Schemas.inject(root, wrapper);
1460   equal(typeof root.noPerms, "object", "noPerms namespace should exist");
1461   equal(
1462     typeof root.noPerms.noPerms,
1463     "function",
1464     "noPerms.noPerms method should exist"
1465   );
1466   equal(
1467     typeof root.noPerms.fooPerm,
1468     "function",
1469     "noPerms.fooPerm method should exist"
1470   );
1472   equal(typeof root.fooPerm, "object", "fooPerm namespace should exist");
1473   equal(
1474     typeof root.fooPerm.noPerms,
1475     "function",
1476     "noPerms.noPerms method should exist"
1477   );
1479   equal(
1480     root.fooPerm.fooBarPerm,
1481     undefined,
1482     "fooPerm.fooBarPerm method should not exist"
1483   );
1485   info('Add "foo.bar" permission');
1486   wrapper.permissions.add("foo.bar");
1488   root = {};
1489   Schemas.inject(root, wrapper);
1491   equal(typeof root.noPerms, "object", "noPerms namespace should exist");
1492   equal(
1493     typeof root.noPerms.noPerms,
1494     "function",
1495     "noPerms.noPerms method should exist"
1496   );
1497   equal(
1498     typeof root.noPerms.fooPerm,
1499     "function",
1500     "noPerms.fooPerm method should exist"
1501   );
1503   equal(typeof root.fooPerm, "object", "fooPerm namespace should exist");
1504   equal(
1505     typeof root.fooPerm.noPerms,
1506     "function",
1507     "noPerms.noPerms method should exist"
1508   );
1509   equal(
1510     typeof root.fooPerm.fooBarPerm,
1511     "function",
1512     "noPerms.fooBarPerm method should exist"
1513   );
1516 let nestedNamespaceJson = [
1517   {
1518     namespace: "nested.namespace",
1519     types: [
1520       {
1521         id: "CustomType",
1522         type: "object",
1523         events: [
1524           {
1525             name: "onEvent",
1526             type: "function",
1527           },
1528         ],
1529         properties: {
1530           url: {
1531             type: "string",
1532           },
1533         },
1534         functions: [
1535           {
1536             name: "functionOnCustomType",
1537             type: "function",
1538             parameters: [
1539               {
1540                 name: "title",
1541                 type: "string",
1542               },
1543             ],
1544           },
1545         ],
1546       },
1547     ],
1548     properties: {
1549       instanceOfCustomType: {
1550         $ref: "CustomType",
1551       },
1552     },
1553     functions: [
1554       {
1555         name: "create",
1556         type: "function",
1557         parameters: [
1558           {
1559             name: "title",
1560             type: "string",
1561           },
1562         ],
1563       },
1564     ],
1565   },
1568 add_task(async function testNestedNamespace() {
1569   let url = "data:," + JSON.stringify(nestedNamespaceJson);
1570   let wrapper = getContextWrapper();
1572   Schemas._rootSchema = null;
1573   await Schemas.load(url);
1575   let root = {};
1576   Schemas.inject(root, wrapper);
1578   ok(root.nested, "The root object contains the first namespace level");
1579   ok(
1580     root.nested.namespace,
1581     "The first level object contains the second namespace level"
1582   );
1584   ok(
1585     root.nested.namespace.create,
1586     "Got the expected function in the nested namespace"
1587   );
1588   equal(
1589     typeof root.nested.namespace.create,
1590     "function",
1591     "The property is a function as expected"
1592   );
1594   let { instanceOfCustomType } = root.nested.namespace;
1596   ok(
1597     instanceOfCustomType,
1598     "Got the expected instance of the CustomType defined in the schema"
1599   );
1600   ok(
1601     instanceOfCustomType.functionOnCustomType,
1602     "Got the expected method in the CustomType instance"
1603   );
1604   ok(
1605     instanceOfCustomType.onEvent &&
1606       instanceOfCustomType.onEvent.addListener &&
1607       typeof instanceOfCustomType.onEvent.addListener == "function",
1608     "Got the expected event defined in the CustomType instance"
1609   );
1611   instanceOfCustomType.functionOnCustomType("param_value");
1612   wrapper.verify(
1613     "call",
1614     "nested.namespace.instanceOfCustomType",
1615     "functionOnCustomType",
1616     ["param_value"]
1617   );
1619   let fakeListener = () => {};
1620   instanceOfCustomType.onEvent.addListener(fakeListener);
1621   wrapper.verify(
1622     "addListener",
1623     "nested.namespace.instanceOfCustomType",
1624     "onEvent",
1625     [fakeListener, []]
1626   );
1627   instanceOfCustomType.onEvent.removeListener(fakeListener);
1628   wrapper.verify(
1629     "removeListener",
1630     "nested.namespace.instanceOfCustomType",
1631     "onEvent",
1632     [fakeListener]
1633   );
1635   // TODO: test support properties in a SubModuleType defined in the schema,
1636   // once implemented, e.g.:
1637   // ok("url" in instanceOfCustomType,
1638   //   "Got the expected property defined in the CustomType instance");
1641 let $importJson = [
1642   {
1643     namespace: "from_the",
1644     $import: "future",
1645   },
1646   {
1647     namespace: "future",
1648     properties: {
1649       PROP1: { value: "original value" },
1650       PROP2: { value: "second original" },
1651     },
1652     types: [
1653       {
1654         id: "Colour",
1655         type: "string",
1656         enum: ["red", "white", "blue"],
1657       },
1658     ],
1659     functions: [
1660       {
1661         name: "dye",
1662         type: "function",
1663         parameters: [{ name: "arg", $ref: "Colour" }],
1664       },
1665     ],
1666   },
1667   {
1668     namespace: "embrace",
1669     $import: "future",
1670     properties: {
1671       PROP2: { value: "overridden value" },
1672     },
1673     types: [
1674       {
1675         id: "Colour",
1676         type: "string",
1677         enum: ["blue", "orange"],
1678       },
1679     ],
1680   },
1683 add_task(async function test_$import() {
1684   let wrapper = getContextWrapper();
1685   let url = "data:," + JSON.stringify($importJson);
1686   Schemas._rootSchema = null;
1687   await Schemas.load(url);
1689   let root = {};
1690   Schemas.inject(root, wrapper);
1692   equal(root.from_the.PROP1, "original value", "imported property");
1693   equal(root.from_the.PROP2, "second original", "second imported property");
1694   equal(root.from_the.Colour.RED, "red", "imported enum type");
1695   equal(typeof root.from_the.dye, "function", "imported function");
1697   root.from_the.dye("white");
1698   wrapper.verify("call", "from_the", "dye", ["white"]);
1700   Assert.throws(
1701     () => root.from_the.dye("orange"),
1702     /Invalid enumeration value/,
1703     "original imported argument type Colour doesn't include 'orange'"
1704   );
1706   equal(root.embrace.PROP1, "original value", "imported property");
1707   equal(root.embrace.PROP2, "overridden value", "overridden property");
1708   equal(root.embrace.Colour.ORANGE, "orange", "overridden enum type");
1709   equal(typeof root.embrace.dye, "function", "imported function");
1711   root.embrace.dye("orange");
1712   wrapper.verify("call", "embrace", "dye", ["orange"]);
1714   Assert.throws(
1715     () => root.embrace.dye("white"),
1716     /Invalid enumeration value/,
1717     "overridden argument type Colour doesn't include 'white'"
1718   );
1721 add_task(async function testLocalAPIImplementation() {
1722   let countGet2 = 0;
1723   let countProp3 = 0;
1724   let countProp3SubFoo = 0;
1726   let testingApiObj = {
1727     get PROP1() {
1728       // PROP1 is a schema-defined constant.
1729       throw new Error("Unexpected get PROP1");
1730     },
1731     get prop2() {
1732       ++countGet2;
1733       return "prop2 val";
1734     },
1735     get prop3() {
1736       throw new Error("Unexpected get prop3");
1737     },
1738     set prop3(v) {
1739       // prop3 is a submodule, defined as a function, so the API should not pass
1740       // through assignment to prop3.
1741       throw new Error("Unexpected set prop3");
1742     },
1743   };
1744   let submoduleApiObj = {
1745     get sub_foo() {
1746       ++countProp3;
1747       return () => {
1748         return ++countProp3SubFoo;
1749       };
1750     },
1751   };
1753   let localWrapper = {
1754     manifestVersion: 2,
1755     cloneScope: global,
1756     shouldInject(ns, name) {
1757       return name == "testing" || ns == "testing" || ns == "testing.prop3";
1758     },
1759     getImplementation(ns, name) {
1760       Assert.ok(ns == "testing" || ns == "testing.prop3");
1761       if (ns == "testing.prop3" && name == "sub_foo") {
1762         // It is fine to use `null` here because we don't call async functions.
1763         return new LocalAPIImplementation(submoduleApiObj, name, null);
1764       }
1765       // It is fine to use `null` here because we don't call async functions.
1766       return new LocalAPIImplementation(testingApiObj, name, null);
1767     },
1768   };
1770   let root = {};
1771   Schemas.inject(root, localWrapper);
1772   Assert.equal(countGet2, 0);
1773   Assert.equal(countProp3, 0);
1774   Assert.equal(countProp3SubFoo, 0);
1776   Assert.equal(root.testing.PROP1, 20);
1778   Assert.equal(root.testing.prop2, "prop2 val");
1779   Assert.equal(countGet2, 1);
1781   Assert.equal(root.testing.prop2, "prop2 val");
1782   Assert.equal(countGet2, 2);
1784   info(JSON.stringify(root.testing));
1785   Assert.equal(root.testing.prop3.sub_foo(), 1);
1786   Assert.equal(countProp3, 1);
1787   Assert.equal(countProp3SubFoo, 1);
1789   Assert.equal(root.testing.prop3.sub_foo(), 2);
1790   Assert.equal(countProp3, 2);
1791   Assert.equal(countProp3SubFoo, 2);
1793   root.testing.prop3.sub_foo = () => {
1794     return "overwritten";
1795   };
1796   Assert.equal(root.testing.prop3.sub_foo(), "overwritten");
1798   root.testing.prop3 = {
1799     sub_foo() {
1800       return "overwritten again";
1801     },
1802   };
1803   Assert.equal(root.testing.prop3.sub_foo(), "overwritten again");
1804   Assert.equal(countProp3SubFoo, 2);
1807 let defaultsJson = [
1808   {
1809     namespace: "defaultsJson",
1811     types: [],
1813     functions: [
1814       {
1815         name: "defaultFoo",
1816         type: "function",
1817         parameters: [
1818           {
1819             name: "arg",
1820             type: "object",
1821             optional: true,
1822             properties: {
1823               prop1: { type: "integer", optional: true },
1824             },
1825             default: { prop1: 1 },
1826           },
1827         ],
1828         returns: {
1829           type: "object",
1830           additionalProperties: true,
1831         },
1832       },
1833     ],
1834   },
1837 add_task(async function testDefaults() {
1838   let url = "data:," + JSON.stringify(defaultsJson);
1839   Schemas._rootSchema = null;
1840   await Schemas.load(url);
1842   let testingApiObj = {
1843     defaultFoo: function(arg) {
1844       if (Object.keys(arg) != "prop1") {
1845         throw new Error(
1846           `Received the expected default object, default: ${JSON.stringify(
1847             arg
1848           )}`
1849         );
1850       }
1851       arg.newProp = 1;
1852       return arg;
1853     },
1854   };
1856   let localWrapper = {
1857     manifestVersion: 2,
1858     cloneScope: global,
1859     shouldInject(ns) {
1860       return true;
1861     },
1862     getImplementation(ns, name) {
1863       return new LocalAPIImplementation(testingApiObj, name, null);
1864     },
1865   };
1867   let root = {};
1868   Schemas.inject(root, localWrapper);
1870   deepEqual(root.defaultsJson.defaultFoo(), { prop1: 1, newProp: 1 });
1871   deepEqual(root.defaultsJson.defaultFoo({ prop1: 2 }), {
1872     prop1: 2,
1873     newProp: 1,
1874   });
1875   deepEqual(root.defaultsJson.defaultFoo(), { prop1: 1, newProp: 1 });
1878 let returnsJson = [
1879   {
1880     namespace: "returns",
1881     types: [
1882       {
1883         id: "Widget",
1884         type: "object",
1885         properties: {
1886           size: { type: "integer" },
1887           colour: { type: "string", optional: true },
1888         },
1889       },
1890     ],
1891     functions: [
1892       {
1893         name: "complete",
1894         type: "function",
1895         returns: { $ref: "Widget" },
1896         parameters: [],
1897       },
1898       {
1899         name: "optional",
1900         type: "function",
1901         returns: { $ref: "Widget" },
1902         parameters: [],
1903       },
1904       {
1905         name: "invalid",
1906         type: "function",
1907         returns: { $ref: "Widget" },
1908         parameters: [],
1909       },
1910     ],
1911   },
1914 add_task(async function testReturns() {
1915   const url = "data:," + JSON.stringify(returnsJson);
1916   Schemas._rootSchema = null;
1917   await Schemas.load(url);
1919   const apiObject = {
1920     complete() {
1921       return { size: 3, colour: "orange" };
1922     },
1923     optional() {
1924       return { size: 4 };
1925     },
1926     invalid() {
1927       return {};
1928     },
1929   };
1931   const localWrapper = {
1932     manifestVersion: 2,
1933     cloneScope: global,
1934     shouldInject(ns) {
1935       return true;
1936     },
1937     getImplementation(ns, name) {
1938       return new LocalAPIImplementation(apiObject, name, null);
1939     },
1940   };
1942   const root = {};
1943   Schemas.inject(root, localWrapper);
1945   deepEqual(root.returns.complete(), { size: 3, colour: "orange" });
1946   deepEqual(
1947     root.returns.optional(),
1948     { size: 4 },
1949     "Missing optional properties is allowed"
1950   );
1952   if (AppConstants.DEBUG) {
1953     Assert.throws(
1954       () => root.returns.invalid(),
1955       /Type error for result value \(Property "size" is required\)/,
1956       "Should throw for invalid result in DEBUG builds"
1957     );
1958   } else {
1959     deepEqual(
1960       root.returns.invalid(),
1961       {},
1962       "Doesn't throw for invalid result value in release builds"
1963     );
1964   }
1967 let booleanEnumJson = [
1968   {
1969     namespace: "booleanEnum",
1971     types: [
1972       {
1973         id: "enumTrue",
1974         type: "boolean",
1975         enum: [true],
1976       },
1977     ],
1978     functions: [
1979       {
1980         name: "paramMustBeTrue",
1981         type: "function",
1982         parameters: [{ name: "arg", $ref: "enumTrue" }],
1983       },
1984     ],
1985   },
1988 add_task(async function testBooleanEnum() {
1989   let wrapper = getContextWrapper();
1991   let url = "data:," + JSON.stringify(booleanEnumJson);
1992   Schemas._rootSchema = null;
1993   await Schemas.load(url);
1995   let root = {};
1996   Schemas.inject(root, wrapper);
1998   ok(root.booleanEnum, "namespace exists");
1999   root.booleanEnum.paramMustBeTrue(true);
2000   wrapper.verify("call", "booleanEnum", "paramMustBeTrue", [true]);
2001   Assert.throws(
2002     () => root.booleanEnum.paramMustBeTrue(false),
2003     /Type error for parameter arg \(Invalid value false\) for booleanEnum\.paramMustBeTrue\./,
2004     "should throw because enum of the type restricts parameter to true"
2005   );
2008 let xoriginJson = [
2009   {
2010     namespace: "xorigin",
2011     types: [],
2012     functions: [
2013       {
2014         name: "foo",
2015         type: "function",
2016         parameters: [
2017           {
2018             name: "arg",
2019             type: "any",
2020           },
2021         ],
2022       },
2023       {
2024         name: "crossFoo",
2025         type: "function",
2026         allowCrossOriginArguments: true,
2027         parameters: [
2028           {
2029             name: "arg",
2030             type: "any",
2031           },
2032         ],
2033       },
2034     ],
2035   },
2038 add_task(async function testCrossOriginArguments() {
2039   let url = "data:," + JSON.stringify(xoriginJson);
2040   Schemas._rootSchema = null;
2041   await Schemas.load(url);
2043   let sandbox = new Cu.Sandbox("http://test.com");
2045   let testingApiObj = {
2046     foo(arg) {
2047       sandbox.result = JSON.stringify(arg);
2048     },
2049     crossFoo(arg) {
2050       sandbox.xResult = JSON.stringify(arg);
2051     },
2052   };
2054   let localWrapper = {
2055     manifestVersion: 2,
2056     cloneScope: sandbox,
2057     shouldInject(ns) {
2058       return true;
2059     },
2060     getImplementation(ns, name) {
2061       return new LocalAPIImplementation(testingApiObj, name, null);
2062     },
2063   };
2065   let root = {};
2066   Schemas.inject(root, localWrapper);
2068   Assert.throws(
2069     () => root.xorigin.foo({ key: 13 }),
2070     /Permission denied to pass object/
2071   );
2072   equal(sandbox.result, undefined, "Foo can't read cross origin object.");
2074   root.xorigin.crossFoo({ answer: 42 });
2075   equal(sandbox.xResult, '{"answer":42}', "Can read cross origin object.");