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