[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
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 * @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; 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>— 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 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; 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 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 }, 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 masterVariable {Object} 279 * @param objectPathArray {Array} 280 * @param injectFn {Function} 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 /*jshint proto:true */ 295 spy.__proto__ = val; 296 297 // Objects are by reference, members (unless objects) are not. 298 obj[ key ] = spy; 299 } 300 }; 301 302 /* Expose */ 303 window.CompletenessTest = CompletenessTest; 304 305 }( mediaWiki, jQuery ) );
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |