Răsfoiți Sursa

No Ticket. Adding MooTools test-runner to Hue.

Aaron Newton 15 ani în urmă
părinte
comite
9464e9fac3

+ 71 - 0
ext/thirdparty/js/test-runner/mootools-runner/Configuration_Example.js

@@ -0,0 +1,71 @@
+// Put this file in the parent directory of the runner folder. Also rename the file to Configuration.js
+
+(function(context){
+
+var Configuration = context.Configuration = {};
+
+// Runner name
+Configuration.name = 'MooTools More';
+
+
+// Presets - combine the sets and the source to a preset to easily run a test
+Configuration.presets = {
+	
+	'more-all': {
+		sets: ['1.3-all'],
+		source: ['core-1.3-base', 'core-1.3-client']
+	}
+	
+};
+
+// An object with default presets
+Configuration.defaultPresets = {
+	browser: 'more-all',
+	nodejs: 'more-base',
+	jstd: 'more-all'
+};
+
+
+/*
+ * An object with sets. Each item in the object should have an path key, 
+ * that specifies where the spec files are and an array with all the files
+ * without the .js extension relative to the given path
+ */
+Configuration.sets = {
+
+	'1.3-all': {
+		path: '1.3/',
+		files: ['Core/Lang', 'Core/Log']
+	}
+
+};
+
+
+/*
+ * An object with the source files. Each item should have an path key,
+ * that specifies where the source files are and an array with all the files
+ * without the .js extension relative to the given path
+ */
+Configuration.source = {
+
+	'core-1.3-base': {
+		path: 'mootools-core/Source/',
+		files: [
+			'Core/Core',
+
+			'Types/Array',
+			'Types/Function',
+			'Types/Number'
+		]
+	},
+	'core-1.3-client': {
+		path: 'mootools-core/Source/',
+		files: [
+			'Types/Event',
+			'Browser/Browser'
+		]
+	}
+
+};
+
+})(typeof exports != 'undefined' ? exports : this);

+ 36 - 0
ext/thirdparty/js/test-runner/mootools-runner/Helpers/JSSpecToJasmine.js

@@ -0,0 +1,36 @@
+describe = (function(original){
+	var each = 'before each',
+		all = 'before all',
+		after = 'after all';
+
+	return function(name, object){
+		if (object instanceof Function){
+			original(name, object);
+			return;
+		}
+
+		original(name, function(){
+			var beforeAll = object[all],
+				bfEach = object[each],
+				aAll = object[after];
+
+			beforeEach(function(){
+				if (beforeAll){
+					beforeAll();
+					beforeAll = null;
+				}
+
+				if (bfEach) bfEach();
+			});
+
+			delete object[all];
+			delete object[each];
+			delete object[after];
+
+			for (var key in object)
+				it(key, object[key]);
+
+			if (aAll) it('cleans up', aAll);
+		});
+	};
+})(describe);

+ 121 - 0
ext/thirdparty/js/test-runner/mootools-runner/Helpers/Loader.js

@@ -0,0 +1,121 @@
+(function(context){
+
+var toString = Object.prototype.toString;
+var isArray = Array.isArray || function(array){
+	return toString.call(array) == '[object Array]';
+};
+
+var indexOf = function(array, item, from){
+	var len = array.length;
+	for (var i = (from < 0) ? Math.max(0, len + from) : from || 0; i < len; i++){
+		if (array[i] === item) return i;
+	}
+	return -1;
+};
+
+var forEach = function(array, fn, bind){
+	for (var i = 0, l = array.length; i < l; i++){
+		if (i in array) fn.call(bind, array[i], i, array);
+	}
+};
+
+
+context.SpecLoader = function(config, options){
+
+	// initialization
+	var preset;
+	if (options.preset) preset = config.presets[options.preset];
+
+	var setNames = [],
+		sourceNames = [],
+		sourceLoader = function(){},
+		specLoader = function(){},
+		envName = 'browser';
+
+	
+	// private methods
+	
+	var getDefault = function(){
+		return config.presets[config.defaultPresets[envName]];
+	};
+	
+	var getSets = function(){
+		var requestedSets = [],
+			sets = (preset || options).sets || getDefault().sets;
+	
+		forEach(sets && isArray(sets) ? sets : [sets], function(set){
+			if (config.sets[set] && indexOf(requestedSets, set) == -1) requestedSets.push(set);
+		});
+	
+		return requestedSets;			
+	};
+		
+	var getSource = function(){
+		var requestedSource = [],
+			source = (preset || options).source || getDefault().source;
+		
+		forEach(source && isArray(source) ? source : [source], function(src){
+			if (config.source[src] && indexOf(requestedSource, src) == -1) requestedSource.push(src);
+		});
+	
+		return requestedSource;			
+	};
+	
+	var loadSets = function(){
+		forEach(setNames, function(set){
+			specLoader(config.sets[set].files, config.sets[set].path);
+		});
+	};
+		
+	var loadSource = function(){
+		forEach(sourceNames, function(set){
+			sourceLoader(config.source[set].files, config.source[set].path);
+		});
+	};
+
+	// public methods
+	
+	return {
+
+		setSourceLoader: function(loader){
+			sourceLoader = loader;
+			return this;
+		},
+		
+		setSpecLoader: function(load){
+			specLoader = load;
+			return this;
+		},
+		
+		setEnvName: function(name){
+			envName = name;
+			return this;
+		},
+
+		run: function(){
+			
+			// Get the sets and source
+			setNames = getSets();		
+			sourceNames = getSource();
+			
+			// Load the sets and source
+			loadSource();
+			loadSets();
+			
+			return this;
+		},
+		
+		getSetNames: function(){
+			return setNames;
+		},
+
+		getSourceNames: function(){
+			return sourceNames;
+		}
+		
+	};
+	
+};
+
+
+})(typeof exports != 'undefined' ? exports : this);

+ 17 - 0
ext/thirdparty/js/test-runner/mootools-runner/Helpers/RunnerOptions.js

@@ -0,0 +1,17 @@
+var puts = require('sys').puts;
+
+exports.parseOptions = function(arg){
+
+if (!arg) arg = '{}';
+
+var options = {};
+try {
+	options = JSON.parse(arg);
+} catch(e){
+	puts('Please provide a proper JSON-Object');
+	return null;
+}
+
+return options;
+
+};

+ 2287 - 0
ext/thirdparty/js/test-runner/mootools-runner/Helpers/Syn.js

@@ -0,0 +1,2287 @@
+// funcunit/synthetic/synthetic.js
+
+(function($){
+
+var extend = function(d, s) { for (var p in s) d[p] = s[p]; return d;},
+	// only uses browser detection for key events
+	browser = {
+		msie:     !!(window.attachEvent && !window.opera),
+		opera:  !!window.opera,
+		webkit : navigator.userAgent.indexOf('AppleWebKit/') > -1,
+		safari: navigator.userAgent.indexOf('AppleWebKit/') > -1 && navigator.userAgent.indexOf('Chrome/') == -1,
+		gecko:  navigator.userAgent.indexOf('Gecko') > -1,
+		mobilesafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/),
+		rhino : navigator.userAgent.match(/Rhino/) && true
+	},
+	createEventObject = function(type, options, element){
+		var event = element.ownerDocument.createEventObject();
+		return extend(event, options);
+	},
+	data = {}, 
+	id = 0, 
+	expando = "_synthetic"+(new Date() - 0),
+	bind,
+	unbind,
+	key = /keypress|keyup|keydown/,
+	page = /load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll/,
+
+/**
+ * @constructor Syn
+ * @download funcunit/dist/syn.js
+ * @test funcunit/synthetic/qunit.html
+ * Syn is used to simulate user actions.  It creates synthetic events and
+ * performs their default behaviors.
+ * 
+ * <h2>Basic Use</h2>
+ * The following clicks an input element with <code>id='description'</code>
+ * and then types <code>'Hello World'</code>.
+ * 
+@codestart
+Syn.click({},'description')
+  .type("Hello World")
+@codeend
+ * <h2>User Actions and Events</h2>
+ * <p>Syn is typically used to simulate user actions as opposed to triggering events. Typing characters
+ * is an example of a user action.  The keypress that represents an <code>'a'</code>
+ * character being typed is an example of an event. 
+ * </p>
+ * <p>
+ *   While triggering events is supported, it's much more useful to simulate actual user behavior.  The 
+ *   following actions are supported by Syn:
+ * </p>
+ * <ul>
+ *   <li><code>[Syn.prototype.click click]</code> - a mousedown, focus, mouseup, and click.</li>
+ *   <li><code>[Syn.prototype.dblclick dblclick]</code> - two <code>click!</code> events followed by a <code>dblclick</code>.</li>
+ *   <li><code>[Syn.prototype.key key]</code> - types a single character (keydown, keypress, keyup).</li>
+ *   <li><code>[Syn.prototype.type type]</code> - types multiple characters into an element.</li>
+ *   <li><code>[Syn.prototype.move move]</code> - moves the mouse from one position to another (triggering mouseover / mouseouts).</li>
+ *   <li><code>[Syn.prototype.drag drag]</code> - a mousedown, followed by mousemoves, and a mouseup.</li>
+ * </ul>
+ * All actions run asynchronously.  
+ * Click on the links above for more 
+ * information on how to use the specific action.
+ * <h2>Asynchronous Callbacks</h2>
+ * Actions don't complete immediately. This is almost 
+ * entirely because <code>focus()</code> 
+ * doesn't run immediately in IE.
+ * If you provide a callback function to Syn, it will 
+ * be called after the action is completed.
+ * <br/>The following checks that "Hello World" was entered correctly: 
+@codestart
+Syn.click({},'description')
+  .type("Hello World", function(){
+  
+  ok("Hello World" == document.getElementById('description').value)  
+})
+@codeend
+<h2>Asynchronous Chaining</h2>
+<p>You might have noticed the [Syn.prototype.then then] method.  It provides chaining 
+so you can do a sequence of events with a single (final) callback.  
+</p><p>
+If an element isn't provided to then, it uses the previous Syn's element.
+</p>
+The following does a lot of stuff before checking the result:
+@codestart
+Syn.type('ice water','title')
+  .type('ice and water','description')
+  .click({},'create')
+  .drag({to: 'favorites'},'newRecipe',
+    function(){
+      ok($('#newRecipe').parents('#favorites').length);
+    })
+@codeend
+
+<h2>jQuery Helper</h2>
+If jQuery is present, Syn adds a triggerSyn helper you can use like:
+@codestart
+$("#description").triggerSyn("type","Hello World");
+@codeend
+ * <h2>Key Event Recording</h2>
+ * <p>Every browser has very different rules for dispatching key events.  
+ * As there is no way to feature detect how a browser handles key events,
+ * synthetic uses a description of how the browser behaves generated
+ * by a recording application.  </p>
+ * <p>
+ * If you want to support a browser not currently supported, you can
+ * record that browser's key event description and add it to
+ * <code>Syn.key.browsers</code> by it's navigator agent.
+ * </p>
+@codestart
+Syn.key.browsers["Envjs\ Resig/20070309 PilotFish/1.2.0.10\1.6"] = {
+  'prevent':
+    {"keyup":[],"keydown":["char","keypress"],"keypress":["char"]},
+  'character':
+    { ... }
+}
+@codeend
+ * <h2>Limitations</h2>
+ * Syn fully supports IE 6+, FF 3+, Chrome, Safari, Opera 10+.
+ * With FF 1+, drag / move events are only partially supported. They will
+ * not trigger mouseover / mouseout events.<br/>
+ * Safari crashes when a mousedown is triggered on a select.  Syn will not 
+ * create this event.
+ * <h2>Contributing to Syn</h2>
+ * Have we missed something? We happily accept patches.  The following are 
+ * important objects and properties of Syn:
+ * <ul>
+ * 	<li><code>Syn.create</code> - contains methods to setup, convert options, and create an event of a specific type.</li>
+ *  <li><code>Syn.defaults</code> - default behavior by event type (except for keys).</li>
+ *  <li><code>Syn.key.defaults</code> - default behavior by key.</li>
+ *  <li><code>Syn.keycodes</code> - supported keys you can type.</li>
+ * </ul>
+ * <h2>Roll Your Own Functional Test Framework</h2>
+ * <p>Syn is really the foundation of JavaScriptMVC's functional testing framework - [FuncUnit].
+ *   But, we've purposely made Syn work without any dependencies in the hopes that other frameworks or 
+ *   testing solutions can use it as well.
+ * </p>
+ * @init 
+ * Creates a synthetic event on the element.
+ * @param {Object} type
+ * @param {Object} options
+ * @param {Object} element
+ * @param {Object} callback
+ * @return Syn
+ */
+Syn = function(type, options, element, callback){		
+	return ( new Syn.init(type, options, element, callback) )
+}
+	
+if(window.addEventListener){ // Mozilla, Netscape, Firefox
+	bind = function(el, ev, f){
+		el.addEventListener(ev, f, false)
+	}
+	unbind = function(el, ev, f){
+		el.removeEventListener(ev, f, false)
+	}
+}else{
+	bind = function(el, ev, f){
+		el.attachEvent("on"+ev, f)
+	}
+	unbind = function(el, ev, f){
+		el.detachEvent("on"+ev, f)
+	}
+}	
+/**
+ * @Static
+ */	
+extend(Syn,{
+	/**
+	 * Creates a new synthetic event instance
+	 * @hide
+	 * @param {Object} type
+	 * @param {Object} options
+	 * @param {Object} element
+	 * @param {Object} callback
+	 */
+	init : function(type, options, element, callback){
+		var args = Syn.args(options,element, callback),
+			self = this;
+		this.queue = [];
+		this.element = args.element;
+		
+		//run event
+		if(typeof this[type] == "function") {
+			this[type](args.options, args.element, function(defaults,el ){
+				args.callback && args.callback.apply(self, arguments);
+				self.done.apply(self, arguments)		
+			})
+		}else{
+			this.result = Syn.trigger(type, args.options, args.element);
+			args.callback && args.callback.call(this, args.element, this.result);
+		}
+	},
+	/**
+	 * Returns an object with the args for a Syn.
+	 * @hide
+	 * @return {Object}
+	 */
+	args : function(){
+		var res = {}
+		for(var i=0; i < arguments.length; i++){
+			if(typeof arguments[i] == 'function'){
+				res.callback = arguments[i]
+			}else if(arguments[i] && arguments[i].jquery){
+				res.element = arguments[i][0];
+			}else if(arguments[i] && arguments[i].nodeName){
+				res.element = arguments[i];
+			}else if(res.options && typeof arguments[i] == 'string'){ //we can get by id
+				res.element = document.getElementById(arguments[i])
+			}
+			else if(arguments[i]){
+				res.options = arguments[i];
+			}
+		}
+		return res;
+	},
+	click : function( options, element, callback){
+		Syn('click!',options,element, callback);
+	},
+	/**
+	 * @attribute defaults
+	 * Default actions for events.  Each default function is called with this as its 
+	 * element.  It should return true if a timeout 
+	 * should happen after it.  If it returns an element, a timeout will happen
+	 * and the next event will happen on that element.
+	 */
+	defaults : {
+		focus : function(){
+			if(!Syn.support.focusChanges){
+				var element = this,
+					nodeName = element.nodeName.toLowerCase();
+				Syn.data(element,"syntheticvalue", element.value)
+				
+				if(nodeName == "input"){
+					
+					bind(element, "blur", function(){
+						
+						if( Syn.data(element,"syntheticvalue") !=  element.value){
+							
+							Syn.trigger("change", {}, element);
+						}
+						unbind(element,"blur", arguments.callee)
+					})
+					
+				}
+			}
+		}
+	},
+	changeOnBlur : function(element, prop, value){
+		
+		bind(element, "blur", function(){		
+			if( value !=  element[prop]){
+				Syn.trigger("change", {}, element);
+			}
+			unbind(element,"blur", arguments.callee)
+		})
+		
+	},
+	/**
+	 * Returns the closest element of a particular type.
+	 * @hide
+	 * @param {Object} el
+	 * @param {Object} type
+	 */
+	closest : function(el, type){
+		while(el && el.nodeName.toLowerCase() != type.toLowerCase()){
+			el = el.parentNode
+		}
+		return el;
+	},
+	/**
+	 * adds jQuery like data (adds an expando) and data exists FOREVER :)
+	 * @hide
+	 * @param {Object} el
+	 * @param {Object} key
+	 * @param {Object} value
+	 */
+	data : function(el, key, value){
+		var d;
+		if(!el[expando]){
+			el[expando] = id++;
+		}
+		if(!data[el[expando]]){
+			data[el[expando]] = {};
+		}
+		d = data[el[expando]]
+		if(value){
+			data[el[expando]][key] = value;
+		}else{
+			return data[el[expando]][key];
+		}
+	},
+	/**
+	 * Calls a function on the element and all parents of the element until the function returns
+	 * false.
+	 * @hide
+	 * @param {Object} el
+	 * @param {Object} func
+	 */
+	onParents : function(el, func){
+		var res;
+		while(el && res !== false){
+			res = func(el)
+			el = el.parentNode
+		}
+		return el;
+	},
+	//regex to match focusable elements
+	focusable : /^(a|area|frame|iframe|label|input|select|textarea|button|html|object)$/i,
+	/**
+	 * Returns if an element is focusable
+	 * @hide
+	 * @param {Object} elem
+	 */
+	isFocusable : function(elem){
+		var attributeNode;
+		return ( this.focusable.test(elem.nodeName) || (
+			(attributeNode = elem.getAttributeNode( "tabIndex" )) && attributeNode.specified ) )
+			&& Syn.isVisible(elem)
+	},
+	/**
+	 * Returns if an element is visible or not
+	 * @hide
+	 * @param {Object} elem
+	 */
+	isVisible : function(elem){
+		return (elem.offsetWidth && elem.offsetHeight) || (elem.clientWidth && elem.clientHeight)
+	},
+	/**
+	 * Gets the tabIndex as a number or null
+	 * @hide
+	 * @param {Object} elem
+	 */
+	tabIndex : function(elem){
+		var attributeNode = elem.getAttributeNode( "tabIndex" );
+		return attributeNode && attributeNode.specified && ( parseInt( elem.getAttribute('tabIndex') ) || 0 )
+	},
+	bind : bind,
+	unbind : unbind,
+	browser: browser,
+	//some generic helpers
+	helpers : {
+		createEventObject : createEventObject,
+		createBasicStandardEvent : function(type, defaults){
+			var event;
+			try {
+				event = document.createEvent("Events");
+			} catch(e2) {
+				event = document.createEvent("UIEvents");
+			} finally {
+				event.initEvent(type, true, true);
+				extend(event, defaults);
+			}
+			return event;
+		},
+		inArray : function(item, array){
+			for(var i =0; i < array.length; i++){
+				if(array[i] == item){
+					return i;
+				}
+			}
+			return -1;
+		},
+		getWindow : function(element){
+			return element.ownerDocument.defaultView || element.ownerDocument.parentWindow
+		},
+		extend:  extend,
+		scrollOffset : function(win){
+			var doc = win.document.documentElement,
+				body = win.document.body;
+			return {
+				left :  (doc && doc.scrollLeft || body && body.scrollLeft || 0) + (doc.clientLeft || 0),
+				top : (doc && doc.scrollTop || body && body.scrollTop || 0) + (doc.clientTop || 0)
+			}
+				
+		},
+		addOffset : function(options, el){
+			if(typeof options == 'object' &&
+			   options.clientX === undefined &&
+			   options.clientY === undefined &&
+			   options.pageX   === undefined &&
+			   options.pageY   === undefined && window.jQuery){
+				var el = window.jQuery(el)
+					off = el.offset();
+				options.pageX = off.left + el.width() /2 ;
+				options.pageY = off.top + el.height() /2 ;
+			}
+		}
+	},
+	// place for key data
+	key : {
+		ctrlKey : null,
+		altKey : null,
+		shiftKey : null,
+		metaKey : null
+	},
+	//triggers an event on an element, returns true if default events should be run
+	/**
+	 * Dispatches an event and returns true if default events should be run.
+	 * @hide
+	 * @param {Object} event
+	 * @param {Object} element
+	 * @param {Object} type
+	 * @param {Object} autoPrevent
+	 */
+	dispatch : (document.documentElement.dispatchEvent ? 
+				function(event, element, type, autoPrevent){
+					var preventDefault = event.preventDefault, 
+						prevents = autoPrevent ? -1 : 0;
+					
+					//automatically prevents the default behavior for this event
+					//this is to protect agianst nasty browser freezing bug in safari
+					if(autoPrevent){
+						bind(element, type, function(ev){
+							ev.preventDefault()
+							unbind(this, type, arguments.callee)
+						})
+					}
+					
+					
+					event.preventDefault = function(){
+						prevents++;
+						if(++prevents > 0){
+							preventDefault.apply(this,[]);
+						}
+					}
+					element.dispatchEvent(event)
+					return prevents <= 0;
+				} : 
+				function(event, element, type){
+					try {window.event = event;}catch(e) {}
+					//source element makes sure element is still in the document
+					return element.sourceIndex <= 0 || element.fireEvent('on' + type, event)
+				}
+			),
+	/**
+	 * @attribute
+	 * @hide
+	 * An object of eventType -> function that create that event.
+	 */
+	create :  {
+		//-------- PAGE EVENTS ---------------------
+		page : {
+			event : document.createEvent ? function(type, options, element){
+					var event = element.ownerDocument.createEvent("Events");
+					event.initEvent(type, true, true ); 
+					return event;
+				} : createEventObject
+		},
+		// unique events
+		focus : {
+			event : function(type, options, element){
+				Syn.onParents(element, function(el){
+					if( Syn.isFocusable(el)){
+						if(el.nodeName.toLowerCase() != 'html'){
+							el.focus();
+						}
+						return false
+					}
+				});
+				return true;
+			}
+		}
+	},
+	/**
+	 * @attribute support
+	 * Feature detected properties of a browser's event system.
+	 * Support has the following properties:
+	 * <ul>
+	 * 	<li><code>clickChanges</code> - clicking on an option element creates a change event.</li>
+	 *  <li><code>clickSubmits</code> - clicking on a form button submits the form.</li>
+	 *  <li><code>mouseupSubmits</code> - a mouseup on a form button submits the form.</li>
+	 *  <li><code>radioClickChanges</code> - clicking a radio button changes the radio.</li>
+	 *  <li><code>focusChanges</code> - focus/blur creates a change event.</li>
+	 *  <li><code>linkHrefJS</code> - An achor's href JavaScript is run.</li>
+	 *  <li><code>mouseDownUpClicks</code> - A mousedown followed by mouseup creates a click event.</li>
+	 *  <li><code>tabKeyTabs</code> - A tab key changes tabs.</li>
+	 *  <li><code>keypressOnAnchorClicks</code> - Keying enter on an anchor triggers a click.</li>
+	 * </ul>
+	 */
+	support : {
+		clickChanges : false,
+		clickSubmits : false,
+		keypressSubmits : false,
+		mouseupSubmits: false,
+		radioClickChanges : false,
+		focusChanges : false,
+		linkHrefJS : false,
+		keyCharacters : false,
+		backspaceWorks : false,
+		mouseDownUpClicks : false,
+		tabKeyTabs : false,
+		keypressOnAnchorClicks : false,
+		optionClickBubbles : false
+	},
+	/**
+	 * Creates a synthetic event and dispatches it on the element.  
+	 * This will run any default actions for the element.
+	 * Typically you want to use Syn, but if you want the return value, use this.
+	 * @param {String} type
+	 * @param {Object} options
+	 * @param {HTMLElement} element
+	 * @return {Boolean} true if default events were run, false if otherwise.
+	 */
+	trigger : function(type, options, element){
+		options || (options = {});
+		
+		var create = Syn.create,
+			setup = create[type] && create[type].setup,
+			kind = key.test(type) ? 
+				'key' : 
+				( page.test(type) ?
+					"page" : "mouse" ),
+				createType = create[type] || {},
+				createKind = create[kind],
+				event,
+				ret,
+				autoPrevent = options._autoPrevent,
+				dispatchEl = element;
+		
+		//any setup code?
+		Syn.support.ready && setup && setup(type, options, element);
+		
+		
+		//get kind
+		
+		delete options._autoPrevent;
+			
+		if(createType.event){
+			ret = createType.event(type, options, element)
+		}else{
+			//convert options
+			options = createKind.options ? createKind.options(type,options,element) : options;
+			
+			if(!Syn.support.changeBubbles && /option/i.test(element.nodeName)){
+				dispatchEl = element.parentNode; //jQuery expects clicks on select
+			}
+			
+			//create the event
+			event = createKind.event(type,options,dispatchEl)
+			
+			//send the event
+			ret = Syn.dispatch(event, dispatchEl, type, autoPrevent)
+		}
+		
+		//run default behavior
+		ret && Syn.support.ready 
+			&& Syn.defaults[type] 
+			&& Syn.defaults[type].call(element, options, autoPrevent);
+		return ret;
+	},
+	eventSupported: function( eventName ) { 
+		var el = document.createElement("div"); 
+		eventName = "on" + eventName; 
+
+		var isSupported = (eventName in el); 
+		if ( !isSupported ) { 
+			el.setAttribute(eventName, "return;"); 
+			isSupported = typeof el[eventName] === "function"; 
+		} 
+		el = null; 
+
+		return isSupported; 
+	}
+	
+});
+	var h = Syn.helpers;
+/**
+ * @Prototype
+ */
+extend(Syn.init.prototype,{
+	/**
+	 * @function then
+	 * <p>
+	 * Then is used to chain a sequence of actions to be run one after the other.
+	 * This is useful when many asynchronous actions need to be performed before some
+	 * final check needs to be made.
+	 * </p>
+	 * <p>The following clicks and types into the <code>id='age'</code> element and then checks that only numeric characters can be entered.</p>
+	 * <h3>Example</h3>
+	 * @codestart
+	 * Syn('click',{},'age')
+	 *   .then('type','I am 12',function(){
+	 *   equals($('#age').val(),"12")  
+	 * })
+	 * @codeend
+	 * If the element argument is undefined, then the last element is used.
+	 * 
+	 * @param {String} type The type of event or action to create: "_click", "_dblclick", "_drag", "_type".
+	 * @param {Object} options Optiosn to pass to the event.
+	 * @param {String|HTMLElement} [element] A element's id or an element.  If undefined, defaults to the previous element.
+	 * @param {Function} [callback] A function to callback after the action has run, but before any future chained actions are run.
+	 */
+	then : function(type, options, element, callback){
+		if(Syn.autoDelay){
+			this.delay();
+		}
+		var args = Syn.args(options,element, callback),
+			self = this;
+
+		
+		//if stack is empty run right away
+		
+		//otherwise ... unshift it
+		this.queue.unshift(function(el, prevented){
+			
+			if(typeof this[type] == "function") {
+				this.element = args.element || el;
+				this[type](args.options, this.element, function(defaults, el){
+					args.callback && args.callback.apply(self, arguments);
+					self.done.apply(self, arguments)		
+				})
+			}else{
+				this.result = Syn.trigger(type, args.options, args.element);
+				args.callback && args.callback.call(this, args.element, this.result);
+				return this;
+			}
+		})
+		return this;
+	},
+	/**
+	 * Delays the next command a set timeout.
+	 * @param {Number} [timeout]
+	 * @param {Function} [callback]
+	 */
+	delay : function(timeout, callback){
+		if(typeof timeout == 'function'){
+			callback = timeout;
+			timeout = null;
+		}
+		timeout = timeout || 600
+		var self = this;
+		this.queue.unshift(function(){
+			setTimeout(function(){
+				callback && callback.apply(self,[])
+				self.done.apply(self, arguments)
+			},timeout)
+		})
+		return this;
+	},
+	done : function( defaults, el){
+		el && (this.element = el);;
+		if(this.queue.length){
+			this.queue.pop().call(this, this.element, defaults);
+		}
+		
+	},
+	/**
+	 * @function click
+	 * Clicks an element by triggering a mousedown, 
+	 * mouseup, 
+	 * and a click event.
+	 * <h3>Example</h3>
+	 * @codestart
+	 * Syn.click({},'create',function(){
+	 *   //check something
+	 * })
+	 * @codeend
+	 * You can also provide the coordinates of the click.  
+	 * If jQuery is present, it will set clientX and clientY
+	 * for you.  Here's how to set it yourself:
+	 * @codestart
+	 * Syn.click(
+	 *     {clientX: 20, clientY: 100},
+	 *     'create',
+	 *     function(){
+	 *       //check something
+	 *     })
+	 * @codeend
+	 * You can also provide pageX and pageY and Syn will convert it for you.
+	 * @param {Object} options
+	 * @param {HTMLElement} element
+	 * @param {Function} callback
+	 */
+	"_click" : function(options, element, callback){
+		Syn.helpers.addOffset(options, element);
+		Syn.trigger("mousedown", options, element);
+		
+		//timeout is b/c IE is stupid and won't call focus handlers
+		setTimeout(function(){
+			Syn.trigger("mouseup", options, element)
+			if(!Syn.support.mouseDownUpClicks){
+				Syn.trigger("click", options, element)
+			}else{
+				//we still have to run the default (presumably)
+				Syn.defaults.click.call(element)
+			}
+			callback(true)
+		},1)
+	},
+	/**
+	 * Right clicks in browsers that support it (everyone but opera).
+	 * @param {Object} options
+	 * @param {Object} element
+	 * @param {Object} callback
+	 */
+	"_rightClick" : function(options, element, callback){
+		Syn.helpers.addOffset(options, element);
+		var mouseopts =  extend( extend({},Syn.mouse.browser.mouseup ), options)
+		
+		Syn.trigger("mousedown", mouseopts, element);
+		
+		//timeout is b/c IE is stupid and won't call focus handlers
+		setTimeout(function(){
+			Syn.trigger("mouseup", mouseopts, element)
+			if (Syn.mouse.browser.contextmenu) {
+				Syn.trigger("contextmenu", 
+					extend( extend({},Syn.mouse.browser.contextmenu ), options), 
+					element)
+			}
+			callback(true)
+		},1)
+	},
+	/**
+	 * @function dblclick
+	 * Dblclicks an element.  This runs two [Syn.prototype.click click] events followed by
+	 * a dblclick on the element.
+	 * <h3>Example</h3>
+	 * @codestart
+	 * Syn.dblclick({},'open')
+	 * @codeend
+	 * @param {Object} options
+	 * @param {HTMLElement} element
+	 * @param {Function} callback
+	 */
+	"_dblclick" : function(options, element, callback){
+		Syn.helpers.addOffset(options);
+		var self = this;
+		this["click!"](options, element, function(){
+			self["click!"](options, element, function(){
+				Syn.trigger("dblclick", options, element)
+				callback(true)
+			})
+		})
+	}
+})
+
+var actions = ["click","dblclick","move","drag","key","type",'rightClick'],
+	makeAction = function(name){
+		Syn[name] = function(options, element, callback){
+			return Syn("_"+name, options, element, callback)
+		}
+		Syn.init.prototype[name] = function(options, element, callback){
+			return this.then("_"+name, options, element, callback)
+		}
+	}
+for(var i=0; i < actions.length; i++){
+	makeAction(actions[i]);
+}
+/**
+ * Used for creating and dispatching synthetic events.
+ * @codestart
+ * new MVC.Syn('click').send(MVC.$E('id'))
+ * @codeend
+ * @init Sets up a synthetic event.
+ * @param {String} type type of event, ex: 'click'
+ * @param {optional:Object} options
+ */
+
+if (window.jQuery) {
+	jQuery.fn.triggerSyn = function(type, options, callback){
+		Syn(type, options, this[0], callback)
+		return this;
+	};
+}
+
+window.Syn = Syn;
+	
+
+})();
+
+// funcunit/synthetic/mouse.js
+
+(function($){
+
+
+var h = Syn.helpers;
+
+Syn.mouse = {};
+h.extend(Syn.defaults,{
+	mousedown : function(options){
+		Syn.trigger("focus", {}, this)
+	},
+	click : function(){
+		// prevents the access denied issue in IE if the click causes the element to be destroyed
+		var element = this;
+		try {
+			element.nodeType;
+		} catch(e){
+			return;
+		}
+		//get old values
+		var href,
+			checked = Syn.data(element,"checked"),
+			scope = Syn.helpers.getWindow(element),
+			nodeName = element.nodeName.toLowerCase();
+		
+		if( (href = Syn.data(element,"href") ) ){
+			element.setAttribute('href',href)
+		}
+
+		
+		
+		//run href javascript
+		if(!Syn.support.linkHrefJS 
+			&& /^\s*javascript:/.test(element.href)){
+			//eval js
+			var code = element.href.replace(/^\s*javascript:/,"")
+				
+			//try{
+			if (code != "//" && code.indexOf("void(0)") == -1) {
+				if(window.selenium){
+					eval("with(selenium.browserbot.getCurrentWindow()){"+code+"}")
+				}else{
+					eval("with(scope){"+code+"}")
+				}
+			}
+		}
+		
+		//submit a form
+		if(nodeName == "input" 
+			&& element.type == "submit" 
+			&& !(Syn.support.clickSubmits)){
+				
+			var form =  Syn.closest(element, "form");
+			if(form){
+				Syn.trigger("submit",{},form)
+			}
+			
+		}
+		//follow a link, probably needs to check if in an a.
+		if(nodeName == "a" 
+			&& element.href 
+			&& !/^\s*javascript:/.test(element.href)){
+				
+			scope.location.href = element.href;
+			
+		}
+		
+		//change a checkbox
+		if(nodeName == "input" 
+			&& element.type == "checkbox"){
+			
+			if(!Syn.support.clickChecks && !Syn.support.changeChecks){
+				element.checked = !element.checked;
+			}
+			if(!Syn.support.clickChanges){
+				Syn.trigger("change",{},  element );
+			}
+		}
+		
+		//change a radio button
+		if(nodeName == "input" && element.type == "radio"){  // need to uncheck others if not checked
+			
+			if(!Syn.support.clickChecks && !Syn.support.changeChecks){
+				//do the checks manually 
+				if(!element.checked){ //do nothing, no change
+					element.checked = true;
+				}
+			}
+			if(checked != element.checked && !Syn.support.radioClickChanges){
+				Syn.trigger("change",{},  element );
+			}
+		}
+		// change options
+		if(nodeName == "option" && Syn.data(element,"createChange")){
+			Syn.trigger("change",{}, element.parentNode);//does not bubble
+			Syn.data(element,"createChange",false)
+		}
+	}
+})
+	
+
+//add create and setup behavior for mosue events
+h.extend(Syn.create,{
+	mouse : {
+		options : function(type, options, element){
+			var doc = document.documentElement, body = document.body,
+				center = [options.pageX || 0, options.pageY || 0] 
+			return h.extend({
+				bubbles : true,cancelable : true,
+				view : window,detail : 1,
+				screenX : 1, screenY : 1,
+				clientX : options.clientX || center[0] -(doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0), 
+				clientY : options.clientY || center[1] -(doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0),
+				ctrlKey : !!Syn.key.ctrlKey, 
+				altKey : !!Syn.key.altKey, 
+				shiftKey : !!Syn.key.shiftKey, 
+				metaKey : !!Syn.key.metaKey,
+				button : (type == 'contextmenu' ? 2 : 0), 
+				relatedTarget : document.documentElement
+			}, options);
+		},
+		event : document.createEvent ? 
+			function(type, defaults, element){  //Everyone Else
+				var event;
+				
+				try {
+					event = element.ownerDocument.createEvent('MouseEvents');
+					event.initMouseEvent(type, 
+						defaults.bubbles, defaults.cancelable, 
+						defaults.view, 
+						defaults.detail, 
+						defaults.screenX, defaults.screenY,defaults.clientX,defaults.clientY,
+						defaults.ctrlKey,defaults.altKey,defaults.shiftKey,defaults.metaKey,
+						defaults.button,defaults.relatedTarget);
+				} catch(e) {
+					event = h.createBasicStandardEvent(type,defaults)
+				}
+				event.synthetic = true;
+				return event;
+			} : 
+			h.createEventObject
+	},
+	click : {
+		setup : function(type, options, element){
+			try{
+				Syn.data(element,"checked", element.checked);
+			}catch(e){}
+			if( 
+				element.nodeName.toLowerCase() == "a" 
+				&& element.href  
+				&& !/^\s*javascript:/.test(element.href)){
+				
+				//save href
+				Syn.data(element,"href", element.href)
+				
+				//remove b/c safari/opera will open a new tab instead of changing the page
+				element.setAttribute('href','javascript://')
+			}
+			//if select or option, save old value and mark to change
+			
+			
+			if(/option/i.test(element.nodeName)){
+				var child = element.parentNode.firstChild,
+				i = -1;
+				while(child){
+					if(child.nodeType ==1){
+						i++;
+						if(child == element) break;
+					}
+					child = child.nextSibling;
+				}
+				if(i !== element.parentNode.selectedIndex){
+					//shouldn't this wait on triggering
+					//change?
+					element.parentNode.selectedIndex = i;
+					Syn.data(element,"createChange",true)
+				}
+			}
+		}
+	},
+	mousedown : {
+		setup : function(type,options, element){
+			var nn = element.nodeName.toLowerCase();
+			//we have to auto prevent default to prevent freezing error in safari
+			if(Syn.browser.safari && (nn == "select" || nn == "option" )){
+				options._autoPrevent = true;
+			}
+		}
+	}
+});
+//do support code
+(function(){
+	if(!document.body){
+		setTimeout(arguments.callee,1)
+		return;
+	}
+	var oldSynth = window.__synthTest;
+	window.__synthTest = function(){
+		Syn.support.linkHrefJS = true;
+	}
+	var div = document.createElement("div"), 
+		checkbox, 
+		submit, 
+		form, 
+		input, 
+		select;
+		
+	div.innerHTML = "<form id='outer'>"+
+		"<input name='checkbox' type='checkbox'/>"+
+		"<input name='radio' type='radio' />"+
+		"<input type='submit' name='submitter'/>"+
+		"<input type='input' name='inputter'/>"+
+		"<input name='one'>"+
+		"<input name='two'/>"+
+		"<a href='javascript:__synthTest()' id='synlink'></a>"+
+		"<select><option></option></select>"+
+		"</form>";
+	document.documentElement.appendChild(div);
+	form = div.firstChild
+	checkbox = form.childNodes[0];
+	submit = form.childNodes[2];
+	select = form.getElementsByTagName('select')[0]
+	
+	checkbox.checked = false;
+	checkbox.onchange = function(){
+		Syn.support.clickChanges = true;
+	}
+
+	Syn.trigger("click", {}, checkbox)
+	Syn.support.clickChecks = checkbox.checked;
+	checkbox.checked = false;
+	
+	Syn.trigger("change", {}, checkbox);
+	
+	Syn.support.changeChecks = checkbox.checked;
+	
+	form.onsubmit = function(ev){
+		if (ev.preventDefault) 
+			ev.preventDefault();
+		Syn.support.clickSubmits = true;
+		return false;
+	}
+	Syn.trigger("click", {}, submit)
+
+		
+	
+	form.childNodes[1].onchange = function(){
+		Syn.support.radioClickChanges = true;
+	}
+	Syn.trigger("click", {}, form.childNodes[1])
+	
+	
+	Syn.bind(div, 'click', function(){
+		Syn.support.optionClickBubbles = true;
+		Syn.unbind(div,'click', arguments.callee)
+	})
+	Syn.trigger("click",{},select.firstChild)
+	
+	
+	Syn.support.changeBubbles = Syn.eventSupported('change');
+	
+	//test if mousedown followed by mouseup causes click (opera), make sure there are no clicks after this
+	div.onclick = function(){
+		Syn.support.mouseDownUpClicks = true;
+	}
+	Syn.trigger("mousedown",{},div)
+	Syn.trigger("mouseup",{},div)
+	
+	document.documentElement.removeChild(div);
+	
+	//check stuff
+	window.__synthTest = oldSynth;
+	//support.ready = true;
+})();
+
+
+
+})();
+
+// funcunit/synthetic/browsers.js
+
+(function($){
+
+	Syn.key.browsers = {
+		webkit : {
+			'prevent':
+			 {"keyup":[],"keydown":["char","keypress"],"keypress":["char"]},
+			'character':
+			 {"keydown":[0,"key"],"keypress":["char","char"],"keyup":[0,"key"]},
+			'specialChars':
+			 {"keydown":[0,"char"],"keyup":[0,"char"]},
+			'navigation':
+			 {"keydown":[0,"key"],"keyup":[0,"key"]},
+			'special':
+			 {"keydown":[0,"key"],"keyup":[0,"key"]},
+			'tab':
+			 {"keydown":[0,"char"],"keyup":[0,"char"]},
+			'pause-break':
+			 {"keydown":[0,"key"],"keyup":[0,"key"]},
+			'caps':
+			 {"keydown":[0,"key"],"keyup":[0,"key"]},
+			'escape':
+			 {"keydown":[0,"key"],"keyup":[0,"key"]},
+			'num-lock':
+			 {"keydown":[0,"key"],"keyup":[0,"key"]},
+			'scroll-lock':
+			 {"keydown":[0,"key"],"keyup":[0,"key"]},
+			'print':
+			 {"keyup":[0,"key"]},
+			'function':
+			 {"keydown":[0,"key"],"keyup":[0,"key"]},
+			'\r':
+			 {"keydown":[0,"key"],"keypress":["char","key"],"keyup":[0,"key"]}
+		},
+		gecko : {
+			'prevent':
+			 {"keyup":[],"keydown":["char"],"keypress":["char"]},
+			'character':
+			 {"keydown":[0,"key"],"keypress":["char",0],"keyup":[0,"key"]},
+			'specialChars':
+			 {"keydown":[0,"key"],"keypress":[0,"key"],"keyup":[0,"key"]},
+			'navigation':
+			 {"keydown":[0,"key"],"keypress":[0,"key"],"keyup":[0,"key"]},
+			'special':
+			 {"keydown":[0,"key"],"keyup":[0,"key"]},
+			'\t':
+			 {"keydown":[0,"key"],"keypress":[0,"key"],"keyup":[0,"key"]},
+			'pause-break':
+			 {"keydown":[0,"key"],"keypress":[0,"key"],"keyup":[0,"key"]},
+			'caps':
+			 {"keydown":[0,"key"],"keyup":[0,"key"]},
+			'escape':
+			 {"keydown":[0,"key"],"keypress":[0,"key"],"keyup":[0,"key"]},
+			'num-lock':
+			 {"keydown":[0,"key"],"keyup":[0,"key"]},
+			'scroll-lock':
+			 {"keydown":[0,"key"],"keyup":[0,"key"]},
+			'print':
+			 {"keyup":[0,"key"]},
+			'function':
+			 {"keydown":[0,"key"],"keyup":[0,"key"]},
+			'\r':
+			 {"keydown":[0,"key"],"keypress":[0,"key"],"keyup":[0,"key"]}
+		},
+		msie : {
+			'prevent':{"keyup":[],"keydown":["char","keypress"],"keypress":["char"]},
+			'character':{"keydown":[null,"key"],"keypress":[null,"char"],"keyup":[null,"key"]},
+			'specialChars':{"keydown":[null,"char"],"keyup":[null,"char"]},
+			'navigation':{"keydown":[null,"key"],"keyup":[null,"key"]},
+			'special':{"keydown":[null,"key"],"keyup":[null,"key"]},
+			'tab':{"keydown":[null,"char"],"keyup":[null,"char"]},
+			'pause-break':{"keydown":[null,"key"],"keyup":[null,"key"]},
+			'caps':{"keydown":[null,"key"],"keyup":[null,"key"]},
+			'escape':{"keydown":[null,"key"],"keypress":[null,"key"],"keyup":[null,"key"]},
+			'num-lock':{"keydown":[null,"key"],"keyup":[null,"key"]},
+			'scroll-lock':{"keydown":[null,"key"],"keyup":[null,"key"]},
+			'print':{"keyup":[null,"key"]},
+			'function':{"keydown":[null,"key"],"keyup":[null,"key"]},
+			'\r':{"keydown":[null,"key"],"keypress":[null,"key"],"keyup":[null,"key"]}	
+		},
+		opera : {
+			'prevent':
+			 {"keyup":[],"keydown":[],"keypress":["char"]},
+			'character':
+			 {"keydown":[null,"key"],"keypress":[null,"char"],"keyup":[null,"key"]},
+			'specialChars':
+			 {"keydown":[null,"char"],"keypress":[null,"char"],"keyup":[null,"char"]},
+			'navigation':
+			 {"keydown":[null,"key"],"keypress":[null,"key"]},
+			'special':
+			 {"keydown":[null,"key"],"keypress":[null,"key"],"keyup":[null,"key"]},
+			'tab':
+			 {"keydown":[null,"char"],"keypress":[null,"char"],"keyup":[null,"char"]},
+			'pause-break':
+			 {"keydown":[null,"key"],"keypress":[null,"key"],"keyup":[null,"key"]},
+			'caps':
+			 {"keydown":[null,"key"],"keyup":[null,"key"]},
+			'escape':
+			 {"keydown":[null,"key"],"keypress":[null,"key"]},
+			'num-lock':
+			 {"keyup":[null,"key"],"keydown":[null,"key"],"keypress":[null,"key"]},
+			'scroll-lock':
+			 {"keydown":[null,"key"],"keypress":[null,"key"],"keyup":[null,"key"]},
+			'print':
+			 {},
+			'function':
+			 {"keydown":[null,"key"],"keypress":[null,"key"],"keyup":[null,"key"]},
+			'\r':
+			 {"keydown":[null,"key"],"keypress":[null,"key"],"keyup":[null,"key"]}	
+		}
+	};
+	
+	Syn.mouse.browsers = {
+		webkit : {"mouseup":{"button":2,"which":3},"contextmenu":{"button":2,"which":3}},
+		opera: {},
+		msie: {"mouseup":{"button":2},"contextmenu":{"button":0}},
+		chrome : {"mouseup":{"button":2,"which":3},"contextmenu":{"button":2,"which":3}},
+		gecko: {"mouseup":{"button":2,"which":3},"contextmenu":{"button":2,"which":3}}
+	}
+	
+	//set browser
+	Syn.key.browser = 
+	(function(){
+		if(Syn.key.browsers[window.navigator.userAgent]){
+			return Syn.key.browsers[window.navigator.userAgent];
+		}
+		for(var browser in Syn.browser){
+			if(Syn.browser[browser] && Syn.key.browsers[browser]){
+				return Syn.key.browsers[browser]
+			}
+		}
+		return Syn.key.browsers.gecko;
+	})();
+	
+	Syn.mouse.browser = 
+	(function(){
+		if(Syn.mouse.browsers[window.navigator.userAgent]){
+			return Syn.mouse.browsers[window.navigator.userAgent];
+		}
+		for(var browser in Syn.browser){
+			if(Syn.browser[browser] && Syn.mouse.browsers[browser]){
+				return Syn.mouse.browsers[browser]
+			}
+		}
+		return Syn.mouse.browsers.gecko;
+	})();
+	
+
+})();
+
+// funcunit/synthetic/key.js
+
+(function($){
+
+
+var h = Syn.helpers,
+	S = Syn,
+
+// gets the selection of an input or textarea
+getSelection = function(el){
+	// use selectionStart if we can
+	if (el.selectionStart !== undefined) {
+		// this is for opera, so we don't have to focus to type how we think we would
+		if(document.activeElement 
+		 	&& document.activeElement != el 
+			&& el.selectionStart == el.selectionEnd 
+			&& el.selectionStart == 0){
+			return {start: el.value.length, end: el.value.length};
+		}
+		return  {start: el.selectionStart, end: el.selectionEnd}
+	}else{
+		//check if we aren't focused
+		//if(document.activeElement && document.activeElement != el){
+			
+			
+		//}
+		try {
+			//try 2 different methods that work differently (IE breaks depending on type)
+			if (el.nodeName.toLowerCase() == 'input') {
+				var real = h.getWindow(el).document.selection.createRange(), r = el.createTextRange();
+				r.setEndPoint("EndToStart", real);
+				
+				var start = r.text.length
+				return {
+					start: start,
+					end: start + real.text.length
+				}
+			}
+			else {
+				var real = h.getWindow(el).document.selection.createRange(), r = real.duplicate(), r2 = real.duplicate(), r3 = real.duplicate();
+				r2.collapse();
+				r3.collapse(false);
+				r2.moveStart('character', -1)
+				r3.moveStart('character', -1)
+				//select all of our element
+				r.moveToElementText(el)
+				//now move our endpoint to the end of our real range
+				r.setEndPoint('EndToEnd', real);
+				var start = r.text.length - real.text.length, end = r.text.length;
+				if (start != 0 && r2.text == "") {
+					start += 2;
+				}
+				if (end != 0 && r3.text == "") {
+					end += 2;
+				}
+				//if we aren't at the start, but previous is empty, we are at start of newline
+				return {
+					start: start,
+					end: end
+				}
+			}
+		}catch(e){
+			return {start: el.value.length, end: el.value.length};
+		}
+	} 
+},
+// gets all focusable elements
+getFocusable = function(el){
+	var document = h.getWindow(el).document,
+		res = [];
+
+	var els = document.getElementsByTagName('*'),
+		len = els.length;
+		
+	for(var i=0;  i< len; i++){
+		Syn.isFocusable(els[i]) && els[i] != document.documentElement && res.push(els[i])
+	}
+	return res;
+	
+	
+};
+
+/**
+ * @add Syn static
+ */
+h.extend(Syn,{
+	/**
+	 * @attribute
+	 * A list of the keys and their keycodes codes you can type.
+	 * You can add type keys with
+	 * @codestart
+	 * Syn('key','delete','title');
+	 * 
+	 * //or 
+	 * 
+	 * Syn('type','One Two Three[left][left][delete]','title')
+	 * @codeend
+	 * 
+	 * The following are a list of keys you can type:
+	 * @codestart text
+	 * \b        - backspace
+	 * \t        - tab
+	 * \r        - enter
+	 * ' '       - space
+	 * a-Z 0-9   - normal characters
+	 * /!@#$*,.? - All other typeable characters
+	 * page-up   - scrolls up
+	 * page-down - scrolls down
+	 * end       - scrolls to bottom
+	 * home      - scrolls to top
+	 * insert    - changes how keys are entered
+	 * delete    - deletes the next character
+	 * left      - moves cursor left
+	 * right     - moves cursor right
+	 * up        - moves the cursor up
+	 * down      - moves the cursor down
+	 * f1-12     - function buttons
+	 * shift, ctrl, alt - special keys
+	 * pause-break      - the pause button
+	 * scroll-lock      - locks scrolling
+	 * caps      - makes caps
+	 * escape    - escape button
+	 * num-lock  - allows numbers on keypad
+	 * print     - screen capture
+	 * @codeend
+	 */
+	keycodes: {
+		//backspace
+		'\b': 8,
+		
+		//tab
+		'\t': 9,
+		
+		//enter
+		'\r': 13,
+		
+		//special
+		'shift': 16,'ctrl': 17,'alt': 18,
+		
+		//weird
+		'pause-break': 19,
+		'caps': 20,
+		'escape': 27,
+		'num-lock': 144,
+		'scroll-lock': 145,
+		'print' : 44,
+		
+		//navigation
+		'page-up': 33,'page-down': 34,'end': 35,'home': 36,
+		'left': 37,'up': 38 ,'right': 39,'down': 40,'insert': 45,'delete': 46,
+		
+		//normal characters
+		' ': 32,
+		'0':48,'1':49,'2':50,'3':51,'4':52,'5':53,'6':54,'7':55,'8':56,'9':57,
+		'a':65,'b':66,'c':67,'d':68,'e':69,'f':70,'g':71,'h':72,'i':73,'j':74,'k':75,'l':76,'m':77,
+		'n':78,'o':79,'p':80,'q':81,'r':82,'s':83,'t':84,'u':85,'v':86,'w':87,'x':88,'y':89,'z':90,
+		//normal-characters, numpad
+		'num0':96,'num1':97,'num2':98,'num3':99,'num4':100,'num5':101,'num6':102,'num7':103,'num8':104,'num9':105,
+		'*':106,'+':107,'-':109,'.':110,
+		//normal-characters, others
+		'/':111,
+		';':186,
+		'=':187,
+		',':188,
+		'-':189,
+		'.':190,
+		'/':191,
+		'`':192,
+		'[':219,
+		'\\':220,
+		']':221,
+		"'":222,
+		
+		//ignore these, you shouldn't use them
+		'left window key':91,'right window key':92,'select key':93,
+		
+		
+		'f1':112,'f2':113,'f3':114,'f4':115,'f5':116,'f6':117,
+		'f7':118,'f8':119,'f9':120,'f10':121,'f11':122,'f12':123
+	},
+	
+	// what we can type in
+	typeable : /input|textarea/i,
+	
+	// selects text on an element
+	selectText: function(el, start, end){
+		if(el.setSelectionRange){
+			if(!end){
+                el.focus();
+                el.setSelectionRange(start, start);
+			} else {
+				el.selectionStart = start;
+				el.selectionEnd = end;
+			}
+		}else if (el.createTextRange) {
+			//el.focus();
+			var r = el.createTextRange();
+			r.moveStart('character', start);
+			end = end || start;
+			r.moveEnd('character', end - el.value.length);
+			
+			r.select();
+		} 
+	},
+	getText: function(el){
+		//first check if the el has anything selected ..
+		if(Syn.typeable.test(el.nodeName)){
+			var sel = getSelection(el);
+			return el.value.substring(sel.start, sel.end)
+		}
+		//otherwise get from page
+		var win = Syn.helpers.getWindow(el);
+		if (win.getSelection) {
+			return win.getSelection().toString();
+		}
+		else  if (win.document.getSelection) {
+			return win.document.getSelection().toString()
+		}
+		else {
+			return win.document.selection.createRange().text;
+		}
+	},
+	getSelection : getSelection
+});
+
+h.extend(Syn.key,{
+	// retrieves a description of what events for this character should look like
+	data : function(key){
+		//check if it is described directly
+		if(S.key.browser[key]){
+			return S.key.browser[key];
+		}
+		for(var kind in S.key.kinds){
+			if(h.inArray(key, S.key.kinds[kind] ) > -1){
+				return S.key.browser[kind]
+			}
+		}
+		return S.key.browser.character
+	},
+	
+	//returns the special key if special
+	isSpecial : function(keyCode){
+		var specials = S.key.kinds.special;
+		for(var i=0; i < specials.length; i++){
+			if(Syn.keycodes[ specials[i] ] == keyCode){
+				return specials[i];
+			}
+		}
+	},
+	/**
+	 * @hide
+	 * gets the options for a key and event type ...
+	 * @param {Object} key
+	 * @param {Object} event
+	 */
+	options : function(key, event){
+		var keyData = Syn.key.data(key);
+		
+		if(!keyData[event]){
+			//we shouldn't be creating this event
+			return null;
+		}
+			
+		var	charCode = keyData[event][0],
+			keyCode = keyData[event][1],
+			result = {};
+			
+		if(keyCode == 'key'){
+			result.keyCode = Syn.keycodes[key]
+		} else if (keyCode == 'char'){
+			result.keyCode = key.charCodeAt(0)
+		}else{
+			result.keyCode = keyCode;
+		}
+		
+		if(charCode == 'char'){
+			result.charCode = key.charCodeAt(0)
+		}else if(charCode !== null){
+			result.charCode = charCode;
+		}
+		
+		
+		return result
+	},
+	//types of event keys
+	kinds : {
+		special : ["shift",'ctrl','alt','caps'],
+		specialChars : ["\b"],
+		navigation: ["page-up",'page-down','end','home','left','up','right','down','insert','delete'],
+		'function' : ['f1','f2','f3','f4','f5','f6','f7','f8','f9','f10','f11','f12']
+	},
+	//returns the default function
+	getDefault : function(key){
+		//check if it is described directly
+		if(Syn.key.defaults[key]){
+			return Syn.key.defaults[key];
+		}
+		for(var kind in Syn.key.kinds){
+			if(h.inArray(key, Syn.key.kinds[kind])> -1 && Syn.key.defaults[kind]  ){
+				return Syn.key.defaults[kind];
+			}
+		}
+		return Syn.key.defaults.character
+	},
+	// default behavior when typing
+	defaults : 	{
+		'character' : function(options, scope, key, force){
+			if(/num\d+/.test(key)){
+				key = key.match(/\d+/)[0]
+			}
+			
+			if(force || (!S.support.keyCharacters && Syn.typeable.test(this.nodeName))){
+				var current = this.value,
+					sel = getSelection(this),
+					before = current.substr(0,sel.start),
+					after = current.substr(sel.end),
+					character = key;
+				
+				this.value = before+character+after;
+				//handle IE inserting \r\n
+				var charLength = character == "\n" && S.support.textareaCarriage ? 2 : character.length;
+				Syn.selectText(this, before.length + charLength)
+			}		
+		},
+		'c' : function(options, scope, key){
+			if(Syn.key.ctrlKey){
+				Syn.key.clipboard = Syn.getText(this)
+			}else{
+				Syn.key.defaults.character.call(this, options,scope, key);
+			}
+		},
+		'v' : function(options, scope, key){
+			if(Syn.key.ctrlKey){
+				Syn.key.defaults.character.call(this, options,scope, Syn.key.clipboard, true);
+			}else{
+				Syn.key.defaults.character.call(this, options,scope, key);
+			}
+		},
+		'a' : function(options, scope, key){
+			if(Syn.key.ctrlKey){
+				Syn.selectText(this, 0, this.value.length)
+			}else{
+				Syn.key.defaults.character.call(this, options,scope, key);
+			}
+		},
+		'home' : function(){
+			Syn.onParents(this, function(el){
+				if(el.scrollHeight != el.clientHeight){
+					el.scrollTop = 0;
+					return false;
+				}
+			})
+		},
+		'end' : function(){
+			Syn.onParents(this, function(el){
+				if(el.scrollHeight != el.clientHeight){
+					el.scrollTop = el.scrollHeight;
+					return false;
+				}
+			})
+		},
+		'page-down' : function(){
+			//find the first parent we can scroll
+			Syn.onParents(this, function(el){
+				if(el.scrollHeight != el.clientHeight){
+					var ch = el.clientHeight
+					el.scrollTop += ch;
+					return false;
+				}
+			})
+		},
+		'page-up' : function(){
+			Syn.onParents(this, function(el){
+				if(el.scrollHeight != el.clientHeight){
+					var ch = el.clientHeight
+					el.scrollTop -= ch;
+					return false;
+				}
+			})
+		},
+		'\b' : function(){
+			//this assumes we are deleting from the end
+			if(!S.support.backspaceWorks && Syn.typeable.test(this.nodeName)){
+				var current = this.value,
+					sel = getSelection(this),
+					before = current.substr(0,sel.start),
+					after = current.substr(sel.end);
+				if(sel.start == sel.end && sel.start > 0){
+					//remove a character
+					this.value = before.substring(0, before.length - 1)+after
+					Syn.selectText(this, sel.start-1)
+				}else{
+					this.value = before+after;
+					Syn.selectText(this, sel.start)
+				}
+				
+				//set back the selection
+			}	
+		},
+		'delete' : function(){
+			if(!S.support.backspaceWorks && Syn.typeable.test(this.nodeName)){
+				var current = this.value,
+					sel = getSelection(this),
+					before = current.substr(0,sel.start),
+					after = current.substr(sel.end);
+				
+				if(sel.start == sel.end && sel.start < this.value.length - 1){
+					//remove a character
+					this.value = before+after.substring(1)
+				}else{
+					this.value = before+after;
+				}
+			}		
+		},
+		'\r' : function(options, scope){
+			
+			var nodeName = this.nodeName.toLowerCase()
+			// submit a form
+			if(!S.support.keypressSubmits && nodeName == 'input'){
+				var form = Syn.closest(this, "form");
+				if(form){
+					Syn.trigger("submit", {}, form);
+				}
+					
+			}
+			//newline in textarea
+			if(!S.support.keyCharacters && nodeName == 'textarea'){
+				Syn.key.defaults.character.call(this, options, scope, "\n")
+			}
+			// 'click' hyperlinks
+			if(!S.support.keypressOnAnchorClicks && nodeName == 'a'){
+				Syn.trigger("click", {}, this);
+			}
+		},
+		// 
+		// Gets all focusable elements.  If the element (this)
+		// doesn't have a tabindex, finds the next element after.
+		// If the element (this) has a tabindex finds the element 
+		// with the next higher tabindex OR the element with the same
+		// tabindex after it in the document.
+		// @return the next element
+		// 
+		'\t' : function(options, scope){
+				// focusable elements
+			var focusEls = getFocusable(this),
+				// the current element's tabindex
+				tabIndex = Syn.tabIndex(this),
+				// will be set to our guess for the next element
+				current = null,
+				// the next index we care about
+				currentIndex = 1000000000,
+				// set to true once we found 'this' element
+				found = false,
+				i = 0,
+				el, 
+				//the tabindex of the tabable element we are looking at
+				elIndex,
+				firstNotIndexed;
+				
+			for(; i< focusEls.length; i++){
+				el = focusEls[i];
+				elIndex = Syn.tabIndex(el) || 0;
+				if(!firstNotIndexed && elIndex === 0){
+					firstNotIndexed = el;
+				}
+				
+				if(tabIndex 
+					&& (found ? elIndex >= tabIndex : elIndex > tabIndex )  
+					&& elIndex < currentIndex){
+						currentIndex = elIndex;
+						current = el;
+				}
+				
+				if(!tabIndex && found && !elIndex){
+					current = el;
+					break;
+				}
+				
+				if(this === el){
+					found= true;
+				}
+			}
+			
+			//restart if we didn't find anything
+			if(!current){
+				current = firstNotIndexed;
+			}
+			current && current.focus();
+			return current;
+		},
+		'left' : function(){
+			if( Syn.typeable.test(this.nodeName) ){
+				var sel = getSelection(this);
+				
+				if(Syn.key.shiftKey){
+					Syn.selectText(this, sel.start == 0 ? 0 : sel.start - 1, sel.end)
+				}else{
+					Syn.selectText(this, sel.start == 0 ? 0 : sel.start - 1)
+				}
+			}
+		},
+		'right' : function(){
+			if( Syn.typeable.test(this.nodeName) ){
+				var sel = getSelection(this);
+				
+				if(Syn.key.shiftKey){
+					Syn.selectText(this, sel.start, sel.end+1 > this.value.length ? this.value.length  : sel.end+1)
+				}else{
+					Syn.selectText(this, sel.end+1 > this.value.length ? this.value.length  : sel.end+1)
+				}
+			}	
+		},
+		'up' : function(){
+			if(/select/i.test(this.nodeName)){
+				
+				this.selectedIndex = this.selectedIndex ? this.selectedIndex-1 : 0;
+				//set this to change on blur?
+			}
+		},
+		'down' : function(){
+			if(/select/i.test(this.nodeName)){
+				Syn.changeOnBlur(this, "selectedIndex", this.selectedIndex)
+				this.selectedIndex = this.selectedIndex+1;
+				//set this to change on blur?
+			}
+		},
+		'shift' : function(){
+			return null;
+		}
+	}
+});
+
+
+h.extend(Syn.create,{
+	keydown : {
+		setup : function(type, options, element){
+			if(h.inArray(options,Syn.key.kinds.special ) != -1){
+				Syn.key[options+"Key"] = element;
+			}
+		}
+	},
+	keyup : {
+		setup : function(type, options, element){
+			if(h.inArray(options,Syn.key.kinds.special )!= -1){
+				Syn.key[options+"Key"] = null;
+			}
+		}
+		},
+	key : {
+		// return the options for a key event
+		options : function(type, options, element){
+			//check if options is character or has character
+			options = typeof options != "object" ? {character : options} : options;
+			
+			//don't change the orignial
+			options = h.extend({}, options)
+			if(options.character){
+				h.extend(options, S.key.options(options.character, type));
+				delete options.character;
+			}
+			
+			options = h.extend({
+				ctrlKey: !!Syn.key.ctrlKey,
+				altKey: !!Syn.key.altKey,
+				shiftKey: !!Syn.key.shiftKey,
+				metaKey: !!Syn.key.metaKey
+			}, options)
+			
+			return options;
+		},
+		// creates a key event
+		event : document.createEvent ? 
+			function(type, options, element){  //Everyone Else
+				var event;
+				
+				try {
+		
+					event = element.ownerDocument.createEvent("KeyEvents");
+					event.initKeyEvent(type, true, true, window, 
+						options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
+						options.keyCode, options.charCode );
+				} catch(e) {
+					event = h.createBasicStandardEvent(type,options)
+				}
+				event.synthetic = true;
+				return event;
+
+			} : 
+			function(type, options, element){
+				var event = h.createEventObject.apply(this,arguments);
+				h.extend(event, options)
+
+				return event;
+			}
+		}
+});
+
+var convert = {
+			"enter" : "\r",
+			"backspace" : "\b",
+			"tab" : "\t",
+			"space" : " "
+		}
+
+/**
+ * @add Syn prototype
+ */
+h.extend(Syn.init.prototype,
+{
+	/**
+	 * @function key
+	 * Types a single key.  The key should be
+	 * a string that matches a 
+	 * [Syn.static.keycodes].
+	 * 
+	 * The following sends a carridge return
+	 * to the 'name' element.
+	 * @codestart
+	 * Syn.key('\r','name')
+	 * @codeend
+	 * For each character, a keydown, keypress, and keyup is triggered if
+	 * appropriate.
+	 * @param {String} options
+	 * @param {HTMLElement} [element]
+	 * @param {Function} [callback]
+	 * @return {HTMLElement} the element currently focused.
+	 */
+	_key : function(options, element, callback){
+		//first check if it is a special up
+		if(/-up$/.test(options) 
+			&& h.inArray(options.replace("-up",""),Syn.key.kinds.special )!= -1){
+			Syn.trigger('keyup',options.replace("-up",""), element )
+			callback(true, element);
+			return;
+		}
+		
+		
+		var key = convert[options] || options,
+			// should we run default events
+			runDefaults = Syn.trigger('keydown',key, element ),
+			
+			// a function that gets the default behavior for a key
+			getDefault = Syn.key.getDefault,
+			
+			// how this browser handles preventing default events
+			prevent = Syn.key.browser.prevent,
+			
+			// the result of the default event
+			defaultResult,
+			
+			// options for keypress
+			keypressOptions = Syn.key.options(key, 'keypress')
+		
+		
+		if(runDefaults){
+			//if the browser doesn't create keypresses for this key, run default
+			if(!keypressOptions){
+				defaultResult = getDefault(key).call(element, keypressOptions, h.getWindow(element), key)
+			}else{
+				//do keypress
+				runDefaults = Syn.trigger('keypress',keypressOptions, element )
+				if(runDefaults){
+					defaultResult = getDefault(key).call(element, keypressOptions, h.getWindow(element), key)
+				}
+			}
+		}else{
+			//canceled ... possibly don't run keypress
+			if(keypressOptions && h.inArray('keypress',prevent.keydown) == -1 ){
+				Syn.trigger('keypress',keypressOptions, element )
+			}
+		}
+		if(defaultResult && defaultResult.nodeName){
+			element = defaultResult
+		}
+		
+		if(defaultResult !== null){
+			setTimeout(function(){
+				Syn.trigger('keyup',Syn.key.options(key, 'keyup'), element )
+				callback(runDefaults, element)
+			},1)
+		}else{
+			callback(runDefaults, element)
+		}
+		
+		
+		//do mouseup
+		
+		return element;
+		// is there a keypress? .. if not , run default
+		// yes -> did we prevent it?, if not run ...
+		
+	},
+	/**
+	 * @function type
+	 * Types sequence of [Syn.prototype.key key actions].  Each
+	 * character is typed, one at a type.
+	 * Multi-character keys like 'left' should be
+	 * enclosed in square brackents.
+	 * 
+	 * The following types 'JavaScript MVC' then deletes the space.
+	 * @codestart
+	 * Syn.type('JavaScript MVC[left][left][left]\b','name')
+	 * @codeend
+	 * 
+	 * Type is able to handle (and move with) tabs (\t).  
+	 * The following simulates tabing and entering values in a form and 
+	 * eventually submitting the form.
+	 * @codestart
+	 * Syn.type("Justin\tMeyer\t27\tjustinbmeyer@gmail.com\r")
+	 * @codeend
+	 * @param {String} options the text to type
+	 * @param {HTMLElement} [element] an element or an id of an element
+	 * @param {Function} [callback] a function to callback
+	 */
+	_type : function(options, element, callback){
+		//break it up into parts ...
+		//go through each type and run
+		var parts = options.match(/(\[[^\]]+\])|([^\[])/g),
+			self  = this,
+			runNextPart = function(runDefaults, el){
+				var part = parts.shift();
+				if(!part){
+					callback(runDefaults, el);
+					return;
+				}
+				el = el || element;
+				if(part.length > 1){
+					part = part.substr(1,part.length - 2)
+				}
+				self._key(part, el, runNextPart)
+			}
+		
+		runNextPart();
+		
+	}
+});
+
+
+//do support code
+(function(){
+	if(!document.body){
+		setTimeout(arguments.callee,1)
+		return;
+	}
+
+	var div = document.createElement("div"), 
+		checkbox, 
+		submit, 
+		form, 
+		input, 
+		submitted = false,
+		anchor,
+		textarea;
+		
+	div.innerHTML = "<form id='outer'>"+
+		"<input name='checkbox' type='checkbox'/>"+
+		"<input name='radio' type='radio' />"+
+		"<input type='submit' name='submitter'/>"+
+		"<input type='input' name='inputter'/>"+
+		"<input name='one'>"+
+		"<input name='two'/>"+
+		"<a href='#abc'></a>"+
+		"<textarea>1\n2</textarea>"
+		"</form>";
+		
+	document.documentElement.appendChild(div);
+	form = div.firstChild;
+	checkbox = form.childNodes[0];
+	submit = form.childNodes[2];
+	anchor = form.getElementsByTagName("a")[0];
+	textarea = form.getElementsByTagName("textarea")[0]
+	form.onsubmit = function(ev){
+		if (ev.preventDefault) 
+			ev.preventDefault();
+		S.support.keypressSubmits = true;
+		ev.returnValue = false;
+		return false;
+	}
+	Syn.trigger("keypress", "\r", form.childNodes[3]);
+	
+	
+	Syn.trigger("keypress", "a", form.childNodes[3]);
+	S.support.keyCharacters = form.childNodes[3].value == "a";
+	
+	
+	form.childNodes[3].value = "a"
+	Syn.trigger("keypress", "\b", form.childNodes[3]);
+	S.support.backspaceWorks = form.childNodes[3].value == "";
+	
+		
+	
+	form.childNodes[3].onchange = function(){
+		S.support.focusChanges = true;
+	}
+	form.childNodes[3].focus();
+	Syn.trigger("keypress", "a", form.childNodes[3]);
+	form.childNodes[5].focus();
+	
+	//test keypress \r on anchor submits
+	S.bind(anchor,"click",function(ev){
+		if (ev.preventDefault) 
+			ev.preventDefault();
+		S.support.keypressOnAnchorClicks = true;
+		ev.returnValue = false;
+		return false;
+	})
+	Syn.trigger("keypress", "\r", anchor);
+	
+	S.support.textareaCarriage = textarea.value.length == 4
+	document.documentElement.removeChild(div);
+	
+	S.support.ready = true;
+})();
+
+
+
+	
+
+})();
+
+// funcunit/synthetic/drag/drag.js
+
+(function($){
+
+	// document body has to exists for this test
+
+	(function(){
+		if (!document.body) {
+			setTimeout(arguments.callee, 1)
+			return;
+		}
+		var div = document.createElement('div')
+		document.body.appendChild(div);
+		Syn.helpers.extend(div.style, {
+			width: "100px",
+			height: "10000px",
+			backgroundColor: "blue",
+			position: "absolute",
+			top: "10px",
+			left: "0px",
+			zIndex: 19999
+		});
+		document.body.scrollTop = 11;
+		if(!document.elementFromPoint){
+			return;
+		}
+		var el = document.elementFromPoint(3, 1)
+		if (el == div) {
+			Syn.support.elementFromClient = true;
+		}
+		else {
+			Syn.support.elementFromPage = true;
+		}
+		document.body.removeChild(div);
+		document.body.scrollTop = 0;
+	})();
+	
+	
+	//gets an element from a point
+	var elementFromPoint = function(point, element){
+		var clientX = point.clientX, clientY = point.clientY, win = Syn.helpers.getWindow(element)
+		
+		if (Syn.support.elementFromPage) {
+			var off = Syn.helpers.scrollOffset(win);
+			clientX = clientX + off.left; //convert to pageX
+			clientY = clientY + off.top; //convert to pageY
+		}
+		
+		return win.document.elementFromPoint ? win.document.elementFromPoint(clientX, clientY) : element;
+	}, //creates an event at a certain point
+	createEventAtPoint = function(event, point, element){
+		var el = elementFromPoint(point, element)
+		Syn.trigger(event, point, el || element)
+		return el;
+	}, // creates a mousemove event, but first triggering mouseout / mouseover if appropriate
+	mouseMove = function(point, element, last){
+		var el = elementFromPoint(point, element)
+		if (last != el && el) {
+			var options = Syn.helpers.extend({},point);
+			options.relatedTarget = el;
+			Syn.trigger("mouseout", options, last);
+			options.relatedTarget = last;
+			Syn.trigger("mouseover", options, el);
+		}
+		
+		Syn.trigger("mousemove", point, el || element)
+		return el;
+	}, // start and end are in clientX, clientY
+	startMove = function(start, end, duration, element, callback){
+		var startTime = new Date(), distX = end.clientX - start.clientX, distY = end.clientY - start.clientY, win = Syn.helpers.getWindow(element), current = elementFromPoint(start, element), cursor = win.document.createElement('div')
+		move = function(){
+			//get what fraction we are at
+			var now = new Date(), 
+				scrollOffset = Syn.helpers.scrollOffset(win), 
+				fraction = (now - startTime) / duration, 
+				options = {
+					clientX: distX * fraction + start.clientX,
+					clientY: distY * fraction + start.clientY
+				};
+			if (fraction < 1) {
+				Syn.helpers.extend(cursor.style, {
+					left: (options.clientX + scrollOffset.left + 2) + "px",
+					top: (options.clientY + scrollOffset.top + 2) + "px"
+				})
+				current = mouseMove(options, element, current)
+				setTimeout(arguments.callee, 15)
+			}
+			else {
+				current = mouseMove(end, element, current);
+				win.document.body.removeChild(cursor)
+				callback();
+			}
+		}
+		Syn.helpers.extend(cursor.style, {
+			height: "5px",
+			width: "5px",
+			backgroundColor: "red",
+			position: "absolute",
+			zIndex: 19999,
+			fontSize: "1px"
+		})
+		win.document.body.appendChild(cursor)
+		move();
+	}, 
+	startDrag = function(start, end, duration, element, callback){
+		createEventAtPoint("mousedown", start, element);
+		startMove(start, end, duration, element, function(){
+			createEventAtPoint("mouseup", end, element);
+			callback();
+		})
+	},
+	center = function(el){
+		var j = jQuery(el),
+		o = j.offset();
+		return{
+			pageX: o.left + (j.width() / 2),
+			pageY: o.top + (j.height() / 2)
+		}
+	},
+	convertOption = function(option, win, from){
+		var page = /(\d+)x(\d+)/,
+			client = /(\d+)X(\d+)/,
+			relative = /([+-]\d+)[xX]([+-]\d+)/
+		//check relative "+22x-44"
+		if (typeof option == 'string' && relative.test(option) && from) {
+			var cent = center(from),
+				parts = option.match(relative);
+			option = {
+				pageX: cent.pageX + parseInt(parts[1]),
+				pageY: cent.pageY +parseInt(parts[2])
+			}
+		}
+		if (typeof option == 'string' && page.test(option)) {
+			var parts = option.match(page)
+			option = {
+				pageX: parseInt(parts[1]),
+				pageY: parseInt(parts[2])
+			}
+		}
+		if (typeof option == 'string' && client.test(option)) {
+			var parts = option.match(client)
+			option = {
+				clientX: parseInt(parts[1]),
+				clientY: parseInt(parts[2])
+			}
+		}
+		if (typeof option == 'string') {
+			option = jQuery(option, win.document)[0];
+		}
+		if (option.nodeName) {
+			option = center(option)
+		}
+		if (option.pageX) {
+			var off = Syn.helpers.scrollOffset(win);
+			option = {
+				clientX: option.pageX - off.left,
+				clientY: option.pageY - off.top
+			}
+		}
+		return option;
+	}
+/**
+ * @add Syn prototype
+ */	
+Syn.helpers.extend(Syn.init.prototype,{
+	/**
+	 * @function move
+	 * Moves the cursor from one point to another.  
+	 * <h3>Quick Example</h3>
+	 * The following moves the cursor from (0,0) in
+	 * the window to (100,100) in 1 second.
+	 * @codestart
+	 * Syn.move(
+	 *      {
+	 *        from: {clientX: 0, clientY: 0},
+	 *        to: {clientX: 100, clientY: 100},
+	 *        duration: 1000
+	 *      },
+	 *      document.document)
+	 * @codeend
+	 * <h2>Options</h2>
+	 * There are many ways to configure the endpoints of the move.
+	 * 
+	 * <h3>PageX and PageY</h3>
+	 * If you pass pageX or pageY, these will get converted
+	 * to client coordinates.
+	 * @codestart
+	 * Syn.move(
+	 *      {
+	 *        from: {pageX: 0, pageY: 0},
+	 *        to: {pageX: 100, pageY: 100}
+	 *      },
+	 *      document.document)
+	 * @codeend
+	 * <h3>String Coordinates</h3>
+	 * You can set the pageX and pageY as strings like:
+	 * @codestart
+	 * Syn.move(
+	 *      {
+	 *        from: "0x0",
+	 *        to: "100x100"
+	 *      },
+	 *      document.document)
+	 * @codeend
+	 * <h3>Element Coordinates</h3>
+	 * If jQuery is present, you can pass an element as the from or to option
+	 * and the coordinate will be set as the center of the element.
+	 * @codestart
+	 * Syn.move(
+	 *      {
+	 *        from: $(".recipe")[0],
+	 *        to: $("#trash")[0]
+	 *      },
+	 *      document.document)
+	 * @codeend
+	 * <h3>Query Strings</h3>
+	 * If jQuery is present, you can pass a query string as the from or to option.
+	 * @codestart
+	 * Syn.move(
+	 *      {
+	 *        from: ".recipe",
+	 *        to: "#trash"
+	 *      },
+	 *      document.document)
+	 * @codeend   
+	 * <h3>No From</h3>
+	 * If you don't provide a from, the element argument passed to Syn is used.
+	 * @codestart
+	 * Syn.move(
+	 *      { to: "#trash" },
+	 *      'myrecipe')
+	 * @codeend  
+	 * @param {Object} options
+	 * @param {HTMLElement} from
+	 * @param {Function} callback
+	 */
+	_move : function(options, from, callback){
+		//need to convert if elements
+		var win = Syn.helpers.getWindow(from), 
+			fro = convertOption(options.from || from, win), 
+			to = convertOption(options.to || options, win);
+		
+		startMove(fro, to, options.duration || 500, from, callback);
+	},
+	/**
+	 * @function drag
+	 * Creates a mousedown and drags from one point to another.  
+	 * Check out [Syn.prototype.move move] for API details.
+	 * 
+	 * @param {Object} options
+	 * @param {Object} from
+	 * @param {Object} callback
+	 */
+	_drag : function(options, from, callback){
+		//need to convert if elements
+		var win = Syn.helpers.getWindow(from), 
+			fro = convertOption(options.from || from, win, from), 
+			to = convertOption(options.to || options, win, from);
+		
+		startDrag(fro, to, options.duration || 500, from, callback);
+	}
+})
+
+
+})();
+

+ 253 - 0
ext/thirdparty/js/test-runner/mootools-runner/Helpers/jasmine-html.js

@@ -0,0 +1,253 @@
+// Object.toQueryString from MooTools Core
+jasmine.toQueryString = function(object, base){
+	var queryString = [];
+
+	for (var i in object) (function(value, key){
+		if (base) key = base + '[' + key + ']';
+		var result;
+		if (jasmine.isArray_(value)){
+			var qs = {};
+			for (var j = 0; j < value.length; j++)
+				qs[j] = value[j];
+			result = jasmine.toQueryString(qs, key);
+		} else if (typeof value == 'object'){
+			result = jasmine.toQueryString(value, key);
+		} else {
+			result = key + '=' + encodeURIComponent(value);
+		}
+		
+		if (value != undefined) queryString.push(result);
+	})(object[i], i);
+
+	return queryString.join('&');
+};
+
+// String.parseQueryString from MooTools More
+jasmine.parseQueryString = function(string){
+	var vars = string.split(/[&;]/),
+		object = {};
+
+	if (!vars.length) return object;
+
+	for(var i = 0; i < vars.length; i++) (function(val){
+		var index = val.indexOf('='),
+			keys = index < 0 ? [''] : val.substr(0, index).match(/[^\]\[]+/g),
+			value = decodeURIComponent(val.substr(index + 1));
+
+		for(var j = 0; j < keys.length; j++) (function(key, i){
+			var current = object[key];
+			if(i < keys.length - 1)
+				object = object[key] = current || {};
+			else if(current != null && typeof current.length == 'number')
+				current.push(value);
+			else
+				object[key] = current != null ? [current, value] : value;
+		})(keys[j], j);
+
+	})(vars[i]);
+
+	return object;
+};
+
+
+
+jasmine.TrivialReporter = function(doc, appendTo) {
+  this.document = doc || document;
+  this.suiteDivs = {};
+  this.logRunningSpecs = false;
+  this.appendTo = appendTo || this.document.body;
+};
+
+jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) {
+  var el = document.createElement(type);
+
+  for (var i = 2; i < arguments.length; i++) {
+    var child = arguments[i];
+
+    if (typeof child === 'string') {
+      el.appendChild(document.createTextNode(child));
+    } else {
+      if (child) {el.appendChild(child);}
+    }
+  }
+
+  for (var attr in attrs) {
+    if (attr == "className") {
+      el[attr] = attrs[attr];
+    } else {
+      el.setAttribute(attr, attrs[attr]);
+    }
+  }
+
+  return el;
+};
+
+jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) {
+  var showPassed, showSkipped;
+
+  var query = jasmine.parseQueryString(document.location.search.substr(1));
+  delete query.spec;
+  
+   this.outerDiv = this.createDom('div', {className: 'jasmine_reporter'},
+      this.createDom('div', {className: 'banner'},
+        this.createDom('div', {className: 'logo'},
+            "Jasmine",
+            this.createDom('span', {className: 'version'}, runner.env.versionString())),
+        this.createDom('div', {className: 'options'},
+            "Show ",
+            showPassed = this.createDom('input', {id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox'}),
+            this.createDom('label', {"for": "__jasmine_TrivialReporter_showPassed__"}, " passed "),
+            showSkipped = this.createDom('input', {id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox'}),
+            this.createDom('label', {"for": "__jasmine_TrivialReporter_showSkipped__"}, " skipped")
+            )
+          ),
+
+	  this.runnerDiv = this.createDom('div', {className: 'runner running'},
+          this.createDom('a', {className: 'run_spec', href: '?' + jasmine.toQueryString(query)}, "run all"),
+          this.runnerMessageSpan = this.createDom('span', {}, "Running..."),
+          this.finishedAtSpan = this.createDom('span', {className: 'finished-at'}, ""))
+      );
+
+  this.appendTo.appendChild(this.outerDiv);
+
+  var suites = runner.suites();
+  for (var i = 0; i < suites.length; i++) {
+    var suite = suites[i];
+
+	query.spec = suite.getFullName();
+    var suiteDiv = this.createDom('div', {className: 'suite'},
+        this.createDom('a', {className: 'run_spec', href: '?' + jasmine.toQueryString(query)}, "run"),
+        this.createDom('a', {className: 'description', href: '?' + jasmine.toQueryString(query)}, suite.description));
+    this.suiteDivs[suite.id] = suiteDiv;
+    var parentDiv = this.outerDiv;
+    if (suite.parentSuite) {
+      parentDiv = this.suiteDivs[suite.parentSuite.id];
+    }
+    parentDiv.appendChild(suiteDiv);
+  }
+
+  this.startedAt = new Date();
+
+  var self = this;
+  showPassed.onchange = function(evt) {
+    if (evt.target.checked) {
+      self.outerDiv.className += ' show-passed';
+    } else {
+      self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, '');
+    }
+  };
+
+  showSkipped.onchange = function(evt) {
+    if (evt.target.checked) {
+      self.outerDiv.className += ' show-skipped';
+    } else {
+      self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, '');
+    }
+  };
+
+  runner.env.specFilter = this.specFilter;
+
+};
+
+jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) {
+  var results = runner.results();
+  var className = (results.failedCount > 0) ? "runner failed" : "runner passed";
+  this.runnerDiv.setAttribute("class", className);
+  //do it twice for IE
+  this.runnerDiv.setAttribute("className", className);
+  var specs = runner.specs();
+  var specCount = 0;
+  for (var i = 0; i < specs.length; i++) {
+    if (this.specFilter(specs[i])) {
+      specCount++;
+    }
+  }
+  var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.totalCount + " assertion" + (results.totalCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s");
+  message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s";
+  this.runnerMessageSpan.replaceChild(this.createDom('a', {className: 'description', href: '#'}, message), this.runnerMessageSpan.firstChild);
+
+  this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString()));
+};
+
+jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) {
+  var results = suite.results();
+  var status = results.passed() ? 'passed' : 'failed';
+  if (results.totalCount == 0) { // todo: change this to check results.skipped
+    status = 'skipped';
+  }
+  this.suiteDivs[suite.id].className += " " + status;
+};
+
+jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) {
+  if (this.logRunningSpecs) {
+    this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...');
+  }
+};
+
+jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) {
+  var results = spec.results();
+  var status = results.passed() ? 'passed' : 'failed';
+  if (results.skipped) {
+    status = 'skipped';
+  }
+
+  var query = jasmine.parseQueryString(document.location.search.substr(1));
+  query.spec = spec.getFullName();
+  
+  var specDiv = this.createDom('div', {className: 'spec '  + status},
+      this.createDom('a', {className: 'run_spec', href: '?' + jasmine.toQueryString(query)}, "run"),
+      this.createDom('a', {
+        className: 'description',
+        href: '?' + jasmine.toQueryString(query),
+        title: spec.getFullName()
+      }, spec.description));
+
+
+  var resultItems = results.getItems();
+  var messagesDiv = this.createDom('div', {className: 'messages'});
+  for (var i = 0; i < resultItems.length; i++) {
+    var result = resultItems[i];
+    if (result.type == 'log') {
+      messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString()));
+    } else if (result.type == 'expect' && result.passed && !result.passed()) {
+      messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message));
+
+      var fn = spec.queue && spec.queue.blocks && spec.queue.blocks[1] ? spec.queue.blocks[1].func : null;
+      if (fn){
+        var pre = this.createDom('pre', {className: 'examples-code'});
+
+        pre.appendChild(this.createDom('code', null, fn.toString().replace(/</img, '&lt;').replace(/>/img, '&gt;')));
+        messagesDiv.appendChild(pre);
+      }
+    }
+  }
+
+  if (messagesDiv.childNodes.length > 0) {
+    specDiv.appendChild(messagesDiv);
+  }
+
+  this.suiteDivs[spec.suite.id].appendChild(specDiv);
+};
+
+jasmine.TrivialReporter.prototype.log = function() {
+  var console = jasmine.getGlobal().console;
+  if (console && console.log){
+    if (console.log.apply) console.log.apply(console, arguments);
+    else console.log(Array.prototype.join.call(arguments, ', '));
+  }
+};
+
+jasmine.TrivialReporter.prototype.getLocation = function() {
+  return this.document.location;
+};
+
+(function(){
+
+var query = jasmine.parseQueryString(document.location.search.substr(1));
+
+jasmine.TrivialReporter.prototype.specFilter = function(spec) {
+  if (!query.spec) return true;
+  return spec.getFullName().indexOf(query.spec) == 0;
+};
+
+})();

+ 20 - 0
ext/thirdparty/js/test-runner/mootools-runner/Helpers/simulateEvent.js

@@ -0,0 +1,20 @@
+
+var simulateEvent = function(type, args, callback){
+	
+	var called = false;
+	
+	args = Array.prototype.slice.call(args);
+	args.push(function(){
+		called = true;
+	});
+		
+	Syn[type].apply(Syn, args);
+	
+	waitsFor(2, function(){
+		return called;
+	});
+	
+	runs(callback);
+
+};
+

+ 166 - 0
ext/thirdparty/js/test-runner/mootools-runner/Jasmine/jasmine.css

@@ -0,0 +1,166 @@
+body {
+  font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif;
+}
+
+
+.jasmine_reporter a:visited, .jasmine_reporter a {
+  color: #303; 
+}
+
+.jasmine_reporter a:hover, .jasmine_reporter a:active {
+  color: blue; 
+}
+
+.run_spec {
+  float:right;
+  padding-right: 5px;
+  font-size: .8em;
+  text-decoration: none;
+}
+
+.jasmine_reporter {
+  margin: 0 5px;
+}
+
+.banner {
+  color: #303;
+  background-color: #fef;
+  padding: 5px;
+}
+
+.logo {
+  float: left;
+  font-size: 1.1em;
+  padding-left: 5px;
+}
+
+.logo .version {
+  font-size: .6em;
+  padding-left: 1em;
+}
+
+.runner.running {
+  background-color: yellow;
+}
+
+
+.options {
+  text-align: right;
+  font-size: .8em;
+}
+
+
+
+
+.suite {
+  border: 1px outset gray;
+  margin: 5px 0;
+  padding-left: 1em;
+}
+
+.suite .suite {
+  margin: 5px; 
+}
+
+.suite.passed {
+  background-color: #dfd;
+}
+
+.suite.failed {
+  background-color: #fdd;
+}
+
+.spec {
+  margin: 5px;
+  padding-left: 1em;
+  clear: both;
+}
+
+.spec.failed, .spec.passed, .spec.skipped {
+  padding-bottom: 5px;
+  border: 1px solid gray;
+}
+
+.spec.failed {
+  background-color: #fbb;
+  border-color: red;
+}
+
+.spec.passed {
+  background-color: #bfb;
+  border-color: green;
+}
+
+.spec.skipped {
+  background-color: #bbb;
+}
+
+.messages {
+  border-left: 1px dashed gray;
+  padding-left: 1em;
+  padding-right: 1em;
+}
+
+.passed {
+  background-color: #cfc;
+  display: none;
+}
+
+.failed {
+  background-color: #fbb;
+}
+
+.skipped {
+  color: #777;
+  background-color: #eee;
+  display: none;
+}
+
+
+/*.resultMessage {*/
+  /*white-space: pre;*/
+/*}*/
+
+.resultMessage span.result {
+  display: block;
+  line-height: 2em;
+  color: black;
+}
+
+.resultMessage .mismatch {
+  color: black;
+}
+
+.stackTrace {
+  white-space: pre;
+  font-size: .8em;
+  margin-left: 10px;
+  max-height: 5em;
+  overflow: auto;
+  border: 1px inset red;
+  padding: 1em;
+  background: #eef;
+}
+
+.finished-at {
+  padding-left: 1em;
+  font-size: .6em;
+}
+
+.show-passed .passed,
+.show-skipped .skipped {
+  display: block;
+}
+
+
+#jasmine_content {
+  position:fixed;
+  right: 100%;
+}
+
+.runner {
+  border: 1px solid gray;
+  display: block;
+  margin: 5px 0;
+  padding: 2px 0 2px 10px;
+}

+ 2421 - 0
ext/thirdparty/js/test-runner/mootools-runner/Jasmine/jasmine.js

@@ -0,0 +1,2421 @@
+/**
+ * Top level namespace for Jasmine, a lightweight JavaScript BDD/spec/testing framework.
+ *
+ * @namespace
+ */
+var jasmine = {};
+
+/**
+ * @private
+ */
+jasmine.unimplementedMethod_ = function() {
+  throw new Error("unimplemented method");
+};
+
+/**
+ * Use <code>jasmine.undefined</code> instead of <code>undefined</code>, since <code>undefined</code> is just
+ * a plain old variable and may be redefined by somebody else.
+ *
+ * @private
+ */
+jasmine.undefined = jasmine.___undefined___;
+
+/**
+ * Default interval in milliseconds for event loop yields (e.g. to allow network activity or to refresh the screen with the HTML-based runner). Small values here may result in slow test running. Zero means no updates until all tests have completed.
+ *
+ */
+jasmine.DEFAULT_UPDATE_INTERVAL = 250;
+
+/**
+ * Default timeout interval in milliseconds for waitsFor() blocks.
+ */
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000;
+
+jasmine.getGlobal = function() {
+  function getGlobal() {
+    return this;
+  }
+
+  return getGlobal();
+};
+
+/**
+ * Allows for bound functions to be compared.  Internal use only.
+ *
+ * @ignore
+ * @private
+ * @param base {Object} bound 'this' for the function
+ * @param name {Function} function to find
+ */
+jasmine.bindOriginal_ = function(base, name) {
+  var original = base[name];
+  if (original.apply) {
+    return function() {
+      return original.apply(base, arguments);
+    };
+  } else {
+    // IE support
+    return jasmine.getGlobal()[name];
+  }
+};
+
+jasmine.setTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'setTimeout');
+jasmine.clearTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearTimeout');
+jasmine.setInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'setInterval');
+jasmine.clearInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearInterval');
+
+jasmine.MessageResult = function(values) {
+  this.type = 'log';
+  this.values = values;
+  this.trace = new Error(); // todo: test better
+};
+
+jasmine.MessageResult.prototype.toString = function() {
+  var text = "";
+  for(var i = 0; i < this.values.length; i++) {
+    if (i > 0) text += " ";
+    if (jasmine.isString_(this.values[i])) {
+      text += this.values[i];
+    } else {
+      text += jasmine.pp(this.values[i]);
+    }
+  }
+  return text;
+};
+
+jasmine.ExpectationResult = function(params) {
+  this.type = 'expect';
+  this.matcherName = params.matcherName;
+  this.passed_ = params.passed;
+  this.expected = params.expected;
+  this.actual = params.actual;
+
+  this.message = this.passed_ ? 'Passed.' : params.message;
+  this.trace = this.passed_ ? '' : new Error(this.message);
+};
+
+jasmine.ExpectationResult.prototype.toString = function () {
+  return this.message;
+};
+
+jasmine.ExpectationResult.prototype.passed = function () {
+  return this.passed_;
+};
+
+/**
+ * Getter for the Jasmine environment. Ensures one gets created
+ */
+jasmine.getEnv = function() {
+  return jasmine.currentEnv_ = jasmine.currentEnv_ || new jasmine.Env();
+};
+
+/**
+ * @ignore
+ * @private
+ * @param value
+ * @returns {Boolean}
+ */
+jasmine.isArray_ = function(value) {
+  return jasmine.isA_("Array", value);  
+};
+
+/**
+ * @ignore
+ * @private
+ * @param value
+ * @returns {Boolean}
+ */
+jasmine.isString_ = function(value) {
+  return jasmine.isA_("String", value);
+};
+
+/**
+ * @ignore
+ * @private
+ * @param value
+ * @returns {Boolean}
+ */
+jasmine.isNumber_ = function(value) {
+  return jasmine.isA_("Number", value);
+};
+
+/**
+ * @ignore
+ * @private
+ * @param {String} typeName
+ * @param value
+ * @returns {Boolean}
+ */
+jasmine.isA_ = function(typeName, value) {
+  return Object.prototype.toString.apply(value) === '[object ' + typeName + ']';
+};
+
+/**
+ * Pretty printer for expecations.  Takes any object and turns it into a human-readable string.
+ *
+ * @param value {Object} an object to be outputted
+ * @returns {String}
+ */
+jasmine.pp = function(value) {
+  var stringPrettyPrinter = new jasmine.StringPrettyPrinter();
+  stringPrettyPrinter.format(value);
+  return stringPrettyPrinter.string;
+};
+
+/**
+ * Returns true if the object is a DOM Node.
+ *
+ * @param {Object} obj object to check
+ * @returns {Boolean}
+ */
+jasmine.isDomNode = function(obj) {
+  return obj['nodeType'] > 0;
+};
+
+/**
+ * Returns a matchable 'generic' object of the class type.  For use in expecations of type when values don't matter.
+ *
+ * @example
+ * // don't care about which function is passed in, as long as it's a function
+ * expect(mySpy).toHaveBeenCalledWith(jasmine.any(Function));
+ *
+ * @param {Class} clazz
+ * @returns matchable object of the type clazz
+ */
+jasmine.any = function(clazz) {
+  return new jasmine.Matchers.Any(clazz);
+};
+
+/**
+ * Jasmine Spies are test doubles that can act as stubs, spies, fakes or when used in an expecation, mocks.
+ *
+ * Spies should be created in test setup, before expectations.  They can then be checked, using the standard Jasmine
+ * expectation syntax. Spies can be checked if they were called or not and what the calling params were.
+ *
+ * A Spy has the following fields: wasCalled, callCount, mostRecentCall, and argsForCall (see docs).
+ *
+ * Spies are torn down at the end of every spec.
+ *
+ * Note: Do <b>not</b> call new jasmine.Spy() directly - a spy must be created using spyOn, jasmine.createSpy or jasmine.createSpyObj.
+ *
+ * @example
+ * // a stub
+ * var myStub = jasmine.createSpy('myStub');  // can be used anywhere
+ *
+ * // spy example
+ * var foo = {
+ *   not: function(bool) { return !bool; }
+ * }
+ *
+ * // actual foo.not will not be called, execution stops
+ * spyOn(foo, 'not');
+
+ // foo.not spied upon, execution will continue to implementation
+ * spyOn(foo, 'not').andCallThrough();
+ *
+ * // fake example
+ * var foo = {
+ *   not: function(bool) { return !bool; }
+ * }
+ *
+ * // foo.not(val) will return val
+ * spyOn(foo, 'not').andCallFake(function(value) {return value;});
+ *
+ * // mock example
+ * foo.not(7 == 7);
+ * expect(foo.not).toHaveBeenCalled();
+ * expect(foo.not).toHaveBeenCalledWith(true);
+ *
+ * @constructor
+ * @see spyOn, jasmine.createSpy, jasmine.createSpyObj
+ * @param {String} name
+ */
+jasmine.Spy = function(name) {
+  /**
+   * The name of the spy, if provided.
+   */
+  this.identity = name || 'unknown';
+  /**
+   *  Is this Object a spy?
+   */
+  this.isSpy = true;
+  /**
+   * The actual function this spy stubs.
+   */
+  this.plan = function() {
+  };
+  /**
+   * Tracking of the most recent call to the spy.
+   * @example
+   * var mySpy = jasmine.createSpy('foo');
+   * mySpy(1, 2);
+   * mySpy.mostRecentCall.args = [1, 2];
+   */
+  this.mostRecentCall = {};
+
+  /**
+   * Holds arguments for each call to the spy, indexed by call count
+   * @example
+   * var mySpy = jasmine.createSpy('foo');
+   * mySpy(1, 2);
+   * mySpy(7, 8);
+   * mySpy.mostRecentCall.args = [7, 8];
+   * mySpy.argsForCall[0] = [1, 2];
+   * mySpy.argsForCall[1] = [7, 8];
+   */
+  this.argsForCall = [];
+  this.calls = [];
+};
+
+/**
+ * Tells a spy to call through to the actual implemenatation.
+ *
+ * @example
+ * var foo = {
+ *   bar: function() { // do some stuff }
+ * }
+ *
+ * // defining a spy on an existing property: foo.bar
+ * spyOn(foo, 'bar').andCallThrough();
+ */
+jasmine.Spy.prototype.andCallThrough = function() {
+  this.plan = this.originalValue;
+  return this;
+};
+
+/**
+ * For setting the return value of a spy.
+ *
+ * @example
+ * // defining a spy from scratch: foo() returns 'baz'
+ * var foo = jasmine.createSpy('spy on foo').andReturn('baz');
+ *
+ * // defining a spy on an existing property: foo.bar() returns 'baz'
+ * spyOn(foo, 'bar').andReturn('baz');
+ *
+ * @param {Object} value
+ */
+jasmine.Spy.prototype.andReturn = function(value) {
+  this.plan = function() {
+    return value;
+  };
+  return this;
+};
+
+/**
+ * For throwing an exception when a spy is called.
+ *
+ * @example
+ * // defining a spy from scratch: foo() throws an exception w/ message 'ouch'
+ * var foo = jasmine.createSpy('spy on foo').andThrow('baz');
+ *
+ * // defining a spy on an existing property: foo.bar() throws an exception w/ message 'ouch'
+ * spyOn(foo, 'bar').andThrow('baz');
+ *
+ * @param {String} exceptionMsg
+ */
+jasmine.Spy.prototype.andThrow = function(exceptionMsg) {
+  this.plan = function() {
+    throw exceptionMsg;
+  };
+  return this;
+};
+
+/**
+ * Calls an alternate implementation when a spy is called.
+ *
+ * @example
+ * var baz = function() {
+ *   // do some stuff, return something
+ * }
+ * // defining a spy from scratch: foo() calls the function baz
+ * var foo = jasmine.createSpy('spy on foo').andCall(baz);
+ *
+ * // defining a spy on an existing property: foo.bar() calls an anonymnous function
+ * spyOn(foo, 'bar').andCall(function() { return 'baz';} );
+ *
+ * @param {Function} fakeFunc
+ */
+jasmine.Spy.prototype.andCallFake = function(fakeFunc) {
+  this.plan = fakeFunc;
+  return this;
+};
+
+/**
+ * Resets all of a spy's the tracking variables so that it can be used again.
+ *
+ * @example
+ * spyOn(foo, 'bar');
+ *
+ * foo.bar();
+ *
+ * expect(foo.bar.callCount).toEqual(1);
+ *
+ * foo.bar.reset();
+ *
+ * expect(foo.bar.callCount).toEqual(0);
+ */
+jasmine.Spy.prototype.reset = function() {
+  this.wasCalled = false;
+  this.callCount = 0;
+  this.argsForCall = [];
+  this.calls = [];
+  this.mostRecentCall = {};
+};
+
+jasmine.createSpy = function(name) {
+
+  var spyObj = function() {
+    spyObj.wasCalled = true;
+    spyObj.callCount++;
+    var args = jasmine.util.argsToArray(arguments);
+    spyObj.mostRecentCall.object = this;
+    spyObj.mostRecentCall.args = args;
+    spyObj.argsForCall.push(args);
+    spyObj.calls.push({object: this, args: args});
+    return spyObj.plan.apply(this, arguments);
+  };
+
+  var spy = new jasmine.Spy(name);
+
+  for (var prop in spy) {
+    spyObj[prop] = spy[prop];
+  }
+
+  spyObj.reset();
+
+  return spyObj;
+};
+
+/**
+ * Determines whether an object is a spy.
+ *
+ * @param {jasmine.Spy|Object} putativeSpy
+ * @returns {Boolean}
+ */
+jasmine.isSpy = function(putativeSpy) {
+  return putativeSpy && putativeSpy.isSpy;
+};
+
+/**
+ * Creates a more complicated spy: an Object that has every property a function that is a spy.  Used for stubbing something
+ * large in one call.
+ *
+ * @param {String} baseName name of spy class
+ * @param {Array} methodNames array of names of methods to make spies
+ */
+jasmine.createSpyObj = function(baseName, methodNames) {
+  if (!jasmine.isArray_(methodNames) || methodNames.length == 0) {
+    throw new Error('createSpyObj requires a non-empty array of method names to create spies for');
+  }
+  var obj = {};
+  for (var i = 0; i < methodNames.length; i++) {
+    obj[methodNames[i]] = jasmine.createSpy(baseName + '.' + methodNames[i]);
+  }
+  return obj;
+};
+
+/**
+ * All parameters are pretty-printed and concatenated together, then written to the current spec's output.
+ *
+ * Be careful not to leave calls to <code>jasmine.log</code> in production code.
+ */
+jasmine.log = function() {
+  var spec = jasmine.getEnv().currentSpec;
+  spec.log.apply(spec, arguments);
+};
+
+/**
+ * Function that installs a spy on an existing object's method name.  Used within a Spec to create a spy.
+ *
+ * @example
+ * // spy example
+ * var foo = {
+ *   not: function(bool) { return !bool; }
+ * }
+ * spyOn(foo, 'not'); // actual foo.not will not be called, execution stops
+ *
+ * @see jasmine.createSpy
+ * @param obj
+ * @param methodName
+ * @returns a Jasmine spy that can be chained with all spy methods
+ */
+var spyOn = function(obj, methodName) {
+  return jasmine.getEnv().currentSpec.spyOn(obj, methodName);
+};
+
+/**
+ * Creates a Jasmine spec that will be added to the current suite.
+ *
+ * // TODO: pending tests
+ *
+ * @example
+ * it('should be true', function() {
+ *   expect(true).toEqual(true);
+ * });
+ *
+ * @param {String} desc description of this specification
+ * @param {Function} func defines the preconditions and expectations of the spec
+ */
+var it = function(desc, func) {
+  return jasmine.getEnv().it(desc, func);
+};
+
+/**
+ * Creates a <em>disabled</em> Jasmine spec.
+ *
+ * A convenience method that allows existing specs to be disabled temporarily during development.
+ *
+ * @param {String} desc description of this specification
+ * @param {Function} func defines the preconditions and expectations of the spec
+ */
+var xit = function(desc, func) {
+  return jasmine.getEnv().xit(desc, func);
+};
+
+/**
+ * Starts a chain for a Jasmine expectation.
+ *
+ * It is passed an Object that is the actual value and should chain to one of the many
+ * jasmine.Matchers functions.
+ *
+ * @param {Object} actual Actual value to test against and expected value
+ */
+var expect = function(actual) {
+  return jasmine.getEnv().currentSpec.expect(actual);
+};
+
+/**
+ * Defines part of a jasmine spec.  Used in cominbination with waits or waitsFor in asynchrnous specs.
+ *
+ * @param {Function} func Function that defines part of a jasmine spec.
+ */
+var runs = function(func) {
+  jasmine.getEnv().currentSpec.runs(func);
+};
+
+/**
+ * Waits a fixed time period before moving to the next block.
+ *
+ * @deprecated Use waitsFor() instead
+ * @param {Number} timeout milliseconds to wait
+ */
+var waits = function(timeout) {
+  jasmine.getEnv().currentSpec.waits(timeout);
+};
+
+/**
+ * Waits for the latchFunction to return true before proceeding to the next block.
+ *
+ * @param {Function} latchFunction
+ * @param {String} optional_timeoutMessage
+ * @param {Number} optional_timeout
+ */
+var waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) {
+  jasmine.getEnv().currentSpec.waitsFor.apply(jasmine.getEnv().currentSpec, arguments);
+};
+
+/**
+ * A function that is called before each spec in a suite.
+ *
+ * Used for spec setup, including validating assumptions.
+ *
+ * @param {Function} beforeEachFunction
+ */
+var beforeEach = function(beforeEachFunction) {
+  jasmine.getEnv().beforeEach(beforeEachFunction);
+};
+
+/**
+ * A function that is called after each spec in a suite.
+ *
+ * Used for restoring any state that is hijacked during spec execution.
+ *
+ * @param {Function} afterEachFunction
+ */
+var afterEach = function(afterEachFunction) {
+  jasmine.getEnv().afterEach(afterEachFunction);
+};
+
+/**
+ * Defines a suite of specifications.
+ *
+ * Stores the description and all defined specs in the Jasmine environment as one suite of specs. Variables declared
+ * are accessible by calls to beforeEach, it, and afterEach. Describe blocks can be nested, allowing for specialization
+ * of setup in some tests.
+ *
+ * @example
+ * // TODO: a simple suite
+ *
+ * // TODO: a simple suite with a nested describe block
+ *
+ * @param {String} description A string, usually the class under test.
+ * @param {Function} specDefinitions function that defines several specs.
+ */
+var describe = function(description, specDefinitions) {
+  return jasmine.getEnv().describe(description, specDefinitions);
+};
+
+/**
+ * Disables a suite of specifications.  Used to disable some suites in a file, or files, temporarily during development.
+ *
+ * @param {String} description A string, usually the class under test.
+ * @param {Function} specDefinitions function that defines several specs.
+ */
+var xdescribe = function(description, specDefinitions) {
+  return jasmine.getEnv().xdescribe(description, specDefinitions);
+};
+
+
+// Provide the XMLHttpRequest class for IE 5.x-6.x:
+jasmine.XmlHttpRequest = (typeof XMLHttpRequest == "undefined") ? function() {
+  try {
+    return new ActiveXObject("Msxml2.XMLHTTP.6.0");
+  } catch(e) {
+  }
+  try {
+    return new ActiveXObject("Msxml2.XMLHTTP.3.0");
+  } catch(e) {
+  }
+  try {
+    return new ActiveXObject("Msxml2.XMLHTTP");
+  } catch(e) {
+  }
+  try {
+    return new ActiveXObject("Microsoft.XMLHTTP");
+  } catch(e) {
+  }
+  throw new Error("This browser does not support XMLHttpRequest.");
+} : XMLHttpRequest;
+/**
+ * @namespace
+ */
+jasmine.util = {};
+
+/**
+ * Declare that a child class inherit it's prototype from the parent class.
+ *
+ * @private
+ * @param {Function} childClass
+ * @param {Function} parentClass
+ */
+jasmine.util.inherit = function(childClass, parentClass) {
+  /**
+   * @private
+   */
+  var subclass = function() {
+  };
+  subclass.prototype = parentClass.prototype;
+  childClass.prototype = new subclass;
+};
+
+jasmine.util.formatException = function(e) {
+  var lineNumber;
+  if (e.line) {
+    lineNumber = e.line;
+  }
+  else if (e.lineNumber) {
+    lineNumber = e.lineNumber;
+  }
+
+  var file;
+
+  if (e.sourceURL) {
+    file = e.sourceURL;
+  }
+  else if (e.fileName) {
+    file = e.fileName;
+  }
+
+  var message = (e.name && e.message) ? (e.name + ': ' + e.message) : e.toString();
+
+  if (file && lineNumber) {
+    message += ' in ' + file + ' (line ' + lineNumber + ')';
+  }
+
+  return message;
+};
+
+jasmine.util.htmlEscape = function(str) {
+  if (!str) return str;
+  return str.replace(/&/g, '&amp;')
+    .replace(/</g, '&lt;')
+    .replace(/>/g, '&gt;');
+};
+
+jasmine.util.argsToArray = function(args) {
+  var arrayOfArgs = [];
+  for (var i = 0; i < args.length; i++) arrayOfArgs.push(args[i]);
+  return arrayOfArgs;
+};
+
+jasmine.util.extend = function(destination, source) {
+  for (var property in source) destination[property] = source[property];
+  return destination;
+};
+
+/**
+ * Environment for Jasmine
+ *
+ * @constructor
+ */
+jasmine.Env = function() {
+  this.currentSpec = null;
+  this.currentSuite = null;
+  this.currentRunner_ = new jasmine.Runner(this);
+
+  this.reporter = new jasmine.MultiReporter();
+
+  this.updateInterval = jasmine.DEFAULT_UPDATE_INTERVAL;
+  this.defaultTimeoutInterval = jasmine.DEFAULT_TIMEOUT_INTERVAL;
+  this.lastUpdate = 0;
+  this.specFilter = function() {
+    return true;
+  };
+
+  this.nextSpecId_ = 0;
+  this.nextSuiteId_ = 0;
+  this.equalityTesters_ = [];
+
+  // wrap matchers
+  this.matchersClass = function() {
+    jasmine.Matchers.apply(this, arguments);
+  };
+  jasmine.util.inherit(this.matchersClass, jasmine.Matchers);
+
+  jasmine.Matchers.wrapInto_(jasmine.Matchers.prototype, this.matchersClass);
+};
+
+
+jasmine.Env.prototype.setTimeout = jasmine.setTimeout;
+jasmine.Env.prototype.clearTimeout = jasmine.clearTimeout;
+jasmine.Env.prototype.setInterval = jasmine.setInterval;
+jasmine.Env.prototype.clearInterval = jasmine.clearInterval;
+
+/**
+ * @returns an object containing jasmine version build info, if set.
+ */
+jasmine.Env.prototype.version = function () {
+  if (jasmine.version_) {
+    return jasmine.version_;
+  } else {
+    throw new Error('Version not set');
+  }
+};
+
+/**
+ * @returns string containing jasmine version build info, if set.
+ */
+jasmine.Env.prototype.versionString = function() {
+  if (jasmine.version_) {
+    var version = this.version();
+    return version.major + "." + version.minor + "." + version.build + " revision " + version.revision;
+  } else {
+    return "version unknown";
+  }
+};
+
+/**
+ * @returns a sequential integer starting at 0
+ */
+jasmine.Env.prototype.nextSpecId = function () {
+  return this.nextSpecId_++;
+};
+
+/**
+ * @returns a sequential integer starting at 0
+ */
+jasmine.Env.prototype.nextSuiteId = function () {
+  return this.nextSuiteId_++;
+};
+
+/**
+ * Register a reporter to receive status updates from Jasmine.
+ * @param {jasmine.Reporter} reporter An object which will receive status updates.
+ */
+jasmine.Env.prototype.addReporter = function(reporter) {
+  this.reporter.addReporter(reporter);
+};
+
+jasmine.Env.prototype.execute = function() {
+  this.currentRunner_.execute();
+};
+
+jasmine.Env.prototype.describe = function(description, specDefinitions) {
+  var suite = new jasmine.Suite(this, description, specDefinitions, this.currentSuite);
+
+  var parentSuite = this.currentSuite;
+  if (parentSuite) {
+    parentSuite.add(suite);
+  } else {
+    this.currentRunner_.add(suite);
+  }
+
+  this.currentSuite = suite;
+
+  var declarationError = null;
+  try {
+    specDefinitions.call(suite);
+  } catch(e) {
+    declarationError = e;
+  }
+
+  this.currentSuite = parentSuite;
+
+  if (declarationError) {
+    this.it("encountered a declaration exception", function() {
+      throw declarationError;
+    });
+  }
+
+  return suite;
+};
+
+jasmine.Env.prototype.beforeEach = function(beforeEachFunction) {
+  if (this.currentSuite) {
+    this.currentSuite.beforeEach(beforeEachFunction);
+  } else {
+    this.currentRunner_.beforeEach(beforeEachFunction);
+  }
+};
+
+jasmine.Env.prototype.currentRunner = function () {
+  return this.currentRunner_;
+};
+
+jasmine.Env.prototype.afterEach = function(afterEachFunction) {
+  if (this.currentSuite) {
+    this.currentSuite.afterEach(afterEachFunction);
+  } else {
+    this.currentRunner_.afterEach(afterEachFunction);
+  }
+
+};
+
+jasmine.Env.prototype.xdescribe = function(desc, specDefinitions) {
+  return {
+    execute: function() {
+    }
+  };
+};
+
+jasmine.Env.prototype.it = function(description, func) {
+  var spec = new jasmine.Spec(this, this.currentSuite, description);
+  this.currentSuite.add(spec);
+  this.currentSpec = spec;
+
+  if (func) {
+    spec.runs(func);
+  }
+
+  return spec;
+};
+
+jasmine.Env.prototype.xit = function(desc, func) {
+  return {
+    id: this.nextSpecId(),
+    runs: function() {
+    }
+  };
+};
+
+jasmine.Env.prototype.compareObjects_ = function(a, b, mismatchKeys, mismatchValues) {
+  if (a.__Jasmine_been_here_before__ === b && b.__Jasmine_been_here_before__ === a) {
+    return true;
+  }
+
+  a.__Jasmine_been_here_before__ = b;
+  b.__Jasmine_been_here_before__ = a;
+
+  var hasKey = function(obj, keyName) {
+    return obj != null && obj[keyName] !== jasmine.undefined;
+  };
+
+  for (var property in b) {
+    if (!hasKey(a, property) && hasKey(b, property)) {
+      mismatchKeys.push("expected has key '" + property + "', but missing from actual.");
+    }
+  }
+  for (property in a) {
+    if (!hasKey(b, property) && hasKey(a, property)) {
+      mismatchKeys.push("expected missing key '" + property + "', but present in actual.");
+    }
+  }
+  for (property in b) {
+    if (property == '__Jasmine_been_here_before__') continue;
+    if (!this.equals_(a[property], b[property], mismatchKeys, mismatchValues)) {
+	  mismatchValues.push("'" + property + "' was '" + (b[property] ? jasmine.util.htmlEscape(b[property].toString ? b[property].toString() : '' + b[property]) : b[property]) + "' in expected, but was '" + (a[property] ? jasmine.util.htmlEscape(a[property].toString ? a[property].toString() : '' + a[property]) : a[property]) + "' in actual.");
+    }
+  }
+
+  if (jasmine.isArray_(a) && jasmine.isArray_(b) && a.length != b.length) {
+    mismatchValues.push("arrays were not the same length");
+  }
+
+  delete a.__Jasmine_been_here_before__;
+  delete b.__Jasmine_been_here_before__;
+  return (mismatchKeys.length == 0 && mismatchValues.length == 0);
+};
+
+jasmine.Env.prototype.equals_ = function(a, b, mismatchKeys, mismatchValues) {
+  mismatchKeys = mismatchKeys || [];
+  mismatchValues = mismatchValues || [];
+
+  for (var i = 0; i < this.equalityTesters_.length; i++) {
+    var equalityTester = this.equalityTesters_[i];
+    var result = equalityTester(a, b, this, mismatchKeys, mismatchValues);
+    if (result !== jasmine.undefined) return result;
+  }
+
+  if (a === b) return true;
+
+  if (a === jasmine.undefined || a === null || b === jasmine.undefined || b === null) {
+    return (a == jasmine.undefined && b == jasmine.undefined);
+  }
+
+  if (jasmine.isDomNode(a) && jasmine.isDomNode(b)) {
+    return a === b;
+  }
+
+  if (a instanceof Date && b instanceof Date) {
+    return a.getTime() == b.getTime();
+  }
+
+  if (a instanceof jasmine.Matchers.Any) {
+    return a.matches(b);
+  }
+
+  if (b instanceof jasmine.Matchers.Any) {
+    return b.matches(a);
+  }
+
+  if (jasmine.isString_(a) && jasmine.isString_(b)) {
+    return (a == b);
+  }
+
+  if (jasmine.isNumber_(a) && jasmine.isNumber_(b)) {
+    return (a == b);
+  }
+
+  if (typeof a === "object" && typeof b === "object") {
+    return this.compareObjects_(a, b, mismatchKeys, mismatchValues);
+  }
+
+  //Straight check
+  return (a === b);
+};
+
+jasmine.Env.prototype.contains_ = function(haystack, needle) {
+  if (jasmine.isArray_(haystack)) {
+    for (var i = 0; i < haystack.length; i++) {
+      if (this.equals_(haystack[i], needle)) return true;
+    }
+    return false;
+  }
+  return haystack.indexOf(needle) >= 0;
+};
+
+jasmine.Env.prototype.addEqualityTester = function(equalityTester) {
+  this.equalityTesters_.push(equalityTester);
+};
+/** No-op base class for Jasmine reporters.
+ *
+ * @constructor
+ */
+jasmine.Reporter = function() {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.reportRunnerStarting = function(runner) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.reportRunnerResults = function(runner) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.reportSuiteResults = function(suite) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.reportSpecStarting = function(spec) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.reportSpecResults = function(spec) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.log = function(str) {
+};
+
+/**
+ * Blocks are functions with executable code that make up a spec.
+ *
+ * @constructor
+ * @param {jasmine.Env} env
+ * @param {Function} func
+ * @param {jasmine.Spec} spec
+ */
+jasmine.Block = function(env, func, spec) {
+  this.env = env;
+  this.func = func;
+  this.spec = spec;
+};
+
+jasmine.Block.prototype.execute = function(onComplete) {  
+  try {
+    this.func.apply(this.spec);
+  } catch (e) {
+    this.spec.fail(e);
+  }
+  onComplete();
+};
+/** JavaScript API reporter.
+ *
+ * @constructor
+ */
+jasmine.JsApiReporter = function() {
+  this.started = false;
+  this.finished = false;
+  this.suites_ = [];
+  this.results_ = {};
+};
+
+jasmine.JsApiReporter.prototype.reportRunnerStarting = function(runner) {
+  this.started = true;
+  var suites = runner.topLevelSuites();
+  for (var i = 0; i < suites.length; i++) {
+    var suite = suites[i];
+    this.suites_.push(this.summarize_(suite));
+  }
+};
+
+jasmine.JsApiReporter.prototype.suites = function() {
+  return this.suites_;
+};
+
+jasmine.JsApiReporter.prototype.summarize_ = function(suiteOrSpec) {
+  var isSuite = suiteOrSpec instanceof jasmine.Suite;
+  var summary = {
+    id: suiteOrSpec.id,
+    name: suiteOrSpec.description,
+    type: isSuite ? 'suite' : 'spec',
+    children: []
+  };
+  
+  if (isSuite) {
+    var children = suiteOrSpec.children();
+    for (var i = 0; i < children.length; i++) {
+      summary.children.push(this.summarize_(children[i]));
+    }
+  }
+  return summary;
+};
+
+jasmine.JsApiReporter.prototype.results = function() {
+  return this.results_;
+};
+
+jasmine.JsApiReporter.prototype.resultsForSpec = function(specId) {
+  return this.results_[specId];
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.JsApiReporter.prototype.reportRunnerResults = function(runner) {
+  this.finished = true;
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.JsApiReporter.prototype.reportSuiteResults = function(suite) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.JsApiReporter.prototype.reportSpecResults = function(spec) {
+  this.results_[spec.id] = {
+    messages: spec.results().getItems(),
+    result: spec.results().failedCount > 0 ? "failed" : "passed"
+  };
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.JsApiReporter.prototype.log = function(str) {
+};
+
+jasmine.JsApiReporter.prototype.resultsForSpecs = function(specIds){
+  var results = {};
+  for (var i = 0; i < specIds.length; i++) {
+    var specId = specIds[i];
+    results[specId] = this.summarizeResult_(this.results_[specId]);
+  }
+  return results;
+};
+
+jasmine.JsApiReporter.prototype.summarizeResult_ = function(result){
+  var summaryMessages = [];
+  var messagesLength = result.messages.length;
+  for (var messageIndex = 0; messageIndex < messagesLength; messageIndex++) {
+    var resultMessage = result.messages[messageIndex];
+    summaryMessages.push({
+      text: resultMessage.type == 'log' ? resultMessage.toString() : jasmine.undefined,
+      passed: resultMessage.passed ? resultMessage.passed() : true,
+      type: resultMessage.type,
+      message: resultMessage.message,
+      trace: {
+        stack: resultMessage.passed && !resultMessage.passed() ? resultMessage.trace.stack : jasmine.undefined
+      }
+    });
+  }
+
+  return {
+    result : result.result,
+    messages : summaryMessages
+  };
+};
+
+/**
+ * @constructor
+ * @param {jasmine.Env} env
+ * @param actual
+ * @param {jasmine.Spec} spec
+ */
+jasmine.Matchers = function(env, actual, spec, opt_isNot) {
+  this.env = env;
+  this.actual = actual;
+  this.spec = spec;
+  this.isNot = opt_isNot || false;
+  this.reportWasCalled_ = false;
+};
+
+// todo: @deprecated as of Jasmine 0.11, remove soon [xw]
+jasmine.Matchers.pp = function(str) {
+  throw new Error("jasmine.Matchers.pp() is no longer supported, please use jasmine.pp() instead!");
+};
+
+// todo: @deprecated Deprecated as of Jasmine 0.10. Rewrite your custom matchers to return true or false. [xw]
+jasmine.Matchers.prototype.report = function(result, failing_message, details) {
+  throw new Error("As of jasmine 0.11, custom matchers must be implemented differently -- please see jasmine docs");
+};
+
+jasmine.Matchers.wrapInto_ = function(prototype, matchersClass) {
+  for (var methodName in prototype) {
+    if (methodName == 'report') continue;
+    var orig = prototype[methodName];
+    matchersClass.prototype[methodName] = jasmine.Matchers.matcherFn_(methodName, orig);
+  }
+};
+
+jasmine.Matchers.matcherFn_ = function(matcherName, matcherFunction) {
+  return function() {
+    var matcherArgs = jasmine.util.argsToArray(arguments);
+    var result = matcherFunction.apply(this, arguments);
+
+    if (this.isNot) {
+      result = !result;
+    }
+
+    if (this.reportWasCalled_) return result;
+
+    var message;
+    if (!result) {
+      if (this.message) {
+        message = this.message.apply(this, arguments);
+        if (jasmine.isArray_(message)) {
+          message = message[this.isNot ? 1 : 0];
+        }
+      } else {
+        var englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); });
+        message = "Expected " + jasmine.pp(this.actual) + (this.isNot ? " not " : " ") + englishyPredicate;
+        if (matcherArgs.length > 0) {
+          for (var i = 0; i < matcherArgs.length; i++) {
+            if (i > 0) message += ",";
+            message += " " + jasmine.pp(matcherArgs[i]);
+          }
+        }
+        message += ".";
+      }
+    }
+    var expectationResult = new jasmine.ExpectationResult({
+      matcherName: matcherName,
+      passed: result,
+      expected: matcherArgs.length > 1 ? matcherArgs : matcherArgs[0],
+      actual: this.actual,
+      message: message
+    });
+    this.spec.addMatcherResult(expectationResult);
+    return jasmine.undefined;
+  };
+};
+
+
+
+
+/**
+ * toBe: compares the actual to the expected using ===
+ * @param expected
+ */
+jasmine.Matchers.prototype.toBe = function(expected) {
+  return this.actual === expected;
+};
+
+/**
+ * toNotBe: compares the actual to the expected using !==
+ * @param expected
+ * @deprecated as of 1.0. Use not.toBe() instead.
+ */
+jasmine.Matchers.prototype.toNotBe = function(expected) {
+  return this.actual !== expected;
+};
+
+/**
+ * toEqual: compares the actual to the expected using common sense equality. Handles Objects, Arrays, etc.
+ *
+ * @param expected
+ */
+jasmine.Matchers.prototype.toEqual = function(expected) {
+  return this.env.equals_(this.actual, expected);
+};
+
+/**
+ * toNotEqual: compares the actual to the expected using the ! of jasmine.Matchers.toEqual
+ * @param expected
+ * @deprecated as of 1.0. Use not.toNotEqual() instead.
+ */
+jasmine.Matchers.prototype.toNotEqual = function(expected) {
+  return !this.env.equals_(this.actual, expected);
+};
+
+/**
+ * Matcher that compares the actual to the expected using a regular expression.  Constructs a RegExp, so takes
+ * a pattern or a String.
+ *
+ * @param expected
+ */
+jasmine.Matchers.prototype.toMatch = function(expected) {
+  return new RegExp(expected).test(this.actual);
+};
+
+/**
+ * Matcher that compares the actual to the expected using the boolean inverse of jasmine.Matchers.toMatch
+ * @param expected
+ * @deprecated as of 1.0. Use not.toMatch() instead.
+ */
+jasmine.Matchers.prototype.toNotMatch = function(expected) {
+  return !(new RegExp(expected).test(this.actual));
+};
+
+/**
+ * Matcher that compares the actual to jasmine.undefined.
+ */
+jasmine.Matchers.prototype.toBeDefined = function() {
+  return (this.actual !== jasmine.undefined);
+};
+
+/**
+ * Matcher that compares the actual to jasmine.undefined.
+ */
+jasmine.Matchers.prototype.toBeUndefined = function() {
+  return (this.actual === jasmine.undefined);
+};
+
+/**
+ * Matcher that compares the actual to null.
+ */
+jasmine.Matchers.prototype.toBeNull = function() {
+  return (this.actual === null);
+};
+
+/**
+ * Matcher that boolean not-nots the actual.
+ */
+jasmine.Matchers.prototype.toBeTruthy = function() {
+  return !!this.actual;
+};
+
+
+/**
+ * Matcher that boolean nots the actual.
+ */
+jasmine.Matchers.prototype.toBeFalsy = function() {
+  return !this.actual;
+};
+
+
+/**
+ * Matcher that checks to see if the actual, a Jasmine spy, was called.
+ */
+jasmine.Matchers.prototype.toHaveBeenCalled = function() {
+  if (arguments.length > 0) {
+    throw new Error('toHaveBeenCalled does not take arguments, use toHaveBeenCalledWith');
+  }
+
+  if (!jasmine.isSpy(this.actual)) {
+    throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.');
+  }
+
+  this.message = function() {
+    return [
+      "Expected spy " + this.actual.identity + " to have been called.",
+      "Expected spy " + this.actual.identity + " not to have been called."
+    ];
+  };
+
+  return this.actual.wasCalled;
+};
+
+/** @deprecated Use expect(xxx).toHaveBeenCalled() instead */
+jasmine.Matchers.prototype.wasCalled = jasmine.Matchers.prototype.toHaveBeenCalled;
+
+/**
+ * Matcher that checks to see if the actual, a Jasmine spy, was not called.
+ *
+ * @deprecated Use expect(xxx).not.toHaveBeenCalled() instead
+ */
+jasmine.Matchers.prototype.wasNotCalled = function() {
+  if (arguments.length > 0) {
+    throw new Error('wasNotCalled does not take arguments');
+  }
+
+  if (!jasmine.isSpy(this.actual)) {
+    throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.');
+  }
+
+  this.message = function() {
+    return [
+      "Expected spy " + this.actual.identity + " to not have been called.",
+      "Expected spy " + this.actual.identity + " to have been called."
+    ];
+  };
+
+  return !this.actual.wasCalled;
+};
+
+/**
+ * Matcher that checks to see if the actual, a Jasmine spy, was called with a set of parameters.
+ *
+ * @example
+ *
+ */
+jasmine.Matchers.prototype.toHaveBeenCalledWith = function() {
+  var expectedArgs = jasmine.util.argsToArray(arguments);
+  if (!jasmine.isSpy(this.actual)) {
+    throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.');
+  }
+  this.message = function() {
+    if (this.actual.callCount == 0) {
+      // todo: what should the failure message for .not.toHaveBeenCalledWith() be? is this right? test better. [xw]
+      return [
+        "Expected spy to have been called with " + jasmine.pp(expectedArgs) + " but it was never called.",
+        "Expected spy not to have been called with " + jasmine.pp(expectedArgs) + " but it was."
+      ];
+    } else {
+      return [
+        "Expected spy to have been called with " + jasmine.pp(expectedArgs) + " but was called with " + jasmine.pp(this.actual.argsForCall),
+        "Expected spy not to have been called with " + jasmine.pp(expectedArgs) + " but was called with " + jasmine.pp(this.actual.argsForCall)
+      ];
+    }
+  };
+
+  return this.env.contains_(this.actual.argsForCall, expectedArgs);
+};
+
+/** @deprecated Use expect(xxx).toHaveBeenCalledWith() instead */
+jasmine.Matchers.prototype.wasCalledWith = jasmine.Matchers.prototype.toHaveBeenCalledWith;
+
+/** @deprecated Use expect(xxx).not.toHaveBeenCalledWith() instead */
+jasmine.Matchers.prototype.wasNotCalledWith = function() {
+  var expectedArgs = jasmine.util.argsToArray(arguments);
+  if (!jasmine.isSpy(this.actual)) {
+    throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.');
+  }
+
+  this.message = function() {
+    return [
+      "Expected spy not to have been called with " + jasmine.pp(expectedArgs) + " but it was",
+      "Expected spy to have been called with " + jasmine.pp(expectedArgs) + " but it was"
+    ]
+  };
+
+  return !this.env.contains_(this.actual.argsForCall, expectedArgs);
+};
+
+/**
+ * Matcher that checks that the expected item is an element in the actual Array.
+ *
+ * @param {Object} expected
+ */
+jasmine.Matchers.prototype.toContain = function(expected) {
+  return this.env.contains_(this.actual, expected);
+};
+
+/**
+ * Matcher that checks that the expected item is NOT an element in the actual Array.
+ *
+ * @param {Object} expected
+ * @deprecated as of 1.0. Use not.toNotContain() instead.
+ */
+jasmine.Matchers.prototype.toNotContain = function(expected) {
+  return !this.env.contains_(this.actual, expected);
+};
+
+jasmine.Matchers.prototype.toBeLessThan = function(expected) {
+  return this.actual < expected;
+};
+
+jasmine.Matchers.prototype.toBeGreaterThan = function(expected) {
+  return this.actual > expected;
+};
+
+/**
+ * Matcher that checks that the expected exception was thrown by the actual.
+ *
+ * @param {String} expected
+ */
+jasmine.Matchers.prototype.toThrow = function(expected) {
+  var result = false;
+  var exception;
+  if (typeof this.actual != 'function') {
+    throw new Error('Actual is not a function');
+  }
+  try {
+    this.actual();
+  } catch (e) {
+    exception = e;
+  }
+  if (exception) {
+    result = (expected === jasmine.undefined || this.env.equals_(exception.message || exception, expected.message || expected));
+  }
+
+  var not = this.isNot ? "not " : "";
+
+  this.message = function() {
+    if (exception && (expected === jasmine.undefined || !this.env.equals_(exception.message || exception, expected.message || expected))) {
+      return ["Expected function " + not + "to throw", expected ? expected.message || expected : " an exception", ", but it threw", exception.message || exception].join(' ');
+    } else {
+      return "Expected function to throw an exception.";
+    }
+  };
+
+  return result;
+};
+
+jasmine.Matchers.Any = function(expectedClass) {
+  this.expectedClass = expectedClass;
+};
+
+jasmine.Matchers.Any.prototype.matches = function(other) {
+  if (this.expectedClass == String) {
+    return typeof other == 'string' || other instanceof String;
+  }
+
+  if (this.expectedClass == Number) {
+    return typeof other == 'number' || other instanceof Number;
+  }
+
+  if (this.expectedClass == Function) {
+    return typeof other == 'function' || other instanceof Function;
+  }
+
+  if (this.expectedClass == Object) {
+    return typeof other == 'object';
+  }
+
+  return other instanceof this.expectedClass;
+};
+
+jasmine.Matchers.Any.prototype.toString = function() {
+  return '<jasmine.any(' + this.expectedClass + ')>';
+};
+
+/**
+ * @constructor
+ */
+jasmine.MultiReporter = function() {
+  this.subReporters_ = [];
+};
+jasmine.util.inherit(jasmine.MultiReporter, jasmine.Reporter);
+
+jasmine.MultiReporter.prototype.addReporter = function(reporter) {
+  this.subReporters_.push(reporter);
+};
+
+(function() {
+  var functionNames = [
+    "reportRunnerStarting",
+    "reportRunnerResults",
+    "reportSuiteResults",
+    "reportSpecStarting",
+    "reportSpecResults",
+    "log"
+  ];
+  for (var i = 0; i < functionNames.length; i++) {
+    var functionName = functionNames[i];
+    jasmine.MultiReporter.prototype[functionName] = (function(functionName) {
+      return function() {
+        for (var j = 0; j < this.subReporters_.length; j++) {
+          var subReporter = this.subReporters_[j];
+          if (subReporter[functionName]) {
+            subReporter[functionName].apply(subReporter, arguments);
+          }
+        }
+      };
+    })(functionName);
+  }
+})();
+/**
+ * Holds results for a set of Jasmine spec. Allows for the results array to hold another jasmine.NestedResults
+ *
+ * @constructor
+ */
+jasmine.NestedResults = function() {
+  /**
+   * The total count of results
+   */
+  this.totalCount = 0;
+  /**
+   * Number of passed results
+   */
+  this.passedCount = 0;
+  /**
+   * Number of failed results
+   */
+  this.failedCount = 0;
+  /**
+   * Was this suite/spec skipped?
+   */
+  this.skipped = false;
+  /**
+   * @ignore
+   */
+  this.items_ = [];
+};
+
+/**
+ * Roll up the result counts.
+ *
+ * @param result
+ */
+jasmine.NestedResults.prototype.rollupCounts = function(result) {
+  this.totalCount += result.totalCount;
+  this.passedCount += result.passedCount;
+  this.failedCount += result.failedCount;
+};
+
+/**
+ * Adds a log message.
+ * @param values Array of message parts which will be concatenated later.
+ */
+jasmine.NestedResults.prototype.log = function(values) {
+  this.items_.push(new jasmine.MessageResult(values));
+};
+
+/**
+ * Getter for the results: message & results.
+ */
+jasmine.NestedResults.prototype.getItems = function() {
+  return this.items_;
+};
+
+/**
+ * Adds a result, tracking counts (total, passed, & failed)
+ * @param {jasmine.ExpectationResult|jasmine.NestedResults} result
+ */
+jasmine.NestedResults.prototype.addResult = function(result) {
+  if (result.type != 'log') {
+    if (result.items_) {
+      this.rollupCounts(result);
+    } else {
+      this.totalCount++;
+      if (result.passed()) {
+        this.passedCount++;
+      } else {
+        this.failedCount++;
+      }
+    }
+  }
+  this.items_.push(result);
+};
+
+/**
+ * @returns {Boolean} True if <b>everything</b> below passed
+ */
+jasmine.NestedResults.prototype.passed = function() {
+  return this.passedCount === this.totalCount;
+};
+/**
+ * Base class for pretty printing for expectation results.
+ */
+jasmine.PrettyPrinter = function() {
+  this.ppNestLevel_ = 0;
+};
+
+/**
+ * Formats a value in a nice, human-readable string.
+ *
+ * @param value
+ */
+jasmine.PrettyPrinter.prototype.format = function(value) {
+  if (this.ppNestLevel_ > 40) {
+    throw new Error('jasmine.PrettyPrinter: format() nested too deeply!');
+  }
+
+  this.ppNestLevel_++;
+  try {
+    if (value === jasmine.undefined) {
+      this.emitScalar('undefined');
+    } else if (value === null) {
+      this.emitScalar('null');
+    } else if (value === jasmine.getGlobal()) {
+      this.emitScalar('<global>');
+    } else if (value instanceof jasmine.Matchers.Any) {
+      this.emitScalar(value.toString());
+    } else if (typeof value === 'string') {
+      this.emitString(value);
+    } else if (jasmine.isSpy(value)) {
+      this.emitScalar("spy on " + value.identity);
+    } else if (value instanceof RegExp) {
+      this.emitScalar(value.toString());
+    } else if (typeof value === 'function') {
+      this.emitScalar('Function');
+    } else if (typeof value.nodeType === 'number') {
+      this.emitScalar('HTMLNode');
+    } else if (value instanceof Date) {
+      this.emitScalar('Date(' + value + ')');
+    } else if (value.__Jasmine_been_here_before__) {
+      this.emitScalar('<circular reference: ' + (jasmine.isArray_(value) ? 'Array' : 'Object') + '>');
+    } else if (jasmine.isArray_(value) || typeof value == 'object') {
+      value.__Jasmine_been_here_before__ = true;
+      if (jasmine.isArray_(value)) {
+        this.emitArray(value);
+      } else {
+        this.emitObject(value);
+      }
+      delete value.__Jasmine_been_here_before__;
+    } else {
+      this.emitScalar(value.toString());
+    }
+  } finally {
+    this.ppNestLevel_--;
+  }
+};
+
+jasmine.PrettyPrinter.prototype.iterateObject = function(obj, fn) {
+  for (var property in obj) {
+    if (property == '__Jasmine_been_here_before__') continue;
+    fn(property, obj.__lookupGetter__ ? (obj.__lookupGetter__(property) != null) : false);
+  }
+};
+
+jasmine.PrettyPrinter.prototype.emitArray = jasmine.unimplementedMethod_;
+jasmine.PrettyPrinter.prototype.emitObject = jasmine.unimplementedMethod_;
+jasmine.PrettyPrinter.prototype.emitScalar = jasmine.unimplementedMethod_;
+jasmine.PrettyPrinter.prototype.emitString = jasmine.unimplementedMethod_;
+
+jasmine.StringPrettyPrinter = function() {
+  jasmine.PrettyPrinter.call(this);
+
+  this.string = '';
+};
+jasmine.util.inherit(jasmine.StringPrettyPrinter, jasmine.PrettyPrinter);
+
+jasmine.StringPrettyPrinter.prototype.emitScalar = function(value) {
+  this.append(value);
+};
+
+jasmine.StringPrettyPrinter.prototype.emitString = function(value) {
+  this.append("'" + value + "'");
+};
+
+jasmine.StringPrettyPrinter.prototype.emitArray = function(array) {
+  this.append('[ ');
+  for (var i = 0; i < array.length; i++) {
+    if (i > 0) {
+      this.append(', ');
+    }
+    this.format(array[i]);
+  }
+  this.append(' ]');
+};
+
+jasmine.StringPrettyPrinter.prototype.emitObject = function(obj) {
+  var self = this;
+  this.append('{ ');
+  var first = true;
+
+  this.iterateObject(obj, function(property, isGetter) {
+    if (first) {
+      first = false;
+    } else {
+      self.append(', ');
+    }
+
+    self.append(property);
+    self.append(' : ');
+    if (isGetter) {
+      self.append('<getter>');
+    } else {
+      self.format(obj[property]);
+    }
+  });
+
+  this.append(' }');
+};
+
+jasmine.StringPrettyPrinter.prototype.append = function(value) {
+  this.string += value;
+};
+jasmine.Queue = function(env) {
+  this.env = env;
+  this.blocks = [];
+  this.running = false;
+  this.index = 0;
+  this.offset = 0;
+  this.abort = false;
+};
+
+jasmine.Queue.prototype.addBefore = function(block) {
+  this.blocks.unshift(block);
+};
+
+jasmine.Queue.prototype.add = function(block) {
+  this.blocks.push(block);
+};
+
+jasmine.Queue.prototype.insertNext = function(block) {
+  this.blocks.splice((this.index + this.offset + 1), 0, block);
+  this.offset++;
+};
+
+jasmine.Queue.prototype.start = function(onComplete) {
+  this.running = true;
+  this.onComplete = onComplete;
+  this.next_();
+};
+
+jasmine.Queue.prototype.isRunning = function() {
+  return this.running;
+};
+
+jasmine.Queue.LOOP_DONT_RECURSE = true;
+
+jasmine.Queue.prototype.next_ = function() {
+  var self = this;
+  var goAgain = true;
+
+  while (goAgain) {
+    goAgain = false;
+    
+    if (self.index < self.blocks.length && !this.abort) {
+      var calledSynchronously = true;
+      var completedSynchronously = false;
+
+      var onComplete = function () {
+        if (jasmine.Queue.LOOP_DONT_RECURSE && calledSynchronously) {
+          completedSynchronously = true;
+          return;
+        }
+
+        if (self.blocks[self.index].abort) {
+          self.abort = true;
+        }
+
+        self.offset = 0;
+        self.index++;
+
+        var now = new Date().getTime();
+        if (self.env.updateInterval && now - self.env.lastUpdate > self.env.updateInterval) {
+          self.env.lastUpdate = now;
+          self.env.setTimeout(function() {
+            self.next_();
+          }, 0);
+        } else {
+          if (jasmine.Queue.LOOP_DONT_RECURSE && completedSynchronously) {
+            goAgain = true;
+          } else {
+            self.next_();
+          }
+        }
+      };
+      self.blocks[self.index].execute(onComplete);
+
+      calledSynchronously = false;
+      if (completedSynchronously) {
+        onComplete();
+      }
+      
+    } else {
+      self.running = false;
+      if (self.onComplete) {
+        self.onComplete();
+      }
+    }
+  }
+};
+
+jasmine.Queue.prototype.results = function() {
+  var results = new jasmine.NestedResults();
+  for (var i = 0; i < this.blocks.length; i++) {
+    if (this.blocks[i].results) {
+      results.addResult(this.blocks[i].results());
+    }
+  }
+  return results;
+};
+
+
+/**
+ * Runner
+ *
+ * @constructor
+ * @param {jasmine.Env} env
+ */
+jasmine.Runner = function(env) {
+  var self = this;
+  self.env = env;
+  self.queue = new jasmine.Queue(env);
+  self.before_ = [];
+  self.after_ = [];
+  self.suites_ = [];
+};
+
+jasmine.Runner.prototype.execute = function() {
+  var self = this;
+  if (self.env.reporter.reportRunnerStarting) {
+    self.env.reporter.reportRunnerStarting(this);
+  }
+  self.queue.start(function () {
+    self.finishCallback();
+  });
+};
+
+jasmine.Runner.prototype.beforeEach = function(beforeEachFunction) {
+  beforeEachFunction.typeName = 'beforeEach';
+  this.before_.splice(0,0,beforeEachFunction);
+};
+
+jasmine.Runner.prototype.afterEach = function(afterEachFunction) {
+  afterEachFunction.typeName = 'afterEach';
+  this.after_.splice(0,0,afterEachFunction);
+};
+
+
+jasmine.Runner.prototype.finishCallback = function() {
+  this.env.reporter.reportRunnerResults(this);
+};
+
+jasmine.Runner.prototype.addSuite = function(suite) {
+  this.suites_.push(suite);
+};
+
+jasmine.Runner.prototype.add = function(block) {
+  if (block instanceof jasmine.Suite) {
+    this.addSuite(block);
+  }
+  this.queue.add(block);
+};
+
+jasmine.Runner.prototype.specs = function () {
+  var suites = this.suites();
+  var specs = [];
+  for (var i = 0; i < suites.length; i++) {
+    specs = specs.concat(suites[i].specs());
+  }
+  return specs;
+};
+
+jasmine.Runner.prototype.suites = function() {
+  return this.suites_;
+};
+
+jasmine.Runner.prototype.topLevelSuites = function() {
+  var topLevelSuites = [];
+  for (var i = 0; i < this.suites_.length; i++) {
+    if (!this.suites_[i].parentSuite) {
+      topLevelSuites.push(this.suites_[i]);
+    }
+  }
+  return topLevelSuites;
+};
+
+jasmine.Runner.prototype.results = function() {
+  return this.queue.results();
+};
+/**
+ * Internal representation of a Jasmine specification, or test.
+ *
+ * @constructor
+ * @param {jasmine.Env} env
+ * @param {jasmine.Suite} suite
+ * @param {String} description
+ */
+jasmine.Spec = function(env, suite, description) {
+  if (!env) {
+    throw new Error('jasmine.Env() required');
+  }
+  if (!suite) {
+    throw new Error('jasmine.Suite() required');
+  }
+  var spec = this;
+  spec.id = env.nextSpecId ? env.nextSpecId() : null;
+  spec.env = env;
+  spec.suite = suite;
+  spec.description = description;
+  spec.queue = new jasmine.Queue(env);
+
+  spec.afterCallbacks = [];
+  spec.spies_ = [];
+
+  spec.results_ = new jasmine.NestedResults();
+  spec.results_.description = description;
+  spec.matchersClass = null;
+};
+
+jasmine.Spec.prototype.getFullName = function() {
+  return this.suite.getFullName() + ' ' + this.description + '.';
+};
+
+
+jasmine.Spec.prototype.results = function() {
+  return this.results_;
+};
+
+/**
+ * All parameters are pretty-printed and concatenated together, then written to the spec's output.
+ *
+ * Be careful not to leave calls to <code>jasmine.log</code> in production code.
+ */
+jasmine.Spec.prototype.log = function() {
+  return this.results_.log(arguments);
+};
+
+jasmine.Spec.prototype.runs = function (func) {
+  var block = new jasmine.Block(this.env, func, this);
+  this.addToQueue(block);
+  return this;
+};
+
+jasmine.Spec.prototype.addToQueue = function (block) {
+  if (this.queue.isRunning()) {
+    this.queue.insertNext(block);
+  } else {
+    this.queue.add(block);
+  }
+};
+
+/**
+ * @param {jasmine.ExpectationResult} result
+ */
+jasmine.Spec.prototype.addMatcherResult = function(result) {
+  this.results_.addResult(result);
+};
+
+jasmine.Spec.prototype.expect = function(actual) {
+  var positive = new (this.getMatchersClass_())(this.env, actual, this);
+  positive.not = new (this.getMatchersClass_())(this.env, actual, this, true);
+  return positive;
+};
+
+/**
+ * Waits a fixed time period before moving to the next block.
+ *
+ * @deprecated Use waitsFor() instead
+ * @param {Number} timeout milliseconds to wait
+ */
+jasmine.Spec.prototype.waits = function(timeout) {
+  var waitsFunc = new jasmine.WaitsBlock(this.env, timeout, this);
+  this.addToQueue(waitsFunc);
+  return this;
+};
+
+/**
+ * Waits for the latchFunction to return true before proceeding to the next block.
+ *
+ * @param {Function} latchFunction
+ * @param {String} optional_timeoutMessage
+ * @param {Number} optional_timeout
+ */
+jasmine.Spec.prototype.waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) {
+  var latchFunction_ = null;
+  var optional_timeoutMessage_ = null;
+  var optional_timeout_ = null;
+
+  for (var i = 0; i < arguments.length; i++) {
+    var arg = arguments[i];
+    switch (typeof arg) {
+      case 'function':
+        latchFunction_ = arg;
+        break;
+      case 'string':
+        optional_timeoutMessage_ = arg;
+        break;
+      case 'number':
+        optional_timeout_ = arg;
+        break;
+    }
+  }
+
+  var waitsForFunc = new jasmine.WaitsForBlock(this.env, optional_timeout_, latchFunction_, optional_timeoutMessage_, this);
+  this.addToQueue(waitsForFunc);
+  return this;
+};
+
+jasmine.Spec.prototype.fail = function (e) {
+  var expectationResult = new jasmine.ExpectationResult({
+    passed: false,
+    message: e ? jasmine.util.formatException(e) : 'Exception'
+  });
+  this.results_.addResult(expectationResult);
+};
+
+jasmine.Spec.prototype.getMatchersClass_ = function() {
+  return this.matchersClass || this.env.matchersClass;
+};
+
+jasmine.Spec.prototype.addMatchers = function(matchersPrototype) {
+  var parent = this.getMatchersClass_();
+  var newMatchersClass = function() {
+    parent.apply(this, arguments);
+  };
+  jasmine.util.inherit(newMatchersClass, parent);
+  jasmine.Matchers.wrapInto_(matchersPrototype, newMatchersClass);
+  this.matchersClass = newMatchersClass;
+};
+
+jasmine.Spec.prototype.finishCallback = function() {
+  this.env.reporter.reportSpecResults(this);
+};
+
+jasmine.Spec.prototype.finish = function(onComplete) {
+  this.removeAllSpies();
+  this.finishCallback();
+  if (onComplete) {
+    onComplete();
+  }
+};
+
+jasmine.Spec.prototype.after = function(doAfter) {
+  if (this.queue.isRunning()) {
+    this.queue.add(new jasmine.Block(this.env, doAfter, this));
+  } else {
+    this.afterCallbacks.unshift(doAfter);
+  }
+};
+
+jasmine.Spec.prototype.execute = function(onComplete) {
+  var spec = this;
+  if (!spec.env.specFilter(spec)) {
+    spec.results_.skipped = true;
+    spec.finish(onComplete);
+    return;
+  }
+
+  this.env.reporter.reportSpecStarting(this);
+
+  spec.env.currentSpec = spec;
+
+  spec.addBeforesAndAftersToQueue();
+
+  spec.queue.start(function () {
+    spec.finish(onComplete);
+  });
+};
+
+jasmine.Spec.prototype.addBeforesAndAftersToQueue = function() {
+  var runner = this.env.currentRunner();
+  var i;
+
+  for (var suite = this.suite; suite; suite = suite.parentSuite) {
+    for (i = 0; i < suite.before_.length; i++) {
+      this.queue.addBefore(new jasmine.Block(this.env, suite.before_[i], this));
+    }
+  }
+  for (i = 0; i < runner.before_.length; i++) {
+    this.queue.addBefore(new jasmine.Block(this.env, runner.before_[i], this));
+  }
+  for (i = 0; i < this.afterCallbacks.length; i++) {
+    this.queue.add(new jasmine.Block(this.env, this.afterCallbacks[i], this));
+  }
+  for (suite = this.suite; suite; suite = suite.parentSuite) {
+    for (i = 0; i < suite.after_.length; i++) {
+      this.queue.add(new jasmine.Block(this.env, suite.after_[i], this));
+    }
+  }
+  for (i = 0; i < runner.after_.length; i++) {
+    this.queue.add(new jasmine.Block(this.env, runner.after_[i], this));
+  }
+};
+
+jasmine.Spec.prototype.explodes = function() {
+  throw 'explodes function should not have been called';
+};
+
+jasmine.Spec.prototype.spyOn = function(obj, methodName, ignoreMethodDoesntExist) {
+  if (obj == jasmine.undefined) {
+    throw "spyOn could not find an object to spy upon for " + methodName + "()";
+  }
+
+  if (!ignoreMethodDoesntExist && obj[methodName] === jasmine.undefined) {
+    throw methodName + '() method does not exist';
+  }
+
+  if (!ignoreMethodDoesntExist && obj[methodName] && obj[methodName].isSpy) {
+    throw new Error(methodName + ' has already been spied upon');
+  }
+
+  var spyObj = jasmine.createSpy(methodName);
+
+  this.spies_.push(spyObj);
+  spyObj.baseObj = obj;
+  spyObj.methodName = methodName;
+  spyObj.originalValue = obj[methodName];
+
+  obj[methodName] = spyObj;
+
+  return spyObj;
+};
+
+jasmine.Spec.prototype.removeAllSpies = function() {
+  for (var i = 0; i < this.spies_.length; i++) {
+    var spy = this.spies_[i];
+    spy.baseObj[spy.methodName] = spy.originalValue;
+  }
+  this.spies_ = [];
+};
+
+/**
+ * Internal representation of a Jasmine suite.
+ *
+ * @constructor
+ * @param {jasmine.Env} env
+ * @param {String} description
+ * @param {Function} specDefinitions
+ * @param {jasmine.Suite} parentSuite
+ */
+jasmine.Suite = function(env, description, specDefinitions, parentSuite) {
+  var self = this;
+  self.id = env.nextSuiteId ? env.nextSuiteId() : null;
+  self.description = description;
+  self.queue = new jasmine.Queue(env);
+  self.parentSuite = parentSuite;
+  self.env = env;
+  self.before_ = [];
+  self.after_ = [];
+  self.children_ = [];
+  self.suites_ = [];
+  self.specs_ = [];
+};
+
+jasmine.Suite.prototype.getFullName = function() {
+  var fullName = this.description;
+  for (var parentSuite = this.parentSuite; parentSuite; parentSuite = parentSuite.parentSuite) {
+    fullName = parentSuite.description + ' ' + fullName;
+  }
+  return fullName;
+};
+
+jasmine.Suite.prototype.finish = function(onComplete) {
+  this.env.reporter.reportSuiteResults(this);
+  this.finished = true;
+  if (typeof(onComplete) == 'function') {
+    onComplete();
+  }
+};
+
+jasmine.Suite.prototype.beforeEach = function(beforeEachFunction) {
+  beforeEachFunction.typeName = 'beforeEach';
+  this.before_.unshift(beforeEachFunction);
+};
+
+jasmine.Suite.prototype.afterEach = function(afterEachFunction) {
+  afterEachFunction.typeName = 'afterEach';
+  this.after_.unshift(afterEachFunction);
+};
+
+jasmine.Suite.prototype.results = function() {
+  return this.queue.results();
+};
+
+jasmine.Suite.prototype.add = function(suiteOrSpec) {
+  this.children_.push(suiteOrSpec);
+  if (suiteOrSpec instanceof jasmine.Suite) {
+    this.suites_.push(suiteOrSpec);
+    this.env.currentRunner().addSuite(suiteOrSpec);
+  } else {
+    this.specs_.push(suiteOrSpec);
+  }
+  this.queue.add(suiteOrSpec);
+};
+
+jasmine.Suite.prototype.specs = function() {
+  return this.specs_;
+};
+
+jasmine.Suite.prototype.suites = function() {
+  return this.suites_;
+};
+
+jasmine.Suite.prototype.children = function() {
+  return this.children_;
+};
+
+jasmine.Suite.prototype.execute = function(onComplete) {
+  var self = this;
+  this.queue.start(function () {
+    self.finish(onComplete);
+  });
+};
+jasmine.WaitsBlock = function(env, timeout, spec) {
+  this.timeout = timeout;
+  jasmine.Block.call(this, env, null, spec);
+};
+
+jasmine.util.inherit(jasmine.WaitsBlock, jasmine.Block);
+
+jasmine.WaitsBlock.prototype.execute = function (onComplete) {
+  this.env.reporter.log('>> Jasmine waiting for ' + this.timeout + ' ms...');
+  this.env.setTimeout(function () {
+    onComplete();
+  }, this.timeout);
+};
+/**
+ * A block which waits for some condition to become true, with timeout.
+ *
+ * @constructor
+ * @extends jasmine.Block
+ * @param {jasmine.Env} env The Jasmine environment.
+ * @param {Number} timeout The maximum time in milliseconds to wait for the condition to become true.
+ * @param {Function} latchFunction A function which returns true when the desired condition has been met.
+ * @param {String} message The message to display if the desired condition hasn't been met within the given time period.
+ * @param {jasmine.Spec} spec The Jasmine spec.
+ */
+jasmine.WaitsForBlock = function(env, timeout, latchFunction, message, spec) {
+  this.timeout = timeout || env.defaultTimeoutInterval;
+  this.latchFunction = latchFunction;
+  this.message = message;
+  this.totalTimeSpentWaitingForLatch = 0;
+  jasmine.Block.call(this, env, null, spec);
+};
+jasmine.util.inherit(jasmine.WaitsForBlock, jasmine.Block);
+
+jasmine.WaitsForBlock.TIMEOUT_INCREMENT = 10;
+
+jasmine.WaitsForBlock.prototype.execute = function(onComplete) {
+  this.env.reporter.log('>> Jasmine waiting for ' + (this.message || 'something to happen'));
+  var latchFunctionResult;
+  try {
+    latchFunctionResult = this.latchFunction.apply(this.spec);
+  } catch (e) {
+    this.spec.fail(e);
+    onComplete();
+    return;
+  }
+
+  if (latchFunctionResult) {
+    onComplete();
+  } else if (this.totalTimeSpentWaitingForLatch >= this.timeout) {
+    var message = 'timed out after ' + this.timeout + ' msec waiting for ' + (this.message || 'something to happen');
+    this.spec.fail({
+      name: 'timeout',
+      message: message
+    });
+
+    this.abort = true;
+    onComplete();
+  } else {
+    this.totalTimeSpentWaitingForLatch += jasmine.WaitsForBlock.TIMEOUT_INCREMENT;
+    var self = this;
+    this.env.setTimeout(function() {
+      self.execute(onComplete);
+    }, jasmine.WaitsForBlock.TIMEOUT_INCREMENT);
+  }
+};
+// Mock setTimeout, clearTimeout
+// Contributed by Pivotal Computer Systems, www.pivotalsf.com
+
+jasmine.FakeTimer = function() {
+  this.reset();
+
+  var self = this;
+  self.setTimeout = function(funcToCall, millis) {
+    self.timeoutsMade++;
+    self.scheduleFunction(self.timeoutsMade, funcToCall, millis, false);
+    return self.timeoutsMade;
+  };
+
+  self.setInterval = function(funcToCall, millis) {
+    self.timeoutsMade++;
+    self.scheduleFunction(self.timeoutsMade, funcToCall, millis, true);
+    return self.timeoutsMade;
+  };
+
+  self.clearTimeout = function(timeoutKey) {
+    self.scheduledFunctions[timeoutKey] = jasmine.undefined;
+  };
+
+  self.clearInterval = function(timeoutKey) {
+    self.scheduledFunctions[timeoutKey] = jasmine.undefined;
+  };
+
+};
+
+jasmine.FakeTimer.prototype.reset = function() {
+  this.timeoutsMade = 0;
+  this.scheduledFunctions = {};
+  this.nowMillis = 0;
+};
+
+jasmine.FakeTimer.prototype.tick = function(millis) {
+  var oldMillis = this.nowMillis;
+  var newMillis = oldMillis + millis;
+  this.runFunctionsWithinRange(oldMillis, newMillis);
+  this.nowMillis = newMillis;
+};
+
+jasmine.FakeTimer.prototype.runFunctionsWithinRange = function(oldMillis, nowMillis) {
+  var scheduledFunc;
+  var funcsToRun = [];
+  for (var timeoutKey in this.scheduledFunctions) {
+    scheduledFunc = this.scheduledFunctions[timeoutKey];
+    if (scheduledFunc != jasmine.undefined &&
+        scheduledFunc.runAtMillis >= oldMillis &&
+        scheduledFunc.runAtMillis <= nowMillis) {
+      funcsToRun.push(scheduledFunc);
+      this.scheduledFunctions[timeoutKey] = jasmine.undefined;
+    }
+  }
+
+  if (funcsToRun.length > 0) {
+    funcsToRun.sort(function(a, b) {
+      return a.runAtMillis - b.runAtMillis;
+    });
+    for (var i = 0; i < funcsToRun.length; ++i) {
+      try {
+        var funcToRun = funcsToRun[i];
+        this.nowMillis = funcToRun.runAtMillis;
+        funcToRun.funcToCall();
+        if (funcToRun.recurring) {
+          this.scheduleFunction(funcToRun.timeoutKey,
+              funcToRun.funcToCall,
+              funcToRun.millis,
+              true);
+        }
+      } catch(e) {
+      }
+    }
+    this.runFunctionsWithinRange(oldMillis, nowMillis);
+  }
+};
+
+jasmine.FakeTimer.prototype.scheduleFunction = function(timeoutKey, funcToCall, millis, recurring) {
+  this.scheduledFunctions[timeoutKey] = {
+    runAtMillis: this.nowMillis + millis,
+    funcToCall: funcToCall,
+    recurring: recurring,
+    timeoutKey: timeoutKey,
+    millis: millis
+  };
+};
+
+/**
+ * @namespace
+ */
+jasmine.Clock = {
+  defaultFakeTimer: new jasmine.FakeTimer(),
+
+  reset: function() {
+    jasmine.Clock.assertInstalled();
+    jasmine.Clock.defaultFakeTimer.reset();
+  },
+
+  tick: function(millis) {
+    jasmine.Clock.assertInstalled();
+    jasmine.Clock.defaultFakeTimer.tick(millis);
+  },
+
+  runFunctionsWithinRange: function(oldMillis, nowMillis) {
+    jasmine.Clock.defaultFakeTimer.runFunctionsWithinRange(oldMillis, nowMillis);
+  },
+
+  scheduleFunction: function(timeoutKey, funcToCall, millis, recurring) {
+    jasmine.Clock.defaultFakeTimer.scheduleFunction(timeoutKey, funcToCall, millis, recurring);
+  },
+
+  useMock: function() {
+    if (!jasmine.Clock.isInstalled()) {
+      var spec = jasmine.getEnv().currentSpec;
+      spec.after(jasmine.Clock.uninstallMock);
+
+      jasmine.Clock.installMock();
+    }
+  },
+
+  installMock: function() {
+    jasmine.Clock.installed = jasmine.Clock.defaultFakeTimer;
+  },
+
+  uninstallMock: function() {
+    jasmine.Clock.assertInstalled();
+    jasmine.Clock.installed = jasmine.Clock.real;
+  },
+
+  real: {
+    setTimeout: jasmine.getGlobal().setTimeout,
+    clearTimeout: jasmine.getGlobal().clearTimeout,
+    setInterval: jasmine.getGlobal().setInterval,
+    clearInterval: jasmine.getGlobal().clearInterval
+  },
+
+  assertInstalled: function() {
+    if (!jasmine.Clock.isInstalled()) {
+      throw new Error("Mock clock is not installed, use jasmine.Clock.useMock()");
+    }
+  },
+
+  isInstalled: function() {
+    return jasmine.Clock.installed == jasmine.Clock.defaultFakeTimer;
+  },
+
+  installed: null
+};
+jasmine.Clock.installed = jasmine.Clock.real;
+
+//else for IE support
+jasmine.getGlobal().setTimeout = function(funcToCall, millis) {
+  if (jasmine.Clock.installed.setTimeout.apply) {
+    return jasmine.Clock.installed.setTimeout.apply(this, arguments);
+  } else {
+    return jasmine.Clock.installed.setTimeout(funcToCall, millis);
+  }
+};
+
+jasmine.getGlobal().setInterval = function(funcToCall, millis) {
+  if (jasmine.Clock.installed.setInterval.apply) {
+    return jasmine.Clock.installed.setInterval.apply(this, arguments);
+  } else {
+    return jasmine.Clock.installed.setInterval(funcToCall, millis);
+  }
+};
+
+jasmine.getGlobal().clearTimeout = function(timeoutKey) {
+  if (jasmine.Clock.installed.clearTimeout.apply) {
+    return jasmine.Clock.installed.clearTimeout.apply(this, arguments);
+  } else {
+    return jasmine.Clock.installed.clearTimeout(timeoutKey);
+  }
+};
+
+jasmine.getGlobal().clearInterval = function(timeoutKey) {
+  if (jasmine.Clock.installed.clearTimeout.apply) {
+    return jasmine.Clock.installed.clearInterval.apply(this, arguments);
+  } else {
+    return jasmine.Clock.installed.clearInterval(timeoutKey);
+  }
+};
+
+
+jasmine.version_= {
+  "major": 1,
+  "minor": 0,
+  "build": "0.rc1",
+  "revision": 1282853377
+};

+ 47 - 0
ext/thirdparty/js/test-runner/mootools-runner/README.md

@@ -0,0 +1,47 @@
+MooTools Core Specs
+===================
+
+This repository is intended to provide the specification infrastructure for MooTools Core.
+
+The infrastructure uses Jasmine as a UnitTest-Library. It is possible to run Specs via
+the browser, via JSTestDriver and via NodeJS.
+
+### Setup
+
+Clone the MooTools Core repository and initialize the submodules.
+
+Set up the Specs:
+
+	cd Specs
+	git pull origin master
+	git submodule update --init
+	chmod +x server test runner runner.js buildCommonJS buildJSTDConfiguration
+
+### Requirements
+
+* NodeJS
+* PHP (for Packager)
+
+### Usage
+
+* Open index.html in your favorite browser and press the right link
+* Run the JSTD Server
+	* Start via ./server {options}
+	* Point one or more browsers to http://localhost:9876
+	* Run all tests via ./test
+* Run in NodeJS via ./runner.js {options}
+
+### Available Options
+
+Options are specified via JSON.
+
+Example for JSTD
+	./server '{"version": 1.3, "path": "../core/", "specs": [1.2, "1.3base", "1.3client"]}'
+
+Example for NodeJS
+	./runner.js '{"sets": ["1.3base"], "source": "../core/"}'
+
+Options
+	"sets" - The specs to run, see Configuration.js
+	"source" - The source files package, see Configuration.js
+	"preset" - You can simply use a preset with predefined sets and source, see Configuration.js

+ 35 - 0
ext/thirdparty/js/test-runner/mootools-runner/buildJSTDConfiguration.js

@@ -0,0 +1,35 @@
+var options = require('./Helpers/RunnerOptions').parseOptions(process.argv[2]);
+if (!options) return;
+
+// Initialize
+var loader = require('./Helpers/Loader');
+var SpecLoader = loader.SpecLoader(require('../Configuration').Configuration, options);
+
+SpecLoader.setEnvName('jstd');
+
+var data = 'server: http://localhost:9876\n\n';
+data += 'load:\n';
+
+var load = function(object, base){
+	for (var j = 0; j < object.length; j++)
+		data += '  - "../' + (base || '') + object[j] + '.js"\n';
+};
+
+load([
+	'Runner/Jasmine/jasmine',
+	'Runner/JSTD-Adapter/src/JasmineAdapter',
+	'Runner/Helpers/Syn',
+	'Runner/Helpers/JSSpecToJasmine'
+]);
+
+SpecLoader.setSourceLoader(load).setSpecLoader(load).run();
+
+// TODO check why JSTD Coverage fails
+if (options.coverage){
+	data += 'plugin:\n';
+	data += '  - name: "coverage"\n';
+	data += '    jar: "JSTestDriver/plugins/coverage.jar"\n';
+}
+
+var fs = require('fs');
+fs.writeFile('./jsTestDriver.conf', data);

+ 26 - 0
ext/thirdparty/js/test-runner/mootools-runner/index.html

@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<title>Spec Runner</title>
+	<link rel="stylesheet" href="runner.css" type="text/css" media="screen" title="no title" charset="utf-8" />
+	
+	<script type="text/javascript" src="../Configuration.js"></script>
+</head>
+<body>
+<div class="container">
+
+<h1><strong>Specs</strong> for <script>document.write(Configuration.name);</script></h1>
+
+<ul id="prebuilt">
+	<script>
+	(function(){
+		for (name in Configuration.presets){
+			document.write('<li><a href="runner.html?preset=' + name + '">&rarr; ' + name + ': [' + Configuration.presets[name].sets.join(', ') + '] Specs</a></li>');
+		}
+	})();
+	</script>
+</ul>
+
+</div>
+</body>
+</html>

+ 1 - 0
ext/thirdparty/js/test-runner/mootools-runner/jsTestDriverServer.conf

@@ -0,0 +1 @@
+server: http://localhost:9876

+ 1 - 0
ext/thirdparty/js/test-runner/mootools-runner/reports/README

@@ -0,0 +1 @@
+Directory for junit reports.

+ 174 - 0
ext/thirdparty/js/test-runner/mootools-runner/runner.css

@@ -0,0 +1,174 @@
+/* reset.css */
+html, body, div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, code, del, dfn, em, img, q, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td {margin:0;padding:0;border:0;font-weight:inherit;font-style:inherit;font-size:100%;font-family:inherit;vertical-align:baseline;}
+body {line-height:1.5;}
+table {border-collapse:separate;border-spacing:0;}
+caption, th, td {text-align:left;font-weight:normal;}
+table, td, th {vertical-align:middle;}
+blockquote:before, blockquote:after, q:before, q:after {content:"";}
+blockquote, q {quotes:"" "";}
+a img {border:none;}
+
+/* typography.css */
+html {font-size:100.01%;}
+body {font-size:80%;color:#222;background:#fff;font-family:"Helvetica Neue", Arial, Helvetica, sans-serif;}
+h1, h2, h3, h4, h5, h6 {font-weight:normal;color:#111;}
+h1 {font-size:3em;line-height:1;margin-bottom:0.5em;}
+h2 {font-size:2em;margin-bottom:0.75em;}
+h3 {font-size:1.5em;line-height:1;margin-bottom:1em;}
+h4 {font-size:1.2em;line-height:1.25;margin-bottom:1.25em;}
+h5 {font-size:1em;font-weight:bold;margin-bottom:1.5em;}
+h6 {font-size:1em;font-weight:bold;}
+h1 img, h2 img, h3 img, h4 img, h5 img, h6 img {margin:0;}
+p {margin:0 0 1.5em;}
+p img.left {float:left;margin:1.5em 1.5em 1.5em 0;padding:0;}
+p img.right {float:right;margin:1.5em 0 1.5em 1.5em;}
+a:focus, a:hover {color:#000;}
+a {color:#009;text-decoration:underline;}
+blockquote {margin:1.5em;color:#666;font-style:italic;}
+strong {font-weight:bold;}
+em, dfn {font-style:italic;}
+dfn {font-weight:bold;}
+sup, sub {line-height:0;}
+abbr, acronym {border-bottom:1px dotted #666;}
+address {margin:0 0 1.5em;font-style:italic;}
+del {color:#666;}
+pre {margin:1.5em 0;white-space:pre;}
+pre, code, tt {font:1em 'andale mono', 'lucida console', monospace;line-height:1.5;}
+li ul, li ol {margin:0;}
+ul, ol {margin:0 1.5em 1.5em 0;padding-left:3.333em;}
+ul {list-style-type:disc;}
+ol {list-style-type:decimal;}
+
+/* UI */
+body {
+	overflow-y: scroll;
+}
+
+#framework_name {
+	position: fixed;
+	bottom: 0;
+	right: 0;
+	z-index: 1000;
+	font-size: 10px;
+	padding:0.5em 0.75em;
+	background-color: #fff;
+	background-color: rgba(255,255,255,0.9);
+
+	-webkit-border-top-left-radius: 6px;
+}
+* html #framework_name {
+	position: absolute;
+	bottom: auto;
+	top: expression(eval(document.documentElement.scrollTop+document.documentElement.clientHeight-this.clientHeight));
+}
+
+/* Suites */
+.suite.passed, .runner.passed {
+	background-color: #6C6;
+}
+
+.suite.failed, .runner.failed {
+	background-color: #C30;
+}
+
+.spec.failed, .spec.passed, .spec.skipped {
+	padding-bottom: 0;
+}
+
+.spec.failed {
+	background-color: #FFC;
+}
+
+.spec.passed {
+	background-color: #CFC;
+}
+
+.suite a {
+	text-decoration: none;
+}
+
+.suite a.description {
+	color: #fff;
+}
+
+.runner a.description {
+	color: #fff;
+	font-weight: bold;
+}
+
+.spec.passed a.description {
+	color: #363;
+}
+
+.spec.failed a.description {
+	color: #C30;
+}
+
+/* CODE */
+.messages {
+	border: 0;
+}
+
+pre.examples-code {
+	background: #FFFFFF;
+	border: solid 1px #CCC;
+
+	margin: 0.5em 0;
+}
+pre.examples-code code {
+	display:block;
+
+	padding: 0.5em;
+	_padding-right: 0;
+	_padding-left: 0;
+
+	white-space: pre;
+	_width:100%;
+
+	font-size: 10px;
+	font-family: "Panic Sans", "Monaco", monospace !important;
+	overflow-x: auto;
+
+	scrollbar-base-color:       #FFF;
+	scrollbar-arrow-color:      #FFF;
+	scrollbar-track-color:      #FFF;
+	scrollbar-3dlight-color:    #eee;
+	scrollbar-highlight-color:  #eee;
+	scrollbar-face-color:       #eee;
+	scrollbar-shadow-color:     #eee;
+	scrollbar-darkshadow-color: #FFF;
+}
+
+
+/* INDEX PAGE */
+.container {
+	padding:3em;
+	width: auto;
+}
+
+#select {
+	text-align:right;
+}
+#select fieldset {
+	text-align:left;
+}
+#select label {
+	text-align:right;
+}
+#select div input {
+	line-height: 2em;
+}
+
+
+ul#prebuilt {
+	padding-left:0;
+	list-style:none;
+}
+ul#prebuilt li a {
+	font-size: 1.5em;
+	text-decoration: none;
+	padding: 0.5em;
+}
+ul#prebuilt li a:hover {
+	background-color: #eee;
+}

+ 60 - 0
ext/thirdparty/js/test-runner/mootools-runner/runner.html

@@ -0,0 +1,60 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ko">
+<head>
+<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
+
+<!-- Jasmine -->
+
+<link rel="stylesheet" type="text/css" href="Jasmine/jasmine.css">
+<link rel="stylesheet" type="text/css" href="runner.css">
+
+<script type="text/javascript" src="Jasmine/jasmine.js"></script>
+
+<!-- Specs -->
+<script type="text/javascript" src="Helpers/jasmine-html.js"></script>
+<script type="text/javascript" charset="utf-8" src="Helpers/Syn.js"></script>
+<script type="text/javascript" charset="utf-8" src="Helpers/simulateEvent.js"></script>
+<script type="text/javascript" charset="utf-8" src="Helpers/JSSpecToJasmine.js"></script>
+<script type="text/javascript" charset="utf-8" src="Helpers/Loader.js"></script>
+<script type="text/javascript" charset="utf-8" src="../Configuration.js"></script>
+
+<script type="text/javascript" charset="utf-8">
+
+var SetNames = [],
+	SourceNames = [];
+
+(function(){
+
+// Load all the specs
+var loader = SpecLoader(Configuration, jasmine.parseQueryString(document.location.search.substr(1)));
+var load = function(object, base){
+	for (var j = 0; j < object.length; j++)
+		document.write('<scr'+'ipt src="../' + (base || '') + object[j] + '.js" type="text/javascript"><\/script>');
+};
+
+loader.setSourceLoader(load).setSpecLoader(load);
+loader.run();
+
+SetNames = loader.getSetNames();
+SourceNames = loader.getSourceNames();
+
+window.onload = function(){
+	// Run the specs
+	jasmine.getEnv().addReporter(new jasmine.TrivialReporter());
+	jasmine.getEnv().execute();
+	
+	// Change title
+	document.title = 'Specs for ' + Configuration.name;
+};
+
+})();
+</script>
+
+<title>MooTools-Runner</title>
+</head>
+<body>
+	<div id="framework_name"><script type="text/javascript">
+		document.write(Configuration.name + ' ' + SourceNames.join(', ') + ' [Sets: ' + SetNames.join(', ') + ']');
+	</script></div>
+</body>
+</html>

+ 67 - 0
ext/thirdparty/js/test-runner/mootools-runner/runner.js

@@ -0,0 +1,67 @@
+#!/usr/bin/env node
+// Runs Specs in NodeJS
+
+var puts = require('sys').puts;
+
+
+var options = require('./Helpers/RunnerOptions').parseOptions(process.argv[2]);
+if (!options) return;
+
+// Initialize
+var loader = require('./Helpers/Loader');
+var SpecLoader = loader.SpecLoader(require('../Configuration').Configuration, options);
+
+SpecLoader.setEnvName('nodejs');
+
+// set method to require all the sourcefiles and append the objects to this object
+var self = this;
+var append = function(original){
+	for (var i = 1, l = arguments.length; i < l; i++){
+		var extended = arguments[i] || {};
+		for (var key in extended) original[key] = extended[key];
+	}
+	return original;
+};
+
+SpecLoader.setSourceLoader(function(object, base){
+	for (var j = 0; j < object.length; j++){
+		if (object[j] == 'Slick/Slick.Parser'){
+			Slick = require('../' + (base || '') + object[j]).Slick;
+			append(self, Slick);
+		} else {
+			append(self, require('../' + (base || '') + object[j]));
+		}
+		
+	}
+});
+
+
+// Set method to get all the spec files
+var specs = [];
+SpecLoader.setSpecLoader(function(object, base){
+	for (var j = 0; j < object.length; j++)
+		specs.push(__dirname + '/../' + (base || '') + object[j]);
+});
+
+
+// Run loader
+SpecLoader.run();
+
+
+// Fire jasmine
+require.paths.push('./Jasmine-Node/lib');
+
+var jasmine = require('jasmine'),
+	sys = require('sys');
+
+for(var key in jasmine)
+  global[key] = jasmine[key];
+
+require('./Helpers/JSSpecToJasmine');
+
+var reporter = require('reporters/' + (options.reporter || 'console')).Reporter;
+reporter.done = function(runner, log){
+  process.exit(runner.results().failedCount);
+};
+
+jasmine.runSpecs(specs, reporter);

+ 1 - 0
ext/thirdparty/js/test-runner/mootools-runner/server

@@ -0,0 +1 @@
+java -jar ./JSTestDriver/JSTestDriver.jar --config jsTestDriverServer.conf --port 9876 --runnerMode DEBUG

+ 2 - 0
ext/thirdparty/js/test-runner/mootools-runner/test

@@ -0,0 +1,2 @@
+node buildJSTDConfiguration.js "$1"
+java -jar ./JSTestDriver/JSTestDriver.jar --config jsTestDriver.conf --tests all --reset