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