( function ( $, mw, QUnit ) {
'use strict';
- var addons;
+ var addons, nested;
+
+ /**
+ * Make a safe copy of localEnv:
+ * - Creates a new object that inherits, instead of modifying the original.
+ * This prevents recursion in the event that a test suite stores inherits
+ * hooks object statically and passes it to multiple QUnit.module() calls.
+ * - Supporting QUnit 1.x 'setup' and 'teardown' hooks
+ * (deprecated in QUnit 1.16, removed in QUnit 2).
+ */
+ function makeSafeEnv( localEnv ) {
+ var wrap = localEnv ? Object.create( localEnv ) : {};
+ if ( wrap.setup ) {
+ wrap.beforeEach = wrap.beforeEach || wrap.setup;
+ }
+ if ( wrap.teardown ) {
+ wrap.afterEach = wrap.afterEach || wrap.teardown;
+ }
+ return wrap;
+ }
/**
* Add bogus to url to prevent IE crazy caching
*
* Glue code for nicer integration with QUnit setup/teardown
* Inspired by http://sinonjs.org/releases/sinon-qunit-1.0.0.js
- * Fixes:
- * - Work properly with asynchronous QUnit by using module setup/teardown
- * instead of synchronously wrapping QUnit.test.
*/
sinon.assert.fail = function ( msg ) {
QUnit.assert.ok( false, msg );
useFakeTimers: false,
useFakeServer: false
};
+ // Extend QUnit.module with:
+ // - Add support for QUnit 1.x 'setup' and 'teardown' hooks
+ // - Add a Sinon sandbox to the test context.
+ // - Add a test fixture to the test context.
( function () {
var orgModule = QUnit.module;
-
QUnit.module = function ( name, localEnv, executeNow ) {
- if ( QUnit.config.moduleStack.length ) {
- // When inside a nested module, don't add our Sinon
- // setup/teardown a second time.
+ var orgExecute, orgBeforeEach, orgAfterEach;
+ if ( nested ) {
+ // In a nested module, don't re-add our hooks, QUnit does that already.
return orgModule.apply( this, arguments );
}
-
if ( arguments.length === 2 && typeof localEnv === 'function' ) {
executeNow = localEnv;
localEnv = undefined;
}
-
- localEnv = localEnv || {};
- orgModule( name, {
- setup: function () {
- var config = sinon.getConfig( sinon.config );
- config.injectInto = this;
- sinon.sandbox.create( config );
-
- if ( localEnv.setup ) {
- localEnv.setup.call( this );
- }
- },
- teardown: function () {
- if ( localEnv.teardown ) {
- localEnv.teardown.call( this );
- }
-
- this.sandbox.verifyAndRestore();
- }
- }, executeNow );
- };
- }() );
-
- // Extend QUnit.module to provide a fixture element.
- ( function () {
- var orgModule = QUnit.module;
-
- QUnit.module = function ( name, localEnv, executeNow ) {
- var fixture;
-
- if ( arguments.length === 2 && typeof localEnv === 'function' ) {
- executeNow = localEnv;
- localEnv = undefined;
+ if ( executeNow ) {
+ // Wrap executeNow() so that we can detect nested modules
+ orgExecute = executeNow;
+ executeNow = function () {
+ var ret;
+ nested = true;
+ ret = orgExecute.apply( this, arguments );
+ nested = false;
+ return ret;
+ };
}
- localEnv = localEnv || {};
- orgModule( name, {
- setup: function () {
- fixture = document.createElement( 'div' );
- fixture.id = 'qunit-fixture';
- document.body.appendChild( fixture );
+ localEnv = makeSafeEnv( localEnv );
+ orgBeforeEach = localEnv.beforeEach;
+ orgAfterEach = localEnv.afterEach;
- if ( localEnv.setup ) {
- localEnv.setup.call( this );
- }
- },
- teardown: function () {
- if ( localEnv.teardown ) {
- localEnv.teardown.call( this );
- }
+ localEnv.beforeEach = function () {
+ // Sinon sandbox
+ var config = sinon.getConfig( sinon.config );
+ config.injectInto = this;
+ sinon.sandbox.create( config );
+
+ // Fixture element
+ this.fixture = document.createElement( 'div' );
+ this.fixture.id = 'qunit-fixture';
+ document.body.appendChild( this.fixture );
- fixture.parentNode.removeChild( fixture );
+ if ( orgBeforeEach ) {
+ return orgBeforeEach.apply( this, arguments );
}
- }, executeNow );
+ };
+ localEnv.afterEach = function () {
+ var ret;
+ if ( orgAfterEach ) {
+ ret = orgAfterEach.apply( this, arguments );
+ }
+ this.sandbox.verifyAndRestore();
+ this.fixture.parentNode.removeChild( this.fixture );
+ return ret;
+ };
+
+ return orgModule( name, localEnv, executeNow );
};
}() );
ajaxRequests.push( { xhr: jqXHR, options: ajaxOptions } );
}
- return function ( localEnv ) {
- localEnv = $.extend( {
- // QUnit
- setup: $.noop,
- teardown: $.noop,
- // MediaWiki
- config: {},
- messages: {}
- }, localEnv );
+ return function ( orgEnv ) {
+ var localEnv, orgBeforeEach, orgAfterEach;
- return {
- setup: function () {
- // Greetings, mock environment!
- mw.config = new MwMap();
- mw.config.set( freshConfigCopy( localEnv.config ) );
- mw.messages = new MwMap();
- mw.messages.set( freshMessagesCopy( localEnv.messages ) );
- // Update reference to mw.messages
- mw.jqueryMsg.setParserDefaults( {
- messages: mw.messages
- } );
-
- this.suppressWarnings = suppressWarnings;
- this.restoreWarnings = restoreWarnings;
+ localEnv = makeSafeEnv( orgEnv );
+ // MediaWiki env testing
+ localEnv.config = localEnv.config || {};
+ localEnv.messages = localEnv.messages || {};
- // Start tracking ajax requests
- $( document ).on( 'ajaxSend', trackAjax );
+ orgBeforeEach = localEnv.beforeEach;
+ orgAfterEach = localEnv.afterEach;
- localEnv.setup.call( this );
- },
+ localEnv.beforeEach = function () {
+ // Greetings, mock environment!
+ mw.config = new MwMap();
+ mw.config.set( freshConfigCopy( localEnv.config ) );
+ mw.messages = new MwMap();
+ mw.messages.set( freshMessagesCopy( localEnv.messages ) );
+ // Update reference to mw.messages
+ mw.jqueryMsg.setParserDefaults( {
+ messages: mw.messages
+ } );
- teardown: function () {
- var timers, pending, $activeLen;
+ this.suppressWarnings = suppressWarnings;
+ this.restoreWarnings = restoreWarnings;
- localEnv.teardown.call( this );
+ // Start tracking ajax requests
+ $( document ).on( 'ajaxSend', trackAjax );
- // Stop tracking ajax requests
- $( document ).off( 'ajaxSend', trackAjax );
+ if ( orgBeforeEach ) {
+ return orgBeforeEach.apply( this, arguments );
+ }
+ };
+ localEnv.afterEach = function () {
+ var timers, pending, $activeLen, ret;
- // As a convenience feature, automatically restore warnings if they're
- // still suppressed by the end of the test.
- restoreWarnings();
+ if ( orgAfterEach ) {
+ ret = orgAfterEach.apply( this, arguments );
+ }
- // Farewell, mock environment!
- mw.config = liveConfig;
- mw.messages = liveMessages;
- // Restore reference to mw.messages
- mw.jqueryMsg.setParserDefaults( {
- messages: liveMessages
+ // Stop tracking ajax requests
+ $( document ).off( 'ajaxSend', trackAjax );
+
+ // As a convenience feature, automatically restore warnings if they're
+ // still suppressed by the end of the test.
+ restoreWarnings();
+
+ // Farewell, mock environment!
+ mw.config = liveConfig;
+ mw.messages = liveMessages;
+ // Restore reference to mw.messages
+ mw.jqueryMsg.setParserDefaults( {
+ messages: liveMessages
+ } );
+
+ // Tests should use fake timers or wait for animations to complete
+ // Check for incomplete animations/requests/etc and throw if there are any.
+ if ( $.timers && $.timers.length !== 0 ) {
+ timers = $.timers.length;
+ $.each( $.timers, function ( i, timer ) {
+ var node = timer.elem;
+ mw.log.warn( 'Unfinished animation #' + i + ' in ' + timer.queue + ' queue on ' +
+ mw.html.element( node.nodeName.toLowerCase(), $( node ).getAttrs() )
+ );
} );
+ // Force animations to stop to give the next test a clean start
+ $.timers = [];
+ $.fx.stop();
- // Tests should use fake timers or wait for animations to complete
- // Check for incomplete animations/requests/etc and throw if there are any.
- if ( $.timers && $.timers.length !== 0 ) {
- timers = $.timers.length;
- $.each( $.timers, function ( i, timer ) {
- var node = timer.elem;
- mw.log.warn( 'Unfinished animation #' + i + ' in ' + timer.queue + ' queue on ' +
- mw.html.element( node.nodeName.toLowerCase(), $( node ).getAttrs() )
- );
- } );
- // Force animations to stop to give the next test a clean start
- $.timers = [];
- $.fx.stop();
-
- throw new Error( 'Unfinished animations: ' + timers );
- }
+ throw new Error( 'Unfinished animations: ' + timers );
+ }
- // Test should use fake XHR, wait for requests, or call abort()
- $activeLen = $.active;
- if ( $activeLen !== undefined && $activeLen !== 0 ) {
- pending = $.grep( ajaxRequests, function ( ajax ) {
- return ajax.xhr.state() === 'pending';
- } );
- if ( pending.length !== $activeLen ) {
- mw.log.warn( 'Pending requests does not match jQuery.active count' );
- }
- // Force requests to stop to give the next test a clean start
- $.each( ajaxRequests, function ( i, ajax ) {
- mw.log.warn(
- 'AJAX request #' + i + ' (state: ' + ajax.xhr.state() + ')',
- ajax.options
- );
- ajax.xhr.abort();
- } );
- ajaxRequests = [];
-
- throw new Error( 'Pending AJAX requests: ' + pending.length + ' (active: ' + $activeLen + ')' );
+ // Test should use fake XHR, wait for requests, or call abort()
+ $activeLen = $.active;
+ if ( $activeLen !== undefined && $activeLen !== 0 ) {
+ pending = ajaxRequests.filter( function ( ajax ) {
+ return ajax.xhr.state() === 'pending';
+ } );
+ if ( pending.length !== $activeLen ) {
+ mw.log.warn( 'Pending requests does not match jQuery.active count' );
}
+ // Force requests to stop to give the next test a clean start
+ ajaxRequests.forEach( function ( ajax, i ) {
+ mw.log.warn(
+ 'AJAX request #' + i + ' (state: ' + ajax.xhr.state() + ')',
+ ajax.options
+ );
+ ajax.xhr.abort();
+ } );
+ ajaxRequests = [];
+
+ throw new Error( 'Pending AJAX requests: ' + pending.length + ' (active: ' + $activeLen + ')' );
}
+
+ return ret;
};
+ return localEnv;
};
}() );
// Expect boolean true
assertTrue: function ( actual, message ) {
- QUnit.push( actual === true, actual, true, message );
+ this.pushResult( {
+ result: actual === true,
+ actual: actual,
+ expected: true,
+ message: message
+ } );
},
// Expect boolean false
assertFalse: function ( actual, message ) {
- QUnit.push( actual === false, actual, false, message );
+ this.pushResult( {
+ result: actual === false,
+ actual: actual,
+ expected: false,
+ message: message
+ } );
},
// Expect numerical value less than X
lt: function ( actual, expected, message ) {
- QUnit.push( actual < expected, actual, 'less than ' + expected, message );
+ this.pushResult( {
+ result: actual < expected,
+ actual: actual,
+ expected: 'less than ' + expected,
+ message: message
+ } );
},
// Expect numerical value less than or equal to X
ltOrEq: function ( actual, expected, message ) {
- QUnit.push( actual <= expected, actual, 'less than or equal to ' + expected, message );
+ this.pushResult( {
+ result: actual <= expected,
+ actual: actual,
+ expected: 'less than or equal to ' + expected,
+ message: message
+ } );
},
// Expect numerical value greater than X
gt: function ( actual, expected, message ) {
- QUnit.push( actual > expected, actual, 'greater than ' + expected, message );
+ this.pushResult( {
+ result: actual > expected,
+ actual: actual,
+ expected: 'greater than ' + expected,
+ message: message
+ } );
},
// Expect numerical value greater than or equal to X
gtOrEq: function ( actual, expected, message ) {
- QUnit.push( actual >= expected, actual, 'greater than or equal to ' + expected, message );
+ this.pushResult( {
+ result: actual >= true,
+ actual: actual,
+ expected: 'greater than or equal to ' + expected,
+ message: message
+ } );
},
/**
htmlEqual: function ( actualHtml, expectedHtml, message ) {
var actual = getHtmlStructure( actualHtml ),
expected = getHtmlStructure( expectedHtml );
-
- QUnit.push(
- QUnit.equiv(
- actual,
- expected
- ),
- actual,
- expected,
- message
- );
+ this.pushResult( {
+ result: QUnit.equiv( actual, expected ),
+ actual: actual,
+ expected: expected,
+ message: message
+ } );
},
/**
var actual = getHtmlStructure( actualHtml ),
expected = getHtmlStructure( expectedHtml );
- QUnit.push(
- !QUnit.equiv(
- actual,
- expected
- ),
- actual,
- expected,
- message
- );
+ this.pushResult( {
+ result: !QUnit.equiv( actual, expected ),
+ actual: actual,
+ expected: expected,
+ message: message,
+ negative: true
+ } );
}
};
* Small test suite to confirm proper functionality of the utilities and
* initializations defined above in this file.
*/
- QUnit.module( 'test.mediawiki.qunit.testrunner', QUnit.newMwEnvironment( {
+ QUnit.module( 'testrunner', QUnit.newMwEnvironment( {
setup: function () {
this.mwHtmlLive = mw.html;
mw.html = {
assert.deepEqual( missing, [], 'Modules in missing state' );
} );
- QUnit.test( 'htmlEqual', function ( assert ) {
+ QUnit.test( 'assert.htmlEqual', function ( assert ) {
assert.htmlEqual(
'<div><p class="some classes" data-length="10">Child paragraph with <a href="http://example.com">A link</a></p>Regular text<span>A span</span></div>',
'<div><p data-length=\'10\' class=\'some classes\'>Child paragraph with <a href=\'http://example.com\' >A link</a></p>Regular text<span>A span</span></div>',
'foo<a href="http://example.com">example</a>quux',
'Outer text nodes are compared (last text node different)'
);
-
} );
- QUnit.module( 'test.mediawiki.qunit.testrunner-after', QUnit.newMwEnvironment() );
+ QUnit.module( 'testrunner-after', QUnit.newMwEnvironment() );
QUnit.test( 'Teardown', function ( assert ) {
assert.equal( mw.html.escape( '<' ), '<', 'teardown() callback was ran.' );
assert.equal( mw.messages.get( 'testMsg' ), null, 'messages object restored to live in next module()' );
} );
+ QUnit.module( 'testrunner-each', {
+ beforeEach: function () {
+ this.mwHtmlLive = mw.html;
+ },
+ afterEach: function () {
+ mw.html = this.mwHtmlLive;
+ }
+ } );
+ QUnit.test( 'beforeEach', function ( assert ) {
+ assert.ok( this.mwHtmlLive, 'setup() ran' );
+ mw.html = null;
+ } );
+ QUnit.test( 'afterEach', function ( assert ) {
+ assert.equal( mw.html.escape( '<' ), '<', 'afterEach() ran' );
+ } );
+
+ QUnit.module( 'testrunner-each-compat', {
+ setup: function () {
+ this.mwHtmlLive = mw.html;
+ },
+ teardown: function () {
+ mw.html = this.mwHtmlLive;
+ }
+ } );
+ QUnit.test( 'setup', function ( assert ) {
+ assert.ok( this.mwHtmlLive, 'setup() ran' );
+ mw.html = null;
+ } );
+ QUnit.test( 'teardown', function ( assert ) {
+ assert.equal( mw.html.escape( '<' ), '<', 'teardown() ran' );
+ } );
+
+ // Regression test for 'this.sandbox undefined' error, fixed by
+ // ensuring Sinon setup/teardown is not re-run on inner module.
+ QUnit.module( 'testrunner-nested', function () {
+ QUnit.module( 'testrunner-nested-inner', function () {
+ QUnit.test( 'Dummy', function ( assert ) {
+ assert.ok( true, 'Nested modules supported' );
+ } );
+ } );
+ } );
+
+ QUnit.module( 'testrunner-hooks-outer', function () {
+ var beforeHookWasExecuted = false,
+ afterHookWasExecuted = false;
+ QUnit.module( 'testrunner-hooks', {
+ before: function () {
+ beforeHookWasExecuted = true;
+
+ // This way we can be sure that module `testrunner-hook-after` will always
+ // be executed after module `testrunner-hooks`
+ QUnit.module( 'testrunner-hooks-after' );
+ QUnit.test(
+ '`after` hook for module `testrunner-hooks` was executed',
+ function ( assert ) {
+ assert.ok( afterHookWasExecuted );
+ }
+ );
+ },
+ after: function () {
+ afterHookWasExecuted = true;
+ }
+ } );
+
+ QUnit.test( '`before` hook was executed', function ( assert ) {
+ assert.ok( beforeHookWasExecuted );
+ } );
+ } );
+
}( jQuery, mediaWiki, QUnit ) );