diff --git a/src/test.js b/src/test.js index 55a5d8e77..af5de3840 100644 --- a/src/test.js +++ b/src/test.js @@ -528,26 +528,24 @@ Test.prototype = { const then = promise.then; if ( objectType( then ) === "function" ) { const resume = internalStop( test ); + const resolve = function() { resume(); }; if ( config.notrycatch ) { - then.call( promise, function() { resume(); } ); + then.call( promise, resolve ); } else { - then.call( - promise, - function() { resume(); }, - function( error ) { - const message = "Promise rejected " + - ( !phase ? "during" : phase.replace( /Each$/, "" ) ) + - " \"" + test.testName + "\": " + - ( ( error && error.message ) || error ); - test.pushFailure( message, extractStacktrace( error, 0 ) ); - - // Else next test will carry the responsibility - saveGlobal(); - - // Unblock - internalRecover( test ); - } - ); + const reject = function( error ) { + const message = "Promise rejected " + + ( !phase ? "during" : phase.replace( /Each$/, "" ) ) + + " \"" + test.testName + "\": " + + ( ( error && error.message ) || error ); + test.pushFailure( message, extractStacktrace( error, 0 ) ); + + // Else next test will carry the responsibility + saveGlobal(); + + // Unblock + internalRecover( test ); + }; + then.call( promise, resolve, reject ); } } } @@ -806,7 +804,6 @@ function internalStart( test ) { "Invalid value on test.semaphore", sourceFromStacktrace( 2 ) ); - return; } // Don't start until equal number of stop-calls @@ -822,7 +819,6 @@ function internalStart( test ) { "Tried to restart test while already started (test's semaphore was 0 already)", sourceFromStacktrace( 2 ) ); - return; } // Add a slight delay to allow more assertions etc. diff --git a/test/cli/fixtures/assert-expect/failing-expect.js b/test/cli/fixtures/assert-expect/failing-expect.js new file mode 100644 index 000000000..2799d0885 --- /dev/null +++ b/test/cli/fixtures/assert-expect/failing-expect.js @@ -0,0 +1,4 @@ +QUnit.test( "failing test", assert => { + assert.expect( 2 ); + assert.true( true ); +} ); diff --git a/test/cli/fixtures/assert-expect/no-assertions.js b/test/cli/fixtures/assert-expect/no-assertions.js new file mode 100644 index 000000000..edd8a800d --- /dev/null +++ b/test/cli/fixtures/assert-expect/no-assertions.js @@ -0,0 +1,2 @@ +QUnit.test( "test with no assertions", () => { +} ); diff --git a/test/cli/fixtures/assert-expect/require-expects.js b/test/cli/fixtures/assert-expect/require-expects.js new file mode 100644 index 000000000..671bf0412 --- /dev/null +++ b/test/cli/fixtures/assert-expect/require-expects.js @@ -0,0 +1,5 @@ +QUnit.config.requireExpects = true; + +QUnit.test( "passing test", assert => { + assert.true( true ); +} ); diff --git a/test/cli/fixtures/config-module.js b/test/cli/fixtures/config-module.js new file mode 100644 index 000000000..a46bed58f --- /dev/null +++ b/test/cli/fixtures/config-module.js @@ -0,0 +1,19 @@ +QUnit.config.module = "MODULE b"; + +QUnit.module( "Module A", () => { + QUnit.test( "Test A", assert => { + assert.true( false ); // fail if hit + } ); +} ); + +QUnit.module( "Module B", () => { + QUnit.test( "Test B", assert => { + assert.true( true ); + } ); +} ); + +QUnit.module( "Module C", () => { + QUnit.test( "Test C", assert => { + assert.true( false ); // fail if hit + } ); +} ); diff --git a/test/cli/fixtures/config-testTimeout.js b/test/cli/fixtures/config-testTimeout.js new file mode 100644 index 000000000..57ccb90e4 --- /dev/null +++ b/test/cli/fixtures/config-testTimeout.js @@ -0,0 +1,9 @@ +process.on( "unhandledRejection", ( reason ) => { + console.log( "Unhandled Rejection:", reason ); +} ); + +QUnit.config.testTimeout = 10; + +QUnit.test( "slow", () => { + return new Promise( resolve => setTimeout( resolve, 20 ) ); +} ); diff --git a/test/cli/fixtures/done-after-timeout.js b/test/cli/fixtures/done-after-timeout.js index 56e8e2694..30a17e393 100644 --- a/test/cli/fixtures/done-after-timeout.js +++ b/test/cli/fixtures/done-after-timeout.js @@ -2,5 +2,5 @@ QUnit.test( "times out before scheduled done is called", assert => { assert.timeout( 10 ); const done = assert.async(); assert.true( true ); - setTimeout( done, 20 ); + setTimeout( done, 100 ); } ); diff --git a/test/cli/fixtures/expected/tap-outputs.js b/test/cli/fixtures/expected/tap-outputs.js index 5328cf548..4fcd532be 100644 --- a/test/cli/fixtures/expected/tap-outputs.js +++ b/test/cli/fixtures/expected/tap-outputs.js @@ -342,6 +342,32 @@ ok 1 module providing hooks > module not providing hooks > has a test # todo 0 # fail 0`, + "qunit config-module.js": +`TAP version 13 +ok 1 Module B > Test B +1..1 +# pass 1 +# skip 0 +# todo 0 +# fail 0`, + + "qunit config-testTimeout.js": +`TAP version 13 +not ok 1 slow + --- + message: Test took longer than 10ms; test timed out. + severity: failed + actual : null + expected: undefined + stack: | + at internal + ... +1..1 +# pass 0 +# skip 0 +# todo 0 +# fail 1`, + "qunit done-after-timeout.js": `TAP version 13 not ok 1 times out before scheduled done is called diff --git a/test/cli/fixtures/hard-error-in-hook.js b/test/cli/fixtures/hard-error-in-hook.js new file mode 100644 index 000000000..84ba88fa9 --- /dev/null +++ b/test/cli/fixtures/hard-error-in-hook.js @@ -0,0 +1,8 @@ +QUnit.module( "contains a hard error in hook", hooks => { + hooks.before( () => { + throw new Error( "expected error thrown in hook" ); + } ); + QUnit.test( "contains a hard error", assert => { + assert.true( true ); + } ); +} ); diff --git a/test/cli/fixtures/hard-error-in-test-with-no-async-handler.js b/test/cli/fixtures/hard-error-in-test-with-no-async-handler.js new file mode 100644 index 000000000..36994c6a7 --- /dev/null +++ b/test/cli/fixtures/hard-error-in-test-with-no-async-handler.js @@ -0,0 +1,8 @@ +QUnit.test( "contains a hard error after using `assert.async`", assert => { + assert.async(); + assert.true( true ); + throw new Error( "expected error thrown in test" ); + + // the "done" callback from `assert.async` should be called later, + // but the hard-error prevents the test from reaching that +} ); diff --git a/test/cli/fixtures/noglobals/add-global.js b/test/cli/fixtures/noglobals/add-global.js new file mode 100644 index 000000000..fbf0e4673 --- /dev/null +++ b/test/cli/fixtures/noglobals/add-global.js @@ -0,0 +1,6 @@ +QUnit.config.noglobals = true; + +QUnit.test( "adds global var", assert => { + global.dummyGlobal = "hello"; // eslint-disable-line no-undef + assert.true( true ); +} ); diff --git a/test/cli/fixtures/noglobals/ignored.js b/test/cli/fixtures/noglobals/ignored.js new file mode 100644 index 000000000..eacb5a4b6 --- /dev/null +++ b/test/cli/fixtures/noglobals/ignored.js @@ -0,0 +1,6 @@ +QUnit.config.noglobals = true; + +QUnit.test( "adds global var", assert => { + global[ "qunit-test-output-dummy" ] = "hello"; // eslint-disable-line no-undef + assert.true( true ); +} ); diff --git a/test/cli/fixtures/noglobals/remove-global.js b/test/cli/fixtures/noglobals/remove-global.js new file mode 100644 index 000000000..fa718caea --- /dev/null +++ b/test/cli/fixtures/noglobals/remove-global.js @@ -0,0 +1,8 @@ +QUnit.config.noglobals = true; + +global.dummyGlobal = "hello"; // eslint-disable-line no-undef + +QUnit.test( "deletes global var", assert => { + delete global.dummyGlobal; + assert.true( true ); +} ); diff --git a/test/cli/fixtures/notrycatch/returns-rejection-in-hook.js b/test/cli/fixtures/notrycatch/returns-rejection-in-hook.js new file mode 100644 index 000000000..79e361e85 --- /dev/null +++ b/test/cli/fixtures/notrycatch/returns-rejection-in-hook.js @@ -0,0 +1,18 @@ +"use strict"; + +process.on( "unhandledRejection", ( reason ) => { + console.log( "Unhandled Rejection:", reason ); +} ); + +QUnit.config.notrycatch = true; + +QUnit.module( "notrycatch", function( hooks ) { + + hooks.beforeEach( () => { + return Promise.reject( "bad things happen sometimes" ); + } ); + + QUnit.test( "passing test", assert => { + assert.true( true ); + } ); +} ); diff --git a/test/cli/fixtures/semaphore/nan.js b/test/cli/fixtures/semaphore/nan.js new file mode 100644 index 000000000..caafab53a --- /dev/null +++ b/test/cli/fixtures/semaphore/nan.js @@ -0,0 +1,5 @@ +QUnit.test( "semaphore is set to NaN", assert => { + assert.test.semaphore = "not a number"; + assert.async(); + return Promise.resolve(); +} ); diff --git a/test/cli/fixtures/semaphore/restart.js b/test/cli/fixtures/semaphore/restart.js new file mode 100644 index 000000000..3edfefb23 --- /dev/null +++ b/test/cli/fixtures/semaphore/restart.js @@ -0,0 +1,4 @@ +QUnit.test( "tries to 'restart' the test", assert => { + assert.test.semaphore = -1; + return Promise.resolve(); +} ); diff --git a/test/cli/helpers/execute.js b/test/cli/helpers/execute.js index e290b5be5..b8b1537d2 100644 --- a/test/cli/helpers/execute.js +++ b/test/cli/helpers/execute.js @@ -22,7 +22,7 @@ function normalize( actual ) { // Convert "at foo (/min.js:1)\n -> /src.js:2" to "at foo (/src.js:2)" .replace( /\b(at [^(]+\s\()[^)]+(\))\n\s+-> ([^\n]+)/g, "$1$3$2" ) - .replace( / at .+\([^/)][^)]*\)/g, " at internal" ) + .replace( / {2}at .+\([^/)][^)]*\)/g, " at internal" ) // merge successive lines after initial frame .replace( /(\n\s+at internal)+/g, "$1" ) diff --git a/test/cli/main.js b/test/cli/main.js index b7fa49abb..47ece5c3b 100644 --- a/test/cli/main.js +++ b/test/cli/main.js @@ -133,6 +133,40 @@ QUnit.module( "CLI Main", () => { } } ); + QUnit.test( "hard errors in test using `assert.async` are caught and reported", async assert => { + const command = "qunit hard-error-in-test-with-no-async-handler.js"; + + try { + const result = await execute( command ); + assert.pushResult( { + result: false, + actual: result.stdout + } ); + } catch ( e ) { + assert.equal( e.code, 1 ); + assert.equal( e.stderr, "" ); + assert.notEqual( e.stdout.indexOf( "Died on test #2 at " ), -1 ); + assert.notEqual( e.stdout.indexOf( "Error: expected error thrown in test" ), -1 ); + } + } ); + + QUnit.test( "hard errors in hook are caught and reported", async assert => { + const command = "qunit hard-error-in-hook.js"; + + try { + const result = await execute( command ); + assert.pushResult( { + result: false, + actual: result.stdout + } ); + } catch ( e ) { + assert.equal( e.code, 1 ); + assert.equal( e.stderr, "" ); + assert.notEqual( e.stdout.indexOf( "message: before failed on contains a hard error: expected error thrown in hook" ), -1 ); + assert.notEqual( e.stdout.indexOf( "Error: expected error thrown in hook" ), -1 ); + } + } ); + if ( semver.gte( process.versions.node, "12.0.0" ) ) { QUnit.test( "run ESM test suite with import statement", async assert => { const command = "qunit ../../es2018/esm.mjs"; @@ -280,6 +314,94 @@ QUnit.module( "CLI Main", () => { } ); } } ); + + QUnit.test( "errors if notrycatch is used and a rejection occurs in a hook", async assert => { + try { + await execute( "qunit notrycatch/returns-rejection-in-hook.js" ); + } catch ( e ) { + assert.pushResult( { + + // only in stdout due to using `console.log` in manual `unhandledRejection` handler + result: e.stdout.indexOf( "Unhandled Rejection: bad things happen sometimes" ) > -1, + actual: e.stdout + "\n" + e.stderr + } ); + } + } ); + } ); + + QUnit.test( "config.module", async assert => { + const command = "qunit config-module.js"; + const execution = await execute( command ); + + assert.equal( execution.code, 0 ); + assert.equal( execution.stderr, "" ); + assert.equal( execution.stdout, expectedOutput[ command ] ); + } ); + + QUnit.test( "config.testTimeout", async assert => { + const command = "qunit config-testTimeout.js"; + + try { + await execute( command ); + } catch ( e ) { + assert.equal( e.code, 1 ); + assert.equal( e.stderr, "" ); + assert.equal( e.stdout, expectedOutput[ command ] ); + } + } ); + + QUnit.module( "noglobals", () => { + QUnit.test( "add global variable", async assert => { + try { + await execute( "qunit noglobals/add-global.js" ); + } catch ( e ) { + assert.pushResult( { + result: e.stdout.indexOf( "message: Introduced global variable(s): dummyGlobal" ) > -1, + actual: e.stdout + "\n" + e.stderr + } ); + } + } ); + + QUnit.test( "remove global variable", async assert => { + try { + await execute( "qunit noglobals/remove-global.js" ); + } catch ( e ) { + assert.pushResult( { + result: e.stdout.indexOf( "message: Deleted global variable(s): dummyGlobal" ) > -1, + actual: e.stdout + "\n" + e.stderr + } ); + } + } ); + + QUnit.test( "forgive qunit DOM global variables", async assert => { + const execution = await execute( "qunit noglobals/ignored.js" ); + assert.equal( execution.code, 0 ); + assert.equal( execution.stderr, "" ); + } ); + } ); + + QUnit.module( "semaphore", () => { + QUnit.test( "invalid value", async assert => { + try { + await execute( "qunit semaphore/nan.js" ); + } catch ( e ) { + assert.pushResult( { + result: e.stdout.indexOf( "message: Invalid value on test.semaphore" ) > -1, + actual: e.stdout + "\n" + e.stderr + } ); + } + } ); + + QUnit.test( "try to restart ", async assert => { + try { + await execute( "qunit semaphore/restart.js" ); + } catch ( e ) { + assert.pushResult( { + result: e.stdout.indexOf( "message: \"Tried to restart test while already started (test's semaphore was 0 already)" ) > -1, + actual: e.stdout + "\n" + e.stderr + } ); + } + } ); } ); QUnit.module( "assert.async", () => { @@ -375,4 +497,55 @@ QUnit.module( "CLI Main", () => { assert.equal( execution.stderr, "The `beforeEach` hook was called inside the wrong module. Instead, use hooks provided by the callback to the containing module. This will become an error in QUnit 3.0.", "The warning shows" ); assert.equal( execution.stdout, expectedOutput[ command ] ); } ); + + QUnit.module( "assert.expect failing conditions", () => { + QUnit.test( "mismatched expected assertions", async assert => { + const command = "qunit assert-expect/failing-expect.js"; + try { + const result = await execute( command ); + assert.pushResult( { + result: false, + actual: result.stdout + } ); + } catch ( e ) { + assert.equal( e.code, 1 ); + assert.equal( e.stderr, "" ); + + // can't match exactly due to stack frames including internal line numbers + assert.notEqual( e.stdout.indexOf( "message: Expected 2 assertions, but 1 were run" ), -1, e.stdout ); + } + } ); + + QUnit.test( "no assertions run - use expect(0)", async assert => { + const command = "qunit assert-expect/no-assertions.js"; + try { + const result = await execute( command ); + assert.pushResult( { + result: true, + actual: result.stdout + } ); + } catch ( e ) { + assert.equal( e.code, 1 ); + assert.equal( e.stderr, "" ); + + // can't match exactly due to stack frames including internal line numbers + assert.notEqual( e.stdout.indexOf( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions." ), -1, e.stdout ); + } + } ); + + QUnit.test( "requireExpects", async assert => { + const command = "qunit assert-expect/require-expects.js"; + try { + const result = await execute( command ); + assert.pushResult( { + result: false, + actual: result.stdout + } ); + } catch ( e ) { + assert.equal( e.code, 1 ); + assert.equal( e.stderr, "" ); + assert.notEqual( e.stdout.indexOf( "message: Expected number of assertions to be defined, but expect() was not called." ), -1, e.stdout ); + } + } ); + } ); } ); diff --git a/test/main/deepEqual.js b/test/main/deepEqual.js index 522fe1245..c2b707c79 100644 --- a/test/main/deepEqual.js +++ b/test/main/deepEqual.js @@ -121,6 +121,20 @@ QUnit.test( "Primitive types and constants", function( assert ) { assert.equal( QUnit.equiv( new SafeObject(), { a: undefined } ), false, "empty object instantiation vs. other nonempty literal" ); } ); +QUnit.test( "Consecutive argument pairs", function( assert ) { + + // degenerate cases with <2 inputs + assert.equal( QUnit.equiv( ), true ); + assert.equal( QUnit.equiv( 1 ), true ); + + // otherwise every "consecutive pair" must be equivalent + assert.equal( QUnit.equiv( 1, 1, 1 ), true ); + assert.equal( QUnit.equiv( 1, 1, 2 ), false ); + assert.equal( QUnit.equiv( 1, 1, 1, 1 ), true ); + assert.equal( QUnit.equiv( 1, 1, 1, 2 ), false ); + assert.equal( QUnit.equiv( 1, 1, 1, 1, 1 ), true ); +} ); + QUnit.test( "Objects basics", function( assert ) { assert.equal( QUnit.equiv( {}, {} ), true ); assert.equal( QUnit.equiv( {}, null ), false ); @@ -1806,6 +1820,10 @@ QUnit[ hasES6Set ? "test" : "skip" ]( "Sets", function( assert ) { s2 = new Set( [ undefined, null, false, 0, NaN, Infinity, -Infinity ] ); assert.equal( QUnit.equiv( s1, s2 ), true, "Multiple-element sets of tricky values" ); + s1 = new Set( [ 1, 3 ] ); + s2 = new Set( [ 2, 3 ] ); + assert.equal( QUnit.equiv( s1, s2 ), false, "Sets can 'short-circuit' for early failure" ); + // Sets Containing objects o1 = { foo: 0, bar: true }; o2 = { foo: 0, bar: true }; @@ -1870,6 +1888,11 @@ QUnit[ hasES6Map ? "test" : "skip" ]( "Maps", function( assert ) { assert.equal( QUnit.equiv( m1, m2 ), true, "Single element maps [1,1] vs [1,1]" ); assert.equal( QUnit.equiv( m1, m3 ), false, "Single element maps [1,1] vs [1,3]" ); + // Mismatched sizes + m1 = new Map( [ [ 1, 2 ] ] ); + m2 = new Map( [ [ 3, 4], [ 5, 6 ] ] ); + assert.equal( QUnit.equiv( m1, m2 ), false, "Compare maps with mismatch sizes" ); + // Tricky values m1 = new Map( [ [ false, false ], diff --git a/test/reporter-html/diff.js b/test/reporter-html/diff.js index 3fb69d73a..b9d9fa6cb 100644 --- a/test/reporter-html/diff.js +++ b/test/reporter-html/diff.js @@ -158,3 +158,22 @@ QUnit.test( "equal values", function( assert ) { "" ); } ); + +QUnit.test( "Edge cases", function( assert ) { + assert.throws( + function() { + QUnit.diff( "abc", null ); + }, + /Error: Null input. \(DiffMain\)/ ); + + // this hits several hard-to-reach cases for the sake of code coverage + var X = "x".repeat( 100 ); + var Y = "y".repeat( 100 ); + assert.equal( + QUnit.diff( + "A\nB\n" + X + "\nD", + "A\nCCC\nB\nCCC\n" + Y + "\nD" ), + "A\nCCC\nB\n" + + "" + X + "CCC\n" + Y + "" + + "\nD" ); +} );