Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/spinoffs/scriptaculous/src/unittest.js

Revision 7281, 19.7 kB (checked in by madrobby, 3 years ago)

script.aculo.us: Make BDD-style testing compatible with IE6 and IE7. Closes #8972.

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