Moved the test directory.
[jquery.git] / build / js / lib / Test / Builder.js
1 // # $Id: Kinetic.pm 1493 2005-04-07 19:20:18Z theory $
2
3 // Set up namespace.
4 if (typeof self != 'undefined') {
5     // Browser
6     if (typeof Test == 'undefined') Test = {PLATFORM: 'browser'};
7     else Test.PLATFORM = 'browser';
8 } else if (typeof _global != 'undefined') {
9     //Director
10     if (typeof _global.Test == "undefined") _global.Test = {PLATFORM: 'director'};
11     else _global.Test.PLATFORM = 'director';
12 } else {
13     throw new Error("Test.More does not support your platform");
14 }
15
16 // Constructor.
17 Test.Builder = function () {
18     if (!Test.Builder.Test) {
19         Test.Builder.Test = this.reset();
20         Test.Builder.Instances.push(this);
21     }
22     return Test.Builder.Test;
23 };
24
25 // Static variables.
26 Test.Builder.VERSION = '0.11';
27 Test.Builder.Instances = [];
28 Test.Builder.lineEndingRx = /\r?\n|\r/g;
29 Test.Builder.StringOps = {
30     eq: '==',
31     ne: '!=',
32     lt: '<',
33     gt: '>',
34     ge: '>=',
35     le: '<='
36 };
37
38 // Stoopid IE.
39 Test.Builder.LF = typeof document != "undefined"
40                   && typeof document.all != "undefined"
41   ? "\r"
42   : "\n";
43
44 // Static methods.
45 Test.Builder.die = function (msg) {
46     throw new Error(msg);
47 };
48
49 Test.Builder._whoa = function (check, desc) {
50     if (!check) return;
51     Test.Builder.die("WHOA! " + desc + Test.Builder.LF +
52                      + "This should never happen! Please contact the author "
53                      + "immediately!");
54 };
55
56 Test.Builder.typeOf = function (object) {
57     var c = Object.prototype.toString.apply(object);
58     var name = c.substring(8, c.length - 1);
59     if (name != 'Object') return name;
60     // It may be a non-core class. Try to extract the class name from
61     // the constructor function. This may not work in all implementations.
62     if (/function ([^(\s]+)/.test(Function.toString.call(object.constructor))) {
63         return RegExp.$1;
64     }
65     // No idea. :-(
66     return name;
67 };
68
69 // Instance methods.
70 Test.Builder.create = function () {
71     var test = Test.Builder.Test;
72     Test.Builder.Test = null;
73     var ret = new Test.Builder();
74     Test.Builder.Test = test;
75     return ret.reset();
76 };
77
78 Test.Builder.prototype.reset = function () {
79     this.TestDied      = false;
80     this.HavePlan      = false;
81     this.NoPlan        = false;
82     this.CurrTest      = 0;
83     this.ExpectedTests = 0;
84     this.UseNums       = true;
85     this.NoHeader      = false;
86     this.NoEnding      = false;
87     this.TestResults   = [];
88     this.ToDo          = [];
89     this.Buffer       = [];
90     this.asyncs        = [0];
91     this.asyncID       = 0;
92     return this._setupOutput();
93 };
94
95 Test.Builder.prototype._print = function (msg) {
96     this.output().call(this, msg);
97 };
98
99 Test.Builder.prototype.warn = function (msg) {
100     this.warnOutput().apply(this, arguments);
101 };
102
103 Test.Builder.prototype.plan = function (arg) {
104     if (!arg) return;
105     //if (this.HavePlan) Test.Builder.die("You tried to plan twice!");
106                 this.ExpectedTests = 0;
107                 this.HavePlan      = false;
108     this.NoPlan        = false;
109
110     if (!(arg instanceof Object))
111         Test.Builder.die("plan() doesn't understand " + arg);
112     for (var cmd in arg) {
113         if (cmd == 'tests') {
114             if (arg[cmd] == null) {
115                 TestBulder.die(
116                     "Got an undefined number of tests. Looks like you tried to "
117                     + "say how many tests you plan to run but made a mistake."
118                     + Test.Builder.LF
119                 );
120             } else if (!arg[cmd]) {
121                 Test.Builder.die(
122                     "You said to run 0 tests! You've got to run something."
123                     + Test.Builder.LF
124                 );
125             } else {
126                 this.expectedTests(arg[cmd]);
127             }
128         } else if (cmd == 'skipAll') {
129             this.skipAll(arg[cmd]);
130         } else if (cmd == 'noPlan' && arg[cmd]) {
131             this.noPlan();
132         } else {
133             Test.Builder.die("plan() doesn't understand "
134                              + cmd + (arg[cmd] ? (" " + arg[cmd]) : ''));
135         }
136     }
137 };
138
139 Test.Builder.prototype.expectedTests = function (max) {
140     if (max) {
141         if (isNaN(max)) {
142             Test.Builder.die(
143                 "Number of tests must be a postive integer. You gave it '"
144                 + max + "'." + Test.Builder.LF
145             );
146         }
147
148         this.ExpectedTests = max.valueOf();
149         this.HavePlan       = 1;
150         if (!this.noHeader()) this._print("1.." + max + Test.Builder.LF);
151     }
152     return this.ExpectedTests;
153 };
154
155 Test.Builder.prototype.noPlan = function () {
156     this.NoPlan   = 1;
157     this.HavePlan = 1;
158 };
159
160 Test.Builder.prototype.hasPlan = function () {
161     if (this.ExpectedTests) return this.ExpectedTests;
162     if (this.NoPlan) return 'noPlan';
163 };
164
165 Test.Builder.prototype.skipAll = function (reason) {
166     var out = "1..0";
167     if (reason) out += " # Skip " + reason;
168     out += Test.Builder.LF;
169     this.SkipAll = 1;
170     if (!this.noHeader()) this._print(out);
171     // Just throw and catch an exception.
172     window.onerror = function () { return true; }
173     throw new Error("__SKIP_ALL__");
174 };
175
176 Test.Builder.prototype.ok = function (test, desc) {
177     // test might contain an object that we don't want to accidentally
178     // store, so we turn it into a boolean.
179     test = !!test;
180
181     if (!this.HavePlan)
182         Test.Builder.die("You tried to run a test without a plan! Gotta have a plan.");
183
184     // I don't think we need to worry about threading in JavaScript.
185     this.CurrTest++;
186
187     // In case desc is a string overloaded object, force it to stringify.
188     if (desc) desc = desc.toString();
189
190     var startsNumber
191     if (desc != null && /^[\d\s]+$/.test(desc)) {
192         this.diag( "Your test description is '" + desc + "'. You shouldn't use",
193                    Test.Builder.LF,
194                    "numbers for your test names. Very confusing.");
195     }
196
197     var todo = this._todo();
198     // I don't think we need to worry about result beeing shared between
199     // threads.
200     var out = '';
201     var result = {};
202
203     if (test) {
204         result.ok        = true;
205         result.actual_ok = test;
206     } else {
207         out += 'not ';
208         result.ok        = todo ? true : false;
209         result.actual_ok = false;
210     }
211
212     out += 'ok';
213     if (this.useNumbers) out += ' ' + this.CurrTest;
214
215     if (desc == null) {
216         result.desc = '';
217     } else {
218         desc = desc.replace(Test.Builder.lineEndingRx, Test.Builder.LF + "# ");
219         // XXX Does this matter since we don't have a TestHarness?
220         desc.split('#').join('\\#'); // # # in a desc can confuse TestHarness.
221         out += ' - ' + desc;
222         result.desc = desc;
223     }
224
225     if (todo) {
226         todo = todo.replace(Test.Builder.lineEndingRx, Test.Builder.LF + "# ");
227         out += " # TODO " + todo;
228         result.reason = todo;
229         result.type   = 'todo';
230     } else {
231         result.reason = '';
232         result.type   = '';
233     }
234
235     this.TestResults[this.CurrTest - 1] = result;
236
237     out += Test.Builder.LF;
238     this._print(out);
239
240     if (!test) {
241         var msg = todo ? "Failed (TODO)" : "Failed";
242         // XXX Hrm, do I need this?
243         //$self_print_diag(Test.Builder.LF) if $ENV{HARNESS_ACTIVE};
244         this.diag("    " + msg + " test");
245     }
246     result.output = this.Buffer.splice(0).join('');
247     return test;
248 };
249
250 Test.Builder.prototype.isEq = function (got, expect, desc) {
251     if (got == null || expect == null) {
252         // undefined only matches undefined and nothing else
253         return this.isUndef(got, '==', expect, desc);
254     }
255     return this.cmpOK(got, '==', expect, desc);
256 };
257
258 Test.Builder.prototype.isNum = function (got, expect, desc) {
259     if (got == null || expect == null) {
260         // undefined only matches undefined and nothing else
261         return this.isUndef(got, '==', expect, desc);
262     }
263     return this.cmpOK(Number(got), '==', Number(expect), desc);
264 };
265
266 Test.Builder.prototype.isntEq = function (got, dontExpect, desc) {
267     if (got == null || dontExpect == null) {
268         // undefined only matches undefined and nothing else
269         return this.isUndef(got, '!=', dontExpect, desc);
270     }
271     return this.cmpOK(got, '!=', dontExpect, desc);
272 };
273
274 Test.Builder.prototype.isntNum = function (got, dontExpect, desc) {
275     if (got == null || dontExpect == null) {
276         // undefined only matches undefined and nothing else
277         return this.isUndef(got, '!=', dontExpect, desc);
278     }
279     return this.cmpOK(Number(got), '!=', Number(dontExpect), desc);
280 };
281
282 Test.Builder.prototype.like = function (val, regex, desc) {
283     return this._regexOK(val, regex, '=~', desc);
284 };
285
286 Test.Builder.prototype.unlike = function (val, regex, desc) {
287     return this._regexOK(val, regex, '!~', desc);
288 };
289
290 Test.Builder.prototype._regexOK = function (val, regex, cmp, desc) {
291     // Create a regex object.
292     var type = Test.Builder.typeOf(regex);
293     var ok;
294     if (type.toLowerCase() == 'string') {
295         // Create a regex object.
296         regex = new RegExp(regex);
297     } else {
298         if (type != 'RegExp') {
299             ok = this.ok(false, desc);
300             this.diag("'" + regex + "' doesn't look much like a regex to me.");
301             return ok;
302         }
303     }
304
305     if (val == null || typeof val != 'string') {
306         if (cmp == '=~') {
307             // The test fails.
308             ok = this.ok(false, desc);
309             this._diagLike(val, regex, cmp);
310         } else {
311             // undefined matches nothing (unlike in Perl, where undef =~ //).
312             ok = this.ok(true, desc);
313         }
314         return ok;
315     }
316
317     // Use val.match() instead of regex.test() in case they've set g.
318     var test = val.match(regex);
319     if (cmp == '!~') test = !test;
320     ok = this.ok(test, desc);
321     if (!ok) this._diagLike(val, regex, cmp);
322     return ok;
323 };
324
325 Test.Builder.prototype._diagLike = function (val, regex, cmp) {
326     var match = cmp == '=~' ? "doesn't match" : "      matches";
327     return this.diag(
328         "                  '" + val + "" + Test.Builder.LF +
329         "    " + match + " /" + regex.source + "/"
330     );
331 };
332
333 Test.Builder.prototype.cmpOK = function (got, op, expect, desc) {
334
335     var test;
336     if (Test.Builder.StringOps[op]) {
337         // Force string context.
338         test = eval("got.toString() " + Test.Builder.StringOps[op] + " expect.toString()");
339     } else {
340         test = eval("got " + op + " expect");
341     }
342
343     var ok = this.ok(test, desc);
344     if (!ok) {
345         if (/^(eq|==)$/.test(op)) {
346             this._isDiag(got, op, expect);
347         } else {
348             this._cmpDiag(got, op, expect);
349         }
350     }
351     return ok;
352 };
353
354 Test.Builder.prototype._cmpDiag = function (got, op, expect) {
355     if (got != null) got = "'" + got.toString() + "'";
356     if (expect != null) expect = "'" + expect.toString() + "'";
357     return this.diag("    " + got + Test.Builder.LF + "        " + op
358                      + Test.Builder.LF + "    " + expect);
359 };
360
361 Test.Builder.prototype._isDiag = function (got, op, expect) {
362     var args = [got, expect];
363     for (var i = 0; i < args.length; i++) {
364         if (args[i] != null) {
365             args[i] = op == 'eq' ? "'" + args[i].toString() + "'" : args[i].valueOf();
366         }
367     }
368
369     return this.diag(
370         "         got: " + args[0] + Test.Builder.LF +
371         "    expected: " + args[1] + Test.Builder.LF
372     );
373 };
374
375 Test.Builder.prototype.BAILOUT = function (reason) {
376     this._print("Bail out! " + reason);
377     // Just throw and catch an exception.
378     window.onerror = function () {
379         // XXX Do something to tell TestHarness it was a bailout?
380         return true;
381     }
382     throw new Error("__BAILOUT__");
383 };
384
385 Test.Builder.prototype.skip = function (why) {
386     if (!this.HavePlan)
387         Test.Builder.die("You tried to run a test without a plan! Gotta have a plan.");
388
389     // In case desc is a string overloaded object, force it to stringify.
390     if (why) why = why.toString().replace(Test.Builder.lineEndingRx,
391                                           Test.Builder.LF+ "# ");
392
393     this.CurrTest++;
394     this.TestResults[this.CurrTest - 1] = {
395         ok:        true,
396         actual_ok: true,
397         desc:      '',
398         type:      'skip',
399         reason:    why
400     };
401
402     var out = "ok";
403     if (this.useNumbers) out += ' ' + this.CurrTest;
404     out    += " # skip " + why + Test.Builder.LF;
405     this._print(out);
406     this.TestResults[this.CurrTest - 1].output =
407       this.Buffer.splice(0).join('');
408     return true;
409 };
410
411 Test.Builder.prototype.todoSkip = function (why) {
412     if (!this.HavePlan)
413         Test.Builder.die("You tried to run a test without a plan! Gotta have a plan.");
414
415     // In case desc is a string overloaded object, force it to stringify.
416     if (why) why = why.toString().replace(Test.Builder.lineEndingRx,
417                                           Test.Builder.LF + "# ");
418     
419
420     this.CurrTest++;
421     this.TestResults[this.CurrTest - 1] = {
422         ok:        true,
423         actual_ok: false,
424         desc:      '',
425         type:      'todo_skip',
426         reason:    why
427     };
428
429     var out = "not ok";
430     if (this.useNumbers) out += ' ' + this.CurrTest;
431     out    += " # TODO & SKIP " + why + Test.Builder.LF;
432     this._print(out);
433     this.TestResults[this.CurrTest - 1].output =
434       this.Buffer.splice(0).join('');
435     return true;
436 };
437
438 Test.Builder.prototype.skipRest = function (reason) {
439     var out = "# Skip";
440     if (reason) out += " " + reason;
441     out += Test.Builder.LF;
442     if (this.NoPlan) this.skip(reason);
443     else {
444         for (var i = this.CurrTest; i < this.ExpectedTests; i++) {
445             this.skip(reason);
446         }
447     }
448     // Just throw and catch an exception.
449     window.onerror = function () { return true; }
450     throw new Error("__SKIP_REST__");
451 };
452
453 Test.Builder.prototype.useNumbers = function (useNums) {
454     if (useNums != null) this.UseNums = useNums;
455     return this.UseNums;
456 };
457
458 Test.Builder.prototype.noHeader = function (noHeader) {
459     if (noHeader != null) this.NoHeader = !!noHeader;
460     return this.NoHeader;
461 };
462
463 Test.Builder.prototype.noEnding = function (noEnding) {
464     if (noEnding != null) this.NoEnding = !!noEnding;
465     return this.NoEnding;
466 };
467
468 Test.Builder.prototype.diag = function () {
469     if (!arguments.length) return;
470
471     var msg = '# ';
472     // Join each agument and escape each line with a #.
473     for (i = 0; i < arguments.length; i++) {
474         // Replace any newlines.
475         msg += arguments[i].toString().replace(Test.Builder.lineEndingRx,
476                                                Test.Builder.LF + "# ");
477     }
478
479     // Append a new line to the end of the message if there isn't one.
480     if (!(new RegExp(Test.Builder.LF + '$').test(msg)))
481         msg += Test.Builder.LF;
482     // Append the diag message to the most recent result.
483     return this._printDiag(msg);
484 };
485
486 Test.Builder.prototype._printDiag = function () {
487     var fn = this.todo() ? this.todoOutput() : this.failureOutput();
488     fn.apply(this, arguments);
489     return false;
490 };
491
492 Test.Builder.prototype.output = function (fn) {
493     if (fn != null) {
494         var buffer = this.Buffer;
495         this.Output = function (msg) { buffer.push(msg); fn(msg) };
496     }
497     return this.Output;
498 };
499
500 Test.Builder.prototype.failureOutput = function (fn) {
501     if (fn != null) {
502         var buffer = this.Buffer;
503         this.FailureOutput = function (msg) { buffer.push(msg); fn(msg) };
504     }
505     return this.FailureOutput;
506 };
507
508 Test.Builder.prototype.todoOutput = function (fn) {
509     if (fn != null) {
510         var buffer = this.Buffer;
511         this.TodoOutput = function (msg) { buffer.push(msg); fn(msg) };
512     }
513     return this.TodoOutput;
514 };
515
516 Test.Builder.prototype.endOutput = function (fn) {
517     if (fn != null) {
518         var buffer = this.Buffer;
519         this.EndOutput = function (msg) { buffer.push(msg); fn(msg) };
520     }
521     return this.EndOutput;
522 };
523
524 Test.Builder.prototype.warnOutput = function (fn) {
525     if (fn != null) {
526         var buffer = this.Buffer;
527         this.WarnOutput = function (msg) { buffer.push(msg); fn(msg) };
528     }
529     return this.WarnOutput;
530 };
531
532 Test.Builder.prototype._setupOutput = function () {
533     if (Test.PLATFORM == 'browser') {
534         var writer = function (msg) {
535             // I'm sure that there must be a more efficient way to do this,
536             // but if I store the node in a variable outside of this function
537             // and refer to it via the closure, then things don't work right
538             // --the order of output can become all screwed up (see
539             // buffer.html).  I have no idea why this is.
540             var node = document.getElementById("test");
541             if (node) {
542                 // This approach is neater, but causes buffering problems when
543                 // mixed with document.write. See tests/buffer.html.
544                 //node.appendChild(document.createTextNode(msg));
545                 //return;
546                 for (var i = 0; i < node.childNodes.length; i++) {
547                     if (node.childNodes[i].nodeType == 3 /* Text Node */) {
548                         // Append to the node and scroll down.
549                         node.childNodes[i].appendData(msg);
550                         window.scrollTo(0, document.body.offsetHeight
551                                         || document.body.scrollHeight);
552                         return;
553                     }
554                 }
555
556                 // If there was no text node, add one.
557                 node.appendChild(document.createTextNode(msg));
558                 window.scrollTo(0, document.body.offsetHeight
559                                 || document.body.scrollHeight);
560                 return;
561             }
562
563             // Default to the normal write and scroll down...
564             document.write(msg);
565             window.scrollTo(0, document.body.offsetHeight
566                             || document.body.scrollHeight);
567         };
568
569         this.output(writer);
570         this.failureOutput(writer);
571         this.todoOutput(writer);
572         this.endOutput(writer);
573
574         if (window) {
575             if (window.alert.apply) this.warnOutput(window.alert, window);
576             else this.warnOutput(function (msg) { window.alert(msg) });
577         }
578
579     } else if (Test.PLATFORM == 'director') {
580         // Macromedia-Adobe:Director MX 2004 Support
581         // XXX Is _player a definitive enough object?
582         // There may be an even more explicitly Director object.
583         this.output(trace);       
584         this.failureOutput(trace);
585         this.todoOutput(trace);
586         this.warnOutput(trace);
587     }
588
589     return this;
590 };
591
592 Test.Builder.prototype.currentTest = function (num) {
593     if (num == null) return this.CurrTest;
594
595     if (!this.HavePlan)
596         Test.Builder.die("Can't change the current test number without a plan!");
597     this.CurrTest = num;
598     if (num > this.TestResults.length ) {
599         var reason = 'incrementing test number';
600         for (i = this.TestResults.length; i < num; i++) {
601             this.TestResults[i] = {
602                 ok:        true, 
603                 actual_ok: null,
604                 reason:    reason,
605                 type:      'unknown', 
606                 name:      null,
607                 output:    'ok - ' + reason + Test.Builder.LF
608             };
609         }
610     } else if (num < this.TestResults.length) {
611         // IE requires the second argument to truncate the array.
612         this.TestResults.splice(num, this.TestResults.length);
613     }
614     return this.CurrTest;
615 };
616
617 Test.Builder.prototype.summary = function () {
618     var results = new Array(this.TestResults.length);
619     for (var i = 0; i < this.TestResults.length; i++) {
620         results[i] = this.TestResults[i]['ok'];
621     }
622     return results
623 };
624
625 Test.Builder.prototype.details = function () {
626     return this.TestResults;
627 };
628
629 Test.Builder.prototype.todo = function (why, howMany) {
630     if (howMany) this.ToDo = [why, howMany];
631     return this.ToDo[1];
632 };
633
634 Test.Builder.prototype._todo = function () {
635     if (this.ToDo[1]) {
636         if (this.ToDo[1]--) return this.ToDo[0];
637         this.ToDo = [];
638     }
639     return false;
640 };
641
642 Test.Builder.prototype._sanity_check = function () {
643     Test.Builder._whoa(
644         this.CurrTest < 0,
645         'Says here you ran a negative number of tests!'
646     );
647
648     Test.Builder._whoa(
649         !this.HavePlan && this.CurrTest, 
650         'Somehow your tests ran without a plan!'
651     );
652
653     Test.Builder._whoa(
654         this.CurrTest != this.TestResults.length,
655         'Somehow you got a different number of results than tests ran!'
656     );
657 };
658
659 Test.Builder.prototype._notifyHarness = function () {
660     // Special treatment for the browser harness.
661     if (typeof window != 'undefined' && window.parent
662         && window.parent.Test && window.parent.Test.Harness) {
663         window.parent.Test.Harness.Done++;
664     }
665 };
666
667 Test.Builder.prototype._ending = function () {
668     if (this.Ended) return;
669     this.Ended = true;
670     if (this.noEnding()) {
671         this._notifyHarness();
672         return;
673     }
674     this._sanity_check();
675     var out = this.endOutput();
676
677     // Figure out if we passed or failed and print helpful messages.
678     if( this.TestResults.length ) {
679         // The plan?  We have no plan.
680         if (this.NoPlan) {
681             if (!this.noHeader())
682                 this._print("1.." + this.CurrTest + Test.Builder.LF);
683             this.ExpectedTests = this.CurrTest;
684         }
685
686         var numFailed = 0;
687         for (var i = 0; i < this.TestResults.length; i++) {
688             if (!this.TestResults[i]) numFailed++;
689         }
690         numFailed += Math.abs(
691             this.ExpectedTests - this.TestResults.length
692         );
693
694         if (this.CurrTest < this.ExpectedTests) {
695             var s = this.ExpectedTests == 1 ? '' : 's';
696             out(
697                 "# Looks like you planned " + this.ExpectedTests + " test"
698                 + s + " but only ran " + this.CurrTest + "." + Test.Builder.LF
699             );
700         } else if (this.CurrTest > this.ExpectedTests) {
701            var numExtra = this.CurrTest - this.ExpectedTests;
702             var s = this.ExpectedTests == 1 ? '' : 's';
703             out(
704                 "# Looks like you planned " + this.ExpectedTests + " test"
705                 + s + " but ran " + numExtra + " extra." + Test.Builder.LF
706             );
707         } else if (numFailed) {
708             var s = numFailed == 1 ? '' : 's';
709             out(
710                 "# Looks like you failed " + numFailed + "test" + s + " of "
711                 + this.ExpectedTests + "." + Test.Builder.LF
712             );
713         }
714
715         if (this.TestDied) {
716             out(
717                 "# Looks like your test died just after " 
718                 + this.CurrTest + "." + Test.Builder.LF
719             );
720         }
721
722     } else if (!this.SkipAll) {
723         // skipAll requires no status output.
724         if (this.TestDied) {
725             out(
726                 "# Looks like your test died before it could output anything."
727                 + Test.Builder.LF
728             );
729         } else {
730             out("# No tests run!" + Test.Builder.LF);
731         }
732     }
733     this._notifyHarness();
734 };
735
736 Test.Builder.prototype.isUndef = function (got, op, expect, desc) {
737     // Undefined only matches undefined, so we don't need to cast anything.
738     var test = eval("got " + (Test.Builder.StringOps[op] || op) + " expect");
739     this.ok(test, desc);
740     if (!test) this._isDiag(got, op, expect);
741     return test;
742 };
743
744 if (window) {
745     // Set up an onload function to end all tests.
746     window.onload = function () {
747         for (var i = 0; i < Test.Builder.Instances.length; i++) {
748             // The main process is always async ID 0.
749             Test.Builder.Instances[i].endAsync(0);
750         }
751     };
752
753     // Set up an exception handler. This is so that we can capture deaths but
754     // still output information for TestHarness to pick up.
755     window.onerror = function (msg, url, line) {
756         // Output the exception.
757         Test.Builder.Test.TestDied = true;
758         Test.Builder.Test.diag("Error in " + url + " at line " + line + ": " + msg);
759         return true;
760     };
761 };
762
763 Test.Builder.prototype.beginAsync = function (timeout) {
764         var id = ++this.asyncID;
765     if (timeout && window && window.setTimeout) {
766         // Are there other ways of setting timeout in non-browser settings?
767         var aTest = this;
768         this.asyncs[id] = window.setTimeout(
769             function () { aTest.endAsync(id) }, timeout
770         );
771     } else {
772         // Make sure it's defined.
773         this.asyncs[id] = 0;
774     }
775         return id;
776 };
777
778 Test.Builder.prototype.endAsync = function (id) {
779     if (this.asyncs[id] == undefined) return;
780     if (this.asyncs[id]) {
781                 // Remove the timeout
782                 window.clearTimeout(this.asyncs[id]);
783         }
784     if (--this.asyncID < 0) this._ending();
785 };
786
787 Test.Builder.exporter = function (pkg, root) {
788     if (typeof root == 'undefined') {
789         if      (Test.PLATFORM == 'browser')  root = window;
790         else if (Test.PLATFORM == 'director') root = _global;
791         else throw new Error("Platform unknown");
792     }
793     for (var i = 0; i < pkg.EXPORT.length; i++) {
794         if (typeof root[pkg.EXPORT[i]] == 'undefined')
795             root[pkg.EXPORT[i]] = pkg[pkg.EXPORT[i]];
796     }
797 };