tui-tester
Version:
End-to-end testing framework for terminal user interfaces
3,034 lines • 90.5 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var util = require('util');
var fs = require('fs/promises');
var child_process = require('child_process');
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
// node_modules/tsup/assets/cjs_shims.js
var init_cjs_shims = __esm({
"node_modules/tsup/assets/cjs_shims.js"() {
}
});
// src/core/utils.ts
function stripAnsi(text) {
return text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b\][0-9];[^\x07]*\x07/g, "").replace(/\x1b[PX^_].*?\x1b\\/g, "").replace(/\x1b[=><!~]/g, "").replace(/\x1b\[[0-9;]*[mGKHfJ]/g, "").replace(/\x1b\[[\d;]*\d*[A-Za-z]/g, "").replace(/\x1b\(\w/g, "").replace(/\x1b\[\?[\d;]*[hlc]/g, "").replace(/\x08/g, "");
}
function parseScreen(content, cols, rows) {
const lines = content.split("\n");
const grid = [];
for (let y = 0; y < rows; y++) {
grid[y] = [];
const line = lines[y] || "";
for (let x = 0; x < cols; x++) {
grid[y][x] = line[x] || " ";
}
}
return grid;
}
function findText(content, searchText) {
const positions = [];
const lines = content.split("\n");
for (let y = 0; y < lines.length; y++) {
let x = lines[y].indexOf(searchText);
while (x !== -1) {
positions.push({ x, y });
x = lines[y].indexOf(searchText, x + 1);
}
}
return positions;
}
function extractRegion(content, region) {
const lines = content.split("\n");
const extracted = [];
for (let y = region.y; y < region.y + region.height && y < lines.length; y++) {
const line = lines[y] || "";
const start = region.x;
const end = Math.min(region.x + region.width, line.length);
extracted.push(line.substring(start, end));
}
return extracted.join("\n");
}
function normalizeText(text, options = {}) {
let normalized = text;
if (options.ignoreAnsi) {
normalized = stripAnsi(normalized);
}
if (options.normalizeLineEndings) {
normalized = normalized.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
}
if (options.trimLines) {
normalized = normalized.split("\n").map((line) => line.trim()).join("\n");
}
if (options.ignoreWhitespace) {
normalized = normalized.replace(/\s+/g, " ").trim();
}
if (options.ignoreCase) {
normalized = normalized.toLowerCase();
}
return normalized;
}
function compareScreens(actual, expected, options = {}) {
const normalizedActual = normalizeText(actual, options);
const normalizedExpected = normalizeText(expected, options);
return normalizedActual === normalizedExpected;
}
function screenDiff(actual, expected) {
const actualLines = actual.split("\n");
const expectedLines = expected.split("\n");
const diff = [];
const maxLines = Math.max(actualLines.length, expectedLines.length);
for (let i = 0; i < maxLines; i++) {
const actualLine = actualLines[i] || "";
const expectedLine = expectedLines[i] || "";
if (actualLine !== expectedLine) {
diff.push(`Line ${i + 1}:`);
diff.push(` Expected: "${expectedLine}"`);
diff.push(` Actual: "${actualLine}"`);
}
}
return diff.join("\n");
}
async function waitFor(fn, options = {}) {
const timeout = options.timeout ?? 5e3;
const interval = options.interval ?? 100;
const message = options.message ?? "Timeout waiting for condition";
const startTime = Date.now();
while (true) {
const result = await fn();
if (result !== void 0) {
return result;
}
if (Date.now() - startTime > timeout) {
throw new Error(`${message} (timeout: ${timeout}ms)`);
}
await sleep(interval);
}
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function formatTimestamp(timestamp) {
const date = new Date(timestamp);
return date.toISOString().replace("T", " ").substring(0, 23);
}
function generateSessionName(prefix) {
const p = prefix || "tui-test";
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 8);
return `${p}-${timestamp}-${random}`;
}
function parseScreenLines(content) {
const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
return normalized.split("\n").map((line) => stripAnsi(line));
}
async function waitForCondition(condition, options = {}) {
const timeout = options.timeout ?? 5e3;
const interval = options.interval ?? 100;
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const result = await condition();
if (result) {
return true;
}
await sleep(interval);
}
return false;
}
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function normalizeLineEndings(text) {
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
}
function trimScreenContent(content) {
const isArray = Array.isArray(content);
const lines = isArray ? content : content.split("\n");
while (lines.length > 0 && lines[0].trim() === "") {
lines.shift();
}
while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
lines.pop();
}
return isArray ? lines : lines.join("\n");
}
async function isCommandAvailable(command) {
try {
const { exec } = await import('child_process');
const { promisify: promisify2 } = await import('util');
const execAsync2 = promisify2(exec);
const cmd = command.split(" ")[0];
const env = {
...process.env,
PATH: `/opt/homebrew/bin:/usr/local/bin:${process.env.PATH}`
};
await execAsync2(`which ${cmd}`, { env });
return true;
} catch {
return false;
}
}
function getTerminalSize() {
if (typeof process !== "undefined" && process.stdout && process.stdout.isTTY) {
return {
cols: process.stdout.columns || 80,
rows: process.stdout.rows || 24
};
}
return { cols: 80, rows: 24 };
}
function escapeShellArg(arg) {
return `'${arg.replace(/'/g, "'\\''")}'`;
}
function parseTmuxKey(key, modifiers) {
const keyMap = {
"enter": "Enter",
"return": "Enter",
"tab": "Tab",
"escape": "Escape",
"esc": "Escape",
"space": "Space",
"backspace": "BSpace",
"delete": "Delete",
"up": "Up",
"down": "Down",
"left": "Left",
"right": "Right",
"home": "Home",
"end": "End",
"pageup": "PageUp",
"pagedown": "PageDown",
"insert": "Insert",
"f1": "F1",
"f2": "F2",
"f3": "F3",
"f4": "F4",
"f5": "F5",
"f6": "F6",
"f7": "F7",
"f8": "F8",
"f9": "F9",
"f10": "F10",
"f11": "F11",
"f12": "F12"
};
let tmuxKey = keyMap[key.toLowerCase()] || key;
if (modifiers) {
const prefixes = [];
if (modifiers.ctrl) prefixes.push("C-");
if (modifiers.alt) prefixes.push("M-");
if (modifiers.shift) prefixes.push("S-");
if (prefixes.length > 0 && tmuxKey.length === 1) {
tmuxKey = prefixes.join("") + tmuxKey.toLowerCase();
} else if (prefixes.length > 0) {
tmuxKey = prefixes.join("") + tmuxKey;
}
}
return tmuxKey;
}
function parseTmuxMouse(x, y, button = "left") {
const buttonMap = {
"left": 0,
"middle": 1,
"right": 2
};
return `\x1B[<${buttonMap[button]};${x + 1};${y + 1}M`;
}
function extractCursorPosition(ansiResponse) {
const match = ansiResponse.match(/\x1b\[(\d+);(\d+)R/);
if (match) {
return {
x: parseInt(match[2]) - 1,
// Convert from 1-based to 0-based
y: parseInt(match[1]) - 1
};
}
return null;
}
function splitLines(text) {
return text.split(/\r?\n/);
}
function joinLines(lines, lineEnding = "\n") {
return lines.join(lineEnding);
}
function getTextDimensions(text) {
const lines = splitLines(text);
const height = lines.length;
const width = Math.max(...lines.map((line) => getStringWidth(line)));
return { width, height };
}
function getStringWidth(str) {
let width = 0;
for (const char of str) {
width += getCharWidth(char);
}
return width;
}
function getCharWidth(char) {
const code = char.charCodeAt(0);
if (code < 32 || code >= 127 && code < 160) {
return 0;
}
if (code >= 4352 && code <= 4447 || // Hangul Jamo
code >= 11904 && code <= 40959 || // CJK
code >= 44032 && code <= 55215 || // Hangul Syllables
code >= 63744 && code <= 64255 || // CJK Compatibility
code >= 65072 && code <= 65103 || // CJK Compatibility Forms
code >= 65280 && code <= 65376 || // Fullwidth Forms
code >= 65504 && code <= 65510) {
return 2;
}
return 1;
}
exports.delay = void 0;
var init_utils = __esm({
"src/core/utils.ts"() {
init_cjs_shims();
exports.delay = sleep;
}
});
// src/adapters/base.ts
exports.BaseRuntimeAdapter = void 0;
var init_base = __esm({
"src/adapters/base.ts"() {
init_cjs_shims();
exports.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; exports.BunAdapter = void 0;
var init_bun = __esm({
"src/adapters/bun.ts"() {
init_cjs_shims();
init_base();
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 };
}
};
exports.BunAdapter = class extends exports.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, NodeChildProcess; exports.NodeAdapter = void 0;
var init_node = __esm({
"src/adapters/node.ts"() {
init_cjs_shims();
init_base();
execAsync = util.promisify(child_process.exec);
NodeChildProcess = class {
process;
outputBuffer = "";
errorBuffer = "";
constructor(command, args, options) {
this.process = child_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);
});
}
};
exports.NodeAdapter = class extends exports.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__namespace.readFile(filePath, "utf8");
}
async writeFile(filePath, content) {
await fs__namespace.writeFile(filePath, content, "utf8");
}
async exists(filePath) {
try {
await fs__namespace.access(filePath);
return true;
} catch {
return false;
}
}
async mkdir(dirPath, options) {
await fs__namespace.mkdir(dirPath, options);
}
async rmdir(dirPath, options) {
await fs__namespace.rm(dirPath, { recursive: options?.recursive, force: true });
}
};
}
});
// src/adapters/deno.ts
var DenoChildProcess; exports.DenoAdapter = void 0;
var init_deno = __esm({
"src/adapters/deno.ts"() {
init_cjs_shims();
init_base();
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 };
}
};
exports.DenoAdapter = class extends exports.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
function registerAdapter(name, adapterClass, options) {
adapterRegistry.set(name.toLowerCase(), {
adapterClass,
detect: options?.detect,
priority: options?.priority ?? 0
});
}
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 exports.NodeAdapter();
case "deno":
return new exports.DenoAdapter();
case "bun":
return new exports.BunAdapter();
default:
return new exports.NodeAdapter();
}
}
function getAdapter() {
if (!currentAdapter) {
currentAdapter = createAdapter();
}
return currentAdapter;
}
function setAdapter(adapter) {
currentAdapter = adapter;
}
function resetAdapter() {
currentAdapter = null;
}
var adapterRegistry, defaultAdapterName, currentAdapter;
var init_adapters = __esm({
"src/adapters/index.ts"() {
init_cjs_shims();
init_bun();
init_node();
init_deno();
init_base();
adapterRegistry = /* @__PURE__ */ new Map();
defaultAdapterName = null;
currentAdapter = null;
}
});
// src/snapshot/snapshot-manager.ts
var snapshot_manager_exports = {};
__export(snapshot_manager_exports, {
SnapshotManager: () => exports.SnapshotManager,
getSnapshotManager: () => getSnapshotManager,
resetSnapshotManager: () => resetSnapshotManager
});
function getSnapshotManager(options) {
if (!globalSnapshotManager) {
globalSnapshotManager = new exports.SnapshotManager(options);
} else if (options) {
globalSnapshotManager.configure(options);
}
return globalSnapshotManager;
}
function resetSnapshotManager() {
globalSnapshotManager = null;
}
exports.SnapshotManager = void 0; var globalSnapshotManager;
var init_snapshot_manager = __esm({
"src/snapshot/snapshot-manager.ts"() {
init_cjs_shims();
init_adapters();
init_utils();
exports.SnapshotManager = class {
adapter;
options;
snapshots = /* @__PURE__ */ new Map();
snapshotCounter = 0;
snapshotDir;
constructor(snapshotDir) {
this.adapter = getAdapter();
if (typeof snapshotDir === "string") {
this.snapshotDir = snapshotDir;
this.options = {
updateSnapshots: false,
snapshotDir,
diffOptions: {},
format: "text"
};
} else {
const options = snapshotDir || {};
this.snapshotDir = options.snapshotDir ?? "./__snapshots__";
this.options = {
updateSnapshots: options.updateSnapshots ?? false,
snapshotDir: this.snapshotDir,
diffOptions: options.diffOptions ?? {},
format: options.format ?? "text"
};
}
}
/**
* Configure the snapshot manager
*/
configure(options) {
if (options.updateSnapshots !== void 0) {
this.options.updateSnapshots = options.updateSnapshots;
}
if (options.snapshotDir !== void 0) {
this.options.snapshotDir = options.snapshotDir;
this.snapshotDir = options.snapshotDir;
}
if (options.diffOptions !== void 0) {
this.options.diffOptions = options.diffOptions;
}
if (options.format !== void 0) {
this.options.format = options.format;
}
if (options.stripAnsi !== void 0 && this.options.diffOptions) {
this.options.diffOptions.ignoreAnsi = options.stripAnsi;
}
if (options.trim !== void 0 && this.options.diffOptions) {
this.options.diffOptions.ignoreWhitespace = options.trim;
}
}
/**
* Create a snapshot from screen capture
*/
createSnapshot(capture, name) {
const snapshotName = name || `snapshot-${++this.snapshotCounter}`;
const snapshot = {
id: this.generateSnapshotId(snapshotName),
name: snapshotName,
capture,
metadata: {
createdAt: Date.now(),
format: this.options.format
}
};
this.snapshots.set(snapshot.id, snapshot);
return snapshot;
}
/**
* Match a capture against a snapshot
*/
async matchSnapshot(capture, snapshotName, testPath) {
const snapshotPath = this.getSnapshotPath(snapshotName, testPath);
try {
const existingSnapshot = await this.loadSnapshot(snapshotPath);
const pass = this.compareCaptures(capture, existingSnapshot.capture);
if (!pass) {
const diff = this.generateDiff(capture, existingSnapshot.capture);
return {
pass: false,
message: `Snapshot mismatch for "${snapshotName}"`,
diff
};
}
return { pass: true };
} catch (error) {
if (this.options.updateSnapshots) {
const snapshot = this.createSnapshot(capture, snapshotName);
await this.saveSnapshot(snapshot, snapshotPath);
return {
pass: true,
message: `New snapshot created for "${snapshotName}"`
};
} else {
return {
pass: false,
message: `Snapshot "${snapshotName}" does not exist. Run with updateSnapshots=true to create it.`
};
}
}
}
/**
* Compare two screen captures
*/
compareCaptures(actual, expected) {
switch (this.options.format) {
case "ansi":
return compareScreens(actual.raw, expected.raw, this.options.diffOptions);
case "text":
return compareScreens(actual.text, expected.text, this.options.diffOptions);
case "json":
default: {
const actualNorm = normalizeText(actual.text, this.options.diffOptions);
const expectedNorm = normalizeText(expected.text, this.options.diffOptions);
return actualNorm === expectedNorm;
}
}
}
/**
* Generate diff between captures
*/
generateDiff(actual, expected) {
const actualText = this.options.format === "ansi" ? actual.raw : actual.text;
const expectedText = this.options.format === "ansi" ? expected.raw : expected.text;
return screenDiff(actualText, expectedText);
}
/**
* Save snapshot to file
*/
async saveSnapshot(snapshot, path) {
const snapshotPath = path || this.getSnapshotPath(snapshot.name);
const dir = snapshotPath.substring(0, snapshotPath.lastIndexOf("/"));
await this.adapter.mkdir(dir, { recursive: true });
let content;
switch (this.options.format) {
case "text":
content = snapshot.capture.text;
break;
case "ansi":
content = snapshot.capture.raw;
break;
case "json":
default:
content = JSON.stringify(snapshot, null, 2);
break;
}
await this.adapter.writeFile(snapshotPath, content);
}
/**
* Load snapshot from file
*/
async loadSnapshot(path) {
const content = await this.adapter.readFile(path);
let snapshot;
switch (this.options.format) {
case "text":
case "ansi":
const lines = content.split("\n");
snapshot = {
id: path,
name: path.substring(path.lastIndexOf("/") + 1),
capture: {
raw: this.options.format === "ansi" ? content : "",
text: this.options.format === "text" ? content : "",
lines,
timestamp: 0,
size: { cols: 0, rows: lines.length }
}
};
break;
case "json":
default:
snapshot = JSON.parse(content);
break;
}
this.snapshots.set(snapshot.id, snapshot);
return snapshot;
}
/**
* Update existing snapshot
*/
async updateSnapshot(snapshotName, capture, testPath) {
const snapshot = this.createSnapshot(capture, snapshotName);
const snapshotPath = this.getSnapshotPath(snapshotName, testPath);
await this.saveSnapshot(snapshot, snapshotPath);
}
/**
* Remove snapshot
*/
async removeSnapshot(snapshotName, testPath) {
const snapshotPath = this.getSnapshotPath(snapshotName, testPath);
const snapshot = Array.from(this.snapshots.values()).find((s) => s.name === snapshotName);
if (snapshot) {
this.snapshots.delete(snapshot.id);
}
if (await this.adapter.exists(snapshotPath)) {
await this.adapter.exec(`rm "${snapshotPath}"`);
}
}
/**
* List all snapshots
*/
async listSnapshots() {
const extension = this.getFileExtension();
const result = await this.adapter.exec(
`find "${this.options.snapshotDir}" -name "*${extension}" 2>/dev/null || true`
);
return result.stdout.split("\n").filter((line) => line.trim()).map((path) => path.substring(path.lastIndexOf("/") + 1));
}
/**
* Clear all snapshots
*/
async clearSnapshots() {
this.snapshots.clear();
if (await this.adapter.exists(this.options.snapshotDir)) {
await this.adapter.exec(`rm -rf "${this.options.snapshotDir}"`);
}
}
/**
* Get snapshot by name
*/
getSnapshot(name) {
return Array.from(this.snapshots.values()).find((s) => s.name === name);
}
/**
* Check if snapshot exists
*/
async snapshotExists(snapshotName, testPath) {
const snapshotPath = this.getSnapshotPath(snapshotName, testPath);
return this.adapter.exists(snapshotPath);
}
// Simple API methods for backward compatibility
/**
* Save a simple text snapshot
*/
async save(name, content) {
const sanitizedName = this.sanitizeName(name);
const filePath = `${this.snapshotDir}/${sanitizedName}.snap`;
await this.adapter.mkdir(this.snapshotDir, { recursive: true });
await this.adapter.writeFile(filePath, content);
}
/**
* Load a simple text snapshot
*/
async load(name) {
try {
const sanitizedName = this.sanitizeName(name);
const filePath = `${this.snapshotDir}/${sanitizedName}.snap`;
return await this.adapter.readFile(filePath);
} catch (error) {
return null;
}
}
/**
* Compare two snapshots
*/
async compare(name1, name2) {
const content1 = await this.load(name1);
const content2 = await this.load(name2);
if (content1 === null || content2 === null) {
return {
identical: false,
differences: [],
error: `Snapshot does not exist: ${content1 === null ? name1 : name2}`
};
}
const lines1 = content1.split("\n");
const lines2 = content2.split("\n");
const differences = [];
const maxLines = Math.max(lines1.length, lines2.length);
for (let i = 0; i < maxLines; i++) {
const line1 = lines1[i];
const line2 = lines2[i];
if (line1 !== line2) {
differences.push({
line: i + 1,
expected: line1,
actual: line2
});
}
}
return {
identical: differences.length === 0,
differences
};
}
/**
* Compare with options
*/
async compareWithOptions(name1, name2, options) {
const content1 = await this.load(name1);
const content2 = await this.load(name2);
if (content1 === null || content2 === null) {
return { identical: false, match: false, diff: `Snapshot does not exist: ${content1 === null ? name1 : name2}` };
}
let normalized1 = content1;
let normalized2 = content2;
if (options.ignoreWhitespace) {
normalized1 = normalized1.replace(/\s+/g, " ").trim();
normalized2 = normalized2.replace(/\s+/g, " ").trim();
}
if (options.ignoreAnsi) {
const ansiRegex = /\x1b\[[0-9;]*m/g;
normalized1 = normalized1.replace(ansiRegex, "");
normalized2 = normalized2.replace(ansiRegex, "");
}
const match = normalized1 === normalized2;
if (!match) {
const diff = this.createDiff(normalized2, normalized1);
return { identical: false, match: false, diff };
}
return { identical: true, match: true };
}
/**
* Delete a snapshot
*/
async delete(name) {
try {
const sanitizedName = this.sanitizeName(name);
const filePath = `${this.snapshotDir}/${sanitizedName}.snap`;
if (await this.adapter.exists(filePath)) {
await this.adapter.exec(`rm "${filePath}"`);
return true;
}
return false;
} catch (error) {
return false;
}
}
/**
* Delete all snapshots
*/
async deleteAll() {
try {
if (await this.adapter.exists(this.snapshotDir)) {
await this.adapter.exec(`rm -rf "${this.snapshotDir}"`);
await this.adapter.mkdir(this.snapshotDir, { recursive: true });
}
} catch (error) {
}
}
/**
* List all snapshot names
*/
async list(pattern) {
try {
if (!await this.adapter.exists(this.snapshotDir)) {
return [];
}
const result = await this.adapter.exec(
`find "${this.snapshotDir}" -name "*.snap" 2>/dev/null || true`
);
const files = result.stdout.split("\n").filter((line) => line.trim() && line.endsWith(".snap")).map((path) => {
const filename = path.substring(path.lastIndexOf("/") + 1);
return filename.replace(".snap", "");
});
if (pattern) {
const regex = new RegExp(pattern);
return files.filter((name) => regex.test(name));
}
return files;
} catch (error) {
return [];
}
}
/**
* List by pattern (alias for list with pattern)
*/
async listByPattern(pattern) {
return this.list(pattern);
}
/**
* Check if update is needed
*/
async needsUpdate(name, content) {
const existing = await this.load(name);
if (existing === null) {
return true;
}
return existing !== content;
}
/**
* Save with metadata
*/
async saveWithMetadata(name, content, metadata) {
const data = JSON.stringify({
content,
metadata: {
...metadata,
timestamp: Date.now()
}
}, null, 2);
const sanitizedName = this.sanitizeName(name);
const filePath = `${this.snapshotDir}/${sanitizedName}.snap`;
await this.adapter.mkdir(this.snapshotDir, { recursive: true });
await this.adapter.writeFile(filePath, data);
}
/**
* Load with metadata
*/
async loadWithMetadata(name) {
try {
const sanitizedName = this.sanitizeName(name);
const filePath = `${this.snapshotDir}/${sanitizedName}.snap`;
const data = await this.adapter.readFile(filePath);
try {
const parsed = JSON.parse(data);
if (parsed.content !== void 0) {
return parsed;
}
return { content: data };
} catch {
return { content: data };
}
} catch (error) {
return null;
}
}
/**
* Format snapshot for display
*/
async format(name) {
const content = await this.load(name);
if (!content) return null;
const lines = content.split("\n");
return lines.map((line, i) => `${i + 1}: ${line}`).join("\n");
}
/**
* Format with options
*/
async formatWithOptions(name, options) {
const content = await this.load(name);
if (!content) return null;
const lines = content.split("\n");
if (options.lineNumbers) {
return lines.map((line, i) => `${(i + 1).toString().padStart(3, " ")} | ${line}`).join("\n");
}
return content;
}
/**
* Generate unified diff between two snapshots
*/
async diff(name1, name2) {
const content1 = await this.load(name1);
const content2 = await this.load(name2);
if (!content1 || !content2) {
return null;
}
return this.createDiff(content1, content2);
}
/**
* Sanitize snapshot name
*/
sanitizeName(name) {
return name.replace(/\//g, "_slash_").replace(/\.\./g, "_dot_").replace(/\s+/g, "_space_").replace(/[^a-zA-Z0-9-_]/g, "");
}
/**
* Create a simple diff
*/
createDiff(expected, actual) {
const expectedLines = expected.split("\n");
const actualLines = actual.split("\n");
const maxLines = Math.max(expectedLines.length, actualLines.length);
const diff = [];
for (let i = 0; i < maxLines; i++) {
const expectedLine = expectedLines[i];
const actualLine = actualLines[i];
if (expectedLine !== actualLine) {
if (expectedLine !== void 0) {
diff.push(`-${expectedLine}`);
}
if (actualLine !== void 0) {
diff.push(`+${actualLine}`);
}
}
}
return diff.join("\n");
}
// Private helper methods
generateSnapshotId(name) {
return `${name}-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
}
getSnapshotPath(snapshotName, testPath) {
const extension = this.getFileExtension();
if (testPath) {
const testDir = testPath.substring(0, testPath.lastIndexOf("/"));
const testFile = testPath.substring(testPath.lastIndexOf("/") + 1);
const testName = testFile.replace(/\.(test|spec)\.(ts|js)$/, "");
return `${testDir}/__snapshots__/${testName}.${snapshotName}${extension}`;
} else {
return `${this.options.snapshotDir}/${snapshotName}${extension}`;
}
}
getFileExtension() {
switch (this.options.format) {
case "text":
return ".txt";
case "ansi":
return ".ansi";
case "json":
default:
return ".json";
}
}
};
globalSnapshotManager = null;
}
});
// src/index.ts
init_cjs_shims();
// src/core/types.ts
init_cjs_shims();
// src/index.ts
init_utils();
// src/tmux-tester.ts
init_cjs_shims();
init_adapters();
init_utils();
var TmuxTester = class {
config;
adapter;
sessionName;
running = false;
_outputBuffer = "";
// Store last captured output
recording = null;
snapshots = /* @__PURE__ */ new Map();
debugMode;
constructor(config) {
this.config = {
command: config.command,
size: config.size || { cols: 80, rows: 24 },
env: config.env || {},
cwd: config.cwd || process.cwd(),
shell: config.shell || "sh",
sessionName: config.sessionName || generateSessionName(),
debug: config.debug || false,
recordingEnabled: config.recordingEnabled || false,
snapshotDir: config.snapshotDir || "./snapshots"
};
this.adapter = getAdapter();
this.sessionName = this.config.sessionName;
this.debugMode = this.config.debug;
}
/**
* Start the tmux session and launch the application
*/
async start() {
if (this.running) {
throw new Error("Tester is already running");
}
const tmuxAvailable = this.adapter.commandExists ? await this.adapter.commandExists("tmux") : true;
if (!tmuxAvailable) {
throw new Error("tmux is not installed. Please install tmux to use the terminal tester.");
}
if (this.adapter.tryExec) {
await this.adapter.tryExec(`tmux kill-session -t ${this.sessionName} 2>/dev/null`);
}
const { cols, rows } = this.config.size;
const envVars = Object.entries(this.config.env).map(([key, value]) => `-e ${key}=${escapeShellArg(value)}`).join(" ");
const needsShell = this.config.command.length > 0 && !this.config.command[0].match(/^(bash|sh|zsh|fish|dash)$/);
let createCmd;
if (needsShell) {
createCmd = `tmux new-session -d -s ${this.sessionName} -x ${cols} -y ${rows} ${envVars} -c ${escapeShellArg(this.config.cwd)} bash`;
} else {
const shell = this.config.command[0] || "bash";
createCmd = `tmux new-session -d -s ${this.sessionName} -x ${cols} -y ${rows} ${envVars} -c ${escapeShellArg(this.config.cwd)} ${shell}`;
}
this.debug(`Creating tmux session: ${createCmd}`);
const result = await this.adapter.exec(createCmd);
if (result.code !== 0) {
throw new Error(`Failed to create tmux session: ${result.stderr}`);
}
this.running = true;
if (needsShell) {
await this.sleep(1e3);
const appCmd = this.config.command.join(" ");
await this.sendCommand(appCmd);
}
await this.sleep(500);
this.debug("Tmux session started successfully");
if (this.config.recordingEnabled) {
this.startRecording();
}
}
/**
* Stop the tmux session
*/
async stop() {
if (!this.running) {
return;
}
if (this.recording) {
this.stopRecording();
}
const result = await this.adapter.exec(`tmux kill-session -t ${this.sessionName}`);
if (result.code !== 0) {
this.debug(`Warning: Failed to kill tmux session: ${result.stderr}`);
}
this.running = false;
this.debug("Tmux session stopped");
}
/**
* Restart the session
*/
async restart() {
await this.stop();
await this.start();
}
/**
* Check if the session is running
*/
isRunning() {
return this.running;
}
/**
* Send text to the terminal
*/
async sendText(text) {
this.ensureRunning();
const escaped = escapeShellArg(text);
const cmd = `tmux send-keys -t ${this.sessionName} ${escaped}`;
this.debug(`Sending text: ${text}`);
await this.adapter.exec(cmd);
this.recordEvent("input", { type: "text", text });
await this.sleep(100);
}
/**
* Send a key with optional modifiers
*/
async sendKey(key, modifiers) {
this.ensureRunning();
const tmuxKey = parseTmuxKey(key, modifiers);
const cmd = `tmux send-keys -t ${this.sessionName} ${tmuxKey}`;
this.debug(`Sending key: ${key} (tmux: ${tmuxKey})`);
await this.adapter.exec(cmd);
this.recordEvent("key", { key, modifiers });
await this.sleep(100);
}
/**
* Send multiple keys
*/
async sendKeys(keys) {
for (const key of keys) {
await this.sendKey(key);
}
}
/**
* Enable mouse support in the terminal
*/
async enableMouse() {
this.ensureRunning();
await this.adapter.exec(`tmux set -t ${this.sessionName} mouse on`);
this.debug("Mouse support enabled");
}
/**
* Disable mouse support in the terminal
*/
async disableMouse() {
this.ensureRunning();
await this.adapter.exec(`tmux set -t ${this.sessionName} mouse off`);
this.debug("Mouse support disabled");
}
/**
* Click at specific position
*/
async click(x, y) {
await this.sendMouse({
type: "click",
position: { x, y },
button: "left"
});
}
/**
* Click on text in the terminal
*/
async clickText(text) {
const capture = await this.captureScreen();
const lines = capture.lines;
for (let y = 0; y < lines.length; y++) {
const x = lines[y].indexOf(text);
if (x !== -1) {
await this.click(x, y);
return;
}
}
throw new Error(`Text "${text}" not found on screen`);
}
/**
* Double click at specific position
*/
async doubleClick(x, y) {
await this.click(x, y);
await this.sleep(50);
await this.click(x, y);
}
/**
* Right click at specific position
*/
async rightClick(x, y) {
await this.sendMouse({
type: "click",
position: { x, y },
button: "right"
});
}
/**
* Drag from one position to another
*/
async drag(from, to) {
await this.sendMouse({
type: "click",
position: from,
button: "left"
});
await this.sendMouse({
type: "move",
position: to,
button: "left"
});
await this.sendMouse({
type: "click",
position: to,
button: "left"
});
}
/**
* Scroll up or down
*/
async scroll(direction, lines = 1) {
const currentPos = await this.getCursor();
for (let i = 0; i < lines; i++) {
await this.sendMouse({
type: "scroll",
position: currentPos,
button: direction
});
await this.sleep(50);
}
}
/**
* Send a mouse event
*/
async sendMouse(event) {
this.ensureRunning();
await this.adapter.exec(`tmux set -t ${this.sessionName} mouse on`);
let cmd;
switch (event.type) {
case "click":
const button = event.button === "left" || event.button === "middle" || event.button === "right" ? event.button : "left";
const sequence = parseTmuxMouse(event.position.x, event.position.y, button);
cmd = `tmux send-keys -t ${this.sessionName} -H ${sequence}`;
break;
case "move":
cmd = `tmux send-keys -t ${this.sessionName} -H '\x1B[<35;${event.position.x + 1};${event.position.y + 1}M'`;
break;
case "scroll":
const scrollButton = event.button === "up" ? 4 : 5;
cmd = `tmux send-keys -t ${this.sessionName} -H '\x1B[<${scrollButton};${event.position.x + 1};${event.position.y + 1}M'`;
break;
default:
this.debug(`Unsupported mouse event type: ${event.type}`);
return;
}
this.debug(`Sending mouse event: ${event.type} at (${event.position.x}, ${event.position.y})`);
await this.adapter.exec(cmd);
this.recordEvent("mouse", event);
await this.sleep(100);
}
/**
* Paste text (using bracketed paste mode)
*/
async paste(text) {
this.ensureRunning();
await this.adapter.exec(`tmux send-keys -t ${this.sessionName} '\x1B[200~'`);
await this.sendText(text);
await this.adapter.exec(`tmux send-keys -t ${this.sessionName} '\x1B[201~'`);
this.recordEvent("input", { type: "paste", text });
}
/**
* Type text with optional delay between characters
*/
async typeText(text, delayMs = 50) {
for (const char of text) {
await this.sendText(char);
await this.sleep(delayMs);
}
}
/**
* Capture the current screen
*/
async captureScreen() {
this.ensureRunning();
const result = await this.adapter.exec(`tmux capture-pane -t ${this.sessionName} -p -e`);
if (result.code !== 0) {
throw new Error(`Failed to capture screen: ${result.stderr}`);
}
const raw = result.stdout;
const text = stripAnsi(raw);
const lines = splitLines(text);
const capture = {
raw,
text,
lines,
timestamp: Date.now(),
size: this.config.size
};
this._outputBuffer = raw;
if (this.recording) {
this.recording.captures.push(capture);
}
return capture;
}
/**
* Get screen text without ANSI codes
*/
async getScreenText() {
const capture = await this.captureScreen();
return capture.text;
}
/**
* Get screen lines without ANSI codes
*/
async getScreenLines() {
const capture = await this.captureScreen();
return capture.lines;
}
/**
* Get screen content (alias for getScreenText for backward compatibility)
*/
async getScreenContent() {
return this.getScreenText();
}
/**
* Get screen content with options
*/
async getScreen(options) {
if (options?.stripAnsi) {
return this.getScreenText();
}
const capture = await this.captureScreen();
return capture.raw;
}
/**
* Get screen lines (alias for getScreenLines)
*/
async getLines() {
return this.getScreenLines();
}
/**
* Wait for text to appear on screen
*/
async waitForText(text, options) {
await waitFor(
async () => {
const screenText = await this.getScreenText();
return screenText.includes(text) ? true : void 0;
},
{
timeout: options?.timeout ?? 5e3,
interval: options?.interval ?? 100,
message: options?.message ?? `Text "${text}" not found`
}
);
}
/**
* Wait for condition to be true
*/
async waitFor(predicate, options) {
await waitFor(
async () => {
const screenText = await this.getScreenText();
const result = await predicate(screenText);
return result ? true : void 0;
},
{
timeout: options?.timeout ?? 5e3,
interval: options?.interval ?? 100,
message: options?.message ?? `Condition not met`
}
);
}
/**
* Wait for pattern to match screen content
*/
async waitForPattern(pattern, options) {
await waitFor(
async () => {
const screenText = await this.getScreenText();
return pattern.test(screenText) ? true : void 0;
},
{
timeout: options?.timeout ?? 5e3,
interval: options?.interval ?? 100,
message: options?.message ?? `Pattern ${pattern} not matched`
}
);
}
/**
* Wait for specific line to contain text
*/
async waitForLine(lineNumber, text, options) {
await waitFor(
async () => {
const lines = await this.getScreenLines();
const line = lines[lineNumber];
return line && line.includes(text) ? true : void 0;
},
{
timeout: options?.timeout ?? 5e3,
interval: options?.interval ?? 100,
message: options?.message ?? `Line ${lineNumber} does not contain "${text}"`
}
);
}
/**
* Assert specific line contains text
*/
async assertLine(lineNumber, predicate) {
const lines = await this.getScreenLines();
const line = lines[lineNumber];
if (!line) {
throw new Error(`Line ${lineNumber} does not exist. Screen has ${lines.length} lines.`);
}
if (typeof predicate === "string") {
if (!line.includes(predicate)) {
throw new Error(`Line ${lineNumber} does not contain "${predicate}". Line content: "${line}"`);
}
} else {
if (!predicate(line)) {
throw new Error(`Line ${lineNumber} assertion failed. Line content: "${line}"`);
}
}
}
/**
* Assert screen matches expected content or predicate
*/
async assertScreen(expected, options) {
if (typeof expected === "function") {
const screenText = await this.getScreenText();
if (!expected(screenText)) {
throw new Error("Screen assertion failed: predicate returned false");
}
return;
}
const capture = await this.captureScreen();
const actual = options?.ignoreAnsi ? capture.text : capture.raw;
const expectedText = Array.isArray(expected) ? expected.join("\n") : expected;
if (!compareScreens(actual, expectedText, options)) {
const diff = screenDiff(actual, expectedText);
throw new Error(`Screen assertion failed:
${diff}`);
}
}
/**
* Assert screen contains text
*/
async assertScreenContains(text, options) {
const capture = await this.captureScreen();
const screenText = normalizeText(capture.text, options);
const searchText = normalizeText(text, options);
if (!screenText.includes(searchText)) {
throw new Error(`Screen does not contain "${text}"`);
}
}
/**
* Assert screen matches pattern
*/
async assertScreenMatches(pattern, options) {
const capture = await this.captureScreen();
const screenText = normalizeText(capture.text, options);
if (!pattern.test(screenText)) {
throw new Error(`Screen does not match pattern: ${pattern}`);
}
}
/**
* Assert cursor is at specific position
*/
async assertCursorAt(position) {
const cursor = await this.getCursor();
if (cursor.x !== position.x || cursor.y !== position.y) {
throw new Error(
`Cursor position mismatch. Expected (${position.x}, ${position.y}), got (${cursor.x}, ${cursor.y})`
);
}
}
/**
* Get current cursor position
*/
async getCursor() {
this.ensureRunning();
const result = await this.adapter.exec(`tmux display -t ${this.sessionName} -p '#{cursor_x},#{cursor_y}'`);
if (result.code !== 0) {
throw new Error(`Failed to get cursor position: ${result.stderr}`);
}
const [x, y] = result.stdout.trim().split(",").map(Number);
return { x, y };
}
/**
* Take a snapshot (alias for takeSnapshot for backward compatibility)
*/
async snapshot(name, options) {
const { getSnapshotManager: getSnapshotManager2 } = await Promise.resolve().then(() => (init_snapshot_manager(), snapshot_manager_exports));
const manager = getSnapshotManager2();
const capture = await this.captureScreen();
let content = options?.customContent || (options?.stripAnsi ? capture.text : capture.raw);
if (options?.trim) {
content = content.trim();
}
const existing = await manager.load(name);
if (existing && !options?.updateSnapshot) {
let matches = false;
if (options?.compare) {
matches = options.compare(existing, content);
} else {
matches = existing === content;
}
if (!matches) {
const diff = screenDiff(existing, content);
throw new Error(`Snapshot mismatch for "${name}":
${diff}`);
}
} else {
await manager.save(name, content);
}
}
/**
* Take a snapshot
*/
async takeSnapshot(name) {
const capture = await this.captureScreen();
const snapshotName = name || `snapshot-${Date.now()}`;
const snapshot = {
id: `${this.sessionName}-${snapshotName}`,
name: snapshotName,
capture,
metadata: {
sessionName: this.sessionName,
timestamp: Date.now(),
size: this.config.size
}
};
this.snapshots.set(snapshot.id, snapshot);
return snapshot;
}
/**
* Compare current screen with snapshot
*/
async compareSnapshot(snapshot) {
const targetSnapshot = typeof snapshot === "string" ? this.snapshots.get(snapshot) || await this.loadSnapshot(snapshot) : snapshot;
if (!targetSnapshot) {
throw new Error(`Snapshot not found: ${snapshot}`);
}
const currentCapture = await this.captureScreen();
return currentCapture.raw === targetSnapshot.capture.raw;
}
/**
* Save snapshot to file
*/
async saveSnapshot(snapshot, path) {
const snapshotPath = path || `${this.config.snapshotDir}/${snapshot.id}.json`;
const dir = snapshotPath.substring(0, snapshotPath.lastIndexOf("/"));
await this.adapter.mkdir(dir, { recursive: true });
await this.adapter.writeFile(snapshotPath, JSON.stringify(snapshot, null, 2));
this.debug(`Snapshot saved to: ${snapshotPath}`);
}
/**
* Load snapshot from file
*/
async loadSnapshot(path) {
const content = await this.adapter.readFile(path);
const snapshot = JSON.parse(content);
this.snapshots.set(snapshot.id, snapshot);
return snapshot;
}
/**
* Resize the terminal
*/
async resize(size) {
this.ensureRunning();
const cmd = `tmux resize-window -t ${this.sessionName} -x ${size.cols} -y ${size.rows}`;
await this.adapter.exec(cmd);
this.config.size = size;
this.recordEvent("resize", size);
this.debug(`Resized to ${size.cols}x${size.rows}`);
await this.sleep(100);
}
/**
* Clear the screen
*/
async clear() {
this.ensureRunning();
await this.sendKey("l", { ctrl: true });
}
/**
* Reset the terminal
*/
async reset() {
this.ensureRunning();
await this.adapter.exec(`tmux send-keys -t ${this.sessionName} 'reset' Enter`);
await this.sleep(500);
}
/**
* Get current terminal size
*/
getSize() {
return { ...this.config.size };
}
/**
* Get session name
*/
getSessionName() {
return this.sessionName;
}
/**
* Get the last captured output (for debugging)
*/
getLastOutput() {
return this._outputBuffer;
}
/**
* Clear the output buffer
*/
clearOutput() {
this._outputBuffer = "";
}
/**
* Execute a tmux command directly
* Useful for advanced tmux operations not covered by higher-level methods
*/
async exec(command) {
const fullCommand = command.startsWith("tmux") ? command : `tmux ${command}`;
return this.adapter.exec(fullCommand);
}
/**
* Capture screen with cursor position
* Returns both screen content and cursor position
*/
async capture() {
const screen = await this.captureScreen();
const cursor = await this.getCursor();
return {
...screen,
cursor
};
}
/**
* Start recording session
*/
startRecording() {
if (this.recording) {
this.debug("Recording already in progress");
return;
}
this.recording = {
startTime: Date.now(),
events: [],
captures: []
};
this.debug("Recording started");
}
/**
* Stop recording and return the recording
*/
stopRecording() {
if (!this.recording) {
throw new Error("No recording in progress");
}
const recording = this.recording;
this.recording = null;
this.debug(`Recording stopped. ${recording.events.length} events, ${recording.captures.length} captures`);
return recording;
}
/**
* Play back a recording
*/
async playRecording(recording, speed = 1) {
this.debug(`Playing recording with ${recording.events.length} events at ${speed}x speed`);
const startTime = Date.now();
for (const event of recording.events) {
const eventTime = (event.timestamp - recording.startTime) / speed;
const currentTime = Date.now() - startTime;
if (eventTime > currentTime) {
await this.sleep(eventTime - currentTime);
}
switch (event.type) {
case "input":
if (event.data.type === "text") {
await this.sendText(event.data.text);
} else if (event.data.type === "paste") {
await this.paste(event.data.text);
}
break;
case "key":
await this.sendKey(event.data.key, event.data.modifiers);
break;
case "mouse":
await this.sendMouse(event.data);
break;
case "resize":
await this.resize(event.data);
break;
}
}
this.debug("Recording playback complete");
}
/**
* Sleep for specified milliseconds
*/
async sleep(ms) {
return sleep(ms);
}
/**
* Debug log
*/
debug(message) {
if (this.debugMode) {
console.log(`[TmuxTester:${this.sessionName}] ${message}`);
}
}
// Private helper methods
ensureRunning() {
if (!this.running) {
throw new Error("Tester is not running. Call start() first.");
}
}
/**
* Send a command to the terminal (public method)
*/
async sendCommand(command) {
this.ensureRunning();
const escaped = escapeShellArg(command);
const cmd = `tmux send-keys -t ${this.sessionName} ${escaped} Enter`;
await this.adapter.exec(cmd);
this.recordEvent("input", { type: "command", text: command });
await this.sleep(100);
}
recordEvent(type, data) {
if (this.recording) {
this.recording.events.push({
timestamp: Date.now(),
type,
data
});
}
}
};
// src/index.ts
init_adapters();
// src/helpers/test-runner.ts
init_cjs_shims();
init_utils();
var TestRunner = class {
options;
results = [];
constructor(options = {}) {
this.options = {
beforeAll: options.beforeAll || (() => Promise.resolve()),
afterAll: options.afterAll || (() => Promise.resolve()),
beforeEach: options.beforeEach || (() => Promise.resolve()),
afterEach: options.afterEach || (() => Promise.resolve()),
timeout: options.timeout ?? 3e4,
retries: options.retries ?? 0,
parallel: options.parallel ?? false,
debug: options.debug ?? false
};
}
/**
* Run a single test scenario
*/
async runScenario(scenario2, config) {
const startTime = Date.now();
const stepResults = [];
let tester = null;
let error;
try {
tester = new TmuxTester({ ...config, debug: this.options.debug });
if (scenario2.setup) {
await scenario2.setup();
}
await tester.start();
await this.options.beforeEach(tester);
for (const step2 of scenario2.steps) {
const stepResult = await this.runStep(step2, tester);
stepResults.push(stepResult);
if (!stepResult.passed) {
error = stepResult.error;
break;
}
}
} catch (err) {
error = err;
} finally {
if (tester) {
try {
await this.options.afterEach(tester);
await tester.stop();
} catch (cleanupError) {
console.error("Cleanup error:", cleanupError);
}
}
if (scenario2.teardown) {
try {
await scenario2.teardown();
} catch (teardownError) {
console.error("Teardown error:", teardownError);
}
}
}
const result = {
scenario: scenario2.name,
passed: !error && stepResults.every((r) => r.passed),
duration: Date.now() - startTime,
steps: stepResults,
error
};
this.results.push(result);
return result;
}
/**
* Run a single test step
*/
async runStep(step2, tester) {
const startTime = Date.now();
let error;
let capture;
try {
if (step2.skipOn) {
const runtime = this.detectRuntime();
if (step2.skipOn.includes(runtime)) {
return {
name: step2.name,
passed: true,
duration: 0,
capture: await tester.captureScreen()
};
}
}
const timeout = step2.timeout ?? this.options.timeout;
await this.withTimeout(
async () => {
await step2.action(tester);
capture = await tester.captureScreen();
if (step2.assertion) {
await step2.assertion(tester);
}
},
timeout,
`Step "${step2.name}" timed out after ${timeout}ms`
);
} catch (err) {
error = err;
try {
capture = await tester.captureScreen();
} catch {
}
}
return {
name: step2.name,
passed: !error,
duration: Date.now() - startTime,
error,
capture
};
}
/**
* Run multiple scenarios
*/
async runScenarios(scenarios, config) {
await this.options.beforeAll();
try {
if (this.options.parallel) {
const promises = scenarios.map(
(scenario2) => this.runScenarioWithRetries(scenario2, config)
);
await Promise.all(promises);
} else {
for (const scenario2 of scenarios) {
await this.runScenarioWithRetries(scenario2, config);
}
}
} finally {
await this.options.afterAll();
}
return this.results;
}
/**
* Run scenario with retries
*/
async runScenarioWithRetries(scenario2, config) {
let lastResult = null;
for (let attempt = 0; attempt <= this.options.retries; attempt++) {
if (attempt > 0) {
console.log(`Retrying scenario "${scenario2.name}" (attempt ${attempt + 1}/${this.options.retries + 1})`);
await sleep(1e3);
}
lastResult = await this.runScenario(scenario2, config);
if (lastResult.passed) {
return lastResult;
}
}
return lastResult;
}
/**
* Get test results
*/
getResults() {
return [...this.results];
}
/**
* Get summary of test results
*/
getSummary() {
const total = this.results.length;
const passed = this.results.filter((r) => r.passed).length;
const failed = total - passed;
const duration = this.results.reduce((sum, r) => sum + r.duration, 0);
const failures = this.results.filter((r) => !r.passed).map((r) => ({
scenario: r.scenario,
error: r.error?.message || "Unknown error"
}));
return { total, passed, failed, duration, failures };
}
/**
* Print test results to console
*/
printResults() {
const summary = this.getSummary();
console.log("\n" + "=".repeat(60));
console.log("Test Results");
console.log("=".repeat(60));
for (const result of this.results) {
const status = result.passed ? "\u2713" : "\u2717";
const color = result.passed ? "\x1B[32m" : "\x1B[31m";
const reset = "\x1B[0m";
console.log(`${color}${status}${reset} ${result.scenario} (${result.duration}ms)`);
if (!result.passed && result.error) {
console.log(` Error: ${result.error.message}`);
}
for (const step2 of result.steps) {
const stepStatus = step2.passed ? " \u2713" : " \u2717";
const stepColor = step2.passed ? "\x1B[32m" : "\x1B[31m";
console.log(` ${stepColor}${stepStatus}${reset} ${step2.name} (${step2.duration}ms)`);
if (!step2.passed && step2.error) {
console.log(` ${step2.error.message}`);
}
}
}
console.log("\n" + "-".repeat(60));
console.log(`Total: ${summary.total} | Passed: ${summary.passed} | Failed: ${summary.failed}`);
console.log(`Duration: ${summary.duration}ms`);
if (summary.failed > 0) {
console.log("\nFailed scenarios:");
for (const failure of summary.failures) {
console.log(` - ${failure.scenario}: ${failure.error}`);
}
}
}
/**
* Reset results
*/
reset() {
this.results = [];
}
// Private helper methods
async withTimeout(fn, timeoutMs, message) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error(message)), timeoutMs);
});
return Promise.race([fn(), timeoutPromise]);
}
detectRuntime() {
if (typeof Deno !== "undefined") return "deno";
if (typeof Bun !== "undefined") return "bun";
return "node";
}
};
async function runTest(nameOrConfig, config, steps, options) {
if (typeof nameOrConfig === "object" && "scenarios" in nameOrConfig && nameOrConfig.scenarios) {
const runner2 = new TestRunner(options);
const results = [];
for (const scenario3 of nameOrConfig.scenarios) {
const result2 = await runner2.runScenario(scenario3, nameOrConfig);
results.push(result2);
}
runner2.printResults();
const passed = results.filter((r) => r.passed).length;
const failed = results.filter((r) => !r.passed).length;
return {
passed,
failed,
scenarios: results
};
}
const name = nameOrConfig;
const runner = new TestRunner(options);
const scenario2 = { name, steps: steps || [] };
const result = await runner.runScenario(scenario2, config);
runner.printResults();
return result;
}
function step(name, action, assertion) {
return { name, action, assertion };
}
function scenario(name, steps, options) {
return {
name,
steps,
setup: options?.setup,
teardown: options?.teardown
};
}
// src/helpers/interactions.ts
init_cjs_shims();
async function navigateMenu(tester, direction, count = 1) {
for (let i = 0; i < count; i++) {
await tester.sendKey(direction);
await tester.sleep(100);
}
}
async function selectMenuItem(tester, itemText, maxAttempts = 10) {
for (let i = 0; i < maxAttempts; i++) {
const screen = await tester.getScreenText();
if (screen.includes(`> ${itemText}`) || screen.includes(`\u25CF ${itemText}`)) {
await tester.sendKey("enter");
return;
}
await tester.sendKey("down");
await tester.sleep(100);
}
throw new Error(`Menu item "${itemText}" not found after ${maxAttempts} attempts`);
}
async function fillField(tester, fieldName, value) {
await tester.waitForText(fieldName);
await tester.sendKey("a", { ctrl: true });
await tester.sendKey("delete");
await tester.typeText(value);
await tester.sendKey("tab");
}
async function submitForm(tester, formData) {
if (formData) {
for (const [field, value] of Object.entries(formData)) {
await tester.waitForText(field, { timeout: 5e3 });
await tester.typeText(value);
await tester.sendKey("enter");
await tester.sleep(200);
}
} else {
await tester.sendKey("enter");
}
}
async function cancel(tester) {
await tester.sendKey("escape");
}
async function confirmDialog(tester, accept = true) {
if (accept) {
await tester.sendKey("y");
} else {
await tester.sendKey("n");
}
}
async function scroll(tester, direction, count = 1) {
const key = direction === "up" || direction === "down" ? direction : direction.toLowerCase();
for (let i = 0; i < count; i++) {
await tester.sendKey(key);
await tester.sleep(50);
}
}
async function clickOnText(tester, text) {
const screen = await tester.getScreenText();
const lines = screen.split("\n");
for (let y = 0; y < lines.length; y++) {
const x = lines[y].indexOf(text);
if (x !== -1) {
await tester.sendMouse({
type: "click",
position: { x, y },
button: "left"
});
return;
}
}
throw new Error(`Text "${text}" not found on screen`);
}
async function clickAt(tester, position, button = "left") {
await tester.sendMouse({
type: "click",
position,
button
});
}
async function drag(tester, from, to) {
await tester.sendMouse({
type: "down",
position: from,
button: "left"
});
await tester.sleep(100);
await tester.sendMouse({
type: "drag",
position: to,
button: "left"
});
await tester.sleep(100);
await tester.sendMouse({
type: "up",
position: to,
button: "left"
});
}
async function selectText(tester, startText, endText) {
const screen = await tester.getScreenText();
const lines = screen.split("\n");
let startPos = null;
let endPos = null;
for (let y = 0; y < lines.length; y++) {
const startX = lines[y].indexOf(startText);
if (startX !== -1 && !startPos) {
startPos = { x: startX, y };
}
const endX = lines[y].indexOf(endText);
if (endX !== -1) {
endPos = { x: endX + endText.length - 1, y };
}
}
if (!startPos || !endPos) {
throw new Error("Could not find text positions for selection");
}
await drag(tester, startPos, endPos);
}
async function copySelection(tester) {
await tester.sendKey("c", { ctrl: true });
}
async function pasteFromClipboard(tester) {
await tester.sendKey("v", { ctrl: true });
}
async function executeCommand(tester, command) {
await tester.sendText(command);
await tester.sendKey("enter");
}
async function waitForPrompt(tester, prompt = "$", options) {
await tester.waitForText(prompt, options);
}
async function login(tester, username, password) {
await tester.waitForText("Username:");
await tester.typeText(username);
await tester.sendKey("enter");
await tester.waitForText("Password:");
await tester.typeText(password);
await tester.sendKey("enter");
}
async function switchTab(tester, tabIndex) {
await tester.sendKey(tabIndex.toString(), { alt: true });
}
async function openCommandPalette(tester) {
await tester.sendKey("p", { ctrl: true, shift: true });
}
async function search(tester, searchText) {
const screen = await tester.getScreenText();
return screen.includes(searchText);
}
async function exitApplication(tester, force = false) {
if (force) {
await tester.sendKey("c", { ctrl: true });
} else {
await tester.sendKey("q");
}
}
async function waitForLoading(tester, options) {
const indicators = ["Loading...", "Please wait...", "\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
await tester.waitForPattern(
new RegExp(`^(?!.*(${indicators.join("|")})).*$`, "s"),
options
);
}
async function takeAnnotatedSnapshot(tester, name, annotations) {
const snapshot = await tester.takeSnapshot(name);
if (annotations && snapshot.metadata) {
snapshot.metadata.annotations = annotations;
}
await tester.saveSnapshot(snapshot);
}
// src/index.ts
init_bun();
init_node();
init_deno();
init_snapshot_manager();
function createTester(command, options) {
const commandArray = typeof command === "string" ? command.split(" ") : command;
return new TmuxTester({
command: commandArray,
size: { cols: options?.cols ?? 80, rows: options?.rows ?? 24 },
env: options?.env,
cwd: options?.cwd,
debug: options?.debug,
shell: options?.shell,
sessionName: options?.sessionName,
recordingEnabled: options?.recordingEnabled,
snapshotDir: options?.snapshotDir
});
}
exports.TestRunner = TestRunner;
exports.TmuxTester = TmuxTester;
exports.cancel = cancel;
exports.clickAt = clickAt;
exports.clickOnText = clickOnText;
exports.compareScreens = compareScreens;
exports.confirmDialog = confirmDialog;
exports.copySelection = copySelection;
exports.createAdapter = createAdapter;
exports.createTester = createTester;
exports.default = TmuxTester;
exports.detectRuntime = detectRuntime;
exports.drag = drag;
exports.escapeRegex = escapeRegex;
exports.escapeShellArg = escapeShellArg;
exports.executeCommand = executeCommand;
exports.exitApplication = exitApplication;
exports.extractCursorPosition = extractCursorPosition;
exports.extractRegion = extractRegion;
exports.fillField = fillField;
exports.findText = findText;
exports.formatTimestamp = formatTimestamp;
exports.generateSessionName = generateSessionName;
exports.getAdapter = getAdapter;
exports.getCharWidth = getCharWidth;
exports.getSnapshotManager = getSnapshotManager;
exports.getStringWidth = getStringWidth;
exports.getTerminalSize = getTerminalSize;
exports.getTextDimensions = getTextDimensions;
exports.isCommandAvailable = isCommandAvailable;
exports.joinLines = joinLines;
exports.login = login;
exports.navigateMenu = navigateMenu;
exports.normalizeLineEndings = normalizeLineEndings;
exports.normalizeText = normalizeText;
exports.openCommandPalette = openCommandPalette;
exports.parseScreen = parseScreen;
exports.parseScreenLines = parseScreenLines;
exports.parseTmuxKey = parseTmuxKey;
exports.parseTmuxMouse = parseTmuxMouse;
exports.pasteFromClipboard = pasteFromClipboard;
exports.registerAdapter = registerAdapter;
exports.resetAdapter = resetAdapter;
exports.resetSnapshotManager = resetSnapshotManager;
exports.runTest = runTest;
exports.scenario = scenario;
exports.screenDiff = screenDiff;
exports.scroll = scroll;
exports.search = search;
exports.selectMenuItem = selectMenuItem;
exports.selectText = selectText;
exports.setAdapter = setAdapter;
exports.setDefaultAdapter = setDefaultAdapter;
exports.sleep = sleep;
exports.splitLines = splitLines;
exports.step = step;
exports.stripAnsi = stripAnsi;
exports.submitForm = submitForm;
exports.switchTab = switchTab;
exports.takeAnnotatedSnapshot = takeAnnotatedSnapshot;
exports.trimScreenContent = trimScreenContent;
exports.waitFor = waitFor;
exports.waitForCondition = waitForCondition;
exports.waitForLoading = waitForLoading;
exports.waitForPrompt = waitForPrompt;
//# sourceMappingURL=index.cjs.map
//# sourceMappingURL=index.cjs.map