co-mono/packages/tui/test/virtual-terminal.test.ts
Mario Zechner afa807b200 tui-double-buffer: Implement smart differential rendering with terminal abstraction
- 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.
2025-08-10 22:33:03 +02:00

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();
});
});