From 22ecbe0f801421afeb3d4072b9fb924b924909a4 Mon Sep 17 00:00:00 2001 From: Yunfei He Date: Mon, 25 May 2026 21:24:31 +0800 Subject: [PATCH] test: port Ink layout tests (157 passing, 21 known failures) Translates flex, direction, wrap, align, justify, gap, padding, margin, width-height, position, display, and overflow tests from Ink. Adds trimLines mode and trailing-newline strip to test helper. Fixes Box type for alignContent (space-between/around/evenly) and alignSelf (stretch/baseline). 21 tests fail due to behavioral differences (overflow clipping, percentage dimensions, relative position offsets, align-content space-* variants). These represent real gaps to investigate. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../integration/layout/display.test.tsx | 34 ++ .../layout/flex-align-content.test.tsx | 165 ++++++++ .../layout/flex-align-items.test.tsx | 122 ++++++ .../layout/flex-align-self.test.tsx | 136 +++++++ .../layout/flex-direction.test.tsx | 72 ++++ .../layout/flex-justify-content.test.tsx | 160 ++++++++ .../integration/layout/flex-wrap.test.tsx | 86 ++++ .../integration/layout/flex.test.tsx | 136 +++++++ .../integration/layout/gap.test.tsx | 48 +++ .../integration/layout/margin.test.tsx | 147 +++++++ .../integration/layout/overflow.test.tsx | 204 ++++++++++ .../integration/layout/padding.test.tsx | 170 ++++++++ .../integration/layout/position.test.tsx | 198 ++++++++++ .../integration/layout/width-height.test.tsx | 368 ++++++++++++++++++ packages/runtime/src/components/Box.ts | 5 +- packages/runtime/src/host/yoga.ts | 3 + packages/testing/src/render.ts | 22 +- 17 files changed, 2070 insertions(+), 6 deletions(-) create mode 100644 packages/runtime-tests/integration/layout/display.test.tsx create mode 100644 packages/runtime-tests/integration/layout/flex-align-content.test.tsx create mode 100644 packages/runtime-tests/integration/layout/flex-align-items.test.tsx create mode 100644 packages/runtime-tests/integration/layout/flex-align-self.test.tsx create mode 100644 packages/runtime-tests/integration/layout/flex-direction.test.tsx create mode 100644 packages/runtime-tests/integration/layout/flex-justify-content.test.tsx create mode 100644 packages/runtime-tests/integration/layout/flex-wrap.test.tsx create mode 100644 packages/runtime-tests/integration/layout/flex.test.tsx create mode 100644 packages/runtime-tests/integration/layout/gap.test.tsx create mode 100644 packages/runtime-tests/integration/layout/margin.test.tsx create mode 100644 packages/runtime-tests/integration/layout/overflow.test.tsx create mode 100644 packages/runtime-tests/integration/layout/padding.test.tsx create mode 100644 packages/runtime-tests/integration/layout/position.test.tsx create mode 100644 packages/runtime-tests/integration/layout/width-height.test.tsx diff --git a/packages/runtime-tests/integration/layout/display.test.tsx b/packages/runtime-tests/integration/layout/display.test.tsx new file mode 100644 index 0000000..d9191e5 --- /dev/null +++ b/packages/runtime-tests/integration/layout/display.test.tsx @@ -0,0 +1,34 @@ +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("display flex", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + X + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("X"); +}); + +test("display none", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + Kitty! + + Doggo + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("Doggo"); +}); + +// Skipped: display flex - concurrent +// Skipped: display none - concurrent diff --git a/packages/runtime-tests/integration/layout/flex-align-content.test.tsx b/packages/runtime-tests/integration/layout/flex-align-content.test.tsx new file mode 100644 index 0000000..a046299 --- /dev/null +++ b/packages/runtime-tests/integration/layout/flex-align-content.test.tsx @@ -0,0 +1,165 @@ +import { defineComponent, shallowRef, nextTick } from "vue"; +import { expect, test } from "vite-plus/test"; +import { render } from "@vue-tui/testing"; +import { Box, Text } from "@vue-tui/runtime"; + +const alignContentCases = [ + ["flex-start", "AB\nCD\n\n\n\n"], + ["center", "\n\nAB\nCD\n\n"], + ["flex-end", "\n\n\n\nAB\nCD"], + ["space-between", "AB\n\n\n\n\nCD"], + ["space-around", "\nAB\n\nCD\n\n"], + ["space-evenly", "\nAB\n\nCD\n\n"], + ["stretch", "AB\n\n\nCD\n\n"], +] as const; + +for (const [alignContent, expectedOutput] of alignContentCases) { + test(`align content ${alignContent}`, async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + C + D + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe(expectedOutput); + }); +} + +test("align content defaults to flex-start", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + C + D + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("AB\nCD\n\n\n\n"); +}); + +test("align content does not add extra spacing when there is no free cross-axis space", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + C + D + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("AB\nCD"); +}); + +test("clears alignContent on rerender to default flex-start", async () => { + const alignContent = shallowRef< + | "center" + | "flex-start" + | "flex-end" + | "space-between" + | "space-around" + | "space-evenly" + | "stretch" + | undefined + >("center"); + + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + C + D + + )), + { columns: 100 }, + ); + + expect(lastFrame({ trimLines: true })).toBe("\n\nAB\nCD\n\n"); + + alignContent.value = undefined; + await nextTick(); + expect(lastFrame({ trimLines: true })).toBe("AB\nCD\n\n\n\n"); +}); + +test("clears alignContent from stretch on rerender to default flex-start", async () => { + const alignContent = shallowRef< + | "center" + | "flex-start" + | "flex-end" + | "space-between" + | "space-around" + | "space-evenly" + | "stretch" + | undefined + >("stretch"); + + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + C + D + + )), + { columns: 100 }, + ); + + expect(lastFrame({ trimLines: true })).toBe("AB\n\n\nCD\n\n"); + + alignContent.value = undefined; + await nextTick(); + expect(lastFrame({ trimLines: true })).toBe("AB\nCD\n\n\n\n"); +}); + +test("clears alignContent when prop is omitted on rerender", async () => { + const showAlignContent = shallowRef(true); + + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + C + D + + )), + { columns: 100 }, + ); + + expect(lastFrame({ trimLines: true })).toBe("\n\nAB\nCD\n\n"); + + showAlignContent.value = false; + await nextTick(); + expect(lastFrame({ trimLines: true })).toBe("AB\nCD\n\n\n\n"); +}); + +// Skipped: align content center - concurrent diff --git a/packages/runtime-tests/integration/layout/flex-align-items.test.tsx b/packages/runtime-tests/integration/layout/flex-align-items.test.tsx new file mode 100644 index 0000000..c7ed64a --- /dev/null +++ b/packages/runtime-tests/integration/layout/flex-align-items.test.tsx @@ -0,0 +1,122 @@ +import { defineComponent } from "vue"; +import { expect, test } from "vite-plus/test"; +import { render } from "@vue-tui/testing"; +import { Box, Text, Newline } from "@vue-tui/runtime"; + +test("row - align text to center", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Test + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\nTest\n"); +}); + +test("row - align multiple text nodes to center", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\nAB\n"); +}); + +test("row - align text to bottom", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Test + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n\nTest"); +}); + +test("row - align multiple text nodes to bottom", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n\nAB"); +}); + +test("column - align text to center", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Test + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe(" Test"); +}); + +test("column - align text to right", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Test + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe(" Test"); +}); + +test("row - align items stretch", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + X + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("┌─┐\n│X│\n│ │\n│ │\n└─┘"); +}); + +test("row - default align items stretches children", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + X + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("┌─┐\n│X│\n│ │\n│ │\n└─┘"); +}); + +test("row - align text to baseline", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + A + B + + X + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("A\nBX\n"); +}); diff --git a/packages/runtime-tests/integration/layout/flex-align-self.test.tsx b/packages/runtime-tests/integration/layout/flex-align-self.test.tsx new file mode 100644 index 0000000..ececf6b --- /dev/null +++ b/packages/runtime-tests/integration/layout/flex-align-self.test.tsx @@ -0,0 +1,136 @@ +import { defineComponent } from "vue"; +import { expect, test } from "vite-plus/test"; +import { render } from "@vue-tui/testing"; +import { Box, Text, Newline } from "@vue-tui/runtime"; + +test("row - align text to center", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + Test + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\nTest\n"); +}); + +test("row - align multiple text nodes to center", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + A + B + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\nAB\n"); +}); + +test("row - align text to bottom", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + Test + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n\nTest"); +}); + +test("row - align multiple text nodes to bottom", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + A + B + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n\nAB"); +}); + +test("column - align text to center", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + Test + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe(" Test"); +}); + +test("column - align text to right", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + Test + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe(" Test"); +}); + +test("column - align self stretch", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + X + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("┌─────┐\n│X │\n└─────┘"); +}); + +test("row - align self stretch", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + X + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("┌─┐\n│X│\n│ │\n│ │\n└─┘"); +}); + +test("row - align self baseline", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + A + B + + + X + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("AX\nB\n"); +}); diff --git a/packages/runtime-tests/integration/layout/flex-direction.test.tsx b/packages/runtime-tests/integration/layout/flex-direction.test.tsx new file mode 100644 index 0000000..1e83fa1 --- /dev/null +++ b/packages/runtime-tests/integration/layout/flex-direction.test.tsx @@ -0,0 +1,72 @@ +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("direction row", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("AB"); +}); + +test("direction row reverse", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe(" BA"); +}); + +test("direction column", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("A\nB"); +}); + +test("direction column reverse", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n\nB\nA"); +}); + +test("don't squash text nodes when column direction is applied", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("A\nB"); +}); + +// Skipped: direction row - concurrent +// Skipped: direction column - concurrent diff --git a/packages/runtime-tests/integration/layout/flex-justify-content.test.tsx b/packages/runtime-tests/integration/layout/flex-justify-content.test.tsx new file mode 100644 index 0000000..2a9da34 --- /dev/null +++ b/packages/runtime-tests/integration/layout/flex-justify-content.test.tsx @@ -0,0 +1,160 @@ +import { defineComponent } from "vue"; +import { expect, test } from "vite-plus/test"; +import { render } from "@vue-tui/testing"; +import { Box, Text } from "@vue-tui/runtime"; +import chalk from "chalk"; + +test("row - align text to center", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Test + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe(" Test"); +}); + +test("row - align multiple text nodes to center", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe(" AB"); +}); + +test("row - align text to right", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Test + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe(" Test"); +}); + +test("row - align multiple text nodes to right", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe(" AB"); +}); + +test("row - align two text nodes on the edges", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("A B"); +}); + +test("row - space evenly two text nodes", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe(" A B"); +}); + +// Yoga has a bug, where first child in a container with space-around doesn't have +// the correct X coordinate and measure function is used on that child node +test.skip("row - align two text nodes with equal space around them — known Yoga issue", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe(" A B"); +}); + +test("row - align colored text node when text is squashed", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + X + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe(` ${chalk.green("X")}`); +}); + +test("column - align text to center", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Test + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\nTest\n"); +}); + +test("column - align text to bottom", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Test + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n\nTest"); +}); + +test("column - align two text nodes on the edges", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("A\n\n\nB"); +}); + +// Yoga has a bug, where first child in a container with space-around doesn't have +// the correct X coordinate and measure function is used on that child node +test.skip("column - align two text nodes with equal space around them — known Yoga issue", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\nA\n\nB\n"); +}); diff --git a/packages/runtime-tests/integration/layout/flex-wrap.test.tsx b/packages/runtime-tests/integration/layout/flex-wrap.test.tsx new file mode 100644 index 0000000..449b0e8 --- /dev/null +++ b/packages/runtime-tests/integration/layout/flex-wrap.test.tsx @@ -0,0 +1,86 @@ +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("row - no wrap", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + BC + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("BC\n"); +}); + +test("column - no wrap", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + C + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("B\nC"); +}); + +test("row - wrap content", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + BC + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("A\nBC"); +}); + +test("column - wrap content", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + C + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("AC\nB"); +}); + +test("column - wrap content reverse", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + C + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe(" CA\n B"); +}); + +test("row - wrap content reverse", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + C + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\nC\nAB"); +}); diff --git a/packages/runtime-tests/integration/layout/flex.test.tsx b/packages/runtime-tests/integration/layout/flex.test.tsx new file mode 100644 index 0000000..4caf5b6 --- /dev/null +++ b/packages/runtime-tests/integration/layout/flex.test.tsx @@ -0,0 +1,136 @@ +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("grow equally", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + A + + + B + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("A B"); +}); + +test("grow one element", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + A + + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("A B"); +}); + +test("do not shrink", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + A + + + B + + + C + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("A B C"); +}); + +test("shrink equally", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + A + + + B + + C + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("A B C"); +}); + +test('set flex basis with flexDirection="row" container', async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + A + + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("A B"); +}); + +test('set flex basis in percent with flexDirection="row" container', async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + A + + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("A B"); +}); + +test('set flex basis with flexDirection="column" container', async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + A + + B + + )), + { columns: 100 }, + ); + // Ink: "A\n\n\nB\n\n" — trailing newlines trimmed by lastFrame({ trimLines: true }) + expect(lastFrame({ trimLines: true })).toBe("A\n\n\nB"); +}); + +test('set flex basis in percent with flexDirection="column" container', async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + A + + B + + )), + { columns: 100 }, + ); + // Ink: "A\n\n\nB\n\n" — trailing newlines trimmed by lastFrame({ trimLines: true }) + expect(lastFrame({ trimLines: true })).toBe("A\n\n\nB"); +}); diff --git a/packages/runtime-tests/integration/layout/gap.test.tsx b/packages/runtime-tests/integration/layout/gap.test.tsx new file mode 100644 index 0000000..c8e7ea7 --- /dev/null +++ b/packages/runtime-tests/integration/layout/gap.test.tsx @@ -0,0 +1,48 @@ +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("gap", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + C + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("A B\n\nC"); +}); + +test("column gap", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("A B"); +}); + +test("row gap", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("A\n\nB"); +}); + +// Skipped: gap - concurrent +// Skipped: column gap - concurrent +// Skipped: row gap - concurrent diff --git a/packages/runtime-tests/integration/layout/margin.test.tsx b/packages/runtime-tests/integration/layout/margin.test.tsx new file mode 100644 index 0000000..4b1eb25 --- /dev/null +++ b/packages/runtime-tests/integration/layout/margin.test.tsx @@ -0,0 +1,147 @@ +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("margin", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + X + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n\n X\n\n"); +}); + +test("margin X", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + X + + Y + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe(" X Y"); +}); + +test("margin Y", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + X + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n\nX\n\n"); +}); + +test("margin top", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + X + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n\nX"); +}); + +test("margin bottom", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + X + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("X\n\n"); +}); + +test("margin left", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + X + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe(" X"); +}); + +test("margin right", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + X + + Y + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("X Y"); +}); + +test("nested margin", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + X + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n\n\n\n X\n\n\n\n"); +}); + +test("margin with multiline string", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + {"A\nB"} + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n\n A\n B\n\n"); +}); + +test("apply margin to text with newlines", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Hello{"\n"}World + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n Hello\n World\n"); +}); + +test("apply margin to wrapped text", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Hello World + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n Hello\n World\n"); +}); + +// Skipped: margin - concurrent +// Skipped: nested margin - concurrent diff --git a/packages/runtime-tests/integration/layout/overflow.test.tsx b/packages/runtime-tests/integration/layout/overflow.test.tsx new file mode 100644 index 0000000..786f7b0 --- /dev/null +++ b/packages/runtime-tests/integration/layout/overflow.test.tsx @@ -0,0 +1,204 @@ +import { defineComponent } from "vue"; +import { expect, test } from "vite-plus/test"; +import { render } from "@vue-tui/testing"; +import { Box, Text } from "@vue-tui/runtime"; + +// overflowX tests — directional overflow not yet supported +test.todo("overflowX - single text node in a box inside overflow container"); +test.todo("overflowX - single text node inside overflow container with border"); +test.todo("overflowX - single text node in a box with border inside overflow container"); +test.todo("overflowX - multiple text nodes in a box inside overflow container"); +test.todo("overflowX - multiple text nodes in a box inside overflow container with border"); +test.todo("overflowX - multiple text nodes in a box with border inside overflow container"); +test.todo("overflowX - multiple boxes inside overflow container"); +test.todo("overflowX - multiple boxes inside overflow container with border"); +test.todo("overflowX - box before left edge of overflow container"); +test.todo("overflowX - box before left edge of overflow container with border"); +test.todo("overflowX - box intersecting with left edge of overflow container"); +test.todo("overflowX - box intersecting with left edge of overflow container with border"); +test.todo("overflowX - box after right edge of overflow container"); +test.todo("overflowX - box intersecting with right edge of overflow container"); + +// overflowY tests — directional overflow not yet supported +test.todo("overflowY - single text node inside overflow container"); +test.todo("overflowY - single text node inside overflow container with border"); +test.todo("overflowY - multiple boxes inside overflow container"); +test.todo("overflowY - multiple boxes inside overflow container with border"); +test.todo("overflowY - box above top edge of overflow container"); +test.todo("overflowY - box above top edge of overflow container with border"); +test.todo("overflowY - box intersecting with top edge of overflow container"); +test.todo("overflowY - box intersecting with top edge of overflow container with border"); +test.todo("overflowY - box below bottom edge of overflow container"); +test.todo("overflowY - box below bottom edge of overflow container with border"); +test.todo("overflowY - box intersecting with bottom edge of overflow container"); +test.todo("overflowY - box intersecting with bottom edge of overflow container with border"); + +// unified overflow tests +test("overflow - single text node inside overflow container", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + + Hello{"\n"}World + + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("Hello\n"); +}); + +test("overflow - single text node inside overflow container with border", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + + Hello{"\n"}World + + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("╭──────╮\n│Hello │\n╰──────╯\n"); +}); + +test("overflow - multiple boxes inside overflow container", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + + TL{"\n"}BL + + + TR{"\n"}BR + + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("TLTR\n"); +}); + +test("overflow - multiple boxes inside overflow container with border", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + + TL{"\n"}BL + + + TR{"\n"}BR + + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("╭────╮\n│TLTR│\n╰────╯\n"); +}); + +test("overflow - box intersecting with top left edge of overflow container", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + + AAAA{"\n"}BBBB{"\n"}CCCC{"\n"}DDDD + + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("CC\nDD\n\n"); +}); + +test("overflow - box intersecting with top right edge of overflow container", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + + AAAA{"\n"}BBBB{"\n"}CCCC{"\n"}DDDD + + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe(" CC\n DD\n\n"); +}); + +test("overflow - box intersecting with bottom left edge of overflow container", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + + AAAA{"\n"}BBBB{"\n"}CCCC{"\n"}DDDD + + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n\nAA\nBB"); +}); + +test("overflow - box intersecting with bottom right edge of overflow container", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + + AAAA{"\n"}BBBB{"\n"}CCCC{"\n"}DDDD + + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n\n AA\n BB"); +}); + +test("nested overflow", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + + + + AAAA{"\n"}BBBB{"\n"}CCCC{"\n"}DDDD + + + + + + + XXXX{"\n"}YYYY{"\n"}ZZZZ + + + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("AA\nBB\nXXXX\nYYYY\n"); +}); + +test("out of bounds writes do not crash", async () => { + // Just verify it renders without throwing; exact output varies by terminal width + const { lastFrame } = await render( + defineComponent(() => () => ), + { columns: 10 }, + ); + expect(lastFrame({ trimLines: true })).toBeDefined(); +}); diff --git a/packages/runtime-tests/integration/layout/padding.test.tsx b/packages/runtime-tests/integration/layout/padding.test.tsx new file mode 100644 index 0000000..1efdd2d --- /dev/null +++ b/packages/runtime-tests/integration/layout/padding.test.tsx @@ -0,0 +1,170 @@ +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("padding", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + X + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n\n X\n\n"); +}); + +test("padding X", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + X + + Y + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe(" X Y"); +}); + +test("padding Y", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + X + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n\nX\n\n"); +}); + +test("padding top", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + X + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n\nX"); +}); + +test("padding bottom", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + X + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("X\n\n"); +}); + +test("padding left", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + X + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe(" X"); +}); + +test("padding right", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + X + + Y + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("X Y"); +}); + +test("nested padding", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + X + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n\n\n\n X\n\n\n\n"); +}); + +test("padding with multiline string", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + {"A\nB"} + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n\n A\n B\n\n"); +}); + +test("apply padding to text with newlines", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Hello{"\n"}World + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n Hello\n World\n"); +}); + +test("apply padding to wrapped text", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Hello World + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n Hel\n lo\n Wor\n ld\n"); +}); + +test("text wrapping respects paddingX with flexGrow", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + + • + + Lorem ipsum dolor sit amet, consectetur adipiscing elit + + + + + )), + { columns: 100 }, + ); + const output = lastFrame({ trimLines: true }) ?? ""; + const lines = output.split("\n"); + for (const line of lines) { + expect(line.length).toBeLessThanOrEqual(40); + } +}); + +// Skipped: padding - concurrent +// Skipped: nested padding - concurrent diff --git a/packages/runtime-tests/integration/layout/position.test.tsx b/packages/runtime-tests/integration/layout/position.test.tsx new file mode 100644 index 0000000..8272586 --- /dev/null +++ b/packages/runtime-tests/integration/layout/position.test.tsx @@ -0,0 +1,198 @@ +import { defineComponent, shallowRef, nextTick } from "vue"; +import { expect, test } from "vite-plus/test"; +import { render } from "@vue-tui/testing"; +import { Box, Text } from "@vue-tui/runtime"; + +test("absolute position with top and left offsets", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + X + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n X\n"); +}); + +test("absolute position with bottom and right offsets", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + X + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n\n X\n"); +}); + +test("absolute position with percentage offsets", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + X + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n\n X\n"); +}); + +test("absolute position with percentage bottom and right offsets", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + X + + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("\n X\n\n"); +}); + +test("relative position offsets visual position while keeping flow", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + A + + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe(" BA "); +}); + +test("static position ignores offsets", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + A + + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("AB"); +}); + +test("static position ignores percentage offsets", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + A + + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("AB"); +}); + +test("clears top offset on rerender", async () => { + const top = shallowRef(1); + + const { lastFrame } = await render( + defineComponent(() => () => ( + + + X + + + )), + { columns: 100 }, + ); + + expect(lastFrame({ trimLines: true })).toBe("\n X\n"); + + top.value = undefined; + await nextTick(); + expect(lastFrame({ trimLines: true })).toBe(" X\n\n"); +}); + +test("clears percentage top and left offsets on rerender", async () => { + const top = shallowRef("50%"); + const left = shallowRef("50%"); + + const { lastFrame } = await render( + defineComponent(() => () => ( + + + X + + + )), + { columns: 100 }, + ); + + expect(lastFrame({ trimLines: true })).toBe("\n\n X\n"); + + top.value = undefined; + left.value = undefined; + await nextTick(); + expect(lastFrame({ trimLines: true })).toBe("X\n\n\n"); +}); + +test("clears percentage top and left offsets when props are omitted on rerender", async () => { + const showOffsets = shallowRef(true); + + const { lastFrame } = await render( + defineComponent(() => () => ( + + + X + + + )), + { columns: 100 }, + ); + + expect(lastFrame({ trimLines: true })).toBe("\n\n X\n"); + + showOffsets.value = false; + await nextTick(); + expect(lastFrame({ trimLines: true })).toBe("X\n\n\n"); +}); + +test("clears bottom and right offsets on rerender", async () => { + const bottom = shallowRef(1); + const right = shallowRef(1); + + const { lastFrame } = await render( + defineComponent(() => () => ( + + + X + + + )), + { columns: 100 }, + ); + + expect(lastFrame({ trimLines: true })).toBe("\n\n X\n"); + + bottom.value = undefined; + right.value = undefined; + await nextTick(); + expect(lastFrame({ trimLines: true })).toBe("X\n\n\n"); +}); + +// Skipped: absolute position with top and left offsets - concurrent diff --git a/packages/runtime-tests/integration/layout/width-height.test.tsx b/packages/runtime-tests/integration/layout/width-height.test.tsx new file mode 100644 index 0000000..d755c45 --- /dev/null +++ b/packages/runtime-tests/integration/layout/width-height.test.tsx @@ -0,0 +1,368 @@ +import { defineComponent, shallowRef, nextTick } from "vue"; +import { expect, test } from "vite-plus/test"; +import { render } from "@vue-tui/testing"; +import { Box, Text } from "@vue-tui/runtime"; + +test("set width", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + A + + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("A B"); +}); + +test("set width in percent", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + A + + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("A B "); +}); + +test("set min width", async () => { + const { lastFrame: smallerFrame } = await render( + defineComponent(() => () => ( + + + A + + B + + )), + { columns: 100 }, + ); + expect(smallerFrame({ raw: true })).toBe("A B"); + + const { lastFrame: largerFrame } = await render( + defineComponent(() => () => ( + + + AAAAA + + B + + )), + { columns: 100 }, + ); + expect(largerFrame()).toBe("AAAAAB"); +}); + +test.skip("set min width in percent — known Yoga issue", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + A + + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("A B"); +}); + +test("set height", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + A + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("AB\n\n\n"); +}); + +test("set height in percent", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + A + + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("A\n\n\nB\n\n"); +}); + +test("cut text over the set height", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + AAAABBBBCCCC + + )), + { columns: 4 }, + ); + expect(lastFrame({ trimLines: true })).toBe("AAAA\nBBBB"); +}); + +test("set min height", async () => { + const { lastFrame: smallerFrame } = await render( + defineComponent(() => () => ( + + A + + )), + { columns: 100 }, + ); + expect(smallerFrame({ raw: true })).toBe("A\n\n\n"); + + const { lastFrame: largerFrame } = await render( + defineComponent(() => () => ( + + + A + + + )), + { columns: 100 }, + ); + expect(largerFrame({ raw: true })).toBe("A\n\n\n"); +}); + +test("set min height in percent", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + A + + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("A\n\n\nB\n\n"); +}); + +test("set max width", async () => { + const { lastFrame: constrainedFrame } = await render( + defineComponent(() => () => ( + + + AAAAA + + B + + )), + { columns: 10 }, + ); + expect(constrainedFrame()).toBe("AAAB\nAA"); + + const { lastFrame: unconstrainedFrame } = await render( + defineComponent(() => () => ( + + + AAA + + B + + )), + { columns: 100 }, + ); + expect(unconstrainedFrame()).toBe("AAAB"); +}); + +test("clears maxWidth on rerender", async () => { + const maxWidth = shallowRef(3); + + const { lastFrame } = await render( + defineComponent(() => () => ( + + + AAAAA + + B + + )), + { columns: 10 }, + ); + + expect(lastFrame({ trimLines: true })).toBe("AAAB\nAA"); + + maxWidth.value = undefined; + await nextTick(); + expect(lastFrame({ trimLines: true })).toBe("AAAAAB"); +}); + +test("set max height", async () => { + const { lastFrame: constrainedFrame } = await render( + defineComponent(() => () => ( + + + A + + + )), + { columns: 100 }, + ); + expect(constrainedFrame({ raw: true })).toBe("A\n"); + + const { lastFrame: unconstrainedFrame } = await render( + defineComponent(() => () => ( + + A + + )), + { columns: 100 }, + ); + expect(unconstrainedFrame()).toBe("A"); +}); + +test("clears maxHeight on rerender", async () => { + const maxHeight = shallowRef(2); + + const { lastFrame } = await render( + defineComponent(() => () => ( + + + A + + + )), + { columns: 100 }, + ); + + expect(lastFrame({ trimLines: true })).toBe("A\n"); + + maxHeight.value = undefined; + await nextTick(); + expect(lastFrame({ trimLines: true })).toBe("A\n\n\n"); +}); + +test("set aspect ratio with width", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + X + + Y + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("┌──────┐\n│X │\n│ │\n└──────┘\nY"); +}); + +test("set aspect ratio with height", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + X + + Y + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("┌────┐\n│X │\n└────┘\nY"); +}); + +test("set aspect ratio with width and height", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + X + + Y + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("┌────┐\n│X │\n└────┘\nY"); +}); + +test("set aspect ratio with maxHeight constraint", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + X + + Y + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("┌────┐\n│X │\n└────┘\nY"); +}); + +test("clears aspectRatio on rerender", async () => { + const aspectRatio = shallowRef(2); + + const { lastFrame } = await render( + defineComponent(() => () => ( + + + X + + Y + + )), + { columns: 100 }, + ); + + expect(lastFrame({ trimLines: true })).toBe("┌──────┐\n│X │\n│ │\n└──────┘\nY"); + + aspectRatio.value = undefined; + await nextTick(); + expect(lastFrame({ trimLines: true })).toBe("┌──────┐\n│X │\n└──────┘\nY"); +}); + +test.skip("set max width in percent — known Yoga issue", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + AAAAAAAAAA + + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("AAAAAB"); +}); + +test("set max height in percent", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + + A + + + B + + )), + { columns: 100 }, + ); + expect(lastFrame({ trimLines: true })).toBe("A\n\n\nB\n\n"); +}); + +// Skipped: set width - concurrent +// Skipped: set height - concurrent diff --git a/packages/runtime/src/components/Box.ts b/packages/runtime/src/components/Box.ts index 20019f8..e3f4c6a 100644 --- a/packages/runtime/src/components/Box.ts +++ b/packages/runtime/src/components/Box.ts @@ -4,7 +4,8 @@ type Spacing = number; type FlexDirection = "row" | "row-reverse" | "column" | "column-reverse"; type FlexWrap = "nowrap" | "wrap" | "wrap-reverse"; type Align = "flex-start" | "center" | "flex-end" | "stretch" | "baseline"; -type AlignSelf = "auto" | "flex-start" | "center" | "flex-end"; +type AlignSelf = "auto" | "flex-start" | "center" | "flex-end" | "stretch" | "baseline"; +type AlignContent = Align | "space-between" | "space-around" | "space-evenly"; type Justify = | "flex-start" | "center" @@ -44,7 +45,7 @@ export const Box = defineComponent({ maxWidth: [Number, String], maxHeight: [Number, String], aspectRatio: Number, - alignContent: String as PropType, + alignContent: String as PropType, position: String as PropType<"absolute" | "relative" | "static">, top: [Number, String], right: [Number, String], diff --git a/packages/runtime/src/host/yoga.ts b/packages/runtime/src/host/yoga.ts index d1cb217..c865cad 100644 --- a/packages/runtime/src/host/yoga.ts +++ b/packages/runtime/src/host/yoga.ts @@ -197,6 +197,9 @@ function toAlign(v: string): Align { "flex-end": Yoga.ALIGN_FLEX_END, stretch: Yoga.ALIGN_STRETCH, baseline: Yoga.ALIGN_BASELINE, + "space-between": Yoga.ALIGN_SPACE_BETWEEN, + "space-around": Yoga.ALIGN_SPACE_AROUND, + "space-evenly": Yoga.ALIGN_SPACE_EVENLY, }[v]!; } diff --git a/packages/testing/src/render.ts b/packages/testing/src/render.ts index 114aa3f..6cabc53 100644 --- a/packages/testing/src/render.ts +++ b/packages/testing/src/render.ts @@ -18,8 +18,13 @@ export interface Terminal { rawMode: RawModeState; } +export interface LastFrameOptions { + raw?: boolean; + trimLines?: boolean; +} + export interface RenderResult { - lastFrame(this: void, opts?: { raw?: boolean }): string | undefined; + lastFrame(this: void, opts?: LastFrameOptions): string | undefined; frames: string[]; stdin: { write(data: string): Promise; @@ -53,7 +58,10 @@ export async function render( const frames: string[] = []; stdout.on("data", (chunk) => { - frames.push(chunk.toString()); + let raw = chunk.toString(); + // Debug-mode frame writer appends "\n"; strip it so frame height matches yoga layout + if (raw.endsWith("\n")) raw = raw.slice(0, -1); + frames.push(raw); }); const app: TuiApp = createApp(component, options.props ?? undefined); @@ -82,10 +90,16 @@ export async function render( }; return { - lastFrame: (opts?: { raw?: boolean }) => { + lastFrame: (opts?: LastFrameOptions) => { const f = frames.at(-1); if (!f) return undefined; - return opts?.raw ? f : trimFrame(f); + if (opts?.raw) return f; + if (opts?.trimLines) + return f + .split("\n") + .map((l) => l.trimEnd()) + .join("\n"); + return trimFrame(f); }, frames, stdin: {