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
This commit is contained in:
@@ -44,4 +44,4 @@ describe("Auto-Scroll Constants", () => {
|
||||
expect(MOUSE_SCROLL_LINES).toBeGreaterThan(0);
|
||||
expect(MOUSE_SCROLL_LINES).toBeLessThan(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -83,4 +83,4 @@ describe("Input Utils", () => {
|
||||
expect(cleanInput("")).toBe("");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,314 +0,0 @@
|
||||
/**
|
||||
* Unit tests for paste utility functions
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
countLines,
|
||||
shouldSummarizePaste,
|
||||
generatePlaceholder,
|
||||
createPastedContent,
|
||||
generatePasteId,
|
||||
addPastedBlock,
|
||||
updatePastedBlockPositions,
|
||||
updatePastedBlocksAfterDelete,
|
||||
expandPastedContent,
|
||||
normalizeLineEndings,
|
||||
clearPastedBlocks,
|
||||
} from "@utils/tui-app/paste-utils";
|
||||
import type { PastedContent, PasteState } from "@interfaces/PastedContent";
|
||||
import { createInitialPasteState } from "@interfaces/PastedContent";
|
||||
|
||||
describe("countLines", () => {
|
||||
it("should count single line correctly", () => {
|
||||
expect(countLines("hello")).toBe(1);
|
||||
});
|
||||
|
||||
it("should count multiple lines correctly", () => {
|
||||
expect(countLines("line1\nline2\nline3")).toBe(3);
|
||||
});
|
||||
|
||||
it("should handle empty string", () => {
|
||||
expect(countLines("")).toBe(1);
|
||||
});
|
||||
|
||||
it("should handle trailing newline", () => {
|
||||
expect(countLines("line1\nline2\n")).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldSummarizePaste", () => {
|
||||
it("should return false for short single line", () => {
|
||||
expect(shouldSummarizePaste("hello world")).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for 3+ lines", () => {
|
||||
expect(shouldSummarizePaste("line1\nline2\nline3")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for content over 150 chars", () => {
|
||||
const longContent = "a".repeat(151);
|
||||
expect(shouldSummarizePaste(longContent)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for 2 lines under 150 chars", () => {
|
||||
expect(shouldSummarizePaste("line1\nline2")).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for exactly 3 lines", () => {
|
||||
expect(shouldSummarizePaste("a\nb\nc")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for exactly 150 chars on single line", () => {
|
||||
const content = "a".repeat(150);
|
||||
expect(shouldSummarizePaste(content)).toBe(false);
|
||||
expect(shouldSummarizePaste(content + "a")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("generatePlaceholder", () => {
|
||||
it("should generate placeholder with line count", () => {
|
||||
expect(generatePlaceholder(5)).toBe("[Pasted ~5 lines]");
|
||||
});
|
||||
|
||||
it("should handle single line", () => {
|
||||
expect(generatePlaceholder(1)).toBe("[Pasted ~1 lines]");
|
||||
});
|
||||
|
||||
it("should handle large line count", () => {
|
||||
expect(generatePlaceholder(1000)).toBe("[Pasted ~1000 lines]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createPastedContent", () => {
|
||||
it("should create pasted content with correct properties", () => {
|
||||
const content = "line1\nline2\nline3";
|
||||
const result = createPastedContent("test-id", content, 10);
|
||||
|
||||
expect(result.id).toBe("test-id");
|
||||
expect(result.content).toBe(content);
|
||||
expect(result.lineCount).toBe(3);
|
||||
expect(result.placeholder).toBe("[Pasted ~3 lines]");
|
||||
expect(result.startPos).toBe(10);
|
||||
expect(result.endPos).toBe(10 + "[Pasted ~3 lines]".length);
|
||||
});
|
||||
});
|
||||
|
||||
describe("generatePasteId", () => {
|
||||
it("should generate unique ids with counter", () => {
|
||||
const id1 = generatePasteId(1);
|
||||
const id2 = generatePasteId(2);
|
||||
|
||||
expect(id1).toContain("paste-1-");
|
||||
expect(id2).toContain("paste-2-");
|
||||
expect(id1).not.toBe(id2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addPastedBlock", () => {
|
||||
it("should add block to empty state", () => {
|
||||
const state = createInitialPasteState();
|
||||
const content = "line1\nline2\nline3";
|
||||
|
||||
const { newState, pastedContent } = addPastedBlock(state, content, 5);
|
||||
|
||||
expect(newState.pasteCounter).toBe(1);
|
||||
expect(newState.pastedBlocks.size).toBe(1);
|
||||
expect(pastedContent.content).toBe(content);
|
||||
expect(pastedContent.startPos).toBe(5);
|
||||
});
|
||||
|
||||
it("should add multiple blocks", () => {
|
||||
let state = createInitialPasteState();
|
||||
|
||||
const result1 = addPastedBlock(state, "a\nb\nc", 0);
|
||||
state = result1.newState;
|
||||
|
||||
const result2 = addPastedBlock(state, "d\ne\nf", 50);
|
||||
state = result2.newState;
|
||||
|
||||
expect(state.pasteCounter).toBe(2);
|
||||
expect(state.pastedBlocks.size).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updatePastedBlockPositions", () => {
|
||||
it("should shift blocks after insertion point", () => {
|
||||
const blocks = new Map<string, PastedContent>();
|
||||
blocks.set("block1", {
|
||||
id: "block1",
|
||||
content: "test",
|
||||
lineCount: 1,
|
||||
placeholder: "[Pasted ~1 lines]",
|
||||
startPos: 20,
|
||||
endPos: 37,
|
||||
});
|
||||
|
||||
const updated = updatePastedBlockPositions(blocks, 10, 5);
|
||||
|
||||
const block = updated.get("block1");
|
||||
expect(block?.startPos).toBe(25);
|
||||
expect(block?.endPos).toBe(42);
|
||||
});
|
||||
|
||||
it("should not shift blocks before insertion point", () => {
|
||||
const blocks = new Map<string, PastedContent>();
|
||||
blocks.set("block1", {
|
||||
id: "block1",
|
||||
content: "test",
|
||||
lineCount: 1,
|
||||
placeholder: "[Pasted ~1 lines]",
|
||||
startPos: 5,
|
||||
endPos: 22,
|
||||
});
|
||||
|
||||
const updated = updatePastedBlockPositions(blocks, 30, 5);
|
||||
|
||||
const block = updated.get("block1");
|
||||
expect(block?.startPos).toBe(5);
|
||||
expect(block?.endPos).toBe(22);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updatePastedBlocksAfterDelete", () => {
|
||||
it("should shift blocks back when deleting before them", () => {
|
||||
const blocks = new Map<string, PastedContent>();
|
||||
blocks.set("block1", {
|
||||
id: "block1",
|
||||
content: "test",
|
||||
lineCount: 1,
|
||||
placeholder: "[Pasted ~1 lines]",
|
||||
startPos: 20,
|
||||
endPos: 37,
|
||||
});
|
||||
|
||||
const updated = updatePastedBlocksAfterDelete(blocks, 5, 5);
|
||||
|
||||
const block = updated.get("block1");
|
||||
expect(block?.startPos).toBe(15);
|
||||
expect(block?.endPos).toBe(32);
|
||||
});
|
||||
|
||||
it("should remove blocks when deletion contains them", () => {
|
||||
const blocks = new Map<string, PastedContent>();
|
||||
blocks.set("block1", {
|
||||
id: "block1",
|
||||
content: "test",
|
||||
lineCount: 1,
|
||||
placeholder: "[Pasted ~1 lines]",
|
||||
startPos: 10,
|
||||
endPos: 27,
|
||||
});
|
||||
|
||||
const updated = updatePastedBlocksAfterDelete(blocks, 5, 30);
|
||||
|
||||
expect(updated.size).toBe(0);
|
||||
});
|
||||
|
||||
it("should not affect blocks after deletion point", () => {
|
||||
const blocks = new Map<string, PastedContent>();
|
||||
blocks.set("block1", {
|
||||
id: "block1",
|
||||
content: "test",
|
||||
lineCount: 1,
|
||||
placeholder: "[Pasted ~1 lines]",
|
||||
startPos: 5,
|
||||
endPos: 22,
|
||||
});
|
||||
|
||||
const updated = updatePastedBlocksAfterDelete(blocks, 25, 5);
|
||||
|
||||
const block = updated.get("block1");
|
||||
expect(block?.startPos).toBe(5);
|
||||
expect(block?.endPos).toBe(22);
|
||||
});
|
||||
});
|
||||
|
||||
describe("expandPastedContent", () => {
|
||||
it("should expand single pasted block", () => {
|
||||
const blocks = new Map<string, PastedContent>();
|
||||
blocks.set("block1", {
|
||||
id: "block1",
|
||||
content: "expanded content here",
|
||||
lineCount: 1,
|
||||
placeholder: "[Pasted ~1 lines]",
|
||||
startPos: 6,
|
||||
endPos: 23,
|
||||
});
|
||||
|
||||
const input = "Hello [Pasted ~1 lines] world";
|
||||
const result = expandPastedContent(input, blocks);
|
||||
|
||||
expect(result).toBe("Hello expanded content here world");
|
||||
});
|
||||
|
||||
it("should expand multiple pasted blocks in correct order", () => {
|
||||
const blocks = new Map<string, PastedContent>();
|
||||
blocks.set("block1", {
|
||||
id: "block1",
|
||||
content: "FIRST",
|
||||
lineCount: 1,
|
||||
placeholder: "[Pasted ~1 lines]",
|
||||
startPos: 0,
|
||||
endPos: 17,
|
||||
});
|
||||
blocks.set("block2", {
|
||||
id: "block2",
|
||||
content: "SECOND",
|
||||
lineCount: 1,
|
||||
placeholder: "[Pasted ~1 lines]",
|
||||
startPos: 18,
|
||||
endPos: 35,
|
||||
});
|
||||
|
||||
const input = "[Pasted ~1 lines] [Pasted ~1 lines]";
|
||||
const result = expandPastedContent(input, blocks);
|
||||
|
||||
expect(result).toBe("FIRST SECOND");
|
||||
});
|
||||
|
||||
it("should return input unchanged when no blocks", () => {
|
||||
const blocks = new Map<string, PastedContent>();
|
||||
const input = "Hello world";
|
||||
|
||||
const result = expandPastedContent(input, blocks);
|
||||
|
||||
expect(result).toBe("Hello world");
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeLineEndings", () => {
|
||||
it("should convert CRLF to LF", () => {
|
||||
expect(normalizeLineEndings("line1\r\nline2")).toBe("line1\nline2");
|
||||
});
|
||||
|
||||
it("should convert CR to LF", () => {
|
||||
expect(normalizeLineEndings("line1\rline2")).toBe("line1\nline2");
|
||||
});
|
||||
|
||||
it("should handle mixed line endings", () => {
|
||||
expect(normalizeLineEndings("a\r\nb\rc\nd")).toBe("a\nb\nc\nd");
|
||||
});
|
||||
|
||||
it("should not change already normalized text", () => {
|
||||
expect(normalizeLineEndings("line1\nline2")).toBe("line1\nline2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearPastedBlocks", () => {
|
||||
it("should return empty state", () => {
|
||||
const result = clearPastedBlocks();
|
||||
|
||||
expect(result.pastedBlocks.size).toBe(0);
|
||||
expect(result.pasteCounter).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createInitialPasteState", () => {
|
||||
it("should create empty initial state", () => {
|
||||
const state = createInitialPasteState();
|
||||
|
||||
expect(state.pastedBlocks.size).toBe(0);
|
||||
expect(state.pasteCounter).toBe(0);
|
||||
});
|
||||
});
|
||||
25
tests/string-helpers.test.ts
Normal file
25
tests/string-helpers.test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { capitalizeWords } from '../src/utils/string-helpers';
|
||||
|
||||
describe('capitalizeWords', () => {
|
||||
it('should capitalize the first letter of each word in a string', () => {
|
||||
expect(capitalizeWords('hello world')).toBe('Hello World');
|
||||
expect(capitalizeWords('capitalize each word')).toBe('Capitalize Each Word');
|
||||
});
|
||||
|
||||
it('should handle empty strings', () => {
|
||||
expect(capitalizeWords('')).toBe('');
|
||||
});
|
||||
|
||||
it('should handle strings with multiple spaces', () => {
|
||||
expect(capitalizeWords(' hello world ')).toBe(' Hello World ');
|
||||
});
|
||||
|
||||
it('should handle strings with special characters', () => {
|
||||
expect(capitalizeWords('hello-world')).toBe('Hello-World');
|
||||
expect(capitalizeWords('hello_world')).toBe('Hello_World');
|
||||
});
|
||||
|
||||
it('should handle strings with numbers', () => {
|
||||
expect(capitalizeWords('hello 123 world')).toBe('Hello 123 World');
|
||||
});
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { viewTool } from '../src/tools/view.js';
|
||||
import { editTool } from '../src/tools/edit.js';
|
||||
import { writeTool } from '../src/tools/write.js';
|
||||
import { grepTool } from '../src/tools/grep.js';
|
||||
import { globTool } from '../src/tools/glob.js';
|
||||
import { bashTool } from '../src/tools/bash.js';
|
||||
|
||||
describe('Tools', () => {
|
||||
describe('ViewTool', () => {
|
||||
it('should read file contents', async () => {
|
||||
const result = await viewTool.execute('package.json');
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toContain('codetyper-cli');
|
||||
});
|
||||
|
||||
it('should check if file exists', async () => {
|
||||
const exists = await viewTool.exists('package.json');
|
||||
expect(exists).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GlobTool', () => {
|
||||
it('should find TypeScript files', async () => {
|
||||
const result = await globTool.execute('src/**/*.ts');
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.files).toBeDefined();
|
||||
expect(result.files!.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should find files by extension', async () => {
|
||||
const files = await globTool.findByExtension('ts', 'src');
|
||||
expect(files.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BashTool', () => {
|
||||
it('should execute simple command', async () => {
|
||||
const result = await bashTool.execute('echo "Hello World"');
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toContain('Hello World');
|
||||
});
|
||||
|
||||
it('should check if command exists', async () => {
|
||||
const exists = await bashTool.commandExists('node');
|
||||
expect(exists).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
436
tests/ui-components.test.ts
Normal file
436
tests/ui-components.test.ts
Normal file
@@ -0,0 +1,436 @@
|
||||
/**
|
||||
* 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 │");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user