UNPKG

tui-tester

Version:

End-to-end testing framework for terminal user interfaces

881 lines (876 loc) 22.7 kB
import { promisify } from 'util'; import * as fs from 'fs/promises'; import { exec, spawn } from 'child_process'; // src/adapters/base.ts var BaseRuntimeAdapter = class { /** * Cross-platform sleep implementation */ sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Execute command with timeout */ async execWithTimeout(command, timeoutMs = 3e4) { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error(`Command timeout: ${command}`)), timeoutMs); }); return Promise.race([ this.exec(command), timeoutPromise ]); } /** * Try to execute command, return null on failure */ async tryExec(command) { try { return await this.exec(command); } catch { return null; } } /** * Check if command is available */ async commandExists(command) { const result = await this.tryExec(`which ${command} 2>/dev/null`); return result !== null && result.code === 0; } /** * Get environment variable */ getEnv(key) { if (typeof process !== "undefined") { return process.env[key]; } if (typeof Deno !== "undefined") { return Deno.env.get(key); } if (typeof Bun !== "undefined") { return Bun.env[key]; } return void 0; } /** * Set environment variable */ setEnv(key, value) { if (typeof process !== "undefined") { process.env[key] = value; } if (typeof Deno !== "undefined") { Deno.env.set(key, value); } if (typeof Bun !== "undefined") { Bun.env[key] = value; } } /** * Get current working directory */ getCwd() { if (typeof process !== "undefined") { return process.cwd(); } if (typeof Deno !== "undefined") { return Deno.cwd(); } if (typeof Bun !== "undefined") { return globalThis.process?.cwd() || "/"; } return "/"; } /** * Get platform */ getPlatform() { if (typeof process !== "undefined") { return process.platform; } if (typeof Deno !== "undefined") { return Deno.build.os; } if (typeof Bun !== "undefined") { return globalThis.process?.platform || "unknown"; } return "unknown"; } /** * Check if running on Windows */ isWindows() { const platform = this.getPlatform(); return platform === "win32" || platform === "windows"; } /** * Check if running in CI environment */ isCI() { return !!(this.getEnv("CI") || this.getEnv("CONTINUOUS_INTEGRATION") || this.getEnv("GITHUB_ACTIONS") || this.getEnv("GITLAB_CI") || this.getEnv("CIRCLECI") || this.getEnv("TRAVIS") || this.getEnv("JENKINS_URL")); } }; // src/adapters/bun.ts var BunChildProcess = class { process; // Bun.Subprocess _pid; outputBuffer = ""; errorBuffer = ""; constructor(command, args, options) { this.process = Bun.spawn([command, ...args], { stdin: "pipe", stdout: "pipe", stderr: "pipe", env: options?.env ? { ...process.env, ...options.env } : process.env, cwd: options?.cwd || process.cwd() }); this._pid = this.process.pid; this.startReading().catch(() => { }); } async startReading() { this.readStream(this.process.stdout, (data) => { this.outputBuffer += data; }); this.readStream(this.process.stderr, (data) => { this.errorBuffer += data; }); } async readStream(stream, callback) { const reader = stream.getReader(); const decoder = new TextDecoder(); try { while (true) { const { done, value } = await reader.read(); if (done) break; if (value) { callback(decoder.decode(value)); } } } catch (error) { } finally { reader.releaseLock(); } } get pid() { return this._pid; } get stdin() { return this.process.stdin; } get stdout() { return this.process.stdout; } get stderr() { return this.process.stderr; } kill(signal) { this.process.kill(signal); } async wait() { const code = await this.process.exited; return { code }; } }; var BunAdapter = class extends BaseRuntimeAdapter { processes = /* @__PURE__ */ new Set(); async exec(command) { try { const env = { ...process.env, PATH: `/opt/homebrew/bin:/usr/local/bin:${process.env.PATH}` }; const proc = Bun.spawn(["sh", "-c", command], { stdout: "pipe", stderr: "pipe", env }); const stdout = await new Response(proc.stdout).text(); const stderr = await new Response(proc.stderr).text(); const code = await proc.exited; return { stdout, stderr, code }; } catch (error) { return { stdout: "", stderr: error.message, code: 1 }; } } async spawn(command, args, options) { try { const proc = new BunChildProcess(command, args, options); this.processes.add(proc); proc.wait().then(() => { this.processes.delete(proc); }).catch(() => { this.processes.delete(proc); }); return proc; } catch (error) { throw new Error(`Failed to spawn process: ${error.message}`); } } async kill(proc, signal) { try { if (!proc) return false; if (proc instanceof BunChildProcess) { this.processes.delete(proc); } if (typeof proc.kill === "function") { proc.kill(signal || "SIGTERM"); await new Promise((resolve) => setTimeout(resolve, 100)); if (await this.isAlive(proc)) { proc.kill("SIGKILL"); } return true; } return false; } catch { return false; } } async write(proc, data) { try { if (proc && proc.stdin) { const stream = proc.stdin; const writer = stream.getWriter(); try { let dataToWrite; if (typeof data === "string") { if (data === "") { await writer.close(); return true; } const encoder = new TextEncoder(); dataToWrite = encoder.encode(data); } else { dataToWrite = data; } await writer.write(dataToWrite); await writer.ready; } finally { writer.releaseLock(); } return true; } return false; } catch (error) { console.error("Write error:", error); return false; } } async read(proc, timeout = 1e3) { if (!proc) return ""; if (proc instanceof BunChildProcess) { await new Promise((resolve) => setTimeout(resolve, 200)); const output = proc.outputBuffer; return output; } if (!proc.stdout) return ""; try { const stream = proc.stdout; const reader = stream.getReader(); const decoder = new TextDecoder(); const timeoutPromise = new Promise((resolve) => { setTimeout(() => resolve(""), timeout); }); const readPromise = reader.read().then(({ value, done }) => { reader.releaseLock(); if (done || !value) return ""; return decoder.decode(value); }); return await Promise.race([readPromise, timeoutPromise]); } catch { return ""; } } async resize(_proc, _cols, _rows) { return true; } async isAlive(proc) { if (!proc) return false; try { const p = proc.process; if (p && p.exitCode === null) { return true; } } catch { } return false; } async cleanup() { const procs = Array.from(this.processes); this.processes.clear(); await Promise.all( procs.map(async (proc) => { try { proc.kill("SIGTERM"); await new Promise((resolve) => setTimeout(resolve, 100)); if (await this.isAlive(proc)) { proc.kill("SIGKILL"); } } catch { } }) ); } async readFile(path) { const file = Bun.file(path); return await file.text(); } async writeFile(path, content) { await Bun.write(path, content); } async exists(path) { const file = Bun.file(path); return await file.exists(); } async mkdir(path, options) { const recursive = options?.recursive ? "-p" : ""; await this.exec(`mkdir ${recursive} "${path}"`); } async rmdir(path, options) { const recursive = options?.recursive ? "-rf" : ""; await this.exec(`rm ${recursive} "${path}"`); } }; var execAsync = promisify(exec); var NodeChildProcess = class { process; outputBuffer = ""; errorBuffer = ""; constructor(command, args, options) { this.process = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"], env: options?.env ? { ...process.env, ...options.env } : process.env, cwd: options?.cwd, ...options?.pty ? {} : {} }); if (this.process.stdout) { this.process.stdout.on("data", (chunk) => { this.outputBuffer += chunk.toString(); }); } if (this.process.stderr) { this.process.stderr.on("data", (chunk) => { this.errorBuffer += chunk.toString(); }); } } get output() { return this.outputBuffer; } get error() { return this.errorBuffer; } clearOutput() { this.outputBuffer = ""; this.errorBuffer = ""; } get pid() { return this.process.pid || -1; } get stdin() { return this.process.stdin; } get stdout() { return this.process.stdout; } get stderr() { return this.process.stderr; } kill(signal) { this.process.kill(signal); } wait() { return new Promise((resolve, reject) => { if (this.process.exitCode !== null) { resolve({ code: this.process.exitCode }); return; } const onExit = (code) => { cleanup(); resolve({ code: code || 0 }); }; const onError = (err) => { cleanup(); reject(err); }; const cleanup = () => { this.process.removeListener("exit", onExit); this.process.removeListener("error", onError); }; this.process.once("exit", onExit); this.process.once("error", onError); }); } }; var NodeAdapter = class extends BaseRuntimeAdapter { processes = /* @__PURE__ */ new Set(); /** * Check if command is available with proper PATH */ async commandExists(command) { try { const env = { ...process.env, PATH: `/opt/homebrew/bin:/usr/local/bin:${process.env.PATH}` }; const { stdout } = await execAsync(`which ${command}`, { encoding: "utf8", env }); return stdout.trim().length > 0; } catch { return false; } } async exec(command) { try { const env = { ...process.env, PATH: `/opt/homebrew/bin:/usr/local/bin:${process.env.PATH}` }; const { stdout, stderr } = await execAsync(command, { encoding: "utf8", maxBuffer: 10 * 1024 * 1024, // 10MB buffer env }); return { stdout, stderr, code: 0 }; } catch (error) { return { stdout: error.stdout || "", stderr: error.stderr || error.message, code: error.code || 1 }; } } async spawn(command, args, options) { try { const proc = new NodeChildProcess(command, args, options); this.processes.add(proc); proc.wait().then(() => { this.processes.delete(proc); }).catch(() => { this.processes.delete(proc); }); return proc; } catch (error) { throw new Error(`Failed to spawn process: ${error.message}`); } } async kill(proc, signal) { try { if (!proc) return false; if (proc instanceof NodeChildProcess) { this.processes.delete(proc); } if (typeof proc.kill === "function") { proc.kill(signal || "SIGTERM"); await new Promise((resolve) => setTimeout(resolve, 100)); if (await this.isAlive(proc)) { proc.kill("SIGKILL"); } return true; } return false; } catch { return false; } } async write(proc, data) { try { if (proc && proc.stdin) { const stream = proc.stdin; return new Promise((resolve, reject) => { stream.write(data, (err) => { if (err) reject(err); else resolve(true); }); }); } return false; } catch { return false; } } async read(proc, timeout = 1e3) { if (!proc) return ""; if (proc instanceof NodeChildProcess) { const output = proc.output; proc.clearOutput(); return output; } if (!proc.stdout) return ""; const stream = proc.stdout; return new Promise((resolve) => { let data = ""; const handler = (chunk) => { data += chunk.toString(); }; stream.on("data", handler); setTimeout(() => { stream.off("data", handler); resolve(data); }, timeout); }); } async resize(_proc, _cols, _rows) { return true; } async isAlive(proc) { if (!proc) return false; try { if (proc.pid > 0) { process.kill(proc.pid, 0); return true; } } catch { } return false; } async cleanup() { const procs = Array.from(this.processes); this.processes.clear(); await Promise.all( procs.map(async (proc) => { try { proc.kill("SIGTERM"); await new Promise((resolve) => setTimeout(resolve, 100)); if (await this.isAlive(proc)) { proc.kill("SIGKILL"); } } catch { } }) ); } async readFile(filePath) { return fs.readFile(filePath, "utf8"); } async writeFile(filePath, content) { await fs.writeFile(filePath, content, "utf8"); } async exists(filePath) { try { await fs.access(filePath); return true; } catch { return false; } } async mkdir(dirPath, options) { await fs.mkdir(dirPath, options); } async rmdir(dirPath, options) { await fs.rm(dirPath, { recursive: options?.recursive, force: true }); } }; // src/adapters/deno.ts var DenoChildProcess = class { process; // Deno.ChildProcess _stdin; _stdout; _stderr; outputBuffer = ""; errorBuffer = ""; constructor(command, args, options) { const currentEnv = globalThis.Deno?.env?.toObject?.() || (typeof process !== "undefined" ? process.env : {}); this.process = new Deno.Command(command, { args, stdin: "piped", stdout: "piped", stderr: "piped", env: options?.env ? { ...currentEnv, ...options.env } : currentEnv, cwd: options?.cwd }).spawn(); this._stdin = this.process.stdin; this._stdout = this.process.stdout; this._stderr = this.process.stderr; this.startReading(); } async startReading() { this.readStream(this._stdout, (data) => { this.outputBuffer += data; }); this.readStream(this._stderr, (data) => { this.errorBuffer += data; }); } async readStream(stream, callback) { const reader = stream.getReader(); const decoder = new TextDecoder(); try { while (true) { const { done, value } = await reader.read(); if (done) break; if (value) { callback(decoder.decode(value)); } } } catch (error) { } finally { reader.releaseLock(); } } get pid() { return this.process.pid; } get stdin() { return this._stdin; } get stdout() { return this._stdout; } get stderr() { return this._stderr; } kill(signal) { this.process.kill(signal); } async wait() { const status = await this.process.status; return { code: status.code }; } }; var DenoAdapter = class extends BaseRuntimeAdapter { processes = /* @__PURE__ */ new Set(); async exec(command) { try { const currentEnv = globalThis.Deno?.env?.toObject?.() || (typeof process !== "undefined" ? process.env : {}); const currentPath = globalThis.Deno?.env?.get?.("PATH") || (typeof process !== "undefined" ? process.env.PATH : ""); const extendedEnv = { ...currentEnv, PATH: `/opt/homebrew/bin:/usr/local/bin:${currentPath}` }; const cmd = new Deno.Command("sh", { args: ["-c", command], stdout: "piped", stderr: "piped", env: extendedEnv }); const { code, stdout, stderr } = await cmd.output(); return { stdout: new TextDecoder().decode(stdout), stderr: new TextDecoder().decode(stderr), code }; } catch (error) { return { stdout: "", stderr: error.message, code: 1 }; } } async spawn(command, args, options) { try { const proc = new DenoChildProcess(command, args, options); this.processes.add(proc); proc.wait().then(() => { this.processes.delete(proc); }).catch(() => { this.processes.delete(proc); }); return proc; } catch (error) { throw new Error(`Failed to spawn process: ${error.message}`); } } async kill(proc, signal) { try { if (!proc) return false; if (proc instanceof DenoChildProcess) { this.processes.delete(proc); } if (typeof proc.kill === "function") { proc.kill(signal || "SIGTERM"); await new Promise((resolve) => setTimeout(resolve, 100)); if (await this.isAlive(proc)) { proc.kill("SIGKILL"); } return true; } return false; } catch { return false; } } async write(proc, data) { try { if (proc && proc.stdin) { const stream = proc.stdin; const writer = stream.getWriter(); const encoder = new TextEncoder(); await writer.write(encoder.encode(data)); writer.releaseLock(); return true; } return false; } catch { return false; } } async read(proc, timeout = 1e3) { if (!proc) return ""; if (proc instanceof DenoChildProcess) { await new Promise((resolve) => setTimeout(resolve, 50)); const output = proc.outputBuffer; proc.outputBuffer = ""; return output; } if (!proc.stdout) return ""; try { const stream = proc.stdout; const reader = stream.getReader(); const decoder = new TextDecoder(); const timeoutPromise = new Promise((resolve) => { setTimeout(() => resolve(""), timeout); }); const readPromise = reader.read().then(({ value, done }) => { reader.releaseLock(); if (done || !value) return ""; return decoder.decode(value); }); return await Promise.race([readPromise, timeoutPromise]); } catch { return ""; } } async resize(_proc, _cols, _rows) { return true; } async isAlive(proc) { if (!proc) return false; try { const p = proc.process; if (p && typeof p.status === "function") { return true; } } catch { } return false; } async cleanup() { const procs = Array.from(this.processes); this.processes.clear(); await Promise.all( procs.map(async (proc) => { try { proc.kill("SIGTERM"); await new Promise((resolve) => setTimeout(resolve, 100)); if (await this.isAlive(proc)) { proc.kill("SIGKILL"); } } catch { } }) ); } async readFile(path) { return await Deno.readTextFile(path); } async writeFile(path, content) { await Deno.writeTextFile(path, content); } async exists(path) { try { await Deno.stat(path); return true; } catch { return false; } } async mkdir(path, options) { await Deno.mkdir(path, options); } async rmdir(path, options) { await Deno.remove(path, { recursive: options?.recursive }); } }; // src/adapters/index.ts var adapterRegistry = /* @__PURE__ */ new Map(); function registerAdapter(name, adapterClass, options) { adapterRegistry.set(name.toLowerCase(), { adapterClass, detect: options?.detect, priority: options?.priority ?? 0 }); } var defaultAdapterName = null; function setDefaultAdapter(name) { defaultAdapterName = name.toLowerCase(); } function detectRuntime() { if (typeof process !== "undefined" && process.env?.TUI_TESTER_ADAPTER) { const envAdapter = process.env.TUI_TESTER_ADAPTER.toLowerCase(); if (envAdapter === "node" || envAdapter === "deno" || envAdapter === "bun") { return envAdapter; } } if (typeof Deno !== "undefined" && typeof Deno.version !== "undefined") { return "deno"; } if (typeof Bun !== "undefined" && typeof Bun.version !== "undefined") { return "bun"; } if (typeof process !== "undefined" && process.versions?.node) { return "node"; } return "node"; } function createAdapter(runtime) { if (defaultAdapterName) { const customAdapter = adapterRegistry.get(defaultAdapterName); if (customAdapter) { return new customAdapter.adapterClass(); } if (defaultAdapterName === "node" || defaultAdapterName === "deno" || defaultAdapterName === "bun") { runtime = defaultAdapterName; } } if (!runtime) { const sortedAdapters = Array.from(adapterRegistry.entries()).filter(([, config]) => config.detect).sort(([, a], [, b]) => (b.priority ?? 0) - (a.priority ?? 0)); for (const [, config] of sortedAdapters) { if (config.detect && config.detect()) { return new config.adapterClass(); } } } if (typeof runtime === "string" && adapterRegistry.has(runtime.toLowerCase())) { const customAdapter = adapterRegistry.get(runtime.toLowerCase()); return new customAdapter.adapterClass(); } const detectedRuntime = typeof runtime === "string" ? runtime : runtime || detectRuntime(); switch (detectedRuntime) { case "node": return new NodeAdapter(); case "deno": return new DenoAdapter(); case "bun": return new BunAdapter(); default: return new NodeAdapter(); } } var currentAdapter = null; function getAdapter() { if (!currentAdapter) { currentAdapter = createAdapter(); } return currentAdapter; } function setAdapter(adapter) { currentAdapter = adapter; } function resetAdapter() { currentAdapter = null; } export { BaseRuntimeAdapter, createAdapter, detectRuntime, getAdapter, registerAdapter, resetAdapter, setAdapter, setDefaultAdapter }; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map