diff --git a/src/core.js b/src/core.js index 1d03d7f23..48293ea70 100644 --- a/src/core.js +++ b/src/core.js @@ -32,8 +32,10 @@ extend( QUnit, { module = createModule(); moduleFns = { + before: setHook( module, "before" ), beforeEach: setHook( module, "beforeEach" ), - afterEach: setHook( module, "afterEach" ) + afterEach: setHook( module, "afterEach" ), + after: setHook( module, "after" ) }; if ( executeNow instanceof Function ) { @@ -55,11 +57,13 @@ extend( QUnit, { name: moduleName, parentModule: parentModule, tests: [], - moduleId: generateHash( moduleName ) + moduleId: generateHash( moduleName ), + testsRun: 0 }; var env = {}; if ( parentModule ) { + parentModule.childModule = module; extend( env, parentModule.testEnvironment ); delete env.beforeEach; delete env.afterEach; diff --git a/src/test.js b/src/test.js index 90227ef66..91d0b09d1 100644 --- a/src/test.js +++ b/src/test.js @@ -75,8 +75,10 @@ Test.prototype = { config.current = this; if ( this.module.testEnvironment ) { + delete this.module.testEnvironment.before; delete this.module.testEnvironment.beforeEach; delete this.module.testEnvironment.afterEach; + delete this.module.testEnvironment.after; } this.testEnvironment = extend( {}, this.module.testEnvironment ); @@ -133,10 +135,22 @@ Test.prototype = { checkPollution(); }, - queueHook: function( hook, hookName ) { + queueHook: function( hook, hookName, hookOwner ) { var promise, test = this; return function runHook() { + if ( hookName === "before" ) { + if ( hookOwner.testsRun !== 0 ) { + return; + } + + test.preserveEnvironment = true; + } + + if ( hookName === "after" && hookOwner.testsRun !== numberOfTests( hookOwner ) - 1 ) { + return; + } + config.current = test; if ( config.notrycatch ) { callHook(); @@ -166,7 +180,7 @@ Test.prototype = { } if ( module.testEnvironment && QUnit.objectType( module.testEnvironment[ handler ] ) === "function" ) { - hooks.push( test.queueHook( module.testEnvironment[ handler ], handler ) ); + hooks.push( test.queueHook( module.testEnvironment[ handler ], handler, module ) ); } } @@ -205,6 +219,7 @@ Test.prototype = { } } + notifyTestsRan( this.module ); runLoggingCallbacks( "testDone", { name: this.testName, module: this.module.name, @@ -233,6 +248,13 @@ Test.prototype = { config.current = undefined; }, + preserveTestEnvironment: function() { + if ( this.preserveEnvironment ) { + this.module.testEnvironment = this.testEnvironment; + this.testEnvironment = extend( {}, this.module.testEnvironment ); + } + }, + queue: function() { var priority, test = this; @@ -249,16 +271,25 @@ Test.prototype = { test.before(); }, + test.hooks( "before" ), + + function() { + test.preserveTestEnvironment(); + }, + test.hooks( "beforeEach" ), + function() { test.run(); }, test.hooks( "afterEach" ).reverse(), + test.hooks( "after" ).reverse(), function() { test.after(); }, + function() { test.finish(); } @@ -645,3 +676,18 @@ function only( testName, expected, callback, async ) { newTest.queue(); } + +function numberOfTests( module ) { + var count = module.tests.length; + while ( module = module.childModule ) { + count += module.tests.length; + } + return count; +} + +function notifyTestsRan( module ) { + module.testsRun++; + while ( module = module.parentModule ) { + module.testsRun++; + } +} diff --git a/test/amd.js b/test/amd.js index f68aa838e..f3b5929f2 100644 --- a/test/amd.js +++ b/test/amd.js @@ -2,12 +2,22 @@ define( [ "qunit" ], function( QUnit ) { return function() { - QUnit.module( "AMD autostart" ); + QUnit.module( "AMD autostart", { + after: function( assert ) { + assert.ok( true, "after hook ran" ); + } + } ); QUnit.test( "Prove the test run started as expected", function( assert ) { - assert.expect( 1 ); + assert.expect( 2 ); assert.strictEqual( beginData.totalTests, 1, "Should have expected 1 test" ); } ); + + setTimeout( function() { + QUnit.test( "Async-loaded tests should not run after hook again", function( assert ) { + assert.expect( 0 ); + } ); + }, 5000 ); }; } ); diff --git a/test/main/async.js b/test/main/async.js index c9ed30501..196d9a94c 100644 --- a/test/main/async.js +++ b/test/main/async.js @@ -232,6 +232,33 @@ QUnit.test( "fails if callback is called more than callback call count", functio } ); +QUnit.module( "assert.async fails if callback is called more than once in", { + before: function( assert ) { + + // Having an outer async flow in this test avoids the need to manually modify QUnit + // internals in order to avoid post-`done` assertions causing additional failures + var done = assert.async(); + + assert.expect( 1 ); + + // Duck-punch to force an Error to be thrown instead of a `pushFailure` call + assert.test.pushFailure = function( msg ) { + throw new Error( msg ); + }; + + var overDone = assert.async(); + overDone(); + + assert.throws( function() { + overDone(); + }, new RegExp( "Too many calls to the `assert.async` callback" ) ); + + done(); + } +} ); + +QUnit.test( "before", function() {} ); + QUnit.module( "assert.async fails if callback is called more than once in", { beforeEach: function( assert ) { @@ -286,6 +313,50 @@ QUnit.module( "assert.async fails if callback is called more than once in", { QUnit.test( "afterEach", function( /* assert */ ) { } ); +QUnit.module( "assert.async fails if callback is called more than once in", { + after: function( assert ) { + + // Having an outer async flow in this test avoids the need to manually modify QUnit + // internals in order to avoid post-`done` assertions causing additional failures + var done = assert.async(); + + assert.expect( 1 ); + + // Duck-punch to force an Error to be thrown instead of a `pushFailure` call + assert.test.pushFailure = function( msg ) { + throw new Error( msg ); + }; + + var overDone = assert.async(); + overDone(); + + assert.throws( function() { + overDone(); + }, new RegExp( "Too many calls to the `assert.async` callback" ) ); + + done(); + } +} ); + +QUnit.test( "after", function() {} ); + +QUnit.module( "assert.async in before", { + before: function( assert ) { + var done = assert.async(), + testContext = this; + setTimeout( function() { + testContext.state = "before"; + done(); + } ); + } +} ); + +QUnit.test( "before synchronized", function( assert ) { + assert.expect( 1 ); + assert.equal( this.state, "before", "before synchronized before test callback was " + + "executed" ); +} ); + QUnit.module( "assert.async in beforeEach", { beforeEach: function( assert ) { var done = assert.async(), @@ -334,6 +405,37 @@ QUnit.test( "afterEach will synchronize", function( assert ) { assert.expect( 1 ); } ); +QUnit.module( "assert.async before after", { + after: function( assert ) { + assert.equal( this.state, "done", "test callback synchronized before after was " + + "executed" ); + } +} ); + +QUnit.test( "after will synchronize", function( assert ) { + assert.expect( 1 ); + var done = assert.async(), + testContext = this; + setTimeout( function() { + testContext.state = "done"; + done(); + } ); +} ); + +QUnit.module( "assert.async in after", { + after: function( assert ) { + var done = assert.async(); + setTimeout( function() { + assert.ok( true, "after synchronized before test was finished" ); + done(); + } ); + } +} ); + +QUnit.test( "after will synchronize", function( assert ) { + assert.expect( 1 ); +} ); + QUnit.module( "assert.async callback event loop timing" ); QUnit.test( "`done` can be called synchronously", function( assert ) { @@ -457,6 +559,30 @@ QUnit.test( "cannot allow assertions between first `done` call and second `asser } ); } ); +QUnit.module( "assert after last done in before fail, but allow other phases to run", { + before: function( assert ) { + _setupForFailingAssertionsAfterAsyncDone.call( this, assert ); + + // THIS IS THE ACTUAL TEST! + assert.expect( 3 ); + this._assertCatch( function() { + assert.async()(); + + // FAIL!!! (with duck-punch to force an Error to be thrown instead of `pushFailure`) + assert.ok( true, "should fail with a special `done`-related error message if called " + + "after final `done` even if result is passing" ); + } ); + }, + + after: function( assert ) { + assert.ok( true, "This assertion should still run in after" ); + } +} ); + +QUnit.test( "before will fail but test and after will still run", function( assert ) { + assert.ok( true, "This assertion should still run in the test callback" ); +} ); + QUnit.module( "assert after last done in beforeEach fail, but allow other phases to run", { beforeEach: function( assert ) { _setupForFailingAssertionsAfterAsyncDone.call( this, assert ); @@ -482,19 +608,27 @@ QUnit.test( "beforeEach will fail but test and afterEach will still run", functi } ); QUnit.module( "assert after last done in test fail, but allow other phases to run", { + before: function( assert ) { + assert.ok( true, "This assertion should still run in before" ); + }, + beforeEach: function( assert ) { _setupForFailingAssertionsAfterAsyncDone.call( this, assert ); - assert.expect( 3 ); + assert.expect( 5 ); assert.ok( true, "This assertion should still run in beforeEach" ); }, afterEach: function( assert ) { assert.ok( true, "This assertion should still run in afterEach" ); + }, + + after: function( assert ) { + assert.ok( true, "This assertion should still run in after" ); } } ); -QUnit.test( "test will fail, but beforeEach and afterEach will still run", function( assert ) { +QUnit.test( "test will fail, but other hooks will still run", function( assert ) { this._assertCatch( function() { assert.async()(); @@ -526,3 +660,26 @@ QUnit.module( "assert after last done in afterEach fail, but allow other phases QUnit.test( "afterEach will fail but beforeEach and test will still run", function( assert ) { assert.ok( true, "This assertion should still run in the test callback" ); } ); + +QUnit.module( "assert after last done in after fail, but allow other phases to run", { + before: function( assert ) { + _setupForFailingAssertionsAfterAsyncDone.call( this, assert ); + + assert.expect( 3 ); + assert.ok( true, "This assertion should still run in before" ); + }, + + after: function( assert ) { + this._assertCatch( function() { + assert.async()(); + + // FAIL!!! (with duck-punch to force an Error to be thrown instead of `pushFailure`) + assert.ok( true, "should fail with a special `done`-related error message if called " + + "after final `done` even if result is passing" ); + } ); + } +} ); + +QUnit.test( "after will fail but before and test will still run", function( assert ) { + assert.ok( true, "This assertion should still run in the test callback" ); +} ); diff --git a/test/main/modules.js b/test/main/modules.js index 63519e55e..de3b5fb46 100644 --- a/test/main/modules.js +++ b/test/main/modules.js @@ -1,27 +1,68 @@ -QUnit.module( "beforeEach/afterEach", { - beforeEach: function() { +QUnit.module( "before/beforeEach/afterEach/after", { + before: function() { + this.lastHook = "module-before"; + }, + beforeEach: function( assert ) { + assert.strictEqual( this.lastHook, "module-before", + "Module's beforeEach runs after before" ); this.lastHook = "module-beforeEach"; }, afterEach: function( assert ) { - if ( this.hooksTest ) { - assert.strictEqual( this.lastHook, "test-block", - "Module's afterEach runs after current test block" ); - this.lastHook = "module-afterEach"; - } + assert.strictEqual( this.lastHook, "test-block", + "Module's afterEach runs after current test block" ); + this.lastHook = "module-afterEach"; + }, + after: function( assert ) { + assert.strictEqual( this.lastHook, "module-afterEach", + "Module's afterEach runs before after" ); + this.lastHook = "module-after"; } } ); QUnit.test( "hooks order", function( assert ) { - assert.expect( 2 ); - - // This will trigger an assertion on the global and one on the module's afterEach - this.hooksTest = true; + assert.expect( 4 ); assert.strictEqual( this.lastHook, "module-beforeEach", "Module's beforeEach runs before current test block" ); this.lastHook = "test-block"; } ); +QUnit.module( "before", { + before: function( assert ) { + assert.ok( true, "before hook ran" ); + + if ( typeof this.beforeCount === "undefined" ) { + this.beforeCount = 0; + } + + this.beforeCount++; + } +} ); + +QUnit.test( "runs before first test", function( assert ) { + assert.expect( 2 ); + assert.equal( this.beforeCount, 1, "beforeCount should be one" ); +} ); + +QUnit.test( "does not run before subsequent tests", function( assert ) { + assert.expect( 1 ); + assert.equal( this.beforeCount, 1, "beforeCount did not increase from last test" ); +} ); + +QUnit.module( "after", { + after: function( assert ) { + assert.ok( true, "after hook ran" ); + } +} ); + +QUnit.test( "does not run after initial tests", function( assert ) { + assert.expect( 0 ); +} ); + +QUnit.test( "runs after final test", function( assert ) { + assert.expect( 1 ); +} ); + QUnit.module( "Test context object", { beforeEach: function( assert ) { var key, @@ -294,3 +335,54 @@ QUnit.module( "contained suite `this`", function( hooks ) { this.outer = 42; } ); } ); + +QUnit.module( "nested modules before/after", { + before: function( assert ) { + assert.ok( true, "before hook ran" ); + this.lastHook = "before"; + }, + after: function( assert ) { + assert.strictEqual( this.lastHook, "outer-after" ); + } +}, function() { + QUnit.test( "should run before", function( assert ) { + assert.expect( 2 ); + assert.strictEqual( this.lastHook, "before" ); + } ); + + QUnit.module( "outer", { + before: function( assert ) { + assert.ok( true, "outer before hook ran" ); + this.lastHook = "outer-before"; + }, + after: function( assert ) { + assert.strictEqual( this.lastHook, "outer-test" ); + this.lastHook = "outer-after"; + } + }, function() { + QUnit.module( "inner", { + before: function( assert ) { + assert.strictEqual( this.lastHook, "outer-before" ); + this.lastHook = "inner-before"; + }, + after: function( assert ) { + assert.strictEqual( this.lastHook, "inner-test" ); + } + }, function() { + QUnit.test( "should run outer-before and inner-before", function( assert ) { + assert.expect( 3 ); + assert.strictEqual( this.lastHook, "inner-before" ); + } ); + + QUnit.test( "should run inner-after", function( assert ) { + assert.expect( 1 ); + this.lastHook = "inner-test"; + } ); + } ); + + QUnit.test( "should run outer-after and after", function( assert ) { + assert.expect( 2 ); + this.lastHook = "outer-test"; + } ); + } ); +} ); diff --git a/test/main/promise.js b/test/main/promise.js index 3a21dda2c..8c7ad5b86 100644 --- a/test/main/promise.js +++ b/test/main/promise.js @@ -14,6 +14,29 @@ function createMockPromise( assert ) { return thenable; } +QUnit.module( "Module with Promise-aware before", { + before: function( assert ) { + assert.ok( true ); + return {}; + } +} ); + +QUnit.test( "non-Promise", function( assert ) { + assert.expect( 1 ); +} ); + +QUnit.module( "Module with Promise-aware before", { + before: function( assert ) { + + // Adds 1 assertion + return createMockPromise( assert ); + } +} ); + +QUnit.test( "fulfilled Promise", function( assert ) { + assert.expect( 1 ); +} ); + QUnit.module( "Module with Promise-aware beforeEach", { beforeEach: function( assert ) { assert.ok( true ); @@ -60,6 +83,29 @@ QUnit.test( "fulfilled Promise", function( assert ) { assert.expect( 1 ); } ); +QUnit.module( "Module with Promise-aware after", { + after: function( assert ) { + assert.ok( true ); + return {}; + } +} ); + +QUnit.test( "non-Promise", function( assert ) { + assert.expect( 1 ); +} ); + +QUnit.module( "Module with Promise-aware after", { + after: function( assert ) { + + // Adds 1 assertion + return createMockPromise( assert ); + } +} ); + +QUnit.test( "fulfilled Promise", function( assert ) { + assert.expect( 1 ); +} ); + QUnit.module( "Promise-aware return values without beforeEach/afterEach" ); QUnit.test( "non-Promise", function( assert ) {