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