Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,12 +426,13 @@ These apply to every command:

### Environment variables

| Variable | Purpose |
| ------------------------------- | --------------------------------------------------------------------------------- |
| `TESTSPRITE_API_KEY` | API key — overrides the credentials file |
| `TESTSPRITE_API_URL` | API endpoint — overrides the credentials file |
| `TESTSPRITE_PROFILE` | Active profile (below `--profile`, above `default`) |
| `TESTSPRITE_REQUEST_TIMEOUT_MS` | Per-request timeout in **milliseconds** (default `120000`, range `1000`–`600000`) |
| Variable | Purpose |
| ------------------------------- | --------------------------------------------------------------------------------------- |
| `TESTSPRITE_API_KEY` | API key — overrides the credentials file |
| `TESTSPRITE_API_URL` | API endpoint — overrides the credentials file |
| `TESTSPRITE_PROFILE` | Active profile (below `--profile`, above `default`) |
| `TESTSPRITE_REQUEST_TIMEOUT_MS` | Per-request timeout in **milliseconds** (default `120000`, range `1000`–`600000`) |
| `NO_COLOR` | Suppress ANSI escape sequences in ticker output ([no-color.org](https://no-color.org/)) |

### Scopes

Expand Down
64 changes: 63 additions & 1 deletion src/lib/ticker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/

import { describe, expect, it, vi } from 'vitest';
import { createTicker } from './ticker.js';
import { createTicker, isNoColor } from './ticker.js';

describe('createTicker — non-TTY (CI mode)', () => {
it('update is a no-op (no writes)', () => {
Expand Down Expand Up @@ -222,3 +222,65 @@ describe('createTicker — spy on process.stderr', () => {
}
});
});

describe('createTicker — NO_COLOR support', () => {
it('suppresses ANSI escape sequences when noColor=true on TTY', () => {
const lines: string[] = [];
const raw: string[] = [];
const ticker = createTicker(
line => lines.push(line),
true, // isTTY = true
text => raw.push(text),
true, // noColor = true
);
ticker.update('progress');
// Should use stderrWrite (line-oriented) instead of rawWrite with ANSI
expect(raw).toHaveLength(0);
expect(lines).toHaveLength(1);
expect(lines[0]!).not.toContain('\x1b[2K');
expect(lines[0]!).not.toContain('\r');
expect(lines[0]!).toContain('progress');
});

it('finalize emits plain text without ANSI when noColor=true on TTY', () => {
const lines: string[] = [];
const raw: string[] = [];
const ticker = createTicker(
line => lines.push(line),
true,
text => raw.push(text),
true, // noColor = true
);
ticker.finalize('done');
expect(raw).toHaveLength(0);
expect(lines).toHaveLength(1);
expect(lines[0]!).not.toContain('\x1b[2K');
expect(lines[0]!).toContain('done');
});

it('normal ANSI output when noColor=false on TTY', () => {
const raw: string[] = [];
const ticker = createTicker(
() => {},
true,
text => raw.push(text),
false, // noColor = false
);
ticker.update('progress');
expect(raw).toHaveLength(1);
expect(raw[0]!).toContain('\x1b[2K\r');
});
});

describe('isNoColor', () => {
it('returns true when NO_COLOR is present in env', () => {
expect(isNoColor({ NO_COLOR: '' })).toBe(true);
expect(isNoColor({ NO_COLOR: '1' })).toBe(true);
expect(isNoColor({ NO_COLOR: 'true' })).toBe(true);
});

it('returns false when NO_COLOR is not present in env', () => {
expect(isNoColor({})).toBe(false);
expect(isNoColor({ OTHER_VAR: '1' })).toBe(false);
});
});
34 changes: 34 additions & 0 deletions src/lib/ticker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
* - Uses `\r` + ANSI clear-line to overwrite in place on TTY
* - On terminal, emits one final line + newline then prints the result
* - `--output json` disables the ticker (caller doesn't create one)
* - Respects the NO_COLOR env var (https://no-color.org/): when set,
* ANSI escape sequences are suppressed and updates are emitted as
* plain lines instead of in-place overwrites.
*
* Overhead: <2ms per update (no syscalls beyond a single write).
*
Expand All @@ -25,6 +28,14 @@ export interface Ticker {
finalize(line?: string): void;
}

/**
* Returns true when NO_COLOR is present in the environment and is not
* an empty string, per https://no-color.org/.
*/
export function isNoColor(env: NodeJS.ProcessEnv = process.env): boolean {
return 'NO_COLOR' in env;
}

/**
* Create a ticker bound to the given stderr writer. Respects
* `isTTY` to silently no-op in CI environments.
Expand All @@ -35,18 +46,22 @@ export interface Ticker {
* @param stderrRaw - optional raw writer (no \n appended); used for
* the carriage-return + clear-line trick. Defaults to
* `process.stderr.write.bind(process.stderr)`.
* @param noColor - whether to suppress ANSI escape sequences.
* Defaults to checking `NO_COLOR` env var per https://no-color.org/.
*/
export function createTicker(
stderrWrite: (line: string) => void,
isTTY?: boolean,
stderrRaw?: (text: string) => void,
noColor?: boolean,
): Ticker {
const tty = isTTY ?? (typeof process !== 'undefined' ? process.stderr.isTTY === true : false);
const rawWrite =
stderrRaw ??
(typeof process !== 'undefined'
? (text: string) => process.stderr.write(text)
: (_text: string) => undefined);
const suppressAnsi = noColor ?? isNoColor();

let lastLength = 0;

Expand All @@ -58,6 +73,25 @@ export function createTicker(
};
}

if (suppressAnsi) {
// TTY but NO_COLOR: emit plain-text lines without ANSI escape sequences.
return {
update(line: string): void {
const stamped = `${new Date().toISOString()} ${line}`;
stderrWrite(stamped);
lastLength = stamped.length;
},
finalize(line?: string): void {
if (line !== undefined) {
const stamped = `${new Date().toISOString()} ${line}`;
stderrWrite(stamped);
lastLength = stamped.length;
}
void stderrWrite;
},
};
}

return {
update(line: string): void {
// ANSI ESC[2K clears the entire line; \r moves to column 0.
Expand Down