Files
vue-tui/packages/runtime-tests/integration/paint/text-width.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

220 lines
5.7 KiB
TypeScript

import { defineComponent } from "vue";
import { test, expect } from "vite-plus/test";
import stripAnsi from "strip-ansi";
import stringWidth from "string-width";
import { renderToString, Box, Text } from "@vue-tui/runtime";
test("wide characters do not add extra space inside fixed-width Box", () => {
const output = renderToString(
defineComponent(() => () => (
<Box flexDirection="column">
<Box>
<Box width={2}>
<Text>🍔</Text>
</Box>
<Text>|</Text>
</Box>
<Box>
<Box width={2}>
<Text>⏳</Text>
</Box>
<Text>|</Text>
</Box>
</Box>
)),
{ columns: 100 },
);
const lines = output.split("\n");
expect(lines.length).toBe(2);
expect(lines[0]).toBe("🍔|");
expect(lines[1]).toBe("⏳|");
});
test("CJK characters occupy correct width in fixed-width Box", () => {
const output = renderToString(
defineComponent(() => () => (
<Box>
<Box width={4}>
<Text>你好</Text>
</Box>
<Text>|</Text>
</Box>
)),
{ columns: 100 },
);
expect(output).toBe("你好|");
});
test("mixed ASCII and wide characters align correctly", () => {
const output = renderToString(
defineComponent(() => () => (
<Box flexDirection="column">
<Box>
<Box width={6}>
<Text>ab🍔cd</Text>
</Box>
<Text>|</Text>
</Box>
<Box>
<Box width={6}>
<Text>abcdef</Text>
</Box>
<Text>|</Text>
</Box>
</Box>
)),
{ columns: 100 },
);
const lines = output.split("\n");
expect(lines.length).toBe(2);
expect(lines[0]).toBe("ab🍔cd|");
expect(lines[1]).toBe("abcdef|");
});
test("ANSI styled text does not affect layout width", () => {
const output = renderToString(
defineComponent(() => () => (
<Box>
<Box width={5}>
<Text color="red">hello</Text>
</Box>
<Text>|</Text>
</Box>
)),
{ columns: 100 },
);
expect(stripAnsi(output)).toBe("hello|");
});
test("empty Text does not affect sibling layout", () => {
const output = renderToString(
defineComponent(() => () => (
<Box>
<Text />
<Text>hello</Text>
</Box>
)),
{ columns: 100 },
);
expect(output).toBe("hello");
});
test("truncate CJK text at end", () => {
const output = renderToString(
defineComponent(() => () => (
<Box width={20}>
<Text wrap="truncate">あいうえおかきくけこ|end</Text>
</Box>
)),
{ columns: 100 },
);
expect(stringWidth(stripAnsi(output))).toBeLessThanOrEqual(20);
});
test("truncate CJK text in the middle", () => {
const output = renderToString(
defineComponent(() => () => (
<Box width={20}>
<Text wrap="truncate-middle">あいうえおかきくけこ|end</Text>
</Box>
)),
{ columns: 100 },
);
expect(stringWidth(stripAnsi(output))).toBeLessThanOrEqual(20);
});
test("truncate CJK text at start", () => {
const output = renderToString(
defineComponent(() => () => (
<Box width={20}>
<Text wrap="truncate-start">あいうえおかきくけこ|end</Text>
</Box>
)),
{ columns: 100 },
);
expect(stringWidth(stripAnsi(output))).toBeLessThanOrEqual(20);
});
test("truncate CJK text does not exceed Box width", () => {
const output = renderToString(
defineComponent(() => () => (
<Box>
<Box width={20}>
<Text wrap="truncate">あいうえおかきくけこ|end</Text>
</Box>
<Text>|</Text>
</Box>
)),
{ columns: 100 },
);
const lines = output.split("\n");
expect(lines.length).toBe(1);
expect(stripAnsi(lines[0]!).endsWith("|")).toBe(true);
});
test("overlay on 2nd cell of CJK character clears the full character", () => {
const output = renderToString(
defineComponent(() => () => (
<Box width={20} height={1}>
<Text>あいうえおかきくけこ</Text>
<Box position="absolute" left={9}>
<Text>XYZ</Text>
</Box>
</Box>
)),
{ columns: 20 },
);
const lines = output.split("\n");
expect(stringWidth(lines[0]!)).toBe(20);
expect(stripAnsi(lines[0]!)).toBe("あいうえ XYZきくけこ");
});
test("overlay on 1st cell of CJK character clears trailing placeholder", () => {
const output = renderToString(
defineComponent(() => () => (
<Box width={20} height={1}>
<Text>あいうえおかきくけこ</Text>
<Box position="absolute" left={10}>
<Text>X</Text>
</Box>
</Box>
)),
{ columns: 20 },
);
const lines = output.split("\n");
expect(stringWidth(lines[0]!)).toBe(20);
expect(stripAnsi(lines[0]!)).toBe("あいうえおX きくけこ");
});
test("CJK overlay on 2nd cell of CJK clears both sides", () => {
const output = renderToString(
defineComponent(() => () => (
<Box width={20} height={1}>
<Text>あいうえおかきくけこ</Text>
<Box position="absolute" left={5}>
<Text>漢字テスト</Text>
</Box>
</Box>
)),
{ columns: 20 },
);
const lines = output.split("\n");
expect(stringWidth(lines[0]!)).toBe(20);
expect(stripAnsi(lines[0]!)).toBe("あい 漢字テスト けこ");
});
test("clipped empty write does not corrupt existing wide characters", () => {
const output = renderToString(
defineComponent(() => () => (
<Box width={4} height={1} overflowX="hidden">
<Text>あい</Text>
<Box position="absolute" left={-1} width={1}>
<Text>Z</Text>
</Box>
</Box>
)),
{ columns: 4 },
);
expect(stripAnsi(output)).toBe("あい");
});