Repository: nodejs/node
Pull request: test-runner: fix suite timeout

Fixes a regression introduced at https://github.com/nodejs/node/pull/57672 where suite timeout option never takes effect

Changed files:
filename: lib/internal/test_runner/test.js
patch: @@ -1455,7 +1455,9 @@ class Suite extends Test {
   reportedType = 'suite';
   constructor(options) {
     super(options);
-    this.timeout = null;
+    if (options.timeout == null) {
+      this.timeout = null;
+    }
 
     if (this.config.testNamePatterns !== null &&
         this.config.testSkipPatterns !== null &&

filename: test/fixtures/test-runner/output/test-timeout-flag.js
patch: @@ -2,10 +2,10 @@
 const { describe, it, after } = require('node:test');
 const { setTimeout } = require('node:timers');
 
-const timeoutRefs = [];
+describe('--test-timeout is set to 100ms', () => {
+  const timeoutRefs = [];
 
-describe('--test-timeout is set to 20ms', () => {
-  it('should timeout after 20ms', async () => {
+  it('should timeout after 100ms', async () => {
     const { promise, resolve } = Promise.withResolvers();
     timeoutRefs.push(setTimeout(() => {
       resolve();
@@ -37,3 +37,22 @@ describe('--test-timeout is set to 20ms', () => {
     }
   });
 });
+
+
+describe('should inherit timeout options to children', { timeout: 1 }, () => {
+  const timeoutRefs = [];
+
+  after(() => {
+    for (const timeoutRef of timeoutRefs) {
+      clearTimeout(timeoutRef);
+    }
+  });
+
+  it('should timeout after 1ms', async () => {
+    const { promise, resolve } = Promise.withResolvers();
+    timeoutRefs.push(setTimeout(() => {
+      resolve();
+    }, 20000));
+    await promise;
+  });
+});

filename: test/fixtures/test-runner/output/test-timeout-flag.snapshot
patch: @@ -1,13 +1,13 @@
 TAP version 13
-# Subtest: --test-timeout is set to 20ms
-    # Subtest: should timeout after 20ms
-    not ok 1 - should timeout after 20ms
+# Subtest: --test-timeout is set to 100ms
+    # Subtest: should timeout after 100ms
+    not ok 1 - should timeout after 100ms
       ---
       duration_ms: *
       type: 'test'
       location: '/test/fixtures/test-runner/output/test-timeout-flag.js:(LINE):3'
       failureType: 'testTimeoutFailure'
-      error: 'test timed out after 20ms'
+      error: 'test timed out after 100ms'
       code: 'ERR_TEST_FAILURE'
       stack: |-
         async Promise.all (index 0)
@@ -35,7 +35,7 @@ TAP version 13
       type: 'test'
       ...
     1..4
-not ok 1 - --test-timeout is set to 20ms
+not ok 1 - --test-timeout is set to 100ms
   ---
   duration_ms: *
   type: 'suite'
@@ -44,12 +44,33 @@ not ok 1 - --test-timeout is set to 20ms
   error: '2 subtests failed'
   code: 'ERR_TEST_FAILURE'
   ...
-1..1
-# tests 4
-# suites 1
+# Subtest: should inherit timeout options to children
+    # Subtest: should timeout after 1ms
+    not ok 1 - should timeout after 1ms
+      ---
+      duration_ms: *
+      type: 'test'
+      location: '/test/fixtures/test-runner/output/test-timeout-flag.js:(LINE):3'
+      failureType: 'cancelledByParent'
+      error: 'test did not finish before its parent and was cancelled'
+      code: 'ERR_TEST_FAILURE'
+      ...
+    1..1
+not ok 2 - should inherit timeout options to children
+  ---
+  duration_ms: *
+  type: 'suite'
+  location: '/test/fixtures/test-runner/output/test-timeout-flag.js:(LINE):1'
+  failureType: 'testTimeoutFailure'
+  error: 'test timed out after 1ms'
+  code: 'ERR_TEST_FAILURE'
+  ...
+1..2
+# tests 5
+# suites 2
 # pass 2
 # fail 0
-# cancelled 2
+# cancelled 3
 # skipped 0
 # todo 0
 # duration_ms *

filename: test/parallel/test-runner-output.mjs
patch: @@ -137,15 +137,15 @@ const tests = [
     name: 'test-runner/output/test-timeout-flag.js',
     flags: [
       '--test-reporter=tap',
-      '--test-timeout=20',
+      '--test-timeout=100',
     ],
   },
   // --test-timeout should work with or without --test flag
   {
     name: 'test-runner/output/test-timeout-flag.js',
     flags: [
       '--test-reporter=tap',
-      '--test-timeout=20',
+      '--test-timeout=100',
       '--test',
     ],
   },