fix(runtime): correct misleading measureElement() timing guidance (#139)
A bare `measureElement()` called inside `watchPostEffect` reads layout BEFORE
the commit scheduler's post-flush `calculateLayout` runs, so it returns an
uncomputed value (NaN for computed dimensions), not the current size. The JSDoc
previously recommended that exact broken call site.
Align the guidance to vue-tui's real post-flush timing: defer the read with
`nextTick(() => measureElement(ref.value))` — the pattern `useBoxMetrics` itself
uses — or read from an input/timer callback that fires after a flush; prefer
`useBoxMetrics` for reactive metrics. Also correct the stale claim that a
pre-layout read returns `{0,0}` (it returns NaN for an attached-but-uncomputed
node; `{0,0}` is only the detached case).
Adds a characterization test pinning bare-watchPostEffect = NaN vs
nextTick = real width (80), guarding against regressing to the old advice.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -313,6 +313,45 @@ describe("measureElement", () => {
|
||||
expect(measuredHeight.value).toBe(3);
|
||||
});
|
||||
|
||||
// Characterization test locking the JSDoc guidance: a BARE measureElement()
|
||||
// call inside watchPostEffect reads layout BEFORE the commit scheduler's
|
||||
// queuePostFlushCb has run calculateLayout, so it does NOT return the real
|
||||
// width (yoga reports an uncomputed value). The same read deferred with
|
||||
// nextTick — the pattern useBoxMetrics itself uses — returns the real width.
|
||||
// This guards the corrected guidance against a regression to the old
|
||||
// "call it directly from watchPostEffect" advice.
|
||||
test("bare watchPostEffect reads stale layout; nextTick reads the real width", async () => {
|
||||
const bareWidth = shallowRef<number>(-1);
|
||||
const deferredWidth = shallowRef(0);
|
||||
const App = defineComponent(() => {
|
||||
const boxRef = ref(null);
|
||||
watchPostEffect(() => {
|
||||
// Bare read: runs before calculateLayout in the same flush → unusable.
|
||||
bareWidth.value = measureElement(boxRef.value).width;
|
||||
// Deferred read: runs after the commit's layout pass → correct.
|
||||
void nextTick(() => {
|
||||
deferredWidth.value = measureElement(boxRef.value).width;
|
||||
});
|
||||
});
|
||||
// Box with no explicit width fills the 80-column terminal.
|
||||
return () => (
|
||||
<Box ref={boxRef}>
|
||||
<Text>fill</Text>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
await render(App, { columns: 80 });
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
// 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);
|
||||
});
|
||||
|
||||
test("measureElement works when render is throttled", async () => {
|
||||
const measuredWidth = shallowRef(0);
|
||||
const App = defineComponent(() => {
|
||||
|
||||
Reference in New Issue
Block a user