diff --git a/packages/runtime-tests/integration/pty/render.test.ts b/packages/runtime-tests/integration/pty/render.test.ts index aabe270..c2a65c3 100644 --- a/packages/runtime-tests/integration/pty/render.test.ts +++ b/packages/runtime-tests/integration/pty/render.test.ts @@ -51,6 +51,26 @@ it("do not erase screen where is taller than viewport", async () => { } }); +it("erase screen (content overflows viewport)", async () => { + const ps = term("erase", ["3"]); + await ps.waitForExit(); + expect(ps.output).toContain(ansiEscapes.clearTerminal); + + for (const letter of ["A", "B", "C"]) { + expect(ps.output).toContain(letter); + } +}); + +it("erase screen where exists but interactive part is taller than viewport", async () => { + const ps = term("erase-with-static", ["3"]); + await ps.waitForExit(); + expect(ps.output).toContain(ansiEscapes.clearTerminal); + + for (const letter of ["A", "B", "C", "D", "E", "F"]) { + expect(ps.output).toContain(letter); + } +}); + it("erase screen where state changes", async () => { const ps = term("erase-with-state-change", ["4"]); await ps.waitForExit(); diff --git a/packages/runtime/src/render.ts b/packages/runtime/src/render.ts index 7aa8e8e..755204a 100644 --- a/packages/runtime/src/render.ts +++ b/packages/runtime/src/render.ts @@ -150,6 +150,7 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp let mountedGetLastOutput: (() => string) | null = null; let mountedRestoreConsole: (() => void) | null = null; let mountedScheduler: ReturnType | null = null; + let mountedCommit: (() => void) | null = null; // The renderer's onCommit closure is wired at createApp time but only does // real work after mount swaps in scheduler.schedule. One renderer per app @@ -185,6 +186,14 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp function teardown() { if (teardownStarted) return; teardownStarted = true; + + // Final render before unmount (matching Ink ink.tsx:755-761). + // teardownStarted=true makes shouldClearTerminalForFrame see isUnmounting, + // so fullscreen apps get clearTerminal on exit. + if (mountedInteractive && !mountedDebug && mountedCommit) { + mountedCommit(); + } + scheduledCommit = () => {}; // Restore console BEFORE Vue cleanup (matching Ink ink.tsx:779) if (mountedRestoreConsole) { @@ -523,6 +532,7 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp } const scheduler = createCommitScheduler(commit, schedulerOptions); mountedScheduler = scheduler; + mountedCommit = commit; scheduledCommit = scheduler.schedule; // Internal provides — set before the actual mount so components can inject