noalyss  Version-6.7.2
 All Data Structures Namespaces Files Functions Variables Enumerations
unittest.js
Go to the documentation of this file.
00001 // script.aculo.us unittest.js v1.9.0, Thu Dec 23 16:54:48 -0500 2010
00002 
00003 // Copyright (c) 2005-2010 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
00004 //           (c) 2005-2010 Jon Tirsen (http://www.tirsen.com)
00005 //           (c) 2005-2010 Michael Schuerig (http://www.schuerig.de/michael/)
00006 //
00007 // script.aculo.us is freely distributable under the terms of an MIT-style license.
00008 // For details, see the script.aculo.us web site: http://script.aculo.us/
00009 
00010 // experimental, Firefox-only
00011 Event.simulateMouse = function(element, eventName) {
00012   var options = Object.extend({
00013     pointerX: 0,
00014     pointerY: 0,
00015     buttons:  0,
00016     ctrlKey:  false,
00017     altKey:   false,
00018     shiftKey: false,
00019     metaKey:  false
00020   }, arguments[2] || {});
00021   var oEvent = document.createEvent("MouseEvents");
00022   oEvent.initMouseEvent(eventName, true, true, document.defaultView, 
00023     options.buttons, options.pointerX, options.pointerY, options.pointerX, options.pointerY, 
00024     options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, 0, $(element));
00025   
00026   if(this.mark) Element.remove(this.mark);
00027   this.mark = document.createElement('div');
00028   this.mark.appendChild(document.createTextNode(" "));
00029   document.body.appendChild(this.mark);
00030   this.mark.style.position = 'absolute';
00031   this.mark.style.top = options.pointerY + "px";
00032   this.mark.style.left = options.pointerX + "px";
00033   this.mark.style.width = "5px";
00034   this.mark.style.height = "5px;";
00035   this.mark.style.borderTop = "1px solid red;";
00036   this.mark.style.borderLeft = "1px solid red;";
00037   
00038   if(this.step)
00039     alert('['+new Date().getTime().toString()+'] '+eventName+'/'+Test.Unit.inspect(options));
00040   
00041   $(element).dispatchEvent(oEvent);
00042 };
00043 
00044 // Note: Due to a fix in Firefox 1.0.5/6 that probably fixed "too much", this doesn't work in 1.0.6 or DP2.
00045 // You need to downgrade to 1.0.4 for now to get this working
00046 // See https://bugzilla.mozilla.org/show_bug.cgi?id=289940 for the fix that fixed too much
00047 Event.simulateKey = function(element, eventName) {
00048   var options = Object.extend({
00049     ctrlKey: false,
00050     altKey: false,
00051     shiftKey: false,
00052     metaKey: false,
00053     keyCode: 0,
00054     charCode: 0
00055   }, arguments[2] || {});
00056 
00057   var oEvent = document.createEvent("KeyEvents");
00058   oEvent.initKeyEvent(eventName, true, true, window, 
00059     options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
00060     options.keyCode, options.charCode );
00061   $(element).dispatchEvent(oEvent);
00062 };
00063 
00064 Event.simulateKeys = function(element, command) {
00065   for(var i=0; i<command.length; i++) {
00066     Event.simulateKey(element,'keypress',{charCode:command.charCodeAt(i)});
00067   }
00068 };
00069 
00070 var Test = {};
00071 Test.Unit = {};
00072 
00073 // security exception workaround
00074 Test.Unit.inspect = Object.inspect;
00075 
00076 Test.Unit.Logger = Class.create();
00077 Test.Unit.Logger.prototype = {
00078   initialize: function(log) {
00079     this.log = $(log);
00080     if (this.log) {
00081       this._createLogTable();
00082     }
00083   },
00084   start: function(testName) {
00085     if (!this.log) return;
00086     this.testName = testName;
00087     this.lastLogLine = document.createElement('tr');
00088     this.statusCell = document.createElement('td');
00089     this.nameCell = document.createElement('td');
00090     this.nameCell.className = "nameCell";
00091     this.nameCell.appendChild(document.createTextNode(testName));
00092     this.messageCell = document.createElement('td');
00093     this.lastLogLine.appendChild(this.statusCell);
00094     this.lastLogLine.appendChild(this.nameCell);
00095     this.lastLogLine.appendChild(this.messageCell);
00096     this.loglines.appendChild(this.lastLogLine);
00097   },
00098   finish: function(status, summary) {
00099     if (!this.log) return;
00100     this.lastLogLine.className = status;
00101     this.statusCell.innerHTML = status;
00102     this.messageCell.innerHTML = this._toHTML(summary);
00103     this.addLinksToResults();
00104   },
00105   message: function(message) {
00106     if (!this.log) return;
00107     this.messageCell.innerHTML = this._toHTML(message);
00108   },
00109   summary: function(summary) {
00110     if (!this.log) return;
00111     this.logsummary.innerHTML = this._toHTML(summary);
00112   },
00113   _createLogTable: function() {
00114     this.log.innerHTML =
00115     '<div id="logsummary"></div>' +
00116     '<table id="logtable">' +
00117     '<thead><tr><th>Status</th><th>Test</th><th>Message</th></tr></thead>' +
00118     '<tbody id="loglines"></tbody>' +
00119     '</table>';
00120     this.logsummary = $('logsummary');
00121     this.loglines = $('loglines');
00122   },
00123   _toHTML: function(txt) {
00124     return txt.escapeHTML().replace(/\n/g,"<br/>");
00125   },
00126   addLinksToResults: function(){ 
00127     $$("tr.failed .nameCell").each( function(td){ // todo: limit to children of this.log
00128       td.title = "Run only this test";
00129       Event.observe(td, 'click', function(){ window.location.search = "?tests=" + td.innerHTML;});
00130     });
00131     $$("tr.passed .nameCell").each( function(td){ // todo: limit to children of this.log
00132       td.title = "Run all tests";
00133       Event.observe(td, 'click', function(){ window.location.search = "";});
00134     });
00135   }
00136 };
00137 
00138 Test.Unit.Runner = Class.create();
00139 Test.Unit.Runner.prototype = {
00140   initialize: function(testcases) {
00141     this.options = Object.extend({
00142       testLog: 'testlog'
00143     }, arguments[1] || {});
00144     this.options.resultsURL = this.parseResultsURLQueryParameter();
00145     this.options.tests      = this.parseTestsQueryParameter();
00146     if (this.options.testLog) {
00147       this.options.testLog = $(this.options.testLog) || null;
00148     }
00149     if(this.options.tests) {
00150       this.tests = [];
00151       for(var i = 0; i < this.options.tests.length; i++) {
00152         if(/^test/.test(this.options.tests[i])) {
00153           this.tests.push(new Test.Unit.Testcase(this.options.tests[i], testcases[this.options.tests[i]], testcases["setup"], testcases["teardown"]));
00154         }
00155       }
00156     } else {
00157       if (this.options.test) {
00158         this.tests = [new Test.Unit.Testcase(this.options.test, testcases[this.options.test], testcases["setup"], testcases["teardown"])];
00159       } else {
00160         this.tests = [];
00161         for(var testcase in testcases) {
00162           if(/^test/.test(testcase)) {
00163             this.tests.push(
00164                new Test.Unit.Testcase(
00165                  this.options.context ? ' -> ' + this.options.titles[testcase] : testcase, 
00166                  testcases[testcase], testcases["setup"], testcases["teardown"]
00167                ));
00168           }
00169         }
00170       }
00171     }
00172     this.currentTest = 0;
00173     this.logger = new Test.Unit.Logger(this.options.testLog);
00174     setTimeout(this.runTests.bind(this), 1000);
00175   },
00176   parseResultsURLQueryParameter: function() {
00177     return window.location.search.parseQuery()["resultsURL"];
00178   },
00179   parseTestsQueryParameter: function(){
00180     if (window.location.search.parseQuery()["tests"]){
00181         return window.location.search.parseQuery()["tests"].split(',');
00182     };
00183   },
00184   // Returns:
00185   //  "ERROR" if there was an error,
00186   //  "FAILURE" if there was a failure, or
00187   //  "SUCCESS" if there was neither
00188   getResult: function() {
00189     var hasFailure = false;
00190     for(var i=0;i<this.tests.length;i++) {
00191       if (this.tests[i].errors > 0) {
00192         return "ERROR";
00193       }
00194       if (this.tests[i].failures > 0) {
00195         hasFailure = true;
00196       }
00197     }
00198     if (hasFailure) {
00199       return "FAILURE";
00200     } else {
00201       return "SUCCESS";
00202     }
00203   },
00204   postResults: function() {
00205     if (this.options.resultsURL) {
00206       new Ajax.Request(this.options.resultsURL, 
00207         { method: 'get', parameters: 'result=' + this.getResult(), asynchronous: false });
00208     }
00209   },
00210   runTests: function() {
00211     var test = this.tests[this.currentTest];
00212     if (!test) {
00213       // finished!
00214       this.postResults();
00215       this.logger.summary(this.summary());
00216       return;
00217     }
00218     if(!test.isWaiting) {
00219       this.logger.start(test.name);
00220     }
00221     test.run();
00222     if(test.isWaiting) {
00223       this.logger.message("Waiting for " + test.timeToWait + "ms");
00224       setTimeout(this.runTests.bind(this), test.timeToWait || 1000);
00225     } else {
00226       this.logger.finish(test.status(), test.summary());
00227       this.currentTest++;
00228       // tail recursive, hopefully the browser will skip the stackframe
00229       this.runTests();
00230     }
00231   },
00232   summary: function() {
00233     var assertions = 0;
00234     var failures = 0;
00235     var errors = 0;
00236     var messages = [];
00237     for(var i=0;i<this.tests.length;i++) {
00238       assertions +=   this.tests[i].assertions;
00239       failures   +=   this.tests[i].failures;
00240       errors     +=   this.tests[i].errors;
00241     }
00242     return (
00243       (this.options.context ? this.options.context + ': ': '') + 
00244       this.tests.length + " tests, " + 
00245       assertions + " assertions, " + 
00246       failures   + " failures, " +
00247       errors     + " errors");
00248   }
00249 };
00250 
00251 Test.Unit.Assertions = Class.create();
00252 Test.Unit.Assertions.prototype = {
00253   initialize: function() {
00254     this.assertions = 0;
00255     this.failures   = 0;
00256     this.errors     = 0;
00257     this.messages   = [];
00258   },
00259   summary: function() {
00260     return (
00261       this.assertions + " assertions, " + 
00262       this.failures   + " failures, " +
00263       this.errors     + " errors" + "\n" +
00264       this.messages.join("\n"));
00265   },
00266   pass: function() {
00267     this.assertions++;
00268   },
00269   fail: function(message) {
00270     this.failures++;
00271     this.messages.push("Failure: " + message);
00272   },
00273   info: function(message) {
00274     this.messages.push("Info: " + message);
00275   },
00276   error: function(error) {
00277     this.errors++;
00278     this.messages.push(error.name + ": "+ error.message + "(" + Test.Unit.inspect(error) +")");
00279   },
00280   status: function() {
00281     if (this.failures > 0) return 'failed';
00282     if (this.errors > 0) return 'error';
00283     return 'passed';
00284   },
00285   assert: function(expression) {
00286     var message = arguments[1] || 'assert: got "' + Test.Unit.inspect(expression) + '"';
00287     try { expression ? this.pass() : 
00288       this.fail(message); }
00289     catch(e) { this.error(e); }
00290   },
00291   assertEqual: function(expected, actual) {
00292     var message = arguments[2] || "assertEqual";
00293     try { (expected == actual) ? this.pass() :
00294       this.fail(message + ': expected "' + Test.Unit.inspect(expected) + 
00295         '", actual "' + Test.Unit.inspect(actual) + '"'); }
00296     catch(e) { this.error(e); }
00297   },
00298   assertInspect: function(expected, actual) {
00299     var message = arguments[2] || "assertInspect";
00300     try { (expected == actual.inspect()) ? this.pass() :
00301       this.fail(message + ': expected "' + Test.Unit.inspect(expected) + 
00302         '", actual "' + Test.Unit.inspect(actual) + '"'); }
00303     catch(e) { this.error(e); }
00304   },
00305   assertEnumEqual: function(expected, actual) {
00306     var message = arguments[2] || "assertEnumEqual";
00307     try { $A(expected).length == $A(actual).length && 
00308       expected.zip(actual).all(function(pair) { return pair[0] == pair[1] }) ?
00309         this.pass() : this.fail(message + ': expected ' + Test.Unit.inspect(expected) + 
00310           ', actual ' + Test.Unit.inspect(actual)); }
00311     catch(e) { this.error(e); }
00312   },
00313   assertNotEqual: function(expected, actual) {
00314     var message = arguments[2] || "assertNotEqual";
00315     try { (expected != actual) ? this.pass() : 
00316       this.fail(message + ': got "' + Test.Unit.inspect(actual) + '"'); }
00317     catch(e) { this.error(e); }
00318   },
00319   assertIdentical: function(expected, actual) { 
00320     var message = arguments[2] || "assertIdentical"; 
00321     try { (expected === actual) ? this.pass() : 
00322       this.fail(message + ': expected "' + Test.Unit.inspect(expected) +  
00323         '", actual "' + Test.Unit.inspect(actual) + '"'); } 
00324     catch(e) { this.error(e); } 
00325   },
00326   assertNotIdentical: function(expected, actual) { 
00327     var message = arguments[2] || "assertNotIdentical"; 
00328     try { !(expected === actual) ? this.pass() : 
00329       this.fail(message + ': expected "' + Test.Unit.inspect(expected) +  
00330         '", actual "' + Test.Unit.inspect(actual) + '"'); } 
00331     catch(e) { this.error(e); } 
00332   },
00333   assertNull: function(obj) {
00334     var message = arguments[1] || 'assertNull';
00335     try { (obj==null) ? this.pass() : 
00336       this.fail(message + ': got "' + Test.Unit.inspect(obj) + '"'); }
00337     catch(e) { this.error(e); }
00338   },
00339   assertMatch: function(expected, actual) {
00340     var message = arguments[2] || 'assertMatch';
00341     var regex = new RegExp(expected);
00342     try { (regex.exec(actual)) ? this.pass() :
00343       this.fail(message + ' : regex: "' +  Test.Unit.inspect(expected) + ' did not match: ' + Test.Unit.inspect(actual) + '"'); }
00344     catch(e) { this.error(e); }
00345   },
00346   assertHidden: function(element) {
00347     var message = arguments[1] || 'assertHidden';
00348     this.assertEqual("none", element.style.display, message);
00349   },
00350   assertNotNull: function(object) {
00351     var message = arguments[1] || 'assertNotNull';
00352     this.assert(object != null, message);
00353   },
00354   assertType: function(expected, actual) {
00355     var message = arguments[2] || 'assertType';
00356     try { 
00357       (actual.constructor == expected) ? this.pass() : 
00358       this.fail(message + ': expected "' + Test.Unit.inspect(expected) +  
00359         '", actual "' + (actual.constructor) + '"'); }
00360     catch(e) { this.error(e); }
00361   },
00362   assertNotOfType: function(expected, actual) {
00363     var message = arguments[2] || 'assertNotOfType';
00364     try { 
00365       (actual.constructor != expected) ? this.pass() : 
00366       this.fail(message + ': expected "' + Test.Unit.inspect(expected) +  
00367         '", actual "' + (actual.constructor) + '"'); }
00368     catch(e) { this.error(e); }
00369   },
00370   assertInstanceOf: function(expected, actual) {
00371     var message = arguments[2] || 'assertInstanceOf';
00372     try { 
00373       (actual instanceof expected) ? this.pass() : 
00374       this.fail(message + ": object was not an instance of the expected type"); }
00375     catch(e) { this.error(e); } 
00376   },
00377   assertNotInstanceOf: function(expected, actual) {
00378     var message = arguments[2] || 'assertNotInstanceOf';
00379     try { 
00380       !(actual instanceof expected) ? this.pass() : 
00381       this.fail(message + ": object was an instance of the not expected type"); }
00382     catch(e) { this.error(e); } 
00383   },
00384   assertRespondsTo: function(method, obj) {
00385     var message = arguments[2] || 'assertRespondsTo';
00386     try {
00387       (obj[method] && typeof obj[method] == 'function') ? this.pass() : 
00388       this.fail(message + ": object doesn't respond to [" + method + "]"); }
00389     catch(e) { this.error(e); }
00390   },
00391   assertReturnsTrue: function(method, obj) {
00392     var message = arguments[2] || 'assertReturnsTrue';
00393     try {
00394       var m = obj[method];
00395       if(!m) m = obj['is'+method.charAt(0).toUpperCase()+method.slice(1)];
00396       m() ? this.pass() : 
00397       this.fail(message + ": method returned false"); }
00398     catch(e) { this.error(e); }
00399   },
00400   assertReturnsFalse: function(method, obj) {
00401     var message = arguments[2] || 'assertReturnsFalse';
00402     try {
00403       var m = obj[method];
00404       if(!m) m = obj['is'+method.charAt(0).toUpperCase()+method.slice(1)];
00405       !m() ? this.pass() : 
00406       this.fail(message + ": method returned true"); }
00407     catch(e) { this.error(e); }
00408   },
00409   assertRaise: function(exceptionName, method) {
00410     var message = arguments[2] || 'assertRaise';
00411     try { 
00412       method();
00413       this.fail(message + ": exception expected but none was raised"); }
00414     catch(e) {
00415       ((exceptionName == null) || (e.name==exceptionName)) ? this.pass() : this.error(e); 
00416     }
00417   },
00418   assertElementsMatch: function() {
00419     var expressions = $A(arguments), elements = $A(expressions.shift());
00420     if (elements.length != expressions.length) {
00421       this.fail('assertElementsMatch: size mismatch: ' + elements.length + ' elements, ' + expressions.length + ' expressions');
00422       return false;
00423     }
00424     elements.zip(expressions).all(function(pair, index) {
00425       var element = $(pair.first()), expression = pair.last();
00426       if (element.match(expression)) return true;
00427       this.fail('assertElementsMatch: (in index ' + index + ') expected ' + expression.inspect() + ' but got ' + element.inspect());
00428     }.bind(this)) && this.pass();
00429   },
00430   assertElementMatches: function(element, expression) {
00431     this.assertElementsMatch([element], expression);
00432   },
00433   benchmark: function(operation, iterations) {
00434     var startAt = new Date();
00435     (iterations || 1).times(operation);
00436     var timeTaken = ((new Date())-startAt);
00437     this.info((arguments[2] || 'Operation') + ' finished ' + 
00438        iterations + ' iterations in ' + (timeTaken/1000)+'s' );
00439     return timeTaken;
00440   },
00441   _isVisible: function(element) {
00442     element = $(element);
00443     if(!element.parentNode) return true;
00444     this.assertNotNull(element);
00445     if(element.style && Element.getStyle(element, 'display') == 'none')
00446       return false;
00447     
00448     return this._isVisible(element.parentNode);
00449   },
00450   assertNotVisible: function(element) {
00451     this.assert(!this._isVisible(element), Test.Unit.inspect(element) + " was not hidden and didn't have a hidden parent either. " + ("" || arguments[1]));
00452   },
00453   assertVisible: function(element) {
00454     this.assert(this._isVisible(element), Test.Unit.inspect(element) + " was not visible. " + ("" || arguments[1]));
00455   },
00456   benchmark: function(operation, iterations) {
00457     var startAt = new Date();
00458     (iterations || 1).times(operation);
00459     var timeTaken = ((new Date())-startAt);
00460     this.info((arguments[2] || 'Operation') + ' finished ' + 
00461        iterations + ' iterations in ' + (timeTaken/1000)+'s' );
00462     return timeTaken;
00463   }
00464 };
00465 
00466 Test.Unit.Testcase = Class.create();
00467 Object.extend(Object.extend(Test.Unit.Testcase.prototype, Test.Unit.Assertions.prototype), {
00468   initialize: function(name, test, setup, teardown) {
00469     Test.Unit.Assertions.prototype.initialize.bind(this)();
00470     this.name           = name;
00471     
00472     if(typeof test == 'string') {
00473       test = test.gsub(/(\.should[^\(]+\()/,'#{0}this,');
00474       test = test.gsub(/(\.should[^\(]+)\(this,\)/,'#{1}(this)');
00475       this.test = function() {
00476         eval('with(this){'+test+'}');
00477       }
00478     } else {
00479       this.test = test || function() {};
00480     }
00481     
00482     this.setup          = setup || function() {};
00483     this.teardown       = teardown || function() {};
00484     this.isWaiting      = false;
00485     this.timeToWait     = 1000;
00486   },
00487   wait: function(time, nextPart) {
00488     this.isWaiting = true;
00489     this.test = nextPart;
00490     this.timeToWait = time;
00491   },
00492   run: function() {
00493     try {
00494       try {
00495         if (!this.isWaiting) this.setup.bind(this)();
00496         this.isWaiting = false;
00497         this.test.bind(this)();
00498       } finally {
00499         if(!this.isWaiting) {
00500           this.teardown.bind(this)();
00501         }
00502       }
00503     }
00504     catch(e) { this.error(e); }
00505   }
00506 });
00507 
00508 // *EXPERIMENTAL* BDD-style testing to please non-technical folk
00509 // This draws many ideas from RSpec http://rspec.rubyforge.org/
00510 
00511 Test.setupBDDExtensionMethods = function(){
00512   var METHODMAP = {
00513     shouldEqual:     'assertEqual',
00514     shouldNotEqual:  'assertNotEqual',
00515     shouldEqualEnum: 'assertEnumEqual',
00516     shouldBeA:       'assertType',
00517     shouldNotBeA:    'assertNotOfType',
00518     shouldBeAn:      'assertType',
00519     shouldNotBeAn:   'assertNotOfType',
00520     shouldBeNull:    'assertNull',
00521     shouldNotBeNull: 'assertNotNull',
00522     
00523     shouldBe:        'assertReturnsTrue',
00524     shouldNotBe:     'assertReturnsFalse',
00525     shouldRespondTo: 'assertRespondsTo'
00526   };
00527   var makeAssertion = function(assertion, args, object) { 
00528         this[assertion].apply(this,(args || []).concat([object]));
00529   };
00530   
00531   Test.BDDMethods = {};   
00532   $H(METHODMAP).each(function(pair) { 
00533     Test.BDDMethods[pair.key] = function() { 
00534        var args = $A(arguments); 
00535        var scope = args.shift(); 
00536        makeAssertion.apply(scope, [pair.value, args, this]); }; 
00537   });
00538   
00539   [Array.prototype, String.prototype, Number.prototype, Boolean.prototype].each(
00540     function(p){ Object.extend(p, Test.BDDMethods) }
00541   );
00542 };
00543 
00544 Test.context = function(name, spec, log){
00545   Test.setupBDDExtensionMethods();
00546   
00547   var compiledSpec = {};
00548   var titles = {};
00549   for(specName in spec) {
00550     switch(specName){
00551       case "setup":
00552       case "teardown":
00553         compiledSpec[specName] = spec[specName];
00554         break;
00555       default:
00556         var testName = 'test'+specName.gsub(/\s+/,'-').camelize();
00557         var body = spec[specName].toString().split('\n').slice(1);
00558         if(/^\{/.test(body[0])) body = body.slice(1);
00559         body.pop();
00560         body = body.map(function(statement){ 
00561           return statement.strip()
00562         });
00563         compiledSpec[testName] = body.join('\n');
00564         titles[testName] = specName;
00565     }
00566   }
00567   new Test.Unit.Runner(compiledSpec, { titles: titles, testLog: log || 'testlog', context: name });
00568 };
 All Data Structures Namespaces Files Functions Variables Enumerations