/**
* use-text-input.test.tsx — Tests for the useTextInput hook.
*
* useTextInput is a convenience hook that manages controlled state for a
* TextInput component. It provides value, onChange handler, and a reset method.
*
* Verifies:
* - Hook provides initial value
* - onChange updates the value
* - reset restores initial value
* - Can be used in a component that renders and processes input
*/
import { describe, test, expect, mock } from "bun:test";
import React from "react";
import { render, renderToString, Text, Box } from "ink";
import { useTextInput } from "./use-text-input";
import { TextInput } from "./text-input";
import { PassThrough } from "node:stream";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function createTestStreams() {
const stdout = new PassThrough() as unknown as NodeJS.WriteStream;
(stdout as any).columns = 80;
(stdout as any).rows = 24;
const stdin = new PassThrough() as unknown as NodeJS.ReadStream;
(stdin as any).isTTY = true;
(stdin as any).setRawMode = () => stdin;
(stdin as any).ref = () => stdin;
(stdin as any).unref = () => stdin;
return { stdin, stdout };
}
// ---------------------------------------------------------------------------
// Test harness component
// ---------------------------------------------------------------------------
/** A component that uses the hook and renders the value. */
function TestHarness({
initialValue = "",
onSubmit,
}: {
initialValue?: string;
onSubmit?: (value: string) => void;
}): React.ReactElement {
const { value, onChange, reset } = useTextInput({ initialValue });
return (
{
onSubmit?.(v);
}}
/>
current:{value}
);
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("useTextInput", () => {
test("renders initial value", () => {
const output = renderToString();
expect(output).toContain("hello");
expect(output).toContain("current:hello");
});
test("renders empty initial value", () => {
const output = renderToString();
expect(output).toContain("current:");
});
test("typing updates value via onChange", async () => {
const { stdin, stdout } = createTestStreams();
const chunks: string[] = [];
stdout.on("data", (chunk: Buffer) => chunks.push(chunk.toString()));
const instance = render(, {
stdout,
stdin,
debug: true,
exitOnCtrlC: false,
patchConsole: false,
});
// Type a character
(stdin as any as PassThrough).write("x");
await new Promise((r) => setTimeout(r, 100));
const output = chunks.join("");
// The value should have been updated to include 'x'
expect(output).toContain("current:x");
instance.unmount();
await instance.waitUntilExit();
});
test("submit callback receives current value", async () => {
const onSubmit = mock(() => {});
const { stdin, stdout } = createTestStreams();
const instance = render(
,
{
stdout,
stdin: stdin as any,
debug: true,
exitOnCtrlC: false,
patchConsole: false,
}
);
// Ctrl+S to submit
(stdin as any as PassThrough).write("\x13");
await new Promise((r) => setTimeout(r, 50));
expect(onSubmit).toHaveBeenCalledWith("test value");
instance.unmount();
await instance.waitUntilExit();
});
});