test: alternate screen teardown-safety and debug-mode tests

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yunfei He
2026-05-27 13:37:15 +08:00
parent 34827b312f
commit 666cbca73a
@@ -1,7 +1,8 @@
import { defineComponent, nextTick } from "vue";
import { defineComponent, nextTick, onScopeDispose } from "vue";
import { expect, test } from "vite-plus/test";
import { createApp, Text } from "@vue-tui/runtime";
import ansiEscapes from "ansi-escapes";
import stripAnsi from "strip-ansi";
import { PassThrough } from "node:stream";
function makeTtyStream(options?: { isTTY?: boolean }) {
@@ -232,3 +233,134 @@ test("alternate screen - cursor restored after exit", async () => {
expect(exitIndex).not.toBe(-1);
expect(showCursorIndex).toBeGreaterThan(exitIndex);
});
test("alternate screen - does not replay exit(Error) output on primary screen", async () => {
const stdout = makeTtyStream();
const stdin = makeFakeStdin();
const ErrorApp = defineComponent(() => {
throw new Error("Done");
});
const app = createApp(ErrorApp);
app.mount({
stdout,
stdin,
stderr: makeTtyStream(),
alternateScreen: true,
interactive: true,
exitOnCtrlC: false,
});
const exited = app.waitUntilExit();
await nextTick();
await nextTick();
await new Promise<void>((r) => setImmediate(r));
await nextTick();
await expect(exited).rejects.toThrow("Done");
const chunks = stdout.chunks;
const exitIndex = chunks.findLastIndex((w) => w.includes(ansiEscapes.exitAlternativeScreen));
expect(exitIndex).not.toBe(-1);
// After exiting the alternate screen, no error content should leak
// to the primary screen (no stack trace, no "Error: Done" text).
const afterExit = stripAnsi(chunks.slice(exitIndex + 1).join(""));
expect(afterExit).not.toContain("Error: Done");
expect(afterExit).not.toContain("Done\n at");
});
test("alternate screen - does not replay teardown output on primary screen", async () => {
const stdout = makeTtyStream();
const stdin = makeFakeStdin();
const TeardownApp = defineComponent(() => () => <Text>fullscreen content</Text>);
const app = createApp(TeardownApp);
app.mount({
stdout,
stdin,
stderr: makeTtyStream(),
alternateScreen: true,
interactive: true,
exitOnCtrlC: false,
});
await nextTick();
const preUnmount = stdout.chunks.join("");
expect(preUnmount).toContain("fullscreen content");
const exited = app.waitUntilExit();
app.unmount();
await exited;
const chunks = stdout.chunks;
const exitIndex = chunks.findLastIndex((w) => w.includes(ansiEscapes.exitAlternativeScreen));
expect(exitIndex).not.toBe(-1);
const afterExit = stripAnsi(chunks.slice(exitIndex + 1).join(""));
expect(afterExit).not.toContain("fullscreen content");
});
test("alternate screen - cleanup console output does not leak into managed stream", async () => {
const stdout = makeTtyStream();
const stdin = makeFakeStdin();
let disposed = false;
const CleanupApp = defineComponent(() => {
onScopeDispose(() => {
disposed = true;
console.log("cleanup log");
});
return () => <Text>Hello</Text>;
});
const app = createApp(CleanupApp);
app.mount({
stdout,
stdin,
stderr: makeTtyStream(),
alternateScreen: true,
interactive: true,
exitOnCtrlC: false,
patchConsole: true,
});
await nextTick();
const exited = app.waitUntilExit();
app.unmount();
await exited;
expect(disposed).toBe(true);
const output = stdout.chunks.join("");
expect(output).not.toContain("cleanup log");
});
test("alternate screen - still activates in debug mode", async () => {
const stdout = makeTtyStream();
const stdin = makeFakeStdin();
const app = createApp(App);
app.mount({
stdout,
stdin,
stderr: makeTtyStream(),
alternateScreen: true,
interactive: true,
debug: true,
exitOnCtrlC: false,
});
await nextTick();
const exited = app.waitUntilExit();
app.unmount();
await exited;
const chunks = stdout.chunks;
const enterIndex = chunks.findIndex((w) => w.includes(ansiEscapes.enterAlternativeScreen));
const exitIndex = chunks.findLastIndex((w) => w.includes(ansiEscapes.exitAlternativeScreen));
expect(enterIndex).not.toBe(-1);
expect(exitIndex).not.toBe(-1);
});