diff --git a/lib/internal/test_runner/utils.js b/lib/internal/test_runner/utils.js index 5b53342933cdcb..db7e2ee50dd8d6 100644 --- a/lib/internal/test_runner/utils.js +++ b/lib/internal/test_runner/utils.js @@ -384,6 +384,9 @@ function countCompletedTest(test, harness = test.root.harness) { } if (test.reportedType === 'suite') { harness.counters.suites++; + if (!test.passed) { + harness.success = false; + } return; } // Check SKIP and TODO tests first, as those should not be counted as diff --git a/test/fixtures/test-runner/describe_error.js b/test/fixtures/test-runner/describe_error.js new file mode 100644 index 00000000000000..04e9d1faa042d1 --- /dev/null +++ b/test/fixtures/test-runner/describe_error.js @@ -0,0 +1,10 @@ +'use strict'; +const { describe, it } = require('node:test'); + +describe('should fail', () => { + throw new Error('error in describe'); +}); + +describe('should pass', () => { + it('ok', () => {}); +}); diff --git a/test/parallel/test-runner-exit-code.js b/test/parallel/test-runner-exit-code.js index 4024a52841bb28..c25becee3f708f 100644 --- a/test/parallel/test-runner-exit-code.js +++ b/test/parallel/test-runner-exit-code.js @@ -70,6 +70,14 @@ if (process.argv[2] === 'child') { assert.strictEqual(child.status, 1); assert.strictEqual(child.signal, null); + // An error thrown inside describe() should cause a non-zero exit code. + child = spawnSync(process.execPath, [ + '--test', + fixtures.path('test-runner', 'describe_error.js'), + ]); + assert.strictEqual(child.status, 1); + assert.strictEqual(child.signal, null); + // With process isolation (default), the test name shown is the file path // because the parent runner only knows about file-level tests const neverEndingSync = fixtures.path('test-runner', 'never_ending_sync.js');