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 ( function ( mw, $ ) {
19 hasOwn = Object.prototype.hasOwnProperty,
20 log = ( window.console && window.console.log )
21 ? function () { return window.console.log.apply( window.console, arguments ); }
24 // Simplified version of a few jQuery methods, except that they don't
25 // call other jQuery methods. Required to be able to run the CompletenessTest
26 // on jQuery itself as well.
28 keys: Object.keys || function ( object ) {
30 for ( key in object ) {
31 if ( hasOwn.call( object, key ) ) {
37 each: function ( object, callback ) {
39 for ( name in object ) {
40 if ( callback.call( object[ name ], name, object[ name ] ) === false ) {
45 // $.type and $.isEmptyObject are safe as is, they don't call
46 // other $.* methods. Still need to be derefenced into `util`
47 // since the CompletenessTest will overload them with spies.
49 isEmptyObject: $.isEmptyObject
57 * var myTester = new CompletenessTest( myLib );
58 * @param {Object} masterVariable The root variable that contains all object
59 * members. CompletenessTest will recursively traverse objects and keep track
61 * @param {Function} [ignoreFn] Optionally pass a function to filter out certain
62 * methods. Example: You may want to filter out instances of jQuery or some
63 * other constructor. Otherwise "missingTests" will include all methods that
64 * were not called from that instance.
66 function CompletenessTest( masterVariable, ignoreFn ) {
70 // Keep track in these objects. Keyed by strings with the
71 // method names (ie. 'my.foo', 'my.bar', etc.) values are boolean true.
72 this.injectionTracker = {};
73 this.methodCallTracker = {};
74 this.missingTests = {};
76 this.ignoreFn = ignoreFn === undefined ? function () { return false; } : ignoreFn;
78 // Lazy limit in case something weird happends (like recurse (part of) ourself).
79 this.lazyLimit = 2000;
82 // Bind begin and end to QUnit.
83 QUnit.begin( function () {
84 // Suppress warnings (e.g. deprecation notices for accessing the properties)
88 that.walkTheObject( masterVariable, null, masterVariable, [] );
89 log( 'CompletenessTest/walkTheObject', that );
96 QUnit.done( function () {
97 that.populateMissingTests();
98 log( 'CompletenessTest/populateMissingTests', that );
100 var toolbar, testResults, cntTotal, cntCalled, cntMissing;
102 cntTotal = util.keys( that.injectionTracker ).length;
103 cntCalled = util.keys( that.methodCallTracker ).length;
104 cntMissing = util.keys( that.missingTests ).length;
106 function makeTestResults( blob, title, style ) {
107 var elOutputWrapper, elTitle, elContainer, elList, elFoot;
109 elTitle = document.createElement( 'strong' );
110 elTitle.textContent = title || 'Values';
112 elList = document.createElement( 'ul' );
113 util.each( blob, function ( key ) {
114 var elItem = document.createElement( 'li' );
115 elItem.textContent = key;
116 elList.appendChild( elItem );
119 elFoot = document.createElement( 'p' );
120 elFoot.innerHTML = '<em>— CompletenessTest</em>';
122 elContainer = document.createElement( 'div' );
123 elContainer.appendChild( elTitle );
124 elContainer.appendChild( elList );
125 elContainer.appendChild( elFoot );
127 elOutputWrapper = document.getElementById( 'qunit-completenesstest' );
128 if ( !elOutputWrapper ) {
129 elOutputWrapper = document.createElement( 'div' );
130 elOutputWrapper.id = 'qunit-completenesstest';
132 elOutputWrapper.appendChild( elContainer );
134 util.each( style, function ( key, value ) {
135 elOutputWrapper.style[ key ] = value;
137 return elOutputWrapper;
140 if ( cntMissing === 0 ) {
142 testResults = makeTestResults(
144 'Detected calls to ' + cntCalled + '/' + cntTotal + ' methods. No missing tests!',
146 backgroundColor: '#D2E0E6',
150 paddingBottom: '1em',
156 testResults = makeTestResults(
158 'Detected calls to ' + cntCalled + '/' + cntTotal + ' methods. ' + cntMissing + ' methods not covered:',
160 backgroundColor: '#EE5757',
164 paddingBottom: '1em',
170 toolbar = document.getElementById( 'qunit-testrunner-toolbar' );
172 toolbar.insertBefore( testResults, toolbar.firstChild );
180 CompletenessTest.fn = CompletenessTest.prototype = {
183 * CompletenessTest.fn.walkTheObject
185 * This function recursively walks through the given object, calling itself as it goes.
186 * Depending on the action it either injects our listener into the methods, or
187 * reads from our tracker and records which methods have not been called by the test suite.
189 * @param {Mixed} currObj The variable to check (initially an object,
190 * further down it could be anything).
191 * @param {string|null} currName Name of the given object member (Initially this is null).
192 * @param {Object} masterVariable Throughout our interation, always keep track of the master/root.
193 * Initially this is the same as currVar.
194 * @param {Array} parentPathArray Array of names that indicate our breadcrumb path starting at
195 * masterVariable. Not including currName.
197 walkTheObject: function ( currObj, currName, masterVariable, parentPathArray ) {
198 var key, currVal, type,
200 currPathArray = parentPathArray;
203 currPathArray.push( currName );
204 currVal = currObj[ currName ];
210 type = util.type( currVal );
213 if ( this.ignoreFn( currVal, this, currPathArray ) ) {
217 // Handle the lazy limit
219 if ( this.lazyCounter > this.lazyLimit ) {
220 log( 'CompletenessTest.fn.walkTheObject> Limit reached: ' + this.lazyCounter, currPathArray );
225 if ( type === 'function' ) {
226 // Don't put a spy in constructor functions as it messes with
228 if ( !currVal.prototype || util.isEmptyObject( currVal.prototype ) ) {
229 this.injectionTracker[ currPathArray.join( '.' ) ] = true;
230 this.injectCheck( currObj, currName, function () {
231 ct.methodCallTracker[ currPathArray.join( '.' ) ] = true;
236 // Recursively. After all, this is the *completeness* test
237 // This also traverses static properties and the prototype of a constructor
238 if ( type === 'object' || type === 'function' ) {
239 for ( key in currVal ) {
240 if ( hasOwn.call( currVal, key ) ) {
241 this.walkTheObject( currVal, key, masterVariable, currPathArray.slice() );
247 populateMissingTests: function () {
249 util.each( ct.injectionTracker, function ( key ) {
255 * CompletenessTest.fn.hasTest
257 * Checks if the given method name (ie. 'my.foo.bar')
258 * was called during the test suite (as far as the tracker knows).
259 * If not it adds it to missingTests.
261 * @param {string} fnName
264 hasTest: function ( fnName ) {
265 if ( !( fnName in this.methodCallTracker ) ) {
266 this.missingTests[ fnName ] = true;
273 * CompletenessTest.fn.injectCheck
275 * Injects a function (such as a spy that updates methodCallTracker when
276 * it's called) inside another function.
278 * @param {Object} obj The object into which `injectFn` will be inserted
279 * @param {Array} key The key by which `injectFn` will be known in `obj`; if this already
280 * exists, a wrapper will first call `injectFn` and then the original `obj[key]` function.
281 * @param {Function} injectFn The function to insert
283 injectCheck: function ( obj, key, injectFn ) {
289 return val.apply( this, arguments );
292 // Make the spy inherit from the original so that its static methods are also
293 // visible in the spy (e.g. when we inject a check into mw.log, mw.log.warn
294 // must remain accessible).
295 // XXX: https://github.com/jshint/jshint/issues/2656
296 /*jshint ignore:start */
297 /*jshint proto:true */
299 /*jshint ignore:end */
301 // Objects are by reference, members (unless objects) are not.
307 window.CompletenessTest = CompletenessTest;
309 }( mediaWiki, jQuery ) );