qunit.completenessTest: Suppress mw.log.warn in walkTheObject
[mediawiki.git] / resources / src / jquery / jquery.qunit.completenessTest.js
blob8d38401eb9a1d913de95033c5feda132b24326e3
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';
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 () {};
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         };
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                 var warn,
68                         that = this;
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 = {};
76                 this.ignoreFn = ignoreFn === undefined ? function () { return false; } : ignoreFn;
78                 // Lazy limit in case something weird happends (like recurse (part of) ourself).
79                 this.lazyLimit = 2000;
80                 this.lazyCounter = 0;
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;
88                         that.walkTheObject( masterVariable, null, masterVariable, [] );
89                         log( 'CompletenessTest/walkTheObject', that );
91                         // Restore warnings
92                         mw.log.warn = warn;
93                         warn = undefined;
94                 });
96                 QUnit.done( function () {
97                         that.populateMissingTests();
98                         log( 'CompletenessTest/populateMissingTests', that );
100                         var toolbar, testResults, cntTotal, cntCalled, cntMissing;
102                         cntTotal = util.keys( that.injectionTracker ).length;
103                         cntCalled = util.keys( that.methodCallTracker ).length;
104                         cntMissing = util.keys( that.missingTests ).length;
106                         function makeTestResults( blob, title, style ) {
107                                 var elOutputWrapper, elTitle, elContainer, elList, elFoot;
109                                 elTitle = document.createElement( 'strong' );
110                                 elTitle.textContent = title || 'Values';
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                                 });
119                                 elFoot = document.createElement( 'p' );
120                                 elFoot.innerHTML = '<em>&mdash; CompletenessTest</em>';
122                                 elContainer = document.createElement( 'div' );
123                                 elContainer.appendChild( elTitle );
124                                 elContainer.appendChild( elList );
125                                 elContainer.appendChild( elFoot );
127                                 elOutputWrapper = document.getElementById( 'qunit-completenesstest' );
128                                 if ( !elOutputWrapper ) {
129                                         elOutputWrapper = document.createElement( 'div' );
130                                         elOutputWrapper.id = 'qunit-completenesstest';
131                                 }
132                                 elOutputWrapper.appendChild( elContainer );
134                                 util.each( style, function ( key, value ) {
135                                         elOutputWrapper.style[key] = value;
136                                 });
137                                 return elOutputWrapper;
138                         }
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                         }
170                         toolbar = document.getElementById( 'qunit-testrunner-toolbar' );
171                         if ( toolbar ) {
172                                 toolbar.insertBefore( testResults, toolbar.firstChild );
173                         }
174                 });
176                 return this;
177         }
179         /* Public methods */
180         CompletenessTest.fn = CompletenessTest.prototype = {
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 currName {String|Null} Name of the given object member (Initially this is null).
190                  * @param currVar {mixed} The variable to check (initially an object,
191                  *  further down it could be anything).
192                  * @param masterVariable {Object} Throughout our interation, always keep track of the master/root.
193                  *  Initially this is the same as currVar.
194                  * @param parentPathArray {Array} 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;
202                         if ( currName ) {
203                                 currPathArray.push( currName );
204                                 currVal = currObj[currName];
205                         } else {
206                                 currName = '(root)';
207                                 currVal = currObj;
208                         }
210                         type = util.type( currVal );
212                         // Hard ignores
213                         if ( this.ignoreFn( currVal, this, currPathArray ) ) {
214                                 return null;
215                         }
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                         }
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                         }
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                 },
247                 populateMissingTests: function () {
248                         var ct = this;
249                         util.each( ct.injectionTracker, function ( key ) {
250                                 ct.hasTest( key );
251                         });
252                 },
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 fnName {String}
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                 },
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 masterVariable {Object}
279                  * @param objectPathArray {Array}
280                  * @param injectFn {Function}
281                  */
282                 injectCheck: function ( obj, key, injectFn ) {
283                         var spy,
284                                 val = obj[ key ];
286                         spy = function () {
287                                 injectFn();
288                                 return val.apply( this, arguments );
289                         };
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                         /*jshint proto:true */
295                         spy.__proto__ = val;
297                         // Objects are by reference, members (unless objects) are not.
298                         obj[ key ] = spy;
299                 }
300         };
302         /* Expose */
303         window.CompletenessTest = CompletenessTest;
305 }( mediaWiki, jQuery ) );