2011-06-10 22:15:14 +00:00
|
|
|
/**
|
|
|
|
|
* jQuery QUnit CompletenessTest 0.3
|
|
|
|
|
*
|
|
|
|
|
* Tests the completeness of test suites for object oriented javascript
|
|
|
|
|
* libraries. Written to be used in enviroments with jQuery and QUnit.
|
|
|
|
|
* Requires jQuery 1.5.2 or higher.
|
|
|
|
|
*
|
2011-08-16 13:33:22 +00:00
|
|
|
* Globals: jQuery, QUnit, console.log
|
2011-06-10 22:15:14 +00:00
|
|
|
*
|
|
|
|
|
* Built for and tested with:
|
|
|
|
|
* - Safari 5
|
|
|
|
|
* - Firefox 4
|
|
|
|
|
*
|
|
|
|
|
* @author Timo Tijhof, 2011
|
|
|
|
|
*/
|
2011-08-16 13:33:22 +00:00
|
|
|
( function( $ ) {
|
2011-06-10 22:15:14 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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.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 = 1000;
|
|
|
|
|
this.lazyCounter = 0;
|
|
|
|
|
|
|
|
|
|
var that = this;
|
2011-06-11 00:24:36 +00:00
|
|
|
|
|
|
|
|
// Bind begin and end to QUnit.
|
2011-06-10 22:15:14 +00:00
|
|
|
QUnit.begin = function(){
|
|
|
|
|
that.checkTests( null, masterVariable, masterVariable, [], CompletenessTest.ACTION_INJECT );
|
|
|
|
|
};
|
2011-06-11 00:24:36 +00:00
|
|
|
|
2011-06-10 22:15:14 +00:00
|
|
|
QUnit.done = function(){
|
|
|
|
|
that.checkTests( null, masterVariable, masterVariable, [], CompletenessTest.ACTION_CHECK );
|
|
|
|
|
console.log( 'CompletenessTest.ACTION_CHECK', that );
|
|
|
|
|
|
2011-06-11 00:24:36 +00:00
|
|
|
// Build HTML representing the outcome from CompletenessTest
|
|
|
|
|
// And insert it into the header.
|
2011-06-10 22:15:14 +00:00
|
|
|
|
|
|
|
|
var makeList = function( blob, title, style ) {
|
|
|
|
|
title = title || 'Values';
|
2011-06-11 00:24:36 +00:00
|
|
|
var html = '<strong>' + mw.html.escape(title) + '</strong>';
|
2011-06-10 22:15:14 +00:00
|
|
|
$.each( blob, function( key ) {
|
|
|
|
|
html += '<br />' + mw.html.escape(key);
|
|
|
|
|
});
|
2011-06-11 00:24:36 +00:00
|
|
|
html += '<br /><br /><em>— CompletenessTest</em>';
|
2011-07-10 19:10:51 +00:00
|
|
|
var $oldResult = $( '#qunit-completenesstest' ),
|
|
|
|
|
$result = $oldResult.length ? $oldResult : $( '<div id="qunit-completenesstest"></div>' );
|
|
|
|
|
return $result.css( style ).html( html );
|
2011-06-10 22:15:14 +00:00
|
|
|
};
|
2011-06-11 00:24:36 +00:00
|
|
|
|
|
|
|
|
if ( $.isEmptyObject( that.missingTests ) ) {
|
|
|
|
|
// Good
|
|
|
|
|
var $testResults = makeList(
|
|
|
|
|
{ 'No missing tests!': true },
|
|
|
|
|
'missingTests',
|
|
|
|
|
{
|
|
|
|
|
background: '#D2E0E6',
|
|
|
|
|
color: '#366097',
|
|
|
|
|
padding: '1em'
|
|
|
|
|
}
|
|
|
|
|
);
|
2011-06-10 22:15:14 +00:00
|
|
|
} else {
|
2011-06-11 00:24:36 +00:00
|
|
|
// Bad
|
|
|
|
|
var $testResults = makeList(
|
|
|
|
|
that.missingTests,
|
|
|
|
|
'missingTests',
|
|
|
|
|
{
|
|
|
|
|
background: '#EE5757',
|
|
|
|
|
color: 'black',
|
|
|
|
|
padding: '1em'
|
|
|
|
|
}
|
|
|
|
|
);
|
2011-06-10 22:15:14 +00:00
|
|
|
}
|
2011-06-11 00:24:36 +00:00
|
|
|
|
|
|
|
|
$( '#qunit-testrunner-toolbar' ).prepend( $testResults );
|
2011-06-10 22:15:14 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return this;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* Static members */
|
|
|
|
|
CompletenessTest.ACTION_INJECT = 500;
|
|
|
|
|
CompletenessTest.ACTION_CHECK = 501;
|
|
|
|
|
|
|
|
|
|
/* Public methods */
|
|
|
|
|
CompletenessTest.fn = CompletenessTest.prototype = {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* CompletenessTest.fn.checkTests
|
|
|
|
|
*
|
2011-06-11 00:24:36 +00:00
|
|
|
* 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)
|
2011-06-10 22:15:14 +00:00
|
|
|
*/
|
|
|
|
|
checkTests: function( currName, currVar, masterVariable, parentPathArray, action ) {
|
|
|
|
|
|
|
|
|
|
// Handle the lazy limit
|
|
|
|
|
this.lazyCounter++;
|
|
|
|
|
if ( this.lazyCounter > this.lazyLimit ) {
|
|
|
|
|
console.log( 'CompletenessTest.fn.checkTests> Limit reached: ' + this.lazyCounter );
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var type = $.type( currVar ),
|
|
|
|
|
that = this;
|
|
|
|
|
|
|
|
|
|
// Hard ignores
|
|
|
|
|
if ( this.ignoreFn( currVar, that, parentPathArray ) ) {
|
2011-06-11 00:24:36 +00:00
|
|
|
return null;
|
2011-06-10 22:15:14 +00:00
|
|
|
|
|
|
|
|
// Functions
|
|
|
|
|
} else if ( type === 'function' ) {
|
|
|
|
|
|
|
|
|
|
/* CHECK MODE */
|
|
|
|
|
|
|
|
|
|
if ( action === CompletenessTest.ACTION_CHECK ) {
|
|
|
|
|
|
|
|
|
|
if ( !currVar.prototype || $.isEmptyObject( currVar.prototype ) ) {
|
|
|
|
|
|
|
|
|
|
that.hasTest( parentPathArray.join( '.' ) );
|
|
|
|
|
|
|
|
|
|
// We don't support checking object constructors yet...
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
// ...the prototypes are fine tho
|
|
|
|
|
$.each( currVar.prototype, function( key, value ) {
|
Implement mw.Title in core
* Based on UploadWizard/resources/mw.Title.js
* Refactored to use local scope and prototypes instead of re-declaring them per-instance in the private scope through 'this' (re-use by reference, faster instantiation and performance)
* Fix potential ReferenceError in the check for wgArticlePath (inline if statements will fail for undeclared variables, needs typeof undefined check). Using mw.config instead to avoid this problem.
* The following two methods were not ported from UploadWizard because they were or became redundant and/or merged with another method:
-- setNameText (redundant with the improved setName)
-- setPrefix (redundant wit the improved setNamespace)
* Ported all jasmine tests to QUnit. Left them exactly the same to make sure it's compatible with UploadWizard. Perhaps I'll expand or adjust the suite later to be less file-specific, but for now make letting this revision go through TestSwarm to be sure it's compatible and behaves exactly the same.
* Added getName() method instead, replacing direct access to '_name' This in order to check for wgCaseSensitiveNamespaces (bug 29441; r90234)
-- Prevents breakages on wiktionary and other wikis with case sensitivity. ie. on a Wiktionary:
new mw.Title('apple').toString()
> "Apple"
-- This fix will make it return 'apple' instead of 'Apple' (that is, if 0 is in wgCaseSensitiveNamespaces).
* There used to be a strip-bug in scenarios where a namespace-like string appears inside of a title. Imagine pagename: "Category:Wikipedia:Foo bar" (exist on several wikis; NS_CATEGORY= 14)
new mw.Title( 'Wikipedia:Foo bar', 14 ).toString()
> "Category:Foo_bar" // Stripped "Wikipedia:" !!
In order to fix this:
-- Removed assumption that every title has a namespace prefix. UploadWizard/mw.Title has one initialization RegExp (which was ported as-is to "setAll"). In addition there is now a "setNameAndExtension" method (which doesn't extract or set the namespace). Now the above case:
new mw.Title( 'Wikipedia:Foo bar', 14 ).toString()
> "Category:Wikipedia_Foo_bar" // Better, but now the colon is gone..
-- In order to fix that, "\x3a" was removed from the clean() function. Colons are valid in MediaWiki titles, no need to escape.
new mw.Title( 'Wikipedia:Foo bar', 14 ).toString()
> "Category:Wikipedia:Foo_bar" // Yay!
* Last but not least, another little bug fixed due to the previous point. It also fixed a thrown exception in case a colon is part of the title in the main namespace (not rare for movies and books):
new mw.Title( 'The Wiki: Awesomeness')
> Error: mw.Title> Unrecognized canonical namespace: the_wiki
This exception is thrown from setNamespace(). That exception would make sense if setNamespace() was called by the user direcly, but when called from setAll() it should gracefully fallback by putting the prefix in the name instead and assuming NS_MAIN (just like the server side does). To achieve this I added a try/catch around setAll() and fallback to the new setNameAndExtension().
* Passes JSHint.
* Additional feature: exists(). Return true/false if known, otherwise null. Extensions can populate this for titles they are interested in and the front-end can construct url's and UI elements with correct redlink-status. Gadgets can populate it as well but that would require an API-request to get the information. A bit of a stub for later use, although I think it works fine.
* Bugfix in jquery.qunit.completenessTest.js (first triggered by the introduction of mw.Title). Don't traverse the 'constructor' property (recursive loop, ouch!)
---
(bug 29397) Implement mw.Title module in core
2011-06-18 09:17:09 +00:00
|
|
|
if ( key === 'constructor' ) return;
|
2011-06-10 22:15:14 +00:00
|
|
|
|
2011-08-16 13:33:22 +00:00
|
|
|
// Clone and break reference to parentPathArray
|
2011-06-11 00:24:36 +00:00
|
|
|
var tmpPathArray = $.extend( [], parentPathArray );
|
|
|
|
|
tmpPathArray.push( 'prototype' ); tmpPathArray.push( key );
|
2011-06-10 22:15:14 +00:00
|
|
|
|
|
|
|
|
that.hasTest( tmpPathArray.join( '.' ) );
|
|
|
|
|
} );
|
|
|
|
|
}
|
|
|
|
|
|
2011-06-11 00:24:36 +00:00
|
|
|
/* INJECT MODE */
|
2011-06-10 22:15:14 +00:00
|
|
|
|
|
|
|
|
} else if ( action === CompletenessTest.ACTION_INJECT ) {
|
|
|
|
|
|
|
|
|
|
if ( !currVar.prototype || $.isEmptyObject( currVar.prototype ) ) {
|
|
|
|
|
|
|
|
|
|
// Inject check
|
2011-08-16 13:33:22 +00:00
|
|
|
that.injectCheck( masterVariable, parentPathArray, function() {
|
2011-06-10 22:15:14 +00:00
|
|
|
that.methodCallTracker[ parentPathArray.join( '.' ) ] = true;
|
2011-08-16 13:33:22 +00:00
|
|
|
} );
|
2011-06-10 22:15:14 +00:00
|
|
|
|
|
|
|
|
// We don't support checking object constructors yet...
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
// ... the prototypes are fine tho
|
|
|
|
|
$.each( currVar.prototype, function( key, value ) {
|
Implement mw.Title in core
* Based on UploadWizard/resources/mw.Title.js
* Refactored to use local scope and prototypes instead of re-declaring them per-instance in the private scope through 'this' (re-use by reference, faster instantiation and performance)
* Fix potential ReferenceError in the check for wgArticlePath (inline if statements will fail for undeclared variables, needs typeof undefined check). Using mw.config instead to avoid this problem.
* The following two methods were not ported from UploadWizard because they were or became redundant and/or merged with another method:
-- setNameText (redundant with the improved setName)
-- setPrefix (redundant wit the improved setNamespace)
* Ported all jasmine tests to QUnit. Left them exactly the same to make sure it's compatible with UploadWizard. Perhaps I'll expand or adjust the suite later to be less file-specific, but for now make letting this revision go through TestSwarm to be sure it's compatible and behaves exactly the same.
* Added getName() method instead, replacing direct access to '_name' This in order to check for wgCaseSensitiveNamespaces (bug 29441; r90234)
-- Prevents breakages on wiktionary and other wikis with case sensitivity. ie. on a Wiktionary:
new mw.Title('apple').toString()
> "Apple"
-- This fix will make it return 'apple' instead of 'Apple' (that is, if 0 is in wgCaseSensitiveNamespaces).
* There used to be a strip-bug in scenarios where a namespace-like string appears inside of a title. Imagine pagename: "Category:Wikipedia:Foo bar" (exist on several wikis; NS_CATEGORY= 14)
new mw.Title( 'Wikipedia:Foo bar', 14 ).toString()
> "Category:Foo_bar" // Stripped "Wikipedia:" !!
In order to fix this:
-- Removed assumption that every title has a namespace prefix. UploadWizard/mw.Title has one initialization RegExp (which was ported as-is to "setAll"). In addition there is now a "setNameAndExtension" method (which doesn't extract or set the namespace). Now the above case:
new mw.Title( 'Wikipedia:Foo bar', 14 ).toString()
> "Category:Wikipedia_Foo_bar" // Better, but now the colon is gone..
-- In order to fix that, "\x3a" was removed from the clean() function. Colons are valid in MediaWiki titles, no need to escape.
new mw.Title( 'Wikipedia:Foo bar', 14 ).toString()
> "Category:Wikipedia:Foo_bar" // Yay!
* Last but not least, another little bug fixed due to the previous point. It also fixed a thrown exception in case a colon is part of the title in the main namespace (not rare for movies and books):
new mw.Title( 'The Wiki: Awesomeness')
> Error: mw.Title> Unrecognized canonical namespace: the_wiki
This exception is thrown from setNamespace(). That exception would make sense if setNamespace() was called by the user direcly, but when called from setAll() it should gracefully fallback by putting the prefix in the name instead and assuming NS_MAIN (just like the server side does). To achieve this I added a try/catch around setAll() and fallback to the new setNameAndExtension().
* Passes JSHint.
* Additional feature: exists(). Return true/false if known, otherwise null. Extensions can populate this for titles they are interested in and the front-end can construct url's and UI elements with correct redlink-status. Gadgets can populate it as well but that would require an API-request to get the information. A bit of a stub for later use, although I think it works fine.
* Bugfix in jquery.qunit.completenessTest.js (first triggered by the introduction of mw.Title). Don't traverse the 'constructor' property (recursive loop, ouch!)
---
(bug 29397) Implement mw.Title module in core
2011-06-18 09:17:09 +00:00
|
|
|
if ( key === 'constructor' ) return;
|
2011-06-10 22:15:14 +00:00
|
|
|
|
2011-08-16 13:33:22 +00:00
|
|
|
// Clone and break reference to parentPathArray
|
2011-06-11 00:24:36 +00:00
|
|
|
var tmpPathArray = $.extend( [], parentPathArray );
|
|
|
|
|
tmpPathArray.push( 'prototype' ); tmpPathArray.push( key );
|
2011-06-10 22:15:14 +00:00
|
|
|
|
|
|
|
|
that.checkTests( key, value, masterVariable, tmpPathArray, action );
|
|
|
|
|
} );
|
|
|
|
|
}
|
|
|
|
|
|
2011-06-11 00:24:36 +00:00
|
|
|
}
|
2011-06-10 22:15:14 +00:00
|
|
|
|
2011-08-16 13:33:22 +00:00
|
|
|
// Recursively. After all, this *is* the completeness test
|
2011-06-10 22:15:14 +00:00
|
|
|
} else if ( type === 'object' ) {
|
|
|
|
|
|
|
|
|
|
$.each( currVar, function( key, value ) {
|
|
|
|
|
|
2011-08-16 13:33:22 +00:00
|
|
|
// Clone and break reference to parentPathArray
|
2011-06-11 00:24:36 +00:00
|
|
|
var tmpPathArray = $.extend( [], parentPathArray );
|
|
|
|
|
tmpPathArray.push( key );
|
2011-06-10 22:15:14 +00:00
|
|
|
|
|
|
|
|
that.checkTests( key, value, masterVariable, tmpPathArray, action );
|
|
|
|
|
|
|
|
|
|
} );
|
|
|
|
|
|
2011-06-11 00:24:36 +00:00
|
|
|
}
|
2011-06-10 22:15:14 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* CompletenessTest.fn.hasTest
|
|
|
|
|
*
|
2011-06-11 00:24:36 +00:00
|
|
|
* Checks if the given method name (ie. 'my.foo.bar')
|
|
|
|
|
* was called during the test suite (as far as the tracker knows).
|
2011-08-16 13:33:22 +00:00
|
|
|
* If not it adds it to missingTests.
|
2011-06-11 00:24:36 +00:00
|
|
|
*
|
2011-06-10 22:15:14 +00:00
|
|
|
* @param fnName {String}
|
2011-06-11 00:24:36 +00:00
|
|
|
* @return {Boolean}
|
2011-06-10 22:15:14 +00:00
|
|
|
*/
|
|
|
|
|
hasTest: function( fnName ) {
|
2011-08-16 13:33:22 +00:00
|
|
|
if ( !( fnName in this.methodCallTracker ) ) {
|
2011-06-10 22:15:14 +00:00
|
|
|
this.missingTests[fnName] = true;
|
2011-06-11 00:24:36 +00:00
|
|
|
return false;
|
2011-06-10 22:15:14 +00:00
|
|
|
}
|
2011-06-11 00:24:36 +00:00
|
|
|
return true;
|
2011-06-10 22:15:14 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* CompletenessTest.fn.injectCheck
|
|
|
|
|
*
|
2011-06-11 00:24:36 +00:00
|
|
|
* Injects a function (such as a spy that updates methodCallTracker when
|
|
|
|
|
* it's called) inside another function.
|
|
|
|
|
*
|
2011-06-10 22:15:14 +00:00
|
|
|
* @param masterVariable {Object}
|
|
|
|
|
* @param objectPathArray {Array}
|
|
|
|
|
* @param injectFn {Function}
|
|
|
|
|
*/
|
2011-06-11 00:24:36 +00:00
|
|
|
injectCheck: function( masterVariable, objectPathArray, injectFn ) {
|
2011-06-10 22:15:14 +00:00
|
|
|
var prev,
|
|
|
|
|
curr = masterVariable,
|
|
|
|
|
lastMember;
|
|
|
|
|
|
2011-08-16 13:33:22 +00:00
|
|
|
$.each( objectPathArray, function( i, memberName ) {
|
2011-06-10 22:15:14 +00:00
|
|
|
prev = curr;
|
|
|
|
|
curr = prev[memberName];
|
|
|
|
|
lastMember = memberName;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Objects are by reference, members (unless objects) are not.
|
2011-08-16 13:33:22 +00:00
|
|
|
prev[lastMember] = function() {
|
2011-06-10 22:15:14 +00:00
|
|
|
injectFn();
|
2011-06-11 00:24:36 +00:00
|
|
|
return curr.apply( this, arguments );
|
2011-06-10 22:15:14 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
window.CompletenessTest = CompletenessTest;
|
|
|
|
|
|
2011-08-16 13:33:22 +00:00
|
|
|
} )( jQuery );
|