/** * OutputFormatter Tests * * Tests the output module in isolation, covering: * - Color methods (bold, dim, success, error, warning, info, highlight) * - Color enable/disable * - Verbosity levels (quiet, normal, verbose, debug) * - Formatted output: printSuccess, printError, printWarning, printInfo, printDebug, printTrace * - Table formatting: borders, no borders, alignment, column width, maxWidth, formatter * - Progress bar rendering * - JSON output (pretty and compact) * - List and numbered list output * - Box output with and without title * - supportsColor environment detection */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { OutputFormatter } from '../src/output.js'; describe('OutputFormatter', () => { let stdoutWrite: ReturnType; let stderrWrite: ReturnType; let captured: string[]; let errorCaptured: string[]; beforeEach(() => { captured = []; errorCaptured = []; stdoutWrite = vi.spyOn(process.stdout, 'write').mockImplementation((s: string | Uint8Array) => { captured.push(String(s)); return true; }); stderrWrite = vi.spyOn(process.stderr, 'write').mockImplementation((s: string | Uint8Array) => { errorCaptured.push(String(s)); return true; }); }); afterEach(() => { vi.restoreAllMocks(); delete process.env.NO_COLOR; delete process.env.FORCE_COLOR; }); // ------------------------------------------------------------------------- // Color enable/disable // ------------------------------------------------------------------------- describe('color enable/disable', () => { it('should disable colors via setColorEnabled(false)', () => { const out = new OutputFormatter({ color: true }); out.setColorEnabled(false); expect(out.isColorEnabled()).toBe(false); // With color disabled, color methods return plain text expect(out.bold('hello')).toBe('hello'); }); it('should enable colors via setColorEnabled(true)', () => { const out = new OutputFormatter({ color: false }); out.setColorEnabled(true); expect(out.isColorEnabled()).toBe(true); expect(out.bold('hello')).toContain('\x1b['); }); it('should respect color=false in constructor', () => { const out = new OutputFormatter({ color: false }); expect(out.isColorEnabled()).toBe(false); expect(out.success('ok')).toBe('ok'); }); it('should respect color=true in constructor', () => { const out = new OutputFormatter({ color: true }); expect(out.isColorEnabled()).toBe(true); expect(out.error('fail')).toContain('\x1b[31m'); }); }); // ------------------------------------------------------------------------- // Color methods // ------------------------------------------------------------------------- describe('color methods', () => { let out: OutputFormatter; beforeEach(() => { out = new OutputFormatter({ color: true }); }); it('bold should wrap text with bold ANSI code', () => { const result = out.bold('test'); expect(result).toContain('\x1b[1m'); expect(result).toContain('test'); expect(result).toContain('\x1b[0m'); }); it('dim should wrap text with dim ANSI code', () => { const result = out.dim('test'); expect(result).toContain('\x1b[2m'); }); it('success should wrap text in green', () => { const result = out.success('ok'); expect(result).toContain('\x1b[32m'); }); it('error should wrap text in red', () => { const result = out.error('fail'); expect(result).toContain('\x1b[31m'); }); it('warning should wrap text in yellow', () => { const result = out.warning('careful'); expect(result).toContain('\x1b[33m'); }); it('info should wrap text in blue', () => { const result = out.info('note'); expect(result).toContain('\x1b[34m'); }); it('highlight should wrap text in cyan+bold', () => { const result = out.highlight('important'); expect(result).toContain('\x1b[36m'); expect(result).toContain('\x1b[1m'); }); it('color with multiple colors should apply all codes', () => { const result = out.color('text', 'red', 'bold'); expect(result).toContain('\x1b[31m'); expect(result).toContain('\x1b[1m'); }); }); // ------------------------------------------------------------------------- // Verbosity // ------------------------------------------------------------------------- describe('verbosity', () => { it('default verbosity should be "normal"', () => { const out = new OutputFormatter(); expect(out.getVerbosity()).toBe('normal'); }); it('should set verbosity via constructor', () => { const out = new OutputFormatter({ verbosity: 'debug' }); expect(out.getVerbosity()).toBe('debug'); }); it('should update verbosity via setVerbosity', () => { const out = new OutputFormatter(); out.setVerbosity('quiet'); expect(out.getVerbosity()).toBe('quiet'); expect(out.isQuiet()).toBe(true); }); it('isVerbose should be true for "verbose" and "debug"', () => { const out = new OutputFormatter(); out.setVerbosity('verbose'); expect(out.isVerbose()).toBe(true); out.setVerbosity('debug'); expect(out.isVerbose()).toBe(true); out.setVerbosity('normal'); expect(out.isVerbose()).toBe(false); }); it('isDebug should only be true for "debug"', () => { const out = new OutputFormatter(); out.setVerbosity('debug'); expect(out.isDebug()).toBe(true); out.setVerbosity('verbose'); expect(out.isDebug()).toBe(false); }); }); // ------------------------------------------------------------------------- // Formatted output respecting verbosity // ------------------------------------------------------------------------- describe('formatted output', () => { let out: OutputFormatter; beforeEach(() => { out = new OutputFormatter({ color: false }); }); it('printSuccess should always output', () => { out.setVerbosity('quiet'); out.printSuccess('done'); expect(captured.join('')).toContain('done'); }); it('printError should always output to stderr', () => { out.setVerbosity('quiet'); out.printError('bad', 'details here'); const allErr = errorCaptured.join(''); expect(allErr).toContain('bad'); expect(allErr).toContain('details here'); }); it('printWarning should be suppressed in quiet mode', () => { out.setVerbosity('quiet'); out.printWarning('warning'); expect(captured.join('')).not.toContain('warning'); }); it('printWarning should output in normal mode', () => { out.setVerbosity('normal'); out.printWarning('warning'); expect(captured.join('')).toContain('warning'); }); it('printInfo should be suppressed in quiet mode', () => { out.setVerbosity('quiet'); out.printInfo('information'); expect(captured.join('')).not.toContain('information'); }); it('printInfo should output in normal mode', () => { out.setVerbosity('normal'); out.printInfo('information'); expect(captured.join('')).toContain('information'); }); it('printDebug should only output in verbose/debug mode', () => { out.setVerbosity('normal'); out.printDebug('debug msg'); expect(captured.join('')).not.toContain('debug msg'); out.setVerbosity('verbose'); out.printDebug('debug msg'); expect(captured.join('')).toContain('debug msg'); }); it('printTrace should only output in debug mode', () => { out.setVerbosity('verbose'); out.printTrace('trace msg'); expect(captured.join('')).not.toContain('trace msg'); out.setVerbosity('debug'); out.printTrace('trace msg'); expect(captured.join('')).toContain('trace msg'); }); }); // ------------------------------------------------------------------------- // write/writeln/writeError/writeErrorln // ------------------------------------------------------------------------- describe('write methods', () => { let out: OutputFormatter; beforeEach(() => { out = new OutputFormatter({ color: false }); }); it('write should write to stdout without newline', () => { out.write('hello'); expect(captured).toEqual(['hello']); }); it('writeln should write to stdout with newline', () => { out.writeln('hello'); expect(captured).toEqual(['hello\n']); }); it('writeln with no argument should write empty newline', () => { out.writeln(); expect(captured).toEqual(['\n']); }); it('writeError should write to stderr', () => { out.writeError('err'); expect(errorCaptured).toEqual(['err']); }); it('writeErrorln should write to stderr with newline', () => { out.writeErrorln('err'); expect(errorCaptured).toEqual(['err\n']); }); }); // ------------------------------------------------------------------------- // Table formatting // ------------------------------------------------------------------------- describe('table formatting', () => { let out: OutputFormatter; beforeEach(() => { out = new OutputFormatter({ color: false }); }); it('should render a basic table with borders and header', () => { const result = out.table({ columns: [ { key: 'name', header: 'Name' }, { key: 'value', header: 'Value' }, ], data: [ { name: 'alpha', value: '100' }, { name: 'beta', value: '200' }, ], }); expect(result).toContain('Name'); expect(result).toContain('Value'); expect(result).toContain('alpha'); expect(result).toContain('beta'); expect(result).toContain('+'); // border character expect(result).toContain('|'); // vertical border }); it('should render table without borders', () => { const result = out.table({ columns: [{ key: 'x', header: 'X' }], data: [{ x: '1' }], border: false, }); expect(result).not.toContain('+'); expect(result).toContain('X'); expect(result).toContain('1'); }); it('should render table without header', () => { const result = out.table({ columns: [{ key: 'x', header: 'X' }], data: [{ x: 'val' }], header: false, }); expect(result).toContain('val'); // The header 'X' should not appear as a data row const lines = result.split('\n'); const dataLines = lines.filter(l => l.includes('val')); expect(dataLines.length).toBeGreaterThan(0); }); it('should use column formatter', () => { const result = out.table({ columns: [ { key: 'count', header: 'Count', format: (v: unknown) => `#${v}`, }, ], data: [{ count: 42 }], }); expect(result).toContain('#42'); }); it('should handle null/undefined values gracefully', () => { const result = out.table({ columns: [{ key: 'x', header: 'X' }], data: [{ x: null }, { x: undefined }], }); expect(result).toBeDefined(); // Should not throw and should contain empty-ish cells }); it('should handle right alignment', () => { const result = out.table({ columns: [{ key: 'num', header: 'Num', align: 'right' as const }], data: [{ num: '5' }], }); expect(result).toContain('5'); }); it('should handle center alignment', () => { const result = out.table({ columns: [{ key: 'mid', header: 'Mid', align: 'center' as const }], data: [{ mid: 'X' }], }); expect(result).toContain('X'); }); it('should respect column width limit', () => { const result = out.table({ columns: [{ key: 'long', header: 'L', width: 5 }], data: [{ long: 'abcdefghij' }], }); // Content should be truncated expect(result).toContain('...'); }); it('should respect maxWidth constraint', () => { const result = out.table({ columns: [ { key: 'a', header: 'A' }, { key: 'b', header: 'B' }, ], data: [{ a: 'aaaaaaaaaa', b: 'bbbbbbbbbb' }], maxWidth: 20, }); expect(result).toBeDefined(); }); it('printTable should write table to stdout', () => { out.printTable({ columns: [{ key: 'x', header: 'X' }], data: [{ x: '1' }], }); expect(captured.join('')).toContain('X'); }); }); // ------------------------------------------------------------------------- // Progress bar // ------------------------------------------------------------------------- describe('progress bar', () => { let out: OutputFormatter; beforeEach(() => { out = new OutputFormatter({ color: false }); }); it('should render progress bar at 0%', () => { const bar = out.progressBar(0, 100); expect(bar).toContain('0.0%'); }); it('should render progress bar at 50%', () => { const bar = out.progressBar(50, 100); expect(bar).toContain('50.0%'); }); it('should render progress bar at 100%', () => { const bar = out.progressBar(100, 100); expect(bar).toContain('100.0%'); }); it('should clamp progress bar above 100%', () => { const bar = out.progressBar(200, 100); expect(bar).toContain('100.0%'); }); it('should respect custom width', () => { const bar = out.progressBar(50, 100, 20); expect(bar).toContain('50.0%'); }); }); // ------------------------------------------------------------------------- // JSON output // ------------------------------------------------------------------------- describe('JSON output', () => { let out: OutputFormatter; beforeEach(() => { out = new OutputFormatter({ color: false }); }); it('should produce pretty JSON by default', () => { const result = out.json({ a: 1 }); expect(result).toContain(' "a": 1'); }); it('should produce compact JSON when pretty=false', () => { const result = out.json({ a: 1 }, false); expect(result).toBe('{"a":1}'); }); it('printJson should write JSON to stdout', () => { out.printJson({ key: 'val' }); expect(captured.join('')).toContain('"key": "val"'); }); }); // ------------------------------------------------------------------------- // List output // ------------------------------------------------------------------------- describe('list output', () => { let out: OutputFormatter; beforeEach(() => { out = new OutputFormatter({ color: false }); }); it('should render bulleted list', () => { const result = out.list(['apple', 'banana']); expect(result).toContain('- apple'); expect(result).toContain('- banana'); }); it('should use custom bullet', () => { const result = out.list(['item'], '*'); expect(result).toContain('* item'); }); it('printList should write to stdout', () => { out.printList(['x', 'y']); const text = captured.join(''); expect(text).toContain('- x'); expect(text).toContain('- y'); }); it('should render numbered list', () => { const result = out.numberedList(['first', 'second', 'third']); expect(result).toContain('1. first'); expect(result).toContain('2. second'); expect(result).toContain('3. third'); }); it('printNumberedList should write to stdout', () => { out.printNumberedList(['a', 'b']); const text = captured.join(''); expect(text).toContain('1. a'); expect(text).toContain('2. b'); }); }); // ------------------------------------------------------------------------- // Box output // ------------------------------------------------------------------------- describe('box output', () => { let out: OutputFormatter; beforeEach(() => { out = new OutputFormatter({ color: false }); }); it('should render box without title', () => { const result = out.box('Hello world'); expect(result).toContain('+'); expect(result).toContain('|'); expect(result).toContain('Hello world'); }); it('should render box with title', () => { const result = out.box('Content', 'Title'); expect(result).toContain('Title'); expect(result).toContain('Content'); expect(result).toContain('+'); }); it('should handle multiline content', () => { const result = out.box('Line 1\nLine 2\nLine 3'); expect(result).toContain('Line 1'); expect(result).toContain('Line 2'); expect(result).toContain('Line 3'); }); it('printBox should write to stdout', () => { out.printBox('test content', 'box title'); const text = captured.join(''); expect(text).toContain('test content'); expect(text).toContain('box title'); }); }); // ------------------------------------------------------------------------- // supportsColor environment detection // ------------------------------------------------------------------------- describe('supportsColor environment detection', () => { it('should disable color when NO_COLOR is set', () => { process.env.NO_COLOR = '1'; const out = new OutputFormatter(); expect(out.isColorEnabled()).toBe(false); }); it('should enable color when FORCE_COLOR is set', () => { process.env.FORCE_COLOR = '1'; const out = new OutputFormatter(); expect(out.isColorEnabled()).toBe(true); }); }); });