OpenTUI provides a comprehensive testing framework built on Bun’s test runner. The test renderer allows you to write unit and integration tests for your terminal UI without needing an actual terminal.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/anomalyco/opentui/llms.txt
Use this file to discover all available pages before exploring further.
Test Renderer
The test renderer creates a virtual terminal environment for testing:import { test, expect } from 'bun:test'
import { createTestRenderer } from '@opentui/core/testing'
test('renders text', async () => {
const { renderer, renderOnce, captureCharFrame } = await createTestRenderer({
width: 80,
height: 24,
})
const text = new TextRenderable(renderer, {
content: 'Hello, World!',
})
renderer.root.add(text)
await renderOnce()
const output = captureCharFrame()
expect(output).toContain('Hello, World!')
})
Test Renderer Options
interface TestRendererOptions {
width?: number // Terminal width (default: 80)
height?: number // Terminal height (default: 24)
kittyKeyboard?: boolean // Enable Kitty keyboard protocol
otherModifiersMode?: boolean // Enable modifyOtherKeys mode
// ... other CliRendererConfig options
}
Returned Utilities
const {
renderer, // The test renderer instance
mockInput, // Mock keyboard input
mockMouse, // Mock mouse input
renderOnce, // Render a single frame
captureCharFrame, // Capture current frame as string
captureSpans, // Capture frame with styling info
resize, // Resize the terminal
} = await createTestRenderer({ width: 80, height: 24 })
Capturing Output
Character Frame
Capture the rendered output as a plain string:await renderOnce()
const output = captureCharFrame()
expect(output).toContain('Expected text')
expect(output.split('\n')).toHaveLength(24) // 24 rows
Spans with Styling
Capture detailed styling information:const frame = captureSpans()
console.log(frame.cols) // Terminal width
console.log(frame.rows) // Terminal height
console.log(frame.cursor) // [x, y] cursor position
console.log(frame.lines) // Array of styled span lines
Mock Keyboard Input
Simulate keyboard input in your tests:import { createMockKeys, KeyCodes } from '@opentui/core/testing'
test('handles keyboard input', async () => {
const { renderer, mockInput, renderOnce, captureCharFrame } =
await createTestRenderer({ width: 80, height: 24 })
const input = new InputRenderable(renderer)
renderer.root.add(input)
// Type text
await mockInput.typeText('Hello')
await renderOnce()
expect(captureCharFrame()).toContain('Hello')
})
Typing Text
// Type text immediately
mockInput.typeText('hello world')
// Type with delay between keys
await mockInput.typeText('hello', 10) // 10ms between keys
Pressing Keys
// Press single character
mockInput.pressKey('a')
// Press special keys
mockInput.pressKey(KeyCodes.ENTER)
mockInput.pressKey(KeyCodes.ESCAPE)
mockInput.pressKey(KeyCodes.BACKSPACE)
mockInput.pressKey(KeyCodes.TAB)
// Press with modifiers
mockInput.pressKey('a', { ctrl: true })
mockInput.pressKey('f', { meta: true })
mockInput.pressKey('z', { ctrl: true, shift: true })
mockInput.pressKey(KeyCodes.ARROW_LEFT, { meta: true })
Available Key Codes
KeyCodes.RETURN
KeyCodes.LINEFEED
KeyCodes.TAB
KeyCodes.BACKSPACE
KeyCodes.DELETE
KeyCodes.HOME
KeyCodes.END
KeyCodes.ESCAPE
KeyCodes.ARROW_UP
KeyCodes.ARROW_DOWN
KeyCodes.ARROW_LEFT
KeyCodes.ARROW_RIGHT
KeyCodes.F1 through KeyCodes.F12
Convenience Methods
// Press common keys
mockInput.pressEnter()
mockInput.pressEnter({ meta: true })
mockInput.pressEscape()
mockInput.pressTab()
mockInput.pressBackspace()
mockInput.pressCtrlC()
// Press arrow keys
mockInput.pressArrow('up')
mockInput.pressArrow('down')
mockInput.pressArrow('left')
mockInput.pressArrow('right')
mockInput.pressArrow('left', { meta: true })
// Paste bracketed text
mockInput.pasteBracketedText('pasted content')
// Press multiple keys
mockInput.pressKeys(['h', 'e', 'l', 'l', 'o'])
await mockInput.pressKeys(['a', 'b'], 10) // with delay
Mock Mouse Input
Simulate mouse interactions:import { createMockMouse, MouseButtons } from '@opentui/core/testing'
test('handles mouse clicks', async () => {
const { renderer, mockMouse, renderOnce } =
await createTestRenderer({ width: 80, height: 24 })
let clicked = false
const button = new Button(renderer, {
text: 'Click me',
onClick: () => { clicked = true },
})
renderer.root.add(button)
await renderOnce()
await mockMouse.click(10, 5)
expect(clicked).toBe(true)
})
Mouse Operations
// Click
await mockMouse.click(x, y)
await mockMouse.click(x, y, MouseButtons.RIGHT)
await mockMouse.click(x, y, MouseButtons.LEFT, {
modifiers: { ctrl: true, shift: true },
delayMs: 10,
})
// Double click
await mockMouse.doubleClick(x, y)
// Press and release
await mockMouse.pressDown(x, y, MouseButtons.MIDDLE)
await mockMouse.release(x, y, MouseButtons.MIDDLE)
// Move cursor
await mockMouse.moveTo(x, y)
await mockMouse.moveTo(x, y, { modifiers: { shift: true } })
// Drag
await mockMouse.drag(startX, startY, endX, endY)
await mockMouse.drag(startX, startY, endX, endY, MouseButtons.RIGHT, {
modifiers: { alt: true },
})
// Scroll
await mockMouse.scroll(x, y, 'up')
await mockMouse.scroll(x, y, 'down')
await mockMouse.scroll(x, y, 'left')
await mockMouse.scroll(x, y, 'right')
await mockMouse.scroll(x, y, 'up', { modifiers: { shift: true } })
Mouse State
// Get current position
const pos = mockMouse.getCurrentPosition() // { x, y }
// Get pressed buttons
const buttons = mockMouse.getPressedButtons() // MouseButton[]
Mouse Buttons
MouseButtons.LEFT // 0
MouseButtons.MIDDLE // 1
MouseButtons.RIGHT // 2
MouseButtons.WHEEL_UP // 64
MouseButtons.WHEEL_DOWN // 65
MouseButtons.WHEEL_LEFT // 66
MouseButtons.WHEEL_RIGHT // 67
Test Recorder
Record frames during rendering for analysis:import { TestRecorder } from '@opentui/core/testing'
test('records frames', async () => {
const { renderer, renderOnce } = await createTestRenderer({
width: 80,
height: 24,
})
const recorder = new TestRecorder(renderer)
// Start recording
recorder.rec()
const text = new TextRenderable(renderer, { content: 'Frame 1' })
renderer.root.add(text)
await Bun.sleep(1)
text.content = 'Frame 2'
await Bun.sleep(1)
// Stop recording
recorder.stop()
const frames = recorder.recordedFrames
expect(frames.length).toBeGreaterThan(0)
frames.forEach((frame) => {
console.log(`Frame ${frame.frameNumber} at ${frame.timestamp}ms:`)
console.log(frame.frame)
})
})
Recorder API
const recorder = new TestRecorder(renderer, {
recordBuffers: {
fg: true, // Record foreground colors
bg: true, // Record background colors
attributes: true, // Record text attributes
},
now: () => performance.now(), // Custom time function
})
// Start recording
recorder.rec()
// Stop recording
recorder.stop()
// Check if recording
if (recorder.isRecording) {
// ...
}
// Get recorded frames
const frames = recorder.recordedFrames
// Clear frames
recorder.clear()
Recorded Frame Format
interface RecordedFrame {
frame: string // Frame content
timestamp: number // Time since recording started (ms)
frameNumber: number // Sequential frame number
buffers?: { // Optional buffer data
fg?: Float32Array // Foreground colors
bg?: Float32Array // Background colors
attributes?: Uint8Array // Text attributes
}
}
Spy Utility
Track function calls in tests:import { createSpy } from '@opentui/core/testing'
test('callback is called', async () => {
const spy = createSpy()
someFunction(spy)
expect(spy.callCount()).toBe(1)
expect(spy.calledWith('arg1', 'arg2')).toBe(true)
expect(spy.calls).toEqual([['arg1', 'arg2']])
spy.reset()
expect(spy.callCount()).toBe(0)
})
Running Tests
Run All Tests
cd packages/core
bun test
Run Specific Test File
bun test src/testing/integration.test.ts
Run with Filter
bun test --test-name-pattern="keyboard"
Watch Mode
bun test --watch
Native Tests
Test the Zig core directly:cd packages/core
bun run test:native
Filter Native Tests
bun run test:native -Dtest-filter="text buffer"
Testing Best Practices
1. Use Descriptive Test Names
test('text input accepts typed characters', async () => {
// ...
})
test('button triggers onClick when clicked', async () => {
// ...
})
2. Test User Interactions
test('form submits on Enter key', async () => {
const { renderer, mockInput } = await createTestRenderer()
const submitted = createSpy()
const form = new Form(renderer, { onSubmit: submitted })
renderer.root.add(form)
await renderOnce()
mockInput.pressEnter()
expect(submitted.callCount()).toBe(1)
})
3. Test Edge Cases
test('handles empty input', async () => {
// Test with no input
})
test('handles very long input', async () => {
// Test with maximum length input
})
test('handles resize during render', async () => {
const { renderer, resize, renderOnce } = await createTestRenderer()
await renderOnce()
resize(100, 30)
await renderOnce()
// Verify layout adapted
})
4. Use Setup and Teardown
import { test, expect, beforeEach, afterEach } from 'bun:test'
let renderer: TestRenderer
let cleanup: () => void
beforeEach(async () => {
const result = await createTestRenderer({ width: 80, height: 24 })
renderer = result.renderer
cleanup = () => renderer.destroy()
})
afterEach(() => {
cleanup()
})
test('test 1', async () => {
// Use renderer
})
test('test 2', async () => {
// Use renderer
})
5. Test Async Operations
test('loads data asynchronously', async () => {
const { renderer, renderOnce, captureCharFrame } = await createTestRenderer()
const component = new AsyncComponent(renderer)
renderer.root.add(component)
await renderOnce()
expect(captureCharFrame()).toContain('Loading...')
await component.loadComplete
await renderOnce()
expect(captureCharFrame()).toContain('Data loaded')
})
Example: Complete Integration Test
import { test, expect } from 'bun:test'
import {
createTestRenderer,
createSpy,
KeyCodes,
MouseButtons,
} from '@opentui/core/testing'
test('todo list integration', async () => {
const { renderer, mockInput, mockMouse, renderOnce, captureCharFrame } =
await createTestRenderer({ width: 80, height: 24 })
const onAddItem = createSpy()
const todoList = new TodoList(renderer, { onAddItem })
renderer.root.add(todoList)
// Render initial state
await renderOnce()
expect(captureCharFrame()).toContain('Todo List')
expect(todoList.items).toHaveLength(0)
// Type new item
await mockInput.typeText('Buy groceries')
await renderOnce()
expect(captureCharFrame()).toContain('Buy groceries')
// Submit item
mockInput.pressEnter()
await renderOnce()
expect(onAddItem.callCount()).toBe(1)
expect(todoList.items).toHaveLength(1)
// Click item to toggle
await mockMouse.click(5, 5)
await renderOnce()
expect(todoList.items[0].completed).toBe(true)
// Delete with keyboard
mockInput.pressKey('d', { ctrl: true })
await renderOnce()
expect(todoList.items).toHaveLength(0)
})
Next Steps
Performance
Optimize your application performance
Environment Variables
Configure with environment variables