1// script.aculo.us unittest.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009
2
3// Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
4//           (c) 2005-2009 Jon Tirsen (http://www.tirsen.com)
5//           (c) 2005-2009 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/
9
10// experimental, Firefox-only
11Event.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);
42};
43
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
47Event.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] || {});
56
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);
62};
63
64Event.simulateKeys = function(element, command) {
65  for(var i=0; i<command.length; i++) {
66    Event.simulateKey(element,'keypress',{charCode:command.charCodeAt(i)});
67  }
68};
69
70var Test = {};
71Test.Unit = {};
72
73// security exception workaround
74Test.Unit.inspect = Object.inspect;
75
76Test.Unit.Logger = Class.create();
77Test.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  }
136};
137
138Test.Unit.Runner = Class.create();
139Test.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  }
249};
250
251Test.Unit.Assertions = Class.create();
252Test.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  }
464};
465
466Test.Unit.Testcase = Class.create();
467Object.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  }
506});
507
508// *EXPERIMENTAL* BDD-style testing to please non-technical folk
509// This draws many ideas from RSpec http://rspec.rubyforge.org/
510
511Test.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  );
542};
543
544Test.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 });
568};