diff --git a/lib/internal/test_runner/colors.js b/lib/internal/test_runner/colors.js new file mode 100644 index 00000000000000..d12b57c84f063d --- /dev/null +++ b/lib/internal/test_runner/colors.js @@ -0,0 +1,67 @@ +'use strict'; + +const { + blue, + green, + red, + yellow, + white, + gray, +} = require('internal/util/colors'); + +const DEFAULT_THEME = { + info: 'blue', + pass: 'green', + fail: 'red', + skip: 'yellow', + base: 'white', + duration: 'gray', +}; + +const COLOR_MAP = { + blue, + green, + red, + yellow, + white, + gray, +}; + +function loadUserTheme() { + const raw = process.env.NODE_TEST_RUNNER_THEME; + if (!raw) return {}; + + try { + const parsed = JSON.parse(raw); + const allowedKeys = Object.keys(DEFAULT_THEME); + const sanitized = {}; + + for (const key of allowedKeys) { + const val = parsed[key]; + if (typeof val === 'string' && val in COLOR_MAP) { + sanitized[key] = val; + } + } + + return sanitized; + } catch { + return {}; + } +} + +const userTheme = loadUserTheme(); + +function resolveColor(name) { + const userColor = userTheme[name]; + const fallback = DEFAULT_THEME[name]; + return COLOR_MAP[userColor] || COLOR_MAP[fallback]; +} + +module.exports = { + info: resolveColor('info'), + pass: resolveColor('pass'), + fail: resolveColor('fail'), + skip: resolveColor('skip'), + base: resolveColor('base'), + duration: resolveColor('duration'), +}; diff --git a/lib/internal/test_runner/reporter/dot.js b/lib/internal/test_runner/reporter/dot.js index 45ff047bc4e5a0..a80dc7746772d0 100644 --- a/lib/internal/test_runner/reporter/dot.js +++ b/lib/internal/test_runner/reporter/dot.js @@ -4,6 +4,7 @@ const { MathMax, } = primordials; const colors = require('internal/util/colors'); +const colorsTheme = require('internal/test_runner/colors'); const { formatTestReport } = require('internal/test_runner/reporter/utils'); module.exports = async function* dot(source) { @@ -12,10 +13,10 @@ module.exports = async function* dot(source) { const failedTests = []; for await (const { type, data } of source) { if (type === 'test:pass') { - yield `${colors.green}.${colors.reset}`; + yield `${colorsTheme.pass}.${colors.reset}`; } if (type === 'test:fail') { - yield `${colors.red}X${colors.reset}`; + yield `${colorsTheme.fail}X${colors.reset}`; ArrayPrototypePush(failedTests, data); } if ((type === 'test:fail' || type === 'test:pass') && ++count === columns) { @@ -28,7 +29,7 @@ module.exports = async function* dot(source) { } yield '\n'; if (failedTests.length > 0) { - yield `\n${colors.red}Failed tests:${colors.white}\n\n`; + yield `\n${colorsTheme.fail}Failed tests:${base}\n\n`; for (const test of failedTests) { yield formatTestReport('test:fail', test); } diff --git a/lib/internal/test_runner/reporter/spec.js b/lib/internal/test_runner/reporter/spec.js index 9031025e57d930..85cbda7fbb606b 100644 --- a/lib/internal/test_runner/reporter/spec.js +++ b/lib/internal/test_runner/reporter/spec.js @@ -9,6 +9,7 @@ const { const assert = require('assert'); const Transform = require('internal/streams/transform'); const colors = require('internal/util/colors'); +const colorsTheme = require('internal/test_runner/colors'); const { kSubtestsFailed } = require('internal/test_runner/test'); const { getCoverageReport } = require('internal/test_runner/utils'); const { relative } = require('path'); @@ -36,7 +37,7 @@ class SpecReporter extends Transform { } const results = [ - `\n${reporterColorMap['test:fail']}${reporterUnicodeSymbolMap['test:fail']}failing tests:${colors.white}\n`, + `\n${reporterColorMap['test:fail']}${reporterUnicodeSymbolMap['test:fail']}failing tests:${colorsTheme.base}\n`, ]; for (let i = 0; i < this.#failedTests.length; i++) { @@ -96,11 +97,11 @@ class SpecReporter extends Transform { return data.message; case 'test:diagnostic':{ const diagnosticColor = reporterColorMap[data.level] || reporterColorMap['test:diagnostic']; - return `${diagnosticColor}${indent(data.nesting)}${reporterUnicodeSymbolMap[type]}${data.message}${colors.white}\n`; + return `${diagnosticColor}${indent(data.nesting)}${reporterUnicodeSymbolMap[type]}${data.message}${colorsTheme.base}\n`; } case 'test:coverage': return getCoverageReport(indent(data.nesting), data.summary, - reporterUnicodeSymbolMap['test:coverage'], colors.blue, true); + reporterUnicodeSymbolMap['test:coverage'], colorsTheme.info, true); case 'test:summary': // We report only the root test summary if (data.file === undefined) { diff --git a/lib/internal/test_runner/reporter/utils.js b/lib/internal/test_runner/reporter/utils.js index eb1a008aaf006a..3db7b401f0703c 100644 --- a/lib/internal/test_runner/reporter/utils.js +++ b/lib/internal/test_runner/reporter/utils.js @@ -7,6 +7,7 @@ const { hardenRegExp, } = primordials; const colors = require('internal/util/colors'); +const colorsTheme = require('internal/test_runner/colors'); const { inspectWithNoCustomRetry } = require('internal/errors'); const indentMemo = new SafeMap(); @@ -29,22 +30,22 @@ const reporterUnicodeSymbolMap = { const reporterColorMap = { '__proto__': null, get 'test:fail'() { - return colors.red; + return colorsTheme.fail; }, get 'test:pass'() { - return colors.green; + return colorsTheme.pass; }, get 'test:diagnostic'() { - return colors.blue; + return colorsTheme.info; }, get 'info'() { - return colors.blue; + return colorsTheme.info; }, get 'warn'() { - return colors.yellow; + return colorsTheme.warn; }, get 'error'() { - return colors.red; + return colorsTheme.fail; }, }; @@ -69,10 +70,10 @@ function formatError(error, indent) { } function formatTestReport(type, data, prefix = '', indent = '', hasChildren = false, showErrorDetails = true) { - let color = reporterColorMap[type] ?? colors.white; + let color = reporterColorMap[type] ?? colorsTheme.base; let symbol = reporterUnicodeSymbolMap[type] ?? ' '; const { skip, todo } = data; - const duration_ms = data.details?.duration_ms ? ` ${colors.gray}(${data.details.duration_ms}ms)${colors.white}` : ''; + const duration_ms = data.details?.duration_ms ? ` ${colorsTheme.duration}(${data.details.duration_ms}ms)${colorsTheme.base}` : ''; let title = `${data.name}${duration_ms}`; if (skip !== undefined) { @@ -87,10 +88,10 @@ function formatTestReport(type, data, prefix = '', indent = '', hasChildren = fa error; if (skip !== undefined) { - color = colors.gray; + color = colorsTheme.duration; symbol = reporterUnicodeSymbolMap['hyphen:minus']; } - return `${prefix}${indent}${color}${symbol}${title}${colors.white}${err}`; + return `${prefix}${indent}${color}${symbol}${title}${colorsTheme.base}${err}`; } module.exports = { diff --git a/lib/internal/test_runner/utils.js b/lib/internal/test_runner/utils.js index 44789451d18335..c9d68feca947c5 100644 --- a/lib/internal/test_runner/utils.js +++ b/lib/internal/test_runner/utils.js @@ -31,7 +31,8 @@ const { relative, sep, resolve } = require('path'); const { createWriteStream } = require('fs'); const { pathToFileURL } = require('internal/url'); const { getOptionValue } = require('internal/options'); -const { green, yellow, red, white, shouldColorize } = require('internal/util/colors'); +const { shouldColorize } = require('internal/util/colors'); +const colorsTheme = require('internal/test_runner/colors'); const { codes: { @@ -50,9 +51,9 @@ const { kEmptyObject } = require('internal/util'); const coverageColors = { __proto__: null, - high: green, - medium: yellow, - low: red, + high: colorsTheme.pass, + medium: colorsTheme.skip, + low: colorsTheme.fail, }; const kMultipleCallbackInvocations = 'multipleCallbackInvocations'; @@ -600,7 +601,7 @@ function getCoverageReport(pad, summary, symbol, color, table) { report += `${prefix}end of coverage report\n`; if (color) { - report += white; + report += colorsTheme.base; } return report; }