fix: Box defaults to flexDirection='row', flexShrink=1, flexWrap='nowrap', flexGrow=0

Match Ink's Box defaults by setting yoga properties at node creation time.
This ensures proper row-based layout regardless of Vue's prop patching order.
Updated affected test snapshots and added explicit flexDirection="column"
where tests relied on the old column default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yunfei He
2026-05-26 00:49:27 +08:00
parent 8351712ea2
commit c4161c99a7
5 changed files with 148 additions and 67 deletions
@@ -114,10 +114,9 @@ test("Multiple Text elements inherit same background", async () => {
)),
{ columns: 100 },
);
expect(lastFrame()).toMatchInlineSnapshot(`
"Hello 
World "
`);
expect(lastFrame()).toMatchInlineSnapshot(
`"Hello World"`,
);
});
test("Mixed text with and without background inheritance", async () => {
@@ -131,11 +130,9 @@ test("Mixed text with and without background inheritance", async () => {
)),
{ columns: 100 },
);
expect(lastFrame()).toMatchInlineSnapshot(`
"Inherited 
No BG 
Red BG "
`);
expect(lastFrame()).toMatchInlineSnapshot(
`"Inherited No BG Red BG"`,
);
});
test("Complex nested structure with background inheritance", async () => {
@@ -153,11 +150,9 @@ test("Complex nested structure with background inheritance", async () => {
)),
{ columns: 100 },
);
expect(lastFrame()).toMatchInlineSnapshot(`
"Outer:  
Inner:  
Explicit"
`);
expect(lastFrame()).toMatchInlineSnapshot(
`"Outer: Inner: Explicit"`,
);
});
test("Box background with standard color", async () => {
@@ -246,7 +241,7 @@ test("Box background fills entire area with standard color", async () => {
{ columns: 100 },
);
expect(lastFrame()).toMatchInlineSnapshot(`
"Hello 
"Hello 
 
 "
`);
@@ -317,7 +312,7 @@ test("Box background with border fills content area", async () => {
);
expect(lastFrame()).toMatchInlineSnapshot(`
"╭────────╮
│Hi │
│Hi │
│ │
│ │
╰────────╯"
@@ -335,7 +330,7 @@ test("Box background with padding fills entire padded area", async () => {
);
expect(lastFrame()).toMatchInlineSnapshot(`
" 
 Hi  
 Hi 
 
 
 "
@@ -358,8 +353,8 @@ test("Box background with center alignment fills entire area", async () => {
{ columns: 100 },
);
expect(lastFrame()).toMatchInlineSnapshot(`
" 
Hi 
" Hi 
 
 "
`);
});
@@ -422,8 +417,8 @@ test("Box backgroundColor fills full width on every line when text wraps", async
{ columns: 100 },
);
expect(lastFrame()).toMatchInlineSnapshot(`
"Hello  
World!! "
"Hello   
World!! "
`);
});
@@ -187,7 +187,7 @@ test("single node - box with horizontal alignment", async () => {
);
expect(lastFrame()).toMatchInlineSnapshot(`
"╭──────────────────╮
│Hello World │
│ Hello World │
╰──────────────────╯"
`);
});
@@ -204,6 +204,14 @@ test("single node - box with vertical alignment", async () => {
);
expect(lastFrame()).toMatchInlineSnapshot(`
"╭───────────╮
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│Hello World│
│ │
│ │
@@ -214,14 +222,6 @@ test("single node - box with vertical alignment", async () => {
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰───────────╯"
`);
});
@@ -377,7 +377,7 @@ test("multiple nodes - box with horizontal alignment", async () => {
);
expect(lastFrame()).toMatchInlineSnapshot(`
"╭──────────────────╮
│Hello World │
│ Hello World │
╰──────────────────╯"
`);
});
@@ -394,6 +394,14 @@ test("multiple nodes - box with vertical alignment", async () => {
);
expect(lastFrame()).toMatchInlineSnapshot(`
"╭───────────╮
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│Hello World│
│ │
│ │
@@ -404,14 +412,6 @@ test("multiple nodes - box with vertical alignment", async () => {
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰───────────╯"
`);
});
@@ -486,11 +486,11 @@ test("nested boxes", async () => {
expect(lastFrame()).toMatchInlineSnapshot(`
"╭──────────────────────────────────────╮
│ │
│ ╭──────────────────────────────────╮ │
│ │ │ │
│ │ Hello World │ │
│ │ │ │
│ ╰──────────────────────────────────╯ │
│ ╭─────────────╮ │
│ │ │ │
│ │ Hello World │ │
│ │ │ │
│ ╰─────────────╯ │
│ │
╰──────────────────────────────────────╯"
`);
@@ -515,17 +515,11 @@ test("nested boxes - fit-content box with wide characters on flex-direction row"
{ columns: 100 },
);
expect(lastFrame()).toMatchInlineSnapshot(`
"╭────────────╮
│╭──────────╮│
││ミスター ││
│╰──────────╯│
│╭──────────╮│
││スポック ││
│╰──────────╯│
│╭──────────╮│
││カーク船長││
│╰──────────╯│
╰────────────╯"
"╭────────────────────────────────╮
│╭────────╮╭────────╮╭──────────╮│
││ミスター││スポック││カーク船長││
│╰────────╯╰────────╯╰──────────╯│
╰────────────────────────────────╯"
`);
});
@@ -548,17 +542,11 @@ test("nested boxes - fit-content box with emojis on flex-direction row", async (
{ columns: 100 },
);
expect(lastFrame()).toMatchInlineSnapshot(`
"╭────╮
│╭──╮│
││🦾││
│╰──╯│
│╭──╮│
││🌏││
│╰──╯│
│╭──╮│
││😋││
│╰──╯│
╰────╯"
"╭────────────╮
│╭──╮╭──╮╭──╮│
││🦾││🌏││😋││
│╰──╯╰──╯╰──╯│
╰────────────╯"
`);
});
@@ -173,7 +173,7 @@ test("remeasure text when text nodes are changed", async () => {
const add = shallowRef(false);
const { lastFrame } = await render(
defineComponent(() => () => (
<Box>
<Box flexDirection="column">
<Text>abc{add.value ? <Text>x</Text> : null}</Text>
</Box>
)),
@@ -0,0 +1,87 @@
import { defineComponent } from "vue";
import { expect, test } from "vite-plus/test";
import { render } from "@vue-tui/testing";
import { Box, Text } from "@vue-tui/runtime";
test("Box defaults to flexDirection='row'", async () => {
const { lastFrame } = await render(
defineComponent(() => () => (
<Box>
<Text>A</Text>
<Text>B</Text>
</Box>
)),
{ columns: 100 },
);
expect(lastFrame({ trimLines: true })).toBe("AB");
});
test("Box defaults to flexShrink=1", async () => {
// Two boxes inside a constrained parent: if flexShrink defaults to 1,
// they should shrink to fit rather than overflowing.
const { lastFrame } = await render(
defineComponent(() => () => (
<Box width={10}>
<Box width={8}>
<Text>AAAAAAAA</Text>
</Box>
<Box width={8}>
<Text>BBBBBBBB</Text>
</Box>
</Box>
)),
{ columns: 100 },
);
// Both boxes should shrink to fit within 10 columns
const frame = lastFrame({ trimLines: true })!;
// The frame should be at most 10 chars wide
const maxLineWidth = Math.max(...frame.split("\n").map((l) => l.length));
expect(maxLineWidth).toBeLessThanOrEqual(10);
});
test("Box defaults to flexWrap='nowrap'", async () => {
// With nowrap, items stay on one line even if they overflow
const { lastFrame } = await render(
defineComponent(() => () => (
<Box width={10}>
<Text>Hello</Text>
<Text>World</Text>
</Box>
)),
{ columns: 100 },
);
// Items should be on the same line (row direction, nowrap)
const frame = lastFrame({ trimLines: true })!;
expect(frame.split("\n")).toHaveLength(1);
expect(frame).toBe("HelloWorld");
});
test("Box defaults to flexGrow=0", async () => {
// A box with no explicit flexGrow should not grow to fill available space
const { lastFrame } = await render(
defineComponent(() => () => (
<Box width={20}>
<Box>
<Text>A</Text>
</Box>
</Box>
)),
{ columns: 100 },
);
// The inner box should only be as wide as its content (1 char), not 20
const frame = lastFrame({ trimLines: true })!;
expect(frame).toBe("A");
});
test("user-provided props override Box defaults", async () => {
const { lastFrame } = await render(
defineComponent(() => () => (
<Box flexDirection="column">
<Text>A</Text>
<Text>B</Text>
</Box>
)),
{ columns: 100 },
);
expect(lastFrame({ trimLines: true })).toBe("A\nB");
});