cleaned up tomato genome disclaimer html, fixed validation error
[sgn.git] / js / Scriptaculous / UnitTest.js
blob1506a3656355883a910611382834606f5dd3c144
1 // Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
2 //           (c) 2005-2007 Jon Tirsen (http://www.tirsen.com)
3 //           (c) 2005-2007 Michael Schuerig (http://www.schuerig.de/michael/)
4 //
5 // script.aculo.us is freely distributable under the terms of an MIT-style license.
6 // For details, see the script.aculo.us web site: http://script.aculo.us/
8 // experimental, Firefox-only
10 JSAN.use('Prototype');
12 Event.simulateMouse = function(element, eventName) {
13   var options = Object.extend({
14     pointerX: 0,
15     pointerY: 0,
16     buttons:  0,
17     ctrlKey:  false,
18     altKey:   false,
19     shiftKey: false,
20     metaKey:  false
21   }, arguments[2] || {});
22   var oEvent = document.createEvent("MouseEvents");
23   oEvent.initMouseEvent(eventName, true, true, document.defaultView, 
24     options.buttons, options.pointerX, options.pointerY, options.pointerX, options.pointerY, 
25     options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, 0, $(element));
26   
27   if(this.mark) Element.remove(this.mark);
28   this.mark = document.createElement('div');
29   this.mark.appendChild(document.createTextNode(" "));
30   document.body.appendChild(this.mark);
31   this.mark.style.position = 'absolute';
32   this.mark.style.top = options.pointerY + "px";
33   this.mark.style.left = options.pointerX + "px";
34   this.mark.style.width = "5px";
35   this.mark.style.height = "5px;";
36   this.mark.style.borderTop = "1px solid red;"
37   this.mark.style.borderLeft = "1px solid red;"
38   
39   if(this.step)
40     alert('['+new Date().getTime().toString()+'] '+eventName+'/'+Test.Unit.inspect(options));
41   
42   $(element).dispatchEvent(oEvent);
45 // 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.
46 // You need to downgrade to 1.0.4 for now to get this working
47 // See https://bugzilla.mozilla.org/show_bug.cgi?id=289940 for the fix that fixed too much
48 Event.simulateKey = function(element, eventName) {
49   var options = Object.extend({
50     ctrlKey: false,
51     altKey: false,
52     shiftKey: false,
53     metaKey: false,
54     keyCode: 0,
55     charCode: 0
56   }, arguments[2] || {});
58   var oEvent = document.createEvent("KeyEvents");
59   oEvent.initKeyEvent(eventName, true, true, window, 
60     options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
61     options.keyCode, options.charCode );
62   $(element).dispatchEvent(oEvent);
65 Event.simulateKeys = function(element, command) {
66   for(var i=0; i<command.length; i++) {
67     Event.simulateKey(element,'keypress',{charCode:command.charCodeAt(i)});
68   }
71 var Test = {}
72 Test.Unit = {};
74 // security exception workaround
75 Test.Unit.inspect = Object.inspect;
77 Test.Unit.Logger = Class.create();
78 Test.Unit.Logger.prototype = {
79   initialize: function(log) {
80     this.log = $(log);
81     if (this.log) {
82       this._createLogTable();
83     }
84   },
85   start: function(testName) {
86     if (!this.log) return;
87     this.testName = testName;
88     this.lastLogLine = document.createElement('tr');
89     this.statusCell = document.createElement('td');
90     this.nameCell = document.createElement('td');
91     this.nameCell.className = "nameCell";
92     this.nameCell.appendChild(document.createTextNode(testName));
93     this.messageCell = document.createElement('td');
94     this.lastLogLine.appendChild(this.statusCell);
95     this.lastLogLine.appendChild(this.nameCell);
96     this.lastLogLine.appendChild(this.messageCell);
97     this.loglines.appendChild(this.lastLogLine);
98   },
99   finish: function(status, summary) {
100     if (!this.log) return;
101     this.lastLogLine.className = status;
102     this.statusCell.innerHTML = status;
103     this.messageCell.innerHTML = this._toHTML(summary);
104     this.addLinksToResults();
105   },
106   message: function(message) {
107     if (!this.log) return;
108     this.messageCell.innerHTML = this._toHTML(message);
109   },
110   summary: function(summary) {
111     if (!this.log) return;
112     this.logsummary.innerHTML = this._toHTML(summary);
113   },
114   _createLogTable: function() {
115     this.log.innerHTML =
116     '<div id="logsummary"></div>' +
117     '<table id="logtable">' +
118     '<thead><tr><th>Status</th><th>Test</th><th>Message</th></tr></thead>' +
119     '<tbody id="loglines"></tbody>' +
120     '</table>';
121     this.logsummary = $('logsummary')
122     this.loglines = $('loglines');
123   },
124   _toHTML: function(txt) {
125     return txt.escapeHTML().replace(/\n/g,"<br/>");
126   },
127   addLinksToResults: function(){ 
128     $$("tr.failed .nameCell").each( function(td){ // todo: limit to children of this.log
129       td.title = "Run only this test"
130       Event.observe(td, 'click', function(){ window.location.search = "?tests=" + td.innerHTML;});
131     });
132     $$("tr.passed .nameCell").each( function(td){ // todo: limit to children of this.log
133       td.title = "Run all tests"
134       Event.observe(td, 'click', function(){ window.location.search = "";});
135     });
136   }
139 Test.Unit.Runner = Class.create();
140 Test.Unit.Runner.prototype = {
141   initialize: function(testcases) {
142     this.options = Object.extend({
143       testLog: 'testlog'
144     }, arguments[1] || {});
145     this.options.resultsURL = this.parseResultsURLQueryParameter();
146     this.options.tests      = this.parseTestsQueryParameter();
147     if (this.options.testLog) {
148       this.options.testLog = $(this.options.testLog) || null;
149     }
150     if(this.options.tests) {
151       this.tests = [];
152       for(var i = 0; i < this.options.tests.length; i++) {
153         if(/^test/.test(this.options.tests[i])) {
154           this.tests.push(new Test.Unit.Testcase(this.options.tests[i], testcases[this.options.tests[i]], testcases["setup"], testcases["teardown"]));
155         }
156       }
157     } else {
158       if (this.options.test) {
159         this.tests = [new Test.Unit.Testcase(this.options.test, testcases[this.options.test], testcases["setup"], testcases["teardown"])];
160       } else {
161         this.tests = [];
162         for(var testcase in testcases) {
163           if(/^test/.test(testcase)) {
164             this.tests.push(
165                new Test.Unit.Testcase(
166                  this.options.context ? ' -> ' + this.options.titles[testcase] : testcase, 
167                  testcases[testcase], testcases["setup"], testcases["teardown"]
168                ));
169           }
170         }
171       }
172     }
173     this.currentTest = 0;
174     this.logger = new Test.Unit.Logger(this.options.testLog);
175     setTimeout(this.runTests.bind(this), 1000);
176   },
177   parseResultsURLQueryParameter: function() {
178     return window.location.search.parseQuery()["resultsURL"];
179   },
180   parseTestsQueryParameter: function(){
181     if (window.location.search.parseQuery()["tests"]){
182         return window.location.search.parseQuery()["tests"].split(',');
183     };
184   },
185   // Returns:
186   //  "ERROR" if there was an error,
187   //  "FAILURE" if there was a failure, or
188   //  "SUCCESS" if there was neither
189   getResult: function() {
190     var hasFailure = false;
191     for(var i=0;i<this.tests.length;i++) {
192       if (this.tests[i].errors > 0) {
193         return "ERROR";
194       }
195       if (this.tests[i].failures > 0) {
196         hasFailure = true;
197       }
198     }
199     if (hasFailure) {
200       return "FAILURE";
201     } else {
202       return "SUCCESS";
203     }
204   },
205   postResults: function() {
206     if (this.options.resultsURL) {
207       new Ajax.Request(this.options.resultsURL, 
208         { method: 'get', parameters: 'result=' + this.getResult(), asynchronous: false });
209     }
210   },
211   runTests: function() {
212     var test = this.tests[this.currentTest];
213     if (!test) {
214       // finished!
215       this.postResults();
216       this.logger.summary(this.summary());
217       return;
218     }
219     if(!test.isWaiting) {
220       this.logger.start(test.name);
221     }
222     test.run();
223     if(test.isWaiting) {
224       this.logger.message("Waiting for " + test.timeToWait + "ms");
225       setTimeout(this.runTests.bind(this), test.timeToWait || 1000);
226     } else {
227       this.logger.finish(test.status(), test.summary());
228       this.currentTest++;
229       // tail recursive, hopefully the browser will skip the stackframe
230       this.runTests();
231     }
232   },
233   summary: function() {
234     var assertions = 0;
235     var failures = 0;
236     var errors = 0;
237     var messages = [];
238     for(var i=0;i<this.tests.length;i++) {
239       assertions +=   this.tests[i].assertions;
240       failures   +=   this.tests[i].failures;
241       errors     +=   this.tests[i].errors;
242     }
243     return (
244       (this.options.context ? this.options.context + ': ': '') + 
245       this.tests.length + " tests, " + 
246       assertions + " assertions, " + 
247       failures   + " failures, " +
248       errors     + " errors");
249   }
252 Test.Unit.Assertions = Class.create();
253 Test.Unit.Assertions.prototype = {
254   initialize: function() {
255     this.assertions = 0;
256     this.failures   = 0;
257     this.errors     = 0;
258     this.messages   = [];
259   },
260   summary: function() {
261     return (
262       this.assertions + " assertions, " + 
263       this.failures   + " failures, " +
264       this.errors     + " errors" + "\n" +
265       this.messages.join("\n"));
266   },
267   pass: function() {
268     this.assertions++;
269   },
270   fail: function(message) {
271     this.failures++;
272     this.messages.push("Failure: " + message);
273   },
274   info: function(message) {
275     this.messages.push("Info: " + message);
276   },
277   error: function(error) {
278     this.errors++;
279     this.messages.push(error.name + ": "+ error.message + "(" + Test.Unit.inspect(error) +")");
280   },
281   status: function() {
282     if (this.failures > 0) return 'failed';
283     if (this.errors > 0) return 'error';
284     return 'passed';
285   },
286   assert: function(expression) {
287     var message = arguments[1] || 'assert: got "' + Test.Unit.inspect(expression) + '"';
288     try { expression ? this.pass() : 
289       this.fail(message); }
290     catch(e) { this.error(e); }
291   },
292   assertEqual: function(expected, actual) {
293     var message = arguments[2] || "assertEqual";
294     try { (expected == actual) ? this.pass() :
295       this.fail(message + ': expected "' + Test.Unit.inspect(expected) + 
296         '", actual "' + Test.Unit.inspect(actual) + '"'); }
297     catch(e) { this.error(e); }
298   },
299   assertInspect: function(expected, actual) {
300     var message = arguments[2] || "assertInspect";
301     try { (expected == actual.inspect()) ? this.pass() :
302       this.fail(message + ': expected "' + Test.Unit.inspect(expected) + 
303         '", actual "' + Test.Unit.inspect(actual) + '"'); }
304     catch(e) { this.error(e); }
305   },
306   assertEnumEqual: function(expected, actual) {
307     var message = arguments[2] || "assertEnumEqual";
308     try { $A(expected).length == $A(actual).length && 
309       expected.zip(actual).all(function(pair) { return pair[0] == pair[1] }) ?
310         this.pass() : this.fail(message + ': expected ' + Test.Unit.inspect(expected) + 
311           ', actual ' + Test.Unit.inspect(actual)); }
312     catch(e) { this.error(e); }
313   },
314   assertNotEqual: function(expected, actual) {
315     var message = arguments[2] || "assertNotEqual";
316     try { (expected != actual) ? this.pass() : 
317       this.fail(message + ': got "' + Test.Unit.inspect(actual) + '"'); }
318     catch(e) { this.error(e); }
319   },
320   assertIdentical: function(expected, actual) { 
321     var message = arguments[2] || "assertIdentical"; 
322     try { (expected === actual) ? this.pass() : 
323       this.fail(message + ': expected "' + Test.Unit.inspect(expected) +  
324         '", actual "' + Test.Unit.inspect(actual) + '"'); } 
325     catch(e) { this.error(e); } 
326   },
327   assertNotIdentical: function(expected, actual) { 
328     var message = arguments[2] || "assertNotIdentical"; 
329     try { !(expected === actual) ? this.pass() : 
330       this.fail(message + ': expected "' + Test.Unit.inspect(expected) +  
331         '", actual "' + Test.Unit.inspect(actual) + '"'); } 
332     catch(e) { this.error(e); } 
333   },
334   assertNull: function(obj) {
335     var message = arguments[1] || 'assertNull'
336     try { (obj==null) ? this.pass() : 
337       this.fail(message + ': got "' + Test.Unit.inspect(obj) + '"'); }
338     catch(e) { this.error(e); }
339   },
340   assertMatch: function(expected, actual) {
341     var message = arguments[2] || 'assertMatch';
342     var regex = new RegExp(expected);
343     try { (regex.exec(actual)) ? this.pass() :
344       this.fail(message + ' : regex: "' +  Test.Unit.inspect(expected) + ' did not match: ' + Test.Unit.inspect(actual) + '"'); }
345     catch(e) { this.error(e); }
346   },
347   assertHidden: function(element) {
348     var message = arguments[1] || 'assertHidden';
349     this.assertEqual("none", element.style.display, message);
350   },
351   assertNotNull: function(object) {
352     var message = arguments[1] || 'assertNotNull';
353     this.assert(object != null, message);
354   },
355   assertType: function(expected, actual) {
356     var message = arguments[2] || 'assertType';
357     try { 
358       (actual.constructor == expected) ? this.pass() : 
359       this.fail(message + ': expected "' + Test.Unit.inspect(expected) +  
360         '", actual "' + (actual.constructor) + '"'); }
361     catch(e) { this.error(e); }
362   },
363   assertNotOfType: function(expected, actual) {
364     var message = arguments[2] || 'assertNotOfType';
365     try { 
366       (actual.constructor != expected) ? this.pass() : 
367       this.fail(message + ': expected "' + Test.Unit.inspect(expected) +  
368         '", actual "' + (actual.constructor) + '"'); }
369     catch(e) { this.error(e); }
370   },
371   assertInstanceOf: function(expected, actual) {
372     var message = arguments[2] || 'assertInstanceOf';
373     try { 
374       (actual instanceof expected) ? this.pass() : 
375       this.fail(message + ": object was not an instance of the expected type"); }
376     catch(e) { this.error(e); } 
377   },
378   assertNotInstanceOf: function(expected, actual) {
379     var message = arguments[2] || 'assertNotInstanceOf';
380     try { 
381       !(actual instanceof expected) ? this.pass() : 
382       this.fail(message + ": object was an instance of the not expected type"); }
383     catch(e) { this.error(e); } 
384   },
385   assertRespondsTo: function(method, obj) {
386     var message = arguments[2] || 'assertRespondsTo';
387     try {
388       (obj[method] && typeof obj[method] == 'function') ? this.pass() : 
389       this.fail(message + ": object doesn't respond to [" + method + "]"); }
390     catch(e) { this.error(e); }
391   },
392   assertReturnsTrue: function(method, obj) {
393     var message = arguments[2] || 'assertReturnsTrue';
394     try {
395       var m = obj[method];
396       if(!m) m = obj['is'+method.charAt(0).toUpperCase()+method.slice(1)];
397       m() ? this.pass() : 
398       this.fail(message + ": method returned false"); }
399     catch(e) { this.error(e); }
400   },
401   assertReturnsFalse: function(method, obj) {
402     var message = arguments[2] || 'assertReturnsFalse';
403     try {
404       var m = obj[method];
405       if(!m) m = obj['is'+method.charAt(0).toUpperCase()+method.slice(1)];
406       !m() ? this.pass() : 
407       this.fail(message + ": method returned true"); }
408     catch(e) { this.error(e); }
409   },
410   assertRaise: function(exceptionName, method) {
411     var message = arguments[2] || 'assertRaise';
412     try { 
413       method();
414       this.fail(message + ": exception expected but none was raised"); }
415     catch(e) {
416       ((exceptionName == null) || (e.name==exceptionName)) ? this.pass() : this.error(e); 
417     }
418   },
419   assertElementsMatch: function() {
420     var expressions = $A(arguments), elements = $A(expressions.shift());
421     if (elements.length != expressions.length) {
422       this.fail('assertElementsMatch: size mismatch: ' + elements.length + ' elements, ' + expressions.length + ' expressions');
423       return false;
424     }
425     elements.zip(expressions).all(function(pair, index) {
426       var element = $(pair.first()), expression = pair.last();
427       if (element.match(expression)) return true;
428       this.fail('assertElementsMatch: (in index ' + index + ') expected ' + expression.inspect() + ' but got ' + element.inspect());
429     }.bind(this)) && this.pass();
430   },
431   assertElementMatches: function(element, expression) {
432     this.assertElementsMatch([element], expression);
433   },
434   benchmark: function(operation, iterations) {
435     var startAt = new Date();
436     (iterations || 1).times(operation);
437     var timeTaken = ((new Date())-startAt);
438     this.info((arguments[2] || 'Operation') + ' finished ' + 
439        iterations + ' iterations in ' + (timeTaken/1000)+'s' );
440     return timeTaken;
441   },
442   _isVisible: function(element) {
443     element = $(element);
444     if(!element.parentNode) return true;
445     this.assertNotNull(element);
446     if(element.style && Element.getStyle(element, 'display') == 'none')
447       return false;
448     
449     return this._isVisible(element.parentNode);
450   },
451   assertNotVisible: function(element) {
452     this.assert(!this._isVisible(element), Test.Unit.inspect(element) + " was not hidden and didn't have a hidden parent either. " + ("" || arguments[1]));
453   },
454   assertVisible: function(element) {
455     this.assert(this._isVisible(element), Test.Unit.inspect(element) + " was not visible. " + ("" || arguments[1]));
456   },
457   benchmark: function(operation, iterations) {
458     var startAt = new Date();
459     (iterations || 1).times(operation);
460     var timeTaken = ((new Date())-startAt);
461     this.info((arguments[2] || 'Operation') + ' finished ' + 
462        iterations + ' iterations in ' + (timeTaken/1000)+'s' );
463     return timeTaken;
464   }
467 Test.Unit.Testcase = Class.create();
468 Object.extend(Object.extend(Test.Unit.Testcase.prototype, Test.Unit.Assertions.prototype), {
469   initialize: function(name, test, setup, teardown) {
470     Test.Unit.Assertions.prototype.initialize.bind(this)();
471     this.name           = name;
472     
473     if(typeof test == 'string') {
474       test = test.gsub(/(\.should[^\(]+\()/,'#{0}this,');
475       test = test.gsub(/(\.should[^\(]+)\(this,\)/,'#{1}(this)');
476       this.test = function() {
477         eval('with(this){'+test+'}');
478       }
479     } else {
480       this.test = test || function() {};
481     }
482     
483     this.setup          = setup || function() {};
484     this.teardown       = teardown || function() {};
485     this.isWaiting      = false;
486     this.timeToWait     = 1000;
487   },
488   wait: function(time, nextPart) {
489     this.isWaiting = true;
490     this.test = nextPart;
491     this.timeToWait = time;
492   },
493   run: function() {
494     try {
495       try {
496         if (!this.isWaiting) this.setup.bind(this)();
497         this.isWaiting = false;
498         this.test.bind(this)();
499       } finally {
500         if(!this.isWaiting) {
501           this.teardown.bind(this)();
502         }
503       }
504     }
505     catch(e) { this.error(e); }
506   }
509 // *EXPERIMENTAL* BDD-style testing to please non-technical folk
510 // This draws many ideas from RSpec http://rspec.rubyforge.org/
512 Test.setupBDDExtensionMethods = function(){
513   var METHODMAP = {
514     shouldEqual:     'assertEqual',
515     shouldNotEqual:  'assertNotEqual',
516     shouldEqualEnum: 'assertEnumEqual',
517     shouldBeA:       'assertType',
518     shouldNotBeA:    'assertNotOfType',
519     shouldBeAn:      'assertType',
520     shouldNotBeAn:   'assertNotOfType',
521     shouldBeNull:    'assertNull',
522     shouldNotBeNull: 'assertNotNull',
523     
524     shouldBe:        'assertReturnsTrue',
525     shouldNotBe:     'assertReturnsFalse',
526     shouldRespondTo: 'assertRespondsTo'
527   };
528   var makeAssertion = function(assertion, args, object) { 
529         this[assertion].apply(this,(args || []).concat([object]));
530   }
531   
532   Test.BDDMethods = {};   
533   $H(METHODMAP).each(function(pair) { 
534     Test.BDDMethods[pair.key] = function() { 
535        var args = $A(arguments); 
536        var scope = args.shift(); 
537        makeAssertion.apply(scope, [pair.value, args, this]); }; 
538   });
539   
540   [Array.prototype, String.prototype, Number.prototype, Boolean.prototype].each(
541     function(p){ Object.extend(p, Test.BDDMethods) }
542   );
545 Test.context = function(name, spec, log){
546   Test.setupBDDExtensionMethods();
547   
548   var compiledSpec = {};
549   var titles = {};
550   for(specName in spec) {
551     switch(specName){
552       case "setup":
553       case "teardown":
554         compiledSpec[specName] = spec[specName];
555         break;
556       default:
557         var testName = 'test'+specName.gsub(/\s+/,'-').camelize();
558         var body = spec[specName].toString().split('\n').slice(1);
559         if(/^\{/.test(body[0])) body = body.slice(1);
560         body.pop();
561         body = body.map(function(statement){ 
562           return statement.strip()
563         });
564         compiledSpec[testName] = body.join('\n');
565         titles[testName] = specName;
566     }
567   }
568   new Test.Unit.Runner(compiledSpec, { titles: titles, testLog: log || 'testlog', context: name });