/** * Node.js Test Helper Functions * * Utility functions for CLI testing with Jest */ import { execSync, spawn, SpawnOptions } from 'child_process'; import fs from 'fs'; import path from 'path'; import os from 'os'; /** * CLI execution result interface */ export interface CLIResult { stdout: string; stderr: string; code: number; success: boolean; } /** * Execute CLI command synchronously * @param cliPath - Path to CLI executable * @param args - Command arguments * @param options - Execution options * @returns CLI execution result */ export function runCLI( cliPath: string, args: string, options: { cwd?: string; env?: Record; timeout?: number; } = {} ): CLIResult { try { const stdout = execSync(`${cliPath} ${args}`, { encoding: 'utf8', stdio: 'pipe', cwd: options.cwd, env: { ...process.env, ...options.env }, timeout: options.timeout, }); return { stdout, stderr: '', code: 0, success: true, }; } catch (error: any) { return { stdout: error.stdout || '', stderr: error.stderr || '', code: error.status || 1, success: false, }; } } /** * Execute CLI command asynchronously * @param cliPath - Path to CLI executable * @param args - Command arguments array * @param options - Spawn options * @returns Promise of CLI execution result */ export function runCLIAsync( cliPath: string, args: string[], options: SpawnOptions = {} ): Promise { return new Promise((resolve) => { const child = spawn(cliPath, args, { ...options, stdio: 'pipe', }); let stdout = ''; let stderr = ''; child.stdout?.on('data', (data) => { stdout += data.toString(); }); child.stderr?.on('data', (data) => { stderr += data.toString(); }); child.on('close', (code) => { resolve({ stdout, stderr, code: code || 0, success: code === 0, }); }); child.on('error', (error) => { resolve({ stdout, stderr: stderr + error.message, code: 1, success: false, }); }); }); } /** * Create temporary test directory * @returns Path to temporary directory */ export function createTempDir(): string { const tempDir = path.join(os.tmpdir(), `cli-test-${Date.now()}-${Math.random().toString(36).slice(2)}`); fs.mkdirSync(tempDir, { recursive: true }); return tempDir; } /** * Clean up temporary directory * @param dirPath - Directory to remove */ export function cleanupTempDir(dirPath: string): void { if (fs.existsSync(dirPath)) { fs.rmSync(dirPath, { recursive: true, force: true }); } } /** * Create temporary file with content * @param content - File content * @param extension - File extension * @returns Path to created file */ export function createTempFile(content: string, extension: string = 'txt'): string { const tempFile = path.join(os.tmpdir(), `test-${Date.now()}.${extension}`); fs.writeFileSync(tempFile, content); return tempFile; } /** * Assert CLI command succeeds * @param result - CLI execution result * @param expectedOutput - Optional expected output substring */ export function assertSuccess(result: CLIResult, expectedOutput?: string): void { if (!result.success) { throw new Error(`CLI command failed with exit code ${result.code}\nStderr: ${result.stderr}`); } if (expectedOutput && !result.stdout.includes(expectedOutput)) { throw new Error(`Expected output to contain "${expectedOutput}"\nActual: ${result.stdout}`); } } /** * Assert CLI command fails * @param result - CLI execution result * @param expectedError - Optional expected error substring */ export function assertFailure(result: CLIResult, expectedError?: string): void { if (result.success) { throw new Error(`CLI command should have failed but succeeded\nStdout: ${result.stdout}`); } if (expectedError && !result.stderr.includes(expectedError) && !result.stdout.includes(expectedError)) { throw new Error(`Expected error to contain "${expectedError}"\nActual stderr: ${result.stderr}\nActual stdout: ${result.stdout}`); } } /** * Assert exit code matches expected value * @param result - CLI execution result * @param expectedCode - Expected exit code */ export function assertExitCode(result: CLIResult, expectedCode: number): void { if (result.code !== expectedCode) { throw new Error(`Expected exit code ${expectedCode} but got ${result.code}\nStderr: ${result.stderr}`); } } /** * Parse JSON output from CLI * @param result - CLI execution result * @returns Parsed JSON object */ export function parseJSONOutput(result: CLIResult): T { try { return JSON.parse(result.stdout); } catch (error) { throw new Error(`Failed to parse JSON output: ${error}\nStdout: ${result.stdout}`); } } /** * Mock environment variables for test * @param vars - Environment variables to set * @returns Function to restore original environment */ export function mockEnv(vars: Record): () => void { const original = { ...process.env }; Object.entries(vars).forEach(([key, value]) => { process.env[key] = value; }); return () => { Object.keys(process.env).forEach((key) => { if (!(key in original)) { delete process.env[key]; } }); Object.entries(original).forEach(([key, value]) => { process.env[key] = value; }); }; } /** * Wait for file to exist * @param filePath - Path to file * @param timeout - Timeout in milliseconds * @returns Promise that resolves when file exists */ export async function waitForFile(filePath: string, timeout: number = 5000): Promise { const startTime = Date.now(); while (!fs.existsSync(filePath)) { if (Date.now() - startTime > timeout) { throw new Error(`Timeout waiting for file: ${filePath}`); } await new Promise((resolve) => setTimeout(resolve, 100)); } } /** * Create CLI test fixture with setup and teardown * @param setup - Setup function * @param teardown - Teardown function * @returns Test fixture object */ export function createFixture( setup: () => T | Promise, teardown: (fixture: T) => void | Promise ): { beforeEach: () => Promise; afterEach: (fixture: T) => Promise; } { return { beforeEach: async () => setup(), afterEach: async (fixture: T) => teardown(fixture), }; } /** * Capture stdout/stderr during function execution * @param fn - Function to execute * @returns Captured output */ export function captureOutput(fn: () => void): { stdout: string; stderr: string } { const originalStdout = process.stdout.write; const originalStderr = process.stderr.write; let stdout = ''; let stderr = ''; process.stdout.write = ((chunk: any) => { stdout += chunk.toString(); return true; }) as any; process.stderr.write = ((chunk: any) => { stderr += chunk.toString(); return true; }) as any; try { fn(); } finally { process.stdout.write = originalStdout; process.stderr.write = originalStderr; } return { stdout, stderr }; } /** * Test helper for testing CLI with different input combinations */ export class CLITestHarness { constructor(private cliPath: string) {} /** * Run command with arguments */ run(args: string, options?: { cwd?: string; env?: Record }): CLIResult { return runCLI(this.cliPath, args, options); } /** * Run command and assert success */ assertSuccess(args: string, expectedOutput?: string): CLIResult { const result = this.run(args); assertSuccess(result, expectedOutput); return result; } /** * Run command and assert failure */ assertFailure(args: string, expectedError?: string): CLIResult { const result = this.run(args); assertFailure(result, expectedError); return result; } /** * Run command and parse JSON output */ runJSON(args: string): T { const result = this.run(args); assertSuccess(result); return parseJSONOutput(result); } } /** * Validate JSON schema in CLI output * @param result - CLI execution result * @param schema - Expected schema object */ export function validateJSONSchema(result: CLIResult, schema: Record): void { const output = parseJSONOutput(result); Object.entries(schema).forEach(([key, expectedType]) => { if (!(key in output)) { throw new Error(`Missing expected key in JSON output: ${key}`); } const actualType = typeof output[key]; if (actualType !== expectedType) { throw new Error(`Expected type ${expectedType} for key ${key}, but got ${actualType}`); } }); } /** * Compare CLI output with snapshot * @param result - CLI execution result * @param snapshotPath - Path to snapshot file * @param update - Whether to update snapshot */ export function compareSnapshot(result: CLIResult, snapshotPath: string, update: boolean = false): void { if (update || !fs.existsSync(snapshotPath)) { fs.writeFileSync(snapshotPath, result.stdout); return; } const snapshot = fs.readFileSync(snapshotPath, 'utf8'); if (result.stdout !== snapshot) { throw new Error(`Output does not match snapshot\nExpected:\n${snapshot}\n\nActual:\n${result.stdout}`); } }