refactor!: tree validation throws on invalid nesting instead of warn+skip
Change Box-in-Text validation from dev-only warn+skip to unconditional throw, matching Ink's reconciler behavior. Add text-outside-Text validation that throws when non-empty text-leaf nodes are inserted into box/root/static containers outside a Text context. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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("<Box> inside <Text> emits a dev warning", async () => {
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
|
||||
await render(
|
||||
defineComponent(() => () => (
|
||||
test("<Box> inside <Text> throws an error", async () => {
|
||||
const App = defineComponent(() => () => (
|
||||
<Text>
|
||||
<Box>invalid</Box>
|
||||
<Box />
|
||||
</Text>
|
||||
)),
|
||||
);
|
||||
));
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("<Box>"));
|
||||
|
||||
warnSpy.mockRestore();
|
||||
await expect(render(App)).rejects.toThrow("can’t be nested inside <Text>");
|
||||
});
|
||||
|
||||
@@ -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 <Box> (not inside <Text>) throw an error
|
||||
// 2. A <Box> nested inside <Text> throws an error
|
||||
//
|
||||
// In vue-tui:
|
||||
// - Raw text-leaf nodes inside <Box> are silently allowed (no validation yet)
|
||||
// - <Box> inside <Text> 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 <Text> component — vue-tui silently allows text-leaf inside box; validation not yet implemented",
|
||||
);
|
||||
test("fail when Box nested inside Text", async () => {
|
||||
const App = defineComponent(() => () => (
|
||||
<Text>
|
||||
<Box />
|
||||
</Text>
|
||||
));
|
||||
await expect(render(App)).rejects.toThrow("can’t be nested inside <Text>");
|
||||
});
|
||||
|
||||
test.todo(
|
||||
"fail when text node is not within <Text> component — vue-tui silently allows text-leaf inside box; validation not yet implemented",
|
||||
);
|
||||
|
||||
// Resolved: <Box> inside <Text> 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(() => () => <Box>bare text</Box>);
|
||||
await expect(render(App)).rejects.toThrow("must be rendered inside <Text>");
|
||||
});
|
||||
|
||||
@@ -142,19 +142,20 @@ export function buildNodeOps(options: TtyRendererOptions): RendererOptions<TuiNo
|
||||
}
|
||||
const parentC = parent as TuiContainer;
|
||||
|
||||
// Dev warning: <Box> inside <Text> is invalid (matches Ink's validation).
|
||||
// Inserting a box into a text context corrupts the yoga WASM layout engine.
|
||||
// <Box> inside <Text> is invalid (matches Ink's validation).
|
||||
if (child.type === "box" && isInsideTextContext(parentC)) {
|
||||
throw new Error("<Box> can’t be nested inside <Text> component");
|
||||
}
|
||||
|
||||
// Text-leaf nodes must live inside a <Text> 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 <Box> cannot be nested inside a <Text> component. " +
|
||||
"Wrap it in a sibling <Box> instead.",
|
||||
);
|
||||
return; // Skip insertion to prevent WASM crash
|
||||
throw new Error(`Text string "${child.value}" must be rendered inside <Text> component`);
|
||||
}
|
||||
|
||||
// Move semantics: if the child is already mounted (Vue's keyed reorder
|
||||
|
||||
Reference in New Issue
Block a user