wiki.techinc.nl/resources/jquery/jquery.qunit.completenessTest.js
Timo Tijhof 089c58d232 jshint: resources/jquery/*
* .jshintrc: Updated to include more strict options that match
  our code conventions.
  Also separated into 3 groups:
  - stricter (curly, eqeqeq etc.)
  - laxer (smarttabs, laxbreak)
  - envrionment (browser)

* .jshintignore: Updated to include more third-party/upstream files
   that should not be linted.

* Most of it is just routine cleanup, a few notable points:

 - jquery.autoEllipsis: Removed unused variable $protectedText.

 - jquery.arrowSteps.js: Remove <!-- --> and language="javascript"
   that hasn't been needed for almost a decade.

 - jquery.byteLimit: Use dashToCamel key for .data(), this already
   happens internally in jQuery data(), since data storage should use
   keys that are usable as identifiers. The dashed versions are to
   populate these from data-attribute-names, which then becomes
   data.attributeNames. jQuery data() takes both forms as
   convenience.

 - jquery.client.js: To avoid a rewrite of it, allowing unexpected
   assignments (boss) and eval (evil) in the functions that use that.
   Left as it is for now, could use a rewrite later.

 - jquery.color.js: Tolerate unexpected assignment for now (boss).
   Left as it is for now, should perhaps be refactored later.
   Also re-ordered per jshint/jslint to put definition before
   invocation. This option can be disabled, but then it doesn't
   warn for invoking undefined functions (or typos) at all.

 - jquery.expandableField.js: Remove empty switch/case.

 - jquery.localize.js: Alias mw global.

 - jquery.suggestions.js: Use e.which; jQuery.Event normalizes
   e.keyCode etc.

 - jquery.tablesorter.js: Alias mw global.

 - jquery.textSelection.js: Fix leakage of variable in global scope
   of var "i" and "j".

 - mediawiki.util.test.js: Fixed implied global `pCustom`.

* Review with -w for your own sanity.

Change-Id: Ia972f79539a96a38357ec4e92b0b6e38cc301681
2012-07-15 11:13:20 -04:00

362 lines
10 KiB
JavaScript

/**
* jQuery QUnit CompletenessTest 0.4
*
* Tests the completeness of test suites for object oriented javascript
* libraries. Written to be used in environments with jQuery and QUnit.
* Requires jQuery 1.7.2 or higher.
*
* Built for and tested with:
* - Chrome 19
* - Firefox 4
* - Safari 5
*
* @author Timo Tijhof, 2011-2012
*/
/*global jQuery, QUnit */
/*jshint eqeqeq:false, eqnull:false, forin:false */
( function ( $ ) {
"use strict";
var util,
hasOwn = Object.prototype.hasOwnProperty,
log = (window.console && window.console.log)
? function () { return window.console.log.apply(window.console, arguments); }
: function () {};
// Simplified version of a few jQuery methods, except that they don't
// call other jQuery methods. Required to be able to run the CompletenessTest
// on jQuery itself as well.
util = {
keys: Object.keys || function ( object ) {
var key, keys = [];
for ( key in object ) {
if ( hasOwn.call( object, key ) ) {
keys.push( key );
}
}
return keys;
},
extend: function () {
var options, name, src, copy,
target = arguments[0] || {},
i = 1,
length = arguments.length;
for ( ; i < length; i++ ) {
options = arguments[ i ];
// Only deal with non-null/undefined values
if ( options !== null && options !== undefined ) {
// Extend the base object
for ( name in options ) {
src = target[ name ];
copy = options[ name ];
// Prevent never-ending loop
if ( target === copy ) {
continue;
}
if ( copy !== undefined ) {
target[ name ] = copy;
}
}
}
}
// Return the modified object
return target;
},
each: function ( object, callback ) {
var name;
for ( name in object ) {
if ( callback.call( object[ name ], name, object[ name ] ) === false ) {
break;
}
}
},
// $.type and $.isEmptyObject are safe as is, they don't call
// other $.* methods. Still need to be derefenced into `util`
// since the CompletenessTest will overload them with spies.
type: $.type,
isEmptyObject: $.isEmptyObject
};
/**
* CompletenessTest
* @constructor
*
* @example
* var myTester = new CompletenessTest( myLib );
* @param masterVariable {Object} The root variable that contains all object
* members. CompletenessTest will recursively traverse objects and keep track
* of all methods.
* @param ignoreFn {Function} Optionally pass a function to filter out certain
* methods. Example: You may want to filter out instances of jQuery or some
* other constructor. Otherwise "missingTests" will include all methods that
* were not called from that instance.
*/
var CompletenessTest = function ( masterVariable, ignoreFn ) {
// Keep track in these objects. Keyed by strings with the
// method names (ie. 'my.foo', 'my.bar', etc.) values are boolean true.
this.injectionTracker = {};
this.methodCallTracker = {};
this.missingTests = {};
this.ignoreFn = undefined === ignoreFn ? function () { return false; } : ignoreFn;
// Lazy limit in case something weird happends (like recurse (part of) ourself).
this.lazyLimit = 2000;
this.lazyCounter = 0;
var that = this;
// Bind begin and end to QUnit.
QUnit.begin( function () {
that.walkTheObject( null, masterVariable, masterVariable, [], CompletenessTest.ACTION_INJECT );
log( 'CompletenessTest/walkTheObject/ACTION_INJECT', that );
});
QUnit.done( function () {
that.populateMissingTests();
log( 'CompletenessTest/populateMissingTests', that );
var toolbar, testResults, cntTotal, cntCalled, cntMissing;
cntTotal = util.keys( that.injectionTracker ).length;
cntCalled = util.keys( that.methodCallTracker ).length;
cntMissing = util.keys( that.missingTests ).length;
function makeTestResults( blob, title, style ) {
var elOutputWrapper, elTitle, elContainer, elList, elFoot;
elTitle = document.createElement( 'strong' );
elTitle.textContent = title || 'Values';
elList = document.createElement( 'ul' );
util.each( blob, function ( key ) {
var elItem = document.createElement( 'li' );
elItem.textContent = key;
elList.appendChild( elItem );
});
elFoot = document.createElement( 'p' );
elFoot.innerHTML = '<em>&mdash; CompletenessTest</em>';
elContainer = document.createElement( 'div' );
elContainer.appendChild( elTitle );
elContainer.appendChild( elList );
elContainer.appendChild( elFoot );
elOutputWrapper = document.getElementById( 'qunit-completenesstest' );
if ( !elOutputWrapper ) {
elOutputWrapper = document.createElement( 'div' );
elOutputWrapper.id = 'qunit-completenesstest';
}
elOutputWrapper.appendChild( elContainer );
util.each( style, function ( key, value ) {
elOutputWrapper.style[key] = value;
});
return elOutputWrapper;
}
if ( cntMissing === 0 ) {
// Good
testResults = makeTestResults(
{},
'Detected calls to ' + cntCalled + '/' + cntTotal + ' methods. No missing tests!',
{
backgroundColor: '#D2E0E6',
color: '#366097',
paddingTop: '1em',
paddingRight: '1em',
paddingBottom: '1em',
paddingLeft: '1em'
}
);
} else {
// Bad
testResults = makeTestResults(
that.missingTests,
'Detected calls to ' + cntCalled + '/' + cntTotal + ' methods. ' + cntMissing + ' methods not covered:',
{
backgroundColor: '#EE5757',
color: 'black',
paddingTop: '1em',
paddingRight: '1em',
paddingBottom: '1em',
paddingLeft: '1em'
}
);
}
toolbar = document.getElementById( 'qunit-testrunner-toolbar' );
if ( toolbar ) {
toolbar.insertBefore( testResults, toolbar.firstChild );
}
});
return this;
};
/* Static members */
CompletenessTest.ACTION_INJECT = 500;
CompletenessTest.ACTION_CHECK = 501;
/* Public methods */
CompletenessTest.fn = CompletenessTest.prototype = {
/**
* CompletenessTest.fn.walkTheObject
*
* This function recursively walks through the given object, calling itself as it goes.
* Depending on the action it either injects our listener into the methods, or
* reads from our tracker and records which methods have not been called by the test suite.
*
* @param currName {String|Null} Name of the given object member (Initially this is null).
* @param currVar {mixed} The variable to check (initially an object,
* further down it could be anything).
* @param masterVariable {Object} Throughout our interation, always keep track of the master/root.
* Initially this is the same as currVar.
* @param parentPathArray {Array} Array of names that indicate our breadcrumb path starting at
* masterVariable. Not including currName.
* @param action {Number} What is this function supposed to do (ACTION_INJECT or ACTION_CHECK)
*/
walkTheObject: function ( currName, currVar, masterVariable, parentPathArray, action ) {
var key, value, tmpPathArray,
type = util.type( currVar ),
that = this;
// Hard ignores
if ( this.ignoreFn( currVar, that, parentPathArray ) ) {
return null;
}
// Handle the lazy limit
this.lazyCounter++;
if ( this.lazyCounter > this.lazyLimit ) {
log( 'CompletenessTest.fn.walkTheObject> Limit reached: ' + this.lazyCounter, parentPathArray );
return null;
}
// Functions
if ( type === 'function' ) {
if ( !currVar.prototype || util.isEmptyObject( currVar.prototype ) ) {
if ( action === CompletenessTest.ACTION_INJECT ) {
that.injectionTracker[ parentPathArray.join( '.' ) ] = true;
that.injectCheck( masterVariable, parentPathArray, function () {
that.methodCallTracker[ parentPathArray.join( '.' ) ] = true;
} );
}
// We don't support checking object constructors yet...
// ...we can check the prototypes fine, though.
} else {
if ( action === CompletenessTest.ACTION_INJECT ) {
for ( key in currVar.prototype ) {
if ( hasOwn.call( currVar.prototype, key ) ) {
value = currVar.prototype[key];
if ( key === 'constructor' ) {
continue;
}
// Clone and break reference to parentPathArray
tmpPathArray = util.extend( [], parentPathArray );
tmpPathArray.push( 'prototype' );
tmpPathArray.push( key );
that.walkTheObject( key, value, masterVariable, tmpPathArray, action );
}
}
}
}
}
// Recursively. After all, this is the *completeness* test
if ( type === 'function' || type === 'object' ) {
for ( key in currVar ) {
if ( hasOwn.call( currVar, key ) ) {
value = currVar[key];
// Clone and break reference to parentPathArray
tmpPathArray = util.extend( [], parentPathArray );
tmpPathArray.push( key );
that.walkTheObject( key, value, masterVariable, tmpPathArray, action );
}
}
}
},
populateMissingTests: function () {
var ct = this;
util.each( ct.injectionTracker, function ( key ) {
ct.hasTest( key );
});
},
/**
* CompletenessTest.fn.hasTest
*
* Checks if the given method name (ie. 'my.foo.bar')
* was called during the test suite (as far as the tracker knows).
* If not it adds it to missingTests.
*
* @param fnName {String}
* @return {Boolean}
*/
hasTest: function ( fnName ) {
if ( !( fnName in this.methodCallTracker ) ) {
this.missingTests[fnName] = true;
return false;
}
return true;
},
/**
* CompletenessTest.fn.injectCheck
*
* Injects a function (such as a spy that updates methodCallTracker when
* it's called) inside another function.
*
* @param masterVariable {Object}
* @param objectPathArray {Array}
* @param injectFn {Function}
*/
injectCheck: function ( masterVariable, objectPathArray, injectFn ) {
var i, len, prev, memberName, lastMember,
curr = masterVariable;
// Get the object in question through the path from the master variable,
// We can't pass the value directly because we need to re-define the object
// member and keep references to the parent object, member name and member
// value at all times.
for ( i = 0, len = objectPathArray.length; i < len; i++ ) {
memberName = objectPathArray[i];
prev = curr;
curr = prev[memberName];
lastMember = memberName;
}
// Objects are by reference, members (unless objects) are not.
prev[lastMember] = function () {
injectFn();
return curr.apply( this, arguments );
};
}
};
/* Expose */
window.CompletenessTest = CompletenessTest;
}( jQuery ) );