diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a2e753da..35f569025 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). Please see [CONTRIBUTING.md](https://github.com/cucumber/cucumber/blob/master/CONTRIBUTING.md) on how to contribute to Cucumber. ## [Unreleased] +### Changed +- Fix issues with colored output, support `FORCE_COLOR` environment variable as an override ([#2026](https://github.com/cucumber/cucumber-js/pull/2026)) ## [8.1.2] - 2022-04-22 ### Added diff --git a/docs/formatters.md b/docs/formatters.md index f611989fd..c7490002e 100644 --- a/docs/formatters.md +++ b/docs/formatters.md @@ -39,9 +39,17 @@ This option is repeatable, so you can use it multiple times and the objects will Some options offered by built-in formatters: -- `colorsEnabled` - if set to `false`, colors in terminal output are disabled +- `colorsEnabled` - [see below](#colored-output) - `printAttachments` - if set to `false`, attachments won't be part of progress bars and summary reports +## Colored output + +Many formatters, including the built-in ones, emit some colored output. By default, Cucumber will automatically detect the colors support of the output stream and decide whether to emit colors accordingly. This check comes via the [supports-colors](https://github.com/chalk/supports-color) library and is pretty comprehensive, including awareness of commonly-used operating systems and CI platforms that represent edge cases. + +If you'd like to override the auto-detection behaviour, you can provide the `colorsEnabled` format option - either `true` to forcibly emit colors, or `false` to forcibly disable them. + +It's worth noting that this option only influences output that Cucumber is in control of. Other tools in your stack such as assertion libraries might have their own way of handling colors. For this reason we'd recommend setting the `FORCE_COLOR` environment variable if you want to forcibly enable (by setting it to `1`) or disable (by setting it to `0`) colors, as a variety of tools (including Cucumber) will honour it. + ## Built-in formatters ### `summary` diff --git a/features/colors.feature b/features/colors.feature new file mode 100644 index 000000000..05f6cc537 --- /dev/null +++ b/features/colors.feature @@ -0,0 +1,38 @@ +@spawn +Feature: Colors + + As a developer + I want to control when/whether the output includes colors + + Background: + Given a file named "features/a.feature" with: + """ + Feature: + Scenario: + Given a step + """ + And a file named "features/step_definitions/steps.js" with: + """ + const {Given} = require('@cucumber/cucumber') + Given('a step', function() {}) + """ + And a file named "cucumber.json" with: + """ + { "default": { "format": ["summary:summary.out"] } } + """ + + Scenario: no colored output by default for a file stream + When I run cucumber-js + Then the file "summary.out" doesn't contain colors + + Scenario: colored output can be activated with the format option + When I run cucumber-js with `--format-options '{"colorsEnabled":true}'` + Then the file "summary.out" contains colors + + Scenario: colored output can be activated with FORCE_COLOR + When I run cucumber-js with env `FORCE_COLOR=1` + Then the file "summary.out" contains colors + + Scenario: FORCE_COLOR takes precedence over the format option + When I run cucumber-js with arguments `--format-options '{"colorsEnabled":false}'` and env `FORCE_COLOR=1` + Then the file "summary.out" contains colors diff --git a/features/step_definitions/file_steps.ts b/features/step_definitions/file_steps.ts index 5b3b00644..218731b9e 100644 --- a/features/step_definitions/file_steps.ts +++ b/features/step_definitions/file_steps.ts @@ -1,5 +1,6 @@ import { Given, Then } from '../../' import { expect } from 'chai' +import hasAnsi from 'has-ansi' import { normalizeText } from '../support/helpers' import fs from 'mz/fs' import fsExtra from 'fs-extra' @@ -50,3 +51,23 @@ Then( expect(actualContent).to.eql(expectedContent) } ) + +Then( + 'the file {string} contains colors', + async function (this: World, filePath: string) { + filePath = Mustache.render(filePath, this) + const absoluteFilePath = path.resolve(this.tmpDir, filePath) + const content = await fs.readFile(absoluteFilePath, 'utf8') + expect(hasAnsi(content)).to.be.true + } +) + +Then( + "the file {string} doesn't contain colors", + async function (this: World, filePath: string) { + filePath = Mustache.render(filePath, this) + const absoluteFilePath = path.resolve(this.tmpDir, filePath) + const content = await fs.readFile(absoluteFilePath, 'utf8') + expect(hasAnsi(content)).to.be.false + } +) diff --git a/package-lock.json b/package-lock.json index d93e86b92..2d9d71760 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "semver": "7.3.7", "stack-chain": "^2.0.0", "string-argv": "^0.3.1", + "supports-color": "^8.1.1", "tmp": "^0.2.1", "util-arity": "^1.1.0", "verror": "^1.10.0", @@ -59,6 +60,7 @@ "@types/express": "4.17.13", "@types/fs-extra": "9.0.13", "@types/glob": "7.2.0", + "@types/has-ansi": "^5.0.0", "@types/lodash.merge": "4.6.7", "@types/lodash.mergewith": "4.6.7", "@types/mocha": "9.1.1", @@ -89,6 +91,7 @@ "express": "4.18.1", "fs-extra": "10.1.0", "genversion": "3.1.1", + "has-ansi": "^4.0.1", "mocha": "10.0.0", "mustache": "4.2.0", "nyc": "15.1.0", @@ -1354,6 +1357,12 @@ "@types/node": "*" } }, + "node_modules/@types/has-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/has-ansi/-/has-ansi-5.0.0.tgz", + "integrity": "sha512-1mwAilzr9t7E+h+KHaOE3Jxio9G3c2DSQTcjz/JV/xJ9DUuQastSiYmjJz4D07Vh12+tKM4owiFOe2+XOmvEig==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.9", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", @@ -2358,6 +2367,17 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", @@ -4040,6 +4060,27 @@ "node": ">= 0.4.0" } }, + "node_modules/has-ansi": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-4.0.1.tgz", + "integrity": "sha512-Qr4RtTm30xvEdqUXbSBVWDu+PrTokJOwe/FU+VdfJPk+MXAPoeOzKpRyrDTnZIJwAkQ4oBLTU53nu0HrkF/Z2A==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/has-bigints": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", @@ -4632,6 +4673,18 @@ "node": ">=8" } }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/istanbul-lib-source-maps": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", @@ -5263,21 +5316,6 @@ "node": ">=8" } }, - "node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -6787,6 +6825,18 @@ "node": ">=0.3.1" } }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -7007,14 +7057,17 @@ } }, "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dependencies": { "has-flag": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/supports-hyperlinks": { @@ -7030,6 +7083,18 @@ "node": ">=8" } }, + "node_modules/supports-hyperlinks/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -8805,6 +8870,12 @@ "@types/node": "*" } }, + "@types/has-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/has-ansi/-/has-ansi-5.0.0.tgz", + "integrity": "sha512-1mwAilzr9t7E+h+KHaOE3Jxio9G3c2DSQTcjz/JV/xJ9DUuQastSiYmjJz4D07Vh12+tKM4owiFOe2+XOmvEig==", + "dev": true + }, "@types/json-schema": { "version": "7.0.9", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", @@ -9572,6 +9643,16 @@ "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } } }, "check-error": { @@ -10870,6 +10951,23 @@ "function-bind": "^1.1.1" } }, + "has-ansi": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-4.0.1.tgz", + "integrity": "sha512-Qr4RtTm30xvEdqUXbSBVWDu+PrTokJOwe/FU+VdfJPk+MXAPoeOzKpRyrDTnZIJwAkQ4oBLTU53nu0HrkF/Z2A==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true + } + } + }, "has-bigints": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", @@ -11282,6 +11380,17 @@ "istanbul-lib-coverage": "^3.0.0", "make-dir": "^3.0.0", "supports-color": "^7.1.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, "istanbul-lib-source-maps": { @@ -11766,15 +11875,6 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } } } }, @@ -12902,6 +13002,15 @@ "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } } } }, @@ -13089,9 +13198,9 @@ "dev": true }, "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "requires": { "has-flag": "^4.0.0" } @@ -13104,6 +13213,17 @@ "requires": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, "supports-preserve-symlinks-flag": { diff --git a/package.json b/package.json index 704caec7a..829ddc825 100644 --- a/package.json +++ b/package.json @@ -225,6 +225,7 @@ "semver": "7.3.7", "stack-chain": "^2.0.0", "string-argv": "^0.3.1", + "supports-color": "^8.1.1", "tmp": "^0.2.1", "util-arity": "^1.1.0", "verror": "^1.10.0", @@ -241,6 +242,7 @@ "@types/express": "4.17.13", "@types/fs-extra": "9.0.13", "@types/glob": "7.2.0", + "@types/has-ansi": "^5.0.0", "@types/lodash.merge": "4.6.7", "@types/lodash.mergewith": "4.6.7", "@types/mocha": "9.1.1", @@ -271,6 +273,7 @@ "express": "4.18.1", "fs-extra": "10.1.0", "genversion": "3.1.1", + "has-ansi": "^4.0.1", "mocha": "10.0.0", "mustache": "4.2.0", "nyc": "15.1.0", diff --git a/src/api/formatters.ts b/src/api/formatters.ts index 734f8a842..f01a16a10 100644 --- a/src/api/formatters.ts +++ b/src/api/formatters.ts @@ -3,7 +3,6 @@ import { EventEmitter } from 'events' import { EventDataCollector } from '../formatter/helpers' import { ISupportCodeLibrary } from '../support_code_library_builder/types' import { promisify } from 'util' -import { doesNotHaveValue } from '../value_checker' import { WriteStream as TtyWriteStream } from 'tty' import FormatterBuilder from '../formatter/builder' import fs from 'mz/fs' @@ -14,6 +13,7 @@ import { Writable } from 'stream' import { IRunOptionsFormats } from './types' export async function initializeFormatters({ + env, cwd, stdout, logger, @@ -23,6 +23,7 @@ export async function initializeFormatters({ configuration, supportCodeLibrary, }: { + env: NodeJS.ProcessEnv cwd: string stdout: IFormatterStream logger: Console @@ -42,6 +43,7 @@ export async function initializeFormatters({ onStreamError() }) const typeOptions = { + env, cwd, eventBroadcaster, eventDataCollector, @@ -54,11 +56,6 @@ export async function initializeFormatters({ : promisify(stream.end.bind(stream)), supportCodeLibrary, } - if (doesNotHaveValue(configuration.options.colorsEnabled)) { - typeOptions.parsedArgvOptions.colorsEnabled = ( - stream as TtyWriteStream - ).isTTY - } if (type === 'progress-bar' && !(stream as TtyWriteStream).isTTY) { logger.warn( `Cannot use 'progress-bar' formatter for output to '${target}' as not a TTY. Switching to 'progress' formatter.` diff --git a/src/api/run_cucumber.ts b/src/api/run_cucumber.ts index cf5faa05c..096d728f1 100644 --- a/src/api/run_cucumber.ts +++ b/src/api/run_cucumber.ts @@ -55,6 +55,7 @@ export async function runCucumber( let formatterStreamError = false const cleanup = await initializeFormatters({ + env, cwd, stdout, logger, diff --git a/src/formatter/builder.ts b/src/formatter/builder.ts index ac0419efd..264ecaa5d 100644 --- a/src/formatter/builder.ts +++ b/src/formatter/builder.ts @@ -26,6 +26,7 @@ interface IGetStepDefinitionSnippetBuilderOptions { } export interface IBuildOptions { + env: NodeJS.ProcessEnv cwd: string eventBroadcaster: EventEmitter eventDataCollector: EventDataCollector @@ -42,7 +43,11 @@ const FormatterBuilder = { type, options.cwd ) - const colorFns = getColorFns(options.parsedArgvOptions.colorsEnabled) + const colorFns = getColorFns( + options.stream, + options.env, + options.parsedArgvOptions.colorsEnabled + ) const snippetBuilder = await FormatterBuilder.getStepDefinitionSnippetBuilder({ cwd: options.cwd, diff --git a/src/formatter/get_color_fns.ts b/src/formatter/get_color_fns.ts index 31df7684f..8fdcc7b18 100644 --- a/src/formatter/get_color_fns.ts +++ b/src/formatter/get_color_fns.ts @@ -1,5 +1,8 @@ import chalk from 'chalk' +import { ColorInfo, supportsColor } from 'supports-color' import { TestStepResultStatus } from '@cucumber/messages' +import { Writable } from 'stream' +import { doesNotHaveValue } from '../value_checker' export type IColorFn = (text: string) => string @@ -13,26 +16,32 @@ export interface IColorFns { errorStack: IColorFn } -export default function getColorFns(enabled: boolean): IColorFns { - if (enabled) { +export default function getColorFns( + stream: Writable, + env: NodeJS.ProcessEnv, + enabled?: boolean +): IColorFns { + const support: ColorInfo = detectSupport(stream, env, enabled) + if (support) { + const chalkInstance = new chalk.Instance(support) return { forStatus(status: TestStepResultStatus) { return { - AMBIGUOUS: chalk.red.bind(chalk), - FAILED: chalk.red.bind(chalk), - PASSED: chalk.green.bind(chalk), - PENDING: chalk.yellow.bind(chalk), - SKIPPED: chalk.cyan.bind(chalk), - UNDEFINED: chalk.yellow.bind(chalk), - UNKNOWN: chalk.yellow.bind(chalk), + AMBIGUOUS: chalkInstance.red.bind(chalk), + FAILED: chalkInstance.red.bind(chalk), + PASSED: chalkInstance.green.bind(chalk), + PENDING: chalkInstance.yellow.bind(chalk), + SKIPPED: chalkInstance.cyan.bind(chalk), + UNDEFINED: chalkInstance.yellow.bind(chalk), + UNKNOWN: chalkInstance.yellow.bind(chalk), }[status] }, - location: chalk.gray.bind(chalk), - tag: chalk.cyan.bind(chalk), - diffAdded: chalk.green.bind(chalk), - diffRemoved: chalk.red.bind(chalk), - errorMessage: chalk.red.bind(chalk), - errorStack: chalk.grey.bind(chalk), + location: chalkInstance.gray.bind(chalk), + tag: chalkInstance.cyan.bind(chalk), + diffAdded: chalkInstance.green.bind(chalk), + diffRemoved: chalkInstance.red.bind(chalk), + errorMessage: chalkInstance.red.bind(chalk), + errorStack: chalkInstance.grey.bind(chalk), } } else { return { @@ -48,3 +57,16 @@ export default function getColorFns(enabled: boolean): IColorFns { } } } + +function detectSupport( + stream: Writable, + env: NodeJS.ProcessEnv, + enabled?: boolean +): ColorInfo { + const support: ColorInfo = supportsColor(stream) + // if we find FORCE_COLOR, we can let the supports-color library handle that + if ('FORCE_COLOR' in env || doesNotHaveValue(enabled)) { + return support + } + return enabled ? support || { level: 1 } : false +} diff --git a/src/formatter/helpers/issue_helpers_spec.ts b/src/formatter/helpers/issue_helpers_spec.ts index 9c2c350dc..0c00b90d1 100644 --- a/src/formatter/helpers/issue_helpers_spec.ts +++ b/src/formatter/helpers/issue_helpers_spec.ts @@ -7,6 +7,7 @@ import { getTestCaseAttempts } from '../../../test/formatter_helpers' import { reindent } from 'reindent-template-literals' import { getBaseSupportCodeLibrary } from '../../../test/fixtures/steps' import FormatterBuilder from '../builder' +import { PassThrough } from 'stream' async function testFormatIssue( sourceData: string, @@ -24,7 +25,7 @@ async function testFormatIssue( supportCodeLibrary, }) return formatIssue({ - colorFns: getColorFns(false), + colorFns: getColorFns(new PassThrough(), {}, false), number: 1, snippetBuilder: await FormatterBuilder.getStepDefinitionSnippetBuilder({ cwd: 'project/', diff --git a/src/formatter/helpers/summary_helpers_spec.ts b/src/formatter/helpers/summary_helpers_spec.ts index 42cded1bb..0c80d8cee 100644 --- a/src/formatter/helpers/summary_helpers_spec.ts +++ b/src/formatter/helpers/summary_helpers_spec.ts @@ -11,6 +11,7 @@ import { IRuntimeOptions } from '../../runtime' import { ISupportCodeLibrary } from '../../support_code_library_builder/types' import { doesNotHaveValue } from '../../value_checker' import * as messages from '@cucumber/messages' +import { PassThrough } from 'stream' interface ITestFormatSummaryOptions { runtimeOptions?: Partial @@ -53,7 +54,7 @@ async function testFormatSummary({ supportCodeLibrary, }) return formatSummary({ - colorFns: getColorFns(false), + colorFns: getColorFns(new PassThrough(), {}, false), testCaseAttempts, testRunDuration: durationBetweenTimestamps( testRunStarted.timestamp, diff --git a/src/formatter/progress_bar_formatter_spec.ts b/src/formatter/progress_bar_formatter_spec.ts index cc893d4ee..0def59cef 100644 --- a/src/formatter/progress_bar_formatter_spec.ts +++ b/src/formatter/progress_bar_formatter_spec.ts @@ -59,6 +59,7 @@ async function testProgressBarFormatter({ } const passThrough = new PassThrough() const progressBarFormatter = (await FormatterBuilder.build('progress-bar', { + env: {}, cwd: '', eventBroadcaster, eventDataCollector: new EventDataCollector(eventBroadcaster), diff --git a/src/types/supports-color/index.d.ts b/src/types/supports-color/index.d.ts new file mode 100644 index 000000000..61a768d05 --- /dev/null +++ b/src/types/supports-color/index.d.ts @@ -0,0 +1,17 @@ +declare module 'supports-color' { + import { Writable } from 'stream' + + export interface Options { + readonly sniffFlags?: boolean + } + + export type ColorSupportLevel = 0 | 1 | 2 | 3 + + export interface ColorSupport { + level: ColorSupportLevel + } + + export type ColorInfo = ColorSupport | false + + export function supportsColor(stream: Writable, options?: Options): ColorInfo +} diff --git a/test/formatter_helpers.ts b/test/formatter_helpers.ts index 3a0008854..51b13f69b 100644 --- a/test/formatter_helpers.ts +++ b/test/formatter_helpers.ts @@ -61,6 +61,7 @@ export async function testFormatter({ } const passThrough = new PassThrough() await FormatterBuilder.build(type, { + env: {}, cwd: '', eventBroadcaster, eventDataCollector,