Merge "Improve sorting on SpecialWanted*-Pages"
[mediawiki.git] / resources / src / jquery / jquery.qunit.completenessTest.js
blob0aaa4ff39de516b63fdf6487ad7d289d943a788f
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 /* eslint-env qunit */
16 ( function ( mw, $ ) {
17         'use strict';
19         var util,
20                 hasOwn = Object.prototype.hasOwnProperty,
21                 log = ( window.console && window.console.log ) ?
22                         function () { return window.console.log.apply( window.console, arguments ); } :
23                         function () {};
25         // Simplified version of a few jQuery methods, except that they don't
26         // call other jQuery methods. Required to be able to run the CompletenessTest
27         // on jQuery itself as well.
28         util = {
29                 keys: Object.keys || function ( object ) {
30                         var key, keys = [];
31                         for ( key in object ) {
32                                 if ( hasOwn.call( object, key ) ) {
33                                         keys.push( key );
34                                 }
35                         }
36                         return keys;
37                 },
38                 each: function ( object, callback ) {
39                         var name;
40                         for ( name in object ) {
41                                 if ( callback.call( object[ name ], name, object[ name ] ) === false ) {
42                                         break;
43                                 }
44                         }
45                 },
46                 // $.type and $.isEmptyObject are safe as is, they don't call
47                 // other $.* methods. Still need to be derefenced into `util`
48                 // since the CompletenessTest will overload them with spies.
49                 type: $.type,
50                 isEmptyObject: $.isEmptyObject
51         };
53         /**
54          * CompletenessTest
55          *
56          * @constructor
57          * @example
58          *  var myTester = new CompletenessTest( myLib );
59          * @param {Object} masterVariable The root variable that contains all object
60          *  members. CompletenessTest will recursively traverse objects and keep track
61          *  of all methods.
62          * @param {Function} [ignoreFn] Optionally pass a function to filter out certain
63          *  methods. Example: You may want to filter out instances of jQuery or some
64          *  other constructor. Otherwise "missingTests" will include all methods that
65          *  were not called from that instance.
66          */
67         function CompletenessTest( masterVariable, ignoreFn ) {
68                 var warn,
69                         that = this;
71                 // Keep track in these objects. Keyed by strings with the
72                 // method names (ie. 'my.foo', 'my.bar', etc.) values are boolean true.
73                 this.injectionTracker = {};
74                 this.methodCallTracker = {};
75                 this.missingTests = {};
77                 this.ignoreFn = ignoreFn === undefined ? function () { return false; } : ignoreFn;
79                 // Lazy limit in case something weird happends (like recurse (part of) ourself).
80                 this.lazyLimit = 2000;
81                 this.lazyCounter = 0;
83                 // Bind begin and end to QUnit.
84                 QUnit.begin( function () {
85                         // Suppress warnings (e.g. deprecation notices for accessing the properties)
86                         warn = mw.log.warn;
87                         mw.log.warn = $.noop;
89                         that.walkTheObject( masterVariable, null, masterVariable, [] );
90                         log( 'CompletenessTest/walkTheObject', that );
92                         // Restore warnings
93                         mw.log.warn = warn;
94                         warn = undefined;
95                 } );
97                 QUnit.done( function () {
98                         var toolbar, testResults, cntTotal, cntCalled, cntMissing;
100                         that.populateMissingTests();
101                         log( 'CompletenessTest/populateMissingTests', that );
103                         cntTotal = util.keys( that.injectionTracker ).length;
104                         cntCalled = util.keys( that.methodCallTracker ).length;
105                         cntMissing = util.keys( that.missingTests ).length;
107                         function makeTestResults( blob, title, style ) {
108                                 var elOutputWrapper, elTitle, elContainer, elList, elFoot;
110                                 elTitle = document.createElement( 'strong' );
111                                 elTitle.textContent = title || 'Values';
113                                 elList = document.createElement( 'ul' );
114                                 util.each( blob, function ( key ) {
115                                         var elItem = document.createElement( 'li' );
116                                         elItem.textContent = key;
117                                         elList.appendChild( elItem );
118                                 } );
120                                 elFoot = document.createElement( 'p' );
121                                 elFoot.innerHTML = '<em>&mdash; CompletenessTest</em>';
123                                 elContainer = document.createElement( 'div' );
124                                 elContainer.appendChild( elTitle );
125                                 elContainer.appendChild( elList );
126                                 elContainer.appendChild( elFoot );
128                                 elOutputWrapper = document.getElementById( 'qunit-completenesstest' );
129                                 if ( !elOutputWrapper ) {
130                                         elOutputWrapper = document.createElement( 'div' );
131                                         elOutputWrapper.id = 'qunit-completenesstest';
132                                 }
133                                 elOutputWrapper.appendChild( elContainer );
135                                 util.each( style, function ( key, value ) {
136                                         elOutputWrapper.style[ key ] = value;
137                                 } );
138                                 return elOutputWrapper;
139                         }
141                         if ( cntMissing === 0 ) {
142                                 // Good
143                                 testResults = makeTestResults(
144                                         {},
145                                         'Detected calls to ' + cntCalled + '/' + cntTotal + ' methods. No missing tests!',
146                                         {
147                                                 backgroundColor: '#D2E0E6',
148                                                 color: '#366097',
149                                                 paddingTop: '1em',
150                                                 paddingRight: '1em',
151                                                 paddingBottom: '1em',
152                                                 paddingLeft: '1em'
153                                         }
154                                 );
155                         } else {
156                                 // Bad
157                                 testResults = makeTestResults(
158                                         that.missingTests,
159                                         'Detected calls to ' + cntCalled + '/' + cntTotal + ' methods. ' + cntMissing + ' methods not covered:',
160                                         {
161                                                 backgroundColor: '#EE5757',
162                                                 color: 'black',
163                                                 paddingTop: '1em',
164                                                 paddingRight: '1em',
165                                                 paddingBottom: '1em',
166                                                 paddingLeft: '1em'
167                                         }
168                                 );
169                         }
171                         toolbar = document.getElementById( 'qunit-testrunner-toolbar' );
172                         if ( toolbar ) {
173                                 toolbar.insertBefore( testResults, toolbar.firstChild );
174                         }
175                 } );
177                 return this;
178         }
180         /* Public methods */
181         CompletenessTest.fn = CompletenessTest.prototype = {
183                 /**
184                  * CompletenessTest.fn.walkTheObject
185                  *
186                  * This function recursively walks through the given object, calling itself as it goes.
187                  * Depending on the action it either injects our listener into the methods, or
188                  * reads from our tracker and records which methods have not been called by the test suite.
189                  *
190                  * @param {Mixed} currObj The variable to check (initially an object,
191                  *  further down it could be anything).
192                  * @param {string|null} currName Name of the given object member (Initially this is null).
193                  * @param {Object} masterVariable Throughout our interation, always keep track of the master/root.
194                  *  Initially this is the same as currVar.
195                  * @param {Array} parentPathArray Array of names that indicate our breadcrumb path starting at
196                  *  masterVariable. Not including currName.
197                  */
198                 walkTheObject: function ( currObj, currName, masterVariable, parentPathArray ) {
199                         var key, currVal, type,
200                                 ct = this,
201                                 currPathArray = parentPathArray;
203                         if ( currName ) {
204                                 currPathArray.push( currName );
205                                 currVal = currObj[ currName ];
206                         } else {
207                                 currName = '(root)';
208                                 currVal = currObj;
209                         }
211                         type = util.type( currVal );
213                         // Hard ignores
214                         if ( this.ignoreFn( currVal, this, currPathArray ) ) {
215                                 return;
216                         }
218                         // Handle the lazy limit
219                         this.lazyCounter++;
220                         if ( this.lazyCounter > this.lazyLimit ) {
221                                 log( 'CompletenessTest.fn.walkTheObject> Limit reached: ' + this.lazyCounter, currPathArray );
222                                 return;
223                         }
225                         // Functions
226                         if ( type === 'function' ) {
227                                 // Don't put a spy in constructor functions as it messes with
228                                 // instanceof etc.
229                                 if ( !currVal.prototype || util.isEmptyObject( currVal.prototype ) ) {
230                                         this.injectionTracker[ currPathArray.join( '.' ) ] = true;
231                                         this.injectCheck( currObj, currName, function () {
232                                                 ct.methodCallTracker[ currPathArray.join( '.' ) ] = true;
233                                         } );
234                                 }
235                         }
237                         // Recursively. After all, this is the *completeness* test
238                         // This also traverses static properties and the prototype of a constructor
239                         if ( type === 'object' || type === 'function' ) {
240                                 for ( key in currVal ) {
241                                         if ( hasOwn.call( currVal, key ) ) {
242                                                 this.walkTheObject( currVal, key, masterVariable, currPathArray.slice() );
243                                         }
244                                 }
245                         }
246                 },
248                 populateMissingTests: function () {
249                         var ct = this;
250                         util.each( ct.injectionTracker, function ( key ) {
251                                 ct.hasTest( key );
252                         } );
253                 },
255                 /**
256                  * CompletenessTest.fn.hasTest
257                  *
258                  * Checks if the given method name (ie. 'my.foo.bar')
259                  * was called during the test suite (as far as the tracker knows).
260                  * If not it adds it to missingTests.
261                  *
262                  * @param {string} fnName
263                  * @return {boolean}
264                  */
265                 hasTest: function ( fnName ) {
266                         if ( !( fnName in this.methodCallTracker ) ) {
267                                 this.missingTests[ fnName ] = true;
268                                 return false;
269                         }
270                         return true;
271                 },
273                 /**
274                  * CompletenessTest.fn.injectCheck
275                  *
276                  * Injects a function (such as a spy that updates methodCallTracker when
277                  * it's called) inside another function.
278                  *
279                  * @param {Object} obj The object into which `injectFn` will be inserted
280                  * @param {Array} key The key by which `injectFn` will be known in `obj`; if this already
281                  *   exists, a wrapper will first call `injectFn` and then the original `obj[key]` function.
282                  * @param {Function} injectFn The function to insert
283                  */
284                 injectCheck: function ( obj, key, injectFn ) {
285                         var spy,
286                                 val = obj[ key ];
288                         spy = function () {
289                                 injectFn();
290                                 return val.apply( this, arguments );
291                         };
293                         // Make the spy inherit from the original so that its static methods are also
294                         // visible in the spy (e.g. when we inject a check into mw.log, mw.log.warn
295                         // must remain accessible).
296                         spy.__proto__ = val;
298                         // Objects are by reference, members (unless objects) are not.
299                         obj[ key ] = spy;
300                 }
301         };
303         /* Expose */
304         window.CompletenessTest = CompletenessTest;
306 }( mediaWiki, jQuery ) );