diff --git a/packages/runtime-tests/integration/components/box-in-text-validation.test.tsx b/packages/runtime-tests/integration/components/box-in-text-validation.test.tsx index 7c3487f..866015c 100644 --- a/packages/runtime-tests/integration/components/box-in-text-validation.test.tsx +++ b/packages/runtime-tests/integration/components/box-in-text-validation.test.tsx @@ -1,20 +1,14 @@ import { defineComponent } from "vue"; -import { expect, test, vi } from "vite-plus/test"; +import { expect, test } from "vite-plus/test"; import { render } from "@vue-tui/testing"; import { Box, Text } from "@vue-tui/runtime"; -test(" inside emits a dev warning", async () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); +test(" inside throws an error", async () => { + const App = defineComponent(() => () => ( + + + + )); - await render( - defineComponent(() => () => ( - - invalid - - )), - ); - - expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("")); - - warnSpy.mockRestore(); + await expect(render(App)).rejects.toThrow("can’t be nested inside "); }); diff --git a/packages/runtime-tests/integration/lifecycle/error-handling.test.tsx b/packages/runtime-tests/integration/lifecycle/error-handling.test.tsx index 5416988..935b257 100644 --- a/packages/runtime-tests/integration/lifecycle/error-handling.test.tsx +++ b/packages/runtime-tests/integration/lifecycle/error-handling.test.tsx @@ -1,7 +1,7 @@ import { defineComponent, nextTick, shallowRef } from "vue"; import { expect, test } from "vite-plus/test"; import { render } from "@vue-tui/testing"; -import { Text, useExit } from "@vue-tui/runtime"; +import { Box, Text, useExit } from "@vue-tui/runtime"; test("setup() throw rejects render()", async () => { const Boom = defineComponent(() => { @@ -77,22 +77,17 @@ test.todo( ); // --- Ink error validation tests --- -// In Ink these tests use React error boundaries to validate that: -// 1. Raw text strings inside (not inside ) throw an error -// 2. A nested inside throws an error -// -// In vue-tui: -// - Raw text-leaf nodes inside are silently allowed (no validation yet) -// - inside causes a WASM yoga crash (table index out of bounds) -// All three are marked todo until the runtime adds proper validation. -test.todo( - "fail when text nodes are not within component — vue-tui silently allows text-leaf inside box; validation not yet implemented", -); +test("fail when Box nested inside Text", async () => { + const App = defineComponent(() => () => ( + + + + )); + await expect(render(App)).rejects.toThrow("can’t be nested inside "); +}); -test.todo( - "fail when text node is not within component — vue-tui silently allows text-leaf inside box; validation not yet implemented", -); - -// Resolved: inside now emits a dev warning and skips insertion -// to prevent WASM crash. See box-in-text-validation.test.tsx. +test("fail when text string not within Text component", async () => { + const App = defineComponent(() => () => bare text); + await expect(render(App)).rejects.toThrow("must be rendered inside "); +}); diff --git a/packages/runtime/src/host/node-ops.ts b/packages/runtime/src/host/node-ops.ts index 2cf2b85..0b27676 100644 --- a/packages/runtime/src/host/node-ops.ts +++ b/packages/runtime/src/host/node-ops.ts @@ -142,19 +142,20 @@ export function buildNodeOps(options: TtyRendererOptions): RendererOptions inside is invalid (matches Ink's validation). - // Inserting a box into a text context corrupts the yoga WASM layout engine. + // inside is invalid (matches Ink's validation). + if (child.type === "box" && isInsideTextContext(parentC)) { + throw new Error(" can’t be nested inside component"); + } + + // Text-leaf nodes must live inside a context. + // Skip empty text-leaves — Vue uses them as fragment anchors. if ( - process.env["NODE_ENV"] !== "production" && - child.type === "box" && - isInsideTextContext(parentC) + child.type === "text-leaf" && + child.value !== "" && + (parentC.type === "box" || parentC.type === "root" || parentC.type === "static") && + !isInsideTextContext(parentC) ) { - // eslint-disable-next-line no-console - console.warn( - "[vue-tui] A cannot be nested inside a component. " + - "Wrap it in a sibling instead.", - ); - return; // Skip insertion to prevent WASM crash + throw new Error(`Text string "${child.value}" must be rendered inside component`); } // Move semantics: if the child is already mounted (Vue's keyed reorder