From 1bd91dbbb91a8f0b583fc9c703901135c1495f52 Mon Sep 17 00:00:00 2001 From: Yunfei He Date: Tue, 9 Jun 2026 11:32:19 +0800 Subject: [PATCH] test(runtime): pin absolute-child position to the padding box; fix wording MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses PR review: the containing block for an absolutely-positioned child is the **padding box** (inside the borders), not the "border-box". Verified by running yoga (an abs child at top:0/left:0 insets by the border only, never by padding — confirmed across border/padding combos) and the real Ink/vue-tui renderers (X lands at the inner-border edge, byte-identical in both). - Tighten the regression test: exact-frame assertions instead of `toContain`, including a border+padding case that distinguishes the padding box from the content box — the assertion that would have caught the original wording slip (presence-only assertions could not). - Correct "containing block (border-box)" -> "padding box (inside the borders)" in paint.ts, layout-guards.ts, and ink-divergences.md. (The unrelated "border-box-like" *sizing* notes are correct and left as-is.) No runtime behavior change; the code already used yoga's computed position. Co-Authored-By: Claude Opus 4.8 (1M context) --- .agents/docs/ink-divergences.md | 6 +-- .../absolute-in-degenerate-box.test.tsx | 39 +++++++++++++++---- packages/runtime/src/host/layout-guards.ts | 7 ++-- packages/runtime/src/paint/paint.ts | 5 ++- 4 files changed, 42 insertions(+), 15 deletions(-) diff --git a/.agents/docs/ink-divergences.md b/.agents/docs/ink-divergences.md index dae4dab..c605a42 100644 --- a/.agents/docs/ink-divergences.md +++ b/.agents/docs/ink-divergences.md @@ -410,9 +410,9 @@ different runtime behavior, ownership rule, or out-of-contract handling. dimension is zero. Paint applies the same inner-content gate, so the in-flow child subtree neither reserves invisible rows nor writes glyphs outside a nonexistent content area. Border and background are still painted as far as the outer area permits. - **Absolutely-positioned children are exempt** — their region is the containing block - (border-box), not the content rect, so they still lay out and paint (clipped only by - `overflow:hidden`), matching Ink. Positive-size content areas keep the + **Absolutely-positioned children are exempt** — their containing block is the padding box + (inside the borders), not the content rect, so they still lay out and paint (clipped only + by `overflow:hidden`), matching Ink. Positive-size content areas keep the existing overflow behavior; this is not a blanket `overflow:hidden`. - **Layout model guidance:** primitive `Box` should preserve the Yoga/flexbox model rather than paper over it with ad-hoc layout corrections. Defaults such as `flexShrink: 1` are diff --git a/packages/runtime-tests/integration/layout/absolute-in-degenerate-box.test.tsx b/packages/runtime-tests/integration/layout/absolute-in-degenerate-box.test.tsx index 1707068..e1fec24 100644 --- a/packages/runtime-tests/integration/layout/absolute-in-degenerate-box.test.tsx +++ b/packages/runtime-tests/integration/layout/absolute-in-degenerate-box.test.tsx @@ -5,13 +5,18 @@ import { Box, Text } from "@vue-tui/runtime"; // A Box whose inner content area collapses to zero must still paint its // position:"absolute" children — an absolutely-positioned child is placed -// against the containing block (border-box), not the (nonexistent) content +// against its containing block, the **padding box** (the area inside the +// borders; padding itself does NOT inset it), not the (nonexistent) content // rect, so the zero-content guard must not suppress it. Ink v7.0.4 paints -// these (verified by running real Ink: a w=2 h=2 single-border box with an -// absolute child renders "┌┐#\n└X"); vue-tui previously suppressed ALL -// children, including absolute ones, rendering "┌┐#\n└┘". +// these (verified by running real Ink); vue-tui previously suppressed ALL +// children. Flow children stay suppressed (the blessed degenerate-box +// divergence). +// +// Assertions use exact frames (not `toContain`) so they pin WHERE the child +// lands — the padding-box edge — distinguishing it from the border box and +// the content box. -test("absolute child paints when border eats the whole content area (w=2 h=2)", async () => { +test("absolute child paints at the padding-box edge when content area is zero (w=2 h=2)", async () => { const { lastFrame } = await render( defineComponent(() => () => ( @@ -25,7 +30,27 @@ test("absolute child paints when border eats the whole content area (w=2 h=2)", )), { columns: 100 }, ); - expect(lastFrame()).toContain("X"); + // X lands at the inner-border (padding-box) corner — it replaces the `┘`. + expect(lastFrame()).toBe("┌┐#\n└X"); +}); + +test("absolute child positions against the padding box, not the content box", async () => { + // Discriminator: with border=1 AND padding=1, the padding-box edge is at + // row 1 / col 1 (inside the border, before padding). A content-box containing + // block would instead put X at row 2 / col 2. Verified byte-identical in real + // Ink v7.0.4. This is the assertion that would have caught a "border-box" vs + // "padding-box" mistake. + const { lastFrame } = await render( + defineComponent(() => () => ( + + + X + + + )), + { columns: 100 }, + ); + expect(lastFrame()).toBe("┌─────┐\n│X │\n│ │\n│ │\n└─────┘"); }); test("flow (non-absolute) child stays suppressed when the content area is zero", async () => { @@ -42,5 +67,5 @@ test("flow (non-absolute) child stays suppressed when the content area is zero", )), { columns: 100 }, ); - expect(lastFrame()).not.toContain("Y"); + expect(lastFrame()).toBe("┌┐#\n└┘"); }); diff --git a/packages/runtime/src/host/layout-guards.ts b/packages/runtime/src/host/layout-guards.ts index e5c01ac..57dcbbe 100644 --- a/packages/runtime/src/host/layout-guards.ts +++ b/packages/runtime/src/host/layout-guards.ts @@ -64,9 +64,10 @@ function applyZeroContentGuards(node: TuiNode, guarded: Map): const inner = getBoxInnerSize(node); if (inner.width === 0 || inner.height === 0) { for (const child of node.children) { - // Absolutely-positioned children are placed against the containing - // block (border-box), not the content rect, so the zero-content guard - // must not hide them — Ink lays them out and paints them regardless. + // Absolutely-positioned children are placed against their containing + // block — the padding box (inside the borders) — not the content rect, + // so the zero-content guard must not hide them; Ink lays them out and + // paints them regardless. if (hasYoga(child) && child.yoga.getPositionType() === Yoga.POSITION_TYPE_ABSOLUTE) { continue; } diff --git a/packages/runtime/src/paint/paint.ts b/packages/runtime/src/paint/paint.ts index bca0dc5..a7c3802 100644 --- a/packages/runtime/src/paint/paint.ts +++ b/packages/runtime/src/paint/paint.ts @@ -685,8 +685,9 @@ function paintNode( const contentMetrics = getBoxContentMetrics(node, w, h); // A Box with no inner content area has no legal paint region for FLOW // children. Absolutely-positioned children, though, are placed against - // the containing block (border-box), not the content rect — Ink still - // paints them — so paint just those and keep flow children suppressed. + // their containing block — the padding box (inside the borders) — not the + // content rect, so Ink still paints them; paint just those and keep flow + // children suppressed. if (contentMetrics.width === 0 || contentMetrics.height === 0) { for (const child of node.children) { const childYoga = (child as { yoga?: { getPositionType?: () => number } }).yoga;