X-Git-Url: http://git.asbjorn.it/?a=blobdiff_plain;f=build%2Fjs%2Flib%2FTest%2FBuilder.js;fp=build%2Fjs%2Flib%2FTest%2FBuilder.js;h=143508213e976ca3407d92cdec3affb99c79de98;hb=e943090a72fada9068705a701e2f37ca2907d310;hp=0000000000000000000000000000000000000000;hpb=0cd3821ac2b74d37384a36f281e5a952138aaae2;p=jquery.git diff --git a/build/js/lib/Test/Builder.js b/build/js/lib/Test/Builder.js new file mode 100644 index 0000000..1435082 --- /dev/null +++ b/build/js/lib/Test/Builder.js @@ -0,0 +1,797 @@ +// # $Id: Kinetic.pm 1493 2005-04-07 19:20:18Z theory $ + +// Set up namespace. +if (typeof self != 'undefined') { + // Browser + if (typeof Test == 'undefined') Test = {PLATFORM: 'browser'}; + else Test.PLATFORM = 'browser'; +} else if (typeof _global != 'undefined') { + //Director + if (typeof _global.Test == "undefined") _global.Test = {PLATFORM: 'director'}; + else _global.Test.PLATFORM = 'director'; +} else { + throw new Error("Test.More does not support your platform"); +} + +// Constructor. +Test.Builder = function () { + if (!Test.Builder.Test) { + Test.Builder.Test = this.reset(); + Test.Builder.Instances.push(this); + } + return Test.Builder.Test; +}; + +// Static variables. +Test.Builder.VERSION = '0.11'; +Test.Builder.Instances = []; +Test.Builder.lineEndingRx = /\r?\n|\r/g; +Test.Builder.StringOps = { + eq: '==', + ne: '!=', + lt: '<', + gt: '>', + ge: '>=', + le: '<=' +}; + +// Stoopid IE. +Test.Builder.LF = typeof document != "undefined" + && typeof document.all != "undefined" + ? "\r" + : "\n"; + +// Static methods. +Test.Builder.die = function (msg) { + throw new Error(msg); +}; + +Test.Builder._whoa = function (check, desc) { + if (!check) return; + Test.Builder.die("WHOA! " + desc + Test.Builder.LF + + + "This should never happen! Please contact the author " + + "immediately!"); +}; + +Test.Builder.typeOf = function (object) { + var c = Object.prototype.toString.apply(object); + var name = c.substring(8, c.length - 1); + if (name != 'Object') return name; + // It may be a non-core class. Try to extract the class name from + // the constructor function. This may not work in all implementations. + if (/function ([^(\s]+)/.test(Function.toString.call(object.constructor))) { + return RegExp.$1; + } + // No idea. :-( + return name; +}; + +// Instance methods. +Test.Builder.create = function () { + var test = Test.Builder.Test; + Test.Builder.Test = null; + var ret = new Test.Builder(); + Test.Builder.Test = test; + return ret.reset(); +}; + +Test.Builder.prototype.reset = function () { + this.TestDied = false; + this.HavePlan = false; + this.NoPlan = false; + this.CurrTest = 0; + this.ExpectedTests = 0; + this.UseNums = true; + this.NoHeader = false; + this.NoEnding = false; + this.TestResults = []; + this.ToDo = []; + this.Buffer = []; + this.asyncs = [0]; + this.asyncID = 0; + return this._setupOutput(); +}; + +Test.Builder.prototype._print = function (msg) { + this.output().call(this, msg); +}; + +Test.Builder.prototype.warn = function (msg) { + this.warnOutput().apply(this, arguments); +}; + +Test.Builder.prototype.plan = function (arg) { + if (!arg) return; + //if (this.HavePlan) Test.Builder.die("You tried to plan twice!"); + this.ExpectedTests = 0; + this.HavePlan = false; + this.NoPlan = false; + + if (!(arg instanceof Object)) + Test.Builder.die("plan() doesn't understand " + arg); + for (var cmd in arg) { + if (cmd == 'tests') { + if (arg[cmd] == null) { + TestBulder.die( + "Got an undefined number of tests. Looks like you tried to " + + "say how many tests you plan to run but made a mistake." + + Test.Builder.LF + ); + } else if (!arg[cmd]) { + Test.Builder.die( + "You said to run 0 tests! You've got to run something." + + Test.Builder.LF + ); + } else { + this.expectedTests(arg[cmd]); + } + } else if (cmd == 'skipAll') { + this.skipAll(arg[cmd]); + } else if (cmd == 'noPlan' && arg[cmd]) { + this.noPlan(); + } else { + Test.Builder.die("plan() doesn't understand " + + cmd + (arg[cmd] ? (" " + arg[cmd]) : '')); + } + } +}; + +Test.Builder.prototype.expectedTests = function (max) { + if (max) { + if (isNaN(max)) { + Test.Builder.die( + "Number of tests must be a postive integer. You gave it '" + + max + "'." + Test.Builder.LF + ); + } + + this.ExpectedTests = max.valueOf(); + this.HavePlan = 1; + if (!this.noHeader()) this._print("1.." + max + Test.Builder.LF); + } + return this.ExpectedTests; +}; + +Test.Builder.prototype.noPlan = function () { + this.NoPlan = 1; + this.HavePlan = 1; +}; + +Test.Builder.prototype.hasPlan = function () { + if (this.ExpectedTests) return this.ExpectedTests; + if (this.NoPlan) return 'noPlan'; +}; + +Test.Builder.prototype.skipAll = function (reason) { + var out = "1..0"; + if (reason) out += " # Skip " + reason; + out += Test.Builder.LF; + this.SkipAll = 1; + if (!this.noHeader()) this._print(out); + // Just throw and catch an exception. + window.onerror = function () { return true; } + throw new Error("__SKIP_ALL__"); +}; + +Test.Builder.prototype.ok = function (test, desc) { + // test might contain an object that we don't want to accidentally + // store, so we turn it into a boolean. + test = !!test; + + if (!this.HavePlan) + Test.Builder.die("You tried to run a test without a plan! Gotta have a plan."); + + // I don't think we need to worry about threading in JavaScript. + this.CurrTest++; + + // In case desc is a string overloaded object, force it to stringify. + if (desc) desc = desc.toString(); + + var startsNumber + if (desc != null && /^[\d\s]+$/.test(desc)) { + this.diag( "Your test description is '" + desc + "'. You shouldn't use", + Test.Builder.LF, + "numbers for your test names. Very confusing."); + } + + var todo = this._todo(); + // I don't think we need to worry about result beeing shared between + // threads. + var out = ''; + var result = {}; + + if (test) { + result.ok = true; + result.actual_ok = test; + } else { + out += 'not '; + result.ok = todo ? true : false; + result.actual_ok = false; + } + + out += 'ok'; + if (this.useNumbers) out += ' ' + this.CurrTest; + + if (desc == null) { + result.desc = ''; + } else { + desc = desc.replace(Test.Builder.lineEndingRx, Test.Builder.LF + "# "); + // XXX Does this matter since we don't have a TestHarness? + desc.split('#').join('\\#'); // # # in a desc can confuse TestHarness. + out += ' - ' + desc; + result.desc = desc; + } + + if (todo) { + todo = todo.replace(Test.Builder.lineEndingRx, Test.Builder.LF + "# "); + out += " # TODO " + todo; + result.reason = todo; + result.type = 'todo'; + } else { + result.reason = ''; + result.type = ''; + } + + this.TestResults[this.CurrTest - 1] = result; + + out += Test.Builder.LF; + this._print(out); + + if (!test) { + var msg = todo ? "Failed (TODO)" : "Failed"; + // XXX Hrm, do I need this? + //$self_print_diag(Test.Builder.LF) if $ENV{HARNESS_ACTIVE}; + this.diag(" " + msg + " test"); + } + result.output = this.Buffer.splice(0).join(''); + return test; +}; + +Test.Builder.prototype.isEq = function (got, expect, desc) { + if (got == null || expect == null) { + // undefined only matches undefined and nothing else + return this.isUndef(got, '==', expect, desc); + } + return this.cmpOK(got, '==', expect, desc); +}; + +Test.Builder.prototype.isNum = function (got, expect, desc) { + if (got == null || expect == null) { + // undefined only matches undefined and nothing else + return this.isUndef(got, '==', expect, desc); + } + return this.cmpOK(Number(got), '==', Number(expect), desc); +}; + +Test.Builder.prototype.isntEq = function (got, dontExpect, desc) { + if (got == null || dontExpect == null) { + // undefined only matches undefined and nothing else + return this.isUndef(got, '!=', dontExpect, desc); + } + return this.cmpOK(got, '!=', dontExpect, desc); +}; + +Test.Builder.prototype.isntNum = function (got, dontExpect, desc) { + if (got == null || dontExpect == null) { + // undefined only matches undefined and nothing else + return this.isUndef(got, '!=', dontExpect, desc); + } + return this.cmpOK(Number(got), '!=', Number(dontExpect), desc); +}; + +Test.Builder.prototype.like = function (val, regex, desc) { + return this._regexOK(val, regex, '=~', desc); +}; + +Test.Builder.prototype.unlike = function (val, regex, desc) { + return this._regexOK(val, regex, '!~', desc); +}; + +Test.Builder.prototype._regexOK = function (val, regex, cmp, desc) { + // Create a regex object. + var type = Test.Builder.typeOf(regex); + var ok; + if (type.toLowerCase() == 'string') { + // Create a regex object. + regex = new RegExp(regex); + } else { + if (type != 'RegExp') { + ok = this.ok(false, desc); + this.diag("'" + regex + "' doesn't look much like a regex to me."); + return ok; + } + } + + if (val == null || typeof val != 'string') { + if (cmp == '=~') { + // The test fails. + ok = this.ok(false, desc); + this._diagLike(val, regex, cmp); + } else { + // undefined matches nothing (unlike in Perl, where undef =~ //). + ok = this.ok(true, desc); + } + return ok; + } + + // Use val.match() instead of regex.test() in case they've set g. + var test = val.match(regex); + if (cmp == '!~') test = !test; + ok = this.ok(test, desc); + if (!ok) this._diagLike(val, regex, cmp); + return ok; +}; + +Test.Builder.prototype._diagLike = function (val, regex, cmp) { + var match = cmp == '=~' ? "doesn't match" : " matches"; + return this.diag( + " '" + val + "" + Test.Builder.LF + + " " + match + " /" + regex.source + "/" + ); +}; + +Test.Builder.prototype.cmpOK = function (got, op, expect, desc) { + + var test; + if (Test.Builder.StringOps[op]) { + // Force string context. + test = eval("got.toString() " + Test.Builder.StringOps[op] + " expect.toString()"); + } else { + test = eval("got " + op + " expect"); + } + + var ok = this.ok(test, desc); + if (!ok) { + if (/^(eq|==)$/.test(op)) { + this._isDiag(got, op, expect); + } else { + this._cmpDiag(got, op, expect); + } + } + return ok; +}; + +Test.Builder.prototype._cmpDiag = function (got, op, expect) { + if (got != null) got = "'" + got.toString() + "'"; + if (expect != null) expect = "'" + expect.toString() + "'"; + return this.diag(" " + got + Test.Builder.LF + " " + op + + Test.Builder.LF + " " + expect); +}; + +Test.Builder.prototype._isDiag = function (got, op, expect) { + var args = [got, expect]; + for (var i = 0; i < args.length; i++) { + if (args[i] != null) { + args[i] = op == 'eq' ? "'" + args[i].toString() + "'" : args[i].valueOf(); + } + } + + return this.diag( + " got: " + args[0] + Test.Builder.LF + + " expected: " + args[1] + Test.Builder.LF + ); +}; + +Test.Builder.prototype.BAILOUT = function (reason) { + this._print("Bail out! " + reason); + // Just throw and catch an exception. + window.onerror = function () { + // XXX Do something to tell TestHarness it was a bailout? + return true; + } + throw new Error("__BAILOUT__"); +}; + +Test.Builder.prototype.skip = function (why) { + if (!this.HavePlan) + Test.Builder.die("You tried to run a test without a plan! Gotta have a plan."); + + // In case desc is a string overloaded object, force it to stringify. + if (why) why = why.toString().replace(Test.Builder.lineEndingRx, + Test.Builder.LF+ "# "); + + this.CurrTest++; + this.TestResults[this.CurrTest - 1] = { + ok: true, + actual_ok: true, + desc: '', + type: 'skip', + reason: why + }; + + var out = "ok"; + if (this.useNumbers) out += ' ' + this.CurrTest; + out += " # skip " + why + Test.Builder.LF; + this._print(out); + this.TestResults[this.CurrTest - 1].output = + this.Buffer.splice(0).join(''); + return true; +}; + +Test.Builder.prototype.todoSkip = function (why) { + if (!this.HavePlan) + Test.Builder.die("You tried to run a test without a plan! Gotta have a plan."); + + // In case desc is a string overloaded object, force it to stringify. + if (why) why = why.toString().replace(Test.Builder.lineEndingRx, + Test.Builder.LF + "# "); + + + this.CurrTest++; + this.TestResults[this.CurrTest - 1] = { + ok: true, + actual_ok: false, + desc: '', + type: 'todo_skip', + reason: why + }; + + var out = "not ok"; + if (this.useNumbers) out += ' ' + this.CurrTest; + out += " # TODO & SKIP " + why + Test.Builder.LF; + this._print(out); + this.TestResults[this.CurrTest - 1].output = + this.Buffer.splice(0).join(''); + return true; +}; + +Test.Builder.prototype.skipRest = function (reason) { + var out = "# Skip"; + if (reason) out += " " + reason; + out += Test.Builder.LF; + if (this.NoPlan) this.skip(reason); + else { + for (var i = this.CurrTest; i < this.ExpectedTests; i++) { + this.skip(reason); + } + } + // Just throw and catch an exception. + window.onerror = function () { return true; } + throw new Error("__SKIP_REST__"); +}; + +Test.Builder.prototype.useNumbers = function (useNums) { + if (useNums != null) this.UseNums = useNums; + return this.UseNums; +}; + +Test.Builder.prototype.noHeader = function (noHeader) { + if (noHeader != null) this.NoHeader = !!noHeader; + return this.NoHeader; +}; + +Test.Builder.prototype.noEnding = function (noEnding) { + if (noEnding != null) this.NoEnding = !!noEnding; + return this.NoEnding; +}; + +Test.Builder.prototype.diag = function () { + if (!arguments.length) return; + + var msg = '# '; + // Join each agument and escape each line with a #. + for (i = 0; i < arguments.length; i++) { + // Replace any newlines. + msg += arguments[i].toString().replace(Test.Builder.lineEndingRx, + Test.Builder.LF + "# "); + } + + // Append a new line to the end of the message if there isn't one. + if (!(new RegExp(Test.Builder.LF + '$').test(msg))) + msg += Test.Builder.LF; + // Append the diag message to the most recent result. + return this._printDiag(msg); +}; + +Test.Builder.prototype._printDiag = function () { + var fn = this.todo() ? this.todoOutput() : this.failureOutput(); + fn.apply(this, arguments); + return false; +}; + +Test.Builder.prototype.output = function (fn) { + if (fn != null) { + var buffer = this.Buffer; + this.Output = function (msg) { buffer.push(msg); fn(msg) }; + } + return this.Output; +}; + +Test.Builder.prototype.failureOutput = function (fn) { + if (fn != null) { + var buffer = this.Buffer; + this.FailureOutput = function (msg) { buffer.push(msg); fn(msg) }; + } + return this.FailureOutput; +}; + +Test.Builder.prototype.todoOutput = function (fn) { + if (fn != null) { + var buffer = this.Buffer; + this.TodoOutput = function (msg) { buffer.push(msg); fn(msg) }; + } + return this.TodoOutput; +}; + +Test.Builder.prototype.endOutput = function (fn) { + if (fn != null) { + var buffer = this.Buffer; + this.EndOutput = function (msg) { buffer.push(msg); fn(msg) }; + } + return this.EndOutput; +}; + +Test.Builder.prototype.warnOutput = function (fn) { + if (fn != null) { + var buffer = this.Buffer; + this.WarnOutput = function (msg) { buffer.push(msg); fn(msg) }; + } + return this.WarnOutput; +}; + +Test.Builder.prototype._setupOutput = function () { + if (Test.PLATFORM == 'browser') { + var writer = function (msg) { + // I'm sure that there must be a more efficient way to do this, + // but if I store the node in a variable outside of this function + // and refer to it via the closure, then things don't work right + // --the order of output can become all screwed up (see + // buffer.html). I have no idea why this is. + var node = document.getElementById("test"); + if (node) { + // This approach is neater, but causes buffering problems when + // mixed with document.write. See tests/buffer.html. + //node.appendChild(document.createTextNode(msg)); + //return; + for (var i = 0; i < node.childNodes.length; i++) { + if (node.childNodes[i].nodeType == 3 /* Text Node */) { + // Append to the node and scroll down. + node.childNodes[i].appendData(msg); + window.scrollTo(0, document.body.offsetHeight + || document.body.scrollHeight); + return; + } + } + + // If there was no text node, add one. + node.appendChild(document.createTextNode(msg)); + window.scrollTo(0, document.body.offsetHeight + || document.body.scrollHeight); + return; + } + + // Default to the normal write and scroll down... + document.write(msg); + window.scrollTo(0, document.body.offsetHeight + || document.body.scrollHeight); + }; + + this.output(writer); + this.failureOutput(writer); + this.todoOutput(writer); + this.endOutput(writer); + + if (window) { + if (window.alert.apply) this.warnOutput(window.alert, window); + else this.warnOutput(function (msg) { window.alert(msg) }); + } + + } else if (Test.PLATFORM == 'director') { + // Macromedia-Adobe:Director MX 2004 Support + // XXX Is _player a definitive enough object? + // There may be an even more explicitly Director object. + this.output(trace); + this.failureOutput(trace); + this.todoOutput(trace); + this.warnOutput(trace); + } + + return this; +}; + +Test.Builder.prototype.currentTest = function (num) { + if (num == null) return this.CurrTest; + + if (!this.HavePlan) + Test.Builder.die("Can't change the current test number without a plan!"); + this.CurrTest = num; + if (num > this.TestResults.length ) { + var reason = 'incrementing test number'; + for (i = this.TestResults.length; i < num; i++) { + this.TestResults[i] = { + ok: true, + actual_ok: null, + reason: reason, + type: 'unknown', + name: null, + output: 'ok - ' + reason + Test.Builder.LF + }; + } + } else if (num < this.TestResults.length) { + // IE requires the second argument to truncate the array. + this.TestResults.splice(num, this.TestResults.length); + } + return this.CurrTest; +}; + +Test.Builder.prototype.summary = function () { + var results = new Array(this.TestResults.length); + for (var i = 0; i < this.TestResults.length; i++) { + results[i] = this.TestResults[i]['ok']; + } + return results +}; + +Test.Builder.prototype.details = function () { + return this.TestResults; +}; + +Test.Builder.prototype.todo = function (why, howMany) { + if (howMany) this.ToDo = [why, howMany]; + return this.ToDo[1]; +}; + +Test.Builder.prototype._todo = function () { + if (this.ToDo[1]) { + if (this.ToDo[1]--) return this.ToDo[0]; + this.ToDo = []; + } + return false; +}; + +Test.Builder.prototype._sanity_check = function () { + Test.Builder._whoa( + this.CurrTest < 0, + 'Says here you ran a negative number of tests!' + ); + + Test.Builder._whoa( + !this.HavePlan && this.CurrTest, + 'Somehow your tests ran without a plan!' + ); + + Test.Builder._whoa( + this.CurrTest != this.TestResults.length, + 'Somehow you got a different number of results than tests ran!' + ); +}; + +Test.Builder.prototype._notifyHarness = function () { + // Special treatment for the browser harness. + if (typeof window != 'undefined' && window.parent + && window.parent.Test && window.parent.Test.Harness) { + window.parent.Test.Harness.Done++; + } +}; + +Test.Builder.prototype._ending = function () { + if (this.Ended) return; + this.Ended = true; + if (this.noEnding()) { + this._notifyHarness(); + return; + } + this._sanity_check(); + var out = this.endOutput(); + + // Figure out if we passed or failed and print helpful messages. + if( this.TestResults.length ) { + // The plan? We have no plan. + if (this.NoPlan) { + if (!this.noHeader()) + this._print("1.." + this.CurrTest + Test.Builder.LF); + this.ExpectedTests = this.CurrTest; + } + + var numFailed = 0; + for (var i = 0; i < this.TestResults.length; i++) { + if (!this.TestResults[i]) numFailed++; + } + numFailed += Math.abs( + this.ExpectedTests - this.TestResults.length + ); + + if (this.CurrTest < this.ExpectedTests) { + var s = this.ExpectedTests == 1 ? '' : 's'; + out( + "# Looks like you planned " + this.ExpectedTests + " test" + + s + " but only ran " + this.CurrTest + "." + Test.Builder.LF + ); + } else if (this.CurrTest > this.ExpectedTests) { + var numExtra = this.CurrTest - this.ExpectedTests; + var s = this.ExpectedTests == 1 ? '' : 's'; + out( + "# Looks like you planned " + this.ExpectedTests + " test" + + s + " but ran " + numExtra + " extra." + Test.Builder.LF + ); + } else if (numFailed) { + var s = numFailed == 1 ? '' : 's'; + out( + "# Looks like you failed " + numFailed + "test" + s + " of " + + this.ExpectedTests + "." + Test.Builder.LF + ); + } + + if (this.TestDied) { + out( + "# Looks like your test died just after " + + this.CurrTest + "." + Test.Builder.LF + ); + } + + } else if (!this.SkipAll) { + // skipAll requires no status output. + if (this.TestDied) { + out( + "# Looks like your test died before it could output anything." + + Test.Builder.LF + ); + } else { + out("# No tests run!" + Test.Builder.LF); + } + } + this._notifyHarness(); +}; + +Test.Builder.prototype.isUndef = function (got, op, expect, desc) { + // Undefined only matches undefined, so we don't need to cast anything. + var test = eval("got " + (Test.Builder.StringOps[op] || op) + " expect"); + this.ok(test, desc); + if (!test) this._isDiag(got, op, expect); + return test; +}; + +if (window) { + // Set up an onload function to end all tests. + window.onload = function () { + for (var i = 0; i < Test.Builder.Instances.length; i++) { + // The main process is always async ID 0. + Test.Builder.Instances[i].endAsync(0); + } + }; + + // Set up an exception handler. This is so that we can capture deaths but + // still output information for TestHarness to pick up. + window.onerror = function (msg, url, line) { + // Output the exception. + Test.Builder.Test.TestDied = true; + Test.Builder.Test.diag("Error in " + url + " at line " + line + ": " + msg); + return true; + }; +}; + +Test.Builder.prototype.beginAsync = function (timeout) { + var id = ++this.asyncID; + if (timeout && window && window.setTimeout) { + // Are there other ways of setting timeout in non-browser settings? + var aTest = this; + this.asyncs[id] = window.setTimeout( + function () { aTest.endAsync(id) }, timeout + ); + } else { + // Make sure it's defined. + this.asyncs[id] = 0; + } + return id; +}; + +Test.Builder.prototype.endAsync = function (id) { + if (this.asyncs[id] == undefined) return; + if (this.asyncs[id]) { + // Remove the timeout + window.clearTimeout(this.asyncs[id]); + } + if (--this.asyncID < 0) this._ending(); +}; + +Test.Builder.exporter = function (pkg, root) { + if (typeof root == 'undefined') { + if (Test.PLATFORM == 'browser') root = window; + else if (Test.PLATFORM == 'director') root = _global; + else throw new Error("Platform unknown"); + } + for (var i = 0; i < pkg.EXPORT.length; i++) { + if (typeof root[pkg.EXPORT[i]] == 'undefined') + root[pkg.EXPORT[i]] = pkg[pkg.EXPORT[i]]; + } +}; \ No newline at end of file