fix(runtime): align Ink parity behavior
Align several user-observable runtime behaviors with the Ink v7.0.4 parity audit: live input/paste handler refs, duplicate focus id registration, string-only color props, noninteractive empty final newlines, cross-realm error headers, and contained zero-content box layout/paint. Document Vue-specific KEEP decisions and require Conventional Commits for commit messages and PR titles. Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -207,13 +207,9 @@ test("non-number/non-string flexBasis falls back to auto (Ink parity), does not
|
||||
expect(lastFrame({ trimLines: true })).toBe("AB");
|
||||
});
|
||||
|
||||
// A zero/negative parsed percent ("0"→0%, "-5"→-5%, "0x10"→parseInt=0→0%) produces a
|
||||
// 0-width inner box. Ink renders "B\nA" (B on the row, A wraps onto the next line). The
|
||||
// 0-width text measures via wrapAnsi("A", 0, {hard:true, trim:false}) = "\nA" → height 2,
|
||||
// so A occupies a second row. vue previously dropped the text ("B") because wrapText's
|
||||
// `width <= 0 → [""]` guard collapsed the measure to height 1. Verified against Ink v7.0.4
|
||||
// (@40b3a75): all four of flexBasis=0/"0%" and width=0/"0%" render "B\nA".
|
||||
test("zero/negative flexBasis% wraps the sibling in Ink (downstream divergence)", async () => {
|
||||
// A zero-width inner content rect has no legal child paint area. Children must
|
||||
// neither paint nor reserve the extra rows Ink's zero-width wrapping creates.
|
||||
test("zero flexBasis hides children and does not reserve invisible rows", async () => {
|
||||
const { lastFrame } = await render(
|
||||
defineComponent(() => () => (
|
||||
<Box flexDirection="row" width={6}>
|
||||
@@ -225,11 +221,10 @@ test("zero/negative flexBasis% wraps the sibling in Ink (downstream divergence)"
|
||||
)),
|
||||
{ columns: 100 },
|
||||
);
|
||||
// Ink v7.0.4 renders "B\nA".
|
||||
expect(lastFrame({ trimLines: true })).toBe("B\nA");
|
||||
expect(lastFrame({ trimLines: true })).toBe("B");
|
||||
});
|
||||
|
||||
test("zero-width Box wraps its text onto its own line (width={0})", async () => {
|
||||
test("zero-width Box hides children and does not reserve invisible rows (width={0})", async () => {
|
||||
const { lastFrame } = await render(
|
||||
defineComponent(() => () => (
|
||||
<Box width={6}>
|
||||
@@ -241,12 +236,10 @@ test("zero-width Box wraps its text onto its own line (width={0})", async () =>
|
||||
)),
|
||||
{ columns: 100 },
|
||||
);
|
||||
// Ink v7.0.4 renders "B\nA": the 0-width text measures height 2 via
|
||||
// wrapAnsi("A", 0, {hard:true}) = "\nA", so A wraps below sibling B.
|
||||
expect(lastFrame({ trimLines: true })).toBe("B\nA");
|
||||
expect(lastFrame({ trimLines: true })).toBe("B");
|
||||
});
|
||||
|
||||
test('zero-percent-width Box wraps its text onto its own line (width="0%")', async () => {
|
||||
test('zero-percent-width Box hides children and does not reserve invisible rows (width="0%")', async () => {
|
||||
const { lastFrame } = await render(
|
||||
defineComponent(() => () => (
|
||||
<Box width={6}>
|
||||
@@ -258,8 +251,7 @@ test('zero-percent-width Box wraps its text onto its own line (width="0%")', asy
|
||||
)),
|
||||
{ columns: 100 },
|
||||
);
|
||||
// Ink v7.0.4 renders "B\nA" — same as width={0}; a 0% resolved width is also 0px.
|
||||
expect(lastFrame({ trimLines: true })).toBe("B\nA");
|
||||
expect(lastFrame({ trimLines: true })).toBe("B");
|
||||
});
|
||||
|
||||
test("zero-width Box with EMPTY text adds no spurious row", async () => {
|
||||
@@ -279,14 +271,7 @@ test("zero-width Box with EMPTY text adds no spurious row", async () => {
|
||||
expect(lastFrame({ trimLines: true })).toBe("B");
|
||||
});
|
||||
|
||||
test("zero-width Box with backgroundColor wraps cleanly, keeping the bg glyph (Ink parity)", async () => {
|
||||
// Regression guard for the wrap-ansi width<=0 byte-split: at width 0 the 0-width Box's
|
||||
// text wraps onto its own row, but vue bakes the bg color INTO the string before wrapping,
|
||||
// and wrap-ansi@10 byte-splits the SGR escapes of a STYLED string at width<=0
|
||||
// (wrapAnsi("\x1b[41mA\x1b[49m", 0) = "\x1b\n[\n4\n1\nm\nA\n…"). That scattered the escape
|
||||
// bytes across rows and rendered a garbage "B\n[" (the 2nd byte of "\x1b[41m"). wrapText
|
||||
// now routes width<=0 styled text through an ANSI-aware per-grapheme split, matching Ink,
|
||||
// which wraps PLAIN text and colorizes per line afterwards.
|
||||
test("zero-width Box with backgroundColor hides children and does not reserve invisible rows", async () => {
|
||||
const { lastFrame } = await render(
|
||||
defineComponent(() => () => (
|
||||
<Box flexDirection="row" width={6}>
|
||||
@@ -298,13 +283,22 @@ test("zero-width Box with backgroundColor wraps cleanly, keeping the bg glyph (I
|
||||
)),
|
||||
{ columns: 100 },
|
||||
);
|
||||
// RAW-byte parity target captured from Ink v7.0.4 (@40b3a75) with chalk level 3:
|
||||
// "B\n\x1b[41mA\x1b[49m\n" — row 2 keeps the FULL bg-colored glyph (overflow:visible).
|
||||
// vue trims trailing whitespace/newlines per frame line, so the equivalent raw frame is
|
||||
// "B\n\x1b[41mA\x1b[49m" (no trailing newline). The bg glyph must survive intact.
|
||||
expect(lastFrame({ raw: true })).toBe("B\n\x1b[41mA\x1b[49m");
|
||||
// And the stripped visible layout is "B\nA" (sanity check on the wrap position).
|
||||
// eslint-disable-next-line no-control-regex -- strip ANSI to assert the visible layout
|
||||
const visible = lastFrame({ trimLines: true })!.replace(/\x1b\[[0-9;]*m/g, "");
|
||||
expect(visible).toBe("B\nA");
|
||||
expect(lastFrame({ raw: true })).toBe("B");
|
||||
});
|
||||
|
||||
test("zero-width Box hides nested Box children and does not reserve invisible rows", async () => {
|
||||
const { lastFrame } = await render(
|
||||
defineComponent(() => () => (
|
||||
<Box flexDirection="row" width={6}>
|
||||
<Box width={0}>
|
||||
<Box borderStyle="single">
|
||||
<Text>A</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Text>B</Text>
|
||||
</Box>
|
||||
)),
|
||||
{ columns: 100 },
|
||||
);
|
||||
expect(lastFrame({ trimLines: true })).toBe("B");
|
||||
});
|
||||
|
||||
@@ -64,3 +64,55 @@ test("text wraps within border+padding content area", async () => {
|
||||
expect(frame).toContain("Hello");
|
||||
expect(frame).toContain("World!");
|
||||
});
|
||||
|
||||
test("children are not painted when border consumes content height", async () => {
|
||||
const { lastFrame } = await render(
|
||||
defineComponent(() => () => (
|
||||
<Box borderStyle="single" width={3} height={2}>
|
||||
<Text>x</Text>
|
||||
</Box>
|
||||
)),
|
||||
{ columns: 20 },
|
||||
);
|
||||
|
||||
expect(lastFrame()).toBe("┌─┐\n└─┘");
|
||||
});
|
||||
|
||||
test("children are not painted when border consumes content width", async () => {
|
||||
const { lastFrame } = await render(
|
||||
defineComponent(() => () => (
|
||||
<Box borderStyle="single" width={2} height={3}>
|
||||
<Text>x</Text>
|
||||
</Box>
|
||||
)),
|
||||
{ columns: 20 },
|
||||
);
|
||||
|
||||
expect(lastFrame()).toBe("┌┐\n││\n└┘");
|
||||
});
|
||||
|
||||
test("children are not painted when padding consumes the remaining content area", async () => {
|
||||
const { lastFrame } = await render(
|
||||
defineComponent(() => () => (
|
||||
<Box borderStyle="single" width={4} height={3} paddingX={1}>
|
||||
<Text>x</Text>
|
||||
</Box>
|
||||
)),
|
||||
{ columns: 20 },
|
||||
);
|
||||
|
||||
expect(lastFrame()).toBe("┌──┐\n│ │\n└──┘");
|
||||
});
|
||||
|
||||
test("children are not painted inside a zero-height side-only border box", async () => {
|
||||
const { lastFrame } = await render(
|
||||
defineComponent(() => () => (
|
||||
<Box borderStyle="single" borderTop={false} borderBottom={false} height={0}>
|
||||
<Text>x</Text>
|
||||
</Box>
|
||||
)),
|
||||
{ columns: 20 },
|
||||
);
|
||||
|
||||
expect(lastFrame()).toBe("");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user