Files
codetyper.cli/tests/ui-components.test.ts
Carlos Gutierrez 187cc68304 Improve agent autonomy and diff view readability
Agent behavior improvements:
  - Add project context detection (tsconfig.json, pom.xml, etc.)
  - Enforce validation after changes (tsc --noEmit, mvn compile, etc.)
  - Run tests automatically - never ask "do you want me to run tests"
  - Complete full loop: create → type-check → test → confirm
  - Add command detection for direct execution (run tree, run ls)

  Diff view improvements:
  - Use darker backgrounds for added/removed lines
  - Add diffLineBgAdded, diffLineBgRemoved, diffLineText theme colors
  - Improve text visibility with white text on dark backgrounds
  - Update both React/Ink and SolidJS diff components

  Streaming fixes:
  - Fix tool call argument accumulation using OpenAI index field
  - Fix streaming content display after tool calls
  - Add consecutive error tracking to prevent token waste

  Other changes:
  - ESC to abort operations, Ctrl+C to exit
  - Fix model selection when provider changes in cascade mode
  - Add debug logging for troubleshooting
  - Move tests to root tests/ folder
  - Fix banner test GRADIENT_COLORS reference
2026-01-29 07:33:30 -05:00

437 lines
14 KiB
TypeScript

/**
* UI Components Tests
*
* Tests for terminal UI component utility functions
*/
import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test";
import { Style, Theme, Icons } from "@constants/styles";
import { BoxChars, BOX_DEFAULTS } from "@constants/components";
// Mock getTerminalWidth to return consistent value for tests
const mockTerminalWidth = 80;
const originalStdoutColumns = process.stdout.columns;
beforeEach(() => {
Object.defineProperty(process.stdout, "columns", {
value: mockTerminalWidth,
writable: true,
configurable: true,
});
});
afterEach(() => {
Object.defineProperty(process.stdout, "columns", {
value: originalStdoutColumns,
writable: true,
configurable: true,
});
});
describe("UI Components", () => {
describe("box", () => {
it("should create a box with default options", async () => {
const { box } = await import("@ui/components/box");
const result = box("Hello");
expect(result).toContain(BoxChars.rounded.topLeft);
expect(result).toContain(BoxChars.rounded.topRight);
expect(result).toContain(BoxChars.rounded.bottomLeft);
expect(result).toContain(BoxChars.rounded.bottomRight);
expect(result).toContain("Hello");
});
it("should create a box with title", async () => {
const { box } = await import("@ui/components/box");
const result = box("Content", { title: "Title" });
expect(result).toContain("Title");
expect(result).toContain("Content");
});
it("should handle array content", async () => {
const { box } = await import("@ui/components/box");
const result = box(["Line 1", "Line 2"]);
expect(result).toContain("Line 1");
expect(result).toContain("Line 2");
});
it("should apply different box styles", async () => {
const { box } = await import("@ui/components/box");
const singleBox = box("Test", { style: "single" });
expect(singleBox).toContain(BoxChars.single.topLeft);
const doubleBox = box("Test", { style: "double" });
expect(doubleBox).toContain(BoxChars.double.topLeft);
const boldBox = box("Test", { style: "bold" });
expect(boldBox).toContain(BoxChars.bold.topLeft);
});
it("should align content correctly", async () => {
const { box } = await import("@ui/components/box");
const leftAligned = box("Hi", { align: "left", width: 20, padding: 0 });
const rightAligned = box("Hi", { align: "right", width: 20, padding: 0 });
const centerAligned = box("Hi", {
align: "center",
width: 20,
padding: 0,
});
// Left alignment: content at start
const leftLines = leftAligned.split("\n");
const leftContentLine = leftLines.find((l) => l.includes("Hi"));
expect(leftContentLine).toBeDefined();
// Right alignment: content at end
const rightLines = rightAligned.split("\n");
const rightContentLine = rightLines.find((l) => l.includes("Hi"));
expect(rightContentLine).toBeDefined();
// Center alignment: content centered
const centerLines = centerAligned.split("\n");
const centerContentLine = centerLines.find((l) => l.includes("Hi"));
expect(centerContentLine).toBeDefined();
});
it("should respect custom width", async () => {
const { box } = await import("@ui/components/box");
const result = box("Test", { width: 30, padding: 0 });
const lines = result.split("\n");
// Top border should be 30 chars (including box chars and ANSI codes)
const topLine = lines[0];
expect(topLine).toContain(BoxChars.rounded.topLeft);
expect(topLine).toContain(BoxChars.rounded.topRight);
});
it("should add padding", async () => {
const { box } = await import("@ui/components/box");
const noPadding = box("Test", { padding: 0, width: 20 });
const withPadding = box("Test", { padding: 2, width: 20 });
const noPaddingLines = noPadding.split("\n");
const withPaddingLines = withPadding.split("\n");
// With padding should have more lines
expect(withPaddingLines.length).toBeGreaterThan(noPaddingLines.length);
});
});
describe("panel", () => {
it("should create a panel with left border", async () => {
const { panel } = await import("@ui/components/box");
const result = panel("Hello");
expect(result).toContain("│");
expect(result).toContain("Hello");
});
it("should handle multiline content", async () => {
const { panel } = await import("@ui/components/box");
const result = panel(["Line 1", "Line 2"]);
const lines = result.split("\n");
expect(lines.length).toBe(2);
expect(lines[0]).toContain("Line 1");
expect(lines[1]).toContain("Line 2");
});
it("should apply custom color", async () => {
const { panel } = await import("@ui/components/box");
const result = panel("Test", Theme.primary);
expect(result).toContain(Theme.primary);
});
});
describe("errorBox", () => {
it("should create an error styled box", async () => {
const { errorBox } = await import("@ui/components/box");
const result = errorBox("Error Title", "Error message");
expect(result).toContain("Error Title");
expect(result).toContain("Error message");
expect(result).toContain(Theme.error);
});
});
describe("successBox", () => {
it("should create a success styled box", async () => {
const { successBox } = await import("@ui/components/box");
const result = successBox("Success Title", "Success message");
expect(result).toContain("Success Title");
expect(result).toContain("Success message");
expect(result).toContain(Theme.success);
});
});
describe("header", () => {
it("should create a line-style header by default", async () => {
const { header } = await import("@ui/components/header");
const result = header("Section");
expect(result).toContain("Section");
expect(result).toContain("─");
});
it("should create a simple-style header", async () => {
const { header } = await import("@ui/components/header");
const result = header("Section", "simple");
expect(result).toContain("Section");
expect(result).toContain(Style.BOLD);
});
it("should create a box-style header", async () => {
const { header } = await import("@ui/components/header");
const result = header("Section", "box");
expect(result).toContain("Section");
expect(result).toContain(BoxChars.rounded.topLeft);
});
});
describe("divider", () => {
it("should create a divider line", async () => {
const { divider } = await import("@ui/components/header");
const result = divider();
expect(result).toContain("─");
expect(result).toContain(Theme.textMuted);
expect(result).toContain(Style.RESET);
});
it("should use custom character", async () => {
const { divider } = await import("@ui/components/header");
const result = divider("=");
expect(result).toContain("=");
});
it("should apply custom color", async () => {
const { divider } = await import("@ui/components/header");
const result = divider("─", Theme.primary);
expect(result).toContain(Theme.primary);
});
});
describe("keyValue", () => {
it("should create key-value pairs", async () => {
const { keyValue } = await import("@ui/components/list");
const result = keyValue({ Name: "John", Age: 30 });
expect(result).toContain("Name");
expect(result).toContain("John");
expect(result).toContain("Age");
expect(result).toContain("30");
});
it("should handle boolean values", async () => {
const { keyValue } = await import("@ui/components/list");
const result = keyValue({ Active: true, Disabled: false });
expect(result).toContain("Yes");
expect(result).toContain("No");
});
it("should skip undefined values", async () => {
const { keyValue } = await import("@ui/components/list");
const result = keyValue({ Present: "value", Missing: undefined });
expect(result).toContain("Present");
expect(result).not.toContain("Missing");
});
it("should use custom separator", async () => {
const { keyValue } = await import("@ui/components/list");
const result = keyValue({ Key: "Value" }, { separator: " = " });
expect(result).toContain(" = ");
});
it("should apply label and value colors", async () => {
const { keyValue } = await import("@ui/components/list");
const result = keyValue(
{ Key: "Value" },
{ labelColor: Theme.primary, valueColor: Theme.success },
);
expect(result).toContain(Theme.primary);
expect(result).toContain(Theme.success);
});
});
describe("list", () => {
it("should create a bulleted list", async () => {
const { list } = await import("@ui/components/list");
const result = list(["Item 1", "Item 2", "Item 3"]);
expect(result).toContain("Item 1");
expect(result).toContain("Item 2");
expect(result).toContain("Item 3");
expect(result).toContain(Icons.bullet);
});
it("should use custom bullet", async () => {
const { list } = await import("@ui/components/list");
const result = list(["Item"], { bullet: "-" });
expect(result).toContain("-");
expect(result).toContain("Item");
});
it("should apply custom indent", async () => {
const { list } = await import("@ui/components/list");
const noIndent = list(["Item"], { indent: 0 });
const withIndent = list(["Item"], { indent: 4 });
expect(withIndent.length).toBeGreaterThan(noIndent.length);
});
it("should apply custom color", async () => {
const { list } = await import("@ui/components/list");
const result = list(["Item"], { color: Theme.success });
expect(result).toContain(Theme.success);
});
});
describe("status", () => {
it("should create status indicators for all states", async () => {
const { status } = await import("@ui/components/status");
const success = status("success", "Operation complete");
expect(success).toContain(Icons.success);
expect(success).toContain("Operation complete");
expect(success).toContain(Theme.success);
const error = status("error", "Failed");
expect(error).toContain(Icons.error);
expect(error).toContain(Theme.error);
const warning = status("warning", "Caution");
expect(warning).toContain(Icons.warning);
expect(warning).toContain(Theme.warning);
const info = status("info", "Note");
expect(info).toContain(Icons.info);
expect(info).toContain(Theme.info);
const pending = status("pending", "Waiting");
expect(pending).toContain(Icons.pending);
const running = status("running", "Processing");
expect(running).toContain(Icons.running);
expect(running).toContain(Theme.primary);
});
});
describe("toolCall", () => {
it("should create tool call display with default state", async () => {
const { toolCall } = await import("@ui/components/status");
const result = toolCall("bash", "Running command");
expect(result).toContain("Running command");
expect(result).toContain(Style.DIM);
});
it("should show different states", async () => {
const { toolCall } = await import("@ui/components/status");
const pending = toolCall("read", "Reading file", "pending");
expect(pending).toContain(Style.DIM);
const running = toolCall("read", "Reading file", "running");
expect(running).toContain(Theme.primary);
const success = toolCall("read", "Reading file", "success");
expect(success).toContain(Theme.success);
const error = toolCall("read", "Reading file", "error");
expect(error).toContain(Theme.error);
});
it("should use default icon for unknown tools", async () => {
const { toolCall } = await import("@ui/components/status");
const result = toolCall("unknown_tool", "Description");
expect(result).toContain("Description");
});
});
describe("message", () => {
it("should create messages for different roles", async () => {
const { message } = await import("@ui/components/message");
const userMsg = message("user", "Hello");
expect(userMsg).toContain("You");
expect(userMsg).toContain("Hello");
const assistantMsg = message("assistant", "Hi there");
expect(assistantMsg).toContain("CodeTyper");
expect(assistantMsg).toContain("Hi there");
const systemMsg = message("system", "System info");
expect(systemMsg).toContain("System");
expect(systemMsg).toContain("System info");
const toolMsg = message("tool", "Tool output");
expect(toolMsg).toContain("Tool");
expect(toolMsg).toContain("Tool output");
});
it("should hide role label when showRole is false", async () => {
const { message } = await import("@ui/components/message");
const result = message("user", "Hello", { showRole: false });
expect(result).not.toContain("You");
expect(result).toContain("Hello");
});
});
describe("codeBlock", () => {
it("should create a code block", async () => {
const { codeBlock } = await import("@ui/components/message");
const result = codeBlock("const x = 1;");
expect(result).toContain("```");
expect(result).toContain("const x = 1;");
expect(result).toContain("1 │");
});
it("should show language when provided", async () => {
const { codeBlock } = await import("@ui/components/message");
const result = codeBlock("const x = 1;", "typescript");
expect(result).toContain("```typescript");
});
it("should number multiple lines", async () => {
const { codeBlock } = await import("@ui/components/message");
const result = codeBlock("line1\nline2\nline3");
expect(result).toContain("1 │");
expect(result).toContain("2 │");
expect(result).toContain("3 │");
});
it("should pad line numbers for alignment", async () => {
const { codeBlock } = await import("@ui/components/message");
const code = Array.from({ length: 15 }, (_, i) => `line${i + 1}`).join(
"\n",
);
const result = codeBlock(code);
// Line numbers should be padded (e.g., " 1 │" for single digit when max is 15)
expect(result).toContain(" 1 │");
expect(result).toContain("15 │");
});
});
});