688da13872
Ink keeps its `measure-text` module internal and never re-exports it. vue-tui exported `measureText` / `measureTextNatural` from the public index under the mistaken belief — stated verbatim in commit 0e7d775's own message — that doing so "matched Ink's public API". It does not; Ink keeps that module internal. A later design doc then rationalized the leak post-hoc as an intentional divergence. It was neither intentional nor a divergence — it was a mistake. Align with Ink: - Drop both from the public index. `measureTextNatural` stays as an internal helper (yoga.ts uses it). `measureText` had zero production callers (yoga uses `wrapText` + `measureTextNatural`, never `measureText`) and is removed. - Integration tests that used `measureText(stripAnsi(x), 9999).width` as a line-width helper now use `stringWidth(stripAnsi(x))` directly. - public-api.test.ts gains a regression test asserting neither is exported. - Remove the now-obsolete entry from .agents/docs/ink-divergences.md. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
71 lines
2.6 KiB
TypeScript
71 lines
2.6 KiB
TypeScript
import { defineComponent } from "vue";
|
|
import { describe, expect, test } from "vite-plus/test";
|
|
import { render } from "@vue-tui/testing";
|
|
import { Box, Text } from "@vue-tui/runtime";
|
|
import stripAnsi from "strip-ansi";
|
|
import stringWidth from "string-width";
|
|
|
|
function lineWidth(text: string): number {
|
|
return stringWidth(stripAnsi(text));
|
|
}
|
|
|
|
describe("grapheme-aware clipping (issue #21)", () => {
|
|
test("ZWJ emoji at right clip edge is dropped whole, not split", async () => {
|
|
const { lastFrame } = await render(
|
|
defineComponent(() => () => (
|
|
<Box width={4} height={1} overflow="hidden">
|
|
<Text>ab</Text>
|
|
<Box position="absolute" left={3}>
|
|
<Text>👨👩👧👦</Text>
|
|
</Box>
|
|
</Box>
|
|
)),
|
|
{ columns: 100 },
|
|
);
|
|
const frame = stripAnsi(lastFrame({ trimLines: true })!);
|
|
expect(lineWidth(frame)).toBeLessThanOrEqual(4);
|
|
expect(frame).not.toContain("👨");
|
|
});
|
|
|
|
test("left-edge wide grapheme straddle starts following text at clip origin (Ink parity)", async () => {
|
|
const { lastFrame } = await render(
|
|
defineComponent(() => () => (
|
|
<Box width={4} height={1} overflow="hidden">
|
|
<Box marginLeft={-1} flexShrink={0}>
|
|
<Text>中x</Text>
|
|
</Box>
|
|
</Box>
|
|
)),
|
|
{ columns: 100 },
|
|
);
|
|
const frame = stripAnsi(lastFrame({ trimLines: true })!);
|
|
// "中" (width 2) straddles the left clip edge → dropped whole. Ink output.ts:210-212
|
|
// sets the write origin to the clipped left edge, so the kept "x" starts AT that
|
|
// origin with NO leading space. Verified against the built Ink reference
|
|
// (/tmp/ink-40b3a75 renderToString of this exact tree → "x"). Previously this
|
|
// locked vue-tui's leading-space padding (" x"); rewritten to Ink per the G63
|
|
// decisions-log entry in .agents/docs/parity-ledger.md.
|
|
expect(frame).toBe("x");
|
|
});
|
|
|
|
// absolute-non-edge class (R2-000045): an absolutely-positioned ZWJ emoji is
|
|
// kept whole, matching Ink (verified via ink-testing-library:
|
|
// ["coabc", "ct👨👩👧👦"]).
|
|
test("absolutely-positioned ZWJ emoji is not split", async () => {
|
|
const { lastFrame } = await render(
|
|
defineComponent(() => () => (
|
|
<Box width={7} height={2}>
|
|
<Text>coabc</Text>
|
|
<Box position="absolute" marginTop={1}>
|
|
<Text>ct👨👩👧👦</Text>
|
|
</Box>
|
|
</Box>
|
|
)),
|
|
{ columns: 100 },
|
|
);
|
|
const lines = stripAnsi(lastFrame({ trimLines: true })!).split("\n");
|
|
expect(lines[0]).toBe("coabc");
|
|
expect(lines[1]).toBe("ct👨👩👧👦");
|
|
});
|
|
});
|