Absolute-positioned wide characters (CJK, emoji) could paint past the right edge of a clipped box or the terminal grid, producing output wider than the column limit. Three fixes: - Safe-slice after sliceAnsi in clip logic to handle wide char overshoot - Bounds check in grid write loop to skip chars exceeding grid width - Width-aware border fill to account for measured corner char widths
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { defineComponent } from "vue";
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { render } from "@vue-tui/testing";
|
||||
import { Box, Text } from "@vue-tui/runtime";
|
||||
import { Box, Text, measureText } from "@vue-tui/runtime";
|
||||
import stripAnsi from "strip-ansi";
|
||||
|
||||
/** Build a round-border box string like boxen(text, { borderStyle: "round" }) */
|
||||
function box(text: string): string {
|
||||
@@ -581,3 +582,78 @@ test("out of bounds writes do not crash", async () => {
|
||||
);
|
||||
expect(lastFrame({ trimLines: true })).toBeDefined();
|
||||
});
|
||||
|
||||
// --- absolute overlay wide glyph clipping (issue #10) ---
|
||||
|
||||
function lineWidth(text: string): number {
|
||||
return measureText(stripAnsi(text), 9999).width;
|
||||
}
|
||||
|
||||
describe("absolute overlay wide glyph clipping", () => {
|
||||
test("wide char at right edge of clipped box is omitted", async () => {
|
||||
const { lastFrame } = await render(
|
||||
defineComponent(() => () => (
|
||||
<Box width={4} height={1} overflow="hidden">
|
||||
<Text>abc</Text>
|
||||
<Box position="absolute" left={3}>
|
||||
<Text>中</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)),
|
||||
{ columns: 100 },
|
||||
);
|
||||
const frame = lastFrame({ trimLines: true })!;
|
||||
expect(stripAnsi(frame)).toBe("abc");
|
||||
});
|
||||
|
||||
test("wide emoji at right edge of clipped box is omitted", async () => {
|
||||
const { lastFrame } = await render(
|
||||
defineComponent(() => () => (
|
||||
<Box width={4} height={1} overflow="hidden">
|
||||
<Text>abc</Text>
|
||||
<Box position="absolute" left={3}>
|
||||
<Text>🍔</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)),
|
||||
{ columns: 100 },
|
||||
);
|
||||
const frame = lastFrame({ trimLines: true })!;
|
||||
expect(stripAnsi(frame)).toBe("abc");
|
||||
});
|
||||
|
||||
test("wide char fully inside clipped box is preserved", async () => {
|
||||
const { lastFrame } = await render(
|
||||
defineComponent(() => () => (
|
||||
<Box width={4} height={1} overflow="hidden">
|
||||
<Text>a</Text>
|
||||
<Box position="absolute" left={2}>
|
||||
<Text>中</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)),
|
||||
{ columns: 100 },
|
||||
);
|
||||
const frame = lastFrame({ trimLines: true })!;
|
||||
expect(stripAnsi(frame)).toContain("中");
|
||||
expect(lineWidth(frame)).toBeLessThanOrEqual(4);
|
||||
});
|
||||
|
||||
test("output does not exceed terminal columns with absolute wide char", async () => {
|
||||
const { lastFrame } = await render(
|
||||
defineComponent(() => () => (
|
||||
<Box width={8} height={1} overflow="hidden">
|
||||
<Text>hello</Text>
|
||||
<Box position="absolute" left={7}>
|
||||
<Text>你好</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)),
|
||||
{ columns: 8 },
|
||||
);
|
||||
const frame = lastFrame({ trimLines: true })!;
|
||||
for (const line of frame.split("\n")) {
|
||||
expect(lineWidth(line)).toBeLessThanOrEqual(8);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user