Skip to content

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:

FileTestsDescription
unicode.test.ts223Unicode handling: graphemes, display width, CJK, emoji, ZWJ sequences
output.test.ts63ANSI output generation, style conversion, buffer rendering
input.test.tsx49Keyboard input handling, escape sequences, modifiers
terminal-multiplexers.test.ts41tmux/screen compatibility, synchronized update mode
ink-compat.test.tsx40Ink API compatibility verification
compat/layout.test.tsx42Flex layout API compatibility
ime.test.tsx39CJK/IME input handling
buffer.test.ts38Terminal buffer operations, cell packing
pipeline.test.ts36Render pipeline: measure, layout, content, output phases
ansi-parsing.test.ts29ANSI escape sequence parsing
hooks.test.tsx28useContentRect, useFocusable, useFocusManager, useStdout
layout-equivalence.test.tsx26Yoga vs Flexily layout engine parity
render.test.ts24Core render API
memory.test.tsx20Memory leak detection, listener cleanup
accessibility.test.tsx20Screen reader compatibility
react19.test.tsx18React 19 compatibility
exit.test.tsx17Process exit timing and useApp
measureElement.test.tsx14Element measurement API
layout-engines.test.ts14Yoga and Flexily engine interoperability
border-dim-color.test.tsx13Border styling and colors
integration.test.tsx13Component rendering integration
rerender-bugs.test.tsx13Re-render bug reproductions
performance.test.tsx12Rendering performance benchmarks
examples-bugs.test.tsx11Bug reproductions from examples
view-bugs.test.tsx11View component bug reproductions
examples-cursor.test.tsx9Cursor positioning tests
non-tty.test.tsx9Non-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

tsx
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:

tsx
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:

tsx
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()

KeyName
Up ArrowArrowUp
Down ArrowArrowDown
Right ArrowArrowRight
Left ArrowArrowLeft
EscapeEscape
Return/EnterEnter
TabTab
BackspaceBackspace
HomeHome
EndEnd
Page UpPageUp
Page DownPageDown

Testing Re-renders

Use app.rerender() to update props and verify state changes:

tsx
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:

tsx
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:

tsx
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:

tsx
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:

tsx
import { normalizeFrame } from "@silvery/test"

const app = render(<MyComponent />)
const normalized = normalizeFrame(app.ansi)
// Strips ANSI, trims trailing whitespace, removes empty trailing lines

waitFor

Wait for async conditions:

tsx
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

tsx
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

tsx
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:

tsx
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

bash
# 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 --verbose

Test Organization Patterns

Bug Reproduction Tests

Bug fixes include regression tests named after issue IDs:

tsx
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:

tsx
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:

tsx
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
})

Released under the MIT License.