unittest.js 20 KB

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