Files
vue-tui/packages/runtime-tests/integration/composables/raw-mode-unsupported.test.tsx
T
Yunfei He 33055e429c fix(runtime): throw a descriptive error when raw mode is unsupported (Ink parity, G10) (#40)
* fix(runtime): throw a descriptive error when raw mode is unsupported (Ink parity, G10)

Previously the raw-mode acquire path silently no-opped on a stdin where raw mode
is unsupported (non-TTY / isRawModeSupported false), so using useInput on such a
stdin did nothing with no diagnostic. Ink's handleSetRawMode (App.tsx:315-327)
instead throws immediately when enabling raw mode is unsupported, with two
distinct messages (default process.stdin vs a custom stdin) both pointing at the
isRawModeSupported docs.

StdinController.acquireRawMode now throws that two-message error on
!isRawModeSupported. The unguarded useInput path surfaces it (matching Ink's
use-input.ts, which calls setRawMode(true) ungated). useFocus now guards on
isRawModeSupported before acquiring (matching Ink's use-focus.ts), so focus
degrades to a safe no-op on a non-TTY rather than throwing. releaseRawMode keeps
its no-op guard — Ink only throws when enabling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(parity): ledger — G10 pr-open, reconcile G09 merged

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(runtime): assert full raw-mode error message for exact Ink parity (G10, codex)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 03:07:32 +08:00

90 lines
3.1 KiB
TypeScript

import { PassThrough } from "node:stream";
import { nextTick, defineComponent } from "vue";
import { expect, test } from "vite-plus/test";
import { createApp, Text, useFocus, useInput } from "@vue-tui/runtime";
import { makeFakeWritable } from "../lifecycle/test-streams.ts";
// Builds a stdin that is NOT a TTY, so isRawModeSupported is false. This mirrors
// piping input into a program (e.g. `echo x | node app.js`) where raw mode can't
// be enabled. Matches Ink's isRawModeSupported = stdin.isTTY check.
function makeNonTtyStdin(): NodeJS.ReadStream {
const s = new PassThrough() as unknown as NodeJS.ReadStream;
Object.assign(s, {
isTTY: false,
setEncoding(this: NodeJS.ReadStream) {
return this;
},
});
(s as { ref?: () => void }).ref = () => {};
(s as { unref?: () => void }).unref = () => {};
return s;
}
// Mounts a component against a non-TTY stdin and resolves with any error that
// surfaces through the app's exit promise (the error-boundary → exit path the
// testing render() helper relies on), or undefined if it mounts cleanly.
async function mountNonTtyAndCaptureError(component: Parameters<typeof createApp>[0]): Promise<{
error: Error | undefined;
unmount: () => void;
}> {
const stdout = makeFakeWritable();
const stdin = makeNonTtyStdin();
const app = createApp(component);
let error: Error | undefined;
app.waitUntilExit().catch((e) => {
error = e as Error;
});
try {
app.mount({ stdout, stdin, debug: true, exitOnCtrlC: false });
} catch (e) {
error = e as Error;
}
// Flush the Vue queue so the error boundary → nextTick → exit → reject chain runs.
await nextTick();
await nextTick();
await Promise.resolve();
await Promise.resolve();
await new Promise<void>((r) => setImmediate(r));
await Promise.resolve();
return { error, unmount: () => app.unmount() };
}
// Test A: useInput on an unsupported stdin must surface Ink's descriptive error.
test("useInput on a non-TTY stdin throws a descriptive raw-mode error", async () => {
const App = defineComponent(() => {
useInput(() => {});
return () => <Text>listening</Text>;
});
const { error, unmount } = await mountNonTtyAndCaptureError(App);
unmount();
expect(error).toBeInstanceOf(Error);
// Assert the FULL custom-stdin message (a non-process.stdin PassThrough hits the
// "provided to Vue TUI" branch), so exact two-line parity with Ink's wording +
// docs URL can't silently regress (mirrors Ink App.tsx:323-325).
expect(error?.message).toBe(
"Raw mode is not supported on the stdin provided to Vue TUI.\n" +
"Read about how to prevent this error on https://github.com/vadimdemedes/ink/#israwmodesupported",
);
});
// Test B (regression): useFocus must guard like Ink's use-focus.ts and NOT throw
// on a non-TTY stdin. This guards against over-throwing in the acquire chokepoint.
test("useFocus on a non-TTY stdin does not throw (graceful no-op)", async () => {
const App = defineComponent(() => {
useFocus();
return () => <Text>focusable</Text>;
});
const { error, unmount } = await mountNonTtyAndCaptureError(App);
unmount();
expect(error).toBeUndefined();
});