qunit: Prepare testrunner for QUnit 2

* Nested modules:
  - Support for Sinon extension was fixed by Ib17bbbef45b2bd.
  - Support for Fixture extension was still broken, masked by the use
    of a local variable that made the handler not fail when setup ran twice
    in a row. Fixed using the same moduleStack.length check.
  - Add regression test.

* beforeEach/afterEach:
  - Added in 1.16, with compat for setup/teardown.
    Our wrapper adds its own setup/teardown, and preserves any original one.
    However, it didn't account for beforeEach/afterEach, so it ends up
    sending both but only one is used.
  - Fix to support both on the incoming localEnv object, and also switch
    our wrapper to use beforeEach/afterEach in prep for QUnit 2.0.
  - Fix our wrappers to preserve return value since QUnit 2 allows beforeEach
    and afterEach hooks to be asynchronous by returning a Promise, similar
    to how one can do from QUnit.test().
  - Add regression test.

* Centralise makeSafeEnv logic
  - We always create our own env object to pass to orgModule().
    Document why this is (to avoid recursion).
  - Add regression test.

* Custom assertion methods:
  - Use this.pushResult instead of the deprecated QUnit.push() method.
    This also improves the in-browser reporting of errors by properly
    supporting 'negative' results for notHtmlEqual reporter.

Bug: T170515
Change-Id: If4141df10eae55cbe8a5ca7a26707be1cd7b9217
This commit is contained in:
Timo Tijhof 2017-07-17 14:29:11 -05:00 committed by Krinkle
parent f05716b1f6
commit 43dc5c1539

View file

@ -4,6 +4,22 @@
var addons;
/**
* Make a safe copy of localEnv:
* - Creates a copy so that when the same object reference to module hooks is
* used by multipe test hooks, our QUnit.module extension will not wrap the
* callbacks multiple times. Instead, they wrap using a new object.
* - Normalise setup/teardown to avoid having to repeat this in each extension
* (deprecated in QUnit 1.16, removed in QUnit 2).
* - Strip any other properties.
*/
function makeSafeEnv( localEnv ) {
return {
beforeEach: localEnv.setup || localEnv.beforeEach,
afterEach: localEnv.teardown || localEnv.afterEach
};
}
/**
* Add bogus to url to prevent IE crazy caching
*
@ -42,9 +58,6 @@
*
* 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 );
@ -60,74 +73,94 @@
useFakeTimers: false,
useFakeServer: false
};
// Extend QUnit.module to provide a Sinon sandbox.
( function () {
var orgModule = QUnit.module;
QUnit.module = function ( name, localEnv, executeNow ) {
var orgBeforeEach, orgAfterEach;
if ( QUnit.config.moduleStack.length ) {
// When inside a nested module, don't add our Sinon
// setup/teardown a second time.
// In a nested module, don't re-run our handlers.
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 );
orgBeforeEach = localEnv.beforeEach;
orgAfterEach = localEnv.afterEach;
localEnv.beforeEach = 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();
if ( orgBeforeEach ) {
return orgBeforeEach.apply( this, arguments );
}
}, executeNow );
};
localEnv.afterEach = function () {
var ret;
if ( orgAfterEach ) {
ret = orgAfterEach.apply( this, arguments );
}
this.sandbox.verifyAndRestore();
return ret;
};
return orgModule( name, localEnv, executeNow );
};
}() );
// Extend QUnit.module to provide a fixture element.
( function () {
var orgModule = QUnit.module;
QUnit.module = function ( name, localEnv, executeNow ) {
var fixture;
var orgBeforeEach, orgAfterEach;
if ( QUnit.config.moduleStack.length ) {
// In a nested module, don't re-run our handlers.
return orgModule.apply( this, arguments );
}
if ( arguments.length === 2 && typeof localEnv === 'function' ) {
executeNow = localEnv;
localEnv = undefined;
}
localEnv = localEnv || {};
orgModule( name, {
setup: function () {
fixture = document.createElement( 'div' );
fixture.id = 'qunit-fixture';
document.body.appendChild( fixture );
orgBeforeEach = localEnv.beforeEach;
orgAfterEach = localEnv.afterEach;
localEnv.beforeEach = function () {
this.fixture = document.createElement( 'div' );
this.fixture.id = 'qunit-fixture';
document.body.appendChild( this.fixture );
if ( localEnv.setup ) {
localEnv.setup.call( this );
}
},
teardown: function () {
if ( localEnv.teardown ) {
localEnv.teardown.call( this );
}
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.fixture.parentNode.removeChild( this.fixture );
return ret;
};
return orgModule( name, localEnv, executeNow );
};
}() );
// Extend QUnit.module to normalise localEnv.
// NOTE: This MUST be the last QUnit.module extension so that the above extensions
// may safely modify the object and assume beforeEach/afterEach.
( function () {
var orgModule = QUnit.module;
QUnit.module = function ( name, localEnv, executeNow ) {
if ( typeof localEnv === 'object' ) {
localEnv = makeSafeEnv( localEnv );
}
return orgModule( name, localEnv, executeNow );
};
}() );
@ -194,18 +227,14 @@
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 = orgEnv ? makeSafeEnv( orgEnv ) : {};
// MediaWiki env testing
localEnv.config = orgEnv && orgEnv.config || {};
localEnv.messages = orgEnv && orgEnv.messages || {};
return {
setup: function () {
beforeEach: function () {
// Greetings, mock environment!
mw.config = new MwMap();
mw.config.set( freshConfigCopy( localEnv.config ) );
@ -222,13 +251,17 @@
// Start tracking ajax requests
$( document ).on( 'ajaxSend', trackAjax );
localEnv.setup.call( this );
if ( localEnv.beforeEach ) {
return localEnv.beforeEach.apply( this, arguments );
}
},
teardown: function () {
var timers, pending, $activeLen;
afterEach: function () {
var timers, pending, $activeLen, ret;
localEnv.teardown.call( this );
if ( localEnv.afterEach ) {
ret = localEnv.afterEach.apply( this, arguments );
}
// Stop tracking ajax requests
$( document ).off( 'ajaxSend', trackAjax );
@ -283,6 +316,8 @@
throw new Error( 'Pending AJAX requests: ' + pending.length + ' (active: ' + $activeLen + ')' );
}
return ret;
}
};
};
@ -356,32 +391,62 @@
// 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
} );
},
/**
@ -394,16 +459,12 @@
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
} );
},
/**
@ -417,15 +478,13 @@
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
} );
}
};
@ -435,7 +494,7 @@
* 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 = {
@ -488,7 +547,7 @@
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>',
@ -535,10 +594,9 @@
'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( '<' ), '&lt;', 'teardown() callback was ran.' );
@ -546,4 +604,46 @@
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( '<' ), '&lt;', '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( '<' ), '&lt;', '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' );
} );
} );
} );
}( jQuery, mediaWiki, QUnit ) );