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" );
+} );