1/* Jsunittest, version 0.6.0 2 * (c) 2008 Dr Nic Williams 3 * 4 * Jsunittest is freely distributable under 5 * the terms of an MIT-style license. 6 * For details, see the web site: http://jsunittest.rubyforge.org 7 * 8 *--------------------------------------------------------------------------*/ 9 10var JsUnitTest = { 11 Version: '0.6.0', 12}; 13 14var DrNicTest = { 15 Unit: {}, 16 inspect: function(object) { 17 try { 18 if (typeof object == "undefined") return 'undefined'; 19 if (object === null) return 'null'; 20 if (typeof object == "string") { 21 var useDoubleQuotes = arguments[1]; 22 var escapedString = this.gsub(object, /[\x00-\x1f\\]/, function(match) { 23 var character = String.specialChar[match[0]]; 24 return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16); 25 }); 26 if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"'; 27 return "'" + escapedString.replace(/'/g, '\\\'') + "'"; 28 }; 29 return String(object); 30 } catch (e) { 31 if (e instanceof RangeError) return '...'; 32 throw e; 33 } 34 }, 35 $: function(element) { 36 if (arguments.length > 1) { 37 for (var i = 0, elements = [], length = arguments.length; i < length; i++) 38 elements.push(this.$(arguments[i])); 39 return elements; 40 } 41 if (typeof element == "string") 42 element = document.getElementById(element); 43 return element; 44 }, 45 gsub: function(source, pattern, replacement) { 46 var result = '', match; 47 replacement = arguments.callee.prepareReplacement(replacement); 48 49 while (source.length > 0) { 50 if (match = source.match(pattern)) { 51 result += source.slice(0, match.index); 52 result += DrNicTest.String.interpret(replacement(match)); 53 source = source.slice(match.index + match[0].length); 54 } else { 55 result += source, source = ''; 56 } 57 } 58 return result; 59 }, 60 scan: function(source, pattern, iterator) { 61 this.gsub(source, pattern, iterator); 62 return String(source); 63 }, 64 escapeHTML: function(data) { 65 return data.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); 66 }, 67 arrayfromargs: function(args) { 68 var myarray = new Array(); 69 var i; 70 71 for (i=0;i<args.length;i++) 72 myarray[i] = args[i]; 73 74 return myarray; 75 }, 76 hashToSortedArray: function(hash) { 77 var results = []; 78 for (key in hash) { 79 results.push([key, hash[key]]); 80 } 81 return results.sort(); 82 }, 83 flattenArray: function(array) { 84 var results = arguments[1] || []; 85 for (var i=0; i < array.length; i++) { 86 var object = array[i]; 87 if (object != null && typeof object == "object" && 88 'splice' in object && 'join' in object) { 89 this.flattenArray(object, results); 90 } else { 91 results.push(object); 92 } 93 }; 94 return results; 95 }, 96 selectorMatch: function(expression, element) { 97 var tokens = []; 98 var patterns = { 99 // combinators must be listed first 100 // (and descendant needs to be last combinator) 101 laterSibling: /^\s*~\s*/, 102 child: /^\s*>\s*/, 103 adjacent: /^\s*\+\s*/, 104 descendant: /^\s/, 105 106 // selectors follow 107 tagName: /^\s*(\*|[\w\-]+)(\b|$)?/, 108 id: /^#([\w\-\*]+)(\b|$)/, 109 className: /^\.([\w\-\*]+)(\b|$)/, 110 pseudo: 111 /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/, 112 attrPresence: /^\[((?:[\w]+:)?[\w]+)\]/, 113 attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ 114 }; 115 116 var assertions = { 117 tagName: function(element, matches) { 118 return matches[1].toUpperCase() == element.tagName.toUpperCase(); 119 }, 120 121 className: function(element, matches) { 122 return Element.hasClassName(element, matches[1]); 123 }, 124 125 id: function(element, matches) { 126 return element.id === matches[1]; 127 }, 128 129 attrPresence: function(element, matches) { 130 return Element.hasAttribute(element, matches[1]); 131 }, 132 133 attr: function(element, matches) { 134 var nodeValue = Element.readAttribute(element, matches[1]); 135 return nodeValue && operators[matches[2]](nodeValue, matches[5] || matches[6]); 136 } 137 }; 138 var e = this.expression, ps = patterns, as = assertions; 139 var le, p, m; 140 141 while (e && le !== e && (/\S/).test(e)) { 142 le = e; 143 for (var i in ps) { 144 p = ps[i]; 145 if (m = e.match(p)) { 146 // use the Selector.assertions methods unless the selector 147 // is too complex. 148 if (as[i]) { 149 tokens.push([i, Object.clone(m)]); 150 e = e.replace(m[0], ''); 151 } 152 } 153 } 154 } 155 156 var match = true, name, matches; 157 for (var i = 0, token; token = tokens[i]; i++) { 158 name = token[0], matches = token[1]; 159 if (!assertions[name](element, matches)) { 160 match = false; break; 161 } 162 } 163 164 return match; 165 }, 166 toQueryParams: function(query, separator) { 167 var query = query || window.location.search; 168 var match = query.replace(/^\s+/, '').replace(/\s+$/, '').match(/([^?#]*)(#.*)?$/); 169 if (!match) return { }; 170 171 var hash = {}; 172 var parts = match[1].split(separator || '&'); 173 for (var i=0; i < parts.length; i++) { 174 var pair = parts[i].split('='); 175 if (pair[0]) { 176 var key = decodeURIComponent(pair.shift()); 177 var value = pair.length > 1 ? pair.join('=') : pair[0]; 178 if (value != undefined) value = decodeURIComponent(value); 179 180 if (key in hash) { 181 var object = hash[key]; 182 var isArray = object != null && typeof object == "object" && 183 'splice' in object && 'join' in object 184 if (!isArray) hash[key] = [hash[key]]; 185 hash[key].push(value); 186 } 187 else hash[key] = value; 188 } 189 }; 190 return hash; 191 }, 192 193 String: { 194 interpret: function(value) { 195 return value == null ? '' : String(value); 196 } 197 } 198}; 199 200DrNicTest.gsub.prepareReplacement = function(replacement) { 201 if (typeof replacement == "function") return replacement; 202 var template = new Template(replacement); 203 return function(match) { return template.evaluate(match) }; 204}; 205 206DrNicTest.Template = function(template, pattern) { 207 this.template = template; //template.toString(); 208 this.pattern = pattern || DrNicTest.Template.Pattern; 209}; 210 211DrNicTest.Template.prototype.evaluate = function(object) { 212 if (typeof object.toTemplateReplacements == "function") 213 object = object.toTemplateReplacements(); 214 215 return DrNicTest.gsub(this.template, this.pattern, function(match) { 216 if (object == null) return ''; 217 218 var before = match[1] || ''; 219 if (before == '\\') return match[2]; 220 221 var ctx = object, expr = match[3]; 222 var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/; 223 match = pattern.exec(expr); 224 if (match == null) return before; 225 226 while (match != null) { 227 var comp = (match[1].indexOf('[]') === 0) ? match[2].gsub('\\\\]', ']') : match[1]; 228 ctx = ctx[comp]; 229 if (null == ctx || '' == match[3]) break; 230 expr = expr.substring('[' == match[3] ? match[1].length : match[0].length); 231 match = pattern.exec(expr); 232 } 233 234 return before + DrNicTest.String.interpret(ctx); 235 }); 236} 237 238DrNicTest.Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; 239DrNicTest.Event = {}; 240// written by Dean Edwards, 2005 241// with input from Tino Zijdel, Matthias Miller, Diego Perini 242// namespaced by Dr Nic Williams 2008 243 244// http://dean.edwards.name/weblog/2005/10/add-event/ 245// http://dean.edwards.name/weblog/2005/10/add-event2/ 246DrNicTest.Event.addEvent = function(element, type, handler) { 247 if (element.addEventListener) { 248 element.addEventListener(type, handler, false); 249 } else { 250 // assign each event handler a unique ID 251 if (!handler.$$guid) handler.$$guid = addEvent.guid++; 252 // create a hash table of event types for the element 253 if (!element.events) element.events = {}; 254 // create a hash table of event handlers for each element/event pair 255 var handlers = element.events[type]; 256 if (!handlers) { 257 handlers = element.events[type] = {}; 258 // store the existing event handler (if there is one) 259 if (element["on" + type]) { 260 handlers[0] = element["on" + type]; 261 } 262 } 263 // store the event handler in the hash table 264 handlers[handler.$$guid] = handler; 265 // assign a global event handler to do all the work 266 element["on" + type] = handleEvent; 267 } 268}; 269// a counter used to create unique IDs 270DrNicTest.Event.addEvent.guid = 1; 271 272DrNicTest.Event.removeEvent = function(element, type, handler) { 273 if (element.removeEventListener) { 274 element.removeEventListener(type, handler, false); 275 } else { 276 // delete the event handler from the hash table 277 if (element.events && element.events[type]) { 278 delete element.events[type][handler.$$guid]; 279 } 280 } 281}; 282 283DrNicTest.Event.handleEvent = function(event) { 284 var returnValue = true; 285 // grab the event object (IE uses a global event object) 286 event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event); 287 // get a reference to the hash table of event handlers 288 var handlers = this.events[event.type]; 289 // execute each event handler 290 for (var i in handlers) { 291 this.$$handleEvent = handlers[i]; 292 if (this.$$handleEvent(event) === false) { 293 returnValue = false; 294 } 295 } 296 return returnValue; 297}; 298 299DrNicTest.Event.fixEvent = function(event) { 300 // add W3C standard event methods 301 event.preventDefault = fixEvent.preventDefault; 302 event.stopPropagation = fixEvent.stopPropagation; 303 return event; 304}; 305DrNicTest.Event.fixEvent.preventDefault = function() { 306 this.returnValue = false; 307}; 308DrNicTest.Event.fixEvent.stopPropagation = function() { 309 this.cancelBubble = true; 310}; 311 312DrNicTest.Unit.Logger = function(element) { 313 this.element = DrNicTest.$(element); 314 if (this.element) this._createLogTable(); 315}; 316 317DrNicTest.Unit.Logger.prototype.start = function(testName) { 318 if (!this.element) return; 319 var tbody = this.element.getElementsByTagName('tbody')[0]; 320 tbody.innerHTML = tbody.innerHTML + '<tr><td>' + testName + '</td><td></td><td></td></tr>'; 321}; 322 323DrNicTest.Unit.Logger.prototype.setStatus = function(status) { 324 var logline = this.getLastLogLine(); 325 logline.className = status; 326 var statusCell = logline.getElementsByTagName('td')[1]; 327 statusCell.innerHTML = status; 328}; 329 330DrNicTest.Unit.Logger.prototype.finish = function(status, summary) { 331 if (!this.element) return; 332 this.setStatus(status); 333 this.message(summary); 334}; 335 336DrNicTest.Unit.Logger.prototype.message = function(message) { 337 if (!this.element) return; 338 var cell = this.getMessageCell(); 339 cell.innerHTML = this._toHTML(message); 340}; 341 342DrNicTest.Unit.Logger.prototype.summary = function(summary) { 343 if (!this.element) return; 344 var div = this.element.getElementsByTagName('div')[0]; 345 div.innerHTML = this._toHTML(summary); 346}; 347 348DrNicTest.Unit.Logger.prototype.getLastLogLine = function() { 349 var tbody = this.element.getElementsByTagName('tbody')[0]; 350 var loglines = tbody.getElementsByTagName('tr'); 351 return loglines[loglines.length - 1]; 352}; 353 354DrNicTest.Unit.Logger.prototype.getMessageCell = function() { 355 var logline = this.getLastLogLine(); 356 return logline.getElementsByTagName('td')[2]; 357}; 358 359DrNicTest.Unit.Logger.prototype._createLogTable = function() { 360 var html = '<div class="logsummary">running...</div>' + 361 '<table class="logtable">' + 362 '<thead><tr><th>Status</th><th>Test</th><th>Message</th></tr></thead>' + 363 '<tbody class="loglines"></tbody>' + 364 '</table>'; 365 this.element.innerHTML = html; 366}; 367 368DrNicTest.Unit.Logger.prototype.appendActionButtons = function(actions) { 369 // actions = $H(actions); 370 // if (!actions.any()) return; 371 // var div = new Element("div", {className: 'action_buttons'}); 372 // actions.inject(div, function(container, action) { 373 // var button = new Element("input").setValue(action.key).observe("click", action.value); 374 // button.type = "button"; 375 // return container.insert(button); 376 // }); 377 // this.getMessageCell().insert(div); 378}; 379 380DrNicTest.Unit.Logger.prototype._toHTML = function(txt) { 381 return DrNicTest.escapeHTML(txt).replace(/\n/g,"<br/>"); 382}; 383DrNicTest.Unit.MessageTemplate = function(string) { 384 var parts = []; 385 var str = DrNicTest.scan((string || ''), /(?=[^\\])\?|(?:\\\?|[^\?])+/, function(part) { 386 parts.push(part[0]); 387 }); 388 this.parts = parts; 389}; 390 391DrNicTest.Unit.MessageTemplate.prototype.evaluate = function(params) { 392 var results = []; 393 for (var i=0; i < this.parts.length; i++) { 394 var part = this.parts[i]; 395 var result = (part == '?') ? DrNicTest.inspect(params.shift()) : part.replace(/\\\?/, '?'); 396 results.push(result); 397 }; 398 return results.join(''); 399}; 400// A generic function for performming AJAX requests 401// It takes one argument, which is an object that contains a set of options 402// All of which are outline in the comments, below 403// From John Resig's book Pro JavaScript Techniques 404// published by Apress, 2006-8 405DrNicTest.ajax = function( options ) { 406 407 // Load the options object with defaults, if no 408 // values were provided by the user 409 options = { 410 // The type of HTTP Request 411 type: options.type || "POST", 412 413 // The URL the request will be made to 414 url: options.url || "", 415 416 // How long to wait before considering the request to be a timeout 417 timeout: options.timeout || 5000, 418 419 // Functions to call when the request fails, succeeds, 420 // or completes (either fail or succeed) 421 onComplete: options.onComplete || function(){}, 422 onError: options.onError || function(){}, 423 onSuccess: options.onSuccess || function(){}, 424 425 // The data type that'll be returned from the server 426 // the default is simply to determine what data was returned from the 427 // and act accordingly. 428 data: options.data || "" 429 }; 430 431 // Create the request object 432 var xml = new XMLHttpRequest(); 433 434 // Open the asynchronous POST request 435 xml.open(options.type, options.url, true); 436 437 // We're going to wait for a request for 5 seconds, before giving up 438 var timeoutLength = 5000; 439 440 // Keep track of when the request has been succesfully completed 441 var requestDone = false; 442 443 // Initalize a callback which will fire 5 seconds from now, cancelling 444 // the request (if it has not already occurred). 445 setTimeout(function(){ 446 requestDone = true; 447 }, timeoutLength); 448 449 // Watch for when the state of the document gets updated 450 xml.onreadystatechange = function(){ 451 // Wait until the data is fully loaded, 452 // and make sure that the request hasn't already timed out 453 if ( xml.readyState == 4 && !requestDone ) { 454 455 // Check to see if the request was successful 456 if ( httpSuccess( xml ) ) { 457 458 // Execute the success callback with the data returned from the server 459 options.onSuccess( httpData( xml, options.type ) ); 460 461 // Otherwise, an error occurred, so execute the error callback 462 } else { 463 options.onError(); 464 } 465 466 // Call the completion callback 467 options.onComplete(); 468 469 // Clean up after ourselves, to avoid memory leaks 470 xml = null; 471 } 472 }; 473 474 // Establish the connection to the server 475 xml.send(); 476 477 // Determine the success of the HTTP response 478 function httpSuccess(r) { 479 try { 480 // If no server status is provided, and we're actually 481 // requesting a local file, then it was successful 482 return !r.status && location.protocol == "file:" || 483 484 // Any status in the 200 range is good 485 ( r.status >= 200 && r.status < 300 ) || 486 487 // Successful if the document has not been modified 488 r.status == 304 || 489 490 // Safari returns an empty status if the file has not been modified 491 navigator.userAgent.indexOf("Safari") >= 0 && typeof r.status == "undefined"; 492 } catch(e){} 493 494 // If checking the status failed, then assume that the request failed too 495 return false; 496 } 497 498 // Extract the correct data from the HTTP response 499 function httpData(r,type) { 500 // Get the content-type header 501 var ct = r.getResponseHeader("content-type"); 502 503 // If no default type was provided, determine if some 504 // form of XML was returned from the server 505 var data = !type && ct && ct.indexOf("xml") >= 0; 506 507 // Get the XML Document object if XML was returned from 508 // the server, otherwise return the text contents returned by the server 509 data = type == "xml" || data ? r.responseXML : r.responseText; 510 511 // If the specified type is "script", execute the returned text 512 // response as if it was JavaScript 513 if ( type == "script" ) 514 eval.call( window, data ); 515 516 // Return the response data (either an XML Document or a text string) 517 return data; 518 } 519 520} 521DrNicTest.Unit.Assertions = { 522 buildMessage: function(message, template) { 523 var args = DrNicTest.arrayfromargs(arguments).slice(2); 524 return (message ? message + '\n' : '') + 525 new DrNicTest.Unit.MessageTemplate(template).evaluate(args); 526 }, 527 528 flunk: function(message) { 529 this.assertBlock(message || 'Flunked', function() { return false }); 530 }, 531 532 assertBlock: function(message, block) { 533 try { 534 block.call(this) ? this.pass() : this.fail(message); 535 } catch(e) { this.error(e) } 536 }, 537 538 assert: function(expression, message) { 539 message = this.buildMessage(message || 'assert', 'got <?>', expression); 540 this.assertBlock(message, function() { return expression }); 541 }, 542 543 assertEqual: function(expected, actual, message) { 544 message = this.buildMessage(message || 'assertEqual', 'expected <?>, actual: <?>', expected, actual); 545 this.assertBlock(message, function() { return expected == actual }); 546 }, 547 548 assertNotEqual: function(expected, actual, message) { 549 message = this.buildMessage(message || 'assertNotEqual', 'expected <?>, actual: <?>', expected, actual); 550 this.assertBlock(message, function() { return expected != actual }); 551 }, 552 553 assertEnumEqual: function(expected, actual, message) { 554 message = this.buildMessage(message || 'assertEnumEqual', 'expected <?>, actual: <?>', expected, actual); 555 var expected_array = DrNicTest.flattenArray(expected); 556 var actual_array = DrNicTest.flattenArray(actual); 557 this.assertBlock(message, function() { 558 if (expected_array.length == actual_array.length) { 559 for (var i=0; i < expected_array.length; i++) { 560 if (expected_array[i] != actual_array[i]) return false; 561 }; 562 return true; 563 } 564 return false; 565 }); 566 }, 567 568 assertEnumNotEqual: function(expected, actual, message) { 569 message = this.buildMessage(message || 'assertEnumNotEqual', '<?> was the same as <?>', expected, actual); 570 var expected_array = DrNicTest.flattenArray(expected); 571 var actual_array = DrNicTest.flattenArray(actual); 572 this.assertBlock(message, function() { 573 if (expected_array.length == actual_array.length) { 574 for (var i=0; i < expected_array.length; i++) { 575 if (expected_array[i] != actual_array[i]) return true; 576 }; 577 return false; 578 } 579 return true; 580 }); 581 }, 582 583 assertHashEqual: function(expected, actual, message) { 584 message = this.buildMessage(message || 'assertHashEqual', 'expected <?>, actual: <?>', expected, actual); 585 var expected_array = DrNicTest.flattenArray(DrNicTest.hashToSortedArray(expected)); 586 var actual_array = DrNicTest.flattenArray(DrNicTest.hashToSortedArray(actual)); 587 var block = function() { 588 if (expected_array.length == actual_array.length) { 589 for (var i=0; i < expected_array.length; i++) { 590 if (expected_array[i] != actual_array[i]) return false; 591 }; 592 return true; 593 } 594 return false; 595 }; 596 this.assertBlock(message, block); 597 }, 598 599 assertHashNotEqual: function(expected, actual, message) { 600 message = this.buildMessage(message || 'assertHashNotEqual', '<?> was the same as <?>', expected, actual); 601 var expected_array = DrNicTest.flattenArray(DrNicTest.hashToSortedArray(expected)); 602 var actual_array = DrNicTest.flattenArray(DrNicTest.hashToSortedArray(actual)); 603 // from now we recursively zip & compare nested arrays 604 var block = function() { 605 if (expected_array.length == actual_array.length) { 606 for (var i=0; i < expected_array.length; i++) { 607 if (expected_array[i] != actual_array[i]) return true; 608 }; 609 return false; 610 } 611 return true; 612 }; 613 this.assertBlock(message, block); 614 }, 615 616 assertIdentical: function(expected, actual, message) { 617 message = this.buildMessage(message || 'assertIdentical', 'expected <?>, actual: <?>', expected, actual); 618 this.assertBlock(message, function() { return expected === actual }); 619 }, 620 621 assertNotIdentical: function(expected, actual, message) { 622 message = this.buildMessage(message || 'assertNotIdentical', 'expected <?>, actual: <?>', expected, actual); 623 this.assertBlock(message, function() { return expected !== actual }); 624 }, 625 626 assertNull: function(obj, message) { 627 message = this.buildMessage(message || 'assertNull', 'got <?>', obj); 628 this.assertBlock(message, function() { return obj === null }); 629 }, 630 631 assertNotNull: function(obj, message) { 632 message = this.buildMessage(message || 'assertNotNull', 'got <?>', obj); 633 this.assertBlock(message, function() { return obj !== null }); 634 }, 635 636 assertUndefined: function(obj, message) { 637 message = this.buildMessage(message || 'assertUndefined', 'got <?>', obj); 638 this.assertBlock(message, function() { return typeof obj == "undefined" }); 639 }, 640 641 assertNotUndefined: function(obj, message) { 642 message = this.buildMessage(message || 'assertNotUndefined', 'got <?>', obj); 643 this.assertBlock(message, function() { return typeof obj != "undefined" }); 644 }, 645 646 assertNullOrUndefined: function(obj, message) { 647 message = this.buildMessage(message || 'assertNullOrUndefined', 'got <?>', obj); 648 this.assertBlock(message, function() { return obj == null }); 649 }, 650 651 assertNotNullOrUndefined: function(obj, message) { 652 message = this.buildMessage(message || 'assertNotNullOrUndefined', 'got <?>', obj); 653 this.assertBlock(message, function() { return obj != null }); 654 }, 655 656 assertMatch: function(expected, actual, message) { 657 message = this.buildMessage(message || 'assertMatch', 'regex <?> did not match <?>', expected, actual); 658 this.assertBlock(message, function() { return new RegExp(expected).exec(actual) }); 659 }, 660 661 assertNoMatch: function(expected, actual, message) { 662 message = this.buildMessage(message || 'assertNoMatch', 'regex <?> matched <?>', expected, actual); 663 this.assertBlock(message, function() { return !(new RegExp(expected).exec(actual)) }); 664 }, 665 666 assertHidden: function(element, message) { 667 message = this.buildMessage(message || 'assertHidden', '? isn\'t hidden.', element); 668 this.assertBlock(message, function() { return element.style.display == 'none' }); 669 }, 670 671 assertInstanceOf: function(expected, actual, message) { 672 message = this.buildMessage(message || 'assertInstanceOf', '<?> was not an instance of the expected type', actual); 673 this.assertBlock(message, function() { return actual instanceof expected }); 674 }, 675 676 assertNotInstanceOf: function(expected, actual, message) { 677 message = this.buildMessage(message || 'assertNotInstanceOf', '<?> was an instance of the expected type', actual); 678 this.assertBlock(message, function() { return !(actual instanceof expected) }); 679 }, 680 681 assertRespondsTo: function(method, obj, message) { 682 message = this.buildMessage(message || 'assertRespondsTo', 'object doesn\'t respond to <?>', method); 683 this.assertBlock(message, function() { return (method in obj && typeof obj[method] == 'function') }); 684 }, 685 686 assertRaise: function(exceptionName, method, message) { 687 message = this.buildMessage(message || 'assertRaise', '<?> exception expected but none was raised', exceptionName); 688 var block = function() { 689 try { 690 method(); 691 return false; 692 } catch(e) { 693 if (e.name == exceptionName) return true; 694 else throw e; 695 } 696 }; 697 this.assertBlock(message, block); 698 }, 699 700 assertNothingRaised: function(method, message) { 701 try { 702 method(); 703 this.assert(true, "Expected nothing to be thrown"); 704 } catch(e) { 705 message = this.buildMessage(message || 'assertNothingRaised', '<?> was thrown when nothing was expected.', e); 706 this.flunk(message); 707 } 708 }, 709 710 _isVisible: function(element) { 711 element = DrNicTest.$(element); 712 if(!element.parentNode) return true; 713 this.assertNotNull(element); 714 if(element.style && element.style.display == 'none') 715 return false; 716 717 return arguments.callee.call(this, element.parentNode); 718 }, 719 720 assertVisible: function(element, message) { 721 message = this.buildMessage(message, '? was not visible.', element); 722 this.assertBlock(message, function() { return this._isVisible(element) }); 723 }, 724 725 assertNotVisible: function(element, message) { 726 message = this.buildMessage(message, '? was not hidden and didn\'t have a hidden parent either.', element); 727 this.assertBlock(message, function() { return !this._isVisible(element) }); 728 }, 729 730 assertElementsMatch: function() { 731 var pass = true, expressions = DrNicTest.arrayfromargs(arguments); 732 var elements = expressions.shift(); 733 if (elements.length != expressions.length) { 734 message = this.buildMessage('assertElementsMatch', 'size mismatch: ? elements, ? expressions (?).', elements.length, expressions.length, expressions); 735 this.flunk(message); 736 pass = false; 737 } 738 for (var i=0; i < expressions.length; i++) { 739 var expression = expressions[i]; 740 var element = DrNicTest.$(elements[i]); 741 if (DrNicTest.selectorMatch(expression, element)) { 742 pass = true; 743 break; 744 } 745 message = this.buildMessage('assertElementsMatch', 'In index <?>: expected <?> but got ?', index, expression, element); 746 this.flunk(message); 747 pass = false; 748 }; 749 this.assert(pass, "Expected all elements to match."); 750 }, 751 752 assertElementMatches: function(element, expression, message) { 753 this.assertElementsMatch([element], expression); 754 } 755}; 756DrNicTest.Unit.Runner = function(testcases) { 757 var argumentOptions = arguments[1] || {}; 758 var options = this.options = {}; 759 options.testLog = ('testLog' in argumentOptions) ? argumentOptions.testLog : 'testlog'; 760 options.resultsURL = this.queryParams.resultsURL; 761 options.testLog = DrNicTest.$(options.testLog); 762 763 this.tests = this.getTests(testcases); 764 this.currentTest = 0; 765 this.logger = new DrNicTest.Unit.Logger(options.testLog); 766 767 var self = this; 768 DrNicTest.Event.addEvent(window, "load", function() { 769 setTimeout(function() { 770 self.runTests(); 771 }, 0.1); 772 }); 773}; 774 775DrNicTest.Unit.Runner.prototype.queryParams = DrNicTest.toQueryParams(); 776 777DrNicTest.Unit.Runner.prototype.portNumber = function() { 778 if (window.location.search.length > 0) { 779 var matches = window.location.search.match(/\:(\d{3,5})\//); 780 if (matches) { 781 return parseInt(matches[1]); 782 } 783 } 784 return null; 785}; 786 787DrNicTest.Unit.Runner.prototype.getTests = function(testcases) { 788 var tests = [], options = this.options; 789 if (this.queryParams.tests) tests = this.queryParams.tests.split(','); 790 else if (options.tests) tests = options.tests; 791 else if (options.test) tests = [option.test]; 792 else { 793 for (testname in testcases) { 794 if (testname.match(/^test/)) tests.push(testname); 795 } 796 } 797 var results = []; 798 for (var i=0; i < tests.length; i++) { 799 var test = tests[i]; 800 if (testcases[test]) 801 results.push( 802 new DrNicTest.Unit.Testcase(test, testcases[test], testcases.setup, testcases.teardown) 803 ); 804 }; 805 return results; 806}; 807 808DrNicTest.Unit.Runner.prototype.getResult = function() { 809 var results = { 810 tests: this.tests.length, 811 assertions: 0, 812 failures: 0, 813 errors: 0 814 }; 815 816 for (var i=0; i < this.tests.length; i++) { 817 var test = this.tests[i]; 818 results.assertions += test.assertions; 819 results.failures += test.failures; 820 results.errors += test.errors; 821 }; 822 return results; 823}; 824 825DrNicTest.Unit.Runner.prototype.postResults = function() { 826 if (this.options.resultsURL) { 827 // new Ajax.Request(this.options.resultsURL, 828 // { method: 'get', parameters: this.getResult(), asynchronous: false }); 829 var results = this.getResult(); 830 var url = this.options.resultsURL + "?"; 831 url += "assertions="+ results.assertions + "&"; 832 url += "failures=" + results.failures + "&"; 833 url += "errors=" + results.errors; 834 DrNicTest.ajax({ 835 url: url, 836 type: 'GET' 837 }) 838 } 839}; 840 841DrNicTest.Unit.Runner.prototype.runTests = function() { 842 var test = this.tests[this.currentTest], actions; 843 844 if (!test) return this.finish(); 845 if (!test.isWaiting) this.logger.start(test.name); 846 test.run(); 847 var self = this; 848 if(test.isWaiting) { 849 this.logger.message("Waiting for " + test.timeToWait + "ms"); 850 // setTimeout(this.runTests.bind(this), test.timeToWait || 1000); 851 setTimeout(function() { 852 self.runTests(); 853 }, test.timeToWait || 1000); 854 return; 855 } 856 857 this.logger.finish(test.status(), test.summary()); 858 if (actions = test.actions) this.logger.appendActionButtons(actions); 859 this.currentTest++; 860 // tail recursive, hopefully the browser will skip the stackframe 861 this.runTests(); 862}; 863 864DrNicTest.Unit.Runner.prototype.finish = function() { 865 this.postResults(); 866 this.logger.summary(this.summary()); 867}; 868 869DrNicTest.Unit.Runner.prototype.summary = function() { 870 return new DrNicTest.Template('#{tests} tests, #{assertions} assertions, #{failures} failures, #{errors} errors').evaluate(this.getResult()); 871}; 872DrNicTest.Unit.Testcase = function(name, test, setup, teardown) { 873 this.name = name; 874 this.test = test || function() {}; 875 this.setup = setup || function() {}; 876 this.teardown = teardown || function() {}; 877 this.messages = []; 878 this.actions = {}; 879}; 880// import DrNicTest.Unit.Assertions 881 882for (method in DrNicTest.Unit.Assertions) { 883 DrNicTest.Unit.Testcase.prototype[method] = DrNicTest.Unit.Assertions[method]; 884} 885 886DrNicTest.Unit.Testcase.prototype.isWaiting = false; 887DrNicTest.Unit.Testcase.prototype.timeToWait = 1000; 888DrNicTest.Unit.Testcase.prototype.assertions = 0; 889DrNicTest.Unit.Testcase.prototype.failures = 0; 890DrNicTest.Unit.Testcase.prototype.errors = 0; 891// DrNicTest.Unit.Testcase.prototype.isRunningFromRake = window.location.port == 4711; 892DrNicTest.Unit.Testcase.prototype.isRunningFromRake = window.location.port; 893 894DrNicTest.Unit.Testcase.prototype.wait = function(time, nextPart) { 895 this.isWaiting = true; 896 this.test = nextPart; 897 this.timeToWait = time; 898}; 899 900DrNicTest.Unit.Testcase.prototype.run = function(rethrow) { 901 try { 902 try { 903 if (!this.isWaiting) this.setup(); 904 this.isWaiting = false; 905 this.test(); 906 } finally { 907 if(!this.isWaiting) { 908 this.teardown(); 909 } 910 } 911 } 912 catch(e) { 913 if (rethrow) throw e; 914 this.error(e, this); 915 } 916}; 917 918DrNicTest.Unit.Testcase.prototype.summary = function() { 919 var msg = '#{assertions} assertions, #{failures} failures, #{errors} errors\n'; 920 return new DrNicTest.Template(msg).evaluate(this) + 921 this.messages.join("\n"); 922}; 923 924DrNicTest.Unit.Testcase.prototype.pass = function() { 925 this.assertions++; 926}; 927 928DrNicTest.Unit.Testcase.prototype.fail = function(message) { 929 this.failures++; 930 var line = ""; 931 try { 932 throw new Error("stack"); 933 } catch(e){ 934 line = (/\.html:(\d+)/.exec(e.stack || '') || ['',''])[1]; 935 } 936 this.messages.push("Failure: " + message + (line ? " Line #" + line : "")); 937}; 938 939DrNicTest.Unit.Testcase.prototype.info = function(message) { 940 this.messages.push("Info: " + message); 941}; 942 943DrNicTest.Unit.Testcase.prototype.error = function(error, test) { 944 this.errors++; 945 this.actions['retry with throw'] = function() { test.run(true) }; 946 this.messages.push(error.name + ": "+ error.message + "(" + DrNicTest.inspect(error) + ")"); 947}; 948 949DrNicTest.Unit.Testcase.prototype.status = function() { 950 if (this.failures > 0) return 'failed'; 951 if (this.errors > 0) return 'error'; 952 return 'passed'; 953}; 954 955DrNicTest.Unit.Testcase.prototype.benchmark = function(operation, iterations) { 956 var startAt = new Date(); 957 (iterations || 1).times(operation); 958 var timeTaken = ((new Date())-startAt); 959 this.info((arguments[2] || 'Operation') + ' finished ' + 960 iterations + ' iterations in ' + (timeTaken/1000)+'s' ); 961 return timeTaken; 962}; 963 964Test = DrNicTest 965