UNPKG

tui-tester

Version:

End-to-end testing framework for terminal user interfaces

2,320 lines 69.3 kB
import 'path';
import 'url';
import { promisify } from 'util';
import * as fs from 'fs/promises';
import { exec, spawn } from 'child_process';

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 });
};
var init_esm_shims = __esm({
  "node_modules/tsup/assets/esm_shims.js"() {
  }
});

// src/adapters/base.ts
var BaseRuntimeAdapter;
var init_base = __esm({
  "src/adapters/base.ts"() {
    init_esm_shims();
    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, BunAdapter;
var init_bun = __esm({
  "src/adapters/bun.ts"() {
    init_esm_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 };
      }
    };
    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(path2) {
        const file = Bun.file(path2);
        return await file.text();
      }
      async writeFile(path2, content) {
        await Bun.write(path2, content);
      }
      async exists(path2) {
        const file = Bun.file(path2);
        return await file.exists();
      }
      async mkdir(path2, options) {
        const recursive = options?.recursive ? "-p" : "";
        await this.exec(`mkdir ${recursive} "${path2}"`);
      }
      async rmdir(path2, options) {
        const recursive = options?.recursive ? "-rf" : "";
        await this.exec(`rm ${recursive} "${path2}"`);
      }
    };
  }
});
var execAsync, NodeChildProcess, NodeAdapter;
var init_node = __esm({
  "src/adapters/node.ts"() {
    init_esm_shims();
    init_base();
    execAsync = promisify(exec);
    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);
        });
      }
    };
    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, DenoAdapter;
var init_deno = __esm({
  "src/adapters/deno.ts"() {
    init_esm_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 };
      }
    };
    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(path2) {
        return await Deno.readTextFile(path2);
      }
      async writeFile(path2, content) {
        await Deno.writeTextFile(path2, content);
      }
      async exists(path2) {
        try {
          await Deno.stat(path2);
          return true;
        } catch {
          return false;
        }
      }
      async mkdir(path2, options) {
        await Deno.mkdir(path2, options);
      }
      async rmdir(path2, options) {
        await Deno.remove(path2, { recursive: options?.recursive });
      }
    };
  }
});

// src/adapters/index.ts
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();
  }
}
function getAdapter() {
  if (!currentAdapter) {
    currentAdapter = createAdapter();
  }
  return currentAdapter;
}
var adapterRegistry, defaultAdapterName, currentAdapter;
var init_adapters = __esm({
  "src/adapters/index.ts"() {
    init_esm_shims();
    init_bun();
    init_node();
    init_deno();
    init_base();
    adapterRegistry = /* @__PURE__ */ new Map();
    defaultAdapterName = null;
    currentAdapter = null;
  }
});

// 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 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 generateSessionName(prefix) {
  const p = "tui-test";
  const timestamp = Date.now();
  const random = Math.random().toString(36).substring(2, 8);
  return `${p}-${timestamp}-${random}`;
}
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 splitLines(text) {
  return text.split(/\r?\n/);
}
var init_utils = __esm({
  "src/core/utils.ts"() {
    init_esm_shims();
  }
});

// src/snapshot/snapshot-manager.ts
var snapshot_manager_exports = {};
__export(snapshot_manager_exports, {
  SnapshotManager: () => SnapshotManager,
  getSnapshotManager: () => getSnapshotManager,
  resetSnapshotManager: () => resetSnapshotManager
});
function getSnapshotManager(options) {
  if (!globalSnapshotManager) {
    globalSnapshotManager = new SnapshotManager(options);
  } else if (options) {
    globalSnapshotManager.configure(options);
  }
  return globalSnapshotManager;
}
function resetSnapshotManager() {
  globalSnapshotManager = null;
}
var SnapshotManager, globalSnapshotManager;
var init_snapshot_manager = __esm({
  "src/snapshot/snapshot-manager.ts"() {
    init_esm_shims();
    init_adapters();
    init_utils();
    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, path2) {
        const snapshotPath = path2 || 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(path2) {
        const content = await this.adapter.readFile(path2);
        let snapshot;
        switch (this.options.format) {
          case "text":
          case "ansi":
            const lines = content.split("\n");
            snapshot = {
              id: path2,
              name: path2.substring(path2.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((path2) => path2.substring(path2.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((path2) => {
            const filename = path2.substring(path2.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/tmux-tester.ts
init_esm_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, path2) {
    const snapshotPath = path2 || `${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(path2) {
    const content = await this.adapter.readFile(path2);
    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
      });
    }
  }
};

export { TmuxTester };
//# sourceMappingURL=tmux-tester.js.map
//# sourceMappingURL=tmux-tester.js.map