@wolf-tui/testing-library
v1.0.0
Published
<div align="center">
Readme
@wolf-tui/testing-library
Testing CLI apps means fighting with process.stdout and ANSI escape codes. This library fixes that.
pnpm add -D @wolf-tui/testing-libraryThe Problem
Testing CLI and TUI applications headlessly is tedious. You have to mock terminal I/O, intercept frame rendering, and strip complex ANSI escape sequences just to assert on plain text.
If you've used ink-testing-library, you already know the fix: virtual streams. What's new here is a framework-agnostic implementation designed specifically for the wolf-tui ecosystem (React, Vue, Angular, Solid, Svelte).
See It Work
Recommended: import the testing-aware render from your adapter's /testing subpath. It wires up the virtual streams for you, registers the instance for global cleanup, and re-exports the helpers from this library:
import { afterEach, test, expect } from 'vitest'
import React from 'react'
import {
render,
cleanup,
KEYS,
delay,
stripAnsi,
} from '@wolf-tui/react/testing'
import { App } from './App'
afterEach(cleanup) // unmounts every instance created via render()
test('navigates menu', async () => {
const { stdin, lastFrame } = render(React.createElement(App), {
columns: 80,
rows: 24,
})
await stdin.write(KEYS.DOWN)
await stdin.write(KEYS.ENTER)
await delay(100)
expect(stripAnsi(lastFrame() ?? '')).toContain('Selection: Option B')
})If your adapter doesn't have a /testing entry point yet, or you need finer control, wire the streams up yourself:
import { MockStdout, MockStdin, stripAnsi, KEYS, delay } from '@wolf-tui/testing-library'
import { render } from '@wolf-tui/react'
import { App } from './App'
const stdout = new MockStdout(80, 24)
const stdin = new MockStdin(stdout) // MockStdin needs MockStdout for frame sync
render(<App />, { stdout, stdin })
await stdin.write(KEYS.DOWN)
await delay(100)
expect(stripAnsi(stdout.lastFrame() ?? '')).toContain('Option B')Getting Started
- Install the package.
- Setup your environment: Ensure your test runner is forcing colors so ANSI rendering is deterministic across environments.
For Vitest, add a setup file:
// test/setup.ts
import chalk from 'chalk'
process.env.FORCE_COLOR = '3'
chalk.level = 3 // Force 16m colors in headless test runsReference it from vitest.config.ts via test.setupFiles. The create-wolf-tui scaffolder generates this automatically when --test is enabled.
- Import what you need:
Most
wolf-tuiadapters export these utilities directly from a/testingsubpath (e.g.,@wolf-tui/react/testing) wrapping them seamlessly, but you can also use this library directly if needed.
How It Works
This library provides in-memory implementations of Node.js stream interfaces (NodeJS.WriteStream and NodeJS.ReadStream). When you pass these virtual streams into wolf-tui's render function, it bypasses the real terminal entirely.
MockStdout/MockStderr: Captures rendered frames instead of writing them to the terminal. Provides methods like.lastFrame()to access the most recently rendered output.MockStdin: Simulates terminal input. Useawait stdin.write(sequence)to send keystrokes to your application in tests.stripAnsi: Removes ANSI color and layout escape codes from strings with a single regex — no third-partystrip-ansidependency.KEYS: A collection of common ANSI escape sequences (e.g.,KEYS.UP,KEYS.ENTER) for use withMockStdin.
Reference
MockStdout / MockStderr
constructor(columns?: number, rows?: number):MockStdoutaccepts initial terminal dimensions (default80 × 24).MockStderrtakes no arguments.lastFrame(): string | undefined: Returns the most recent frame output (undefinedif nothing rendered yet).frames: string[]: Array of all captured frames.frameCount(): number: Number of frames captured.getFrame(index: number): string: Returns the frame at a specific index.clear(): void: Clears the captured frames.
MockStdin
constructor(stdout: MockStdout, options?: { isTTY?: boolean }): RequiresMockStdoutto sync rendering events; pass{ isTTY: false }to simulate non-interactive input.write(data: string | Buffer): Promise<void>: Sends a keystroke and resolves oncestdoutemits the resulting frame (or after a 50ms safety timeout if no render happens).- Implements the bare
NodeJS.ReadStreamsurface used by wolf-tui:setRawMode,setEncoding,ref,unref,resume,pause,read, plusdata/readableevents.
stripAnsi(str: string): string
Strips ANSI escape codes from a string.
KEYS
Dictionary containing ANSI sequences for keys:
KEYS.UP,KEYS.DOWN,KEYS.LEFT,KEYS.RIGHTKEYS.ENTER,KEYS.ESC,KEYS.SPACE,KEYS.HOME
delay(ms: number): Promise<void>
Helper for async tests to wait for rendering to settle.
cleanup(): Promise<void>
Unmounts every render instance created via an adapter's /testing render(). Wire it into your test runner once and stop tracking instances by hand:
import { afterEach } from 'vitest'
import { cleanup } from '@wolf-tui/testing-library'
// or: import { cleanup } from '@wolf-tui/react/testing'
afterEach(cleanup)For custom render setups that don't go through an adapter, register manually:
import { registerInstance, unregisterInstance } from '@wolf-tui/testing-library'
const handle = { unmount: () => myInstance.unmount() }
registerInstance(handle)
// later, on manual teardown:
unregisterInstance(handle)Contributing
Please see the monorepo root for contribution guidelines.
License
MIT
