Merge "Choose parentids in tests as they are in real dumps"
[lhc/web/wiklou.git] / resources / jquery / jquery.qunit.completenessTest.js
1 /**
2 * jQuery QUnit CompletenessTest 0.4
3 *
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.
7 *
8 * Built for and tested with:
9 * - Chrome 19
10 * - Firefox 4
11 * - Safari 5
12 *
13 * @author Timo Tijhof, 2011-2012
14 */
15 /*global jQuery, QUnit */
16 /*jshint eqeqeq:false, eqnull:false, forin:false */
17 ( function ( $ ) {
18 "use strict";
19
20 var util,
21 hasOwn = Object.prototype.hasOwnProperty,
22 log = (window.console && window.console.log)
23 ? function () { return window.console.log.apply(window.console, arguments); }
24 : function () {};
25
26 // Simplified version of a few jQuery methods, except that they don't
27 // call other jQuery methods. Required to be able to run the CompletenessTest
28 // on jQuery itself as well.
29 util = {
30 keys: Object.keys || function ( object ) {
31 var key, keys = [];
32 for ( key in object ) {
33 if ( hasOwn.call( object, key ) ) {
34 keys.push( key );
35 }
36 }
37 return keys;
38 },
39 extend: function () {
40 var options, name, src, copy,
41 target = arguments[0] || {},
42 i = 1,
43 length = arguments.length;
44
45 for ( ; i < length; i++ ) {
46 // Only deal with non-null/undefined values
47 if ( (options = arguments[ i ]) != null ) {
48 // Extend the base object
49 for ( name in options ) {
50 src = target[ name ];
51 copy = options[ name ];
52
53 // Prevent never-ending loop
54 if ( target === copy ) {
55 continue;
56 }
57
58 if ( copy !== undefined ) {
59 target[ name ] = copy;
60 }
61 }
62 }
63 }
64
65 // Return the modified object
66 return target;
67 },
68 each: function ( object, callback ) {
69 var name;
70 for ( name in object ) {
71 if ( callback.call( object[ name ], name, object[ name ] ) === false ) {
72 break;
73 }
74 }
75 },
76 // $.type and $.isEmptyObject are safe as is, they don't call
77 // other $.* methods. Still need to be derefenced into `util`
78 // since the CompletenessTest will overload them with spies.
79 type: $.type,
80 isEmptyObject: $.isEmptyObject
81 };
82
83
84 /**
85 * CompletenessTest
86 * @constructor
87 *
88 * @example
89 * var myTester = new CompletenessTest( myLib );
90 * @param masterVariable {Object} The root variable that contains all object
91 * members. CompletenessTest will recursively traverse objects and keep track
92 * of all methods.
93 * @param ignoreFn {Function} Optionally pass a function to filter out certain
94 * methods. Example: You may want to filter out instances of jQuery or some
95 * other constructor. Otherwise "missingTests" will include all methods that
96 * were not called from that instance.
97 */
98 var CompletenessTest = function ( masterVariable, ignoreFn ) {
99
100 // Keep track in these objects. Keyed by strings with the
101 // method names (ie. 'my.foo', 'my.bar', etc.) values are boolean true.
102 this.injectionTracker = {};
103 this.methodCallTracker = {};
104 this.missingTests = {};
105
106 this.ignoreFn = undefined === ignoreFn ? function () { return false; } : ignoreFn;
107
108 // Lazy limit in case something weird happends (like recurse (part of) ourself).
109 this.lazyLimit = 2000;
110 this.lazyCounter = 0;
111
112 var that = this;
113
114 // Bind begin and end to QUnit.
115 QUnit.begin( function () {
116 that.walkTheObject( null, masterVariable, masterVariable, [], CompletenessTest.ACTION_INJECT );
117 log( 'CompletenessTest/walkTheObject/ACTION_INJECT', that );
118 });
119
120 QUnit.done( function () {
121 that.populateMissingTests();
122 log( 'CompletenessTest/populateMissingTests', that );
123
124 var toolbar, testResults, cntTotal, cntCalled, cntMissing;
125
126 cntTotal = util.keys( that.injectionTracker ).length;
127 cntCalled = util.keys( that.methodCallTracker ).length;
128 cntMissing = util.keys( that.missingTests ).length;
129
130 function makeTestResults( blob, title, style ) {
131 var elOutputWrapper, elTitle, elContainer, elList, elFoot;
132
133 elTitle = document.createElement( 'strong' );
134 elTitle.textContent = title || 'Values';
135
136 elList = document.createElement( 'ul' );
137 util.each( blob, function ( key ) {
138 var elItem = document.createElement( 'li' );
139 elItem.textContent = key;
140 elList.appendChild( elItem );
141 });
142
143 elFoot = document.createElement( 'p' );
144 elFoot.innerHTML = '<em>&mdash; CompletenessTest</em>';
145
146 elContainer = document.createElement( 'div' );
147 elContainer.appendChild( elTitle );
148 elContainer.appendChild( elList );
149 elContainer.appendChild( elFoot );
150
151 elOutputWrapper = document.getElementById( 'qunit-completenesstest' );
152 if ( !elOutputWrapper ) {
153 elOutputWrapper = document.createElement( 'div' );
154 elOutputWrapper.id = 'qunit-completenesstest';
155 }
156 elOutputWrapper.appendChild( elContainer );
157
158 util.each( style, function ( key, value ) {
159 elOutputWrapper.style[key] = value;
160 });
161 return elOutputWrapper;
162 }
163
164 if ( cntMissing === 0 ) {
165 // Good
166 testResults = makeTestResults(
167 {},
168 'Detected calls to ' + cntCalled + '/' + cntTotal + ' methods. No missing tests!',
169 {
170 backgroundColor: '#D2E0E6',
171 color: '#366097',
172 paddingTop: '1em',
173 paddingRight: '1em',
174 paddingBottom: '1em',
175 paddingLeft: '1em'
176 }
177 );
178 } else {
179 // Bad
180 testResults = makeTestResults(
181 that.missingTests,
182 'Detected calls to ' + cntCalled + '/' + cntTotal + ' methods. ' + cntMissing + ' methods not covered:',
183 {
184 backgroundColor: '#EE5757',
185 color: 'black',
186 paddingTop: '1em',
187 paddingRight: '1em',
188 paddingBottom: '1em',
189 paddingLeft: '1em'
190 }
191 );
192 }
193
194 toolbar = document.getElementById( 'qunit-testrunner-toolbar' );
195 if ( toolbar ) {
196 toolbar.insertBefore( testResults, toolbar.firstChild );
197 }
198 });
199
200 return this;
201 };
202
203 /* Static members */
204 CompletenessTest.ACTION_INJECT = 500;
205 CompletenessTest.ACTION_CHECK = 501;
206
207 /* Public methods */
208 CompletenessTest.fn = CompletenessTest.prototype = {
209
210 /**
211 * CompletenessTest.fn.walkTheObject
212 *
213 * This function recursively walks through the given object, calling itself as it goes.
214 * Depending on the action it either injects our listener into the methods, or
215 * reads from our tracker and records which methods have not been called by the test suite.
216 *
217 * @param currName {String|Null} Name of the given object member (Initially this is null).
218 * @param currVar {mixed} The variable to check (initially an object,
219 * further down it could be anything).
220 * @param masterVariable {Object} Throughout our interation, always keep track of the master/root.
221 * Initially this is the same as currVar.
222 * @param parentPathArray {Array} Array of names that indicate our breadcrumb path starting at
223 * masterVariable. Not including currName.
224 * @param action {Number} What is this function supposed to do (ACTION_INJECT or ACTION_CHECK)
225 */
226 walkTheObject: function ( currName, currVar, masterVariable, parentPathArray, action ) {
227
228 var key, value, tmpPathArray,
229 type = util.type( currVar ),
230 that = this;
231
232 // Hard ignores
233 if ( this.ignoreFn( currVar, that, parentPathArray ) ) {
234 return null;
235 }
236
237 // Handle the lazy limit
238 this.lazyCounter++;
239 if ( this.lazyCounter > this.lazyLimit ) {
240 log( 'CompletenessTest.fn.walkTheObject> Limit reached: ' + this.lazyCounter, parentPathArray );
241 return null;
242 }
243
244 // Functions
245 if ( type === 'function' ) {
246
247 if ( !currVar.prototype || util.isEmptyObject( currVar.prototype ) ) {
248
249 if ( action === CompletenessTest.ACTION_INJECT ) {
250
251 that.injectionTracker[ parentPathArray.join( '.' ) ] = true;
252 that.injectCheck( masterVariable, parentPathArray, function () {
253 that.methodCallTracker[ parentPathArray.join( '.' ) ] = true;
254 } );
255 }
256
257 // We don't support checking object constructors yet...
258 // ...we can check the prototypes fine, though.
259 } else {
260 if ( action === CompletenessTest.ACTION_INJECT ) {
261
262 for ( key in currVar.prototype ) {
263 if ( hasOwn.call( currVar.prototype, key ) ) {
264 value = currVar.prototype[key];
265 if ( key === 'constructor' ) {
266 continue;
267 }
268
269 // Clone and break reference to parentPathArray
270 tmpPathArray = util.extend( [], parentPathArray );
271 tmpPathArray.push( 'prototype' );
272 tmpPathArray.push( key );
273
274 that.walkTheObject( key, value, masterVariable, tmpPathArray, action );
275 }
276 }
277
278 }
279 }
280
281 }
282
283 // Recursively. After all, this is the *completeness* test
284 if ( type === 'function' || type === 'object' ) {
285 for ( key in currVar ) {
286 if ( hasOwn.call( currVar, key ) ) {
287 value = currVar[key];
288
289 // Clone and break reference to parentPathArray
290 tmpPathArray = util.extend( [], parentPathArray );
291 tmpPathArray.push( key );
292
293 that.walkTheObject( key, value, masterVariable, tmpPathArray, action );
294 }
295 }
296 }
297 },
298
299 populateMissingTests: function () {
300 var ct = this;
301 util.each( ct.injectionTracker, function ( key ) {
302 ct.hasTest( key );
303 });
304 },
305
306 /**
307 * CompletenessTest.fn.hasTest
308 *
309 * Checks if the given method name (ie. 'my.foo.bar')
310 * was called during the test suite (as far as the tracker knows).
311 * If not it adds it to missingTests.
312 *
313 * @param fnName {String}
314 * @return {Boolean}
315 */
316 hasTest: function ( fnName ) {
317 if ( !( fnName in this.methodCallTracker ) ) {
318 this.missingTests[fnName] = true;
319 return false;
320 }
321 return true;
322 },
323
324 /**
325 * CompletenessTest.fn.injectCheck
326 *
327 * Injects a function (such as a spy that updates methodCallTracker when
328 * it's called) inside another function.
329 *
330 * @param masterVariable {Object}
331 * @param objectPathArray {Array}
332 * @param injectFn {Function}
333 */
334 injectCheck: function ( masterVariable, objectPathArray, injectFn ) {
335 var i, len, prev, memberName, lastMember,
336 curr = masterVariable;
337
338 // Get the object in question through the path from the master variable,
339 // We can't pass the value directly because we need to re-define the object
340 // member and keep references to the parent object, member name and member
341 // value at all times.
342 for ( i = 0, len = objectPathArray.length; i < len; i++ ) {
343 memberName = objectPathArray[i];
344
345 prev = curr;
346 curr = prev[memberName];
347 lastMember = memberName;
348 }
349
350 // Objects are by reference, members (unless objects) are not.
351 prev[lastMember] = function () {
352 injectFn();
353 return curr.apply( this, arguments );
354 };
355 }
356 };
357
358 window.CompletenessTest = CompletenessTest;
359
360 } )( jQuery );