1 // # $Id: Kinetic.pm 1493 2005-04-07 19:20:18Z theory $
4 if (typeof self != 'undefined') {
6 if (typeof Test == 'undefined') Test = {PLATFORM: 'browser'};
7 else Test.PLATFORM = 'browser';
8 } else if (typeof _global != 'undefined') {
10 if (typeof _global.Test == "undefined") _global.Test = {PLATFORM: 'director'};
11 else _global.Test.PLATFORM = 'director';
13 throw new Error("Test.More does not support your platform");
17 Test.Builder = function () {
18 if (!Test.Builder.Test) {
19 Test.Builder.Test = this.reset();
20 Test.Builder.Instances.push(this);
22 return Test.Builder.Test;
26 Test.Builder.VERSION = '0.11';
27 Test.Builder.Instances = [];
28 Test.Builder.lineEndingRx = /\r?\n|\r/g;
29 Test.Builder.StringOps = {
39 Test.Builder.LF = typeof document != "undefined"
40 && typeof document.all != "undefined"
45 Test.Builder.die = function (msg) {
49 Test.Builder._whoa = function (check, desc) {
51 Test.Builder.die("WHOA! " + desc + Test.Builder.LF +
52 + "This should never happen! Please contact the author "
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))) {
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;
78 Test.Builder.prototype.reset = function () {
79 this.TestDied = false;
80 this.HavePlan = false;
83 this.ExpectedTests = 0;
85 this.NoHeader = false;
86 this.NoEnding = false;
87 this.TestResults = [];
92 return this._setupOutput();
95 Test.Builder.prototype._print = function (msg) {
96 this.output().call(this, msg);
99 Test.Builder.prototype.warn = function (msg) {
100 this.warnOutput().apply(this, arguments);
103 Test.Builder.prototype.plan = function (arg) {
105 //if (this.HavePlan) Test.Builder.die("You tried to plan twice!");
106 this.ExpectedTests = 0;
107 this.HavePlan = false;
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) {
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."
120 } else if (!arg[cmd]) {
122 "You said to run 0 tests! You've got to run something."
126 this.expectedTests(arg[cmd]);
128 } else if (cmd == 'skipAll') {
129 this.skipAll(arg[cmd]);
130 } else if (cmd == 'noPlan' && arg[cmd]) {
133 Test.Builder.die("plan() doesn't understand "
134 + cmd + (arg[cmd] ? (" " + arg[cmd]) : ''));
139 Test.Builder.prototype.expectedTests = function (max) {
143 "Number of tests must be a postive integer. You gave it '"
144 + max + "'." + Test.Builder.LF
148 this.ExpectedTests = max.valueOf();
150 if (!this.noHeader()) this._print("1.." + max + Test.Builder.LF);
152 return this.ExpectedTests;
155 Test.Builder.prototype.noPlan = function () {
160 Test.Builder.prototype.hasPlan = function () {
161 if (this.ExpectedTests) return this.ExpectedTests;
162 if (this.NoPlan) return 'noPlan';
165 Test.Builder.prototype.skipAll = function (reason) {
167 if (reason) out += " # Skip " + reason;
168 out += Test.Builder.LF;
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__");
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.
182 Test.Builder.die("You tried to run a test without a plan! Gotta have a plan.");
184 // I don't think we need to worry about threading in JavaScript.
187 // In case desc is a string overloaded object, force it to stringify.
188 if (desc) desc = desc.toString();
191 if (desc != null && /^[\d\s]+$/.test(desc)) {
192 this.diag( "Your test description is '" + desc + "'. You shouldn't use",
194 "numbers for your test names. Very confusing.");
197 var todo = this._todo();
198 // I don't think we need to worry about result beeing shared between
205 result.actual_ok = test;
208 result.ok = todo ? true : false;
209 result.actual_ok = false;
213 if (this.useNumbers) out += ' ' + this.CurrTest;
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.
226 todo = todo.replace(Test.Builder.lineEndingRx, Test.Builder.LF + "# ");
227 out += " # TODO " + todo;
228 result.reason = todo;
229 result.type = 'todo';
235 this.TestResults[this.CurrTest - 1] = result;
237 out += Test.Builder.LF;
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");
246 result.output = this.Buffer.splice(0).join('');
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);
255 return this.cmpOK(got, '==', expect, desc);
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);
263 return this.cmpOK(Number(got), '==', Number(expect), desc);
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);
271 return this.cmpOK(got, '!=', dontExpect, desc);
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);
279 return this.cmpOK(Number(got), '!=', Number(dontExpect), desc);
282 Test.Builder.prototype.like = function (val, regex, desc) {
283 return this._regexOK(val, regex, '=~', desc);
286 Test.Builder.prototype.unlike = function (val, regex, desc) {
287 return this._regexOK(val, regex, '!~', desc);
290 Test.Builder.prototype._regexOK = function (val, regex, cmp, desc) {
291 // Create a regex object.
292 var type = Test.Builder.typeOf(regex);
294 if (type.toLowerCase() == 'string') {
295 // Create a regex object.
296 regex = new RegExp(regex);
298 if (type != 'RegExp') {
299 ok = this.ok(false, desc);
300 this.diag("'" + regex + "' doesn't look much like a regex to me.");
305 if (val == null || typeof val != 'string') {
308 ok = this.ok(false, desc);
309 this._diagLike(val, regex, cmp);
311 // undefined matches nothing (unlike in Perl, where undef =~ //).
312 ok = this.ok(true, desc);
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);
325 Test.Builder.prototype._diagLike = function (val, regex, cmp) {
326 var match = cmp == '=~' ? "doesn't match" : " matches";
328 " '" + val + "" + Test.Builder.LF +
329 " " + match + " /" + regex.source + "/"
333 Test.Builder.prototype.cmpOK = function (got, op, expect, desc) {
336 if (Test.Builder.StringOps[op]) {
337 // Force string context.
338 test = eval("got.toString() " + Test.Builder.StringOps[op] + " expect.toString()");
340 test = eval("got " + op + " expect");
343 var ok = this.ok(test, desc);
345 if (/^(eq|==)$/.test(op)) {
346 this._isDiag(got, op, expect);
348 this._cmpDiag(got, op, expect);
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);
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();
370 " got: " + args[0] + Test.Builder.LF +
371 " expected: " + args[1] + Test.Builder.LF
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?
382 throw new Error("__BAILOUT__");
385 Test.Builder.prototype.skip = function (why) {
387 Test.Builder.die("You tried to run a test without a plan! Gotta have a plan.");
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+ "# ");
394 this.TestResults[this.CurrTest - 1] = {
403 if (this.useNumbers) out += ' ' + this.CurrTest;
404 out += " # skip " + why + Test.Builder.LF;
406 this.TestResults[this.CurrTest - 1].output =
407 this.Buffer.splice(0).join('');
411 Test.Builder.prototype.todoSkip = function (why) {
413 Test.Builder.die("You tried to run a test without a plan! Gotta have a plan.");
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 + "# ");
421 this.TestResults[this.CurrTest - 1] = {
430 if (this.useNumbers) out += ' ' + this.CurrTest;
431 out += " # TODO & SKIP " + why + Test.Builder.LF;
433 this.TestResults[this.CurrTest - 1].output =
434 this.Buffer.splice(0).join('');
438 Test.Builder.prototype.skipRest = function (reason) {
440 if (reason) out += " " + reason;
441 out += Test.Builder.LF;
442 if (this.NoPlan) this.skip(reason);
444 for (var i = this.CurrTest; i < this.ExpectedTests; i++) {
448 // Just throw and catch an exception.
449 window.onerror = function () { return true; }
450 throw new Error("__SKIP_REST__");
453 Test.Builder.prototype.useNumbers = function (useNums) {
454 if (useNums != null) this.UseNums = useNums;
458 Test.Builder.prototype.noHeader = function (noHeader) {
459 if (noHeader != null) this.NoHeader = !!noHeader;
460 return this.NoHeader;
463 Test.Builder.prototype.noEnding = function (noEnding) {
464 if (noEnding != null) this.NoEnding = !!noEnding;
465 return this.NoEnding;
468 Test.Builder.prototype.diag = function () {
469 if (!arguments.length) return;
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 + "# ");
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);
486 Test.Builder.prototype._printDiag = function () {
487 var fn = this.todo() ? this.todoOutput() : this.failureOutput();
488 fn.apply(this, arguments);
492 Test.Builder.prototype.output = function (fn) {
494 var buffer = this.Buffer;
495 this.Output = function (msg) { buffer.push(msg); fn(msg) };
500 Test.Builder.prototype.failureOutput = function (fn) {
502 var buffer = this.Buffer;
503 this.FailureOutput = function (msg) { buffer.push(msg); fn(msg) };
505 return this.FailureOutput;
508 Test.Builder.prototype.todoOutput = function (fn) {
510 var buffer = this.Buffer;
511 this.TodoOutput = function (msg) { buffer.push(msg); fn(msg) };
513 return this.TodoOutput;
516 Test.Builder.prototype.endOutput = function (fn) {
518 var buffer = this.Buffer;
519 this.EndOutput = function (msg) { buffer.push(msg); fn(msg) };
521 return this.EndOutput;
524 Test.Builder.prototype.warnOutput = function (fn) {
526 var buffer = this.Buffer;
527 this.WarnOutput = function (msg) { buffer.push(msg); fn(msg) };
529 return this.WarnOutput;
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");
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));
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);
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);
563 // Default to the normal write and scroll down...
565 window.scrollTo(0, document.body.offsetHeight
566 || document.body.scrollHeight);
570 this.failureOutput(writer);
571 this.todoOutput(writer);
572 this.endOutput(writer);
575 if (window.alert.apply) this.warnOutput(window.alert, window);
576 else this.warnOutput(function (msg) { window.alert(msg) });
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.
584 this.failureOutput(trace);
585 this.todoOutput(trace);
586 this.warnOutput(trace);
592 Test.Builder.prototype.currentTest = function (num) {
593 if (num == null) return this.CurrTest;
596 Test.Builder.die("Can't change the current test number without a plan!");
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] = {
607 output: 'ok - ' + reason + Test.Builder.LF
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);
614 return this.CurrTest;
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'];
625 Test.Builder.prototype.details = function () {
626 return this.TestResults;
629 Test.Builder.prototype.todo = function (why, howMany) {
630 if (howMany) this.ToDo = [why, howMany];
634 Test.Builder.prototype._todo = function () {
636 if (this.ToDo[1]--) return this.ToDo[0];
642 Test.Builder.prototype._sanity_check = function () {
645 'Says here you ran a negative number of tests!'
649 !this.HavePlan && this.CurrTest,
650 'Somehow your tests ran without a plan!'
654 this.CurrTest != this.TestResults.length,
655 'Somehow you got a different number of results than tests ran!'
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++;
667 Test.Builder.prototype._ending = function () {
668 if (this.Ended) return;
670 if (this.noEnding()) {
671 this._notifyHarness();
674 this._sanity_check();
675 var out = this.endOutput();
677 // Figure out if we passed or failed and print helpful messages.
678 if( this.TestResults.length ) {
679 // The plan? We have no plan.
681 if (!this.noHeader())
682 this._print("1.." + this.CurrTest + Test.Builder.LF);
683 this.ExpectedTests = this.CurrTest;
687 for (var i = 0; i < this.TestResults.length; i++) {
688 if (!this.TestResults[i]) numFailed++;
690 numFailed += Math.abs(
691 this.ExpectedTests - this.TestResults.length
694 if (this.CurrTest < this.ExpectedTests) {
695 var s = this.ExpectedTests == 1 ? '' : 's';
697 "# Looks like you planned " + this.ExpectedTests + " test"
698 + s + " but only ran " + this.CurrTest + "." + Test.Builder.LF
700 } else if (this.CurrTest > this.ExpectedTests) {
701 var numExtra = this.CurrTest - this.ExpectedTests;
702 var s = this.ExpectedTests == 1 ? '' : 's';
704 "# Looks like you planned " + this.ExpectedTests + " test"
705 + s + " but ran " + numExtra + " extra." + Test.Builder.LF
707 } else if (numFailed) {
708 var s = numFailed == 1 ? '' : 's';
710 "# Looks like you failed " + numFailed + "test" + s + " of "
711 + this.ExpectedTests + "." + Test.Builder.LF
717 "# Looks like your test died just after "
718 + this.CurrTest + "." + Test.Builder.LF
722 } else if (!this.SkipAll) {
723 // skipAll requires no status output.
726 "# Looks like your test died before it could output anything."
730 out("# No tests run!" + Test.Builder.LF);
733 this._notifyHarness();
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");
740 if (!test) this._isDiag(got, op, expect);
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);
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);
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?
768 this.asyncs[id] = window.setTimeout(
769 function () { aTest.endAsync(id) }, timeout
772 // Make sure it's defined.
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]);
784 if (--this.asyncID < 0) this._ending();
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");
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]];