Localisation updates from https://translatewiki.net.
[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 /* eslint-env qunit */
16 ( function ( mw, $ ) {
17 'use strict';
18
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 () {};
24
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 };
52
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;
70
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 = {};
76
77 this.ignoreFn = ignoreFn === undefined ? function () { return false; } : ignoreFn;
78
79 // Lazy limit in case something weird happends (like recurse (part of) ourself).
80 this.lazyLimit = 2000;
81 this.lazyCounter = 0;
82
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;
88
89 that.walkTheObject( masterVariable, null, masterVariable, [] );
90 log( 'CompletenessTest/walkTheObject', that );
91
92 // Restore warnings
93 mw.log.warn = warn;
94 warn = undefined;
95 } );
96
97 QUnit.done( function () {
98 var toolbar, testResults, cntTotal, cntCalled, cntMissing;
99
100 that.populateMissingTests();
101 log( 'CompletenessTest/populateMissingTests', that );
102
103 cntTotal = util.keys( that.injectionTracker ).length;
104 cntCalled = util.keys( that.methodCallTracker ).length;
105 cntMissing = util.keys( that.missingTests ).length;
106
107 function makeTestResults( blob, title, style ) {
108 var elOutputWrapper, elTitle, elContainer, elList, elFoot;
109
110 elTitle = document.createElement( 'strong' );
111 elTitle.textContent = title || 'Values';
112
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 } );
119
120 elFoot = document.createElement( 'p' );
121 elFoot.innerHTML = '<em>&mdash; CompletenessTest</em>';
122
123 elContainer = document.createElement( 'div' );
124 elContainer.appendChild( elTitle );
125 elContainer.appendChild( elList );
126 elContainer.appendChild( elFoot );
127
128 elOutputWrapper = document.getElementById( 'qunit-completenesstest' );
129 if ( !elOutputWrapper ) {
130 elOutputWrapper = document.createElement( 'div' );
131 elOutputWrapper.id = 'qunit-completenesstest';
132 }
133 elOutputWrapper.appendChild( elContainer );
134
135 util.each( style, function ( key, value ) {
136 elOutputWrapper.style[ key ] = value;
137 } );
138 return elOutputWrapper;
139 }
140
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 }
170
171 toolbar = document.getElementById( 'qunit-testrunner-toolbar' );
172 if ( toolbar ) {
173 toolbar.insertBefore( testResults, toolbar.firstChild );
174 }
175 } );
176
177 return this;
178 }
179
180 /* Public methods */
181 CompletenessTest.fn = CompletenessTest.prototype = {
182
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;
202
203 if ( currName ) {
204 currPathArray.push( currName );
205 currVal = currObj[ currName ];
206 } else {
207 currName = '(root)';
208 currVal = currObj;
209 }
210
211 type = util.type( currVal );
212
213 // Hard ignores
214 if ( this.ignoreFn( currVal, this, currPathArray ) ) {
215 return;
216 }
217
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 }
224
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 }
236
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 },
247
248 populateMissingTests: function () {
249 var ct = this;
250 util.each( ct.injectionTracker, function ( key ) {
251 ct.hasTest( key );
252 } );
253 },
254
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 },
272
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 ];
287
288 spy = function () {
289 injectFn();
290 return val.apply( this, arguments );
291 };
292
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;
297
298 // Objects are by reference, members (unless objects) are not.
299 obj[ key ] = spy;
300 }
301 };
302
303 /* Expose */
304 window.CompletenessTest = CompletenessTest;
305
306 }( mediaWiki, jQuery ) );