/** * jQuery QUnit CompletenessTest 0.3 * * Tests the completeness of test suites for object oriented javascript * libraries. Written to be used in environments with jQuery and QUnit. * Requires jQuery 1.5.2 or higher. * * Globals: jQuery, QUnit, console.log * * Built for and tested with: * - Safari 5 * - Firefox 4 * * @author Timo Tijhof, 2011 */ ( function( $ ) { /** * CompletenessTest * @constructor * * @example * var myTester = new CompletenessTest( myLib ); * @param masterVariable {Object} The root variable that contains all object * members. CompletenessTest will recursively traverse objects and keep track * of all methods. * @param ignoreFn {Function} Optionally pass a function to filter out certain * methods. Example: You may want to filter out instances of jQuery or some * other constructor. Otherwise "missingTests" will include all methods that * were not called from that instance. */ var CompletenessTest = function ( masterVariable, ignoreFn ) { // Keep track in these objects. Keyed by strings with the // method names (ie. 'my.foo', 'my.bar', etc.) values are boolean true. this.methodCallTracker = {}; this.missingTests = {}; this.ignoreFn = undefined === ignoreFn ? function(){ return false; } : ignoreFn; // Lazy limit in case something weird happends (like recurse (part of) ourself). this.lazyLimit = 1000; this.lazyCounter = 0; var that = this; // Bind begin and end to QUnit. QUnit.begin = function(){ that.checkTests( null, masterVariable, masterVariable, [], CompletenessTest.ACTION_INJECT ); }; QUnit.done = function(){ that.checkTests( null, masterVariable, masterVariable, [], CompletenessTest.ACTION_CHECK ); console.log( 'CompletenessTest.ACTION_CHECK', that ); // Build HTML representing the outcome from CompletenessTest // And insert it into the header. var makeList = function( blob, title, style ) { title = title || 'Values'; var html = '' + mw.html.escape(title) + ''; $.each( blob, function( key ) { html += '
' + mw.html.escape(key); }); html += '

— CompletenessTest'; var $oldResult = $( '#qunit-completenesstest' ), $result = $oldResult.length ? $oldResult : $( '
' ); return $result.css( style ).html( html ); }; if ( $.isEmptyObject( that.missingTests ) ) { // Good var $testResults = makeList( { 'No missing tests!': true }, 'missingTests', { background: '#D2E0E6', color: '#366097', padding: '1em' } ); } else { // Bad var $testResults = makeList( that.missingTests, 'missingTests', { background: '#EE5757', color: 'black', padding: '1em' } ); } $( '#qunit-testrunner-toolbar' ).prepend( $testResults ); }; return this; }; /* Static members */ CompletenessTest.ACTION_INJECT = 500; CompletenessTest.ACTION_CHECK = 501; /* Public methods */ CompletenessTest.fn = CompletenessTest.prototype = { /** * CompletenessTest.fn.checkTests * * This function recursively walks through the given object, calling itself as it goes. * Depending on the action it either injects our listener into the methods, or * reads from our tracker and records which methods have not been called by the test suite. * * @param currName {String|Null} Name of the given object member (Initially this is null). * @param currVar {mixed} The variable to check (initially an object, * further down it could be anything). * @param masterVariable {Object} Throughout our interation, always keep track of the master/root. * Initially this is the same as currVar. * @param parentPathArray {Array} Array of names that indicate our breadcrumb path starting at * masterVariable. Not including currName. * @param action {Number} What is this function supposed to do (ACTION_INJECT or ACTION_CHECK) */ checkTests: function( currName, currVar, masterVariable, parentPathArray, action ) { // Handle the lazy limit this.lazyCounter++; if ( this.lazyCounter > this.lazyLimit ) { console.log( 'CompletenessTest.fn.checkTests> Limit reached: ' + this.lazyCounter ); return null; } var type = $.type( currVar ), that = this; // Hard ignores if ( this.ignoreFn( currVar, that, parentPathArray ) ) { return null; // Functions } else if ( type === 'function' ) { /* CHECK MODE */ if ( action === CompletenessTest.ACTION_CHECK ) { if ( !currVar.prototype || $.isEmptyObject( currVar.prototype ) ) { that.hasTest( parentPathArray.join( '.' ) ); // We don't support checking object constructors yet... } else { // ...the prototypes are fine tho $.each( currVar.prototype, function( key, value ) { if ( key === 'constructor' ) return; // Clone and break reference to parentPathArray var tmpPathArray = $.extend( [], parentPathArray ); tmpPathArray.push( 'prototype' ); tmpPathArray.push( key ); that.hasTest( tmpPathArray.join( '.' ) ); } ); } /* INJECT MODE */ } else if ( action === CompletenessTest.ACTION_INJECT ) { if ( !currVar.prototype || $.isEmptyObject( currVar.prototype ) ) { // Inject check that.injectCheck( masterVariable, parentPathArray, function() { that.methodCallTracker[ parentPathArray.join( '.' ) ] = true; } ); // We don't support checking object constructors yet... } else { // ... the prototypes are fine tho $.each( currVar.prototype, function( key, value ) { if ( key === 'constructor' ) return; // Clone and break reference to parentPathArray var tmpPathArray = $.extend( [], parentPathArray ); tmpPathArray.push( 'prototype' ); tmpPathArray.push( key ); that.checkTests( key, value, masterVariable, tmpPathArray, action ); } ); } } // Recursively. After all, this *is* the completeness test } else if ( type === 'object' ) { $.each( currVar, function( key, value ) { // Clone and break reference to parentPathArray var tmpPathArray = $.extend( [], parentPathArray ); tmpPathArray.push( key ); that.checkTests( key, value, masterVariable, tmpPathArray, action ); } ); } }, /** * CompletenessTest.fn.hasTest * * Checks if the given method name (ie. 'my.foo.bar') * was called during the test suite (as far as the tracker knows). * If not it adds it to missingTests. * * @param fnName {String} * @return {Boolean} */ hasTest: function( fnName ) { if ( !( fnName in this.methodCallTracker ) ) { this.missingTests[fnName] = true; return false; } return true; }, /** * CompletenessTest.fn.injectCheck * * Injects a function (such as a spy that updates methodCallTracker when * it's called) inside another function. * * @param masterVariable {Object} * @param objectPathArray {Array} * @param injectFn {Function} */ injectCheck: function( masterVariable, objectPathArray, injectFn ) { var prev, curr = masterVariable, lastMember; $.each( objectPathArray, function( i, memberName ) { prev = curr; curr = prev[memberName]; lastMember = memberName; }); // Objects are by reference, members (unless objects) are not. prev[lastMember] = function() { injectFn(); return curr.apply( this, arguments ); }; } }; window.CompletenessTest = CompletenessTest; } )( jQuery );