/** * shell.test.tsx — Tests for the full Ink shell with input handling and clean exit. * * Verifies: * - Shell renders child components * - Shell handles q key to exit * - Shell handles Ctrl+C to exit * - Shell unmounts cleanly via waitUntilExit * - Shell accepts and renders children */ import { describe, test, expect } from "bun:test"; import React from "react"; import { render, renderToString, Text } from "ink"; import { Shell } from "./shell"; import { PassThrough } from "node:stream"; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /** Create fake stdin/stdout streams for testing Ink render. */ function createTestStreams() { const stdout = new PassThrough() as unknown as NodeJS.WriteStream; // Ink needs .columns on stdout (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 }; } // --------------------------------------------------------------------------- // Static render tests (renderToString) // --------------------------------------------------------------------------- describe("Shell (static rendering)", () => { test("renders children", () => { const output = renderToString( child content ); expect(output).toContain("child content"); }); test("renders header with app name", () => { const output = renderToString(); expect(output).toContain("wombo-combo"); }); test("renders keybind hints", () => { const output = renderToString(); expect(output).toContain("q"); }); }); // --------------------------------------------------------------------------- // Live render tests (render with streams) // --------------------------------------------------------------------------- describe("Shell (live rendering)", () => { test("mounts and unmounts cleanly", async () => { const { stdin, stdout } = createTestStreams(); const instance = render(, { stdout, stdin, debug: true, exitOnCtrlC: false, patchConsole: false, }); // Unmount manually and verify it resolves instance.unmount(); await instance.waitUntilExit(); }); test("exits when q is pressed", async () => { const { stdin, stdout } = createTestStreams(); const instance = render(, { stdout, stdin, debug: true, exitOnCtrlC: false, patchConsole: false, }); // Simulate pressing 'q' (stdin as any as PassThrough).write("q"); // waitUntilExit should resolve because the shell calls exit() on 'q' await instance.waitUntilExit(); }); test("renders child components in live mode", async () => { const { stdin, stdout } = createTestStreams(); const chunks: string[] = []; stdout.on("data", (chunk: Buffer) => { chunks.push(chunk.toString()); }); const instance = render( live child , { stdout, stdin, debug: true, exitOnCtrlC: false, patchConsole: false, } ); // Give it a tick to render await new Promise((resolve) => setTimeout(resolve, 100)); instance.unmount(); await instance.waitUntilExit(); const output = chunks.join(""); expect(output).toContain("live child"); }); });