Skip to content

Commit

Permalink
Improved test runner modularization
Browse files Browse the repository at this point in the history
  • Loading branch information
snaterlicious committed Jan 24, 2014
1 parent e732493 commit 98d1970
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 126 deletions.
156 changes: 156 additions & 0 deletions lib/tests/tests.TestRunner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/**
* Test runner for QUnit tests powered by requireJS.
* The test runner may be instantiated and started on a plain HTML page. After starting the test
* runner, an iframe for every test module is created and attached to the DOM one after another as
* soon as the tests of the currently run test module have finished. The iframe loads the HTML
* testRunner specified in the config parameters. This concept ensures loading only the dependencies
* specified for the particular tests.
*
* @option {string[]} queue Set of test modules to run as defined via requireJS.
* @option {string} testRunner The path to the testRunner HTML file that executes QUnit tests.
*
* Example:
* testRunner = new tests.TestRunner( {
* queue: tests.modules,
* testRunner: './testRunner.html'
* } );
* testRunner.start();
*
* @licence GNU GPL v2+
* @author H. Snater < [email protected] >
*/

/* global tests, console */
this.tests = this.tests || {};

tests.TestRunner = ( function( console ) {
'use strict';

/**
* Test runner for QUnit tests powered by requrieJS.
*
* @param {Object} options
* @constructor
*
* @throws {Error} if a required configuration option is not set
*/
var TestRunner = function( options ) {
if( !options.queue ) {
throw new Error( 'No tests specified' );
} else if( !options.testRunner ) {
throw new Error( 'No HTML test runner specified' );
}

this._options = options;
this._interval = null;
this._iFrames = [];
};

/**
* Starts running a set of test modules.
*/
TestRunner.prototype.start = function() {
var self = this,
currentModule,
globalFailures = 0,
queue = this._options.queue;

console.log( 'TEST START' );

// Interval polling the most recently add iframe whether its test(s) have been finished:
this._interval = setInterval( function() {
if( self._iFrames.length === 0 ) {
currentModule = queue.pop();
self._generateFrame( currentModule );
}

var currentFrame = self._iFrames[self._iFrames.length - 1],
frameWindow = currentFrame.contentWindow,
frameBody = currentFrame.contentDocument.body;

if( !frameBody || !currentFrame.contentWindow.require ) {
return;
}

var testResult = frameWindow.document.getElementById( 'qunit-testresult' );

if( !testResult ) {
return;
}

var failed = testResult.getElementsByClassName( 'failed' );

if( failed.length > 0 ) {
// Test hast finished.
var localFailures = parseInt( failed[0].firstChild.nodeValue, 10 );

if( localFailures === 0 ) {
console.log( currentModule + ': passed' );
} else {
console.error( currentModule + ': FAILED (' + localFailures + ' failures)' );
globalFailures += localFailures;
}

if( queue.length === 0 ) {
clearInterval( self._interval );
console.log( 'TEST END (' + globalFailures + ' failure(s))' );
} else {
currentModule = queue.pop();
self._generateFrame( currentModule );
}
}
}, 100 );

};

/**
* Creates an iframe to run a test module in.
*
* @param {string} module
*/
TestRunner.prototype._generateFrame = function( module ) {
var iFrame = document.createElement( 'iframe' );

document.body.appendChild( iFrame );

iFrame.setAttribute( 'src', this._options.testRunner + '?test=' + module );
iFrame.setAttribute( 'id', 'testFrame-' + module.replace( /\./g, '-' ) );
iFrame.setAttribute( 'style', 'width: 100%' );

this._iFrames.push( iFrame );
};

/**
* Extracts the module to test from the URI. Returns an array with the module name as single
* value. If the module is not within the list of modules passed to the function, the array
* of test modules is returned.
*
* @return {string[]}
*/
TestRunner.filterTestModules = function( testModules ) {
var queryString = ( function( urlParams ) {
var params = {};
if( urlParams.length === 1 && urlParams[0] === '' ) {
return params;
}
for( var i = 0; i < urlParams.length; i++ ) {
var param = urlParams[i].split( '=' );
if( param.length === 2 ) {
params[param[0]] = decodeURIComponent( param[1].replace( /\+/g, ' ' ) );
}
}
return params;
} )( window.location.search.substr( 1 ).split( '&' ) );

for( var i = 0; i < testModules.length; i++ ) {
if( testModules[i] === queryString.test ) {
return [queryString.test];
}
}

return testModules;
};

return TestRunner;

}( console ) );
39 changes: 39 additions & 0 deletions lib/tests/tests.TestRunner.phantom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* PhantomJS test runner module corresponding to the native test runner implementation.
*
* @licence GNU GPL v2+
* @author H. Snater < [email protected] >
*/

/* global tests, exports, console */
this.tests = this.tests || {};

this.tests.TestRunner = this.tests.TestRunner || {};

this.tests.TestRunner.phantom = ( function( console ) {
'use strict';

var TestRunner = function() {};

/**
* Evaluates the console messages generated in the native TestRunner.
*
* @param {string} msg
* @return {boolean|undefined}
*/
TestRunner.prototype.onConsoleMessage = function( msg ) {
console.log( msg );

if( msg.indexOf( 'TEST END' ) === 0 ) {
var msgParts = msg.match( /(\d{1,}) failure/ );
return ( parseInt( msgParts[1], 10 ) > 0 );
}

return undefined;
};

return TestRunner;

}( console ) );

exports.tests = this.tests;
75 changes: 10 additions & 65 deletions tests/runTests.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
<!--
Main entrance point for running QUnit tests.
Accessing this file will run all QUnit tests one after another by simply adding iframes to the body
node. Loading the tests in iframes ensures that only the necessary dependencies are loaded for the
individual tests.
@licence GNU GPL v2+
@author: H. Snater < [email protected] >
-->
<!DOCTYPE html>
<html>
Expand All @@ -14,69 +8,20 @@
<title>QUnit tests</title>
</head>
<body>

<script src="./testConfig.js"></script>
<script src="../lib/jquery/jquery.js"></script>

<script src="../lib/tests/tests.TestRunner.js"></script>
<script>
$( document ).ready( function() {
var queue = [],
currentModule,
$iFrame,
globalFailures = 0;

/**
* Triggers running a test by adding an iframe to the body element. Within the iframe, only
* the required dependencies to run the test are loaded.
* @param {string} module
*/
function triggerTest( module ) {
currentModule = module;

$iFrame = $( '<iframe/>' )
.attr( 'id', 'testFrame' + module.replace( /\./g, '-' ) )
.attr( 'src', './testRunner.html?testModule=' + module )
.attr( 'style', 'width:100%;' );

$( 'body' ).append( $iFrame );
}

for( var module in testConfig.paths ) {
if( /\.tests$/.test( module ) ) {
queue.unshift( module );
}
}
( function( window, tests ) {
window.onload = function() {

console.log( 'TEST START' );
var testRunner = new tests.TestRunner( {
queue: tests.modules,
testRunner: './testRunner.html'
} );

triggerTest( queue.pop() );

var testInterval = setInterval( function() {
var $failed = $iFrame.contents().find( '#qunit-testresult .failed' );

if( $failed.length > 0 ) {
// Test has finished.

var localFailures = parseInt( $failed.text() );

if( localFailures === 0 ) {
console.log( currentModule + ': passed' );
} else {
console.error( currentModule + ': FAILED (' + localFailures + ' failures)' );
globalFailures += localFailures;
}

if( queue.length === 0 ) {
clearInterval( testInterval );
console.log( 'TEST END (' + globalFailures + ' failure(s))' );
} else {
triggerTest( queue.pop() );
}
}
}, 100 );

} );
testRunner.start();
};
}( window, tests ) );
</script>

</body>
</html>
16 changes: 7 additions & 9 deletions tests/runTests.phantom.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* PhantomJS testrunner.
* PhantomJS test runner.
* PhantomJS will exit with an error code if one ore more tests fail.
*
* @licence GNU GPL v2+
Expand All @@ -12,16 +12,14 @@
var URL = './runTests.html',
TIMEOUT = 30;

var page = require( 'webpage' ).create();
var page = require( 'webpage' ).create(),
TestRunner = require( '../lib/tests/tests.TestRunner.phantom' ).tests.TestRunner.phantom,
testRunner = new TestRunner();

page.onConsoleMessage = function( msg ) {
console.log( msg );

if( msg.indexOf( 'TEST END' ) === 0 ) {
var msgParts = msg.match( /(\d{1,}) failure/ ),
failures = parseInt( msgParts[1], 10 );

phantom.exit( failures > 0 ? 1 : 0 );
var failed = testRunner.onConsoleMessage( msg );
if( failed !== undefined ) {
phantom.exit( failed ? 1 : 0 );
}
};

Expand Down
27 changes: 25 additions & 2 deletions tests/testConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
* @licence GNU GPL v2+
* @author H. Snater < [email protected] >
*/
var testConfig = ( function() {

/* global tests */
this.tests = this.tests || {};

tests.config = ( function() {
'use strict';

return {
Expand Down Expand Up @@ -147,7 +151,7 @@ var testConfig = ( function() {
},
'time.Time': {
exports: 'time.Time',
deps: ['time', 'time.Parser']
deps: ['time', 'jquery', 'time.Parser']
},
'time.Time.validate': {
exports: 'time.Time.validate',
Expand Down Expand Up @@ -280,3 +284,22 @@ var testConfig = ( function() {
};

} )();

/**
* Array of all QUnit test modules.
* @type {string[]}
*/
tests.modules = ( function( tests ) {
'use strict';

var modules = [];

for( var module in tests.config.paths ) {
if( /\.tests$/.test( module ) ) {
modules.unshift( module );
}
}

return modules;

}( tests ) );
Loading

0 comments on commit 98d1970

Please sign in to comment.