co-mono/packages/tui/test/tui-render.test.ts

143 lines
5 KiB
TypeScript

import assert from "node:assert";
import { describe, it } from "node:test";
import { type Component, TUI } from "../src/tui.js";
import { VirtualTerminal } from "./virtual-terminal.js";
class TestComponent implements Component {
lines: string[] = [];
render(_width: number): string[] {
return this.lines;
}
invalidate(): void {}
}
describe("TUI differential rendering", () => {
it("tracks cursor correctly when content shrinks with unchanged remaining lines", async () => {
const terminal = new VirtualTerminal(40, 10);
const tui = new TUI(terminal);
const component = new TestComponent();
tui.addChild(component);
// Initial render: 5 identical lines
component.lines = ["Line 0", "Line 1", "Line 2", "Line 3", "Line 4"];
tui.start();
await terminal.flush();
// Shrink to 3 lines, all identical to before (no content changes in remaining lines)
component.lines = ["Line 0", "Line 1", "Line 2"];
tui.requestRender();
await terminal.flush();
// cursorRow should be 2 (last line of new content)
// Verify by doing another render with a change on line 1
component.lines = ["Line 0", "CHANGED", "Line 2"];
tui.requestRender();
await terminal.flush();
const viewport = terminal.getViewport();
// Line 1 should show "CHANGED", proving cursor tracking was correct
assert.ok(viewport[1]?.includes("CHANGED"), `Expected "CHANGED" on line 1, got: ${viewport[1]}`);
tui.stop();
});
it("renders correctly when only a middle line changes (spinner case)", async () => {
const terminal = new VirtualTerminal(40, 10);
const tui = new TUI(terminal);
const component = new TestComponent();
tui.addChild(component);
// Initial render
component.lines = ["Header", "Working...", "Footer"];
tui.start();
await terminal.flush();
// Simulate spinner animation - only middle line changes
const spinnerFrames = ["|", "/", "-", "\\"];
for (const frame of spinnerFrames) {
component.lines = ["Header", `Working ${frame}`, "Footer"];
tui.requestRender();
await terminal.flush();
const viewport = terminal.getViewport();
assert.ok(viewport[0]?.includes("Header"), `Header preserved: ${viewport[0]}`);
assert.ok(viewport[1]?.includes(`Working ${frame}`), `Spinner updated: ${viewport[1]}`);
assert.ok(viewport[2]?.includes("Footer"), `Footer preserved: ${viewport[2]}`);
}
tui.stop();
});
it("renders correctly when first line changes but rest stays same", async () => {
const terminal = new VirtualTerminal(40, 10);
const tui = new TUI(terminal);
const component = new TestComponent();
tui.addChild(component);
component.lines = ["Line 0", "Line 1", "Line 2", "Line 3"];
tui.start();
await terminal.flush();
// Change only first line
component.lines = ["CHANGED", "Line 1", "Line 2", "Line 3"];
tui.requestRender();
await terminal.flush();
const viewport = terminal.getViewport();
assert.ok(viewport[0]?.includes("CHANGED"), `First line changed: ${viewport[0]}`);
assert.ok(viewport[1]?.includes("Line 1"), `Line 1 preserved: ${viewport[1]}`);
assert.ok(viewport[2]?.includes("Line 2"), `Line 2 preserved: ${viewport[2]}`);
assert.ok(viewport[3]?.includes("Line 3"), `Line 3 preserved: ${viewport[3]}`);
tui.stop();
});
it("renders correctly when last line changes but rest stays same", async () => {
const terminal = new VirtualTerminal(40, 10);
const tui = new TUI(terminal);
const component = new TestComponent();
tui.addChild(component);
component.lines = ["Line 0", "Line 1", "Line 2", "Line 3"];
tui.start();
await terminal.flush();
// Change only last line
component.lines = ["Line 0", "Line 1", "Line 2", "CHANGED"];
tui.requestRender();
await terminal.flush();
const viewport = terminal.getViewport();
assert.ok(viewport[0]?.includes("Line 0"), `Line 0 preserved: ${viewport[0]}`);
assert.ok(viewport[1]?.includes("Line 1"), `Line 1 preserved: ${viewport[1]}`);
assert.ok(viewport[2]?.includes("Line 2"), `Line 2 preserved: ${viewport[2]}`);
assert.ok(viewport[3]?.includes("CHANGED"), `Last line changed: ${viewport[3]}`);
tui.stop();
});
it("renders correctly when multiple non-adjacent lines change", async () => {
const terminal = new VirtualTerminal(40, 10);
const tui = new TUI(terminal);
const component = new TestComponent();
tui.addChild(component);
component.lines = ["Line 0", "Line 1", "Line 2", "Line 3", "Line 4"];
tui.start();
await terminal.flush();
// Change lines 1 and 3, keep 0, 2, 4 the same
component.lines = ["Line 0", "CHANGED 1", "Line 2", "CHANGED 3", "Line 4"];
tui.requestRender();
await terminal.flush();
const viewport = terminal.getViewport();
assert.ok(viewport[0]?.includes("Line 0"), `Line 0 preserved: ${viewport[0]}`);
assert.ok(viewport[1]?.includes("CHANGED 1"), `Line 1 changed: ${viewport[1]}`);
assert.ok(viewport[2]?.includes("Line 2"), `Line 2 preserved: ${viewport[2]}`);
assert.ok(viewport[3]?.includes("CHANGED 3"), `Line 3 changed: ${viewport[3]}`);
assert.ok(viewport[4]?.includes("Line 4"), `Line 4 preserved: ${viewport[4]}`);
tui.stop();
});
});