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(() => () => ( ๐Ÿ” | โณ | )), { 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(() => () => ( ไฝ ๅฅฝ | )), { columns: 100 }, ); expect(output).toBe("ไฝ ๅฅฝ|"); }); test("mixed ASCII and wide characters align correctly", () => { const output = renderToString( defineComponent(() => () => ( ab๐Ÿ”cd | abcdef | )), { 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(() => () => ( hello | )), { columns: 100 }, ); expect(stripAnsi(output)).toBe("hello|"); }); test("empty Text does not affect sibling layout", () => { const output = renderToString( defineComponent(() => () => ( hello )), { columns: 100 }, ); expect(output).toBe("hello"); }); test("truncate CJK text at end", () => { const output = renderToString( defineComponent(() => () => ( ใ‚ใ„ใ†ใˆใŠใ‹ใใใ‘ใ“|end )), { columns: 100 }, ); expect(stringWidth(stripAnsi(output))).toBeLessThanOrEqual(20); }); test("truncate CJK text in the middle", () => { const output = renderToString( defineComponent(() => () => ( ใ‚ใ„ใ†ใˆใŠใ‹ใใใ‘ใ“|end )), { columns: 100 }, ); expect(stringWidth(stripAnsi(output))).toBeLessThanOrEqual(20); }); test("truncate CJK text at start", () => { const output = renderToString( defineComponent(() => () => ( ใ‚ใ„ใ†ใˆใŠใ‹ใใใ‘ใ“|end )), { columns: 100 }, ); expect(stringWidth(stripAnsi(output))).toBeLessThanOrEqual(20); }); test("truncate CJK text does not exceed Box width", () => { const output = renderToString( defineComponent(() => () => ( ใ‚ใ„ใ†ใˆใŠใ‹ใใใ‘ใ“|end | )), { 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(() => () => ( ใ‚ใ„ใ†ใˆใŠใ‹ใใใ‘ใ“ XYZ )), { 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(() => () => ( ใ‚ใ„ใ†ใˆใŠใ‹ใใใ‘ใ“ X )), { 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(() => () => ( ใ‚ใ„ใ†ใˆใŠใ‹ใใใ‘ใ“ ๆผขๅญ—ใƒ†ใ‚นใƒˆ )), { 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(() => () => ( ใ‚ใ„ Z )), { columns: 4 }, ); expect(stripAnsi(output)).toBe("ใ‚ใ„"); }); // A wide char whose LEADING cell is in-bounds but TRAILING cell exceeds the // terminal/box width must still render its leading cell and OVERFLOW the row โ€” // matching Ink, which has no x-bounds check in its Output write loop. The // past-width trailing placeholder is dropped later by line.filter + trimEnd. // Box width 4 (== terminal); ไฝ  (width 2) overlaid at left=3 lands on cols 3,4 โ€” // trailing col 4 is past width. Ink renders "aa ไฝ " (visible width 5); vue used // to drop ไฝ  wholesale (its leading col-3 cell included) via the now-removed // `offsetX + characterWidth > this.width` guard. Verified against the built Ink // reference (/tmp/ink @ v7.0.4 renderToString of this exact tree โ†’ "aa ไฝ "). test("wide char with in-bounds leading cell but out-of-bounds trailing cell still renders (overflows row, Ink parity)", () => { const output = renderToString( defineComponent(() => () => ( aa ไฝ  )), { columns: 4 }, ); expect(stripAnsi(output)).toBe("aa ไฝ "); expect(stringWidth(stripAnsi(output))).toBe(5); });