fix(runtime): render synchronously on resize, matching Ink
The resize handler routed through scheduler.schedule(), deferring the repaint through the ~32ms commit throttle. Ink's resized() calls onRender() directly, and a resize is a discrete viewport change that should repaint immediately — deferring it can leave stale/overlapping content on screen for a frame. It also made the clearTerminal-on-overflow behavior depend on wall-clock timing: the #450 "shrink into overflow" test passed only because the throttled resize emitted ZERO clears (its trailing timer never fired within the test's nextTicks) and the single clear came entirely from unmount. The test asserted the right number for the wrong reason, and the dependency on real elapsed time made it flaky under CPU contention. Change the resize handler to commit() directly. Now the resize itself emits the overflow clear deterministically. Update the test to assert the clear happens ON the resize (clearsAfterResize - clearsBeforeResize === 1) after a single nextTick — no longer dependent on throttle timing.
This commit is contained in:
@@ -56,7 +56,7 @@ it("#450: non-TTY overflow transitions should never clear terminal", async () =>
|
||||
expect(clearCount).toBe(0);
|
||||
});
|
||||
|
||||
it("#450: viewport shrink into overflow should clear once", async () => {
|
||||
it("#450: viewport shrink into overflow clears exactly once on resize", async () => {
|
||||
const stdout = makeFakeWritable({ rows: 10 });
|
||||
const stderr = makeFakeWritable();
|
||||
const { stream: stdin } = makeFakeStdin();
|
||||
@@ -69,15 +69,20 @@ it("#450: viewport shrink into overflow should clear once", async () => {
|
||||
app.mount({ stdout, stdin, stderr, exitOnCtrlC: false });
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
const clearsBeforeResize = writes.filter((w) => w.includes(ansiEscapes.clearTerminal)).length;
|
||||
expect(clearsBeforeResize).toBe(0); // 8 lines fit in 10 rows — no clear yet
|
||||
|
||||
// Shrink so the content overflows the viewport. The resize renders
|
||||
// synchronously (matching Ink), so the overflow clear is emitted right here,
|
||||
// not deferred through the commit throttle.
|
||||
stdout.rows = 4;
|
||||
stdout.emit("resize");
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
|
||||
const clearsAfterResize = writes.filter((w) => w.includes(ansiEscapes.clearTerminal)).length;
|
||||
expect(clearsAfterResize - clearsBeforeResize).toBe(1);
|
||||
|
||||
app.unmount();
|
||||
const clearCount = writes.filter((w) => w.includes(ansiEscapes.clearTerminal)).length;
|
||||
expect(clearCount).toBe(1);
|
||||
});
|
||||
|
||||
it("#450: non-TTY grow-to-overflow rerender should not clear terminal", async () => {
|
||||
|
||||
@@ -658,8 +658,14 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp
|
||||
}
|
||||
|
||||
// Only listen for resize in interactive mode (matching Ink).
|
||||
// Render synchronously on resize rather than through the commit throttle:
|
||||
// a resize is a discrete event that changes the viewport, and Ink's
|
||||
// resized() handler calls onRender() directly. Deferring it through the
|
||||
// ~32ms throttle can leave stale/overlapping content on screen for a frame
|
||||
// and makes the clearTerminal-on-overflow behavior depend on wall-clock
|
||||
// timing rather than the resize itself.
|
||||
if (interactive) {
|
||||
const onResize = () => scheduler.schedule();
|
||||
const onResize = () => commit();
|
||||
stdout.on("resize", onResize);
|
||||
mountedResizeHandler = onResize;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user