From ce9ed4c399a9fcac1690b3dc0fc9f9285aa8ba5d Mon Sep 17 00:00:00 2001 From: Yunfei He Date: Mon, 25 May 2026 21:43:08 +0800 Subject: [PATCH] test: port Ink border tests (48 tests) Covers border styles, edge hiding, colors, dim, per-side dim, nested boxes, wide chars, emojis, and reactive updates. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../integration/components/borders.test.tsx | 1094 +++++++++++++++++ 1 file changed, 1094 insertions(+) create mode 100644 packages/runtime-tests/integration/components/borders.test.tsx diff --git a/packages/runtime-tests/integration/components/borders.test.tsx b/packages/runtime-tests/integration/components/borders.test.tsx new file mode 100644 index 0000000..72c787b --- /dev/null +++ b/packages/runtime-tests/integration/components/borders.test.tsx @@ -0,0 +1,1094 @@ +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"; + +// single node — full width box +test("single node - full width box", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Hello World + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Hello World │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" + `); +}); + +// single node — full width box with colorful border +test("single node - full width box with colorful border", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Hello World + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Hello World │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" + `); +}); + +// single node — fit-content box +test("single node - fit-content box", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Hello World + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭───────────╮ + │Hello World│ + ╰───────────╯" + `); +}); + +// single node — fit-content box with wide characters +test("single node - fit-content box with wide characters", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + こんにちは + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭──────────╮ + │こんにちは│ + ╰──────────╯" + `); +}); + +// single node — fit-content box with emojis +test("single node - fit-content box with emojis", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + 🌊🌊 + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭────╮ + │🌊🌊│ + ╰────╯" + `); +}); + +// single node — fit-content box with variation selector emojis +test("single node - fit-content box with variation selector emojis", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + 🌡️⚠️✅ + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭──────╮ + │🌡️⚠️✅ + ╰──────╯" + `); +}); + +// single node — fixed width box +test("single node - fixed width box", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Hello World + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭──────────────────╮ + │Hello World │ + ╰──────────────────╯" + `); +}); + +// single node — fixed width and height box +test("single node - fixed width and height box", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Hello World + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭──────────────────╮ + │Hello World │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────╯" + `); +}); + +// single node — box with padding +test("single node - box with padding", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Hello World + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭─────────────╮ + │ │ + │ Hello World │ + │ │ + ╰─────────────╯" + `); +}); + +// single node — box with horizontal alignment +test("single node - box with horizontal alignment", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Hello World + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭──────────────────╮ + │Hello World │ + ╰──────────────────╯" + `); +}); + +// single node — box with vertical alignment +test("single node - box with vertical alignment", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Hello World + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭───────────╮ + │Hello World│ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────╯" + `); +}); + +// single node — box with wrapping +test("single node - box with wrapping", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Hello World + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭────────╮ + │Hello │ + │World │ + ╰────────╯" + `); +}); + +// multiple nodes — full width box +test("multiple nodes - full width box", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + {"Hello "}World + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Hello World │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" + `); +}); + +// multiple nodes — full width box with colorful border +test("multiple nodes - full width box with colorful border", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + {"Hello "}World + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Hello World │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" + `); +}); + +// multiple nodes — fit-content box +test("multiple nodes - fit-content box", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + {"Hello "}World + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭───────────╮ + │Hello World│ + ╰───────────╯" + `); +}); + +// multiple nodes — fixed width box +test("multiple nodes - fixed width box", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + {"Hello "}World + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭──────────────────╮ + │Hello World │ + ╰──────────────────╯" + `); +}); + +// multiple nodes — fixed width and height box +test("multiple nodes - fixed width and height box", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + {"Hello "}World + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭──────────────────╮ + │Hello World │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰──────────────────╯" + `); +}); + +// multiple nodes — box with padding +test("multiple nodes - box with padding", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + {"Hello "}World + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭─────────────╮ + │ │ + │ Hello World │ + │ │ + ╰─────────────╯" + `); +}); + +// multiple nodes — box with horizontal alignment +test("multiple nodes - box with horizontal alignment", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + {"Hello "}World + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭──────────────────╮ + │Hello World │ + ╰──────────────────╯" + `); +}); + +// multiple nodes — box with vertical alignment +test("multiple nodes - box with vertical alignment", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + {"Hello "}World + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭───────────╮ + │Hello World│ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + ╰───────────╯" + `); +}); + +// multiple nodes — box with wrapping +test("multiple nodes - box with wrapping", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + {"Hello "}World + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭────────╮ + │Hello │ + │World │ + ╰────────╯" + `); +}); + +// multiple nodes — box with wrapping and long first node +test("multiple nodes - box with wrapping and long first node", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + {"Helloooooo"} World + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭────────╮ + │Helloooo│ + │oo World│ + ╰────────╯" + `); +}); + +// multiple nodes — box with wrapping and very long first node +test("multiple nodes - box with wrapping and very long first node", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + {"Hellooooooooooooo"} World + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭────────╮ + │Helloooo│ + │oooooooo│ + │o World │ + ╰────────╯" + `); +}); + +// nested boxes +test("nested boxes", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + Hello World + + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭──────────────────────────────────────╮ + │ │ + │ ╭──────────────────────────────────╮ │ + │ │ │ │ + │ │ Hello World │ │ + │ │ │ │ + │ ╰──────────────────────────────────╯ │ + │ │ + ╰──────────────────────────────────────╯" + `); +}); + +// nested boxes — fit-content box with wide characters on flex-direction row +test("nested boxes - fit-content box with wide characters on flex-direction row", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + ミスター + + + スポック + + + カーク船長 + + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭────────────╮ + │╭──────────╮│ + ││ミスター ││ + │╰──────────╯│ + │╭──────────╮│ + ││スポック ││ + │╰──────────╯│ + │╭──────────╮│ + ││カーク船長││ + │╰──────────╯│ + ╰────────────╯" + `); +}); + +// nested boxes — fit-content box with emojis on flex-direction row +test("nested boxes - fit-content box with emojis on flex-direction row", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + 🦾 + + + 🌏 + + + 😋 + + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭────╮ + │╭──╮│ + ││🦾││ + │╰──╯│ + │╭──╮│ + ││🌏││ + │╰──╯│ + │╭──╮│ + ││😋││ + │╰──╯│ + ╰────╯" + `); +}); + +// nested boxes — fit-content box with wide characters on flex-direction column +test("nested boxes - fit-content box with wide characters on flex-direction column", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + ミスター + + + スポック + + + カーク船長 + + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭────────────╮ + │╭──────────╮│ + ││ミスター ││ + │╰──────────╯│ + │╭──────────╮│ + ││スポック ││ + │╰──────────╯│ + │╭──────────╮│ + ││カーク船長││ + │╰──────────╯│ + ╰────────────╯" + `); +}); + +// nested boxes — fit-content box with emojis on flex-direction column +test("nested boxes - fit-content box with emojis on flex-direction column", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + 🦾 + + + 🌏 + + + 😋 + + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭────╮ + │╭──╮│ + ││🦾││ + │╰──╯│ + │╭──╮│ + ││🌏││ + │╰──╯│ + │╭──╮│ + ││😋││ + │╰──╯│ + ╰────╯" + `); +}); + +// render border after update — reactive borderColor changes +test("render border after update", async () => { + const borderColor = shallowRef(undefined); + const { lastFrame } = await render( + defineComponent(() => () => ( + + Hello World + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Hello World │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" + `); + + borderColor.value = "green"; + await nextTick(); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Hello World │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" + `); + + borderColor.value = undefined; + await nextTick(); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Hello World │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" + `); +}); + +// render border edge changes after update when borderStyle is unchanged +test("render border edge changes after update when borderStyle is unchanged", async () => { + const showTop = shallowRef(true); + const { lastFrame } = await render( + defineComponent(() => () => ( + + Content + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭───────╮ + │Content│ + ╰───────╯" + `); + + showTop.value = false; + await nextTick(); + expect(lastFrame()).toMatchInlineSnapshot(` + " Content + ╰───────╯" + `); + + showTop.value = true; + await nextTick(); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭───────╮ + │Content│ + ╰───────╯" + `); +}); + +// hide top border +test("hide top border", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Above + + Content + + Below + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "Above + Content + ╰───────╯ + Below" + `); +}); + +// hide bottom border +test("hide bottom border", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Above + + Content + + Below + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "Above + ╭───────╮ + Content + Below" + `); +}); + +// hide top and bottom borders +test("hide top and bottom borders", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Above + + Content + + Below + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "Above + Content + Below" + `); +}); + +// hide left border +test("hide left border", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Above + + Content + + Below + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "Above + ───────╮ + Content│ + ───────╯ + Below" + `); +}); + +// hide right border +test("hide right border", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Above + + Content + + Below + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "Above + ╭─────── + │Content + ╰─────── + Below" + `); +}); + +// hide left and right border +test("hide left and right border", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Above + + Content + + Below + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "Above + ─────── + Content + ─────── + Below" + `); +}); + +// hide all borders +test("hide all borders", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Above + + Content + + Below + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "Above + Content + Below" + `); +}); + +// change color of top border +test("change color of top border", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Above + + Content + + Below + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "Above + ╭───────╮ + │Content│ + ╰───────╯ + Below" + `); +}); + +// change color of bottom border +test("change color of bottom border", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Above + + Content + + Below + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "Above + ╭───────╮ + │Content│ + ╰───────╯ + Below" + `); +}); + +// change color of left border +test("change color of left border", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Above + + Content + + Below + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "Above + ╭───────╮ + │Content│ + ╰───────╯ + Below" + `); +}); + +// change color of right border +test("change color of right border", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Above + + Content + + Below + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "Above + ╭───────╮ + │Content│ + ╰───────╯ + Below" + `); +}); + +// custom border style — uses "arrow" named style (same chars as the Ink custom object) +test("custom border style", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Content + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "↘↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ + →Content ← + ↗↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" + `); +}); + +// dim border color +test("dim border color", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Content + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Content │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" + `); +}); + +// dim top border color +test("dim top border color", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Above + + Content + + Below + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "Above + ╭───────╮ + │Content│ + ╰───────╯ + Below" + `); +}); + +// dim bottom border color +test("dim bottom border color", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Above + + Content + + Below + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "Above + ╭───────╮ + │Content│ + ╰───────╯ + Below" + `); +}); + +// dim left border color +test("dim left border color", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Above + + Content + + Below + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "Above + ╭───────╮ + │Content│ + ╰───────╯ + Below" + `); +}); + +// dim right border color +test("dim right border color", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + Above + + Content + + Below + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "Above + ╭───────╮ + │Content│ + ╰───────╯ + Below" + `); +}); + +// borderDimColor should not dim styled child Text touching left edge +test("borderDimColor does not dim styled child Text touching left edge", async () => { + const { lastFrame } = await render( + defineComponent(() => () => ( + + + styled text + + + )), + { columns: 100 }, + ); + expect(lastFrame()).toMatchInlineSnapshot(` + "╭───────────╮ + │styled text│ + ╰───────────╯" + `); +});