Files
vue-tui/packages/runtime-tests/integration/pty/issue-450-inline.test.tsx
T
Yunfei He 1ffb847a65 feat: test parity final push — 64 new tests, BSU/ESU, clear() API
Closes remaining test gaps between vue-tui and Ink:

Features:
- Add synchronized output (BSU/ESU) via DEC private mode 2026
- Add clear() API to TuiApp for erasing rendered output

Bug fixes:
- Cancel scheduler trailing timer on teardown (prevents stale commits)
- Guard teardown writes against ended/destroyed streams
- Reorder teardown to cancel timer before final commit

Tests (64 new, 2 skipped for known feature gaps):
- 7 BSU/ESU shouldSynchronize tests
- 5 borderBackgroundColor tests
- 13 component edge cases (empty text, number child, OSC hyperlink
  wrap-width, bare-text-in-Box validation, transform multi-line,
  leading whitespace, link escape closing)
- 13 text-width/CJK tests (alignment, truncation, overlay edge cases)
- 4 throttle + unmount edge cases
- 10 waitUntilRenderFlush write-callback-level tests + 1 clear() test
- 3 exit re-entrance tests
- 5 PTY #450 regression tests + 4 inline #450 tests

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:18:23 +08:00

129 lines
3.9 KiB
TypeScript

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(() => () => <Text>{msg.value}</Text>);
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(() => () => <Text>{msg.value}</Text>);
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 should clear once", 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(() => () => <Text>{msg.value}</Text>);
const app = createApp(App);
app.mount({ stdout, stdin, stderr, exitOnCtrlC: false });
await nextTick();
await nextTick();
stdout.rows = 4;
stdout.emit("resize");
await nextTick();
await nextTick();
app.unmount();
const clearCount = writes.filter((w) => w.includes(ansiEscapes.clearTerminal)).length;
expect(clearCount).toBe(1);
});
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<string>((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);
});