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