Tooee has two layers of tests: component tests (headless, fast) and e2e tests (full process, slower).
bun test # All tests
bun test packages/layout/test # Layout component tests
bun test packages/renderers/test # Renderer component tests
bun test packages/shell/test # Shell tests only
bun test packages/view/test/e2e # E2E tests onlyUse the shared testRender wrapper from @tooee/test-support. It builds on OpenTUI's headless test renderer. These tests are fast and do not spawn a process.
Location: packages/layout/test/, packages/renderers/test/, packages/shell/test/
| File | Covers |
|---|---|
layout/test/title.bar.test.tsx |
Title and subtitle rendering |
layout/test/status.bar.test.tsx |
Label:value pairs, one row at every width, width snapshots |
layout/test/fit.status.bar.test.ts |
Status bar fitting rules: priority, compact forms, truncation, hidden marker |
layout/test/app.layout.test.tsx |
Full layout chrome (title bar, status bar, scroll area) |
renderers/test/markdown.view.test.tsx |
Heading, list, code block rendering |
renderers/test/code.view.test.tsx |
Code content and line numbers |
renderers/test/table.behavior.test.tsx |
Table rendering |
renderers/test/parsers.test.ts |
Table parser logic |
shell/test/modal.behavior.test.tsx |
j/k scroll, gg/G jump, ctrl+d/u, mode transitions |
shell/test/search.behavior.test.tsx |
findMatchingLines, search activation/cancel/navigation |
shell/test/cursor.behavior.test.tsx |
Cursor/select mode transitions, movement, selection |
shell/test/command-palette.behavior.test.tsx |
Palette open/close, entry filtering |
shell/test/commands.behavior.test.tsx |
Theme cycling (t/T), quit (q) |
choose/test/fuzzy.behavior.test.ts |
Fuzzy filter scoring, matching, sorting |
import { test, expect, afterEach } from "bun:test";
import { testRender } from "@tooee/test-support";
let testSetup: Awaited<ReturnType<typeof testRender>>;
afterEach(() => {
testSetup?.renderer.destroy();
});
test("renders content", async () => {
testSetup = await testRender(<MyComponent />, { width: 80, height: 24 });
await testSetup.renderOnce();
const frame = testSetup.captureCharFrame();
expect(frame).toContain("expected text");
});testSetup.renderer.keyInput.emit("keypress", {
name: "j",
sequence: "j",
ctrl: false,
shift: false,
meta: false,
option: false,
eventType: "press",
repeated: false,
});
await testSetup.renderOnce();Key input must be wrapped in React's act() to flush state updates through the command system.
Several tests use toMatchSnapshot(). Update with:
bun test --update-snapshotsUse tuistory ("Playwright for TUIs") to launch the real CLI binary in a terminal emulator and interact with it.
Location: packages/view/test/e2e/
| File | Covers |
|---|---|
e2e/render.test.ts |
Markdown, code, and plain text rendering; status bar content |
e2e/navigation.test.ts |
j/k scroll, gg jump to top, G jump to bottom, ctrl+d/u |
e2e/theme.test.ts |
Theme cycling via t/T, status bar updates |
e2e/quit.test.ts |
q exits the process cleanly |
e2e/search.test.ts |
Search bar open, query+submit, Escape cancel |
e2e/cursor.test.ts |
Cursor/select mode via v, j/k movement, Escape chains |
Test fixtures live in packages/view/test/fixtures/:
sample.md— markdown with headings, lists, code blockssample.ts— typescript source fileplain.txt— plain textlong.md— 200+ lines for scroll testing
packages/view/test/e2e/helpers.ts provides launchView(fixture) which:
- Launches
bun apps/cli/src/main.ts view <fixture> - Waits for the status bar (
Format:) to confirm the app is ready - Returns a tuistory
Session
import { describe, test, expect, afterEach } from "bun:test";
import { type Session } from "tuistory";
import { launchView } from "./helpers.ts";
let session: Session;
afterEach(() => {
try {
session?.close();
} catch {}
});
test("renders markdown heading", async () => {
session = await launchView("sample.md");
const text = await session.text();
expect(text).toContain("Hello World");
}, 20000);await session.press("j"); // single key
await session.press(["ctrl", "d"]); // chord
await session.type("search query"); // type string
await session.waitForText("pattern"); // wait for text
await session.waitForText(/regex/); // regex match
const text = await session.text(); // get terminal text
session.close(); // cleanupE2E tests spawn real processes. Use generous timeouts (20s per test) to account for process startup.
Name component tests <subject>.<facet>.test.tsx. Name non-JSX tests <subject>.<facet>.test.ts. Keep a multiword subject hyphenated, for example command-palette.mouse.test.tsx.
Import shared test utilities from @tooee/test-support. It exports testRender, expectDefined, keyEvent, the keyboard press* helpers, the clipboard mock, and isolated test config helpers.
- Create
packages/<pkg>/test/<subject>.<facet>.test.tsx - Use
testRenderto render the component with providers - Assert on
captureCharFrame()content or usetoMatchSnapshot()
- Add fixtures to
packages/view/test/fixtures/if needed - Create test file in
packages/view/test/e2e/ - Use
launchView()helper and tuistory assertions - Set 20s timeout on each test
Component tests using testRender emit synthetic key events directly, bypassing stdin parsing. The test renderer should be configured with kitty keyboard mode so that key events match the format the command system expects:
testSetup = await testRender(<MyComponent />, {
width: 80,
height: 24,
kittyKeyboard: true,
})Escape key simulation works reliably in component tests:
testSetup.renderer.keyInput.emit("keypress", {
name: "escape",
sequence: "\x1b",
ctrl: false,
shift: false,
meta: false,
option: false,
eventType: "press",
repeated: false,
});tuistory sends raw \x1b for session.press("escape"), but OpenTUI enables the Kitty keyboard protocol by default and expects Escape encoded as \x1b[27u. The raw \x1b byte is ambiguous — it's also the first byte of every ANSI escape sequence — so OpenTUI either drops it or misparses it.
Workaround: Send the kitty-encoded Escape directly:
// Instead of: await session.press("escape")
await session.writeRaw("\x1b[27u");Affected e2e scenarios (skip or use workaround):
- Exiting search mode (Escape to dismiss search bar)
- Returning from select mode to cursor mode
- Any test that presses Escape
Until tuistory adds Kitty keyboard protocol support, use session.writeRaw("\x1b[27u") for Escape in e2e tests. See the full investigation at phase-2-test-coverage/tuistory-escape-investigation.md.
For component-level command testing, create a harness component that wraps with providers (TooeeProvider or CommandProvider) and renders state as text so you can read it from the frame.
For e2e command testing, read state from the status bar which shows Mode:, Scroll:, Theme:, etc.