Testing
Render apps offscreen, press keys and assert on frames with @textjs/next/testing.
@textjs/next/testing (or @textjs/react/testing) renders your app offscreen, presses keys and returns the frame as text. Tests run without a browser or DevTools.
counter.test.tsxtsx
import { afterEach, expect, test } from "bun:test";import { cleanup, render } from "@textjs/next/testing";import { Counter } from "./counter";afterEach(cleanup);test("counts up", async () => { const app = await render(<Counter />, { cols: 40, rows: 6 }); await app.press("up up"); expect(app.lastFrame()).toContain("count = 2");});It works with any runner that supports async tests, including Bun, Vitest and Jest.
Rendering
| Method | Description |
|---|---|
render(el, { cols, rows, theme }) | Mount offscreen (default 80×24) |
lastFrame() | The frame as plain text |
lines() | The frame as an array of rows |
frames | Every distinct frame so far |
toAnsi() | Truecolor ANSI, for snapshots |
cell(x, y) | { char, fg, bg, attrs } |
locate(text) | { x, y } of the first match |
rerender(el) / unmount() | Update or tear down |
renderToString(el, options) renders once and returns the text.
Input
await app.press("down");await app.press("ctrl+k"); // modifiersawait app.press("g g"); // a sequenceawait app.press("G"); // Shift+Gawait app.type("hello\n"); // one key per character, \n is Enterawait app.paste("multi\nline");await app.wheel(3);await app.resize(120, 36);Every call runs inside act() and flushes the resulting frames, so the next assertion sees the update.
Time
await app.wait(300); // real time passes, then frames flushawait app.waitForText("Deployed"); // poll until the frame contains itawait app.waitFor(() => expect(saved).toBe(true), { timeout: 2000 });Animations, spinners and toasts use real timers, so wait and waitFor are the reliable way to observe them.
Snapshots
expect(app.lastFrame()).toMatchSnapshot();expect(app.toAnsi()).toMatchSnapshot(); // colors tooFrames are trimmed on the right and at the bottom, so snapshots don't churn on whitespace.