From 666cbca73a7fea2e42781b412f3aed70d8e22ae4 Mon Sep 17 00:00:00 2001 From: Yunfei He Date: Wed, 27 May 2026 13:37:15 +0800 Subject: [PATCH] test: alternate screen teardown-safety and debug-mode tests Co-Authored-By: Claude Opus 4.7 (1M context) --- .../lifecycle/alternate-screen.test.tsx | 134 +++++++++++++++++- 1 file changed, 133 insertions(+), 1 deletion(-) diff --git a/packages/runtime-tests/integration/lifecycle/alternate-screen.test.tsx b/packages/runtime-tests/integration/lifecycle/alternate-screen.test.tsx index 23eb93e..c4b075d 100644 --- a/packages/runtime-tests/integration/lifecycle/alternate-screen.test.tsx +++ b/packages/runtime-tests/integration/lifecycle/alternate-screen.test.tsx @@ -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((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(() => () => fullscreen content); + + 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 () => Hello; + }); + + 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); +});