0aaa4ff39de516b63fdf6487ad7d289d943a788f
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
) );