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