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,'&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
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