mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 20:03:05 +00:00
- Switch to function based API - Anthropic SDK style async generator - Fully typed with escape hatches for custom models
203 lines
7 KiB
TypeScript
203 lines
7 KiB
TypeScript
import assert from "node:assert";
|
|
import { describe, test } from "node:test";
|
|
import { Container, TextComponent, TextEditor, TUI } from "../src/index.js";
|
|
import { VirtualTerminal } from "./virtual-terminal.js";
|
|
|
|
describe("Differential Rendering - Dynamic Content", () => {
|
|
test("handles static text, dynamic container, and text editor correctly", async () => {
|
|
const terminal = new VirtualTerminal(80, 10); // Small viewport to test scrolling
|
|
const ui = new TUI(terminal);
|
|
ui.start();
|
|
|
|
// Step 1: Add a static text component
|
|
const staticText = new TextComponent("Static Header Text");
|
|
ui.addChild(staticText);
|
|
|
|
// Step 2: Add an initially empty container
|
|
const container = new Container();
|
|
ui.addChild(container);
|
|
|
|
// Step 3: Add a text editor field
|
|
const editor = new TextEditor();
|
|
ui.addChild(editor);
|
|
ui.setFocus(editor);
|
|
|
|
// Wait for next tick to complete and flush virtual terminal
|
|
await new Promise((resolve) => process.nextTick(resolve));
|
|
await terminal.flush();
|
|
|
|
// Step 4: Check initial output in scrollbuffer
|
|
let scrollBuffer = terminal.getScrollBuffer();
|
|
let viewport = terminal.getViewport();
|
|
|
|
console.log("Initial render:");
|
|
console.log("Viewport lines:", viewport.length);
|
|
console.log("ScrollBuffer lines:", scrollBuffer.length);
|
|
|
|
// Count non-empty lines in scrollbuffer
|
|
const nonEmptyInBuffer = scrollBuffer.filter((line) => line.trim() !== "").length;
|
|
console.log("Non-empty lines in scrollbuffer:", nonEmptyInBuffer);
|
|
|
|
// Verify initial render has static text in scrollbuffer
|
|
assert.ok(
|
|
scrollBuffer.some((line) => line.includes("Static Header Text")),
|
|
`Expected static text in scrollbuffer`,
|
|
);
|
|
|
|
// Step 5: Add 100 text components to container
|
|
console.log("\nAdding 100 components to container...");
|
|
for (let i = 1; i <= 100; i++) {
|
|
container.addChild(new TextComponent(`Dynamic Item ${i}`));
|
|
}
|
|
|
|
// Request render after adding all components
|
|
ui.requestRender();
|
|
|
|
// Wait for next tick to complete and flush
|
|
await new Promise((resolve) => process.nextTick(resolve));
|
|
await terminal.flush();
|
|
|
|
// Step 6: Check output after adding 100 components
|
|
scrollBuffer = terminal.getScrollBuffer();
|
|
viewport = terminal.getViewport();
|
|
|
|
console.log("\nAfter adding 100 items:");
|
|
console.log("Viewport lines:", viewport.length);
|
|
console.log("ScrollBuffer lines:", scrollBuffer.length);
|
|
|
|
// Count all dynamic items in scrollbuffer
|
|
let dynamicItemsInBuffer = 0;
|
|
const allItemNumbers = new Set<number>();
|
|
for (const line of scrollBuffer) {
|
|
const match = line.match(/Dynamic Item (\d+)/);
|
|
if (match) {
|
|
dynamicItemsInBuffer++;
|
|
allItemNumbers.add(parseInt(match[1]));
|
|
}
|
|
}
|
|
|
|
console.log("Dynamic items found in scrollbuffer:", dynamicItemsInBuffer);
|
|
console.log("Unique item numbers:", allItemNumbers.size);
|
|
console.log("Item range:", Math.min(...allItemNumbers), "-", Math.max(...allItemNumbers));
|
|
|
|
// CRITICAL TEST: The scrollbuffer should contain ALL 100 items
|
|
// This is what the differential render should preserve!
|
|
assert.strictEqual(
|
|
allItemNumbers.size,
|
|
100,
|
|
`Expected all 100 unique items in scrollbuffer, but found ${allItemNumbers.size}`,
|
|
);
|
|
|
|
// Verify items are 1-100
|
|
for (let i = 1; i <= 100; i++) {
|
|
assert.ok(allItemNumbers.has(i), `Missing Dynamic Item ${i} in scrollbuffer`);
|
|
}
|
|
|
|
// Also verify the static header is still in scrollbuffer
|
|
assert.ok(
|
|
scrollBuffer.some((line) => line.includes("Static Header Text")),
|
|
"Static header should still be in scrollbuffer",
|
|
);
|
|
|
|
// And the editor should be there too
|
|
assert.ok(
|
|
scrollBuffer.some((line) => line.includes("╭") && line.includes("╮")),
|
|
"Editor top border should be in scrollbuffer",
|
|
);
|
|
assert.ok(
|
|
scrollBuffer.some((line) => line.includes("╰") && line.includes("╯")),
|
|
"Editor bottom border should be in scrollbuffer",
|
|
);
|
|
|
|
ui.stop();
|
|
});
|
|
|
|
test("differential render correctly updates only changed components", async () => {
|
|
const terminal = new VirtualTerminal(80, 24);
|
|
const ui = new TUI(terminal);
|
|
ui.start();
|
|
|
|
// Create multiple containers with different content
|
|
const header = new TextComponent("=== Application Header ===");
|
|
const statusContainer = new Container();
|
|
const contentContainer = new Container();
|
|
const footer = new TextComponent("=== Footer ===");
|
|
|
|
ui.addChild(header);
|
|
ui.addChild(statusContainer);
|
|
ui.addChild(contentContainer);
|
|
ui.addChild(footer);
|
|
|
|
// Add initial content
|
|
statusContainer.addChild(new TextComponent("Status: Ready"));
|
|
contentContainer.addChild(new TextComponent("Content Line 1"));
|
|
contentContainer.addChild(new TextComponent("Content Line 2"));
|
|
|
|
// Initial render
|
|
await new Promise((resolve) => process.nextTick(resolve));
|
|
await terminal.flush();
|
|
|
|
let viewport = terminal.getViewport();
|
|
assert.strictEqual(viewport[0], "=== Application Header ===");
|
|
assert.strictEqual(viewport[1], "Status: Ready");
|
|
assert.strictEqual(viewport[2], "Content Line 1");
|
|
assert.strictEqual(viewport[3], "Content Line 2");
|
|
assert.strictEqual(viewport[4], "=== Footer ===");
|
|
|
|
// Track lines redrawn
|
|
const initialLinesRedrawn = ui.getLinesRedrawn();
|
|
|
|
// Update only the status
|
|
statusContainer.clear();
|
|
statusContainer.addChild(new TextComponent("Status: Processing..."));
|
|
ui.requestRender();
|
|
|
|
await new Promise((resolve) => process.nextTick(resolve));
|
|
await terminal.flush();
|
|
|
|
viewport = terminal.getViewport();
|
|
assert.strictEqual(viewport[0], "=== Application Header ===");
|
|
assert.strictEqual(viewport[1], "Status: Processing...");
|
|
assert.strictEqual(viewport[2], "Content Line 1");
|
|
assert.strictEqual(viewport[3], "Content Line 2");
|
|
assert.strictEqual(viewport[4], "=== Footer ===");
|
|
|
|
const afterStatusUpdate = ui.getLinesRedrawn();
|
|
const statusUpdateLines = afterStatusUpdate - initialLinesRedrawn;
|
|
console.log(`Lines redrawn for status update: ${statusUpdateLines}`);
|
|
|
|
// Add many items to content container
|
|
for (let i = 3; i <= 20; i++) {
|
|
contentContainer.addChild(new TextComponent(`Content Line ${i}`));
|
|
}
|
|
ui.requestRender();
|
|
|
|
await new Promise((resolve) => process.nextTick(resolve));
|
|
await terminal.flush();
|
|
|
|
viewport = terminal.getViewport();
|
|
|
|
// With 24 rows - 1 for cursor = 23 visible
|
|
// We have: 1 header + 1 status + 20 content + 1 footer = 23 lines
|
|
// Should fit exactly
|
|
assert.strictEqual(viewport[0], "=== Application Header ===");
|
|
assert.strictEqual(viewport[1], "Status: Processing...");
|
|
assert.strictEqual(viewport[21], "Content Line 20");
|
|
assert.strictEqual(viewport[22], "=== Footer ===");
|
|
|
|
// Now update just one content line
|
|
const contentLine10 = contentContainer.getChild(9) as TextComponent;
|
|
contentLine10.setText("Content Line 10 - MODIFIED");
|
|
ui.requestRender();
|
|
|
|
await new Promise((resolve) => process.nextTick(resolve));
|
|
await terminal.flush();
|
|
|
|
viewport = terminal.getViewport();
|
|
assert.strictEqual(viewport[11], "Content Line 10 - MODIFIED");
|
|
assert.strictEqual(viewport[0], "=== Application Header ==="); // Should be unchanged
|
|
assert.strictEqual(viewport[22], "=== Footer ==="); // Should be unchanged
|
|
|
|
ui.stop();
|
|
});
|
|
});
|