diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js
index 034840f22176aa..4cee29ee0c9c65 100644
--- a/lib/internal/test_runner/test.js
+++ b/lib/internal/test_runner/test.js
@@ -1,5 +1,6 @@
'use strict';
const {
+ ArrayPrototypeJoin,
ArrayPrototypePush,
ArrayPrototypePushApply,
ArrayPrototypeShift,
@@ -1360,6 +1361,18 @@ class Test extends AsyncResource {
if (this.passedAttempt !== undefined) {
details.passed_on_attempt = this.passedAttempt;
}
+
+ // Generate classname from suite hierarchy for JUnit reporter
+ if (this.parent && this.parent !== this.root) {
+ const parts = [];
+ for (let t = this.parent; t !== t.root; t = t.parent) {
+ ArrayPrototypeUnshift(parts, t.name);
+ }
+ if (parts.length > 0) {
+ details.classname = ArrayPrototypeJoin(parts, '.');
+ }
+ }
+
return { __proto__: null, details, directive };
}
diff --git a/lib/internal/test_runner/tests_stream.js b/lib/internal/test_runner/tests_stream.js
index 318d7f49998c0e..02a7fde60b9733 100644
--- a/lib/internal/test_runner/tests_stream.js
+++ b/lib/internal/test_runner/tests_stream.js
@@ -41,6 +41,7 @@ class TestsStream extends Readable {
nesting,
testNumber,
details,
+ ...(details.classname && { __proto__: null, classname: details.classname }),
...loc,
...directive,
});
@@ -53,6 +54,7 @@ class TestsStream extends Readable {
nesting,
testNumber,
details,
+ ...(details.classname && { __proto__: null, classname: details.classname }),
...loc,
...directive,
});
diff --git a/test/fixtures/test-runner/output/junit_classname_hierarchy.js b/test/fixtures/test-runner/output/junit_classname_hierarchy.js
new file mode 100644
index 00000000000000..8b634be39380e7
--- /dev/null
+++ b/test/fixtures/test-runner/output/junit_classname_hierarchy.js
@@ -0,0 +1,19 @@
+'use strict';
+require('../../../common');
+const { suite, test } = require('node:test');
+
+suite('Math', () => {
+ suite('Addition', () => {
+ test('adds positive numbers', () => {});
+ });
+
+ suite('Multiplication', () => {
+ test('multiplies positive numbers', () => {});
+ });
+});
+
+suite('String', () => {
+ test('concatenates strings', () => {});
+});
+
+test('standalone test', () => {});
diff --git a/test/fixtures/test-runner/output/junit_classname_hierarchy.snapshot b/test/fixtures/test-runner/output/junit_classname_hierarchy.snapshot
new file mode 100644
index 00000000000000..8b645b067d2dc5
--- /dev/null
+++ b/test/fixtures/test-runner/output/junit_classname_hierarchy.snapshot
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/test-runner/output/junit_reporter.snapshot b/test/fixtures/test-runner/output/junit_reporter.snapshot
index e955f9666049ca..67154be7fbce2d 100644
--- a/test/fixtures/test-runner/output/junit_reporter.snapshot
+++ b/test/fixtures/test-runner/output/junit_reporter.snapshot
@@ -153,7 +153,7 @@ true !== false
-
+
Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fail
*
@@ -182,15 +182,15 @@ Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fail
-
-
-
-
+
+
+
+
-
+
-
+
@@ -307,9 +307,9 @@ Error [ERR_TEST_FAILURE]: thrown from callback async throw
-
-
-
+
+
+
@@ -329,7 +329,7 @@ Error [ERR_TEST_FAILURE]: thrown from callback async throw
-
+
Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fails at first
*
@@ -350,7 +350,7 @@ Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fails at first
}
-
+
Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fails at second
* {
diff --git a/test/parallel/test-runner-output.mjs b/test/parallel/test-runner-output.mjs
index f854447c4526b1..a4f2377b95c187 100644
--- a/test/parallel/test-runner-output.mjs
+++ b/test/parallel/test-runner-output.mjs
@@ -181,6 +181,11 @@ const tests = [
{ name: 'test-runner/output/only_tests.js', flags: ['--test-reporter=tap'] },
{ name: 'test-runner/output/dot_reporter.js', transform: specTransform },
{ name: 'test-runner/output/junit_reporter.js', transform: junitTransform },
+ {
+ name: 'test-runner/output/junit_classname_hierarchy.js',
+ flags: ['--test-reporter=junit'],
+ transform: junitTransform,
+ },
{ name: 'test-runner/output/spec_reporter_successful.js', transform: specTransform },
{ name: 'test-runner/output/spec_reporter.js', transform: specTransform },
{ name: 'test-runner/output/spec_reporter_cli.js', transform: specTransform },
diff --git a/test/parallel/test-runner-reporters.js b/test/parallel/test-runner-reporters.js
index 50a47578a1da7e..63dac1346c90ef 100644
--- a/test/parallel/test-runner-reporters.js
+++ b/test/parallel/test-runner-reporters.js
@@ -201,7 +201,7 @@ describe('node:test reporters', { concurrency: true }, () => {
const fileContents = fs.readFileSync(file, 'utf8');
assert.match(fileContents, //);
assert.match(fileContents, /\s*/);
- assert.match(fileContents, //);
+ assert.match(fileContents, //);
assert.match(fileContents, //);
});
});