import xterm from '@xterm/headless'; import type { Terminal as XtermTerminalType } from '@xterm/headless'; import { Terminal } from '../src/terminal.js'; // Extract Terminal class from the module const XtermTerminal = xterm.Terminal; /** * Virtual terminal for testing using xterm.js for accurate terminal emulation */ export class VirtualTerminal implements Terminal { private xterm: XtermTerminalType; private inputHandler?: (data: string) => void; private resizeHandler?: () => void; private _columns: number; private _rows: number; constructor(columns = 80, rows = 24) { this._columns = columns; this._rows = rows; // Create xterm instance with specified dimensions this.xterm = new XtermTerminal({ cols: columns, rows: rows, // Disable all interactive features for testing disableStdin: true, allowProposedApi: true, }); } start(onInput: (data: string) => void, onResize: () => void): void { this.inputHandler = onInput; this.resizeHandler = onResize; // No need for raw mode in virtual terminal } stop(): void { this.inputHandler = undefined; this.resizeHandler = undefined; } write(data: string): void { this.xterm.write(data); } get columns(): number { return this._columns; } get rows(): number { return this._rows; } // Test-specific methods not in Terminal interface /** * Simulate keyboard input */ sendInput(data: string): void { if (this.inputHandler) { this.inputHandler(data); } } /** * Resize the terminal */ resize(columns: number, rows: number): void { this._columns = columns; this._rows = rows; this.xterm.resize(columns, rows); if (this.resizeHandler) { this.resizeHandler(); } } /** * Wait for all pending writes to complete. Viewport and scroll buffer will be updated. */ async flush(): Promise { // Write an empty string to ensure all previous writes are flushed return new Promise((resolve) => { this.xterm.write('', () => resolve()); }); } /** * Flush and get viewport - convenience method for tests */ async flushAndGetViewport(): Promise { await this.flush(); return this.getViewport(); } /** * Get the visible viewport (what's currently on screen) * Note: You should use getViewportAfterWrite() for testing after writing data */ getViewport(): string[] { const lines: string[] = []; const buffer = this.xterm.buffer.active; // Get only the visible lines (viewport) for (let i = 0; i < this.xterm.rows; i++) { const line = buffer.getLine(buffer.viewportY + i); if (line) { lines.push(line.translateToString(true)); } else { lines.push(''); } } return lines; } /** * Get the entire scroll buffer */ getScrollBuffer(): string[] { const lines: string[] = []; const buffer = this.xterm.buffer.active; // Get all lines in the buffer (including scrollback) for (let i = 0; i < buffer.length; i++) { const line = buffer.getLine(i); if (line) { lines.push(line.translateToString(true)); } else { lines.push(''); } } return lines; } /** * Clear the terminal viewport */ clear(): void { this.xterm.clear(); } /** * Reset the terminal completely */ reset(): void { this.xterm.reset(); } /** * Get cursor position */ getCursorPosition(): { x: number; y: number } { const buffer = this.xterm.buffer.active; return { x: buffer.cursorX, y: buffer.cursorY }; } }