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 var toolbar
, testResults
, cntTotal
, cntCalled
, cntMissing
;
99 that
.populateMissingTests();
100 log( 'CompletenessTest/populateMissingTests', that
);
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).
297 // Objects are by reference, members (unless objects) are not.
303 window
.CompletenessTest
= CompletenessTest
;
305 }( mediaWiki
, jQuery
) );