[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/resources/src/jquery/ -> jquery.qunit.completenessTest.js (source)

   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>&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 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 ) );


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1