2 * jQuery QUnit CompletenessTest 0.4
4 * Tests the completeness of test suites for object oriented javascript
5 * libraries. Written to be used in environments with jQuery and QUnit.
6 * Requires jQuery 1.7.2 or higher.
8 * Built for and tested with:
13 * @author Timo Tijhof, 2011-2012
15 /* eslint-env qunit */
16 ( function ( mw, $ ) {
20 hasOwn = Object.prototype.hasOwnProperty,
21 log = ( window.console && window.console.log ) ?
22 function () { return window.console.log.apply( window.console, arguments ); } :
25 // Simplified version of a few jQuery methods, except that they don't
26 // call other jQuery methods. Required to be able to run the CompletenessTest
27 // on jQuery itself as well.
29 keys: Object.keys || function ( object ) {
31 for ( key in object ) {
32 if ( hasOwn.call( object, key ) ) {
38 each: function ( object, callback ) {
40 for ( name in object ) {
41 if ( callback.call( object[ name ], name, object[ name ] ) === false ) {
46 // $.type and $.isEmptyObject are safe as is, they don't call
47 // other $.* methods. Still need to be derefenced into `util`
48 // since the CompletenessTest will overload them with spies.
50 isEmptyObject: $.isEmptyObject
58 * var myTester = new CompletenessTest( myLib );
59 * @param {Object} masterVariable The root variable that contains all object
60 * members. CompletenessTest will recursively traverse objects and keep track
62 * @param {Function} [ignoreFn] Optionally pass a function to filter out certain
63 * methods. Example: You may want to filter out instances of jQuery or some
64 * other constructor. Otherwise "missingTests" will include all methods that
65 * were not called from that instance.
67 function CompletenessTest( masterVariable, ignoreFn ) {
71 // Keep track in these objects. Keyed by strings with the
72 // method names (ie. 'my.foo', 'my.bar', etc.) values are boolean true.
73 this.injectionTracker = {};
74 this.methodCallTracker = {};
75 this.missingTests = {};
77 this.ignoreFn = ignoreFn === undefined ? function () { return false; } : ignoreFn;
79 // Lazy limit in case something weird happends (like recurse (part of) ourself).
80 this.lazyLimit = 2000;
83 // Bind begin and end to QUnit.
84 QUnit.begin( function () {
85 // Suppress warnings (e.g. deprecation notices for accessing the properties)
89 that.walkTheObject( masterVariable, null, masterVariable, [] );
90 log( 'CompletenessTest/walkTheObject', that );
97 QUnit.done( function () {
98 var toolbar, testResults, cntTotal, cntCalled, cntMissing;
100 that.populateMissingTests();
101 log( 'CompletenessTest/populateMissingTests', that );
103 cntTotal = util.keys( that.injectionTracker ).length;
104 cntCalled = util.keys( that.methodCallTracker ).length;
105 cntMissing = util.keys( that.missingTests ).length;
107 function makeTestResults( blob, title, style ) {
108 var elOutputWrapper, elTitle, elContainer, elList, elFoot;
110 elTitle = document.createElement( 'strong' );
111 elTitle.textContent = title || 'Values';
113 elList = document.createElement( 'ul' );
114 util.each( blob, function ( key ) {
115 var elItem = document.createElement( 'li' );
116 elItem.textContent = key;
117 elList.appendChild( elItem );
120 elFoot = document.createElement( 'p' );
121 elFoot.innerHTML = '<em>— CompletenessTest</em>';
123 elContainer = document.createElement( 'div' );
124 elContainer.appendChild( elTitle );
125 elContainer.appendChild( elList );
126 elContainer.appendChild( elFoot );
128 elOutputWrapper = document.getElementById( 'qunit-completenesstest' );
129 if ( !elOutputWrapper ) {
130 elOutputWrapper = document.createElement( 'div' );
131 elOutputWrapper.id = 'qunit-completenesstest';
133 elOutputWrapper.appendChild( elContainer );
135 util.each( style, function ( key, value ) {
136 elOutputWrapper.style[ key ] = value;
138 return elOutputWrapper;
141 if ( cntMissing === 0 ) {
143 testResults = makeTestResults(
145 'Detected calls to ' + cntCalled + '/' + cntTotal + ' methods. No missing tests!',
147 backgroundColor: '#D2E0E6',
151 paddingBottom: '1em',
157 testResults = makeTestResults(
159 'Detected calls to ' + cntCalled + '/' + cntTotal + ' methods. ' + cntMissing + ' methods not covered:',
161 backgroundColor: '#EE5757',
165 paddingBottom: '1em',
171 toolbar = document.getElementById( 'qunit-testrunner-toolbar' );
173 toolbar.insertBefore( testResults, toolbar.firstChild );
181 CompletenessTest.fn = CompletenessTest.prototype = {
184 * CompletenessTest.fn.walkTheObject
186 * This function recursively walks through the given object, calling itself as it goes.
187 * Depending on the action it either injects our listener into the methods, or
188 * reads from our tracker and records which methods have not been called by the test suite.
190 * @param {Mixed} currObj The variable to check (initially an object,
191 * further down it could be anything).
192 * @param {string|null} currName Name of the given object member (Initially this is null).
193 * @param {Object} masterVariable Throughout our interation, always keep track of the master/root.
194 * Initially this is the same as currVar.
195 * @param {Array} parentPathArray Array of names that indicate our breadcrumb path starting at
196 * masterVariable. Not including currName.
198 walkTheObject: function ( currObj, currName, masterVariable, parentPathArray ) {
199 var key, currVal, type,
201 currPathArray = parentPathArray;
204 currPathArray.push( currName );
205 currVal = currObj[ currName ];
211 type = util.type( currVal );
214 if ( this.ignoreFn( currVal, this, currPathArray ) ) {
218 // Handle the lazy limit
220 if ( this.lazyCounter > this.lazyLimit ) {
221 log( 'CompletenessTest.fn.walkTheObject> Limit reached: ' + this.lazyCounter, currPathArray );
226 if ( type === 'function' ) {
227 // Don't put a spy in constructor functions as it messes with
229 if ( !currVal.prototype || util.isEmptyObject( currVal.prototype ) ) {
230 this.injectionTracker[ currPathArray.join( '.' ) ] = true;
231 this.injectCheck( currObj, currName, function () {
232 ct.methodCallTracker[ currPathArray.join( '.' ) ] = true;
237 // Recursively. After all, this is the *completeness* test
238 // This also traverses static properties and the prototype of a constructor
239 if ( type === 'object' || type === 'function' ) {
240 for ( key in currVal ) {
241 if ( hasOwn.call( currVal, key ) ) {
242 this.walkTheObject( currVal, key, masterVariable, currPathArray.slice() );
248 populateMissingTests: function () {
250 util.each( ct.injectionTracker, function ( key ) {
256 * CompletenessTest.fn.hasTest
258 * Checks if the given method name (ie. 'my.foo.bar')
259 * was called during the test suite (as far as the tracker knows).
260 * If not it adds it to missingTests.
262 * @param {string} fnName
265 hasTest: function ( fnName ) {
266 if ( !( fnName in this.methodCallTracker ) ) {
267 this.missingTests[ fnName ] = true;
274 * CompletenessTest.fn.injectCheck
276 * Injects a function (such as a spy that updates methodCallTracker when
277 * it's called) inside another function.
279 * @param {Object} obj The object into which `injectFn` will be inserted
280 * @param {Array} key The key by which `injectFn` will be known in `obj`; if this already
281 * exists, a wrapper will first call `injectFn` and then the original `obj[key]` function.
282 * @param {Function} injectFn The function to insert
284 injectCheck: function ( obj, key, injectFn ) {
290 return val.apply( this, arguments );
293 // Make the spy inherit from the original so that its static methods are also
294 // visible in the spy (e.g. when we inject a check into mw.log, mw.log.warn
295 // must remain accessible).
298 // Objects are by reference, members (unless objects) are not.
304 window.CompletenessTest = CompletenessTest;
306 }( mediaWiki, jQuery ) );