1 module webidl.binding.test;
2 
3 import webidl.binding.generator;
4 import std.stdio;
5 import webidl.grammar;
6 import pegged.grammar : ParseTree;
7 
8 import std.array : appender, array, Appender;
9 import std.algorithm : each, sort, schwartzSort, filter, uniq, sum, max, maxElement, copy;
10 import std.algorithm : each, joiner, map;
11 import std.range : chain, enumerate;
12 import std.conv : text, to;
13 import std.range : zip, only;
14 
15 version (unittest) {
16   import unit_threaded;
17   __gshared bool updatedMethods = false;
18   static this() {
19     import openmethods : updateMethods;
20     if (!updatedMethods) {
21       updateMethods();
22       updatedMethods = true;
23     }
24   }
25   auto getGenerator(string input) {
26     auto semantics = new Semantics();
27     auto document = WebIDL(input);
28     if (!document.successful)
29       writeln(document);
30     document.successful.shouldEqual(true);
31     semantics.analyse("mod",document);
32     struct Helper {
33       IR ir;
34       Module module_;
35       Semantics semantics;
36       string generateDBindings() { return ir.generateDBindings(module_); }
37       string generateDImports() { return ir.generateDImports(module_); }
38       string generateJsExports(string[] filtered = []) {
39         auto app = IndentedStringAppender();
40         ir.nodes.each!(n => n.toJsExport(semantics, filtered, &app));
41         return app.data;
42       }
43       string generateJsGlobalBindings(string[] filtered = []) {
44         auto app = IndentedStringAppender();
45         ir.generateJsGlobalBindings(filtered, app);
46         return app.data;
47       }
48       string generateJsDecoders(string[] filtered = []) {
49         auto decodedTypes = ir.generateDecodedTypes(filtered).sort!((a,b){return a.mangled < b.mangled;}).uniq!((a, b){return a.mangled == b.mangled;});
50         bool first = true;
51         auto app = IndentedStringAppender();
52         foreach(decoder; decodedTypes.filter!(e => e.mangled != "string")) {
53           if (!first)
54             app.putLn(",");
55           else
56             first = false;
57           decoder.generateJsDecoder(app, semantics, true);
58         }
59         return app.data;
60       }
61       string generateJsEncoders(string[] filtered = []) {
62         auto encodedTypes = ir.generateEncodedTypes(filtered).sort!((a,b){return a.mangled < b.mangled;}).uniq!((a, b){return a.mangled == b.mangled;});
63         bool first = true;
64         auto app = IndentedStringAppender();
65         foreach(encoder; encodedTypes.filter!(e => e.mangled != "string")) {
66           if (!first)
67             app.putLn(",");
68           else
69             first = false;
70           encoder.generateJsEncoder(app, semantics, true);
71         }
72         return app.data;
73       }
74     }
75     return Helper(semantics.toIr(), semantics.modules["mod"], semantics);
76   }
77   void shouldBeLike(string a, string b, in string file = __FILE__, in size_t line = __LINE__) {
78     import std.string : lineSplitter, strip;
79     import std.algorithm : map, filter, equal;
80     import std.range : empty;
81     auto as = a.lineSplitter.map!(l => l.strip).filter!(l => !l.empty);
82     auto bs = b.lineSplitter.map!(l => l.strip).filter!(l => !l.empty);
83     while (!as.empty && !bs.empty) {
84       if (!as.front.equal(bs.front))
85         a.shouldEqual(b, file, line);
86       as.popFront();
87       bs.popFront();
88     }
89     if (as.empty != bs.empty)
90       a.shouldEqual(b);
91   }
92 }
93 
94 @("default")
95 unittest {
96   auto gen = getGenerator(q{
97       interface Event {
98         DelayNode createDelay (optional double maxDelayTime = 1.0);
99       };
100     });
101   gen.generateDBindings.shouldBeLike(q{
102       struct Event {
103         nothrow:
104         JsHandle handle;
105         alias handle this;
106         this(Handle h) {
107           this.handle = JsHandle(h);
108         }
109         auto createDelay()(double maxDelayTime /* = 1.0 */) {
110           return DelayNode(Event_createDelay(this.handle, maxDelayTime));
111         }
112         auto createDelay()() {
113           return DelayNode(Event_createDelay_0(this.handle));
114         }
115       }
116     });
117   gen.generateDImports.shouldBeLike(q{
118       extern (C) Handle Event_createDelay(Handle, double);
119       extern (C) Handle Event_createDelay_0(Handle);
120     });
121 }
122 
123 @("test-b")
124 unittest {
125   auto gen = getGenerator(q{
126       [Constructor(DOMString type, optional EventInit eventInitDict),
127        Exposed=(Window,Worker,AudioWorklet)]
128       interface Event {
129         readonly attribute DOMString type;
130         attribute EventTarget? target;
131         sequence<EventTarget> composedPath();
132 
133         const unsigned short NONE = 0;
134         attribute unsigned short eventPhase;
135       };
136       interface Window {
137       };
138       interface AudioWorklet {
139       };
140     });
141   gen.generateDBindings.shouldBeLike(q{
142       struct AudioWorklet {
143         nothrow:
144         JsHandle handle;
145         alias handle this;
146         this(Handle h) {
147           this.handle = JsHandle(h);
148         }
149         auto Event()(string type, scope ref EventInit eventInitDict) {
150           return .Event(AudioWorklet_Event(this.handle, type, eventInitDict.handle));
151         }
152       }
153       struct Event {
154         nothrow:
155         JsHandle handle;
156         alias handle this;
157         this(Handle h) {
158           this.handle = JsHandle(h);
159         }
160         auto type()() {
161           return Event_type_Get(this.handle);
162         }
163         void target(T0)(scope auto ref Optional!(T0) target) if (isTOrPointer!(T0, EventTarget)) {
164           Event_target_Set(this.handle, !target.empty, target.front.handle);
165         }
166         auto target()() {
167           return Event_target_Get(this.handle);
168         }
169         auto composedPath()() {
170           return Sequence!(EventTarget)(Event_composedPath(this.handle));
171         }
172         enum ushort NONE = 0;
173         void eventPhase()(ushort eventPhase) {
174           Event_eventPhase_Set(this.handle, eventPhase);
175         }
176         auto eventPhase()() {
177           return Event_eventPhase_Get(this.handle);
178         }
179       }
180       struct Window {
181         nothrow:
182         JsHandle handle;
183         alias handle this;
184         this(Handle h) {
185           this.handle = JsHandle(h);
186         }
187         auto Event()(string type, scope ref EventInit eventInitDict) {
188           return .Event(Window_Event(this.handle, type, eventInitDict.handle));
189         }
190       }
191     });
192   gen.generateDImports.shouldBeLike(q{
193       extern (C) Handle AudioWorklet_Event(Handle, string, Handle);
194       extern (C) string Event_type_Get(Handle);
195       extern (C) void Event_target_Set(Handle, bool, Handle);
196       extern (C) Optional!(EventTarget) Event_target_Get(Handle);
197       extern (C) Handle Event_composedPath(Handle);
198       extern (C) void Event_eventPhase_Set(Handle, ushort);
199       extern (C) ushort Event_eventPhase_Get(Handle);
200       extern (C) Handle Window_Event(Handle, string, Handle);
201     });
202   gen.generateJsExports.shouldBeLike("
203           AudioWorklet_Event: (ctx, typeLen, typePtr, eventInitDict) => {
204             setupMemory();
205             return addObject(new objects[ctx].Event(spasm_decode_string(typeLen, typePtr), objects[eventInitDict]));
206           },
207           Event_type_Get: (rawResult, ctx) => {
208             setupMemory();
209             spasm_encode_string(rawResult, objects[ctx].type);
210           },
211           Event_target_Set: (ctx, targetDefined, target) => {
212             setupMemory();
213             objects[ctx].target = targetDefined ? objects[target] : undefined;
214           },
215           Event_target_Get: (rawResult, ctx) => {
216             setupMemory();
217             spasm_encode_optional_Handle(rawResult, objects[ctx].target);
218           },
219           Event_composedPath: (ctx) => {
220             setupMemory();
221             return addObject(objects[ctx].composedPath());
222           },
223           Event_eventPhase_Set: (ctx, eventPhase) => {
224             setupMemory();
225             objects[ctx].eventPhase = eventPhase;
226           },
227           Event_eventPhase_Get: (ctx) => {
228             setupMemory();
229             return objects[ctx].eventPhase;
230           },
231           Window_Event: (ctx, typeLen, typePtr, eventInitDict) => {
232             setupMemory();
233             return addObject(new objects[ctx].Event(spasm_decode_string(typeLen, typePtr), objects[eventInitDict]));
234           },
235       ");
236 }
237 
238 @("enum")
239 unittest {
240   auto gen = getGenerator(q{
241       enum ImageOrientation { "none", "flipY", "with-hypen" };
242       interface Foo {
243         attribute ImageOrientation orientation;
244         ImageOrientation needs();
245         void wants(ImageOrientation o);
246       };
247     });
248 
249   gen.generateDBindings.shouldBeLike(q{
250       struct Foo {
251         nothrow:
252         JsHandle handle;
253         alias handle this;
254         this(Handle h) {
255           this.handle = JsHandle(h);
256         }
257         void orientation()(ImageOrientation orientation) {
258           Foo_orientation_Set(this.handle, orientation);
259         }
260         auto orientation()() {
261           return Foo_orientation_Get(this.handle);
262         }
263         auto needs()() {
264           return Foo_needs(this.handle);
265         }
266         void wants()(ImageOrientation o) {
267           Foo_wants(this.handle, o);
268         }
269       }
270       enum ImageOrientation {
271         none,
272         flipY,
273         with_hypen
274       }
275   });
276   gen.generateDImports.shouldBeLike(q{
277       extern (C) void Foo_orientation_Set(Handle, ImageOrientation);
278       extern (C) ImageOrientation Foo_orientation_Get(Handle);
279       extern (C) ImageOrientation Foo_needs(Handle);
280       extern (C) void Foo_wants(Handle, ImageOrientation);
281     });
282   gen.generateJsExports.shouldBeLike("
283     Foo_orientation_Set: (ctx, orientation) => {
284       setupMemory();
285       objects[ctx].orientation = spasm_decode_ImageOrientation(orientation);
286     },
287     Foo_orientation_Get: (ctx) => {
288       setupMemory();
289       return spasm_encode_ImageOrientation(objects[ctx].orientation);
290     },
291     Foo_needs: (ctx) => {
292       setupMemory();
293       return spasm_encode_ImageOrientation(objects[ctx].needs());
294     },
295     Foo_wants: (ctx, o) => {
296       setupMemory();
297       objects[ctx].wants(spasm_decode_ImageOrientation(o));
298     },
299 ");
300 }
301 
302 unittest {
303   auto gen = getGenerator(q{
304       callback SomethingCallback = DOMString (DOMString msg, boolean v);
305       callback DecodeErrorCallback = void (DOMException error);
306     });
307   gen.generateDBindings.shouldBeLike(q{
308       alias DecodeErrorCallback = void delegate(DOMException);
309       alias SomethingCallback = string delegate(string, bool);
310     });
311   gen.generateDImports.shouldBeEmpty;
312   gen.generateJsExports.shouldBeLike("");
313 }
314 
315 @("sequence")
316 unittest {
317   auto gen = getGenerator(q{
318       interface BaseAudioContext : EventTarget {
319         PeriodicWave createPeriodicWave (sequence<float> real, optional PeriodicWaveConstraints constraints);
320       };
321     });
322   gen.generateDBindings.shouldBeLike(q{
323       struct BaseAudioContext {
324         nothrow:
325         EventTarget _parent;
326         alias _parent this;
327         this(Handle h) {
328           _parent = .EventTarget(h);
329         }
330         auto createPeriodicWave()(scope ref Sequence!(float) real_, scope ref PeriodicWaveConstraints constraints) {
331           return PeriodicWave(BaseAudioContext_createPeriodicWave(this._parent, real_.handle, constraints.handle));
332         }
333         auto createPeriodicWave()(scope ref Sequence!(float) real_) {
334           return PeriodicWave(BaseAudioContext_createPeriodicWave_0(this._parent, real_.handle));
335         }
336       }
337     });
338   gen.generateDImports.shouldBeLike(q{
339       extern (C) Handle BaseAudioContext_createPeriodicWave(Handle, Handle, Handle);
340       extern (C) Handle BaseAudioContext_createPeriodicWave_0(Handle, Handle);
341     });
342   gen.generateJsExports.shouldBeLike("
343 BaseAudioContext_createPeriodicWave: (ctx, real, constraints) => {
344   setupMemory();
345   return addObject(objects[ctx].createPeriodicWave(objects[real], objects[constraints]));
346 },
347 BaseAudioContext_createPeriodicWave_0: (ctx, real) => {
348   setupMemory();
349   return addObject(objects[ctx].createPeriodicWave(objects[real]));
350 },
351 ");
352 }
353 
354 @("optional")
355 unittest {
356   auto gen = getGenerator(q{
357       interface Foo {
358         void bar (optional unsigned long number);
359       };
360     });
361   gen.generateDBindings.shouldBeLike(q{
362       struct Foo {
363         nothrow:
364         JsHandle handle;
365         alias handle this;
366         this(Handle h) {
367           this.handle = JsHandle(h);
368         }
369         void bar()(uint number) {
370           Foo_bar(this.handle, number);
371         }
372         void bar()() {
373           Foo_bar_0(this.handle);
374         }
375       }
376     });
377   gen.generateDImports.shouldBeLike(q{
378       extern (C) void Foo_bar(Handle, uint);
379       extern (C) void Foo_bar_0(Handle);
380     });
381   gen.generateJsExports.shouldBeLike("
382 Foo_bar: (ctx, number) => {
383   setupMemory();
384   objects[ctx].bar(number);
385 },
386 Foo_bar_0: (ctx) => {
387   setupMemory();
388   objects[ctx].bar();
389 },
390 ");
391 }
392 
393 @("null")
394 unittest {
395   auto gen = getGenerator(q{
396       interface Foo {
397         void bar (unsigned long? number, Bar? constraints);
398       };
399     });
400   gen.generateDBindings.shouldBeLike(q{
401       struct Foo {
402         nothrow:
403         JsHandle handle;
404         alias handle this;
405         this(Handle h) {
406           this.handle = JsHandle(h);
407         }
408         void bar(T0, T1)(scope auto ref Optional!(T0) number, scope auto ref Optional!(T1) constraints) if (isTOrPointer!(T0, uint) && isTOrPointer!(T1, Bar)) {
409           Foo_bar(this.handle, !number.empty, number.front, !constraints.empty, constraints.front.handle);
410         }
411       }
412     });
413   gen.generateDImports.shouldBeLike(q{
414       extern (C) void Foo_bar(Handle, bool, uint, bool, Handle);
415     });
416   gen.generateJsExports.shouldBeLike("
417     Foo_bar: (ctx, numberDefined, number, constraintsDefined, constraints) => {
418       setupMemory();
419       objects[ctx].bar(numberDefined ? number : undefined, constraintsDefined ? objects[constraints] : undefined);
420     },
421 ");
422 }
423 
424 @("interface.callback")
425 unittest {
426   auto gen = getGenerator(q{
427       callback DecodeErrorCallback = void (DOMException error);
428       callback DecodeSuccessCallback = void (AudioBuffer decodedData);
429       interface BaseAudioContext : EventTarget {
430         Promise<AudioBuffer> decodeAudioData (ArrayBuffer audioData, optional DecodeSuccessCallback? successCallback, optional DecodeErrorCallback? errorCallback);
431       };
432     });
433   gen.generateDBindings.shouldBeLike(q{
434       struct BaseAudioContext {
435         nothrow:
436         EventTarget _parent;
437         alias _parent this;
438         this(Handle h) {
439           _parent = .EventTarget(h);
440         }
441         auto decodeAudioData(T1, T2)(scope ref ArrayBuffer audioData, scope auto ref Optional!(T1) successCallback, scope auto ref Optional!(T2) errorCallback) if (isTOrPointer!(T1, DecodeSuccessCallback) && isTOrPointer!(T2, DecodeErrorCallback)) {
442           return Promise!(AudioBuffer)(BaseAudioContext_decodeAudioData(this._parent, audioData.handle, !successCallback.empty, successCallback.front, !errorCallback.empty, errorCallback.front));
443         }
444         auto decodeAudioData(T1)(scope ref ArrayBuffer audioData, scope auto ref Optional!(T1) successCallback) if (isTOrPointer!(T1, DecodeSuccessCallback)) {
445           return Promise!(AudioBuffer)(BaseAudioContext_decodeAudioData_0(this._parent, audioData.handle, !successCallback.empty, successCallback.front));
446         }
447         auto decodeAudioData()(scope ref ArrayBuffer audioData) {
448           return Promise!(AudioBuffer)(BaseAudioContext_decodeAudioData_1(this._parent, audioData.handle));
449         }
450       }
451       alias DecodeErrorCallback = void delegate(DOMException);
452       alias DecodeSuccessCallback = void delegate(AudioBuffer);
453     });
454   gen.generateDImports.shouldBeLike(q{
455       extern (C) Handle BaseAudioContext_decodeAudioData(Handle, Handle, bool, DecodeSuccessCallback, bool, DecodeErrorCallback);
456       extern (C) Handle BaseAudioContext_decodeAudioData_0(Handle, Handle, bool, DecodeSuccessCallback);
457       extern (C) Handle BaseAudioContext_decodeAudioData_1(Handle, Handle);
458     });
459   gen.generateJsExports.shouldBeLike("
460 BaseAudioContext_decodeAudioData: (ctx, audioData, successCallbackDefined, successCallbackCtx, successCallbackPtr, errorCallbackDefined, errorCallbackCtx, errorCallbackPtr) => {
461   setupMemory();
462   return addObject(objects[ctx].decodeAudioData(objects[audioData], successCallbackDefined ? (decodedData)=>{encode_handle(0, decodedData);spasm_indirect_function_get(successCallbackPtr)(successCallbackCtx, 0)} : undefined, errorCallbackDefined ? (error)=>{encode_handle(0, error);spasm_indirect_function_get(errorCallbackPtr)(errorCallbackCtx, 0)} : undefined));
463 },
464 BaseAudioContext_decodeAudioData_0: (ctx, audioData, successCallbackDefined, successCallbackCtx, successCallbackPtr) => {
465   setupMemory();
466   return addObject(objects[ctx].decodeAudioData(objects[audioData], successCallbackDefined ? (decodedData)=>{encode_handle(0, decodedData);spasm_indirect_function_get(successCallbackPtr)(successCallbackCtx, 0)} : undefined));
467 },
468 BaseAudioContext_decodeAudioData_1: (ctx, audioData) => {
469   setupMemory();
470   return addObject(objects[ctx].decodeAudioData(objects[audioData]));
471 },
472 ");
473 }
474 
475 @("sumType.interface")
476 unittest {
477   auto gen = getGenerator(q{
478       enum AudioContextLatencyCategory { "balanced", "interactive", "playback" };
479       interface AudioContextOptions {
480         attribute (AudioContextLatencyCategory or double) latencyHint;
481         attribute (boolean or double)? sampleRate;
482         (DOMString or AudioContextLatencyCategory) fooBar();
483       };
484     });
485   gen.generateDBindings.shouldBeLike(q{
486       enum AudioContextLatencyCategory {
487         balanced,
488         interactive,
489         playback
490       }
491       struct AudioContextOptions {
492         nothrow:
493         JsHandle handle;
494         alias handle this;
495         this(Handle h) {
496           this.handle = JsHandle(h);
497         }
498         void latencyHint()(scope ref SumType!(AudioContextLatencyCategory, double) latencyHint) {
499           AudioContextOptions_latencyHint_Set(this.handle, latencyHint);
500         }
501         auto latencyHint()() {
502           return AudioContextOptions_latencyHint_Get(this.handle);
503         }
504         void sampleRate(T0)(scope auto ref Optional!(T0) sampleRate) if (isTOrPointer!(T0, SumType!(bool, double))) {
505           AudioContextOptions_sampleRate_Set(this.handle, !sampleRate.empty, *sampleRate.frontRef);
506         }
507         auto sampleRate()() {
508           return AudioContextOptions_sampleRate_Get(this.handle);
509         }
510         auto fooBar()() {
511           return AudioContextOptions_fooBar(this.handle);
512         }
513       }
514     });
515   gen.generateDImports.shouldBeLike(q{
516       extern (C) void AudioContextOptions_latencyHint_Set(Handle, scope ref SumType!(AudioContextLatencyCategory, double));
517       extern (C) SumType!(AudioContextLatencyCategory, double) AudioContextOptions_latencyHint_Get(Handle);
518       extern (C) void AudioContextOptions_sampleRate_Set(Handle, bool, scope ref SumType!(bool, double));
519       extern (C) Optional!(SumType!(bool, double)) AudioContextOptions_sampleRate_Get(Handle);
520       extern (C) SumType!(string, AudioContextLatencyCategory) AudioContextOptions_fooBar(Handle);
521     });
522   // TODO: the optionals and unions returned from js should probably be stored in first extra param
523   gen.generateJsExports.shouldBeLike("
524     AudioContextOptions_latencyHint_Set: (ctx, latencyHint) => {
525       setupMemory();
526       objects[ctx].latencyHint = spasm_decode_union2_AudioContextLatencyCategory_double(latencyHint);
527     },
528     AudioContextOptions_latencyHint_Get: (rawResult, ctx) => {
529       setupMemory();
530       spasm_encode_union2_AudioContextLatencyCategory_double(rawResult, objects[ctx].latencyHint);
531     },
532     AudioContextOptions_sampleRate_Set: (ctx, sampleRateDefined, sampleRate) => {
533       setupMemory();
534       objects[ctx].sampleRate = sampleRateDefined ? spasm_decode_union2_bool_double(sampleRate) : undefined;
535     },
536     AudioContextOptions_sampleRate_Get: (rawResult, ctx) => {
537       setupMemory();
538       spasm_encode_optional_union2_bool_double(rawResult, objects[ctx].sampleRate);
539     },
540     AudioContextOptions_fooBar: (rawResult, ctx) => {
541       setupMemory();
542       spasm_encode_union2_string_AudioContextLatencyCategory(rawResult, objects[ctx].fooBar());
543     },
544 ");
545 }
546 
547 @("sumType.dictionary")
548 unittest {
549   auto gen = getGenerator(q{
550       enum AudioContextLatencyCategory { "balanced", "interactive", "playback" };
551       dictionary AudioContextOptions {
552         (AudioContextLatencyCategory or double) latencyHint = "interactive";
553         (boolean or double)? sampleRate;
554       };
555     });
556   gen.generateDBindings.shouldBeLike(q{
557       enum AudioContextLatencyCategory {
558         balanced,
559         interactive,
560         playback
561       }
562       struct AudioContextOptions {
563         nothrow:
564         JsHandle handle;
565         alias handle this;
566         this(Handle h) {
567           this.handle = JsHandle(h);
568         }
569         static auto create() {
570           return AudioContextOptions(spasm_add__object());
571         }
572         void latencyHint()(scope ref SumType!(AudioContextLatencyCategory, double) latencyHint) {
573           AudioContextOptions_latencyHint_Set(this.handle, latencyHint);
574         }
575         auto latencyHint()() {
576           return AudioContextOptions_latencyHint_Get(this.handle);
577         }
578         void sampleRate(T0)(scope auto ref Optional!(T0) sampleRate) if (isTOrPointer!(T0, SumType!(bool, double))) {
579           AudioContextOptions_sampleRate_Set(this.handle, !sampleRate.empty, *sampleRate.frontRef);
580         }
581         auto sampleRate()() {
582           return AudioContextOptions_sampleRate_Get(this.handle);
583         }
584       }
585     });
586   gen.generateDImports.shouldBeLike(q{
587       extern (C) void AudioContextOptions_latencyHint_Set(Handle, scope ref SumType!(AudioContextLatencyCategory, double));
588       extern (C) SumType!(AudioContextLatencyCategory, double) AudioContextOptions_latencyHint_Get(Handle);
589       extern (C) void AudioContextOptions_sampleRate_Set(Handle, bool, scope ref SumType!(bool, double));
590       extern (C) Optional!(SumType!(bool, double)) AudioContextOptions_sampleRate_Get(Handle);
591     });
592   gen.generateJsExports.shouldBeLike("
593     AudioContextOptions_latencyHint_Set: (ctx, latencyHint) => {
594       setupMemory();
595       objects[ctx].latencyHint = spasm_decode_union2_AudioContextLatencyCategory_double(latencyHint);
596     },
597     AudioContextOptions_latencyHint_Get: (rawResult, ctx) => {
598       setupMemory();
599       spasm_encode_union2_AudioContextLatencyCategory_double(rawResult, objects[ctx].latencyHint);
600     },
601     AudioContextOptions_sampleRate_Set: (ctx, sampleRateDefined, sampleRate) => {
602       setupMemory();
603       objects[ctx].sampleRate = sampleRateDefined ? spasm_decode_union2_bool_double(sampleRate) : undefined;
604     },
605     AudioContextOptions_sampleRate_Get: (rawResult, ctx) => {
606       setupMemory();
607       spasm_encode_optional_union2_bool_double(rawResult, objects[ctx].sampleRate);
608     },
609 ");
610 
611 }
612 
613 @("partial")
614 unittest {
615   // TODO: test partial interface with an optional attribute
616 }
617 
618 @("partial.friendlyName")
619 unittest {
620   auto gen = getGenerator(q{
621       interface double {
622       };
623       partial interface double {
624         [CEReactions] attribute DOMString real;
625       };
626     });
627   gen.generateDBindings.shouldBeLike(q{
628       struct double_ {
629         nothrow:
630         JsHandle handle;
631         alias handle this;
632         this(Handle h) {
633           this.handle = JsHandle(h);
634         }
635         void real_()(string real_) {
636           double_real_Set(this.handle, real_);
637         }
638         auto real_()() {
639           return double_real_Get(this.handle);
640         }
641       }
642     });
643   gen.generateDImports.shouldBeLike(q{
644       extern (C) void double_real_Set(Handle, string);
645       extern (C) string double_real_Get(Handle);
646     });
647   gen.generateJsExports.shouldBeLike("
648     double_real_Set: (ctx, realLen, realPtr) => {
649       setupMemory();
650       objects[ctx].real = spasm_decode_string(realLen, realPtr);
651     },
652     double_real_Get: (rawResult, ctx) => {
653       setupMemory();
654       spasm_encode_string(rawResult, objects[ctx].real);
655     },
656 ");
657 }
658 
659 @("dictionary.primitives")
660 unittest {
661   auto gen = getGenerator(q{
662       dictionary AudioTimestamp {
663         double contextTime;
664       };
665     });
666   gen.generateDBindings.shouldBeLike(q{
667       struct AudioTimestamp {
668         nothrow:
669         JsHandle handle;
670         alias handle this;
671         this(Handle h) {
672           this.handle = JsHandle(h);
673         }
674         static auto create() {
675           return AudioTimestamp(spasm_add__object());
676         }
677         void contextTime()(double contextTime) {
678           AudioTimestamp_contextTime_Set(this.handle, contextTime);
679         }
680         auto contextTime()() {
681           return AudioTimestamp_contextTime_Get(this.handle);
682         }
683       }
684     });
685   gen.generateDImports.shouldBeLike(q{
686       extern (C) void AudioTimestamp_contextTime_Set(Handle, double);
687       extern (C) double AudioTimestamp_contextTime_Get(Handle);
688     });
689   gen.generateJsExports.shouldBeLike("
690     AudioTimestamp_contextTime_Set: (ctx, contextTime) => {
691       setupMemory();
692       objects[ctx].contextTime = contextTime;
693     },
694     AudioTimestamp_contextTime_Get: (ctx) => {
695       setupMemory();
696       return objects[ctx].contextTime;
697     },
698 ");
699 }
700 
701 @("dictionary.inheritance")
702 unittest {
703   auto gen = getGenerator(q{
704       dictionary AudioWorkletNodeOptions : AudioNodeOptions {
705       };
706     });
707   gen.generateDBindings.shouldBeLike(q{
708       struct AudioWorkletNodeOptions {
709         nothrow:
710         AudioNodeOptions _parent;
711         alias _parent this;
712         this(Handle h) {
713           _parent = .AudioNodeOptions(h);
714         }
715         static auto create() {
716           return AudioWorkletNodeOptions(spasm_add__object());
717         }
718       }
719     });
720   gen.generateDImports.shouldBeLike(q{});
721   gen.generateJsExports.shouldBeLike("");
722 }
723 
724 @("interface.friendlyName")
725 unittest {
726   auto gen = getGenerator(q{
727       interface double {
728         attribute double real;
729       };
730     });
731   gen.generateDBindings.shouldBeLike(q{
732       struct double_ {
733         nothrow:
734         JsHandle handle;
735         alias handle this;
736         this(Handle h) {
737           this.handle = JsHandle(h);
738         }
739         void real_()(double real_) {
740           double_real_Set(this.handle, real_);
741         }
742         auto real_()() {
743           return double_real_Get(this.handle);
744         }
745       }
746     });
747   gen.generateDImports.shouldBeLike(q{
748       extern (C) void double_real_Set(Handle, double);
749       extern (C) double double_real_Get(Handle);
750     });
751   gen.generateJsExports.shouldBeLike("
752     double_real_Set: (ctx, real) => {
753       setupMemory();
754       objects[ctx].real = real;
755     },
756     double_real_Get: (ctx) => {
757       setupMemory();
758       return objects[ctx].real;
759     },
760 ");
761 }
762 
763 @("dictionary.record")
764 unittest {
765   auto gen = getGenerator(q{
766       dictionary AudioWorkletNodeOptions {
767         record<DOMString, double> real;
768       };
769     });
770   gen.generateDBindings.shouldBeLike(q{
771       struct AudioWorkletNodeOptions {
772         nothrow:
773         JsHandle handle;
774         alias handle this;
775         this(Handle h) {
776           this.handle = JsHandle(h);
777         }
778         static auto create() {
779           return AudioWorkletNodeOptions(spasm_add__object());
780         }
781         void real_()(scope ref Record!(string, double) real_) {
782           AudioWorkletNodeOptions_real_Set(this.handle, real_.handle);
783         }
784         auto real_()() {
785           return Record!(string, double)(AudioWorkletNodeOptions_real_Get(this.handle));
786         }
787       }
788     });
789   gen.generateDImports.shouldBeLike(q{
790       extern (C) void AudioWorkletNodeOptions_real_Set(Handle, Handle);
791       extern (C) Handle AudioWorkletNodeOptions_real_Get(Handle);
792     });
793   // TODO: jsExports
794 }
795 
796 @("dictionary.float")
797 unittest {
798   auto gen = getGenerator(q{
799       dictionary AudioParamDescriptor {
800         float maxValue = 3.4028235e38;
801         float minValue = -3.4028235e38;
802       };
803     });
804   gen.generateDBindings.shouldBeLike(q{
805       struct AudioParamDescriptor {
806         nothrow:
807         JsHandle handle;
808         alias handle this;
809         this(Handle h) {
810           this.handle = JsHandle(h);
811         }
812         static auto create() {
813           return AudioParamDescriptor(spasm_add__object());
814         }
815         void maxValue()(float maxValue) {
816           AudioParamDescriptor_maxValue_Set(this.handle, maxValue);
817         }
818         auto maxValue()() {
819           return AudioParamDescriptor_maxValue_Get(this.handle);
820         }
821         void minValue()(float minValue) {
822           AudioParamDescriptor_minValue_Set(this.handle, minValue);
823         }
824         auto minValue()() {
825           return AudioParamDescriptor_minValue_Get(this.handle);
826         }
827       }
828     });
829   gen.generateDImports.shouldBeLike(q{
830       extern (C) void AudioParamDescriptor_maxValue_Set(Handle, float);
831       extern (C) float AudioParamDescriptor_maxValue_Get(Handle);
832       extern (C) void AudioParamDescriptor_minValue_Set(Handle, float);
833       extern (C) float AudioParamDescriptor_minValue_Get(Handle);
834     });
835   gen.generateJsExports.shouldBeLike("
836     AudioParamDescriptor_maxValue_Set: (ctx, maxValue) => {
837       setupMemory();
838       objects[ctx].maxValue = maxValue;
839     },
840     AudioParamDescriptor_maxValue_Get: (ctx) => {
841       setupMemory();
842       return objects[ctx].maxValue;
843     },
844     AudioParamDescriptor_minValue_Set: (ctx, minValue) => {
845       setupMemory();
846       objects[ctx].minValue = minValue;
847     },
848     AudioParamDescriptor_minValue_Get: (ctx) => {
849       setupMemory();
850       return objects[ctx].minValue;
851     },
852 ");
853 }
854 
855 @("interface.maplike")
856 unittest {
857   auto gen = getGenerator(q{
858       [Exposed=Window]
859       interface AudioParamMap {
860         readonly maplike<DOMString, AudioParam>;
861       };
862     });
863   // TODO: a readonly maplike should probably not have clear, set, delete, etc.
864   gen.generateDBindings.shouldBeLike(q{
865       struct AudioParamMap {
866         nothrow:
867         JsHandle handle;
868         alias handle this;
869         this(Handle h) {
870           this.handle = JsHandle(h);
871         }
872         uint size() {
873           return Maplike_string_Handle_size(this.handle);
874         }
875         void clear() {
876           Maplike_string_Handle_clear(this.handle);
877         }
878         void delete_(string key) {
879           Maplike_string_Handle_delete(this.handle, key);
880         }
881         Iterator!(ArrayPair!(string, AudioParam)) entries() {
882           return Iterator!(ArrayPair!(string, AudioParam))(Maplike_string_Handle_entries(this.handle));
883         }
884         extern(C) void forEach(void delegate(string, Handle, Handle) callback) {
885           Maplike_string_Handle_forEach(this.handle, callback);
886         }
887         AudioParam get(string key) {
888           return AudioParam(Maplike_string_Handle_get(this.handle, key));
889         }
890         bool has(string key) {
891           return Maplike_string_Handle_has(this.handle, key);
892         }
893         Iterator!(string) keys() {
894           return Iterator!(string)(Maplike_string_Handle_keys(this.handle));
895         }
896         void set(string key, scope ref AudioParam value) {
897           Maplike_string_Handle_set(this.handle, key, value.handle);
898         }
899         Iterator!(AudioParam) values() {
900           return Iterator!(AudioParam)(Maplike_string_Handle_values(this.handle));
901         }
902       }
903     });
904   gen.generateDImports.shouldBeLike(q{
905       extern (C) uint Maplike_string_Handle_size(Handle);
906       extern (C) void Maplike_string_Handle_clear(Handle);
907       extern (C) void Maplike_string_Handle_delete(Handle, string key);
908       extern (C) Handle Maplike_string_Handle_entries(Handle);
909       extern (C) void Maplike_string_Handle_forEach(Handle, void delegate(string, Handle, Handle));
910       extern (C) AudioParam Maplike_string_Handle_get(Handle, string);
911       extern (C) bool Maplike_string_Handle_has(Handle, string);
912       extern (C) Handle Maplike_string_Handle_keys(Handle);
913       extern (C) void Maplike_string_Handle_set(Handle, string key, Handle value);
914       extern (C) Handle Maplike_string_Handle_values(Handle);
915     });
916   gen.generateJsExports.shouldBeLike("");
917 }
918 
919 @("friendlyName")
920 unittest {
921   "real".friendlyName.shouldEqual("real_");
922   "with-hypen".friendlyName.shouldEqual("with_hypen");
923   "0with-number".friendlyName.shouldEqual("_0with_number");
924 }
925 
926 @("putCamelCase")
927 unittest {
928   {
929     auto app = appender!string;
930     app.putCamelCase("HTMLAnchorElement");
931     app.data.shouldEqual("htmlAnchorElement");
932   }
933   {
934     auto app = appender!string;
935     app.putCamelCase("HtmlAnchorElement");
936     app.data.shouldEqual("htmlAnchorElement");
937   }
938   {
939     auto app = appender!string;
940     app.putCamelCase("htmlAnchorElement");
941     app.data.shouldEqual("htmlAnchorElement");
942   }
943 }
944 
945 @("optional.string")
946 unittest {
947   auto gen = getGenerator(q{
948       interface BaseAudioContext : EventTarget {
949         DOMString? createPeriodicWave();
950         attribute DOMString? name;
951         void foo(DOMString? title);
952       };
953     });
954   gen.generateDBindings.shouldBeLike(q{
955       struct BaseAudioContext {
956         nothrow:
957         EventTarget _parent;
958         alias _parent this;
959         this(Handle h) {
960           _parent = .EventTarget(h);
961         }
962         auto createPeriodicWave()() {
963           return BaseAudioContext_createPeriodicWave(this._parent);
964         }
965         void name(T0)(scope auto ref Optional!(T0) name) if (isTOrPointer!(T0, string)) {
966           BaseAudioContext_name_Set(this._parent, !name.empty, name.front);
967         }
968         auto name()() {
969           return BaseAudioContext_name_Get(this._parent);
970         }
971         void foo(T0)(scope auto ref Optional!(T0) title) if (isTOrPointer!(T0, string)) {
972           BaseAudioContext_foo(this._parent, !title.empty, title.front);
973         }
974       }
975     });
976   gen.generateDImports.shouldBeLike(q{
977       extern (C) Optional!(string) BaseAudioContext_createPeriodicWave(Handle);
978       extern (C) void BaseAudioContext_name_Set(Handle, bool, string);
979       extern (C) Optional!(string) BaseAudioContext_name_Get(Handle);
980       extern (C) void BaseAudioContext_foo(Handle, bool, string);
981     });
982   gen.generateJsExports.shouldBeLike("
983     BaseAudioContext_createPeriodicWave: (rawResult, ctx) => {
984       setupMemory();
985       spasm_encode_optional_string(rawResult, objects[ctx].createPeriodicWave());
986     },
987     BaseAudioContext_name_Set: (ctx, nameDefined, nameLen, namePtr) => {
988       setupMemory();
989       objects[ctx].name = nameDefined ? spasm_decode_string(nameLen, namePtr) : undefined;
990     },
991     BaseAudioContext_name_Get: (rawResult, ctx) => {
992       setupMemory();
993       spasm_encode_optional_string(rawResult, objects[ctx].name);
994     },
995     BaseAudioContext_foo: (ctx, titleDefined, titleLen, titlePtr) => {
996       setupMemory();
997       objects[ctx].foo(titleDefined ? spasm_decode_string(titleLen, titlePtr) : undefined);
998     },
999 ");
1000 }
1001 
1002 @("interface.nullable")
1003 unittest {
1004   auto gen = getGenerator(q{
1005       interface ClipboardEvent : Event {
1006         readonly attribute DataTransfer? clipboardData;
1007       };
1008     });
1009   gen.generateDBindings.shouldBeLike(q{
1010       struct ClipboardEvent {
1011         nothrow:
1012         Event _parent;
1013         alias _parent this;
1014         this(Handle h) {
1015           _parent = .Event(h);
1016         }
1017         auto clipboardData()() {
1018           return ClipboardEvent_clipboardData_Get(this._parent);
1019         }
1020       }
1021     });
1022   gen.generateDImports.shouldBeLike(q{
1023       extern (C) Optional!(DataTransfer) ClipboardEvent_clipboardData_Get(Handle);
1024     });
1025   gen.generateJsExports.shouldBeLike("
1026     ClipboardEvent_clipboardData_Get: (rawResult, ctx) => {
1027       setupMemory();
1028       spasm_encode_optional_Handle(rawResult, objects[ctx].clipboardData);
1029     },
1030 ");
1031 }
1032 
1033 @("dictionary.nullable")
1034 unittest {
1035   auto gen = getGenerator(q{
1036       dictionary FocusEventInit {
1037         EventTarget? relatedTarget = null;
1038       };
1039     });
1040   gen.generateDBindings.shouldBeLike(q{
1041       struct FocusEventInit {
1042         nothrow:
1043         JsHandle handle;
1044         alias handle this;
1045         this(Handle h) {
1046           this.handle = JsHandle(h);
1047         }
1048         static auto create() {
1049           return FocusEventInit(spasm_add__object());
1050         }
1051         void relatedTarget(T0)(scope auto ref Optional!(T0) relatedTarget) if (isTOrPointer!(T0, EventTarget)) {
1052           FocusEventInit_relatedTarget_Set(this.handle, !relatedTarget.empty, relatedTarget.front.handle);
1053         }
1054         auto relatedTarget()() {
1055           return FocusEventInit_relatedTarget_Get(this.handle);
1056         }
1057       }
1058     });
1059   gen.generateDImports.shouldBeLike(q{
1060       extern (C) void FocusEventInit_relatedTarget_Set(Handle, bool, Handle);
1061       extern (C) Optional!(EventTarget) FocusEventInit_relatedTarget_Get(Handle);
1062     });
1063   gen.generateJsExports.shouldBeLike("
1064     FocusEventInit_relatedTarget_Set: (ctx, relatedTargetDefined, relatedTarget) => {
1065       setupMemory();
1066       objects[ctx].relatedTarget = relatedTargetDefined ? objects[relatedTarget] : undefined;
1067     },
1068     FocusEventInit_relatedTarget_Get: (rawResult, ctx) => {
1069       setupMemory();
1070       spasm_encode_optional_Handle(rawResult, objects[ctx].relatedTarget);
1071     },
1072 ");
1073 }
1074 
1075 @("interface.mixin")
1076 unittest {
1077   auto gen = getGenerator(q{
1078       interface mixin GenericTransformStream {
1079         readonly attribute WritableStream writable;
1080         readonly attribute ReadbleStream readable;
1081       };
1082       interface TextDecoderStream {
1083       };
1084       TextDecoderStream includes GenericTransformStream;
1085     });
1086   gen.generateDBindings.shouldBeLike(q{
1087       struct TextDecoderStream {
1088         nothrow:
1089         JsHandle handle;
1090         alias handle this;
1091         this(Handle h) {
1092           this.handle = JsHandle(h);
1093         }
1094         auto writable()() {
1095           return WritableStream(GenericTransformStream_writable_Get(this.handle));
1096         }
1097         auto readable()() {
1098           return ReadbleStream(GenericTransformStream_readable_Get(this.handle));
1099         }
1100       }
1101     });
1102   gen.generateDImports.shouldBeLike(q{
1103       extern (C) Handle GenericTransformStream_writable_Get(Handle);
1104       extern (C) Handle GenericTransformStream_readable_Get(Handle);
1105     });
1106   gen.generateJsExports.shouldBeLike("
1107     GenericTransformStream_writable_Get: (ctx) => {
1108       setupMemory();
1109       return addObject(objects[ctx].writable);
1110     },
1111     GenericTransformStream_readable_Get: (ctx) => {
1112       setupMemory();
1113       return addObject(objects[ctx].readable);
1114     },
1115 ");
1116 }
1117 
1118 @("interface.ExtendedAttribute")
1119 unittest {
1120   auto gen = getGenerator(q{
1121       interface HTMLOrSVGElement {
1122         [SameObject] readonly attribute DOMStringMap datacube;
1123       };
1124     });
1125   gen.generateDBindings.shouldBeLike(q{
1126       struct HTMLOrSVGElement {
1127         nothrow:
1128         JsHandle handle;
1129         alias handle this;
1130         this(Handle h) {
1131           this.handle = JsHandle(h);
1132         }
1133         auto datacube()() {
1134           return DOMStringMap(HTMLOrSVGElement_datacube_Get(this.handle));
1135         }
1136       }
1137     });
1138   gen.generateDImports.shouldBeLike(q{
1139       extern (C) Handle HTMLOrSVGElement_datacube_Get(Handle);
1140     });
1141   gen.generateJsExports.shouldBeLike("
1142     HTMLOrSVGElement_datacube_Get: (ctx) => {
1143       setupMemory();
1144       return addObject(objects[ctx].datacube);
1145     },
1146 ");
1147 }
1148 
1149 @("interface.special")
1150 unittest {
1151   auto gen = getGenerator(q{
1152       [Exposed=Window,
1153        OverrideBuiltins]
1154       interface DOMStringMap {
1155         getter DOMString (DOMString name);
1156         [CEReactions] setter void (DOMString name, DOMString value);
1157         [CEReactions] deleter void (DOMString name);
1158         getter DOMString byKey(DOMString name);
1159         [CEReactions] setter void byKey(DOMString name, DOMString value);
1160       };
1161     });
1162   gen.generateDBindings.shouldBeLike(q{
1163       struct DOMStringMap {
1164         nothrow:
1165         JsHandle handle;
1166         alias handle this;
1167         this(Handle h) {
1168           this.handle = JsHandle(h);
1169         }
1170         auto opIndex()(string name) {
1171           return DOMStringMap_getter__string(this.handle, name);
1172         }
1173         auto opDispatch(string name)() {
1174           return DOMStringMap_getter__string(this.handle, name);
1175         }
1176         void opIndexAssign()(string value, string name) {
1177           DOMStringMap_setter__string_string(this.handle, name, value);
1178         }
1179         void opDispatch(string name)(string value) {
1180           DOMStringMap_setter__string_string(this.handle, name, value);
1181         }
1182         void remove()(string name) {
1183           DOMStringMap_deleter(this.handle, name);
1184         }
1185         auto byKey()(string name) {
1186           return DOMStringMap_byKey_getter(this.handle, name);
1187         }
1188         void byKey()(string name, string value) {
1189           DOMStringMap_byKey_setter(this.handle, name, value);
1190         }
1191       }
1192     });
1193   gen.generateDImports.shouldBeLike(q{
1194       extern (C) string DOMStringMap_getter__string(Handle, string);
1195       extern (C) void DOMStringMap_setter__string_string(Handle, string, string);
1196       extern (C) void DOMStringMap_deleter(Handle, string);
1197       extern (C) string DOMStringMap_byKey_getter(Handle, string);
1198       extern (C) void DOMStringMap_byKey_setter(Handle, string, string);
1199     });
1200   gen.generateJsExports.shouldBeLike("
1201     DOMStringMap_getter__string: (rawResult, ctx, nameLen, namePtr) => {
1202       setupMemory();
1203       spasm_encode_string(rawResult, objects[ctx][spasm_decode_string(nameLen, namePtr)]);
1204     },
1205     DOMStringMap_setter__string_string: (ctx, nameLen, namePtr, valueLen, valuePtr) => {
1206       setupMemory();
1207       objects[ctx][spasm_decode_string(nameLen, namePtr)] = spasm_decode_string(valueLen, valuePtr);
1208     },
1209     DOMStringMap_deleter: (ctx, nameLen, namePtr) => {
1210       setupMemory();
1211       delete objects[ctx][spasm_decode_string(nameLen, namePtr)];
1212     },
1213     DOMStringMap_byKey_getter: (rawResult, ctx, nameLen, namePtr) => {
1214       setupMemory();
1215       spasm_encode_string(rawResult, objects[ctx].byKey(spasm_decode_string(nameLen, namePtr)));
1216     },
1217     DOMStringMap_byKey_setter: (ctx, nameLen, namePtr, valueLen, valuePtr) => {
1218       setupMemory();
1219       objects[ctx].byKey(spasm_decode_string(nameLen, namePtr), spasm_decode_string(valueLen, valuePtr));
1220     },
1221 ");
1222 }
1223 
1224 @("interface.extendedAttributeList")
1225 unittest {
1226   getGenerator(q{
1227       [Constructor(DOMString type, ErrorEventInit eventInitDict, symbolBar bar, any thing)]
1228       interface ErrorEvent : Event {
1229       };
1230     });
1231 }
1232 
1233 @("interface.mixin.required")
1234 unittest {
1235   getGenerator(q{
1236       interface mixin SVGTests {
1237         [SameObject] readonly attribute Foo requiredExtensions;
1238       };
1239     });
1240 }
1241 
1242 @("comments")
1243 unittest {
1244   getGenerator(q{
1245       interface Foo {
1246     /* AlphaFunction (not supported in ES20) */
1247     /*      NEVER */
1248     /*      LESS */
1249     /*      EQUAL */
1250     /*      LEQUAL */
1251       };
1252     });
1253 }
1254 
1255 @("typedef.callback")
1256 unittest {
1257   auto gen = getGenerator(q{
1258       typedef double DOMHighResTimeStamp;
1259       callback FrameRequestCallback = void (DOMHighResTimeStamp time);
1260 
1261       interface AnimationFrameProvider {
1262         unsigned long requestAnimationFrame(FrameRequestCallback callback);
1263       };
1264     });
1265   gen.generateDBindings.shouldBeLike(q{
1266       struct AnimationFrameProvider {
1267         nothrow:
1268         JsHandle handle;
1269         alias handle this;
1270         this(Handle h) {
1271           this.handle = JsHandle(h);
1272         }
1273         auto requestAnimationFrame()(FrameRequestCallback callback) {
1274           return AnimationFrameProvider_requestAnimationFrame(this.handle, callback);
1275         }
1276       }
1277       alias DOMHighResTimeStamp = double;
1278       alias FrameRequestCallback = void delegate(double);
1279     });
1280   gen.generateDImports.shouldBeLike(q{
1281       extern (C) uint AnimationFrameProvider_requestAnimationFrame(Handle, FrameRequestCallback);
1282     });
1283   gen.generateJsExports.shouldBeLike("
1284     AnimationFrameProvider_requestAnimationFrame: (ctx, callbackCtx, callbackPtr) => {
1285       setupMemory();
1286       return objects[ctx].requestAnimationFrame((time)=>{spasm_indirect_function_get(callbackPtr)(callbackCtx, time)});
1287     },");
1288 }
1289 
1290 @("typedef.interface")
1291 unittest {
1292   auto gen = getGenerator(q{
1293       interface TextDecoder {
1294         USVString decode(optional BufferSource input, optional TextDecodeOptions options);
1295         BufferSource encode(optional TextDecodeOptions options);
1296       };
1297       typedef (ArrayBufferView or ArrayBuffer) BufferSource;
1298     });
1299   gen.generateDBindings.shouldBeLike(q{
1300       alias BufferSource = SumType!(ArrayBufferView, ArrayBuffer);
1301       struct TextDecoder {
1302         nothrow:
1303         JsHandle handle;
1304         alias handle this;
1305         this(Handle h) {
1306           this.handle = JsHandle(h);
1307         }
1308         auto decode()(scope ref BufferSource input, scope ref TextDecodeOptions options) {
1309           return TextDecoder_decode(this.handle, input, options.handle);
1310         }
1311         auto decode()(scope ref BufferSource input) {
1312           return TextDecoder_decode_0(this.handle, input);
1313         }
1314         auto decode()() {
1315           return TextDecoder_decode_1(this.handle);
1316         }
1317         auto encode()(scope ref TextDecodeOptions options) {
1318           return TextDecoder_encode(this.handle, options.handle);
1319         }
1320         auto encode()() {
1321           return TextDecoder_encode_0(this.handle);
1322         }
1323       }
1324     });
1325   gen.generateDImports.shouldBeLike(q{
1326       extern (C) string TextDecoder_decode(Handle, scope ref BufferSource, Handle);
1327       extern (C) string TextDecoder_decode_0(Handle, scope ref BufferSource);
1328       extern (C) string TextDecoder_decode_1(Handle);
1329       extern (C) BufferSource TextDecoder_encode(Handle, Handle);
1330       extern (C) BufferSource TextDecoder_encode_0(Handle);
1331     });
1332   gen.generateJsExports.shouldBeLike("
1333 TextDecoder_decode: (rawResult, ctx, input, options) => {
1334   setupMemory();
1335   spasm_encode_string(rawResult, objects[ctx].decode(spasm_decode_BufferSource(input), objects[options]));
1336 },
1337 TextDecoder_decode_0: (rawResult, ctx, input) => {
1338   setupMemory();
1339   spasm_encode_string(rawResult, objects[ctx].decode(spasm_decode_BufferSource(input)));
1340 },
1341 TextDecoder_decode_1: (rawResult, ctx) => {
1342   setupMemory();
1343   spasm_encode_string(rawResult, objects[ctx].decode());
1344 },
1345 TextDecoder_encode: (rawResult, ctx, options) => {
1346   setupMemory();
1347   spasm_encode_BufferSource(rawResult, objects[ctx].encode(objects[options]));
1348 },
1349 TextDecoder_encode_0: (rawResult, ctx) => {
1350   setupMemory();
1351   spasm_encode_BufferSource(rawResult, objects[ctx].encode());
1352 },
1353 ");
1354 }
1355 
1356 @("typedef.nullable")
1357 unittest {
1358   auto gen = getGenerator(q{
1359       typedef (Blob or BufferSource or FormData or URLSearchParams or ReadableStream or USVString) BodyInit;
1360       dictionary RequestInit {
1361         BodyInit? body;
1362       };
1363     });
1364   gen.generateDBindings.shouldBeLike(q{
1365       alias BodyInit = SumType!(Blob, BufferSource, FormData, URLSearchParams, ReadableStream, string);
1366       struct RequestInit {
1367         nothrow:
1368         JsHandle handle;
1369         alias handle this;
1370         this(Handle h) {
1371           this.handle = JsHandle(h);
1372         }
1373         static auto create() {
1374           return RequestInit(spasm_add__object());
1375         }
1376         void body_(T0)(scope auto ref Optional!(T0) body_) if (isTOrPointer!(T0, BodyInit)) {
1377           RequestInit_body_Set(this.handle, !body_.empty, *body_.frontRef);
1378         }
1379         auto body_()() {
1380           return RequestInit_body_Get(this.handle);
1381         }
1382       }
1383     });
1384   gen.generateDImports.shouldBeLike(q{
1385       extern (C) void RequestInit_body_Set(Handle, bool, scope ref BodyInit);
1386       extern (C) Optional!(BodyInit) RequestInit_body_Get(Handle);
1387     });
1388   gen.generateJsExports.shouldBeLike("
1389     RequestInit_body_Set: (ctx, bodyDefined, body) => {
1390       setupMemory();
1391       objects[ctx].body = bodyDefined ? spasm_decode_BodyInit(body) : undefined;
1392     },
1393     RequestInit_body_Get: (rawResult, ctx) => {
1394       setupMemory();
1395       spasm_encode_optional_BodyInit(rawResult, objects[ctx].body);
1396     },
1397 ");
1398 }
1399 
1400 
1401 @("sequence.nullable")
1402 unittest {
1403   auto gen = getGenerator(q{
1404       interface Foo {
1405         sequence<long>? bar(sequence<DOMString> names);
1406       };
1407     });
1408   gen.generateDBindings.shouldBeLike(q{
1409       struct Foo {
1410         nothrow:
1411         JsHandle handle;
1412         alias handle this;
1413         this(Handle h) {
1414           this.handle = JsHandle(h);
1415         }
1416         auto bar()(scope ref Sequence!(string) names) {
1417           return Foo_bar(this.handle, names.handle);
1418         }
1419       }
1420     });
1421   gen.generateDImports.shouldBeLike(q{
1422       extern (C) Optional!(Sequence!(int)) Foo_bar(Handle, Handle);
1423     });
1424   gen.generateJsExports.shouldBeLike("
1425     Foo_bar: (rawResult, ctx, names) => {
1426       setupMemory();
1427       spasm_encode_optional_sequence(rawResult, objects[ctx].bar(objects[names]));
1428     },
1429 ");
1430 }
1431 
1432 @("return.nullable")
1433 unittest {
1434   auto gen = getGenerator(q{
1435       interface Foo {
1436         getter File? item(unsigned long index);
1437       };
1438     });
1439   // TODO: also opIndex
1440   gen.generateDBindings.shouldBeLike(q{
1441       struct Foo {
1442         nothrow:
1443         JsHandle handle;
1444         alias handle this;
1445         this(Handle h) {
1446           this.handle = JsHandle(h);
1447         }
1448         auto item()(uint index) {
1449           return Foo_item_getter(this.handle, index);
1450         }
1451       }
1452     });
1453   gen.generateDImports.shouldBeLike(q{
1454       extern (C) Optional!(File) Foo_item_getter(Handle, uint);
1455     });
1456   gen.generateJsExports.shouldBeLike("
1457     Foo_item_getter: (rawResult, ctx, index) => {
1458       setupMemory();
1459       spasm_encode_optional_Handle(rawResult, objects[ctx].item(index));
1460     },
1461 ");
1462 }
1463 
1464 @("partial.mixin")
1465 unittest {
1466   auto gen = getGenerator(q{
1467       interface mixin Foo {
1468         DOMString name();
1469         readonly attribute boolean fatal;
1470       };
1471       partial interface Foo {
1472         long long size();
1473       };
1474       interface Bar {
1475       };
1476       Bar includes Foo;
1477     });
1478   gen.generateDBindings.shouldBeLike(q{
1479       struct Bar {
1480         nothrow:
1481         JsHandle handle;
1482         alias handle this;
1483         this(Handle h) {
1484           this.handle = JsHandle(h);
1485         }
1486         auto name()() {
1487           return Foo_name(this.handle);
1488         }
1489         auto fatal()() {
1490           return Foo_fatal_Get(this.handle);
1491         }
1492         auto size()() {
1493           return Foo_size(this.handle);
1494         }
1495       }
1496     });
1497   gen.generateDImports.shouldBeLike(q{
1498       extern (C) string Foo_name(Handle);
1499       extern (C) bool Foo_fatal_Get(Handle);
1500       extern (C) int Foo_size(Handle);
1501     });
1502   gen.generateJsExports.shouldBeLike("
1503     Foo_name: (rawResult, ctx) => {
1504       setupMemory();
1505       spasm_encode_string(rawResult, objects[ctx].name());
1506     },
1507     Foo_fatal_Get: (ctx) => {
1508       setupMemory();
1509       return objects[ctx].fatal;
1510     },
1511     Foo_size: (ctx) => {
1512       setupMemory();
1513       return objects[ctx].size();
1514     },
1515 ");
1516 }
1517 
1518 
1519 @("namespace")
1520 unittest {
1521   auto gen = getGenerator(q{
1522       namespace console {
1523         void clear();
1524       };
1525     });
1526   auto funcs = gen.semantics.toIr();
1527   gen.generateDBindings.shouldBeLike(q{
1528       struct console {
1529         nothrow:
1530       static:
1531         void clear()() {
1532           console_clear();
1533         }
1534       }
1535     });
1536   gen.generateDImports.shouldBeLike(q{
1537       extern (C) void console_clear();
1538     });
1539   gen.generateJsExports.shouldBeLike("
1540     console_clear: () => {
1541       setupMemory();
1542       console.clear();
1543     },
1544 ");
1545 }
1546 
1547 @("callback.sumtype")
1548 unittest {
1549   auto gen = getGenerator(q{
1550       callback OnErrorEventHandlerNonNull = any ((Event or DOMString) event, optional DOMString source, optional unsigned long lineno, optional unsigned long colno, optional any error);
1551     });
1552   gen.generateDBindings.shouldBeLike(q{
1553       alias OnErrorEventHandlerNonNull = Any delegate(SumType!(Event, string), string, uint, uint, Any);
1554     });
1555 }
1556 
1557 @("module.imports.mixin")
1558 unittest {
1559   auto semantics = new Semantics();
1560   auto documentA = WebIDL(q{
1561       interface Foo {
1562       };
1563       Foo includes Bar;
1564     });
1565   auto documentB = WebIDL(q{
1566       interface mixin Bar {
1567         Hup get();
1568       };
1569       interface Hup {
1570       };
1571     });
1572   documentA.successful.shouldBeTrue;
1573   documentB.successful.shouldBeTrue;
1574   auto moduleA = semantics.analyse("a",documentA);
1575   auto moduleB = semantics.analyse("b",documentB);
1576   auto ir = semantics.toIr();
1577   ir.getImports(moduleA).shouldEqual(["import spasm.bindings.b;"]);
1578   ir.getImports(moduleB).shouldEqual([]);
1579 }
1580 
1581 @("module.imports.mixin.indirect")
1582 unittest {
1583   auto semantics = new Semantics();
1584   auto documentA = WebIDL(q{
1585       interface Foo {
1586       };
1587       Foo includes Bar;
1588     });
1589   auto documentB = WebIDL(q{
1590       interface mixin Bar {
1591         Hup get();
1592       };
1593     });
1594   auto documentC = WebIDL(q{
1595       interface Hup {
1596       };
1597     });
1598   documentA.successful.shouldBeTrue;
1599   documentB.successful.shouldBeTrue;
1600   documentC.successful.shouldBeTrue;
1601   auto moduleA = semantics.analyse("a",documentA);
1602   auto moduleB = semantics.analyse("b",documentB);
1603   auto moduleC = semantics.analyse("c",documentC);
1604   auto ir = semantics.toIr();
1605   ir.getImports(moduleA).shouldEqual(["import spasm.bindings.b;","import spasm.bindings.c;"]);
1606   ir.getImports(moduleB).shouldEqual(["import spasm.bindings.c;"]);
1607   ir.generateDImports(moduleB).shouldBeLike("extern (C) Handle Bar_get(Handle);");
1608 }
1609 
1610 @("module.imports.partial")
1611 unittest {
1612   auto semantics = new Semantics();
1613   auto documentA = WebIDL(q{
1614       interface Foo {
1615       };
1616     });
1617   auto documentB = WebIDL(q{
1618       partial interface Foo {
1619         Hup get();
1620       };
1621       interface Hup {
1622       };
1623     });
1624   documentA.successful.shouldBeTrue;
1625   documentB.successful.shouldBeTrue;
1626   auto moduleA = semantics.analyse("a",documentA);
1627   auto moduleB = semantics.analyse("b",documentB);
1628   auto ir = semantics.toIr();
1629   ir.getImports(moduleA).shouldEqual(["import spasm.bindings.b;"]);
1630   ir.getImports(moduleB).shouldEqual([]);
1631 }
1632 
1633 @("module.imports.partial.indirect")
1634 unittest {
1635   auto semantics = new Semantics();
1636   auto documentA = WebIDL(q{
1637       interface Foo {
1638       };
1639     });
1640   auto documentB = WebIDL(q{
1641       partial interface Foo {
1642         Hup get();
1643       };
1644     });
1645   auto documentC = WebIDL(q{
1646       interface Hup {
1647       };
1648     });
1649   documentA.successful.shouldBeTrue;
1650   documentB.successful.shouldBeTrue;
1651   documentC.successful.shouldBeTrue;
1652   auto moduleA = semantics.analyse("a",documentA);
1653   auto moduleB = semantics.analyse("b",documentB);
1654   auto moduleC = semantics.analyse("c",documentC);
1655   auto ir = semantics.toIr();
1656   ir.generateDImports(moduleA).shouldBeLike("extern (C) Handle Foo_get(Handle);");
1657   ir.getImports(moduleA).shouldEqual(["import spasm.bindings.c;"]);
1658   // TODO: we don't need to import c
1659   ir.getImports(moduleB).shouldEqual(["import spasm.bindings.c;"]);
1660 }
1661 
1662 @("any")
1663 unittest {
1664   auto gen = getGenerator(q{
1665       [Exposed=(Window,Worker,Worklet)]
1666       namespace foo {
1667         void log(any data);
1668         any get();
1669       };
1670     });
1671   auto funcs = gen.semantics.toIr();
1672   gen.generateDBindings.shouldBeLike(q{
1673       struct foo {
1674         nothrow:
1675       static:
1676         void log(T0)(scope auto ref T0 data) {
1677           Handle _handle_data = getOrCreateHandle(data);
1678           foo_log(_handle_data);
1679           dropHandle!(T0)(_handle_data);
1680         }
1681         auto get()() {
1682           return Any(foo_get());
1683         }
1684       }
1685     });
1686   gen.generateDImports.shouldBeLike(q{
1687       extern (C) void foo_log(Handle);
1688       extern (C) Handle foo_get();
1689     });
1690   gen.generateJsExports.shouldBeLike("
1691     foo_log: (data) => {
1692       setupMemory();
1693       foo.log(objects[data]);
1694     },
1695     foo_get: () => {
1696       setupMemory();
1697       return addObject(foo.get());
1698     },
1699 ");
1700 }
1701 
1702 @("optional.typedef")
1703 unittest {
1704   auto gen = getGenerator(q{
1705       callback OnErrorEventHandlerNonNull = any ((Event or DOMString) event, optional DOMString source, optional unsigned long lineno, optional unsigned long colno, optional any error);
1706       typedef OnErrorEventHandlerNonNull? OnErrorEventHandler;
1707       interface WorkerGlobalScope : EventTarget {
1708         attribute OnErrorEventHandler onerror;
1709       };
1710     });
1711   gen.generateDBindings.shouldBeLike(q{
1712       alias OnErrorEventHandler = Optional!(OnErrorEventHandlerNonNull);
1713       alias OnErrorEventHandlerNonNull = Any delegate(SumType!(Event, string), string, uint, uint, Any);
1714       struct WorkerGlobalScope {
1715         nothrow:
1716         EventTarget _parent;
1717         alias _parent this;
1718         this(Handle h) {
1719           _parent = .EventTarget(h);
1720         }
1721         void onerror(T0)(scope auto ref Optional!(T0) onerror) if (isTOrPointer!(T0, OnErrorEventHandler)) {
1722           WorkerGlobalScope_onerror_Set(this._parent, !onerror.empty, onerror.front);
1723         }
1724         auto onerror()() {
1725           return WorkerGlobalScope_onerror_Get(this._parent);
1726         }
1727       }
1728     });
1729   gen.generateDImports.shouldBeLike(q{
1730       extern (C) void WorkerGlobalScope_onerror_Set(Handle, bool, OnErrorEventHandlerNonNull);
1731       extern (C) OnErrorEventHandler WorkerGlobalScope_onerror_Get(Handle);
1732     });
1733   gen.generateJsExports.shouldBeLike("
1734 WorkerGlobalScope_onerror_Set: (ctx, onerrorDefined, onerrorCtx, onerrorPtr) => {
1735   setupMemory();
1736   objects[ctx].onerror = onerrorDefined ? (event, source, lineno, colno, error)=>{spasm_encode_union2_Event_string(0, event);spasm_encode_string(12, source);encode_handle(20, error);spasm_indirect_function_get(onerrorPtr)(24, onerrorCtx, 0, 12, lineno, colno, 20); return spasm_decode_Handle(24)} : undefined;
1737 },
1738 WorkerGlobalScope_onerror_Get: (rawResult, ctx) => {
1739   setupMemory();
1740   spasm_encode_optional_Handle(rawResult, objects[ctx].onerror);
1741 },
1742 ");
1743 }
1744 
1745 @("sumtype.nested")
1746 unittest {
1747   auto gen = getGenerator(q{
1748       interface XMLHttpRequest : XMLHttpRequestEventTarget {
1749         void send(optional (Document or BodyInit)? body = null);
1750       };
1751       typedef (Blob or BufferSource or FormData or URLSearchParams or ReadableStream or USVString) BodyInit;
1752     });
1753   gen.generateDBindings.shouldBeLike(q{
1754       alias BodyInit = SumType!(Blob, BufferSource, FormData, URLSearchParams, ReadableStream, string);
1755       struct XMLHttpRequest {
1756         nothrow:
1757         XMLHttpRequestEventTarget _parent;
1758         alias _parent this;
1759         this(Handle h) {
1760           _parent = .XMLHttpRequestEventTarget(h);
1761         }
1762         void send(T0)(scope auto ref Optional!(T0) body_ /* = no!(SumType!(Document, BodyInit)) */) if (isTOrPointer!(T0, SumType!(Document, BodyInit))) {
1763           XMLHttpRequest_send(this._parent, !body_.empty, *body_.frontRef);
1764         }
1765         void send()() {
1766           XMLHttpRequest_send_0(this._parent);
1767         }
1768       }
1769     });
1770   gen.generateDImports.shouldBeLike(q{
1771       extern (C) void XMLHttpRequest_send(Handle, bool, scope ref SumType!(Document, BodyInit));
1772       extern (C) void XMLHttpRequest_send_0(Handle);
1773     });
1774   gen.generateJsExports.shouldBeLike("
1775 XMLHttpRequest_send: (ctx, bodyDefined, body) => {
1776   setupMemory();
1777   objects[ctx].send(bodyDefined ? spasm_decode_union2_Document_BodyInit(body) : undefined);
1778 },
1779 XMLHttpRequest_send_0: (ctx) => {
1780   setupMemory();
1781   objects[ctx].send();
1782 },
1783 ");
1784 }
1785 
1786 @("inheritance.mixin")
1787 unittest {
1788   auto gen = getGenerator(q{
1789       interface mixin GenericTransformStream {
1790         readonly attribute WritableStream writable;
1791       };
1792       interface TextDecoderStream : Foo {
1793       };
1794       TextDecoderStream includes GenericTransformStream;
1795     });
1796   gen.generateDBindings.shouldBeLike(q{
1797       struct TextDecoderStream {
1798         nothrow:
1799         Foo _parent;
1800         alias _parent this;
1801         this(Handle h) {
1802           _parent = .Foo(h);
1803         }
1804         auto writable()() {
1805           return WritableStream(GenericTransformStream_writable_Get(this._parent));
1806         }
1807       }
1808     });
1809   gen.generateDImports.shouldBeLike(q{
1810       extern (C) Handle GenericTransformStream_writable_Get(Handle);
1811     });
1812   gen.generateJsExports.shouldBeLike("
1813     GenericTransformStream_writable_Get: (ctx) => {
1814       setupMemory();
1815       return addObject(objects[ctx].writable);
1816     },
1817 ");
1818 }
1819 
1820 @("exposed.constructor.overloads")
1821 unittest {
1822   auto gen = getGenerator(q{
1823       [Constructor(unsigned long sw, unsigned long sh),
1824        Constructor(Uint8ClampedArray data, unsigned long sw, optional unsigned long sh),
1825        Exposed=(Window),
1826        Serializable]
1827       interface ImageData {
1828       };
1829       interface Window {
1830         void stuff((ImageData or string) s);
1831       };
1832     });
1833   gen.generateDBindings.shouldBeLike(q{
1834       struct ImageData {
1835         nothrow:
1836         JsHandle handle;
1837         alias handle this;
1838         this(Handle h) {
1839           this.handle = JsHandle(h);
1840         }
1841       }
1842       struct Window {
1843         nothrow:
1844         JsHandle handle;
1845         alias handle this;
1846         this(Handle h) {
1847           this.handle = JsHandle(h);
1848         }
1849         void stuff()(scope ref SumType!(.ImageData, string) s) {
1850           Window_stuff(this.handle, s);
1851         }
1852         auto ImageData()(uint sw, uint sh) {
1853           return .ImageData(Window_ImageData__uint_uint(this.handle, sw, sh));
1854         }
1855         auto ImageData()(scope ref Uint8ClampedArray data, uint sw, uint sh) {
1856           return .ImageData(Window_ImageData__Handle_uint_uint(this.handle, data.handle, sw, sh));
1857         }
1858       }
1859     });
1860   gen.generateDImports.shouldBeLike(q{
1861       extern (C) void Window_stuff(Handle, scope ref SumType!(ImageData, string));
1862       extern (C) Handle Window_ImageData__uint_uint(Handle, uint, uint);
1863       extern (C) Handle Window_ImageData__Handle_uint_uint(Handle, Handle, uint, uint);
1864     });
1865   gen.generateJsExports.shouldBeLike("
1866 Window_stuff: (ctx, s) => {
1867   setupMemory();
1868   objects[ctx].stuff(spasm_decode_union2_ImageData_string(s));
1869 },
1870 Window_ImageData__uint_uint: (ctx, sw, sh) => {
1871   setupMemory();
1872   return addObject(new objects[ctx].ImageData(sw, sh));
1873 },
1874 Window_ImageData__Handle_uint_uint: (ctx, data, sw, sh) => {
1875   setupMemory();
1876   return addObject(new objects[ctx].ImageData(objects[data], sw, sh));
1877 },
1878 ");
1879   gen.generateJsDecoders.shouldBeLike("
1880 spasm_decode_Handle = decode_handle,
1881 spasm_decode_union2_ImageData_string = (ptr)=>{
1882   if (getUInt(ptr) == 0) {
1883     return spasm_decode_Handle(ptr+4);
1884   } else if (getUInt(ptr) == 1) {
1885     return spasm_decode_Handle(ptr+4);
1886   }
1887 }");
1888 }
1889 
1890 @("mixin.partial")
1891 unittest {
1892   auto gen = getGenerator(q{
1893       callback EventHandlerNonNull = any (Event event);
1894       typedef EventHandlerNonNull EventHandler;
1895       interface mixin GlobalEventHandlers {
1896         attribute EventHandler onabort;
1897       };
1898       partial interface GlobalEventHandlers {
1899         attribute EventHandler ongotpointercapture;
1900       };
1901       partial interface GlobalEventHandlers {
1902         attribute EventHandler ontouchstart;
1903       };
1904       interface Window {
1905       };
1906       Window includes GlobalEventHandlers;
1907     });
1908   gen.generateDBindings.shouldBeLike(q{
1909       alias EventHandler = EventHandlerNonNull;
1910       alias EventHandlerNonNull = Any delegate(Event);
1911       struct Window {
1912         nothrow:
1913         JsHandle handle;
1914         alias handle this;
1915         this(Handle h) {
1916           this.handle = JsHandle(h);
1917         }
1918         void onabort()(EventHandler onabort) {
1919           GlobalEventHandlers_onabort_Set(this.handle, onabort);
1920         }
1921         auto onabort()() {
1922           return GlobalEventHandlers_onabort_Get(this.handle);
1923         }
1924         void ongotpointercapture()(EventHandler ongotpointercapture) {
1925           GlobalEventHandlers_ongotpointercapture_Set(this.handle, ongotpointercapture);
1926         }
1927         auto ongotpointercapture()() {
1928           return GlobalEventHandlers_ongotpointercapture_Get(this.handle);
1929         }
1930         void ontouchstart()(EventHandler ontouchstart) {
1931           GlobalEventHandlers_ontouchstart_Set(this.handle, ontouchstart);
1932         }
1933         auto ontouchstart()() {
1934           return GlobalEventHandlers_ontouchstart_Get(this.handle);
1935         }
1936       }
1937     });
1938   gen.generateDImports.shouldBeLike(q{
1939       extern (C) void GlobalEventHandlers_onabort_Set(Handle, EventHandler);
1940       extern (C) EventHandler GlobalEventHandlers_onabort_Get(Handle);
1941       extern (C) void GlobalEventHandlers_ongotpointercapture_Set(Handle, EventHandler);
1942       extern (C) EventHandler GlobalEventHandlers_ongotpointercapture_Get(Handle);
1943       extern (C) void GlobalEventHandlers_ontouchstart_Set(Handle, EventHandler);
1944       extern (C) EventHandler GlobalEventHandlers_ontouchstart_Get(Handle);    });
1945   gen.generateJsExports.shouldBeLike("
1946 GlobalEventHandlers_onabort_Set: (ctx, onabortCtx, onabortPtr) => {
1947   setupMemory();
1948   objects[ctx].onabort = (event)=>{encode_handle(0, event);spasm_indirect_function_get(onabortPtr)(4, onabortCtx, 0); return spasm_decode_Handle(4)};
1949 },
1950 GlobalEventHandlers_onabort_Get: (ctx) => {
1951   setupMemory();
1952   return objects[ctx].onabort;
1953 },
1954 GlobalEventHandlers_ongotpointercapture_Set: (ctx, ongotpointercaptureCtx, ongotpointercapturePtr) => {
1955   setupMemory();
1956   objects[ctx].ongotpointercapture = (event)=>{encode_handle(0, event);spasm_indirect_function_get(ongotpointercapturePtr)(4, ongotpointercaptureCtx, 0); return spasm_decode_Handle(4)};
1957 },
1958 GlobalEventHandlers_ongotpointercapture_Get: (ctx) => {
1959   setupMemory();
1960   return objects[ctx].ongotpointercapture;
1961 },
1962 GlobalEventHandlers_ontouchstart_Set: (ctx, ontouchstartCtx, ontouchstartPtr) => {
1963   setupMemory();
1964   objects[ctx].ontouchstart = (event)=>{encode_handle(0, event);spasm_indirect_function_get(ontouchstartPtr)(4, ontouchstartCtx, 0); return spasm_decode_Handle(4)};
1965 },
1966 GlobalEventHandlers_ontouchstart_Get: (ctx) => {
1967   setupMemory();
1968   return objects[ctx].ontouchstart;
1969 },
1970 ");
1971  gen.generateJsDecoders.shouldBeLike("spasm_decode_Handle = decode_handle");
1972 }
1973 
1974 @("decode.sequence")
1975 unittest {
1976   auto gen = getGenerator(q{
1977       [Constructor(USVString url, optional (DOMString or sequence<DOMString>) protocols = []), Exposed=(Window)]
1978       interface WebSocket : EventTarget {
1979       };
1980       interface Window {
1981       };
1982     });
1983   gen.generateDBindings.shouldBeLike(q{
1984       struct WebSocket {
1985         nothrow:
1986         EventTarget _parent;
1987         alias _parent this;
1988         this(Handle h) {
1989           _parent = .EventTarget(h);
1990         }
1991       }
1992       struct Window {
1993         nothrow:
1994         JsHandle handle;
1995         alias handle this;
1996         this(Handle h) {
1997           this.handle = JsHandle(h);
1998         }
1999         auto WebSocket()(string url, scope ref SumType!(string, Sequence!(string)) protocols /* = [] */) {
2000           return .WebSocket(Window_WebSocket(this.handle, url, protocols));
2001         }
2002       }
2003     });
2004   gen.generateDImports.shouldBeLike(q{
2005       extern (C) Handle Window_WebSocket(Handle, string, scope ref SumType!(string, Sequence!(string)));
2006     });
2007   gen.generateJsExports.shouldBeLike("
2008 Window_WebSocket: (ctx, urlLen, urlPtr, protocols) => {
2009   setupMemory();
2010   return addObject(new objects[ctx].WebSocket(spasm_decode_string(urlLen, urlPtr), spasm_decode_union2_string_sequence(protocols)));
2011 },
2012 ");
2013   gen.generateJsDecoders.should == "spasm_decode_sequence = decode_handle,
2014 spasm_decode_union2_string_sequence = (ptr)=>{
2015   if (getUInt(ptr) == 0) {
2016     return spasm_decode_string(ptr+4);
2017   } else if (getUInt(ptr) == 1) {
2018     return spasm_decode_sequence(ptr+4);
2019   }
2020 }";
2021 }
2022 
2023 @("abi.callback.return")
2024 unittest {
2025   auto gen = getGenerator(q{
2026       callback AnyCallback = any (Event event);
2027       callback IntCallback = int (Event event);
2028       callback StringCallback = string (Event event);
2029       callback InterfaceCallback = WebSocket (Event event);
2030       callback VoidCallback = void (Event event);
2031       interface WebSocket {};
2032       interface Window {
2033         attribute AnyCallback onany;
2034         attribute IntCallback onint;
2035         attribute stringCallback onstring;
2036         attribute interfaceCallback oninterface;
2037         attribute VoidCallback onvoid;
2038       };
2039     });
2040   gen.generateJsExports.shouldBeLike("
2041 Window_onany_Set: (ctx, onanyCtx, onanyPtr) => {
2042   setupMemory();
2043   objects[ctx].onany = (event)=>{encode_handle(0, event);spasm_indirect_function_get(onanyPtr)(4, onanyCtx, 0); return spasm_decode_Handle(4)};
2044 },
2045 Window_onany_Get: (ctx) => {
2046   setupMemory();
2047   return objects[ctx].onany;
2048 },
2049 Window_onint_Set: (ctx, onintCtx, onintPtr) => {
2050   setupMemory();
2051   objects[ctx].onint = (event)=>{encode_handle(0, event);return spasm_indirect_function_get(onintPtr)(onintCtx, 0)};
2052 },
2053 Window_onint_Get: (ctx) => {
2054   setupMemory();
2055   return objects[ctx].onint;
2056 },
2057 Window_onstring_Set: (ctx, onstring) => {
2058   setupMemory();
2059   objects[ctx].onstring = objects[onstring];
2060 },
2061 Window_onstring_Get: (ctx) => {
2062   setupMemory();
2063   return addObject(objects[ctx].onstring);
2064 },
2065 Window_oninterface_Set: (ctx, oninterface) => {
2066   setupMemory();
2067   objects[ctx].oninterface = objects[oninterface];
2068 },
2069 Window_oninterface_Get: (ctx) => {
2070   setupMemory();
2071   return addObject(objects[ctx].oninterface);
2072 },
2073 Window_onvoid_Set: (ctx, onvoidCtx, onvoidPtr) => {
2074   setupMemory();
2075   objects[ctx].onvoid = (event)=>{encode_handle(0, event);spasm_indirect_function_get(onvoidPtr)(onvoidCtx, 0)};
2076 },
2077 Window_onvoid_Get: (ctx) => {
2078   setupMemory();
2079   return objects[ctx].onvoid;
2080 },
2081 ");
2082 }