Testing
Silvery includes a comprehensive test suite with 870+ tests covering everything from low-level buffer operations to high-level React component rendering. This guide documents the test structure and how to use createRenderer for testing your own Silvery applications.
Test Suite Overview
The test suite is organized by domain:
| File | Tests | Description |
|---|---|---|
unicode.test.ts | 223 | Unicode handling: graphemes, display width, CJK, emoji, ZWJ sequences |
output.test.ts | 63 | ANSI output generation, style conversion, buffer rendering |
input.test.tsx | 49 | Keyboard input handling, escape sequences, modifiers |
terminal-multiplexers.test.ts | 41 | tmux/screen compatibility, synchronized update mode |
ink-compat.test.tsx | 40 | Ink API compatibility verification |
compat/layout.test.tsx | 42 | Flex layout API compatibility |
ime.test.tsx | 39 | CJK/IME input handling |
buffer.test.ts | 38 | Terminal buffer operations, cell packing |
pipeline.test.ts | 36 | Render pipeline: measure, layout, content, output phases |
ansi-parsing.test.ts | 29 | ANSI escape sequence parsing |
hooks.test.tsx | 28 | useContentRect, useFocusable, useFocusManager, useStdout |
layout-equivalence.test.tsx | 26 | Yoga vs Flexily layout engine parity |
render.test.ts | 24 | Core render API |
memory.test.tsx | 20 | Memory leak detection, listener cleanup |
accessibility.test.tsx | 20 | Screen reader compatibility |
react19.test.tsx | 18 | React 19 compatibility |
exit.test.tsx | 17 | Process exit timing and useApp |
measureElement.test.tsx | 14 | Element measurement API |
layout-engines.test.ts | 14 | Yoga and Flexily engine interoperability |
border-dim-color.test.tsx | 13 | Border styling and colors |
integration.test.tsx | 13 | Component rendering integration |
rerender-bugs.test.tsx | 13 | Re-render bug reproductions |
performance.test.tsx | 12 | Rendering performance benchmarks |
examples-bugs.test.tsx | 11 | Bug reproductions from examples |
view-bugs.test.tsx | 11 | View component bug reproductions |
examples-cursor.test.tsx | 9 | Cursor positioning tests |
non-tty.test.tsx | 9 | Non-TTY output handling |
Using createRenderer
The createRenderer function creates a render function with auto-cleanup between tests. Each call returns an App instance with locators, keyboard input, and text inspection.
Basic Usage
import { createRenderer } from "@silvery/test"
import { Text } from "silvery"
const render = createRenderer()
test("renders text", () => {
const app = render(<Text>Hello</Text>)
expect(app.text).toContain("Hello")
})Auto-Cleanup
Each render() call automatically unmounts the previous render, so you don't need explicit cleanup:
const render = createRenderer()
test("first test", () => {
const app = render(<Text>First</Text>)
expect(app.text).toContain("First")
})
test("second test", () => {
// Previous render is auto-cleaned
const app = render(<Text>Second</Text>)
expect(app.text).toContain("Second")
})Testing Keyboard Input
Use app.press() to simulate keyboard input with named keys:
import { useState } from "react"
import { Box, Text, useInput } from "silvery"
function Counter() {
const [count, setCount] = useState(0)
useInput((input, key) => {
if (input === "+" || key.upArrow) setCount((c) => c + 1)
if (input === "-" || key.downArrow) setCount((c) => c - 1)
})
return <Text>Count: {count}</Text>
}
test("increments with arrow keys", async () => {
const render = createRenderer()
const app = render(<Counter />)
await app.press("ArrowUp")
await app.press("ArrowUp")
await app.press("ArrowDown")
expect(app.text).toContain("Count: 1")
})Named Keys for press()
| Key | Name |
|---|---|
| Up Arrow | ArrowUp |
| Down Arrow | ArrowDown |
| Right Arrow | ArrowRight |
| Left Arrow | ArrowLeft |
| Escape | Escape |
| Return/Enter | Enter |
| Tab | Tab |
| Backspace | Backspace |
| Home | Home |
| End | End |
| Page Up | PageUp |
| Page Down | PageDown |
Testing Re-renders
Use app.rerender() to update props and verify state changes:
function Greeter({ name }: { name: string }) {
return <Text>Hello, {name}!</Text>
}
test("updates on prop change", () => {
const render = createRenderer()
const app = render(<Greeter name="Alice" />)
expect(app.text).toContain("Hello, Alice!")
app.rerender(<Greeter name="Bob" />)
expect(app.text).toContain("Hello, Bob!")
})Custom Dimensions
Specify terminal dimensions at renderer creation:
const render = createRenderer({
cols: 120,
rows: 40,
})
const app = render(<WideComponent />)
expect(app.text).toContain("wide content")Frame Inspection
The App instance provides direct access to rendered output:
const app = render(<MyComponent />)
// Plain text (no ANSI codes)
const text = app.text
// Text with ANSI styling
const ansi = app.ansi
// All rendered frames (for history inspection)
console.log(app.frames.length)
// Clear the frame history
app.clear()Test Utilities
stripAnsi
Remove ANSI escape codes for easier assertions:
import { stripAnsi } from "@silvery/test"
const app = render(<Text color="red">Hello</Text>)
// app.text already strips ANSI, but stripAnsi is useful for app.ansi
const text = stripAnsi(app.ansi)
expect(text).toBe("Hello")normalizeFrame
Strip ANSI codes and normalize whitespace:
import { normalizeFrame } from "@silvery/test"
const app = render(<MyComponent />)
const normalized = normalizeFrame(app.ansi)
// Strips ANSI, trims trailing whitespace, removes empty trailing lineswaitFor
Wait for async conditions:
import { waitFor } from "@silvery/test"
test("async update", async () => {
const app = render(<AsyncComponent />)
await waitFor(() => app.text.includes("Loaded"), {
timeout: 1000,
interval: 10,
})
expect(app.text).toContain("Loaded")
})Test Patterns
Testing Focus Management
import { useFocusable } from "silvery"
function FocusableItem({ testID }: { testID: string }) {
const { focused } = useFocusable()
return (
<Box testID={testID} focusable>
<Text backgroundColor={focused ? "cyan" : undefined}>{testID}</Text>
</Box>
)
}
test("focus navigation", async () => {
const render = createRenderer()
const app = render(
<Box flexDirection="column">
<FocusableItem testID="item1" />
<FocusableItem testID="item2" />
</Box>,
)
// Tab to move focus
await app.press("Tab")
// Verify focus moved using locator
expect(app.getByTestId("item2").textContent()).toBe("item2")
})Testing Layout Dimensions
import { useContentRect, NodeContext } from "silvery"
function LayoutCapture({ onLayout }: { onLayout: (l: any) => void }) {
const layout = useContentRect()
React.useEffect(() => onLayout(layout), [layout])
return <Text>Content</Text>
}
test("layout provides dimensions", () => {
let capturedLayout = null
const render = createRenderer()
render(
<Box width={40} height={10}>
<LayoutCapture onLayout={(l) => (capturedLayout = l)} />
</Box>,
)
expect(capturedLayout).toHaveProperty("width")
expect(capturedLayout).toHaveProperty("height")
})Testing with RuntimeContext
The test renderer (createRenderer) automatically provides RuntimeContext. Components using useApp() or useInput() work out of the box:
test("useApp exit function", async () => {
const render = createRenderer()
const app = render(<ComponentThatCallsExit />)
// press() triggers input through RuntimeContext
await app.press("q")
expect(app.exitCalled()).toBe(true)
})Running Tests
# Run all tests
bun test
# Run specific test file
bun test tests/unicode.test.ts
# Run tests matching pattern
bun test --pattern "CJK"
# Run with verbose output
bun test --verboseTest Organization Patterns
Bug Reproduction Tests
Bug fixes include regression tests named after issue IDs:
describe("Bug km-r0nz: Columns view vertical spacing", () => {
it("items should have consistent vertical spacing", () => {
// Reproduction of original bug
})
})Compatibility Tests
Ink API compatibility is verified through:
describe("Ink API Compatibility", () => {
describe("Component Exports", () => {
test("Box component exists and is a function", () => {
expect(typeof Box).toBe("function")
})
})
})Performance Tests
Performance benchmarks use timing utilities:
function benchmark(fn: () => void, iterations = 5) {
const runs = []
for (let i = 0; i < iterations; i++) {
const start = performance.now()
fn()
runs.push(performance.now() - start)
}
return {
min: Math.min(...runs),
avg: runs.reduce((a, b) => a + b) / runs.length,
}
}
test("renders 200 components efficiently", () => {
const stats = benchmark(() => render(<LargeList items={200} />))
expect(stats.avg).toBeLessThan(100) // ms
})