7707322382
* fix(runtime): align debug-mode unmount byte stream to Ink In DEBUG mode (non-interactive) vue-tui's teardown emitted neither a final-frame re-emit nor a trailing newline, while Ink emits both (ink.tsx:749-762 settleThrottle re-emit + ink.tsx:812-819 `debug ? '\n' : lastOutput + '\n'`). So for a debug app that renders "Hello" once, Ink's byte stream is "HelloHello\n" but vue-tui's was just "Hello" — a divergence that matters when porting Ink debug snapshots / CI logs. - Fire the final-frame re-emit `mountedCommit()` for debug too (was interactive-only). - In the non-interactive teardown write, emit a bare "\n" for debug (Ink parity), keeping the non-debug `lastFrame + "\n"` branch byte-identical. Because the @vue-tui/testing render() helper captures debug commits via an internal frame sink, gate the debug commit's two `frameSink?.(...)` forwards on `!teardownStarted` so the teardown re-emit (a stdout byte-parity FLUSH, not a render) does not append a spurious entry to the helper's live `frames[]`. `teardownStarted` is set at the top of teardown() before the re-emit, so this covers EVERY teardown route (unmount / cleanup / exit / Ctrl+C / signal / process.exit). Both `stdout.write` calls stay unconditional, preserving byte parity. Adds a PTY byte-parity test (asserts "HelloHello\r\n") and a testing-helper test covering all teardown routes (frames.length stable, incl. <Static>). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: drop CI-fragile exitOnCtrlC frames-teardown case The Ctrl+C case timed out in CI (waitUntilExit never resolved — stdin/raw-mode timing is environment-fragile), while passing locally. Ctrl+C routes through the SAME exit-driven teardown path as programmatic useApp().exit() (emitInput → appContext.exit() → teardown()), which the remaining exit() cases already cover, so removing it loses no teardown-route coverage of the !teardownStarted frame-sink gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vue-tui/testing
Early stage — under active development. Bug reports welcome, but not recommended for production use yet.
Test harness for vue-tui — render Vue 3 terminal components, simulate input, assert frames. Like @testing-library, but for the terminal.
Why
- Isolated terminal — renders into a fake TTY, no real terminal needed
- Input simulation — inject keystrokes that reach
useInputhandlers - Frame snapshots — assert exact visual output with
lastFrame()andframes[] - Auto-cleanup — unmounts all rendered apps after each test (requires Vitest
globals: true)
Install
Assumes @vue-tui/runtime and vue are already installed in your project.
npm install -D @vue-tui/testing
Quick Start
import { defineComponent, shallowRef } from "vue";
import { expect, test } from "vitest";
import { render } from "@vue-tui/testing";
import { Box, Text, useInput } from "@vue-tui/runtime";
test("counter responds to + and - keys", async () => {
const Counter = defineComponent(() => {
const count = shallowRef(0);
useInput((input) => {
if (input === "+") count.value++;
if (input === "-") count.value--;
});
return () => (
<Box>
<Text>Count: {count.value}</Text>
</Box>
);
});
const { lastFrame, stdin } = await render(Counter);
expect(lastFrame()).toContain("Count: 0");
await stdin.write("+");
expect(lastFrame()).toContain("Count: 1");
await stdin.write("-");
expect(lastFrame()).toContain("Count: 0");
});
API
render(component, options?)
Mounts a component in a fake terminal environment. Returns a RenderResult.
| Option | Type | Default | Description |
|---|---|---|---|
columns |
number |
100 |
Terminal width in columns |
rows |
number |
100 |
Terminal height in rows |
props |
object |
— | Props passed to the root component |
exitOnCtrlC |
boolean |
false |
Enable Ctrl+C exit handling |
RenderResult
| Property / Method | Description |
|---|---|
lastFrame(opts?) |
Latest rendered frame as a string |
frames |
Array of all captured frame snapshots |
stdin.write(data) |
Inject input (reaches useInput handlers) |
terminal |
Fake terminal — columns, rows, resize(), rawMode |
unmount() |
Tear down the app |
waitUntilExit() |
Settles when the app exits (rejects if exit(error)) |
waitUntilRenderFlush() |
Resolves after the next render cycle completes |
cleanup()
Unmounts all rendered apps. Auto-registered as a Vitest afterEach hook when globals: true is set. Call manually if your test runner doesn't expose a global afterEach.
Links
- vue-tui — monorepo root
@vue-tui/runtime— the core framework@vue-tui/cli— dev server with HMR
License
MIT