test_runner: print failing assertion only once with spec reporter
Some checks are pending
Coverage Linux (without intl) / coverage-linux-without-intl (push) Waiting to run
Coverage Linux / coverage-linux (push) Waiting to run
Coverage Windows / coverage-windows (push) Waiting to run
Test and upload documentation to artifacts / build-docs (push) Waiting to run
Linters / lint-addon-docs (push) Waiting to run
Linters / lint-cpp (push) Waiting to run
Linters / format-cpp (push) Waiting to run
Linters / lint-js-and-md (push) Waiting to run
Linters / lint-py (push) Waiting to run
Linters / lint-yaml (push) Waiting to run
Linters / lint-sh (push) Waiting to run
Linters / lint-codeowners (push) Waiting to run
Linters / lint-pr-url (push) Waiting to run
Linters / lint-readme (push) Waiting to run
Notify on Push / Notify on Force Push on `main` (push) Waiting to run
Notify on Push / Notify on Push on `main` that lacks metadata (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run

Co-authored-by: Llorx <dallorx@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/56662
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Pietro Marchini 2025-01-19 23:57:49 +01:00 committed by James M Snell
parent 01a5aa2ac1
commit c752615e2b
8 changed files with 5 additions and 782 deletions

View File

@ -52,7 +52,7 @@ class SpecReporter extends Transform {
hasChildren = true;
}
const indentation = indent(data.nesting);
return `${formatTestReport(type, data, prefix, indentation, hasChildren)}\n`;
return `${formatTestReport(type, data, prefix, indentation, hasChildren, false)}\n`;
}
#handleEvent({ type, data }) {
switch (type) {

View File

@ -59,7 +59,7 @@ function formatError(error, indent) {
return `\n${indent} ${message}\n`;
}
function formatTestReport(type, data, prefix = '', indent = '', hasChildren = false) {
function formatTestReport(type, data, prefix = '', indent = '', hasChildren = false, showErrorDetails = true) {
let color = reporterColorMap[type] ?? colors.white;
let symbol = reporterUnicodeSymbolMap[type] ?? ' ';
const { skip, todo } = data;
@ -71,10 +71,12 @@ function formatTestReport(type, data, prefix = '', indent = '', hasChildren = fa
} else if (todo !== undefined) {
title += ` # ${typeof todo === 'string' && todo.length ? todo : 'TODO'}`;
}
const error = formatError(data.details?.error, indent);
const error = showErrorDetails ? formatError(data.details?.error, indent) : '';
const err = hasChildren ?
(!error || data.details?.error?.failureType === 'subtestsFailed' ? '' : `\n${error}`) :
error;
if (skip !== undefined) {
color = colors.gray;
symbol = reporterUnicodeSymbolMap['hyphen:minus'];

View File

@ -1,16 +1,4 @@
[31m✖ failing assertion [90m(*ms)[39m[39m
[AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
[32mactual[39m [31mexpected[39m
[39m'[39m[32m![39m[39mH[39m[39me[39m[39ml[39m[39ml[39m[39mo[39m[39m [39m[39mW[39m[39mo[39m[39mr[39m[39ml[39m[39md[39m[31m![39m[39m'[39m
] {
generatedMessage: [33mtrue[39m,
code: [32m'ERR_ASSERTION'[39m,
actual: [32m'!Hello World'[39m,
expected: [32m'Hello World!'[39m,
operator: [32m'strictEqual'[39m
}
[34m tests 1[39m
[34m suites 0[39m
[34m pass 0[39m

View File

@ -1,32 +1,9 @@
[32m✔ should pass [90m(*ms)[39m[39m
[31m✖ should fail [90m(*ms)[39m[39m
Error: fail
*[39m
*[39m
*[39m
*[39m
*[39m
*[39m
*[39m
[90m﹣ should skip [90m(*ms)[39m # SKIP[39m
▶ parent
[31m✖ should fail [90m(*ms)[39m[39m
Error: fail
*[39m
*[39m
*[39m
*[39m
*[39m
*[39m
*[39m
*[39m
*[39m
*[39m
[31m✖ should pass but parent fail [90m(*ms)[39m[39m
[32m'test did not finish before its parent and was cancelled'[39m
[31m✖ parent [90m(*ms)[39m[39m
[34m tests 6[39m
[34m suites 0[39m

View File

@ -1,14 +1,5 @@
✔ passes (*ms)
✖ fails (*ms)
Error: fail
*
*
*
*
*
*
*
tests 2
suites 0
pass 1

View File

@ -10,161 +10,29 @@
describe hooks - no subtests (*ms)
before throws
1
'test did not finish before its parent and was cancelled'
2
'test did not finish before its parent and was cancelled'
before throws (*ms)
Error: before
*
*
*
*
*
*
*
*
before throws - no subtests (*ms)
Error: before
*
*
*
*
*
*
*
*
after throws
1 (*ms)
2 (*ms)
after throws (*ms)
Error: after
*
*
*
*
*
*
*
*
*
*
after throws - no subtests (*ms)
Error: after
*
*
*
*
*
*
*
*
*
*
beforeEach throws
1 (*ms)
Error: beforeEach
*
*
*
*
*
*
*
*
*
at new Promise (<anonymous>)
2 (*ms)
Error: beforeEach
*
*
*
*
*
*
*
*
*
at async Promise.all (index 0)
beforeEach throws (*ms)
afterEach throws
1 (*ms)
Error: afterEach
*
*
*
*
*
*
*
*
at async Promise.all (index 0)
*
2 (*ms)
Error: afterEach
*
*
*
*
*
*
*
*
*
afterEach throws (*ms)
afterEach when test fails
1 (*ms)
Error: test
*
*
*
*
*
*
at new Promise (<anonymous>)
*
*
at Array.map (<anonymous>)
2 (*ms)
afterEach when test fails (*ms)
afterEach throws and test fails
1 (*ms)
Error: test
*
*
*
*
*
*
at new Promise (<anonymous>)
*
*
at Array.map (<anonymous>)
2 (*ms)
Error: afterEach
*
*
*
*
*
*
*
*
*
afterEach throws and test fails (*ms)
test hooks
1 (*ms)
@ -177,155 +45,24 @@
test hooks - no subtests (*ms)
t.before throws
1 (*ms)
Error: before
*
*
*
*
*
*
*
*
*
*
2 (*ms)
Error: before
*
*
*
*
*
*
*
*
*
*
t.before throws (*ms)
Error: before
*
*
*
*
*
*
*
*
*
*
t.before throws - no subtests (*ms)
Error: before
*
*
*
*
*
*
*
*
*
*
t.after throws
1 (*ms)
2 (*ms)
t.after throws (*ms)
Error: after
*
*
*
*
*
*
*
*
*
t.after throws - no subtests (*ms)
Error: after
*
*
*
*
*
*
*
*
*
t.beforeEach throws
1 (*ms)
Error: beforeEach
*
*
*
*
*
*
*
*
*
*
2 (*ms)
Error: beforeEach
*
*
*
*
*
*
*
*
*
*
t.beforeEach throws (*ms)
t.afterEach throws
1 (*ms)
Error: afterEach
*
*
*
*
*
*
*
*
*
*
2 (*ms)
Error: afterEach
*
*
*
*
*
*
*
*
*
*
t.afterEach throws (*ms)
afterEach when test fails
1 (*ms)
Error: test
*
*
*
*
*
*
*
*
*
2 (*ms)
afterEach when test fails (*ms)
afterEach context when test passes
@ -333,64 +70,16 @@
afterEach context when test passes (*ms)
afterEach context when test fails
1 (*ms)
Error: test
*
*
*
*
afterEach context when test fails (*ms)
afterEach throws and test fails
1 (*ms)
Error: test
*
*
*
*
*
*
*
*
*
2 (*ms)
Error: afterEach
*
*
*
*
*
*
*
*
*
*
afterEach throws and test fails (*ms)
t.after() is called if test body throws (*ms)
Error: bye
*
*
*
*
- after() called
run after when before throws
1
'test did not finish before its parent and was cancelled'
run after when before throws (*ms)
Error: before
*
*
*
*
*
*
*
*
test hooks - async
1 (*ms)
2 (*ms)

View File

@ -1,91 +1,19 @@
sync pass todo (*ms) # TODO
sync pass todo with message (*ms) # this is a passing todo
sync fail todo (*ms) # TODO
Error: thrown from sync fail todo
*
*
*
*
*
*
*
sync fail todo with message (*ms) # this is a failing todo
Error: thrown from sync fail todo with message
*
*
*
*
*
*
*
sync skip pass (*ms) # SKIP
sync skip pass with message (*ms) # this is skipped
sync pass (*ms)
this test should pass
sync throw fail (*ms)
Error: thrown from sync throw fail
*
*
*
*
*
*
*
async skip pass (*ms) # SKIP
async pass (*ms)
async throw fail (*ms)
Error: thrown from async throw fail
*
*
*
*
*
*
*
async skip fail (*ms) # SKIP
Error: thrown from async throw fail
*
*
*
*
*
*
*
async assertion fail (*ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
true !== false
*
*
*
*
*
*
* {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: true,
expected: false,
operator: 'strictEqual'
}
resolve pass (*ms)
reject fail (*ms)
Error: rejected from reject fail
*
*
*
*
*
*
*
unhandled rejection - passes but warns (*ms)
async unhandled rejection - passes but warns (*ms)
immediate throw - passes but warns (*ms)
@ -93,23 +21,9 @@
immediate resolve pass (*ms)
subtest sync throw fail
+sync throw fail (*ms)
Error: thrown from subtest sync throw fail
*
*
*
*
*
*
*
*
*
*
this subtest should make its parent test fail
subtest sync throw fail (*ms)
sync throw non-error fail (*ms)
Symbol(thrown symbol from sync throw non-error fail)
level 0a
level 1a (*ms)
level 1b (*ms)
@ -118,8 +32,6 @@
level 0a (*ms)
top level
+long running (*ms)
'test did not finish before its parent and was cancelled'
+short running
++short running (*ms)
+short running (*ms)
@ -128,15 +40,6 @@
sync skip option (*ms) # SKIP
sync skip option with message (*ms) # this is skipped
sync skip option is false fail (*ms)
Error: this should be executed
*
*
*
*
*
*
*
<anonymous> (*ms)
functionOnly (*ms)
<anonymous> (*ms)
@ -147,43 +50,15 @@
functionAndOptions (*ms) # SKIP
callback pass (*ms)
callback fail (*ms)
Error: callback failure
*
*
sync t is this in test (*ms)
async t is this in test (*ms)
callback t is this in test (*ms)
callback also returns a Promise (*ms)
'passed a callback but also returned a Promise'
callback throw (*ms)
Error: thrown from callback throw
*
*
*
*
*
*
*
callback called twice (*ms)
'callback invoked multiple times'
callback called twice in different ticks (*ms)
callback called twice in future tick (*ms)
Error [ERR_TEST_FAILURE]: callback invoked multiple times
* {
code: 'ERR_TEST_FAILURE',
failureType: 'multipleCallbackInvocations',
cause: 'callback invoked multiple times'
}
callback async throw (*ms)
Error: thrown from callback async throw
*
*
callback async throw after done (*ms)
only is set on subtests but not in only mode
running subtest 1 (*ms)
@ -191,108 +66,21 @@
running subtest 4 (*ms)
only is set on subtests but not in only mode (*ms)
custom inspect symbol fail (*ms)
customized
custom inspect symbol that throws fail (*ms)
{ foo: 1, Symbol(nodejs.util.inspect.custom): [Function: [nodejs.util.inspect.custom]] }
subtest sync throw fails
sync throw fails at first (*ms)
Error: thrown from subtest sync throw fails at first
*
*
*
*
*
*
*
*
*
*
sync throw fails at second (*ms)
Error: thrown from subtest sync throw fails at second
*
*
*
*
*
*
*
*
subtest sync throw fails (*ms)
timed out async test (*ms)
'test timed out after *ms'
timed out callback test (*ms)
'test timed out after *ms'
large timeout async test is ok (*ms)
large timeout callback test is ok (*ms)
successful thenable (*ms)
rejected thenable (*ms)
'custom error'
unfinished test with uncaughtException (*ms)
Error: foo
*
*
*
unfinished test with unhandledRejection (*ms)
Error: bar
*
*
*
assertion errors display actual and expected properly (*ms)
AssertionError [ERR_ASSERTION]: Expected values to be loosely deep-equal:
{
bar: 1,
baz: {
date: 1970-01-01T00:00:00.000Z,
null: null,
number: 1,
string: 'Hello',
undefined: undefined
},
boo: [
1
],
foo: 1
}
should loosely deep-equal
{
baz: {
date: 1970-01-01T00:00:00.000Z,
null: null,
number: 1,
string: 'Hello',
undefined: undefined
},
boo: [
1
],
circular: <ref *1> {
bar: 2,
c: [Circular *1]
}
}
* {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: [Object],
expected: [Object],
operator: 'deepEqual'
}
invalid subtest fail (*ms)
'test could not be started because its parent finished'
Error: Test "unhandled rejection - passes but warns" at test/fixtures/test-runner/output/output.js:72:1 generated asynchronous activity after the test ended. This activity created the error "Error: rejected from unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
Error: Test "async unhandled rejection - passes but warns" at test/fixtures/test-runner/output/output.js:76:1 generated asynchronous activity after the test ended. This activity created the error "Error: rejected from async unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
Error: A resource generated asynchronous activity after the test ended. This activity created the error "Error: uncaught from outside of a test" which triggered an uncaughtException event, caught by the test runner.

View File

@ -1,91 +1,19 @@
sync pass todo (*ms) # TODO
sync pass todo with message (*ms) # this is a passing todo
sync fail todo (*ms) # TODO
Error: thrown from sync fail todo
*
*
*
*
*
*
*
sync fail todo with message (*ms) # this is a failing todo
Error: thrown from sync fail todo with message
*
*
*
*
*
*
*
sync skip pass (*ms) # SKIP
sync skip pass with message (*ms) # this is skipped
sync pass (*ms)
this test should pass
sync throw fail (*ms)
Error: thrown from sync throw fail
*
*
*
*
*
*
*
async skip pass (*ms) # SKIP
async pass (*ms)
async throw fail (*ms)
Error: thrown from async throw fail
*
*
*
*
*
*
*
async skip fail (*ms) # SKIP
Error: thrown from async throw fail
*
*
*
*
*
*
*
async assertion fail (*ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
true !== false
*
*
*
*
*
*
* {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: true,
expected: false,
operator: 'strictEqual'
}
resolve pass (*ms)
reject fail (*ms)
Error: rejected from reject fail
*
*
*
*
*
*
*
unhandled rejection - passes but warns (*ms)
async unhandled rejection - passes but warns (*ms)
immediate throw - passes but warns (*ms)
@ -93,23 +21,9 @@
immediate resolve pass (*ms)
subtest sync throw fail
+sync throw fail (*ms)
Error: thrown from subtest sync throw fail
*
*
*
*
*
*
*
*
*
*
this subtest should make its parent test fail
subtest sync throw fail (*ms)
sync throw non-error fail (*ms)
Symbol(thrown symbol from sync throw non-error fail)
level 0a
level 1a (*ms)
level 1b (*ms)
@ -118,8 +32,6 @@
level 0a (*ms)
top level
+long running (*ms)
'test did not finish before its parent and was cancelled'
+short running
++short running (*ms)
+short running (*ms)
@ -128,15 +40,6 @@
sync skip option (*ms) # SKIP
sync skip option with message (*ms) # this is skipped
sync skip option is false fail (*ms)
Error: this should be executed
*
*
*
*
*
*
*
<anonymous> (*ms)
functionOnly (*ms)
<anonymous> (*ms)
@ -147,43 +50,15 @@
functionAndOptions (*ms) # SKIP
callback pass (*ms)
callback fail (*ms)
Error: callback failure
*
*
sync t is this in test (*ms)
async t is this in test (*ms)
callback t is this in test (*ms)
callback also returns a Promise (*ms)
'passed a callback but also returned a Promise'
callback throw (*ms)
Error: thrown from callback throw
*
*
*
*
*
*
*
callback called twice (*ms)
'callback invoked multiple times'
callback called twice in different ticks (*ms)
callback called twice in future tick (*ms)
Error [ERR_TEST_FAILURE]: callback invoked multiple times
* {
code: 'ERR_TEST_FAILURE',
failureType: 'multipleCallbackInvocations',
cause: 'callback invoked multiple times'
}
callback async throw (*ms)
Error: thrown from callback async throw
*
*
callback async throw after done (*ms)
only is set on subtests but not in only mode
running subtest 1 (*ms)
@ -194,108 +69,21 @@
running subtest 4 (*ms)
only is set on subtests but not in only mode (*ms)
custom inspect symbol fail (*ms)
customized
custom inspect symbol that throws fail (*ms)
{ foo: 1 }
subtest sync throw fails
sync throw fails at first (*ms)
Error: thrown from subtest sync throw fails at first
*
*
*
*
*
*
*
*
*
*
sync throw fails at second (*ms)
Error: thrown from subtest sync throw fails at second
*
*
*
*
*
*
*
*
subtest sync throw fails (*ms)
timed out async test (*ms)
'test timed out after *ms'
timed out callback test (*ms)
'test timed out after *ms'
large timeout async test is ok (*ms)
large timeout callback test is ok (*ms)
successful thenable (*ms)
rejected thenable (*ms)
'custom error'
unfinished test with uncaughtException (*ms)
Error: foo
*
*
*
unfinished test with unhandledRejection (*ms)
Error: bar
*
*
*
assertion errors display actual and expected properly (*ms)
AssertionError [ERR_ASSERTION]: Expected values to be loosely deep-equal:
{
bar: 1,
baz: {
date: 1970-01-01T00:00:00.000Z,
null: null,
number: 1,
string: 'Hello',
undefined: undefined
},
boo: [
1
],
foo: 1
}
should loosely deep-equal
{
baz: {
date: 1970-01-01T00:00:00.000Z,
null: null,
number: 1,
string: 'Hello',
undefined: undefined
},
boo: [
1
],
circular: <ref *1> {
bar: 2,
c: [Circular *1]
}
}
* {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: { foo: 1, bar: 1, boo: [ 1 ], baz: { date: 1970-01-01T00:00:00.000Z, null: null, number: 1, string: 'Hello', undefined: undefined } },
expected: { boo: [ 1 ], baz: { date: 1970-01-01T00:00:00.000Z, null: null, number: 1, string: 'Hello', undefined: undefined }, circular: <ref *1> { bar: 2, c: [Circular *1] } },
operator: 'deepEqual'
}
invalid subtest fail (*ms)
'test could not be started because its parent finished'
Error: Test "unhandled rejection - passes but warns" at test/fixtures/test-runner/output/output.js:72:1 generated asynchronous activity after the test ended. This activity created the error "Error: rejected from unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
Error: Test "async unhandled rejection - passes but warns" at test/fixtures/test-runner/output/output.js:76:1 generated asynchronous activity after the test ended. This activity created the error "Error: rejected from async unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
Error: A resource generated asynchronous activity after the test ended. This activity created the error "Error: uncaught from outside of a test" which triggered an uncaughtException event, caught by the test runner.