Browse Source

Setting up unit tests with doh.

jrburke 13 years ago
parent
commit
68f79ed4c1

+ 0 - 1
amdefine.js

@@ -24,7 +24,6 @@ function normalize(name, baseName) {
 
 function makeNormalize(relName) {
     return function (name) {
-debugger;
         return normalize(name, relName);
     };
 }

+ 32 - 0
tests/all.js

@@ -0,0 +1,32 @@
+/*jslint strict: false, evil: true */
+/*global Packages: false, process: false, require: true, define: true, doh: false */
+
+//A hack to doh to avoid dojo setup stuff in doh/runner.js
+var skipDohSetup = true,
+    fs, vm, load, env;
+
+(function () {
+    if (typeof Packages !== 'undefined') {
+        env = 'rhino';
+    } else if (typeof process !== 'undefined') {
+        env = 'node';
+
+        fs = require('fs');
+        vm = require('vm');
+
+        load = function (path) {
+            return vm.runInThisContext(fs.readFileSync(path, 'utf8'), path);
+        };
+    }
+
+}());
+
+//Load the tests.
+load("doh/runner.js");
+load('doh/_' + env + 'Runner.js');
+
+require("./basic/basic-tests");
+require("./plugins/relative/relative-tests");
+
+//Print out the final report
+doh.run();

+ 5 - 3
tests/basic/a.js

@@ -2,7 +2,9 @@
 if (typeof define !== 'function') { var define = (require('../../amdefine'))(module); }
 
 define(['./b', './sub/nested/d'], function (b, d) {
-    console.log('b === ' + b.name);
-    console.log('d === ' + d.name);
-    console.log('c === ' + d.cName);
+    return {
+        name: 'a',
+        b: b,
+        d: d
+    };
 });

+ 15 - 0
tests/basic/basic-tests.js

@@ -0,0 +1,15 @@
+doh.register(
+    "simple",
+    [
+        function simple(t){
+            var a = require('./a');
+
+            t.is('a', a.name);
+            t.is('b', a.b.name);
+            t.is('d', a.d.name);
+            t.is('c', a.d.cName);
+        }
+    ]
+);
+
+doh.run();

+ 195 - 0
tests/doh/LICENSE

@@ -0,0 +1,195 @@
+Dojo is available under *either* the terms of the modified BSD license *or* the
+Academic Free License version 2.1. As a recipient of Dojo, you may choose which
+license to receive this code under (except as noted in per-module LICENSE
+files). Some modules may not be the copyright of the Dojo Foundation. These
+modules contain explicit declarations of copyright in both the LICENSE files in
+the directories in which they reside and in the code itself. No external
+contributions are allowed under licenses which are fundamentally incompatible
+with the AFL or BSD licenses that Dojo is distributed under.
+
+The text of the AFL and BSD licenses is reproduced below. 
+
+-------------------------------------------------------------------------------
+The "New" BSD License:
+**********************
+
+Copyright (c) 2005-2009, The Dojo Foundation
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+  * Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation
+    and/or other materials provided with the distribution.
+  * Neither the name of the Dojo Foundation nor the names of its contributors
+    may be used to endorse or promote products derived from this software
+    without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+-------------------------------------------------------------------------------
+The Academic Free License, v. 2.1:
+**********************************
+
+This Academic Free License (the "License") applies to any original work of
+authorship (the "Original Work") whose owner (the "Licensor") has placed the
+following notice immediately following the copyright notice for the Original
+Work:
+
+Licensed under the Academic Free License version 2.1
+
+1) Grant of Copyright License. Licensor hereby grants You a world-wide,
+royalty-free, non-exclusive, perpetual, sublicenseable license to do the
+following:
+
+a) to reproduce the Original Work in copies;
+
+b) to prepare derivative works ("Derivative Works") based upon the Original
+Work;
+
+c) to distribute copies of the Original Work and Derivative Works to the
+public;
+
+d) to perform the Original Work publicly; and
+
+e) to display the Original Work publicly.
+
+2) Grant of Patent License. Licensor hereby grants You a world-wide,
+royalty-free, non-exclusive, perpetual, sublicenseable license, under patent
+claims owned or controlled by the Licensor that are embodied in the Original
+Work as furnished by the Licensor, to make, use, sell and offer for sale the
+Original Work and Derivative Works.
+
+3) Grant of Source Code License. The term "Source Code" means the preferred
+form of the Original Work for making modifications to it and all available
+documentation describing how to modify the Original Work. Licensor hereby
+agrees to provide a machine-readable copy of the Source Code of the Original
+Work along with each copy of the Original Work that Licensor distributes.
+Licensor reserves the right to satisfy this obligation by placing a
+machine-readable copy of the Source Code in an information repository
+reasonably calculated to permit inexpensive and convenient access by You for as
+long as Licensor continues to distribute the Original Work, and by publishing
+the address of that information repository in a notice immediately following
+the copyright notice that applies to the Original Work.
+
+4) Exclusions From License Grant. Neither the names of Licensor, nor the names
+of any contributors to the Original Work, nor any of their trademarks or
+service marks, may be used to endorse or promote products derived from this
+Original Work without express prior written permission of the Licensor. Nothing
+in this License shall be deemed to grant any rights to trademarks, copyrights,
+patents, trade secrets or any other intellectual property of Licensor except as
+expressly stated herein. No patent license is granted to make, use, sell or
+offer to sell embodiments of any patent claims other than the licensed claims
+defined in Section 2. No right is granted to the trademarks of Licensor even if
+such marks are included in the Original Work. Nothing in this License shall be
+interpreted to prohibit Licensor from licensing under different terms from this
+License any Original Work that Licensor otherwise would have a right to
+license.
+
+5) This section intentionally omitted.
+
+6) Attribution Rights. You must retain, in the Source Code of any Derivative
+Works that You create, all copyright, patent or trademark notices from the
+Source Code of the Original Work, as well as any notices of licensing and any
+descriptive text identified therein as an "Attribution Notice." You must cause
+the Source Code for any Derivative Works that You create to carry a prominent
+Attribution Notice reasonably calculated to inform recipients that You have
+modified the Original Work.
+
+7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that
+the copyright in and to the Original Work and the patent rights granted herein
+by Licensor are owned by the Licensor or are sublicensed to You under the terms
+of this License with the permission of the contributor(s) of those copyrights
+and patent rights. Except as expressly stated in the immediately proceeding
+sentence, the Original Work is provided under this License on an "AS IS" BASIS
+and WITHOUT WARRANTY, either express or implied, including, without limitation,
+the warranties of NON-INFRINGEMENT, MERCHANTABILITY or FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU.
+This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No
+license to Original Work is granted hereunder except under this disclaimer.
+
+8) Limitation of Liability. Under no circumstances and under no legal theory,
+whether in tort (including negligence), contract, or otherwise, shall the
+Licensor be liable to any person for any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License
+or the use of the Original Work including, without limitation, damages for loss
+of goodwill, work stoppage, computer failure or malfunction, or any and all
+other commercial damages or losses. This limitation of liability shall not
+apply to liability for death or personal injury resulting from Licensor's
+negligence to the extent applicable law prohibits such limitation. Some
+jurisdictions do not allow the exclusion or limitation of incidental or
+consequential damages, so this exclusion and limitation may not apply to You.
+
+9) Acceptance and Termination. If You distribute copies of the Original Work or
+a Derivative Work, You must make a reasonable effort under the circumstances to
+obtain the express assent of recipients to the terms of this License. Nothing
+else but this License (or another written agreement between Licensor and You)
+grants You permission to create Derivative Works based upon the Original Work
+or to exercise any of the rights granted in Section 1 herein, and any attempt
+to do so except under the terms of this License (or another written agreement
+between Licensor and You) is expressly prohibited by U.S. copyright law, the
+equivalent laws of other countries, and by international treaty. Therefore, by
+exercising any of the rights granted to You in Section 1 herein, You indicate
+Your acceptance of this License and all of its terms and conditions.
+
+10) Termination for Patent Action. This License shall terminate automatically
+and You may no longer exercise any of the rights granted to You by this License
+as of the date You commence an action, including a cross-claim or counterclaim,
+against Licensor or any licensee alleging that the Original Work infringes a
+patent. This termination provision shall not apply for an action alleging
+patent infringement by combinations of the Original Work with other software or
+hardware.
+
+11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this
+License may be brought only in the courts of a jurisdiction wherein the
+Licensor resides or in which Licensor conducts its primary business, and under
+the laws of that jurisdiction excluding its conflict-of-law provisions. The
+application of the United Nations Convention on Contracts for the International
+Sale of Goods is expressly excluded. Any use of the Original Work outside the
+scope of this License or after its termination shall be subject to the
+requirements and penalties of the U.S. Copyright Act, 17 U.S.C. § 101 et
+seq., the equivalent laws of other countries, and international treaty. This
+section shall survive the termination of this License.
+
+12) Attorneys Fees. In any action to enforce the terms of this License or
+seeking damages relating thereto, the prevailing party shall be entitled to
+recover its costs and expenses, including, without limitation, reasonable
+attorneys' fees and costs incurred in connection with such action, including
+any appeal of such action. This section shall survive the termination of this
+License.
+
+13) Miscellaneous. This License represents the complete agreement concerning
+the subject matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent necessary to
+make it enforceable.
+
+14) Definition of "You" in This License. "You" throughout this License, whether
+in upper or lower case, means an individual or a legal entity exercising rights
+under, and complying with all of the terms of, this License. For legal
+entities, "You" includes any entity that controls, is controlled by, or is
+under common control with you. For purposes of this definition, "control" means
+(i) the power, direct or indirect, to cause the direction or management of such
+entity, whether by contract or otherwise, or (ii) ownership of fifty percent
+(50%) or more of the outstanding shares, or (iii) beneficial ownership of such
+entity.
+
+15) Right to Use. You may use the Original Work in all ways not otherwise
+restricted or conditioned by this License or by law, and Licensor promises not
+to interfere with or be responsible for such uses by You.
+
+This license is Copyright (C) 2003-2004 Lawrence E. Rosen. All rights reserved.
+Permission is hereby granted to copy and distribute this license without
+modification. This license may not be modified without the express written
+permission of its copyright owner.

+ 12 - 0
tests/doh/README

@@ -0,0 +1,12 @@
+DOH may be run standalone by issuing a command like the following:
+
+java -jar ../shrinksafe/js.jar runner.js testModule=tests.colors
+
+where the testModule argument is optional and shrinksafe/js.jar is just a
+convenient copy of the Rhino JavaScript engine -- the custom patch is not
+required.
+
+Optional arguments include:
+ * dojoUrl - specifies the location of dojo.js
+ * testUrl - specifies a Javascript file to load with initialization code
+ * testModule - specifies a test module in the dojo package namespace

+ 855 - 0
tests/doh/_browserRunner.js

@@ -0,0 +1,855 @@
+if(window["dojo"]){
+	dojo.provide("doh._browserRunner");
+}
+
+// FIXME: need to add prompting for monkey-do testing
+
+(function(){
+
+	doh.setTimeout = function (fn, time) {
+            return setTimeout(fn, time);
+        };
+
+	try{
+		var topdog = (window.parent == window) || !Boolean(window.parent.doh);
+	}catch(e){
+		//can't access window.parent.doh, then consider ourselves as topdog
+		topdog=true;
+	}
+	if(topdog){
+		// we're the top-dog window.
+
+		// borrowed from Dojo, etc.
+		var byId = function(id){
+			return document.getElementById(id);
+		};
+
+		var _addOnEvt = function(	type,		// string
+									refOrName,	// function or string
+									scope){		// object, defaults is window
+
+			if(!scope){ scope = window; }
+
+			var funcRef = refOrName;
+			if(typeof refOrName == "string"){
+				funcRef = scope[refOrName];
+			}
+			var enclosedFunc = function(){ return funcRef.apply(scope, arguments); };
+
+			if((window["dojo"])&&(type == "load")){
+				dojo.addOnLoad(enclosedFunc);
+			}else{
+				if(window["attachEvent"]){
+					window.attachEvent("on"+type, enclosedFunc);
+				}else if(window["addEventListener"]){
+					window.addEventListener(type, enclosedFunc, false);
+				}else if(document["addEventListener"]){
+					document.addEventListener(type, enclosedFunc, false);
+				}
+			}
+		};
+
+		//
+		// Over-ride or implement base runner.js-provided methods
+		//
+		var escapeXml = function(str){
+			//summary:
+			//		Adds escape sequences for special characters in XML: &<>"'
+			//		Optionally skips escapes for single quotes
+			return str.replace(/&/gm, "&amp;").replace(/</gm, "&lt;").replace(/>/gm, "&gt;").replace(/"/gm, "&quot;"); // string
+		};
+
+		var _logBacklog = [], _loggedMsgLen = 0;
+		var sendToLogPane = function(args, skip){
+			var msg = "";
+			for(var x=0; x<args.length; x++){
+				msg += " "+args[x];
+			}
+
+			msg = escapeXml(msg);
+
+			// workarounds for IE. Wheeee!!!
+			msg = msg.replace("\t", "&nbsp;&nbsp;&nbsp;&nbsp;")
+				.replace(" ", "&nbsp;")
+				.replace("\n", "<br>&nbsp;");
+			if(!byId("logBody")){
+				_logBacklog.push(msg);
+				return;
+			}else if(_logBacklog.length && !skip){
+				var tm;
+				while((tm=_logBacklog.shift())){
+					sendToLogPane(tm, true);
+				}
+			}
+			var logBody=byId("logBody");
+			var tn = document.createElement("div");
+			tn.innerHTML = msg;
+			//tn.id="logmsg_"+logBody.childNodes.length;
+			logBody.appendChild(tn);
+			_loggedMsgLen++;
+		}
+
+		var findTarget = function(n){
+			while(n && !n.getAttribute('_target')){
+				n=n.parentNode;
+				if(!n.getAttribute){
+					n=null;
+				}
+			}
+			return n;
+		}
+
+		doh._jumpToLog = function(e){
+			//console.log(e);
+
+			var node = findTarget(e?e.target:window.event.srcElement);
+			if(!node){
+				return;
+			}
+			var _t = Number(node.getAttribute('_target'));
+			var lb = byId("logBody");
+			if(_t>=lb.childNodes.length){
+				return;
+			}
+			var t = lb.childNodes[_t];
+			t.scrollIntoView();
+			if(window.dojo){
+				//t.parentNode.parentNode is <div class="tabBody">, only it has a explicitly set background-color,
+				//all children of it are transparent
+				var bgColor = dojo.style(t.parentNode.parentNode,'backgroundColor');
+				//node.parentNode is the tr which has background-color set explicitly
+				var hicolor = dojo.style(node.parentNode,'backgroundColor');
+				var unhilight = dojo.animateProperty({
+					node: t,
+					duration: 500,
+					properties:
+					{
+						backgroundColor: { start:hicolor, end: bgColor }
+					},
+					onEnd: function(){
+						t.style.backgroundColor="";
+					}
+				});
+				var hilight = dojo.animateProperty({
+					node: t,
+					duration: 500,
+					properties:
+					{
+						backgroundColor: { start:bgColor, end: hicolor }
+					},
+					onEnd: function(){
+						unhilight.play();
+					}
+				});
+				hilight.play();
+			}
+		};
+
+		doh._jumpToSuite = function(e){
+			var node = findTarget(e ? e.target : window.event.srcElement);
+			if(!node){
+				return;
+			}
+			var _g = node.getAttribute('_target');
+			var gn = getGroupNode(_g);
+			if(!gn){
+				return;
+			}
+			gn.scrollIntoView();
+		};
+
+		doh._init = (function(oi){
+			return function(){
+				var lb = byId("logBody");
+				if(lb){
+					// clear the console before each run
+					while(lb.firstChild){
+						lb.removeChild(lb.firstChild);
+					}
+					_loggedMsgLen = 0;
+				}
+				this._totalTime = 0;
+				this._suiteCount = 0;
+				oi.apply(doh, arguments);
+			}
+		})(doh._init);
+
+		doh._setupGroupForRun = (function(os){
+			//overload _setupGroupForRun to record which log line to jump to when a suite is clicked
+			return function(groupName){
+				var tg = doh._groups[groupName];
+				doh._curTestCount = tg.length;
+				doh._curGroupCount = 1;
+				var gn = getGroupNode(groupName);
+				if(gn){
+					//two lines will be added, scroll the second line into view
+					gn.getElementsByTagName("td")[2].setAttribute('_target',_loggedMsgLen+1);
+				}
+				os.apply(doh,arguments);
+			}
+		})(doh._setupGroupForRun);
+
+		doh._report = (function(or){
+			//overload _report to insert a tfoot
+			return function(){
+				var tb = byId("testList");
+				if(tb){
+					var tfoots=tb.getElementsByTagName('tfoot');
+					if(tfoots.length){
+						tb.removeChild(tfoots[0]);
+					}
+					var foot = tb.createTFoot();
+					var row = foot.insertRow(-1);
+					row.className = 'inProgress';
+					var cell=row.insertCell(-1);
+					cell.colSpan=2;
+					cell.innerHTML="Result";
+					cell = row.insertCell(-1);
+					cell.innerHTML=this._testCount+" tests in "+this._groupCount+" groups /<span class='failure'>"+this._errorCount+"</span> errors, <span class='failure'>"+this._failureCount+"</span> failures";
+					cell.setAttribute('_target',_loggedMsgLen+1);
+					row.insertCell(-1).innerHTML=doh._totalTime+"ms";
+				}
+
+				//This location can do the final performance rendering for the results
+				//of any performance tests.
+				var plotResults = null;
+				var standby;
+				if(doh.perfTestResults){
+					if(window.dojo){
+						//If we have dojo and here are perf tests results,
+						//well, we'll use the dojo charting functions
+						dojo.require("dojox.charting.Chart2D");
+						dojo.require("dojox.charting.DataChart");
+						dojo.require("dojox.charting.plot2d.Scatter");
+						dojo.require("dojox.charting.plot2d.Lines");
+						dojo.require("dojo.data.ItemFileReadStore");
+						plotResults = doh._dojoPlotPerfResults;
+					}else{
+						plotResults = doh._asciiPlotPerfResults;
+					}
+					try{
+						var g;
+						var pBody = byId("perfTestsBody");
+						var chartsToRender = [];
+
+						if(doh.perfTestResults){
+							doh.showPerfTestsPage();
+						}
+						for(g in doh.perfTestResults){
+							var grp = doh.perfTestResults[g];
+							var hdr = document.createElement("h1");
+							hdr.appendChild(document.createTextNode("Group: " + g));
+							pBody.appendChild(hdr);
+							var ind = document.createElement("blockquote");
+							pBody.appendChild(ind);
+							var f;
+							for(f in grp){
+								var fResults = grp[f];
+								if(!fResults){ continue; }
+								var fhdr = document.createElement("h3");
+								fhdr.appendChild(document.createTextNode("TEST: " + f));
+								fhdr.style.textDecoration = "underline";
+								ind.appendChild(fhdr);
+								var div = document.createElement("div");
+								ind.appendChild(div);
+
+								//Figure out the basic info
+								var results = "<b>TRIAL SIZE: </b>"  + fResults.trials[0].testIterations + " iterations<br>" +
+									"<b>NUMBER OF TRIALS: </b>" + fResults.trials.length + "<br>";
+
+								//Figure out the average test pass cost.
+								var i;
+								var iAvgArray = [];
+								var tAvgArray = [];
+								for(i = 0; i < fResults.trials.length; i++){
+									iAvgArray.push(fResults.trials[i].average);
+									tAvgArray.push(fResults.trials[i].executionTime);
+								}
+								results += "<b>AVERAGE TRIAL EXECUTION TIME: </b>" + doh.average(tAvgArray).toFixed(10) + "ms.<br>";
+								results += "<b>MAXIMUM TEST ITERATION TIME: </b>" + doh.max(iAvgArray).toFixed(10) + "ms.<br>";
+								results += "<b>MINIMUM TEST ITERATION TIME: </b>" + doh.min(iAvgArray).toFixed(10) + "ms.<br>";
+								results += "<b>AVERAGE TEST ITERATION TIME: </b>" + doh.average(iAvgArray).toFixed(10) + "ms.<br>";
+								results += "<b>MEDIAN TEST ITERATION TIME: </b>" + doh.median(iAvgArray).toFixed(10) + "ms.<br>";
+								results += "<b>VARIANCE TEST ITERATION TIME: </b>" + doh.variance(iAvgArray).toFixed(10) + "ms.<br>";
+								results += "<b>STANDARD DEVIATION ON TEST ITERATION TIME: </b>" + doh.standardDeviation(iAvgArray).toFixed(10) + "ms.<br>";
+
+								//Okay, attach it all in.
+								div.innerHTML = results;
+
+								div = document.createElement("div");
+								div.innerHTML = "<h3>Average Test Execution Time (in milliseconds, with median line)</h3>";
+								ind.appendChild(div);
+								div = document.createElement("div");
+								dojo.style(div, "width", "600px");
+								dojo.style(div, "height", "250px");
+								ind.appendChild(div);
+								chartsToRender.push({
+									div: div,
+									title: "Average Test Execution Time",
+									data: iAvgArray
+								});
+
+								div = document.createElement("div");
+								div.innerHTML = "<h3>Average Trial Execution Time (in milliseconds, with median line)</h3>";
+								ind.appendChild(div);
+								div = document.createElement("div");
+								dojo.style(div, "width", "600px");
+								dojo.style(div, "height", "250px");
+								ind.appendChild(div);
+								chartsToRender.push({
+									div: div,
+									title: "Average Trial Execution Time",
+									data: tAvgArray
+								});
+							}
+						}
+
+						//Lazy-render these to give the browser time and not appear locked.
+						var delayedRenders = function() {
+							if(chartsToRender.length){
+								var chartData = chartsToRender.shift();
+								plotResults(chartData.div, chartData.title, chartData.data);
+							}
+							doh.setTimeout(delayedRenders, 50);
+						};
+						doh.setTimeout(delayedRenders, 150);
+					}catch(e){
+						doh.debug(e);
+					}
+				}
+				or.apply(doh,arguments);
+			}
+		})(doh._report);
+
+		if(this["opera"] && opera.postError){
+			doh.debug = function(){
+				var msg = "";
+				for(var x=0; x<arguments.length; x++){
+					msg += " "+arguments[x];
+				}
+				sendToLogPane([msg]);
+				opera.postError("DEBUG:"+msg);
+			}
+		}else if(window["console"]){
+			doh.debug = function(){
+				var msg = "";
+				for(var x=0; x<arguments.length; x++){
+					msg += " "+arguments[x];
+				}
+				sendToLogPane([msg]);
+				console.log("DEBUG:"+msg);
+			};
+		}else{
+			doh.debug = function(){
+				sendToLogPane.call(window, arguments);
+			}
+		}
+
+		var loaded = false;
+		var groupTemplate = null;
+		var testTemplate = null;
+
+		var groupNodes = {};
+
+		var _groupTogglers = {};
+
+		var _getGroupToggler = function(group, toggle){
+			if(_groupTogglers[group]){ return _groupTogglers[group]; }
+			var rolledUp = true;
+			return (_groupTogglers[group] = function(evt, forceOpen){
+				var nodes = groupNodes[group].__items;
+				var x;
+				if(rolledUp||forceOpen){
+					rolledUp = false;
+					for(x=0; x<nodes.length; x++){
+						nodes[x].style.display = "";
+					}
+					toggle.innerHTML = "&#9660;";
+				}else{
+					rolledUp = true;
+					for(x=0; x<nodes.length; x++){
+						nodes[x].style.display = "none";
+					}
+					toggle.innerHTML = "&#9658;";
+				}
+			});
+		};
+
+		var addGroupToList = function(group){
+			if(!byId("testList")){ return; }
+			var tb = byId("testList").tBodies[0];
+			var tg = groupTemplate.cloneNode(true);
+			var tds = tg.getElementsByTagName("td");
+			var toggle = tds[0];
+			toggle.onclick = _getGroupToggler(group, toggle);
+			var cb = tds[1].getElementsByTagName("input")[0];
+			cb.group = group;
+			cb.onclick = function(evt){
+				doh._groups[group].skip = (!this.checked);
+			}
+			tds[2].innerHTML = "<div class='testGroupName'>"+group+"</div><div style='width:0;'>&nbsp;</div>";
+			tds[3].innerHTML = "";
+
+			tb.appendChild(tg);
+			return tg;
+		}
+
+		var addFixtureToList = function(group, fixture){
+			if(!testTemplate){ return; }
+			var cgn = groupNodes[group];
+			if(!cgn["__items"]){ cgn.__items = []; }
+			var tn = testTemplate.cloneNode(true);
+			var tds = tn.getElementsByTagName("td");
+
+			tds[2].innerHTML = fixture.name;
+			tds[3].innerHTML = "";
+
+			var nn = (cgn.__lastFixture||cgn.__groupNode).nextSibling;
+			if(nn){
+				nn.parentNode.insertBefore(tn, nn);
+			}else{
+				cgn.__groupNode.parentNode.appendChild(tn);
+			}
+			// FIXME: need to make group display toggleable!!
+			tn.style.display = "none";
+			cgn.__items.push(tn);
+			return (cgn.__lastFixture = tn);
+		}
+
+		var getFixtureNode = function(group, fixture){
+			if(groupNodes[group]){
+				return groupNodes[group][fixture.name];
+			}
+			return null;
+		}
+
+		var getGroupNode = function(group){
+			if(groupNodes[group]){
+				return groupNodes[group].__groupNode;
+			}
+			return null;
+		}
+
+		var updateBacklog = [];
+		doh._updateTestList = function(group, fixture, unwindingBacklog){
+			if(!loaded){
+				if(group && fixture){
+					updateBacklog.push([group, fixture]);
+				}
+				return;
+			}else if(updateBacklog.length && !unwindingBacklog){
+				var tr;
+				while((tr=updateBacklog.shift())){
+					doh._updateTestList(tr[0], tr[1], true);
+				}
+			}
+			if(group && fixture){
+				if(!groupNodes[group]){
+					groupNodes[group] = {
+						"__groupNode": addGroupToList(group)
+					};
+				}
+				if(!groupNodes[group][fixture.name]){
+					groupNodes[group][fixture.name] = addFixtureToList(group, fixture)
+				}
+			}
+		}
+
+		doh._testRegistered = doh._updateTestList;
+
+		doh._groupStarted = function(group){
+			if(this._suiteCount == 0){
+				this._runedSuite = 0;
+				this._currentGlobalProgressBarWidth = 0;
+				this._suiteCount = this._testCount;
+			}
+			// console.debug("_groupStarted", group);
+			if(doh._inGroup != group){
+				doh._groupTotalTime = 0;
+				doh._runed = 0;
+				doh._inGroup = group;
+				this._runedSuite++;
+			}
+			var gn = getGroupNode(group);
+			if(gn){
+				gn.className = "inProgress";
+			}
+		}
+
+		doh._groupFinished = function(group, success){
+			// console.debug("_groupFinished", group);
+			var gn = getGroupNode(group);
+			if(gn && doh._inGroup == group){
+				doh._totalTime += doh._groupTotalTime;
+				gn.getElementsByTagName("td")[3].innerHTML = doh._groupTotalTime+"ms";
+				gn.getElementsByTagName("td")[2].lastChild.className = "";
+				doh._inGroup = null;
+				//doh._runedSuite++;
+				var failure = doh._updateGlobalProgressBar(this._runedSuite/this._groupCount,success,group);
+				gn.className = failure ? "failure" : "success";
+				//doh._runedSuite--;
+				doh._currentGlobalProgressBarWidth = parseInt(this._runedSuite/this._groupCount*10000)/100;
+				//byId("progressOuter").style.width = parseInt(this._runedSuite/this._suiteCount*100)+"%";
+			}
+			if(doh._inGroup == group){
+				this.debug("Total time for GROUP \"",group,"\" is ",doh._groupTotalTime,"ms");
+			}
+		}
+
+		doh._testStarted = function(group, fixture){
+			// console.debug("_testStarted", group, fixture.name);
+			var fn = getFixtureNode(group, fixture);
+			if(fn){
+				fn.className = "inProgress";
+			}
+		}
+
+		var _nameTimes = {};
+		var _playSound = function(name){
+			if(byId("hiddenAudio") && byId("audio") && byId("audio").checked){
+				// console.debug("playing:", name);
+				var nt = _nameTimes[name];
+				// only play sounds once every second or so
+				if((!nt)||(((new Date)-nt) > 700)){
+					_nameTimes[name] = new Date();
+					var tc = document.createElement("span");
+					byId("hiddenAudio").appendChild(tc);
+					tc.innerHTML = '<embed src="_sounds/'+name+'.wav" autostart="true" loop="false" hidden="true" width="1" height="1"></embed>';
+				}
+			}
+		}
+
+		doh._updateGlobalProgressBar = function(p,success,group){
+			var outerContainer=byId("progressOuter");
+
+			var gdiv=outerContainer.childNodes[doh._runedSuite-1];
+			if(!gdiv){
+				gdiv=document.createElement('div');
+				outerContainer.appendChild(gdiv);
+				gdiv.className='success';
+				gdiv.setAttribute('_target',group);
+			}
+			if(!success && !gdiv._failure){
+				gdiv._failure=true;
+				gdiv.className='failure';
+				if(group){
+					gdiv.setAttribute('title','failed group '+group);
+				}
+			}
+			var tp=parseInt(p*10000)/100;
+			gdiv.style.width = (tp-doh._currentGlobalProgressBarWidth)+"%";
+			return gdiv._failure;
+		}
+		doh._testFinished = function(group, fixture, success){
+			var fn = getFixtureNode(group, fixture);
+			var elapsed = fixture.endTime-fixture.startTime;
+			if(fn){
+				fn.getElementsByTagName("td")[3].innerHTML = elapsed+"ms";
+				fn.className = (success) ? "success" : "failure";
+				fn.getElementsByTagName("td")[2].setAttribute('_target', _loggedMsgLen);
+				if(!success){
+					_playSound("doh");
+					var gn = getGroupNode(group);
+					if(gn){
+						gn.className = "failure";
+						_getGroupToggler(group)(null, true);
+					}
+				}
+			}
+			if(doh._inGroup == group){
+				var gn = getGroupNode(group);
+				doh._runed++;
+				if(gn && doh._curTestCount){
+					var p = doh._runed/doh._curTestCount;
+					var groupfail = this._updateGlobalProgressBar((doh._runedSuite+p-1)/doh._groupCount,success,group);
+
+					var pbar = gn.getElementsByTagName("td")[2].lastChild;
+					pbar.className = groupfail?"failure":"success";
+					pbar.style.width = parseInt(p*100)+"%";
+					gn.getElementsByTagName("td")[3].innerHTML = parseInt(p*10000)/100+"%";
+				}
+			}
+			this._groupTotalTime += elapsed;
+			this.debug((success ? "PASSED" : "FAILED"), "test:", fixture.name, elapsed, 'ms');
+		}
+
+		// FIXME: move implementation to _browserRunner?
+		doh.registerUrl = function(	/*String*/ group,
+										/*String*/ url,
+										/*Integer*/ timeout){
+			var tg = new String(group);
+			this.register(group, {
+				name: url,
+				setUp: function(){
+					doh.currentGroupName = tg;
+					doh.currentGroup = this;
+					doh.currentUrl = url;
+					this.d = new doh.Deferred();
+					doh.currentTestDeferred = this.d;
+					doh.showTestPage();
+					byId("testBody").src = url;
+				},
+				timeout: timeout||10000, // 10s
+				// timeout: timeout||1000, // 10s
+				runTest: function(){
+					// FIXME: implement calling into the url's groups here!!
+					return this.d;
+				},
+				tearDown: function(){
+					doh.currentGroupName = null;
+					doh.currentGroup = null;
+					doh.currentTestDeferred = null;
+					doh.currentUrl = null;
+					// this.d.errback(false);
+					// byId("testBody").src = "about:blank";
+					doh.showLogPage();
+				}
+			});
+		}
+
+		//
+		// Utility code for runner.html
+		//
+		// var isSafari = navigator.appVersion.indexOf("Safari") >= 0;
+		var tabzidx = 1;
+		var _showTab = function(toShow, toHide){
+			// FIXME: I don't like hiding things this way.
+			var i;
+			for(i = 0; i < toHide.length; i++){
+				var node = byId(toHide[i]);
+				if(node){
+					node.style.display="none";
+				}
+			}
+			toShow = byId(toShow);
+			if(toShow){
+				with(toShow.style){
+					display = "";
+					zIndex = ++tabzidx;
+				}
+			}
+		}
+
+		doh.showTestPage = function(){
+			_showTab("testBody", ["logBody", "perfTestsBody"]);
+		}
+
+		doh.showLogPage = function(){
+			_showTab("logBody", ["testBody", "perfTestsBody"]);
+		}
+
+		doh.showPerfTestsPage = function(){
+			_showTab("perfTestsBody", ["testBody", "logBody"]);
+		}
+
+		var runAll = true;
+		doh.toggleRunAll = function(){
+			// would be easier w/ query...sigh
+			runAll = !runAll;
+			if(!byId("testList")){ return; }
+			var tb = byId("testList").tBodies[0];
+			var inputs = tb.getElementsByTagName("input");
+			var x=0; var tn;
+			while((tn=inputs[x++])){
+				tn.checked = runAll;
+				doh._groups[tn.group].skip = (!runAll);
+			}
+		}
+
+		var listHeightTimer = null;
+		var setListHeight = function(){
+			if(listHeightTimer){
+				clearTimeout(listHeightTimer);
+			}
+			var tl = byId("testList");
+			if(!tl){ return; }
+			listHeightTimer = doh.setTimeout(function(){
+				tl.style.display = "none";
+				tl.style.display = "";
+
+			}, 10);
+		}
+
+		_addOnEvt("resize", setListHeight);
+		_addOnEvt("load", setListHeight);
+		_addOnEvt("load", function(){
+			if(loaded){ return; }
+			loaded = true;
+			groupTemplate = byId("groupTemplate");
+			if(!groupTemplate){
+				// make sure we've got an ammenable DOM structure
+				return;
+			}
+			groupTemplate.parentNode.removeChild(groupTemplate);
+			groupTemplate.style.display = "";
+			testTemplate = byId("testTemplate");
+			testTemplate.parentNode.removeChild(testTemplate);
+			testTemplate.style.display = "";
+			doh._updateTestList();
+		});
+
+		_addOnEvt("load",
+			function(){
+				// let robot code run if it gets to this first
+				var __onEnd = doh._onEnd;
+				doh._onEnd = function(){
+					__onEnd.apply(doh, arguments);
+					if(doh._failureCount == 0){
+						doh.debug("WOOHOO!!");
+						_playSound("woohoo");
+					}else{
+						console.debug("doh._failureCount:", doh._failureCount);
+					}
+					if(byId("play")){
+						toggleRunning();
+					}
+				}
+				if(!byId("play")){
+					// make sure we've got an amenable DOM structure
+					return;
+				}
+				var isRunning = false;
+				var toggleRunning = function(){
+					// ugg, this would be so much better w/ dojo.query()
+					if(isRunning){
+						byId("play").style.display = byId("pausedMsg").style.display = "";
+						byId("playingMsg").style.display = byId("pause").style.display = "none";
+						isRunning = false;
+					}else{
+						byId("play").style.display = byId("pausedMsg").style.display = "none";
+						byId("playingMsg").style.display = byId("pause").style.display = "";
+						isRunning = true;
+					}
+				}
+				doh.run = (function(oldRun){
+					return function(){
+						if(!doh._currentGroup){
+							toggleRunning();
+						}
+						return oldRun.apply(doh, arguments);
+					}
+				})(doh.run);
+				var btns = byId("toggleButtons").getElementsByTagName("span");
+				var node; var idx=0;
+				while((node=btns[idx++])){
+					node.onclick = toggleRunning;
+				}
+
+				//Performance report generating functions!
+				doh._dojoPlotPerfResults = function(div, name, dataArray) {
+					var median = doh.median(dataArray);
+					var medarray = [];
+
+					var i;
+					for(i = 0; i < dataArray.length; i++){
+						medarray.push(median);
+					}
+
+					var data = {
+						label: "name",
+						items: [
+							{name: name, trials: dataArray},
+							{name: "Median", trials: medarray}
+						]
+					};
+					var ifs = new dojo.data.ItemFileReadStore({data: data});
+
+					var min = Math.floor(doh.min(dataArray));
+					var max = Math.ceil(doh.max(dataArray));
+					var step = (max - min)/10;
+
+					//Lets try to pad out the bottom and top a bit
+					//Then recalc the step.
+					if(min > 0){
+						min = min - step;
+						if(min < 0){
+							min = 0;
+						}
+						min = Math.floor(min);
+					}
+					if(max > 0){
+						max = max + step;
+						max = Math.ceil(max);
+					}
+					step = (max - min)/10;
+
+					var chart = new dojox.charting.DataChart(div, {
+						type: dojox.charting.plot2d.Lines,
+						displayRange:dataArray.length,
+						xaxis: {min: 1, max: dataArray.length, majorTickStep: Math.ceil((dataArray.length - 1)/10), htmlLabels: false},
+						yaxis: {min: min, max: max, majorTickStep: step, vertical: true, htmlLabels: false}
+					});
+					chart.setStore(ifs, {name:"*"}, "trials");
+				};
+
+				doh._asciiPlotPerfResults = function(){
+					//TODO:  Implement!
+				};
+			}
+		);
+	}else{
+		// we're in an iframe environment. Time to mix it up a bit.
+
+		_doh = window.parent.doh;
+		var _thisGroup = _doh.currentGroupName;
+		var _thisUrl = _doh.currentUrl;
+		if(_thisGroup){
+			doh._testRegistered = function(group, tObj){
+				_doh._updateTestList(_thisGroup, tObj);
+			}
+			doh._onEnd = function(){
+				_doh._errorCount += doh._errorCount;
+				_doh._failureCount += doh._failureCount;
+				_doh._testCount += doh._testCount;
+				// should we be really adding raw group counts?
+				//_doh._groupCount += doh._groupCount;
+				_doh.currentTestDeferred.callback(true);
+			}
+			var otr = doh._getTestObj;
+			doh._getTestObj = function(){
+				var tObj = otr.apply(doh, arguments);
+				tObj.name = _thisUrl+"::"+arguments[0]+"::"+tObj.name;
+				return tObj;
+			}
+			doh.debug = doh.hitch(_doh, "debug");
+			doh.registerUrl = doh.hitch(_doh, "registerUrl");
+			doh._testStarted = function(group, fixture){
+				_doh._testStarted(_thisGroup, fixture);
+			}
+			doh._testFinished = function(g, f, s){
+				_doh._testFinished(_thisGroup, f, s);
+
+				//Okay, there may be performance info we need to filter back
+				//to the parent, so do that here.
+				if(doh.perfTestResults){
+					try{
+						gName = g.toString();
+						var localFName = f.name;
+						while(localFName.indexOf("::") >= 0){
+							localFName = localFName.substring(localFName.indexOf("::") + 2, localFName.length);
+						}
+						if(!_doh.perfTestResults){
+							_doh.perfTestResults = {};
+						}
+						if(!_doh.perfTestResults[gName]){
+							_doh.perfTestResults[gName] = {};
+						}
+						_doh.perfTestResults[gName][f.name] = doh.perfTestResults[gName][localFName];
+					}catch (e){
+						doh.debug(e);
+					}
+				}
+			}
+			doh._groupStarted = function(g){
+				if(!this._setParent){
+					_doh._curTestCount = this._testCount;
+					_doh._curGroupCount = this._groupCount;
+					this._setParent = true;
+				}
+			}
+			doh._report = function(){
+			};
+		}
+	}
+
+})();

+ 20 - 0
tests/doh/_nodeRunner.js

@@ -0,0 +1,20 @@
+
+/*global doh: false, process: false */
+
+var aps = Array.prototype.slice;
+
+doh.debug = function () {
+    //Could have multiple args, join them all together.
+    var msg = aps.call(arguments, 0).join(' ');
+    console.log(msg);
+};
+
+// Override the doh._report method to make it quit with an
+// appropriate exit code in case of test failures.
+var oldReport = doh._report;
+doh._report = function () {
+    oldReport.apply(doh, arguments);
+    if (this._failureCount > 0 || this._errorCount > 0) {
+        process.exit(1);
+    }
+};

+ 17 - 0
tests/doh/_rhinoRunner.js

@@ -0,0 +1,17 @@
+if(this["dojo"]){
+	dojo.provide("doh._rhinoRunner");
+}
+
+doh.debug = print;
+
+// Override the doh._report method to make it quit with an 
+// appropriate exit code in case of test failures.
+(function(){
+	var oldReport = doh._report;
+	doh._report = function(){
+		oldReport.apply(doh, arguments);
+		if(this._failureCount > 0 || this._errorCount > 0){
+			quit(1);
+		}
+	}
+})();

+ 316 - 0
tests/doh/runner.html

@@ -0,0 +1,316 @@
+<html>
+	<!--
+		NOTE: we are INTENTIONALLY in quirks mode. It makes it much easier to
+		get a "full screen" UI w/ straightforward CSS.
+	-->
+	<!--
+		// TODO: provide a UI for prompted tests
+	-->
+	<head>
+		<title>RequireJS Tests Via The Dojo Unit Test Harness, $Rev: 20149 $</title>
+
+		<script type="text/javascript">
+			// workaround for bug in Safari 3.  See #7189
+			if (/3[\.0-9]+ Safari/.test(navigator.appVersion))
+			{
+				window.console = {
+				    origConsole: window.console,
+				    log: function(s){
+						this.origConsole.log(s);
+					},
+					info: function(s){
+						this.origConsole.info(s);
+					},
+					error: function(s){
+						this.origConsole.error(s);
+					},
+					warn: function(s){
+						this.origConsole.warn(s);
+					}
+				};
+			}
+		</script>
+
+		<script type="text/javascript">
+			window.dojoUrl = "../../dojo/dojo.js";
+			window.testUrl = "";
+			window.testModule = "";
+
+			// parse out our test URL and our Dojo URL from the query string
+			var qstr = window.location.search.substr(1);
+			if(qstr.length){
+				var qparts = qstr.split("&");
+				for(var x=0; x<qparts.length; x++){
+					var tp = qparts[x].split("=");
+					if(tp[0] == "dojoUrl"){
+						window.dojoUrl = tp[1];
+					}
+					if(tp[0] == "testUrl"){
+						window.testUrl = tp[1];
+					}
+					if(tp[0] == "testModule"){
+						window.testModule = tp[1];
+					}
+					if(tp[0] == "registerModulePath"){
+						var modules = tp[1].split(";");
+						window.registerModulePath=[];
+						for (var i=0; i<modules.length;i++){
+							window.registerModulePath.push(modules[i].split(","));
+						}
+					}
+				}
+			}
+
+			//document.write("<scr"+"ipt type='text/javascript' djConfig='isDebug: true' src='"+dojoUrl+"'></scr"+"ipt>");
+		</script>
+		<script type="text/javascript" src="runner.js"></script>
+		<script type="text/javascript" src="_browserRunner.js"></script>
+
+		<script type="text/javascript">
+			if(testUrl.length){
+				document.write("<scr"+"ipt type='text/javascript' src='"+testUrl+".js'></scr"+"ipt>");
+			}
+		</script>
+		<style type="text/css">
+			/* @import "../../dojo/resources/dojo.css"; */
+			/*
+			body {
+				margin: 0px;
+				padding: 0px;
+				font-size: 13px;
+				color: #292929;
+				font-family: Myriad, Lucida Grande, Bitstream Vera Sans, Arial, Helvetica, sans-serif;
+				*font-size: small;
+				*font: x-small;
+			}
+
+			th, td {
+				font-size: 13px;
+				color: #292929;
+				font-family: Myriad, Lucida Grande, Bitstream Vera Sans, Arial, Helvetica, sans-serif;
+				font-weight: normal;
+			}
+
+			* body {
+				line-height: 1.25em;
+			}
+			
+			table {
+				border-collapse: collapse;
+			}
+			*/
+
+			#testLayout {
+				position: relative;
+				left: 0px;
+				top: 0px;
+				width: 100%;
+				height: 100%;
+				border: 1px solid black;
+				border: 0px;
+			}
+
+			.tabBody {
+				margin: 0px;
+				padding: 0px;
+				/*
+				border: 1px solid black;
+				*/
+				background-color: #DEDEDE;
+				border: 0px;
+				width: 100%;
+				height: 100%;
+				position: absolute;
+				left: 0px; 
+				top: 0px;
+				overflow: auto;
+			}
+
+			#logBody {
+				padding-left: 5px;
+				padding-top: 5px;
+				font-family: Monaco, monospace;
+				font-size: 11px;
+				white-space: pre;
+			}
+
+			#progressOuter {
+				background:#e9e9e9 url("http://o.aolcdn.com/dojo/1.3/dijit/themes/tundra/images/dojoTundraGradientBg.png") repeat-x 0 0;
+				height: 1em;
+				/*the following trick is necessary to prevent IE from wrapping the last piece of progress bar into a new line*/
+				_margin:1px;
+				_padding: -1px;
+				
+				/*
+				border-color: #e8e8e8;
+				*/
+			}
+
+			#progressOuter .success, #progressOuter .failure{
+				float: left;
+				height: 1em;
+			}
+
+			#play, #pause {
+				font-family: Arial;
+				font-size: 1.4em;
+				border: 1px solid #DEDEDE;
+				cursor: pointer;
+				padding-right: 0.5em;
+			}
+
+			.header {
+				border: 1px solid #DEDEDE;
+			}
+
+			button.tab {
+				border-width: 1px 1px 0px 1px;
+				border-style: solid;
+				border-color: #DEDEDE;
+				margin-right: 5px;
+			}
+
+			#testListContainer {
+				/*
+				border: 1px solid black;
+				*/
+				position: relative;
+				height: 99%;
+				width: 100%;
+				overflow: auto;
+			}
+
+			#testList {
+				border-collapse: collapse;
+				position: absolute;
+				left: 0px;
+				width: 100%;
+			}
+
+			#testList td {
+				border-bottom: 1px solid #DEDEDE;
+				border-right : 1px solid #DEDEDE;
+				padding: 3px;
+			}
+
+			#testListHeader th {
+				border-bottom: 1px solid #DEDEDE;
+				border-right : 1px solid #DEDEDE;
+				padding: 3px;
+				font-weight: bolder;
+				font-style: italic;
+			}
+			
+			#testList tfoot {
+				font-weight: bold;
+			}
+
+			#toggleButtons {
+				float: left;
+				background-color: #DEDEDE;
+			}
+
+			div.testGroupName {
+				position: absolute;
+			}
+
+			.inProgress {
+				background-color: #85afde;
+			}
+
+			.success {
+				background-color: #7cdea7;
+			}
+
+			.failure {
+				background-color: #de827b;
+			}
+		</style>
+	</head>
+	<body>
+		<table id="testLayout" cellpadding="0" cellspacing="0" style="margin: 0;">
+			<tr valign="top" height="40">
+				<td colspan="2" id="logoBar">
+					<h3 style="margin: 5px 5px 0px 5px; float: left;">RequireJS Tests Via D.O.H.: The Dojo Objective Harness</h3>
+					<img src="small_logo.png" height="40" style="margin: 0px 5px 0px 5px; float: right;">
+					<span style="margin: 10px 5px 0px 5px; float: right;">
+						<input type="checkbox" id="audio" name="audio">
+						<label for="audio">sounds?</label>
+					</span>
+				</td>
+			</tr>
+			<tr valign="top" height="10">
+				<td colspan="2"><div id="progressOuter" onclick="doh._jumpToSuite(arguments[0]);"></div></td>
+			</tr>
+			<tr valign="top" height="30">
+				<td width="30%" class="header">
+					<span id="toggleButtons" onclick="doh.togglePaused();">
+						<button id="play">&#9658;</button>
+						<button id="pause" style="display: none;">&#9553;</button>
+					</span>
+					<span id="runningStatus">
+						<span id="pausedMsg">Stopped</span>
+						<span id="playingMsg" style="display: none;">Tests Running</span>
+					</span>
+				</td>
+				<td width="*" class="header" valign="bottom">
+					<button class="tab" onclick="doh.showTestPage();">Test Page</button>
+					<button class="tab" onclick="doh.showLogPage();">Log</button>
+                    <button class="tab" onclick="doh.showPerfTestsPage();">Performance Tests Results</button>
+				</td>
+			</tr>
+			<tr valign="top" style="border: 0; padding: 0; margin: 0;">
+				<td height="100%" style="border: 0; padding: 0; margin: 0;">
+					<div id="testListContainer">
+						<table cellpadding="0" cellspacing="0" border="0"
+							width="100%" id="testList" style="margin: 0;" onclick="doh._jumpToLog(arguments[0]);">
+							<thead>
+								<tr id="testListHeader" style="border: 0; padding: 0; margin: 0;" >
+									<th>&nbsp;</th>
+									<th width="20">
+										<input type="checkbox" checked 
+											onclick="doh.toggleRunAll();">
+									</th>
+									<th width="*" style="text-align: left;">test</th>
+									<th width="50">time</th>
+								</tr>
+							</thead>
+							<tbody valign="top">
+								<tr id="groupTemplate" style="display: none;">
+									<td style="font-family: Arial; width: 15px;">&#9658;</td>
+									<td>
+										<input type="checkbox" checked>
+									</td>
+									<td>group name</td>
+									<td>10ms</td>
+								</tr>
+								<tr id="testTemplate" style="display: none;">
+									<td>&nbsp;</td>
+									<td>&nbsp;</td>
+									<td style="padding-left: 20px;">test name</td>
+									<td>10ms</td>
+								</tr>
+							</tbody>
+						</table>
+					</div>
+				</td>
+				<td>
+					<div style="position: relative; width: 99%; height: 100%; top: 0px; left: 0px;">
+						<div class="tabBody"
+							style="z-index: 1;">
+							<pre id="logBody"></pre>
+							<div id="perfTestsBody" style="background-color: white;"></div>
+						</div>
+						<iframe id="testBody" class="tabBody"
+							style="z-index: -1;"></iframe>
+						<!--
+							src="http://redesign.dojotoolkit.org"></iframe>
+						-->
+					</div>
+				</td>
+			</tr>
+		</table>
+		<span id="hiddenAudio"></span>
+	</body>
+</html>
+

+ 1499 - 0
tests/doh/runner.js

@@ -0,0 +1,1499 @@
+// package system gunk.
+//try{
+//	dojo.provide("doh.runner");
+//}catch(e){
+	if(!this["doh"]){
+		doh = {};
+	}
+//}
+
+//
+// Utility Functions and Classes
+//
+
+doh.selfTest = false;
+
+doh.global = this;
+
+doh.hitch = function(/*Object*/thisObject, /*Function|String*/method /*, ...*/){
+	var args = [];
+	for(var x=2; x<arguments.length; x++){
+		args.push(arguments[x]);
+	}
+	var fcn = ((typeof method == "string") ? thisObject[method] : method) || function(){};
+	return function(){
+		var ta = args.concat([]); // make a copy
+		for(var x=0; x<arguments.length; x++){
+			ta.push(arguments[x]);
+		}
+		return fcn.apply(thisObject, ta); // Function
+	};
+}
+
+doh._mixin = function(/*Object*/ obj, /*Object*/ props){
+	// summary:
+	//		Adds all properties and methods of props to obj. This addition is
+	//		"prototype extension safe", so that instances of objects will not
+	//		pass along prototype defaults.
+	var tobj = {};
+	for(var x in props){
+		// the "tobj" condition avoid copying properties in "props"
+		// inherited from Object.prototype.  For example, if obj has a custom
+		// toString() method, don't overwrite it with the toString() method
+		// that props inherited from Object.protoype
+		if(tobj[x] === undefined || tobj[x] != props[x]){
+			obj[x] = props[x];
+		}
+	}
+	// IE doesn't recognize custom toStrings in for..in
+	if(	this["document"]
+		&& document.all
+		&& (typeof props["toString"] == "function")
+		&& (props["toString"] != obj["toString"])
+		&& (props["toString"] != tobj["toString"])
+	){
+		obj.toString = props.toString;
+	}
+	return obj; // Object
+}
+
+doh.mixin = function(/*Object*/obj, /*Object...*/props){
+	// summary:	Adds all properties and methods of props to obj.
+	for(var i=1, l=arguments.length; i<l; i++){
+		doh._mixin(obj, arguments[i]);
+	}
+	return obj; // Object
+}
+
+doh.extend = function(/*Object*/ constructor, /*Object...*/ props){
+	// summary:
+	//		Adds all properties and methods of props to constructor's
+	//		prototype, making them available to all instances created with
+	//		constructor.
+	for(var i=1, l=arguments.length; i<l; i++){
+		doh._mixin(constructor.prototype, arguments[i]);
+	}
+	return constructor; // Object
+}
+
+
+doh._line = "------------------------------------------------------------";
+
+/*
+doh._delegate = function(obj, props){
+	// boodman-crockford delegation
+	function TMP(){};
+	TMP.prototype = obj;
+	var tmp = new TMP();
+	if(props){
+		dojo.lang.mixin(tmp, props);
+	}
+	return tmp;
+}
+*/
+
+doh.debug = function(){
+	// summary:
+	//		takes any number of arguments and sends them to whatever debugging
+	//		or logging facility is available in this environment
+
+	// YOUR TEST RUNNER NEEDS TO IMPLEMENT THIS
+}
+
+doh._AssertFailure = function(msg, hint){
+	// idea for this as way of dis-ambiguating error types is from JUM.
+	// The JUM is dead! Long live the JUM!
+
+	if(!(this instanceof doh._AssertFailure)){
+		return new doh._AssertFailure(msg, hint);
+	}
+	if(hint){
+		msg = (new String(msg||""))+" with hint: \n\t\t"+(new String(hint)+"\n");
+	}
+	this.message = new String(msg||"");
+	return this;
+}
+doh._AssertFailure.prototype = new Error();
+doh._AssertFailure.prototype.constructor = doh._AssertFailure;
+doh._AssertFailure.prototype.name = "doh._AssertFailure";
+
+doh.Deferred = function(canceller){
+	this.chain = [];
+	this.id = this._nextId();
+	this.fired = -1;
+	this.paused = 0;
+	this.results = [null, null];
+	this.canceller = canceller;
+	this.silentlyCancelled = false;
+};
+
+doh.extend(doh.Deferred, {
+	getTestErrback: function(cb, scope){
+		// summary: Replaces outer getTextCallback's in nested situations to avoid multiple callback(true)'s
+		var _this = this;
+		return function(){
+			try{
+				cb.apply(scope||doh.global||_this, arguments);
+			}catch(e){
+				_this.errback(e);
+			}
+		};
+	},
+
+	getTestCallback: function(cb, scope){
+		var _this = this;
+		return function(){
+			try{
+				cb.apply(scope||doh.global||_this, arguments);
+			}catch(e){
+				_this.errback(e);
+				return;
+			}
+			_this.callback(true);
+		};
+	},
+
+	getFunctionFromArgs: function(){
+		var a = arguments;
+		if((a[0])&&(!a[1])){
+			if(typeof a[0] == "function"){
+				return a[0];
+			}else if(typeof a[0] == "string"){
+				return doh.global[a[0]];
+			}
+		}else if((a[0])&&(a[1])){
+			return doh.hitch(a[0], a[1]);
+		}
+		return null;
+	},
+
+	makeCalled: function() {
+		var deferred = new doh.Deferred();
+		deferred.callback();
+		return deferred;
+	},
+
+	_nextId: (function(){
+		var n = 1;
+		return function(){ return n++; };
+	})(),
+
+	cancel: function(){
+		if(this.fired == -1){
+			if (this.canceller){
+				this.canceller(this);
+			}else{
+				this.silentlyCancelled = true;
+			}
+			if(this.fired == -1){
+				this.errback(new Error("Deferred(unfired)"));
+			}
+		}else if(this.fired == 0 &&
+					(this.results[0] instanceof doh.Deferred)){
+			this.results[0].cancel();
+		}
+	},
+
+
+	_pause: function(){
+		this.paused++;
+	},
+
+	_unpause: function(){
+		this.paused--;
+		if ((this.paused == 0) && (this.fired >= 0)) {
+			this._fire();
+		}
+	},
+
+	_continue: function(res){
+		this._resback(res);
+		this._unpause();
+	},
+
+	_resback: function(res){
+		this.fired = ((res instanceof Error) ? 1 : 0);
+		this.results[this.fired] = res;
+		this._fire();
+	},
+
+	_check: function(){
+		if(this.fired != -1){
+			if(!this.silentlyCancelled){
+				throw new Error("already called!");
+			}
+			this.silentlyCancelled = false;
+			return;
+		}
+	},
+
+	callback: function(res){
+		this._check();
+		this._resback(res);
+	},
+
+	errback: function(res){
+		this._check();
+		if(!(res instanceof Error)){
+			res = new Error(res);
+		}
+		this._resback(res);
+	},
+
+	addBoth: function(cb, cbfn){
+		var enclosed = this.getFunctionFromArgs(cb, cbfn);
+		if(arguments.length > 2){
+			enclosed = doh.hitch(null, enclosed, arguments, 2);
+		}
+		return this.addCallbacks(enclosed, enclosed);
+	},
+
+	addCallback: function(cb, cbfn){
+		var enclosed = this.getFunctionFromArgs(cb, cbfn);
+		if(arguments.length > 2){
+			enclosed = doh.hitch(null, enclosed, arguments, 2);
+		}
+		return this.addCallbacks(enclosed, null);
+	},
+
+	addErrback: function(cb, cbfn){
+		var enclosed = this.getFunctionFromArgs(cb, cbfn);
+		if(arguments.length > 2){
+			enclosed = doh.hitch(null, enclosed, arguments, 2);
+		}
+		return this.addCallbacks(null, enclosed);
+	},
+
+	addCallbacks: function(cb, eb){
+		this.chain.push([cb, eb]);
+		if(this.fired >= 0){
+			this._fire();
+		}
+		return this;
+	},
+
+	_fire: function(){
+		var chain = this.chain;
+		var fired = this.fired;
+		var res = this.results[fired];
+		var self = this;
+		var cb = null;
+		while(chain.length > 0 && this.paused == 0){
+			// Array
+			var pair = chain.shift();
+			var f = pair[fired];
+			if(f == null){
+				continue;
+			}
+			try {
+				res = f(res);
+				fired = ((res instanceof Error) ? 1 : 0);
+				if(res instanceof doh.Deferred){
+					cb = function(res){
+						self._continue(res);
+					};
+					this._pause();
+				}
+			}catch(err){
+				fired = 1;
+				res = err;
+			}
+		}
+		this.fired = fired;
+		this.results[fired] = res;
+		if((cb)&&(this.paused)){
+			res.addBoth(cb);
+		}
+	}
+});
+
+//
+// State Keeping and Reporting
+//
+
+doh._testCount = 0;
+doh._groupCount = 0;
+doh._errorCount = 0;
+doh._failureCount = 0;
+doh._currentGroup = null;
+doh._currentTest = null;
+doh._paused = true;
+
+doh._init = function(){
+	this._currentGroup = null;
+	this._currentTest = null;
+	this._errorCount = 0;
+	this._failureCount = 0;
+	this.debug(this._testCount, "tests to run in", this._groupCount, "groups");
+}
+
+// doh._urls = [];
+doh._groups = {};
+
+//
+// Test Registration
+//
+
+doh.registerTestNs = function(/*String*/ group, /*Object*/ ns){
+	// summary:
+	//		adds the passed namespace object to the list of objects to be
+	//		searched for test groups. Only "public" functions (not prefixed
+	//		with "_") will be added as tests to be run. If you'd like to use
+	//		fixtures (setUp(), tearDown(), and runTest()), please use
+	//		registerTest() or registerTests().
+	for(var x in ns){
+		if(	(x.charAt(0) != "_") &&
+			(typeof ns[x] == "function") ){
+			this.registerTest(group, ns[x]);
+		}
+	}
+}
+
+doh._testRegistered = function(group, fixture){
+	// slot to be filled in
+}
+
+doh._groupStarted = function(group){
+	// slot to be filled in
+}
+
+doh._groupFinished = function(group, success){
+	// slot to be filled in
+}
+
+doh._testStarted = function(group, fixture){
+	// slot to be filled in
+}
+
+doh._testFinished = function(group, fixture, success){
+	// slot to be filled in
+}
+
+doh.registerGroup = function(	/*String*/ group,
+								/*Array||Function||Object*/ tests,
+								/*Function*/ setUp,
+								/*Function*/ tearDown,
+								/*String*/ type){
+	// summary:
+	//		registers an entire group of tests at once and provides a setUp and
+	//		tearDown facility for groups. If you call this method with only
+	//		setUp and tearDown parameters, they will replace previously
+	//		installed setUp or tearDown functions for the group with the new
+	//		methods.
+	// group:
+	//		string name of the group
+	// tests:
+	//		either a function or an object or an array of functions/objects. If
+	//		an object, it must contain at *least* a "runTest" method, and may
+	//		also contain "setUp" and "tearDown" methods. These will be invoked
+	//		on either side of the "runTest" method (respectively) when the test
+	//		is run. If an array, it must contain objects matching the above
+	//		description or test functions.
+	// setUp: a function for initializing the test group
+	// tearDown: a function for initializing the test group
+	// type: The type of tests these are, such as a group of performance tests
+	//		null/undefied are standard DOH tests, the valye 'perf' enables
+	//		registering them as performance tests.
+	if(tests){
+		this.register(group, tests, type);
+	}
+	if(setUp){
+		this._groups[group].setUp = setUp;
+	}
+	if(tearDown){
+		this._groups[group].tearDown = tearDown;
+	}
+}
+
+doh._getTestObj = function(group, test, type){
+	var tObj = test;
+	if(typeof test == "string"){
+		if(test.substr(0, 4)=="url:"){
+			return this.registerUrl(group, test);
+		}else{
+			tObj = {
+				name: test.replace("/\s/g", "_") // FIXME: bad escapement
+			};
+			tObj.runTest = new Function("t", test);
+		}
+	}else if(typeof test == "function"){
+		// if we didn't get a fixture, wrap the function
+		tObj = { "runTest": test };
+		if(test["name"]){
+			tObj.name = test.name;
+		}else{
+			try{
+				var fStr = "function ";
+				var ts = tObj.runTest+"";
+				if(0 <= ts.indexOf(fStr)){
+					tObj.name = ts.split(fStr)[1].split("(", 1)[0];
+				}
+				// doh.debug(tObj.runTest.toSource());
+			}catch(e){
+			}
+		}
+		// FIXME: try harder to get the test name here
+	}
+
+	//Augment the test with some specific options to make it identifiable as a
+	//particular type of test so it can be executed properly.
+	if(type === "perf" || tObj.testType === "perf"){
+		tObj.testType = "perf";
+
+		//Build an object on the root DOH class to contain all the test results.
+		//Cache it on the test object for quick lookup later for results storage.
+		if(!doh.perfTestResults){
+			doh.perfTestResults = {};
+			doh.perfTestResults[group] = {};
+		}
+		if(!doh.perfTestResults[group]){
+			doh.perfTestResults[group] = {};
+		}
+		if(!doh.perfTestResults[group][tObj.name]){
+			doh.perfTestResults[group][tObj.name] = {};
+		}
+		tObj.results = doh.perfTestResults[group][tObj.name];
+
+		//If it's not set, then set the trial duration
+		//default to 100ms.
+		if(!("trialDuration" in tObj)){
+			tObj.trialDuration = 100;
+		}
+
+		//If it's not set, then set the delay between trial runs to 100ms
+		//default to 100ms to allow for GC and to make IE happy.
+		if(!("trialDelay" in tObj)){
+			tObj.trialDelay = 100;
+		}
+
+		//If it's not set, then set number of times a trial is run to 10.
+		if(!("trialIterations" in tObj)){
+			tObj.trialIterations = 10;
+		}
+	}
+	return tObj;
+}
+
+doh.registerTest = function(/*String*/ group, /*Function||Object*/ test, /*String*/ type){
+	// summary:
+	//		add the provided test function or fixture object to the specified
+	//		test group.
+	// group:
+	//		string name of the group to add the test to
+	// test:
+	//		either a function or an object. If an object, it must contain at
+	//		*least* a "runTest" method, and may also contain "setUp" and
+	//		"tearDown" methods. These will be invoked on either side of the
+	//		"runTest" method (respectively) when the test is run.
+	// type:
+	//		An identifier denoting the type of testing that the test performs, such
+	//		as a performance test.  If null, defaults to regular DOH test.
+	if(!this._groups[group]){
+		this._groupCount++;
+		this._groups[group] = [];
+		this._groups[group].inFlight = 0;
+	}
+	var tObj = this._getTestObj(group, test, type);
+	if(!tObj){ return null; }
+	this._groups[group].push(tObj);
+	this._testCount++;
+	this._testRegistered(group, tObj);
+	return tObj;
+}
+
+doh.registerTests = function(/*String*/ group, /*Array*/ testArr, /*String*/ type){
+	// summary:
+	//		registers a group of tests, treating each element of testArr as
+	//		though it were being (along with group) passed to the registerTest
+	//		method.  It also uses the type to decide how the tests should
+	//		behave, by defining the type of tests these are, such as performance tests
+	for(var x=0; x<testArr.length; x++){
+		this.registerTest(group, testArr[x], type);
+	}
+}
+
+// FIXME: move implementation to _browserRunner?
+doh.registerUrl = function(	/*String*/ group,
+								/*String*/ url,
+								/*Integer*/ timeout,
+								/*String*/ type){
+	this.debug("ERROR:");
+	this.debug("\tNO registerUrl() METHOD AVAILABLE.");
+	// this._urls.push(url);
+}
+
+doh.registerString = function(group, str, type){
+}
+
+// FIXME: remove the doh.add alias SRTL.
+doh.register = doh.add = function(groupOrNs, testOrNull, type){
+	// summary:
+	// 		"magical" variant of registerTests, registerTest, and
+	// 		registerTestNs. Will accept the calling arguments of any of these
+	// 		methods and will correctly guess the right one to register with.
+	if(	(arguments.length == 1)&&
+		(typeof groupOrNs == "string") ){
+		if(groupOrNs.substr(0, 4)=="url:"){
+			this.registerUrl(groupOrNs, null, null, type);
+		}else{
+			this.registerTest("ungrouped", groupOrNs, type);
+		}
+	}
+	if(arguments.length == 1){
+		this.debug("invalid args passed to doh.register():", groupOrNs, ",", testOrNull);
+		return;
+	}
+	if(typeof testOrNull == "string"){
+		if(testOrNull.substr(0, 4)=="url:"){
+			this.registerUrl(testOrNull, null, null, type);
+		}else{
+			this.registerTest(groupOrNs, testOrNull, type);
+		}
+		// this.registerTestNs(groupOrNs, testOrNull);
+		return;
+	}
+	if(doh._isArray(testOrNull)){
+		this.registerTests(groupOrNs, testOrNull, type);
+		return;
+	}
+	this.registerTest(groupOrNs, testOrNull, type);
+};
+
+doh.registerDocTests = function(module){
+	// no-op for when Dojo isn't loaded into the page
+	this.debug("registerDocTests() requires dojo to be loaded into the environment. Skipping doctest set for module:", module);
+};
+(function(){
+	if(typeof dojo != "undefined"){
+		try{
+			dojo.require("dojox.testing.DocTest");
+		}catch(e){
+			// if the DocTest module isn't available (e.g., the build we're
+			// running from doesn't include it), stub it out and log the error
+			console.debug(e);
+
+			doh.registerDocTests = function(){}
+			return;
+		}
+		doh.registerDocTests = function(module){
+			//	summary:
+			//		Get all the doctests from the given module and register each of them
+			//		as a single test case here.
+			//
+
+			var docTest = new dojox.testing.DocTest();
+			var docTests = docTest.getTests(module);
+			var len = docTests.length;
+			var tests = [];
+			for (var i=0; i<len; i++){
+				var test = docTests[i];
+				// Extract comment on first line and add to test name.
+				var comment = "";
+				if (test.commands.length && test.commands[0].indexOf("//")!=-1) {
+					var parts = test.commands[0].split("//");
+					comment = ", "+parts[parts.length-1]; // Get all after the last //, so we dont get trapped by http:// or alikes :-).
+				}
+				tests.push({
+					runTest: (function(test){
+						return function(t){
+							var r = docTest.runTest(test.commands, test.expectedResult);
+							t.assertTrue(r.success);
+						}
+					})(test),
+					name:"Line "+test.line+comment
+				}
+				);
+			}
+			this.register("DocTests: "+module, tests);
+		}
+	}
+})();
+
+//
+// Assertions and In-Test Utilities
+//
+
+doh.t = doh.assertTrue = function(/*Object*/ condition, /*String?*/ hint){
+	// summary:
+	//		is the passed item "truthy"?
+	if(arguments.length < 1){
+		throw new doh._AssertFailure("assertTrue failed because it was not passed at least 1 argument");
+	}
+	if(!eval(condition)){
+		throw new doh._AssertFailure("assertTrue('" + condition + "') failed", hint);
+	}
+}
+
+doh.f = doh.assertFalse = function(/*Object*/ condition, /*String?*/ hint){
+	// summary:
+	//		is the passed item "falsey"?
+	if(arguments.length < 1){
+		throw new doh._AssertFailure("assertFalse failed because it was not passed at least 1 argument");
+	}
+	if(eval(condition)){
+		throw new doh._AssertFailure("assertFalse('" + condition + "') failed", hint);
+	}
+}
+
+doh.e = doh.assertError = function(/*Error object*/expectedError, /*Object*/scope, /*String*/functionName, /*Array*/args, /*String?*/ hint){
+	//	summary:
+	//		Test for a certain error to be thrown by the given function.
+	//	example:
+	//		t.assertError(dojox.data.QueryReadStore.InvalidAttributeError, store, "getValue", [item, "NOT THERE"]);
+	//		t.assertError(dojox.data.QueryReadStore.InvalidItemError, store, "getValue", ["not an item", "NOT THERE"]);
+	try{
+		scope[functionName].apply(scope, args);
+	}catch (e){
+		if(e instanceof expectedError){
+			return true;
+		}else{
+			throw new doh._AssertFailure("assertError() failed:\n\texpected error\n\t\t"+expectedError+"\n\tbut got\n\t\t"+e+"\n\n", hint);
+		}
+	}
+	throw new doh._AssertFailure("assertError() failed:\n\texpected error\n\t\t"+expectedError+"\n\tbut no error caught\n\n", hint);
+}
+
+
+doh.is = doh.assertEqual = function(/*Object*/ expected, /*Object*/ actual, /*String?*/ hint){
+	// summary:
+	//		are the passed expected and actual objects/values deeply
+	//		equivalent?
+
+	// Compare undefined always with three equal signs, because undefined==null
+	// is true, but undefined===null is false.
+	if((expected === undefined)&&(actual === undefined)){
+		return true;
+	}
+	if(arguments.length < 2){
+		throw doh._AssertFailure("assertEqual failed because it was not passed 2 arguments");
+	}
+	if((expected === actual)||(expected == actual)||
+				( typeof expected == "number" && typeof actual == "number" && isNaN(expected) && isNaN(actual) )){
+		return true;
+	}
+	if(	(this._isArray(expected) && this._isArray(actual))&&
+		(this._arrayEq(expected, actual)) ){
+		return true;
+	}
+	if( ((typeof expected == "object")&&((typeof actual == "object")))&&
+		(this._objPropEq(expected, actual)) ){
+		return true;
+	}
+	throw new doh._AssertFailure("assertEqual() failed:\n\texpected\n\t\t"+expected+"\n\tbut got\n\t\t"+actual+"\n\n", hint);
+}
+
+doh.isNot = doh.assertNotEqual = function(/*Object*/ notExpected, /*Object*/ actual, /*String?*/ hint){
+	// summary:
+	//		are the passed notexpected and actual objects/values deeply
+	//		not equivalent?
+
+	// Compare undefined always with three equal signs, because undefined==null
+	// is true, but undefined===null is false.
+	if((notExpected === undefined)&&(actual === undefined)){
+        throw new doh._AssertFailure("assertNotEqual() failed: not expected |"+notExpected+"| but got |"+actual+"|", hint);
+	}
+	if(arguments.length < 2){
+		throw doh._AssertFailure("assertEqual failed because it was not passed 2 arguments");
+	}
+	if((notExpected === actual)||(notExpected == actual)){
+        throw new doh._AssertFailure("assertNotEqual() failed: not expected |"+notExpected+"| but got |"+actual+"|", hint);
+	}
+	if(	(this._isArray(notExpected) && this._isArray(actual))&&
+		(this._arrayEq(notExpected, actual)) ){
+		throw new doh._AssertFailure("assertNotEqual() failed: not expected |"+notExpected+"| but got |"+actual+"|", hint);
+	}
+	if( ((typeof notExpected == "object")&&((typeof actual == "object")))&&
+		(this._objPropEq(notExpected, actual)) ){
+        throw new doh._AssertFailure("assertNotEqual() failed: not expected |"+notExpected+"| but got |"+actual+"|", hint);
+	}
+    return true;
+}
+
+doh._arrayEq = function(expected, actual){
+	if(expected.length != actual.length){ return false; }
+	// FIXME: we're not handling circular refs. Do we care?
+	for(var x=0; x<expected.length; x++){
+		if(!doh.assertEqual(expected[x], actual[x])){ return false; }
+	}
+	return true;
+}
+
+doh._objPropEq = function(expected, actual){
+	// Degenerate case: if they are both null, then their "properties" are equal.
+	if(expected === null && actual === null){
+		return true;
+	}
+	// If only one is null, they aren't equal.
+	if(expected === null || actual === null){
+		return false;
+	}
+	if(expected instanceof Date){
+		return actual instanceof Date && expected.getTime()==actual.getTime();
+	}
+	var x;
+	// Make sure ALL THE SAME properties are in both objects!
+	for(x in actual){ // Lets check "actual" here, expected is checked below.
+		if(expected[x] === undefined){
+			return false;
+		}
+	};
+
+	for(x in expected){
+		if(!doh.assertEqual(expected[x], actual[x])){
+			return false;
+		}
+	}
+	return true;
+}
+
+doh._isArray = function(it){
+	return (it && it instanceof Array || typeof it == "array" ||
+		(
+			!!doh.global["dojo"] &&
+			doh.global["dojo"]["NodeList"] !== undefined &&
+			it instanceof doh.global["dojo"]["NodeList"]
+		)
+	);
+}
+
+//
+// Runner-Wrapper
+//
+
+doh._setupGroupForRun = function(/*String*/ groupName, /*Integer*/ idx){
+	var tg = this._groups[groupName];
+	this.debug(this._line);
+	this.debug("GROUP", "\""+groupName+"\"", "has", tg.length, "test"+((tg.length > 1) ? "s" : "")+" to run");
+}
+
+doh._handleFailure = function(groupName, fixture, e){
+	// this.debug("FAILED test:", fixture.name);
+	// mostly borrowed from JUM
+	this._groups[groupName].failures++;
+	var out = "";
+	if(e instanceof this._AssertFailure){
+		this._failureCount++;
+		if(e["fileName"]){ out += e.fileName + ':'; }
+		if(e["lineNumber"]){ out += e.lineNumber + ' '; }
+		out += e+": "+e.message;
+		this.debug("\t_AssertFailure:", out);
+	}else{
+		this._errorCount++;
+	}
+	this.debug(e);
+	if(fixture.runTest["toSource"]){
+		var ss = fixture.runTest.toSource();
+		this.debug("\tERROR IN:\n\t\t", ss);
+	}else{
+		this.debug("\tERROR IN:\n\t\t", fixture.runTest);
+	}
+
+	if(e.rhinoException){
+		e.rhinoException.printStackTrace();
+	}else if(e.javaException){
+		e.javaException.printStackTrace();
+	}
+}
+
+//Assume a setTimeout implementation that is synchronous, so that
+//the Node and Rhino envs work similar to each other. Node defines
+//a setTimeout, so testing for setTimeout is not enough, each environment
+//adapter should set this value accordingly.
+doh.setTimeout = function(func){
+	return func();
+};
+
+doh._runPerfFixture = function(/*String*/groupName, /*Object*/fixture){
+	//	summary:
+	//		This function handles how to execute a 'performance' test
+	//		which is different from a straight UT style test.  These
+	//		will often do numerous iterations of the same operation and
+	//		gather execution statistics about it, like max, min, average,
+	//		etc.  It makes use of the already in place DOH deferred test
+	//		handling since it is a good idea to put a pause inbetween each
+	//		iteration to allow for GC cleanup and the like.
+	//
+	//	groupName:
+	//		The test group that contains this performance test.
+	//	fixture:
+	//		The performance test fixture.
+	var tg = this._groups[groupName];
+	fixture.startTime = new Date();
+
+	//Perf tests always need to act in an async manner as there is a
+	//number of iterations to flow through.
+	var def = new doh.Deferred();
+	tg.inFlight++;
+	def.groupName = groupName;
+	def.fixture = fixture;
+
+	def.addErrback(function(err){
+		doh._handleFailure(groupName, fixture, err);
+	});
+
+	//Set up the finalizer.
+	var retEnd = function(){
+		if(fixture["tearDown"]){ fixture.tearDown(doh); }
+		tg.inFlight--;
+		if((!tg.inFlight)&&(tg.iterated)){
+			doh._groupFinished(groupName, !tg.failures);
+		}
+		doh._testFinished(groupName, fixture, def.results[0]);
+		if(doh._paused){
+			doh.run();
+		}
+	};
+
+	//Since these can take who knows how long, we don't want to timeout
+	//unless explicitly set
+	var timer;
+	var to = fixture.timeout;
+	if(to > 0) {
+		timer = doh.setTimeout(function(){
+			// ret.cancel();
+			// retEnd();
+			def.errback(new Error("test timeout in "+fixture.name.toString()));
+		}, to);
+	}
+
+	//Set up the end calls to the test into the deferred we'll return.
+	def.addBoth(function(arg){
+		if(timer){
+			clearTimeout(timer);
+		}
+		retEnd();
+	});
+
+	//Okay, now set up the timing loop for the actual test.
+	//This is down as an async type test where there is a delay
+	//between each execution to allow for GC time, etc, so the GC
+	//has less impact on the tests.
+	var res = fixture.results;
+	res.trials = [];
+
+	//Try to figure out how many calls are needed to hit a particular threshold.
+	var itrDef = doh._calcTrialIterations(groupName, fixture);
+	itrDef.addErrback(function(err){
+		fixture.endTime = new Date();
+		def.errback(err);
+	});
+
+	//Blah, since tests can be deferred, the actual run has to be deferred until after
+	//we know how many iterations to run.  This is just plain ugly.
+	itrDef.addCallback(function(iterations){
+		if(iterations){
+			var countdown = fixture.trialIterations;
+			doh.debug("TIMING TEST: [" + fixture.name +
+					  "]\n\t\tITERATIONS PER TRIAL: " +
+					  iterations + "\n\tTRIALS: " +
+					  countdown);
+
+			//Figure out how many times we want to run our 'trial'.
+			//Where each trial consists of 'iterations' of the test.
+
+			var trialRunner = function() {
+				//Set up our function to execute a block of tests
+				var start = new Date();
+				var tTimer = new doh.Deferred();
+				var tCountdown = iterations;
+
+				var tState = {
+					countdown: iterations
+				};
+				var testRunner = function(state){
+					while(state){
+						try{
+							state.countdown--;
+							if(state.countdown){
+								var ret = fixture.runTest(doh);
+								if(ret instanceof doh.Deferred){
+									//Deferreds have to be handled async,
+									//otherwise we just keep looping.
+									var atState = {
+										countdown: state.countdown
+									};
+									ret.addCallback(function(){
+										testRunner(atState);
+									});
+									ret.addErrback(function(err) {
+										doh._handleFailure(groupName, fixture, err);
+										fixture.endTime = new Date();
+										def.errback(err);
+									});
+									state = null;
+								}
+							}else{
+								tTimer.callback(new Date());
+								state = null;
+							}
+						}catch(err){
+							fixture.endTime = new Date();
+							tTimer.errback(err);
+						}
+					}
+				};
+				tTimer.addCallback(function(end){
+					//Figure out the results and try to factor out function call costs.
+					var tResults = {
+						trial: (fixture.trialIterations - countdown),
+						testIterations: iterations,
+						executionTime: (end.getTime() - start.getTime()),
+						average: (end.getTime() - start.getTime())/iterations
+					};
+					res.trials.push(tResults);
+					doh.debug("\n\t\tTRIAL #: " +
+							  tResults.trial + "\n\tTIME: " +
+							  tResults.executionTime + "ms.\n\tAVG TEST TIME: " +
+							  (tResults.executionTime/tResults.testIterations) + "ms.");
+
+					//Okay, have we run all the trials yet?
+					countdown--;
+					if(countdown){
+						doh.setTimeout(trialRunner, fixture.trialDelay);
+					}else{
+						//Okay, we're done, lets compute some final performance results.
+						var t = res.trials;
+
+
+
+						//We're done.
+						fixture.endTime = new Date();
+						def.callback(true);
+					}
+				});
+				tTimer.addErrback(function(err){
+					fixture.endTime = new Date();
+					def.errback(err);
+				});
+				testRunner(tState);
+			};
+			trialRunner();
+		}
+	});
+
+	//Set for a pause, returned the deferred.
+	if(def.fired < 0){
+		doh.pause();
+	}
+	return def;
+};
+
+doh._calcTrialIterations =  function(/*String*/ groupName, /*Object*/ fixture){
+	//	summary:
+	//		This function determines the rough number of iterations to
+	//		use to reach a particular MS threshold.  This returns a deferred
+	//		since tests can theoretically by async.  Async tests aren't going to
+	//		give great perf #s, though.
+	//		The callback is passed the # of iterations to hit the requested
+	//		threshold.
+	//
+	//	fixture:
+	//		The test fixture we want to calculate iterations for.
+	var def = new doh.Deferred();
+	var calibrate = function () {
+		var testFunc = fixture.runTest;
+
+		//Set the initial state.  We have to do this as a loop instead
+		//of a recursive function.  Otherwise, it blows the call stack
+		//on some browsers.
+		var iState = {
+			start: new Date(),
+			curIter: 0,
+			iterations: 5
+		};
+		var handleIteration = function(state){
+			while(state){
+				if(state.curIter < state.iterations){
+					try{
+						var ret = testFunc(doh);
+						if(ret instanceof doh.Deferred){
+							var aState = {
+								start: state.start,
+								curIter: state.curIter + 1,
+								iterations: state.iterations
+							};
+							ret.addCallback(function(){
+								handleIteration(aState);
+							});
+							ret.addErrback(function(err) {
+								fixture.endTime = new Date();
+								def.errback(err);
+							});
+							state = null;
+						}else{
+							state.curIter++;
+						}
+					}catch(err){
+						fixture.endTime = new Date();
+						def.errback(err);
+						return;
+					}
+				}else{
+					var end = new Date();
+					var totalTime = (end.getTime() - state.start.getTime());
+					if(totalTime < fixture.trialDuration){
+						var nState = {
+							iterations: state.iterations * 2,
+							curIter: 0
+						}
+						state = null;
+						doh.setTimeout(function(){
+							nState.start = new Date();
+							handleIteration(nState);
+						}, 50);
+					}else{
+						var itrs = state.iterations;
+						doh.setTimeout(function(){def.callback(itrs)}, 50);
+						state = null;
+					}
+				}
+			}
+		};
+		handleIteration(iState);
+	};
+	doh.setTimeout(calibrate, 10);
+	return def;
+};
+
+doh._runRegFixture = function(/*String*/groupName, /*Object*/fixture){
+	//	summary:
+	//		Function to run a generic doh test.  These are not
+	//		specialized tests, like performance groups and such.
+	//
+	//	groupName:
+	//		The groupName of the test.
+	//	fixture:
+	//		The test fixture to execute.
+	var tg = this._groups[groupName];
+	fixture.startTime = new Date();
+	var ret = fixture.runTest(this);
+	fixture.endTime = new Date();
+	// if we get a deferred back from the test runner, we know we're
+	// gonna wait for an async result. It's up to the test code to trap
+	// errors and give us an errback or callback.
+	if(ret instanceof doh.Deferred){
+		tg.inFlight++;
+		ret.groupName = groupName;
+		ret.fixture = fixture;
+
+		ret.addErrback(function(err){
+			doh._handleFailure(groupName, fixture, err);
+		});
+
+		var retEnd = function(){
+			if(fixture["tearDown"]){ fixture.tearDown(doh); }
+			tg.inFlight--;
+			if((!tg.inFlight)&&(tg.iterated)){
+				doh._groupFinished(groupName, !tg.failures);
+			}
+			doh._testFinished(groupName, fixture, ret.results[0]);
+			if(doh._paused){
+				doh.run();
+			}
+		}
+
+		var timer = doh.setTimeout(function(){
+			// ret.cancel();
+			// retEnd();
+			ret.errback(new Error("test timeout in "+fixture.name.toString()));
+		}, fixture["timeout"]||1000);
+
+		ret.addBoth(function(arg){
+			clearTimeout(timer);
+			retEnd();
+		});
+		if(ret.fired < 0){
+			doh.pause();
+		}
+		return ret;
+	}
+};
+
+doh._runFixture = function(groupName, fixture){
+	var tg = this._groups[groupName];
+	this._testStarted(groupName, fixture);
+	var threw = false;
+	var err = null;
+	// run it, catching exceptions and reporting them
+	try{
+		// let doh reference "this.group.thinger..." which can be set by
+		// another test or group-level setUp function
+		fixture.group = tg;
+		// only execute the parts of the fixture we've got
+
+		if(fixture["setUp"]){ fixture.setUp(this); }
+		if(fixture["runTest"]){  // should we error out of a fixture doesn't have a runTest?
+			if(fixture.testType === "perf"){
+				//Always async deferred, so return it.
+				return doh._runPerfFixture(groupName, fixture);
+			}else{
+				//May or may not by async.
+				var ret = doh._runRegFixture(groupName, fixture);
+				if(ret){
+					return ret;
+				}
+			}
+		}
+		if(fixture["tearDown"]){ fixture.tearDown(this); }
+	}catch(e){
+		threw = true;
+		err = e;
+		if(!fixture.endTime){
+			fixture.endTime = new Date();
+		}
+	}
+	var d = new doh.Deferred();
+	doh.setTimeout(this.hitch(this, function(){
+		if(threw){
+			this._handleFailure(groupName, fixture, err);
+		}
+		this._testFinished(groupName, fixture, !threw);
+
+		if((!tg.inFlight)&&(tg.iterated)){
+			doh._groupFinished(groupName, !tg.failures);
+		}else if(tg.inFlight > 0){
+			doh.setTimeout(this.hitch(this, function(){
+				doh.runGroup(groupName); // , idx);
+			}), 100);
+			this._paused = true;
+		}
+		if(doh._paused){
+			doh.run();
+		}
+	}), 30);
+	doh.pause();
+	return d;
+}
+
+doh._testId = 0;
+doh.runGroup = function(/*String*/ groupName, /*Integer*/ idx){
+	// summary:
+	//		runs the specified test group
+
+	// the general structure of the algorithm is to run through the group's
+	// list of doh, checking before and after each of them to see if we're in
+	// a paused state. This can be caused by the test returning a deferred or
+	// the user hitting the pause button. In either case, we want to halt
+	// execution of the test until something external to us restarts it. This
+	// means we need to pickle off enough state to pick up where we left off.
+
+	// FIXME: need to make fixture execution async!!
+
+	var tg = this._groups[groupName];
+	if(tg.skip === true){ return; }
+	if(this._isArray(tg)){
+		if(idx<=tg.length){
+			if((!tg.inFlight)&&(tg.iterated == true)){
+				if(tg["tearDown"]){ tg.tearDown(this); }
+				doh._groupFinished(groupName, !tg.failures);
+				return;
+			}
+		}
+		if(!idx){
+			tg.inFlight = 0;
+			tg.iterated = false;
+			tg.failures = 0;
+		}
+		doh._groupStarted(groupName);
+		if(!idx){
+			this._setupGroupForRun(groupName, idx);
+			if(tg["setUp"]){ tg.setUp(this); }
+		}
+		for(var y=(idx||0); y<tg.length; y++){
+			if(this._paused){
+				this._currentTest = y;
+				// this.debug("PAUSED at:", tg[y].name, this._currentGroup, this._currentTest);
+				return;
+			}
+			doh._runFixture(groupName, tg[y]);
+			if(this._paused){
+				this._currentTest = y+1;
+				if(this._currentTest == tg.length){
+					tg.iterated = true;
+				}
+				// this.debug("PAUSED at:", tg[y].name, this._currentGroup, this._currentTest);
+				return;
+			}
+		}
+		tg.iterated = true;
+		if(!tg.inFlight){
+			if(tg["tearDown"]){ tg.tearDown(this); }
+			doh._groupFinished(groupName, !tg.failures);
+		}
+	}
+}
+
+doh._onEnd = function(){}
+
+doh._report = function(){
+	// summary:
+	//		a private method to be implemented/replaced by the "locally
+	//		appropriate" test runner
+
+	// this.debug("ERROR:");
+	// this.debug("\tNO REPORTING OUTPUT AVAILABLE.");
+	// this.debug("\tIMPLEMENT doh._report() IN YOUR TEST RUNNER");
+
+	this.debug(this._line);
+	this.debug("| TEST SUMMARY:");
+	this.debug(this._line);
+	this.debug("\t", this._testCount, "tests in", this._groupCount, "groups");
+	this.debug("\t", this._errorCount, "errors");
+	this.debug("\t", this._failureCount, "failures");
+}
+
+doh.togglePaused = function(){
+	this[(this._paused) ? "run" : "pause"]();
+}
+
+doh.pause = function(){
+	// summary:
+	//		halt test run. Can be resumed.
+	this._paused = true;
+}
+
+doh.run = function(){
+	// summary:
+	//		begins or resumes the test process.
+	// this.debug("STARTING");
+	this._paused = false;
+	var cg = this._currentGroup;
+	var ct = this._currentTest;
+	var found = false;
+	if(!cg){
+		this._init(); // we weren't paused
+		found = true;
+	}
+	this._currentGroup = null;
+	this._currentTest = null;
+
+	for(var x in this._groups){
+		if(
+			( (!found)&&(x == cg) )||( found )
+		){
+			if(this._paused){ return; }
+			this._currentGroup = x;
+			if(!found){
+				found = true;
+				this.runGroup(x, ct);
+			}else{
+				this.runGroup(x);
+			}
+			if(this._paused){ return; }
+		}
+	}
+	this._currentGroup = null;
+	this._currentTest = null;
+	this._paused = false;
+	this._onEnd();
+	this._report();
+}
+
+//Statistics functions to handle computing performance metrics.
+//Taken from dojox.math
+//	basic statistics
+doh.standardDeviation = function(/* Number[] */a){
+	//	summary:
+	//		Returns the standard deviation of the passed arguments.
+	return Math.sqrt(this.variance(a));	//	Number
+};
+
+doh.variance = function(/* Number[] */a){
+	//	summary:
+	//		Find the variance in the passed array of numbers.
+	var mean=0, squares=0;
+	dojo.forEach(a, function(item){
+		mean+=item;
+		squares+=Math.pow(item,2);
+	});
+	return (squares/a.length)-Math.pow(mean/a.length, 2);	//	Number
+};
+
+doh.mean = function(/* Number[] */a){
+	//	summary:
+	//		Returns the mean value in the passed array.
+	var t=0;
+	dojo.forEach(a, function(v){
+		t += v;
+	});
+	return t / Math.max(a.length, 1);	//	Number
+};
+
+doh.min = function(/* Number[] */a){
+	//	summary:
+	//		Returns the min value in the passed array.
+	return Math.min.apply(null, a);		//	Number
+};
+
+doh.max = function(/* Number[] */a){
+	//	summary:
+	//		Returns the max value in the passed array.
+	return Math.max.apply(null, a);		//	Number
+},
+
+doh.median= function(/* Number[] */a){
+	//	summary:
+	//		Returns the value closest to the middle from a sorted version of the passed array.
+	return a.slice(0).sort()[Math.ceil(a.length/2)-1];	//	Number
+},
+
+doh.mode = function(/* Number[] */a){
+	//	summary:
+	//		Returns the mode from the passed array (number that appears the most often).
+	//		This is not the most efficient method, since it requires a double scan, but
+	//		is ensures accuracy.
+	var o = {}, r = 0, m = Number.MIN_VALUE;
+	dojo.forEach(a, function(v){
+		(o[v]!==undefined)?o[v]++:o[v]=1;
+	});
+
+	//	we did the lookup map because we need the number that appears the most.
+	for(var p in o){
+		if(m < o[p]){
+			m = o[p], r = p;
+		}
+	}
+	return r;	//	Number
+};
+
+doh.average = function(/* Number [] */ a){
+	var i;
+	var s = 0;
+	for(i = 0; i < a.length; i++){
+		s += a[i];
+	}
+	return s/a.length;
+}
+
+tests = doh;
+
+if (typeof skipDohSetup === "undefined") {
+
+    (function(){
+            // scope protection
+            var x;
+            try{
+                    if(typeof dojo != "undefined"){
+                            dojo.platformRequire({
+                                    browser: ["doh._browserRunner"],
+                                    rhino: ["doh._rhinoRunner"],
+                                    spidermonkey: ["doh._rhinoRunner"]
+                            });
+                            try{
+                                    var _shouldRequire = dojo.isBrowser ? (dojo.global == dojo.global["parent"] || !Boolean(dojo.global.parent.doh) ) : true;
+                            }catch(e){
+                                    //can't access dojo.global.parent.doh, then we need to do require
+                                    _shouldRequire = true;
+                            }
+                            if(_shouldRequire){
+                                    if(dojo.isBrowser){
+                                            dojo.addOnLoad(function(){
+                                                    if (dojo.global.registerModulePath){
+                                                            dojo.forEach(dojo.global.registerModulePath, function(m){
+                                                                    dojo.registerModulePath(m[0], m[1]);
+                                                            });
+                                                    }
+                                                    if(dojo.byId("testList")){
+                                                            var _tm = ( (dojo.global.testModule && dojo.global.testModule.length) ? dojo.global.testModule : "dojo.tests.module");
+                                                            dojo.forEach(_tm.split(","), dojo.require, dojo);
+                                                            doh.setTimeout(function(){
+                                                                    doh.run();
+                                                            }, 500);
+                                                    }
+                                            });
+                                    }else{
+                                            // dojo.require("doh._base");
+                                    }
+                            }
+                    }else{
+                            if(typeof load == "function" &&
+                                    (typeof Packages == "function" || typeof Packages == "object")){
+                                    throw new Error();
+                            }else if(typeof load == "function"){
+                                    throw new Error();
+                            }
+
+                            if(this["document"]){
+                                    /*
+                                    // if we survived all of that, we're probably in a browser but
+                                    // don't have Dojo handy. Load _browserRunner.js using a
+                                    // document.write() call.
+
+                                    // find runner.js, load _browserRunner relative to it
+                                    var scripts = document.getElementsByTagName("script"), runnerFile;
+                                    for(x=0; x<scripts.length; x++){
+                                            var s = scripts[x].src;
+                                            if(s){
+                                                    if(!runnerFile && s.substr(s.length - 9) == "runner.js"){
+                                                            runnerFile = s;
+                                                    }else if(s.substr(s.length - 17) == "_browserRunner.js"){
+                                                            runnerFile = null;
+                                                            break;
+                                                    }
+                                            }
+                                    }
+                                    if(runnerFile){
+                                            document.write("<scri"+"pt src='" + runnerFile.substr(0, runnerFile.length - 9)
+                                                    + "_browserRunner.js' type='text/javascript'></scr"+"ipt>");
+                                    }
+                                    */
+                            }
+                    }
+            }catch(e){
+                    print("\n"+doh._line);
+                    print("The Dojo Unit Test Harness, $Rev: 20389 $");
+                    print("Copyright (c) 2009, The Dojo Foundation, All Rights Reserved");
+                    print(doh._line, "\n");
+
+                    try{
+                            var dojoUrl = "../../dojo/dojo.js";
+                            var testUrl = "";
+                            var testModule = "dojo.tests.module";
+                            var dohBase = "";
+                            for(x=0; x<arguments.length; x++){
+                                    if(arguments[x].indexOf("=") > 0){
+                                            var tp = arguments[x].split("=");
+                                            if(tp[0] == "dohBase"){
+                                                    dohBase = tp[1];
+                                                    //Convert slashes to unix style and make sure properly
+                                                    //ended.
+                                                    dohBase = dohBase.replace(/\\/g, "/");
+                                                    if(dohBase.charAt(dohBase.length - 1) != "/"){
+                                                            dohBase += "/";
+                                                    }
+                                            }
+                                            if(tp[0] == "dojoUrl"){
+                                                    dojoUrl = tp[1];
+                                            }
+                                            if(tp[0] == "testUrl"){
+                                                    testUrl = tp[1];
+                                            }
+                                            if(tp[0] == "testModule"){
+                                                    testModule = tp[1];
+                                            }
+                                    }
+                            }
+
+                            load(dohBase + "_rhinoRunner.js");
+
+                            if(dojoUrl.length){
+                                    if(!this["djConfig"]){
+                                            djConfig = {};
+                                    }
+                                    djConfig.baseUrl = dojoUrl.split("dojo.js")[0];
+                                    load(dojoUrl);
+                            }
+                            if(testUrl.length){
+                                    load(testUrl);
+                            }
+                            if(testModule.length){
+                                    dojo.forEach(testModule.split(","), dojo.require, dojo);
+                            }
+                    }catch(e){
+                            print("An exception occurred: " + e);
+                    }
+
+                    doh.run();
+            }
+    }).apply(this, typeof arguments != "undefined" ? arguments : [null]);
+}

+ 3 - 0
tests/doh/runner.sh

@@ -0,0 +1,3 @@
+#!/bin/sh
+
+java -jar ../../build/lib/rhino/js.jar runner.js "$@"

BIN
tests/doh/small_logo.png


+ 0 - 4
tests/plugins/relative/relative-test.js

@@ -1,4 +0,0 @@
-
-var branch = require('./sub/branch');
-
-console.log(branch);

+ 20 - 0
tests/plugins/relative/relative-tests.js

@@ -0,0 +1,20 @@
+doh.register(
+    "pluginsRelative",
+    [
+        function pluginsRelative(t){
+            var branch = require('./sub/branch'),
+                fs = require('fs'),
+                path = require('path'),
+                baseName = path.dirname(module.filename),
+                twoText = fs.readFileSync(path.normalize(path.join(baseName, './sub/templates/two.txt')), 'utf8');
+
+            t.is('branch', branch.name);
+            t.is('leaf', branch.leaf.name);
+            t.is(fs.readFileSync(path.normalize(path.join(baseName, './sub/templates/one.txt')), 'utf8'), branch.one);
+            t.is(twoText, branch.two);
+            t.is(twoText, branch.leaf.two);
+        }
+    ]
+);
+
+doh.run();

+ 0 - 1
tests/plugins/relative/sub/another/leaf.js

@@ -1,7 +1,6 @@
 if (typeof define !== 'function') { var define = (require('../../../../../amdefine'))(module); }
 
 define(function (require) {
-debugger;
     return {
         name: 'leaf',
         two: require('../../text!../templates/two.txt')