- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / common / extensions / docs / examples / extensions / proxy_configuration / test / jsunittest.js
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