/**
* This is an attempt at writing a minimal, event-based test-driver
* This script runs tests from an array passed to the constructor.
* Any associated tasks like keeping track of results is delegated to event handlers
*
* Sample usage:
* 
* var testrunner = new TestsuiteRunner(  ['test2.htm', 'test2.htm'], {testinterval: 8000}  );
* testrunner.addEventListener('OnTestResult', function(e){  reportResult( e.currentTestURL, e.currentTestResult );  });
* testrunner.addEventListener('OnTestTimeout', function(e){  reportResult( e.currentTestURL, 'TEST IS BROKEN' );  });
* testrunner.addEventListener('AfterTestRun', function(e){ submitResults();  });
* testrunner.startTestRun();
*/

/* start of new improved runner */
function TestsuiteRunner( testCases, prefs ){
	/* some default prefs.. */
	prefs.ignoretests=prefs.ignoretests||0; /* start at test # */
	prefs.testinterval=prefs.testinterval||10001; /* a default timeout value */
	prefs.debug=1; /* guess.. */
	var currentTestCaseIndex=-1, This=this, timeout=null;
	this.resultsArray=[]; /* made available to OnTestResult event listeners, 
						framework tries to avoid making assumptions on what sort of test data it will be filled with */
	this.startTime=0; this.lastTestStartTime=0;
	this.startTestRun=function(){
		currentTestCaseIndex = prefs.ignoretests;
		handleEvents( 'BeforeTestRun');
		this.startTime=(new Date()).getTime();
		this.nextTest();
	}
	this.endTestRun=function(){
		handleEvents( 'AfterTestRun' );
	}
	
	this.nextTest=function(){
		currentTestCaseIndex++; 
		top.document.title='test '+currentTestCaseIndex+' of '+testCases.length;
		if( currentTestCaseIndex>=testCases.length ){
			this.endTestRun();return;
		}
		var url=testCases[currentTestCaseIndex];
		this.replaceIframe(); 
		url=handleEvents( 'BeforeTest', {nextTestURL: testCases[currentTestCaseIndex]} ).nextTestURL||url; /* the BeforeTest event handler(s) may modify the URL  */
		this.lastTestStartTime=(new Date()).getTime();
		this.testIframe.setAttribute( 'src', url );
		this.timeoutHandler(); /* set a timeout to launch next test if stuck */
	}
	

	var events={
		'BeforeTestRun':[],
		'BeforeTest':[], 
		'OnTestLoad':[],
		'OnTestTimeout':[], /* typically a stalled test  */
		'OnTestResult':[],
		'AfterTestRun':[]
	}; // All supported events
	var defaultActions={
		'OnTestResult' : This.nextTest, 'OnTestTimeout' : This.nextTest
	};
	this.addEventListener=function( type, eventHandler, foo ){ // registers event listener
		if( ! events[type] ) throw 'Unknown event type: '+type;
		events[type].push( eventHandler );
	}
	
	var handleEvents=function( type, eventObjectExtra ){ // run event listener(s) and return results
		if( ! events[type] ) throw 'Unknown event type: '+type;
		/* Event object may have the following properties, depending on event: 
			.currentTestCaseIndex, .currentTestURL, .timeElapsed (time since test loading was initiated. OnTestLoad and OnTestResult only)
			.currentTestResult (OnTestResult only)
			.nextTestURL (BeforeTest only. )
		*/
		try{var url=testCases[currentTestCaseIndex];}catch(e){var url='';} /* the test might change the URL of the iframe, so we read URL from array */
		var eventObject = new TestEventObject( currentTestCaseIndex,  url, eventObjectExtra);
		if( type=='OnTestLoad' || type=='OnTestResult' ){ eventObject.timeElapsed=(new Date()).getTime()-this.lastTestStartTime; }
		/* calling the event listener(s) */ 
		for(var i=events[type].length-1;i>=0; i--){ /* event listeners are run in "most recently added first" order  */
			/* In event handlers, the this object is the test runner */
			events[type][i].call( This, eventObject ); 
		}
		if( defaultActions[type] && ! eventObject._defaultPrevented ){   setTimeout( function(){ defaultActions[type].call(This); }, 10); };
		return eventObject; /* some events have meaningful return values. They can modify event object properties */
	}	

	if(document.addEventListener){
		document.addEventListener('load',  function (e){ if(e.target==This.testIframe){ handleEvents( 'OnTestLoad' ); } }, true);
	}else if(document.attachEvent){
		document.attachEvent('onload',  function (e){e=e||window.event; if(e.srcElement==This.testIframe){ handleEvents( 'OnTestLoad' ); } });
	}
	
	/* a handful of utility functions */
	this.timeoutHandler=function(){
		this.clearTimeOut();
		timeout=setTimeout( function(){ handleEvents('OnTestTimeout'); }, prefs.testinterval );
	}
	this.clearTimeOut=function(){
		if(timeout)clearTimeout(timeout);
		timeout=null;
	}
	this.replaceIframe=function(){
		if( This.testIframe ){/* sometimes testing gets stuck when browser ignores .src update. 
			We replace the entire IFRAME  every time to avoid having to detect this state */
			This.testIframe.parentNode.removeChild( This.testIframe );
			This.testIframe=document.body.appendChild(document.createElement('iframe'));
		}else if(document.getElementsByTagName('iframe')[0]){
			This.testIframe=document.getElementsByTagName('iframe')[0];
		}else{
			This.testIframe=document.body.appendChild(document.createElement('iframe'));
		}
		//may have to spin until iframe is ready. enable with care..
		//while( ! This.testIframe.contentWindow ){ var tmp=new Date().getTime(); }
	}
	this.appendResultElement=function(n, v){
		var input = top.document.createElement('input'); 
		input.type='hidden';
		input.name = n ; // test URL and/or description
		input.value = v;
		try{top.document.forms[0].appendChild(input);}catch(e){}
	}
	
	/* ..and some integration helper functions.. */ 
	window.opener = window.opener||self; // tests that expect to be run in a popup might call top.opener to report results
	/* back compat with various ways to report the result */
	window.rr=function(  result, message  ){ handleEvents( 'OnTestResult', {currentTestResult: result, message: message, resultReportMode: 'boolean'} ); }

	// the developers' test suite calls this function. Params: passed, failed, failed as expected, object listing all tests in file
	function _autorun_updateTestResult(a,b,c,_a){
		if( _a) { // report all results in _a object
			handleEvents( 'OnTestResult', { currentTestResult: _a, resultReportMode:'complex' } );
		}else{ // report just pass/fail statistics
			handleEvents( 'OnTestResult', { currentTestResult: { passed: a, failed: b+c, resultReportMode:'numbers' } } );
		}
	}
	this.toString=function(){ return '[Test suite runner object, '+testCases.length+' tests]'; }
	
	var TestEventObject=function( index, url, eventObjectExtra ){ 
		this._defaultPrevented=false; 
		this.currentTestCaseIndex=index; 
		this.currentTestURL=url; 
		for(var prop in eventObjectExtra){ this[prop]=eventObjectExtra[prop]; }
	};
	TestEventObject.prototype.preventDefault=function(){ this._defaultPrevented=true; }
}


/* end of new improved runner */
