Home | History | Annotate | Download | only in test
      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 
     10 var JsUnitTest = {
     11   Version: '0.6.0',
     12 };
     13 
     14 var 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
     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 
    200 DrNicTest.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 
    206 DrNicTest.Template = function(template, pattern) {
    207   this.template = template; //template.toString();
    208   this.pattern = pattern || DrNicTest.Template.Pattern;
    209 };
    210 
    211 DrNicTest.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 
    238 DrNicTest.Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
    239 DrNicTest.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/
    246 DrNicTest.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
    270 DrNicTest.Event.addEvent.guid = 1;
    271 
    272 DrNicTest.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 
    283 DrNicTest.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 
    299 DrNicTest.Event.fixEvent = function(event) {
    300   // add W3C standard event methods
    301   event.preventDefault = fixEvent.preventDefault;
    302   event.stopPropagation = fixEvent.stopPropagation;
    303   return event;
    304 };
    305 DrNicTest.Event.fixEvent.preventDefault = function() {
    306   this.returnValue = false;
    307 };
    308 DrNicTest.Event.fixEvent.stopPropagation = function() {
    309   this.cancelBubble = true;
    310 };
    311 
    312 DrNicTest.Unit.Logger = function(element) {
    313   this.element = DrNicTest.$(element);
    314   if (this.element) this._createLogTable();
    315 };
    316 
    317 DrNicTest.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 
    323 DrNicTest.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 
    330 DrNicTest.Unit.Logger.prototype.finish = function(status, summary) {
    331   if (!this.element) return;
    332   this.setStatus(status);
    333   this.message(summary);
    334 };
    335 
    336 DrNicTest.Unit.Logger.prototype.message = function(message) {
    337   if (!this.element) return;
    338   var cell = this.getMessageCell();
    339   cell.innerHTML = this._toHTML(message);
    340 };
    341 
    342 DrNicTest.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 
    348 DrNicTest.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 
    354 DrNicTest.Unit.Logger.prototype.getMessageCell = function() {
    355   var logline = this.getLastLogLine();
    356   return logline.getElementsByTagName('td')[2];
    357 };
    358 
    359 DrNicTest.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 
    368 DrNicTest.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 
    380 DrNicTest.Unit.Logger.prototype._toHTML = function(txt) {
    381   return DrNicTest.escapeHTML(txt).replace(/\n/g,"<br/>");
    382 };
    383 DrNicTest.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 
    391 DrNicTest.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
    405 DrNicTest.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 }
    521 DrNicTest.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 };
    756 DrNicTest.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 
    775 DrNicTest.Unit.Runner.prototype.queryParams = DrNicTest.toQueryParams();
    776 
    777 DrNicTest.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 
    787 DrNicTest.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 
    808 DrNicTest.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 
    825 DrNicTest.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 
    841 DrNicTest.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 
    864 DrNicTest.Unit.Runner.prototype.finish = function() {
    865   this.postResults();
    866   this.logger.summary(this.summary());
    867 };
    868 
    869 DrNicTest.Unit.Runner.prototype.summary = function() {
    870   return new DrNicTest.Template('#{tests} tests, #{assertions} assertions, #{failures} failures, #{errors} errors').evaluate(this.getResult());
    871 };
    872 DrNicTest.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 
    882 for (method in DrNicTest.Unit.Assertions) {
    883   DrNicTest.Unit.Testcase.prototype[method] = DrNicTest.Unit.Assertions[method];
    884 }
    885 
    886 DrNicTest.Unit.Testcase.prototype.isWaiting  = false;
    887 DrNicTest.Unit.Testcase.prototype.timeToWait = 1000;
    888 DrNicTest.Unit.Testcase.prototype.assertions = 0;
    889 DrNicTest.Unit.Testcase.prototype.failures   = 0;
    890 DrNicTest.Unit.Testcase.prototype.errors     = 0;
    891 // DrNicTest.Unit.Testcase.prototype.isRunningFromRake = window.location.port == 4711;
    892 DrNicTest.Unit.Testcase.prototype.isRunningFromRake = window.location.port;
    893 
    894 DrNicTest.Unit.Testcase.prototype.wait = function(time, nextPart) {
    895   this.isWaiting = true;
    896   this.test = nextPart;
    897   this.timeToWait = time;
    898 };
    899 
    900 DrNicTest.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 
    918 DrNicTest.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 
    924 DrNicTest.Unit.Testcase.prototype.pass = function() {
    925   this.assertions++;
    926 };
    927 
    928 DrNicTest.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 
    939 DrNicTest.Unit.Testcase.prototype.info = function(message) {
    940   this.messages.push("Info: " + message);
    941 };
    942 
    943 DrNicTest.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 
    949 DrNicTest.Unit.Testcase.prototype.status = function() {
    950   if (this.failures > 0) return 'failed';
    951   if (this.errors > 0) return 'error';
    952   return 'passed';
    953 };
    954 
    955 DrNicTest.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 
    964 Test = DrNicTest
    965