Files
vue-tui/packages/runtime-tests/integration/pty/console.test.ts
T
Yunfei He 7d6a92ea15 test(runtime): lock render lifecycle, exit, error-handling, alt-screen, use-stdout (Ink parity) (#115)
Round-2 test-only locks (behaviors already at parity with Ink test/render.tsx,
exit.tsx, errors.tsx, hooks.tsx):
- resize re-render reflows content to the new width (exact narrowed round-border bytes)
  + consecutive width decreases each clear; onRender fires exactly once per rerender
  (tightened from `>`); onMounted runs before the first frame write callback (#596);
  bsu/esu wraps a trailing throttled content change, and an unchanged trailing rerender
  emits neither; patchConsole puts a log above the live frame.
- exit-with-static #397 non-duplication (A/B/C each render once) — the fixture is fixed
  to function slots so no [Vue warn] pollutes stdout; exit-with-thrown-error via run()'s
  strict exit-0 gate; DEV is inert; the process stays alive ~500ms while raw mode is held.
- raw mode is disabled on the thrown-error cleanup path; the unhandledRejection test moved
  to a *.sequential file (it mutates a process-global listener).
- alternate-screen enter sequence hides the cursor; useStdout().write() preserves the
  frame with the external write ordered above it.

Codex-reviewed GENUINE.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 03:07:35 +08:00

55 lines
2.6 KiB
TypeScript

import { test as it, expect } from "vite-plus/test";
import ansiEscapes from "ansi-escapes";
import stripAnsi from "strip-ansi";
import { run } from "./helpers/run.ts";
it("console.log doesn't corrupt output", async () => {
const output = await run("console");
expect(output).toContain("First log");
expect(output).toContain("Second log");
});
// Port of Ink render.tsx:797-812 ("intercept console methods and display result
// above output"): a console.log emitted WHILE a frame is on screen is hoisted
// ABOVE the frame (the frame is erased, the log printed, the frame re-rendered).
// Splitting the raw bytes on eraseLines(2) — the exact erase the patched console
// writes to lift the one-line "Hello World" frame — isolates the pre-frame text
// ("First log", logged while mounted) from the post-frame text ("Second log",
// logged after unmount, below the final frame).
it("patched console.log appears above the on-screen frame", async () => {
const output = await run("console");
const frames = output.split(ansiEscapes.eraseLines(2)).map((line) => stripAnsi(line));
// First segment is the initial frame; the second is the post-log re-render,
// with "First log" hoisted above the re-rendered frame and "Second log" below.
expect(frames).toEqual(["Hello World\r\n", "First log\r\nHello World\r\nSecond log\r\n"]);
// Ordering is explicit: First log precedes the (re-rendered) frame, which
// precedes Second log.
const firstIdx = frames[1]!.indexOf("First log");
const frameIdx = frames[1]!.indexOf("Hello World");
const secondIdx = frames[1]!.indexOf("Second log");
expect(firstIdx).toBeGreaterThanOrEqual(0);
expect(firstIdx).toBeLessThan(frameIdx);
expect(frameIdx).toBeLessThan(secondIdx);
});
it("useStdout.write in real terminal", async () => {
const output = await run("use-stdout");
expect(output).toContain("Hello from vue-tui to stdout");
expect(output).toContain("exited");
});
// Port of Ink hooks.tsx:92-103 ("useStdout - write to stdout"): an external
// useStdout().write while a frame is on screen is hoisted ABOVE the preserved
// frame. After stripping ANSI and splitting on \r\n, the inner lines (dropping
// the leading initial-frame line and the trailing empty segment) are EXACTLY
// the external write, the preserved frame, and the post-exit log — in order.
it("useStdout.write appears above the preserved frame (exact ordering)", async () => {
const output = await run("use-stdout");
const lines = stripAnsi(output).split("\r\n");
expect(lines.slice(1, -1)).toEqual(["Hello from vue-tui to stdout", "Hello World", "exited"]);
});