Testing

OpenTUI provides a headless test renderer that creates a real Zig native renderer with mock I/O. Tests exercise the full stack - TypeScript components, Yoga layout, FFI, Zig rendering - with no mocking of the native layer.

Test Renderer

import { createTestRenderer } from "@opentui/core/testing"

const { renderer, mockInput, mockMouse, renderOnce, captureCharFrame, resize } = 
  await createTestRenderer({ width: 80, height: 24 })

createTestRenderer() creates a real CliRenderer with:

  • A TestWriteStream (no-op write) instead of real stdout
  • Real Zig native renderer in test mode
  • Configurable initial terminal size

Rendering and Capturing

import { Box, Text } from "@opentui/core"

// Build UI
renderer.root.add(
  Box(
    { border: true, borderStyle: "rounded", padding: 1 },
    Text({ content: "Hello" }),
  ),
)

// Execute one render cycle
await renderOnce()

// Capture the frame as a string
const frame = captureCharFrame()
console.log(frame)
// ╭─────────────────╮
// │ Hello            │
// ╰─────────────────╯

captureCharFrame() returns the rendered terminal output as a string - what you would see on screen.

Structured Span Capture

For assertions beyond string matching:

const spans = captureSpans()
// Returns structured data: { spans: [...], cursor: { x, y, visible } }
// Each span has: text, fg, bg, attributes, position

Resize

resize(40, 12)  // simulate terminal resize to 40x12
await renderOnce()

Mock Keyboard Input

import { createMockKeys } from "@opentui/core/testing"

const keys = createMockKeys(renderer)

// Single key
await keys.pressKey("a")
await keys.pressKey("escape")
await keys.pressKey("f1")

// With modifiers
await keys.pressKey("c", { ctrl: true })    // Ctrl+C
await keys.pressKey("s", { ctrl: true })    // Ctrl+S
await keys.pressKey("a", { shift: true })   // Shift+A

// Arrow keys
await keys.pressArrow("up")
await keys.pressArrow("down")

// Convenience methods
await keys.pressEnter()
await keys.pressEscape()
await keys.pressTab()
await keys.pressBackspace()
await keys.pressCtrlC()

// Type a string character by character
await keys.typeText("hello world")

// Sequence of keys with delay between
await keys.pressKeys(["h", "e", "l", "l", "o"], 10)

// Bracketed paste
await keys.pasteBracketedText("pasted content here")

Tip: Mock input generates real ANSI escape sequences that flow through the same stdin parsing path as production. This means your tests exercise the full input pipeline.

Mock Mouse Input

import { createMockMouse } from "@opentui/core/testing"

const mouse = createMockMouse(renderer)

// Click at coordinates
await mouse.click(10, 5)                    // left click
await mouse.click(10, 5, "right")           // right click
await mouse.doubleClick(10, 5)

// Mouse movement
await mouse.moveTo(20, 10)

// Press and release (for drag)
await mouse.pressDown(5, 5)
await mouse.moveTo(15, 5)                   // becomes drag while button held
await mouse.release(15, 5)

// Drag helper (includes interpolated intermediate positions)
await mouse.drag(5, 5, 25, 5)

// Scroll
await mouse.scroll(10, 5, "up")
await mouse.scroll(10, 5, "down")

Test Recording

Record frames for visual regression or debugging:

import { TestRecorder } from "@opentui/core/testing"

const recorder = new TestRecorder(renderer)

recorder.rec()  // start recording

// ... interact with the app ...
await renderOnce()
await keys.pressKey("a")
await renderOnce()

recorder.stop()  // stop recording

// Access recorded frames
for (const frame of recorder.frames) {
  console.log(`Frame ${frame.number} at ${frame.timestamp}:`)
  console.log(frame.content)
}

Spy Functions

Minimal spy for asserting callbacks:

import { createSpy } from "@opentui/core/testing"

const spy = createSpy()

const input = Input({
  onChange: spy,
})

// ... trigger input change ...

expect(spy.callCount).toBe(1)
spy.calledWith("expected value")  // assertion

Framework-Specific Test Helpers

Solid

import { testRender } from "@opentui/solid"

const { renderer, renderOnce, captureCharFrame, mockInput, mockMouse } = 
  await testRender(() => (
    <box style={{ padding: 1, border: true }}>
      <text content="Hello from Solid" />
    </box>
  ))

await renderOnce()
const frame = captureCharFrame()

React

import { testRender } from "@opentui/react"

const { renderer, renderOnce, captureCharFrame, mockInput, mockMouse } = 
  await testRender(
    <box style={{ padding: 1, border: true }}>
      <text content="Hello from React" />
    </box>
  )

await renderOnce()
const frame = captureCharFrame()

Writing Effective Tests

Pattern: Test Component Output

import { test, expect } from "bun:test"
import { createTestRenderer } from "@opentui/core/testing"
import { Box, Text } from "@opentui/core"

test("renders bordered box with content", async () => {
  const { renderer, renderOnce, captureCharFrame } = 
    await createTestRenderer({ width: 40, height: 10 })

  renderer.root.add(
    Box(
      { border: true, borderStyle: "single" },
      Text({ content: "Hello" }),
    ),
  )

  await renderOnce()
  const frame = captureCharFrame()

  expect(frame).toContain("Hello")
  expect(frame).toContain("┌")  // border character
})

Pattern: Test Keyboard Interaction

import { test, expect } from "bun:test"
import { createTestRenderer, createMockKeys } from "@opentui/core/testing"
import { Input } from "@opentui/core"

test("input accepts typed text", async () => {
  const { renderer, renderOnce, captureCharFrame } = 
    await createTestRenderer({ width: 40, height: 5 })

  const input = Input({ id: "name", placeholder: "Name..." })
  renderer.root.add(input)
  input.focus()

  await renderOnce()

  const keys = createMockKeys(renderer)
  await keys.typeText("Alice")
  await renderOnce()

  const frame = captureCharFrame()
  expect(frame).toContain("Alice")
})

Pattern: Test Mouse Interaction

import { test, expect } from "bun:test"
import { createTestRenderer, createMockMouse, createSpy } from "@opentui/core/testing"
import { Box } from "@opentui/core"

test("box responds to click", async () => {
  const spy = createSpy()
  const { renderer, renderOnce } = 
    await createTestRenderer({ width: 40, height: 10 })

  renderer.root.add(
    Box({
      id: "clickable",
      width: 20,
      height: 5,
      onMouseDown: spy,
    }),
  )

  await renderOnce()

  const mouse = createMockMouse(renderer)
  await mouse.click(5, 2)

  expect(spy.callCount).toBe(1)
})

Gotcha: Always call await renderOnce() after adding components and before capturing frames or interacting. The first render establishes layout positions, which mouse events need.

Running Tests

cd packages/core
bun test                          # all TypeScript tests
bun test src/renderables/Text.test.ts  # specific test file

cd packages/solid
bun test                          # Solid integration tests

cd packages/react
bun test                          # React integration tests