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 @@