diff --git a/lib/tests/tests.TestRunner.js b/lib/tests/tests.TestRunner.js new file mode 100644 index 0000000..1a4aff8 --- /dev/null +++ b/lib/tests/tests.TestRunner.js @@ -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 < mediawiki@snater.com > + */ + +/* 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 ) ); \ No newline at end of file diff --git a/lib/tests/tests.TestRunner.phantom.js b/lib/tests/tests.TestRunner.phantom.js new file mode 100644 index 0000000..b6d8fb9 --- /dev/null +++ b/lib/tests/tests.TestRunner.phantom.js @@ -0,0 +1,39 @@ +/** + * PhantomJS test runner module corresponding to the native test runner implementation. + * + * @licence GNU GPL v2+ + * @author H. Snater < mediawiki@snater.com > + */ + +/* 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; \ No newline at end of file diff --git a/tests/runTests.html b/tests/runTests.html index e066c03..890d9ac 100644 --- a/tests/runTests.html +++ b/tests/runTests.html @@ -1,11 +1,5 @@ @@ -14,69 +8,20 @@ QUnit tests - - - + - \ No newline at end of file diff --git a/tests/runTests.phantom.js b/tests/runTests.phantom.js index 31ba7f4..d817d52 100644 --- a/tests/runTests.phantom.js +++ b/tests/runTests.phantom.js @@ -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+ @@ -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 ); } }; diff --git a/tests/testConfig.js b/tests/testConfig.js index c8fb355..7052543 100644 --- a/tests/testConfig.js +++ b/tests/testConfig.js @@ -8,7 +8,11 @@ * @licence GNU GPL v2+ * @author H. Snater < mediawiki@snater.com > */ -var testConfig = ( function() { + +/* global tests */ +this.tests = this.tests || {}; + +tests.config = ( function() { 'use strict'; return { @@ -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', @@ -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 ) ); diff --git a/tests/testInit.js b/tests/testInit.js deleted file mode 100644 index 02daf4b..0000000 --- a/tests/testInit.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * QUnit test initialization. - * Loads the test configuration via require.js and triggers running the tests. If no single test - * module has been specified using the "testModule" URL parameter, all tests will be executed at - * once. - * - * @licence GNU GPL v2+ - * @author H. Snater < mediawiki@snater.com > - */ -/* global testConfig */ -( function( require, testConfig ) { - 'use strict'; - - 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( '&' ) ); - - var testModules = [], - testModule = queryString.testModule; - - if( testModule !== undefined ) { - testModules = [testModule]; - } else { - // Run all tests at once. - for( var module in testConfig.paths ) { - if( /\.tests$/.test( module ) ) { - testModules.push( module ); - } - } - } - - require.config( testConfig ); - - require( testModules, function() { - QUnit.load(); - QUnit.start(); - } ); - -} )( require, testConfig ); diff --git a/tests/testRunner.html b/tests/testRunner.html index e98836e..d443c45 100644 --- a/tests/testRunner.html +++ b/tests/testRunner.html @@ -14,6 +14,20 @@
- + + + \ No newline at end of file