Test renderer for TermUI components. Renders your JSX tree into an in-memory screen buffer, then gives you methods to query the output, simulate key presses, and check what's on screen. No real terminal needed.
The API follows the same pattern as React Testing Library: render, query, interact, assert.
npm install --save-dev @termuijs/testingWorks with Vitest (recommended) or any test runner.
import { describe, it, expect } from 'vitest'
import { render } from '@termuijs/testing'
import { useState } from '@termuijs/jsx'
import { Text } from '@termuijs/widgets'
function Counter() {
const [count, setCount] = useState(0)
useInput((key) => {
if (key === '+') setCount((c) => c + 1)
})
return <Text>Count: {count}</Text>
}
describe('Counter', () => {
it('starts at zero', () => {
const t = render(<Counter />)
expect(t.getByText('Count: 0')).toBeTruthy()
t.unmount()
})
it('increments on +', () => {
const t = render(<Counter />)
t.fireKey('+')
expect(t.getByText('Count: 1')).toBeTruthy()
t.unmount()
})
})| Method | Description |
|---|---|
render(element, opts?) |
Render into a virtual screen (default 80x24). Returns a TestInstance |
t.getByText(text) |
Find first widget containing that text, or null |
t.getAllByText(text) |
Find all widgets containing that text |
t.getAllByType(Type) |
Find all widgets of a given constructor |
t.lastFrame() |
Current screen as an array of strings (one per row) |
t.toString() |
Joined non-empty screen rows |
t.renderToString() |
ANSI-free flat string snapshot. Good for assertions and diffs |
t.fireKey(key, mods?) |
Simulate a key press. Dispatches to all useInput and useKeymap handlers in the fiber tree |
t.typeText(text) |
Type characters one by one |
t.rerender(element?) |
Re-render using fiber-aware reconciliation. Hook state is preserved |
t.waitFor(fn, opts?) |
Poll fn() until it does not throw. Use for async state updates |
t.unmount() |
Clean up all component state. Always call this |
Use waitFor for assertions that depend on async state updates.
it('loads data', async () => {
const t = render(<DataList />)
await t.waitFor(() => {
expect(t.getByText('Item 1')).toBeTruthy()
}, { timeout: 2000, interval: 50 })
t.unmount()
})Default options: { timeout: 1000, interval: 10 }.
Get a clean string snapshot without ANSI escape codes:
it('renders correctly', () => {
const t = render(<StatusBar />)
const output = t.renderToString()
expect(output).toContain('Ready')
t.unmount()
})rerender() uses reRenderComponent internally. Hook state (useState, useRef) survives the re-render. You don't need to recreate components or re-supply initial values.
it('keeps state on rerender', () => {
const t = render(<Counter />)
t.fireKey('+')
t.fireKey('+')
t.rerender() // hook state preserved
expect(t.getByText('Count: 2')).toBeTruthy()
t.unmount()
})fireKey dispatches to every handler registered via useInput or useKeymap in the full fiber tree. You don't need to set focus manually for most tests.
The second argument to fireKey allows simulating modifier keys (ctrl, shift, alt).
t.fireKey('s', { ctrl: true }) // triggers useKeymap([{ key: 's', ctrl: true, action: save }])
t.fireKey('enter') // triggers useInput handlersCall destroy() on your stores in afterEach to reset state between tests.
afterEach(() => {
useCounterStore.destroy()
})Wrap the component in a Provider to supply test values.
const t = render(
<ThemeCtx.Provider value={testTheme}>
<MyComponent />
</ThemeCtx.Provider>
)lastFrame() returns the screen as a string array, which works with Vitest's toMatchSnapshot:
expect(t.lastFrame()).toMatchSnapshot()Full docs at www.termui.io/docs/testing/overview.
MIT