Files
OpenQode/tests/tui-components.test.mjs
2025-12-14 00:40:14 +04:00

222 lines
9.6 KiB
JavaScript

/**
* TUI Component Tests
* Tests for markdown rendering, layout utilities, and theme
*
* Run with: node --experimental-vm-modules node_modules/jest/bin/jest.js tests/
* Or: npm test (if configured in package.json)
*/
import { describe, test, expect } from '@jest/globals';
import { computeLayoutMode, truncateText, calculateViewport, getTextWidth } from '../bin/tui-layout.mjs';
import { theme } from '../bin/tui-theme.mjs';
// ═══════════════════════════════════════════════════════════════
// LAYOUT UTILITY TESTS
// ═══════════════════════════════════════════════════════════════
describe('Layout Utilities', () => {
describe('computeLayoutMode', () => {
test('returns wide mode for columns >= 120', () => {
const result = computeLayoutMode(120, 40);
expect(result.mode).toBe('wide');
expect(result.sidebarWidth).toBeGreaterThan(0);
});
test('returns medium mode for columns 90-119', () => {
const result = computeLayoutMode(100, 40);
expect(result.mode).toBe('medium');
});
test('returns narrow mode for columns 60-89', () => {
const result = computeLayoutMode(80, 40);
expect(result.mode).toBe('narrow');
expect(result.sidebarWidth).toBe(0); // collapsed by default
});
test('returns tiny mode for columns < 60', () => {
const result = computeLayoutMode(50, 40);
expect(result.mode).toBe('tiny');
});
test('returns tiny mode for rows < 20', () => {
const result = computeLayoutMode(100, 15);
expect(result.mode).toBe('tiny');
});
test('handles null dimensions with defaults', () => {
const result = computeLayoutMode(null, null);
expect(result.cols).toBe(80);
expect(result.rows).toBe(24);
});
});
describe('truncateText', () => {
test('returns empty string for empty input', () => {
expect(truncateText('', 10)).toBe('');
expect(truncateText(null, 10)).toBe('');
});
test('returns original text if shorter than width', () => {
expect(truncateText('hello', 10)).toBe('hello');
});
test('truncates text longer than width', () => {
const result = truncateText('hello world', 8);
expect(result.length).toBeLessThanOrEqual(8);
});
});
describe('calculateViewport', () => {
test('calculates viewport height correctly', () => {
const layout = { rows: 40, cols: 100, mode: 'wide' };
const viewport = calculateViewport(layout, {
headerRows: 2,
inputRows: 4,
thinkingRows: 0,
marginsRows: 2
});
expect(viewport.viewHeight).toBe(32); // 40 - 2 - 4 - 0 - 2
expect(viewport.maxMessages).toBeGreaterThan(0);
});
test('ensures minimum viewport height', () => {
const layout = { rows: 10, cols: 100, mode: 'wide' };
const viewport = calculateViewport(layout, {
headerRows: 5,
inputRows: 5,
thinkingRows: 5,
marginsRows: 5
});
expect(viewport.viewHeight).toBeGreaterThanOrEqual(4);
});
});
});
// ═══════════════════════════════════════════════════════════════
// THEME TESTS
// ═══════════════════════════════════════════════════════════════
describe('Theme System', () => {
test('has required color properties', () => {
expect(theme.colors).toBeDefined();
expect(theme.colors.fg).toBeDefined();
expect(theme.colors.muted).toBeDefined();
expect(theme.colors.success).toBeDefined();
expect(theme.colors.error).toBeDefined();
expect(theme.colors.warning).toBeDefined();
expect(theme.colors.info).toBeDefined();
});
test('has spacing tokens', () => {
expect(theme.spacing).toBeDefined();
expect(theme.spacing.xs).toBeDefined();
expect(theme.spacing.sm).toBeDefined();
expect(theme.spacing.md).toBeDefined();
});
test('has border styles', () => {
expect(theme.borders).toBeDefined();
expect(theme.borders.single).toBeDefined();
expect(theme.borders.round).toBeDefined();
});
test('has icon definitions', () => {
expect(theme.icons).toBeDefined();
expect(theme.icons.prompt).toBeDefined();
expect(theme.icons.bullet).toBeDefined();
});
});
// ═══════════════════════════════════════════════════════════════
// STREAMING BUFFER TESTS
// ═══════════════════════════════════════════════════════════════
describe('Streaming Buffer', () => {
// Note: These are conceptual tests - actual hook testing requires React testing library
test('buffer module exports correctly', async () => {
const { useStreamBuffer, useResizeDebounce } = await import('../bin/tui-stream-buffer.mjs');
expect(typeof useStreamBuffer).toBe('function');
expect(typeof useResizeDebounce).toBe('function');
});
});
// ═══════════════════════════════════════════════════════════════
// MESSAGE PARSING TESTS
// ═══════════════════════════════════════════════════════════════
// Logic mirrored from flattenMessagesToBlocks in opencode-ink.mjs
const splitMessageContent = (content) => {
// Regex captures: Code blocks OR [AGENT: Name] tags
// Match code blocks (```...```) OR Agent tags ([AGENT: ...])
return content.split(/(```[\s\S]*?```|\[AGENT:[^\]]+\])/g);
};
describe('Message Parsing', () => {
test('splits agent tags correctly', () => {
const input = "Thinking...\n[AGENT: Security]\nReviewing code...";
const parts = splitMessageContent(input).filter(p => p.trim());
expect(parts.length).toBeGreaterThan(1);
expect(parts.some(p => p.includes('[AGENT: Security]'))).toBe(true);
});
test('interleaves agent tags and code blocks', () => {
const input = "Start [AGENT: Planner] Plan:\n```text\nstep 1\n```\n[AGENT: Builder] Go.";
const parts = splitMessageContent(input).filter(p => p.trim());
// Expected: "Start", "[AGENT: Planner]", "Plan:", "```...```", "[AGENT: Builder]", "Go."
expect(parts).toContain('[AGENT: Planner]');
expect(parts).toContain('[AGENT: Builder]');
const codeBlock = parts.find(p => p.startsWith('```'));
expect(codeBlock).toBeDefined();
});
});
// ═══════════════════════════════════════════════════════════════
// SMART AGENT FLOW TESTS
// ═══════════════════════════════════════════════════════════════
describe('Smart Agent Flow', () => {
test('exports SmartAgentFlow class', async () => {
const { SmartAgentFlow, getSmartAgentFlow } = await import('../bin/smart-agent-flow.mjs');
expect(SmartAgentFlow).toBeDefined();
expect(typeof getSmartAgentFlow).toBe('function');
});
test('getSmartAgentFlow returns singleton instance', async () => {
const { getSmartAgentFlow } = await import('../bin/smart-agent-flow.mjs');
const flow1 = getSmartAgentFlow();
const flow2 = getSmartAgentFlow();
expect(flow1).toBe(flow2);
});
test('has built-in agents', async () => {
const { getSmartAgentFlow } = await import('../bin/smart-agent-flow.mjs');
const flow = getSmartAgentFlow();
const agents = flow.getAgents();
expect(agents.length).toBeGreaterThanOrEqual(6);
expect(agents.some(a => a.id === 'build')).toBe(true);
expect(agents.some(a => a.id === 'plan')).toBe(true);
expect(agents.some(a => a.id === 'test')).toBe(true);
});
test('analyzeRequest detects security patterns', async () => {
const { getSmartAgentFlow } = await import('../bin/smart-agent-flow.mjs');
const flow = getSmartAgentFlow();
const result = flow.analyzeRequest('add authentication and password handling');
expect(result.patterns).toContain('security-sensitive');
});
test('toggle enables/disables multi-agent mode', async () => {
const { getSmartAgentFlow } = await import('../bin/smart-agent-flow.mjs');
const flow = getSmartAgentFlow();
flow.toggle(true);
expect(flow.config.enabled).toBe(true);
flow.toggle(false);
expect(flow.config.enabled).toBe(false);
});
});
console.log('✅ All test suites loaded successfully');