2026-05-27 01:03:40 +08:00
|
|
|
import { test as it, expect } from "vite-plus/test";
|
2026-06-01 03:07:35 +08:00
|
|
|
import stripAnsi from "strip-ansi";
|
2026-05-26 22:31:32 +08:00
|
|
|
import { run } from "./helpers/run.ts";
|
|
|
|
|
import term from "./helpers/term.ts";
|
|
|
|
|
|
2026-05-27 01:03:40 +08:00
|
|
|
it("exit normally without unmount() or exit()", async () => {
|
2026-05-26 22:31:32 +08:00
|
|
|
const output = await run("exit-normally");
|
|
|
|
|
expect(output).toContain("exited");
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-27 01:03:40 +08:00
|
|
|
it("exit on unmount()", async () => {
|
2026-05-26 22:31:32 +08:00
|
|
|
const output = await run("exit-on-unmount");
|
|
|
|
|
expect(output).toContain("exited");
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-27 01:03:40 +08:00
|
|
|
it("exit when app finishes execution", async () => {
|
2026-05-26 22:31:32 +08:00
|
|
|
await expect(run("exit-on-finish")).resolves.toBeDefined();
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-27 01:03:40 +08:00
|
|
|
it("exit on exit()", async () => {
|
2026-05-26 22:31:32 +08:00
|
|
|
const output = await run("exit-on-exit");
|
|
|
|
|
expect(output).toContain("exited");
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-27 01:03:40 +08:00
|
|
|
it("exit on exit() with error", async () => {
|
2026-05-26 22:31:32 +08:00
|
|
|
const output = await run("exit-on-exit-with-error");
|
|
|
|
|
expect(output).toContain("errored");
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-27 01:03:40 +08:00
|
|
|
it("exit on exit() with error with value property", async () => {
|
2026-05-26 22:31:32 +08:00
|
|
|
const output = await run("exit-on-exit-with-error-value-property");
|
|
|
|
|
expect(output).toContain("errored");
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-27 01:03:40 +08:00
|
|
|
it("exit on exit() with result value", async () => {
|
2026-05-26 22:31:32 +08:00
|
|
|
const output = await run("exit-on-exit-with-result");
|
|
|
|
|
expect(output).toContain("result:hello from vue-tui");
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-27 01:03:40 +08:00
|
|
|
it("exit on exit() with object result", async () => {
|
2026-05-26 22:31:32 +08:00
|
|
|
const output = await run("exit-on-exit-with-value-object");
|
|
|
|
|
expect(output).toContain("result:hello from vue-tui object");
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-27 01:03:40 +08:00
|
|
|
it("exit on exit() with raw mode", async () => {
|
2026-05-26 22:31:32 +08:00
|
|
|
const output = await run("exit-raw-on-exit");
|
|
|
|
|
expect(output).toContain("exited");
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-27 01:03:40 +08:00
|
|
|
it("exit on exit() with raw mode with error", async () => {
|
2026-05-26 22:31:32 +08:00
|
|
|
const output = await run("exit-raw-on-exit-with-error");
|
|
|
|
|
expect(output).toContain("errored");
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-27 01:03:40 +08:00
|
|
|
it("exit on unmount() with raw mode", async () => {
|
2026-05-26 22:31:32 +08:00
|
|
|
const output = await run("exit-raw-on-unmount");
|
|
|
|
|
expect(output).toContain("exited");
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-27 01:03:40 +08:00
|
|
|
it("exit with thrown error", async () => {
|
2026-06-01 03:07:35 +08:00
|
|
|
// run() rejects on a non-zero exit code; the fixture catches the rejected
|
|
|
|
|
// waitUntilExit, logs "errored", and exits 0 deterministically (Ink exit.tsx:71-74).
|
|
|
|
|
const output = await run("exit-with-thrown-error");
|
|
|
|
|
expect(output).toContain("errored");
|
2026-05-26 22:31:32 +08:00
|
|
|
});
|
|
|
|
|
|
2026-05-27 01:03:40 +08:00
|
|
|
it("don't exit while raw mode is active", async () => {
|
2026-06-01 03:07:35 +08:00
|
|
|
// Port of Ink exit.tsx:100-114 ("don't exit while raw mode is active"). The
|
|
|
|
|
// fixture keeps raw mode enabled and writes __READY__ after 500ms. With raw
|
|
|
|
|
// mode active and no input, the process must STAY ALIVE — Node's keep-alive
|
|
|
|
|
// (the raw-mode stdin handle) blocks exit. We wait ~500ms with NO input and
|
|
|
|
|
// confirm it has not exited, THEN send 'q' to trigger unmount + exit.
|
2026-05-27 01:03:40 +08:00
|
|
|
const ps = term("exit-double-raw-mode");
|
2026-06-01 03:07:35 +08:00
|
|
|
|
|
|
|
|
// After __READY__ (resolved internally by the term helper), wait 500ms and
|
|
|
|
|
// ensure the process has NOT exited (no input has been sent yet).
|
|
|
|
|
const exitedDuringWait = await Promise.race([
|
|
|
|
|
ps.waitForExitInfo().then(() => true),
|
|
|
|
|
new Promise<false>((resolve) => setTimeout(() => resolve(false), 500)),
|
|
|
|
|
]);
|
|
|
|
|
expect(exitedDuringWait).toBe(false);
|
|
|
|
|
|
|
|
|
|
// Now send 'q': the fixture unmounts and the process exits cleanly.
|
2026-05-27 01:03:40 +08:00
|
|
|
ps.write("q");
|
|
|
|
|
await ps.waitForExit();
|
|
|
|
|
expect(ps.output).toContain("exited");
|
|
|
|
|
});
|
2026-05-26 22:31:32 +08:00
|
|
|
|
2026-06-01 17:00:09 +08:00
|
|
|
it("rawMode 'always' (default): a no-input app stays alive and exits on Ctrl+C", async () => {
|
|
|
|
|
// The default rawMode 'always' holds raw mode for the whole interactive run, so
|
|
|
|
|
// even a no-input app (no useInput, no stdin listener) does NOT auto-exit — the
|
|
|
|
|
// lifetime raw-mode ref keeps the loop alive. (Under 'auto' the same app exits
|
|
|
|
|
// immediately; cf. exit-normally, pinned to 'auto'.) Ctrl+C still exits it
|
|
|
|
|
// cleanly because raw mode is held and exitOnCtrlC defaults to true — the
|
|
|
|
|
// headline benefit that Ctrl+C works on a no-input screen.
|
|
|
|
|
const ps = term("exit-rawmode-always");
|
|
|
|
|
|
|
|
|
|
const exitedDuringWait = await Promise.race([
|
|
|
|
|
ps.waitForExitInfo().then(() => true),
|
|
|
|
|
new Promise<false>((resolve) => setTimeout(() => resolve(false), 500)),
|
|
|
|
|
]);
|
|
|
|
|
expect(exitedDuringWait).toBe(false);
|
|
|
|
|
|
|
|
|
|
ps.write("\x03"); // Ctrl+C
|
|
|
|
|
await ps.waitForExit();
|
|
|
|
|
expect(ps.output).toContain("exited");
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-01 03:07:35 +08:00
|
|
|
it("exit when DEV is set", async () => {
|
|
|
|
|
// Port of Ink exit.tsx:133-142 ("exit when DEV is set"). DEV is inert in
|
|
|
|
|
// vue-tui (no React DevTools hookup), so exit-normally must still exit cleanly.
|
|
|
|
|
const output = await run("exit-normally", { env: { DEV: "true" } });
|
|
|
|
|
expect(output).toContain("exited");
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-27 01:03:40 +08:00
|
|
|
it("exit on exit() with error and static output", async () => {
|
2026-05-26 22:31:32 +08:00
|
|
|
const output = await run("exit-with-static");
|
|
|
|
|
expect(output).toContain("errored");
|
2026-06-01 03:07:35 +08:00
|
|
|
|
|
|
|
|
// Static items A/B/C must each render EXACTLY once — not duplicated (Ink #397,
|
|
|
|
|
// exit.tsx:144-155). With the fixture's function slots there are no Vue warns
|
|
|
|
|
// polluting stdout, so the body lines are clean single occurrences.
|
|
|
|
|
const lines = stripAnsi(output).split(/\r?\n/);
|
|
|
|
|
expect(lines.filter((line) => line === "A")).toHaveLength(1);
|
|
|
|
|
expect(lines.filter((line) => line === "B")).toHaveLength(1);
|
|
|
|
|
expect(lines.filter((line) => line === "C")).toHaveLength(1);
|
2026-05-26 22:31:32 +08:00
|
|
|
});
|