fix(runtime): measureElement coerces a non-finite (pre-layout) dimension to 0, not NaN (#191)
yoga's getComputedWidth()/getComputedHeight() return NaN for a node not yet
through a layout pass, and `?? 0` does NOT catch NaN (NaN ?? 0 === NaN), so a
pre-layout / mis-timed measureElement() read returned { width: NaN, height: NaN }
— poisoning user layout math (terminalWidth - measured.width → NaN → a NaN width
prop). Coerce non-finite computed dims to 0 (Number.isFinite(v) ? v : 0).
0 is a safe sentinel ("not yet computed"), not the box's true size — the correct
usage is to read AFTER layout (the JSDoc already steers callers to defer via
nextTick). It's chosen because it is Ink's clear intent (`?? 0`) and matches the
DOM precedent (getBoundingClientRect on display:none / img.naturalWidth pre-load
return 0, not NaN). Deliberate, low-risk robustness divergence from Ink v7.0.4's
NaN-leaking `?? 0`; recorded in ink-divergences.md.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,9 @@ import {
|
||||
useWindowSize,
|
||||
createApp,
|
||||
} from "@vue-tui/runtime";
|
||||
// Internal host primitives — build a node + attach yoga WITHOUT a layout pass so
|
||||
// the pre-layout read below is deterministic (no render/commit timing involved).
|
||||
import { createBox, attachYoga } from "@vue-tui/runtime/internal";
|
||||
import { makeFakeStdin, makeFakeWritable } from "../lifecycle/test-streams.ts";
|
||||
|
||||
describe("useBoxMetrics", () => {
|
||||
@@ -204,6 +207,24 @@ describe("measureElement", () => {
|
||||
expect(measureElement({ yoga: fakeYoga })).toEqual({ width: 42, height: 17 });
|
||||
});
|
||||
|
||||
// A node whose yoga node exists but has NOT been through a layout pass:
|
||||
// yoga.getComputedWidth()/Height() return NaN, and the old `?? 0` did NOT
|
||||
// catch NaN (NaN ?? 0 === NaN). The pre-layout / mis-timed read must degrade
|
||||
// to a finite {0,0} (a safe sentinel) instead of leaking NaN into user layout
|
||||
// math (e.g. terminalWidth - measured.width → NaN → a NaN width prop).
|
||||
test("returns finite { width: 0, height: 0 } for a node not yet laid out (no NaN leak)", () => {
|
||||
const box = createBox();
|
||||
attachYoga(box); // attaches a yoga node but does NOT calculateLayout
|
||||
// Guard the premise: pre-layout yoga genuinely reports NaN (not 0), so this
|
||||
// test exercises the real coercion, not a no-op.
|
||||
expect(Number.isNaN(box.yoga.getComputedWidth())).toBe(true);
|
||||
|
||||
const result = measureElement(box);
|
||||
expect(Number.isFinite(result.width)).toBe(true);
|
||||
expect(Number.isFinite(result.height)).toBe(true);
|
||||
expect(result).toEqual({ width: 0, height: 0 });
|
||||
});
|
||||
|
||||
test("resolves through $el for component instances", () => {
|
||||
const fakeYoga = {
|
||||
getComputedWidth: () => 30,
|
||||
@@ -354,10 +375,13 @@ describe("measureElement", () => {
|
||||
// The deferred (documented-correct) read returns the real terminal width.
|
||||
expect(deferredWidth.value).toBe(80);
|
||||
// The bare read is the uncomputed pre-layout value. On this first-render path
|
||||
// yoga's getComputedWidth() is NaN (and measureElement's `?? 0` does not
|
||||
// coalesce NaN), so pin exactly that — a plain `!== 80` would also pass if the
|
||||
// watcher never ran or returned 0, which would not prove the bare path is stale.
|
||||
expect(Number.isNaN(bareWidth.value)).toBe(true);
|
||||
// yoga's getComputedWidth() is NaN; measureElement coerces non-finite dims to
|
||||
// the safe sentinel 0, so the bare read is a FINITE 0 — not NaN and not the
|
||||
// real 80. Pin all three facts: it ran (not the -1 seed), it is finite (the
|
||||
// NaN-leak fix), and it is stale (0 ≠ 80, proving the bare path is pre-layout).
|
||||
expect(bareWidth.value).not.toBe(-1);
|
||||
expect(Number.isFinite(bareWidth.value)).toBe(true);
|
||||
expect(bareWidth.value).toBe(0);
|
||||
});
|
||||
|
||||
test("measureElement works when render is throttled", async () => {
|
||||
|
||||
Reference in New Issue
Block a user