From: Timo Tijhof Date: Mon, 17 Jul 2017 19:29:11 +0000 (-0500) Subject: qunit: Prepare testrunner for QUnit 2 X-Git-Tag: 1.31.0-rc.0~2649^2 X-Git-Url: https://git.cyclocoop.org/%7B%24www_url%7Dadmin/compta/exercices/journal.php?a=commitdiff_plain;h=43dc5c1539e887c68a6c0ba09de83277ee71b9d3;p=lhc%2Fweb%2Fwiklou.git 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 --- diff --git a/tests/qunit/data/testrunner.js b/tests/qunit/data/testrunner.js index f023ddde3c..929fa1fb5e 100644 --- a/tests/qunit/data/testrunner.js +++ b/tests/qunit/data/testrunner.js @@ -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 ); - - if ( localEnv.setup ) { - localEnv.setup.call( this ); - } - }, - teardown: function () { - if ( localEnv.teardown ) { - localEnv.teardown.call( this ); - } - - this.sandbox.verifyAndRestore(); + orgBeforeEach = localEnv.beforeEach; + orgAfterEach = localEnv.afterEach; + localEnv.beforeEach = function () { + var config = sinon.getConfig( sinon.config ); + config.injectInto = this; + sinon.sandbox.create( config ); + + if ( orgBeforeEach ) { + return orgBeforeEach.apply( this, arguments ); + } + }; + localEnv.afterEach = function () { + var ret; + if ( orgAfterEach ) { + ret = orgAfterEach.apply( this, arguments ); } - }, executeNow ); + + 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 ); - - if ( localEnv.setup ) { - localEnv.setup.call( this ); - } - }, - teardown: function () { - if ( localEnv.teardown ) { - localEnv.teardown.call( this ); - } - - fixture.parentNode.removeChild( 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 ( 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( '

Child paragraph with A link

Regular textA span
', '

Child paragraph with A link

Regular textA span
', @@ -535,10 +594,9 @@ 'fooexamplequux', '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.' ); @@ -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( '<' ), '<', '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' ); + } ); + } ); + } ); + }( jQuery, mediaWiki, QUnit ) );