mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 23:01:30 +00:00
- Create Terminal interface abstracting stdin/stdout operations for dependency injection - Implement ProcessTerminal for production use with process.stdin/stdout - Implement VirtualTerminal using @xterm/headless for accurate terminal emulation in tests - Fix TypeScript imports for @xterm/headless module - Move all component files to src/components/ directory for better organization - Add comprehensive test suite with async/await patterns for proper render timing - Fix critical TUI differential rendering bug when components grow in height - Issue: Old content wasn't properly cleared when component line count increased - Solution: Clear each old line individually before redrawing, ensure cursor at line start - Add test verifying terminal content preservation and text editor growth behavior - Update tsconfig.json to include test files in type checking - Add benchmark test comparing single vs double buffer performance The implementation successfully reduces flicker by only updating changed lines rather than clearing entire sections. Both TUI implementations maintain the same interface for backward compatibility.
158 lines
No EOL
4.5 KiB
TypeScript
158 lines
No EOL
4.5 KiB
TypeScript
import { test, describe } from "node:test";
|
|
import assert from "node:assert";
|
|
import { VirtualTerminal } from "./virtual-terminal.js";
|
|
|
|
describe("VirtualTerminal", () => {
|
|
test("writes and reads simple text", async () => {
|
|
const terminal = new VirtualTerminal(80, 24);
|
|
|
|
terminal.write("Hello, World!");
|
|
|
|
// Wait for write to process
|
|
const output = await terminal.flushAndGetViewport();
|
|
|
|
assert.strictEqual(output[0], "Hello, World!");
|
|
assert.strictEqual(output[1], "");
|
|
});
|
|
|
|
test("handles newlines correctly", async () => {
|
|
const terminal = new VirtualTerminal(80, 24);
|
|
|
|
terminal.write("Line 1\r\nLine 2\r\nLine 3");
|
|
|
|
const output = await terminal.flushAndGetViewport();
|
|
|
|
assert.strictEqual(output[0], "Line 1");
|
|
assert.strictEqual(output[1], "Line 2");
|
|
assert.strictEqual(output[2], "Line 3");
|
|
});
|
|
|
|
test("handles ANSI cursor movement", async () => {
|
|
const terminal = new VirtualTerminal(80, 24);
|
|
|
|
// Write text with proper newlines, move cursor up, overwrite
|
|
terminal.write("First line\r\nSecond line");
|
|
terminal.write("\x1b[1A"); // Move up 1 line
|
|
terminal.write("\rOverwritten");
|
|
|
|
const output = await terminal.flushAndGetViewport();
|
|
|
|
assert.strictEqual(output[0], "Overwritten");
|
|
assert.strictEqual(output[1], "Second line");
|
|
});
|
|
|
|
test("handles clear line escape sequence", async () => {
|
|
const terminal = new VirtualTerminal(80, 24);
|
|
|
|
terminal.write("This will be cleared");
|
|
terminal.write("\r\x1b[2K"); // Clear line
|
|
terminal.write("New text");
|
|
|
|
const output = await terminal.flushAndGetViewport();
|
|
|
|
assert.strictEqual(output[0], "New text");
|
|
});
|
|
|
|
test("tracks cursor position", async () => {
|
|
const terminal = new VirtualTerminal(80, 24);
|
|
|
|
terminal.write("Hello");
|
|
await terminal.flush();
|
|
|
|
const cursor = terminal.getCursorPosition();
|
|
assert.strictEqual(cursor.x, 5); // After "Hello"
|
|
assert.strictEqual(cursor.y, 0); // First line
|
|
|
|
terminal.write("\r\nWorld"); // Use CR+LF for proper newline
|
|
await terminal.flush();
|
|
|
|
const cursor2 = terminal.getCursorPosition();
|
|
assert.strictEqual(cursor2.x, 5); // After "World"
|
|
assert.strictEqual(cursor2.y, 1); // Second line
|
|
});
|
|
|
|
test("handles viewport overflow with scrolling", async () => {
|
|
const terminal = new VirtualTerminal(80, 10); // Small viewport
|
|
|
|
// Write more lines than viewport can hold
|
|
for (let i = 1; i <= 15; i++) {
|
|
terminal.write(`Line ${i}\r\n`);
|
|
}
|
|
|
|
const viewport = await terminal.flushAndGetViewport();
|
|
const scrollBuffer = terminal.getScrollBuffer();
|
|
|
|
// Viewport should show lines 7-15 plus empty line (because viewport starts after scrolling)
|
|
assert.strictEqual(viewport.length, 10);
|
|
assert.strictEqual(viewport[0], "Line 7");
|
|
assert.strictEqual(viewport[8], "Line 15");
|
|
assert.strictEqual(viewport[9], ""); // Last line is empty after the final \r\n
|
|
|
|
// Scroll buffer should have all lines
|
|
assert.ok(scrollBuffer.length >= 15);
|
|
// Check specific lines exist in the buffer
|
|
const hasLine1 = scrollBuffer.some(line => line === "Line 1");
|
|
const hasLine15 = scrollBuffer.some(line => line === "Line 15");
|
|
assert.ok(hasLine1, "Buffer should contain 'Line 1'");
|
|
assert.ok(hasLine15, "Buffer should contain 'Line 15'");
|
|
});
|
|
|
|
test("resize updates dimensions", async () => {
|
|
const terminal = new VirtualTerminal(80, 24);
|
|
|
|
assert.strictEqual(terminal.columns, 80);
|
|
assert.strictEqual(terminal.rows, 24);
|
|
|
|
terminal.resize(100, 30);
|
|
|
|
assert.strictEqual(terminal.columns, 100);
|
|
assert.strictEqual(terminal.rows, 30);
|
|
});
|
|
|
|
test("reset clears terminal completely", async () => {
|
|
const terminal = new VirtualTerminal(80, 24);
|
|
|
|
terminal.write("Some text\r\nMore text");
|
|
|
|
let output = await terminal.flushAndGetViewport();
|
|
assert.strictEqual(output[0], "Some text");
|
|
assert.strictEqual(output[1], "More text");
|
|
|
|
terminal.reset();
|
|
|
|
output = await terminal.flushAndGetViewport();
|
|
assert.strictEqual(output[0], "");
|
|
assert.strictEqual(output[1], "");
|
|
});
|
|
|
|
test("sendInput triggers handler", async () => {
|
|
const terminal = new VirtualTerminal(80, 24);
|
|
|
|
let received = "";
|
|
terminal.start((data) => {
|
|
received = data;
|
|
}, () => {});
|
|
|
|
terminal.sendInput("a");
|
|
assert.strictEqual(received, "a");
|
|
|
|
terminal.sendInput("\x1b[A"); // Up arrow
|
|
assert.strictEqual(received, "\x1b[A");
|
|
|
|
terminal.stop();
|
|
});
|
|
|
|
test("resize triggers handler", async () => {
|
|
const terminal = new VirtualTerminal(80, 24);
|
|
|
|
let resized = false;
|
|
terminal.start(() => {}, () => {
|
|
resized = true;
|
|
});
|
|
|
|
terminal.resize(100, 30);
|
|
assert.strictEqual(resized, true);
|
|
|
|
terminal.stop();
|
|
});
|
|
}); |