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