diff --git a/doc/api/test.md b/doc/api/test.md index f88d3542f954bf..1f031807dd830b 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -2944,6 +2944,11 @@ defined. The corresponding declaration ordered event is `'test:start'`. `undefined` if the test was run through the REPL. * `message` {string} The diagnostic message. * `nesting` {number} The nesting level of the test. + * `level` {string} The severity level of the diagnostic message. + Possible values are: + * `'info'`: Informational messages. + * `'warn'`: Warnings. + * `'error'`: Errors. Emitted when [`context.diagnostic`][] is called. This event is guaranteed to be emitted in the same order as the tests are diff --git a/lib/internal/test_runner/reporter/spec.js b/lib/internal/test_runner/reporter/spec.js index 2092d22e3fe77f..e45351a79f560c 100644 --- a/lib/internal/test_runner/reporter/spec.js +++ b/lib/internal/test_runner/reporter/spec.js @@ -69,8 +69,10 @@ class SpecReporter extends Transform { case 'test:stderr': case 'test:stdout': return data.message; - case 'test:diagnostic': - return `${reporterColorMap[type]}${indent(data.nesting)}${reporterUnicodeSymbolMap[type]}${data.message}${colors.white}\n`; + case 'test:diagnostic':{ + const diagnosticColor = reporterColorMap[data.level] || reporterColorMap['test:diagnostic']; + return `${diagnosticColor}${indent(data.nesting)}${reporterUnicodeSymbolMap[type]}${data.message}${colors.white}\n`; + } case 'test:coverage': return getCoverageReport(indent(data.nesting), data.summary, reporterUnicodeSymbolMap['test:coverage'], colors.blue, true); diff --git a/lib/internal/test_runner/reporter/utils.js b/lib/internal/test_runner/reporter/utils.js index 9b6cc96a185a9a..8bb881a0ca934f 100644 --- a/lib/internal/test_runner/reporter/utils.js +++ b/lib/internal/test_runner/reporter/utils.js @@ -37,6 +37,15 @@ const reporterColorMap = { get 'test:diagnostic'() { return colors.blue; }, + get 'info'() { + return colors.blue; + }, + get 'warn'() { + return colors.yellow; + }, + get 'error'() { + return colors.red; + }, }; function indent(nesting) { diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index 34e1900979960f..d24e19c358a448 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -1087,7 +1087,7 @@ class Test extends AsyncResource { if (actual < threshold) { harness.success = false; process.exitCode = kGenericUserError; - reporter.diagnostic(nesting, loc, `Error: ${NumberPrototypeToFixed(actual, 2)}% ${name} coverage does not meet threshold of ${threshold}%.`); + reporter.diagnostic(nesting, loc, `Error: ${NumberPrototypeToFixed(actual, 2)}% ${name} coverage does not meet threshold of ${threshold}%.`, 'error'); } } diff --git a/lib/internal/test_runner/tests_stream.js b/lib/internal/test_runner/tests_stream.js index ecbc407e01f318..2ae1ce923982ea 100644 --- a/lib/internal/test_runner/tests_stream.js +++ b/lib/internal/test_runner/tests_stream.js @@ -114,11 +114,12 @@ class TestsStream extends Readable { }); } - diagnostic(nesting, loc, message) { + diagnostic(nesting, loc, message, level = 'info') { this[kEmitMessage]('test:diagnostic', { __proto__: null, nesting, message, + level, ...loc, }); } diff --git a/test/parallel/test-runner-coverage-thresholds.js b/test/parallel/test-runner-coverage-thresholds.js index c1b64cb06a83ff..38b329054fb681 100644 --- a/test/parallel/test-runner-coverage-thresholds.js +++ b/test/parallel/test-runner-coverage-thresholds.js @@ -88,6 +88,25 @@ for (const coverage of coverages) { assert(!findCoverageFileForPid(result.pid)); }); + test(`test failing ${coverage.flag} with red color`, () => { + const result = spawnSync(process.execPath, [ + '--test', + '--experimental-test-coverage', + `${coverage.flag}=99`, + '--test-reporter', 'spec', + fixture, + ], { + env: { ...process.env, FORCE_COLOR: '3' }, + }); + + const stdout = result.stdout.toString(); + // eslint-disable-next-line no-control-regex + const redColorRegex = /\u001b\[31mℹ Error: \d{2}\.\d{2}% \w+ coverage does not meet threshold of 99%/; + assert.match(stdout, redColorRegex, 'Expected red color code not found in diagnostic message'); + assert.strictEqual(result.status, 1); + assert(!findCoverageFileForPid(result.pid)); + }); + test(`test failing ${coverage.flag}`, () => { const result = spawnSync(process.execPath, [ '--test', diff --git a/test/parallel/test-runner-run.mjs b/test/parallel/test-runner-run.mjs index 9f13e1a1e56420..6521fec00b96dd 100644 --- a/test/parallel/test-runner-run.mjs +++ b/test/parallel/test-runner-run.mjs @@ -33,6 +33,24 @@ describe('require(\'node:test\').run', { concurrency: true }, () => { for await (const _ of stream); }); + it('should emit diagnostic events with level parameter', async () => { + const diagnosticEvents = []; + + const stream = run({ + files: [join(testFixtures, 'coverage.js')], + reporter: 'spec', + }); + + stream.on('test:diagnostic', (event) => { + diagnosticEvents.push(event); + }); + // eslint-disable-next-line no-unused-vars + for await (const _ of stream); + assert(diagnosticEvents.length > 0, 'No diagnostic events were emitted'); + const infoEvent = diagnosticEvents.find((e) => e.level === 'info'); + assert(infoEvent, 'No diagnostic events with level "info" were emitted'); + }); + const argPrintingFile = join(testFixtures, 'print-arguments.js'); it('should allow custom arguments via execArgv', async () => { const result = await run({ files: [argPrintingFile], execArgv: ['-p', '"Printed"'] }).compose(spec).toArray();