import { defineComponent, shallowRef, nextTick } from "vue";
import { test as it, expect } from "vite-plus/test";
import ansiEscapes from "ansi-escapes";
import { createApp, Text } from "@vue-tui/runtime";
import { makeFakeWritable, makeFakeStdin, captureWrites } from "../lifecycle/test-streams.ts";
function makeFakeNonTtyWritable(rows = 6): NodeJS.WriteStream {
const s = makeFakeWritable({ rows });
(s as any).isTTY = false;
return s;
}
it("#450: non-TTY full-height rerenders should never clear terminal", async () => {
const stdout = makeFakeNonTtyWritable(6);
const stderr = makeFakeWritable();
const { stream: stdin } = makeFakeStdin();
const writes = captureWrites(stdout);
const msg = shallowRef("line1\nline2\nline3\nline4\nline5\nline6");
const App = defineComponent(() => () => {msg.value});
const app = createApp(App);
app.mount({ stdout, stdin, stderr, exitOnCtrlC: false });
await nextTick();
await nextTick();
msg.value = "line1\nline2\nline3\nline4\nline5\nLINE6";
await nextTick();
await nextTick();
app.unmount();
const clearCount = writes.filter((w) => w.includes(ansiEscapes.clearTerminal)).length;
expect(clearCount).toBe(0);
});
it("#450: non-TTY overflow transitions should never clear terminal", async () => {
const stdout = makeFakeNonTtyWritable(6);
const stderr = makeFakeWritable();
const { stream: stdin } = makeFakeStdin();
const writes = captureWrites(stdout);
const msg = shallowRef("line1\nline2\nline3");
const App = defineComponent(() => () => {msg.value});
const app = createApp(App);
app.mount({ stdout, stdin, stderr, exitOnCtrlC: false });
await nextTick();
await nextTick();
msg.value = "line1\nline2\nline3\nline4\nline5\nline6\nline7";
await nextTick();
await nextTick();
app.unmount();
const clearCount = writes.filter((w) => w.includes(ansiEscapes.clearTerminal)).length;
expect(clearCount).toBe(0);
});
it("#450: viewport shrink into overflow clears exactly once on resize", async () => {
const stdout = makeFakeWritable({ rows: 10 });
const stderr = makeFakeWritable();
const { stream: stdin } = makeFakeStdin();
const writes = captureWrites(stdout);
const msg = shallowRef("line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8");
const App = defineComponent(() => () => {msg.value});
const app = createApp(App);
app.mount({ stdout, stdin, stderr, exitOnCtrlC: false });
await nextTick();
await nextTick();
const clearsBeforeResize = writes.filter((w) => w.includes(ansiEscapes.clearTerminal)).length;
expect(clearsBeforeResize).toBe(0); // 8 lines fit in 10 rows — no clear yet
// Shrink so the content overflows the viewport. The resize renders
// synchronously (matching Ink), so the overflow clear is emitted right here,
// not deferred through the commit throttle.
stdout.rows = 4;
stdout.emit("resize");
await nextTick();
const clearsAfterResize = writes.filter((w) => w.includes(ansiEscapes.clearTerminal)).length;
expect(clearsAfterResize - clearsBeforeResize).toBe(1);
app.unmount();
});
it("#450: non-TTY grow-to-overflow rerender should not clear terminal", async () => {
const { spawn } = await import("node:child_process");
const fixturePath = new URL("./fixtures/issue-450-grow-to-overflow-rerender.tsx", import.meta.url)
.pathname;
const output = await new Promise((resolve, reject) => {
let stdout = "";
let stderr = "";
const child = spawn("node", ["--import=tsx", fixturePath, "3"], {
cwd: new URL("./fixtures", import.meta.url).pathname,
env: {
...process.env,
NODE_NO_WARNINGS: "1",
CI: "false",
FORCE_COLOR: "3",
},
stdio: ["ignore", "pipe", "pipe"],
});
child.stdout.on("data", (data: Buffer) => {
stdout += data.toString();
});
child.stderr.on("data", (data: Buffer) => {
stderr += data.toString();
});
child.on("error", reject);
child.on("close", (code) => {
if (code === 0) {
resolve(stdout);
} else {
reject(new Error(`Fixture exited with code ${code}: ${stderr}`));
}
});
setTimeout(() => {
child.kill();
reject(new Error("Fixture timed out"));
}, 10000);
});
const clearCount = output.split(ansiEscapes.clearTerminal).length - 1;
expect(clearCount).toBe(0);
});