mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(tui): sync test awaits with throttled render pipeline (#3076)
Tests assumed renders complete synchronously after requestRender() +
nextTick + flush(), but commit 6f5f37f8 changed requestRender() to
schedule via setTimeout with a 16ms minimum interval. Tests reading
viewport state before doRender() runs saw stale data.
Add VirtualTerminal.waitForRender() helper that waits for the throttled
render to settle (nextTick + 20ms + xterm flush), and replace all
terminal.flush() calls after render-triggering actions with it.
Fixes 8 failing tests across overlay-short-content, tui-render, and
markdown test suites.
This commit is contained in:
parent
fb6ccbeaa1
commit
41377ee8ec
8 changed files with 50 additions and 43 deletions
|
|
@ -5,6 +5,7 @@
|
|||
### Fixed
|
||||
|
||||
- Fixed `Container.render()` stack overflow on long sessions by replacing `Array.push(...spread)` with a loop-based push, preventing `RangeError: Maximum call stack size exceeded` when child output exceeds the V8 call stack argument limit ([#2651](https://github.com/badlogic/pi-mono/issues/2651))
|
||||
- Fixed TUI test suite failures caused by render throttle scheduling: added `VirtualTerminal.waitForRender()` helper that waits for the 16ms throttled render pipeline to settle before asserting viewport state ([#3076](https://github.com/badlogic/pi-mono/pull/3076) by [@aliou](https://github.com/aliou))
|
||||
|
||||
## [0.66.1] - 2026-04-08
|
||||
|
||||
|
|
|
|||
|
|
@ -592,7 +592,7 @@ describe("Markdown component", () => {
|
|||
const component = new MarkdownWithInput(markdown);
|
||||
tui.addChild(component);
|
||||
tui.start();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
assert.ok(component.markdownLineCount > 0);
|
||||
const inputRow = component.markdownLineCount;
|
||||
|
|
@ -1032,7 +1032,7 @@ bar`,
|
|||
const tui = new TUI(terminal);
|
||||
tui.addChild(markdown);
|
||||
tui.start();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
const renderedLine = markdown.render(80)[0];
|
||||
assert.ok(renderedLine, "Should render heading line");
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class FocusableOverlay implements Component, Focusable {
|
|||
async function renderAndFlush(tui: TUI, terminal: VirtualTerminal): Promise<void> {
|
||||
tui.requestRender(true);
|
||||
await new Promise<void>((resolve) => process.nextTick(resolve));
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
}
|
||||
|
||||
describe("TUI overlay non-capturing", () => {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class EmptyContent implements Component {
|
|||
async function renderAndFlush(tui: TUI, terminal: VirtualTerminal): Promise<void> {
|
||||
tui.requestRender(true);
|
||||
await new Promise<void>((resolve) => process.nextTick(resolve));
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
}
|
||||
|
||||
describe("TUI overlay options", () => {
|
||||
|
|
|
|||
|
|
@ -33,8 +33,7 @@ describe("TUI overlay with short content", () => {
|
|||
|
||||
// Trigger render
|
||||
tui.start();
|
||||
await new Promise((r) => process.nextTick(r));
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
const viewport = terminal.getViewport();
|
||||
const hasOverlay = viewport.some((line) => line.includes("OVERLAY"));
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ function getCellItalic(terminal: VirtualTerminal, row: number, col: number): num
|
|||
async function renderAndFlush(tui: TUI, terminal: VirtualTerminal): Promise<void> {
|
||||
tui.requestRender(true);
|
||||
await new Promise<void>((resolve) => process.nextTick(resolve));
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
}
|
||||
|
||||
describe("TUI overlay compositing", () => {
|
||||
|
|
|
|||
|
|
@ -73,13 +73,13 @@ describe("TUI resize handling", () => {
|
|||
|
||||
component.lines = ["Line 0", "Line 1", "Line 2"];
|
||||
tui.start();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
const initialRedraws = tui.fullRedraws;
|
||||
|
||||
// Resize height
|
||||
terminal.resize(40, 15);
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
// Should have triggered a full redraw
|
||||
assert.ok(tui.fullRedraws > initialRedraws, "Height change should trigger full redraw");
|
||||
|
|
@ -100,13 +100,13 @@ describe("TUI resize handling", () => {
|
|||
|
||||
component.lines = Array.from({ length: 20 }, (_, i) => `Line ${i}`);
|
||||
tui.start();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
terminal.clearWrites();
|
||||
|
||||
const initialRedraws = tui.fullRedraws;
|
||||
for (const height of [15, 8, 14, 11]) {
|
||||
terminal.resize(40, height);
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
}
|
||||
|
||||
assert.strictEqual(tui.fullRedraws, initialRedraws, "Height change should not trigger full redraw");
|
||||
|
|
@ -128,13 +128,13 @@ describe("TUI resize handling", () => {
|
|||
|
||||
component.lines = ["Line 0", "Line 1", "Line 2"];
|
||||
tui.start();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
const initialRedraws = tui.fullRedraws;
|
||||
|
||||
// Resize width
|
||||
terminal.resize(60, 10);
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
// Should have triggered a full redraw
|
||||
assert.ok(tui.fullRedraws > initialRedraws, "Width change should trigger full redraw");
|
||||
|
|
@ -154,14 +154,14 @@ describe("TUI content shrinkage", () => {
|
|||
// Start with many lines
|
||||
component.lines = ["Line 0", "Line 1", "Line 2", "Line 3", "Line 4", "Line 5"];
|
||||
tui.start();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
const initialRedraws = tui.fullRedraws;
|
||||
|
||||
// Shrink to fewer lines
|
||||
component.lines = ["Line 0", "Line 1"];
|
||||
tui.requestRender();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
// Should have triggered a full redraw to clear empty rows
|
||||
assert.ok(tui.fullRedraws > initialRedraws, "Content shrinkage should trigger full redraw");
|
||||
|
|
@ -185,12 +185,12 @@ describe("TUI content shrinkage", () => {
|
|||
|
||||
component.lines = ["Line 0", "Line 1", "Line 2", "Line 3"];
|
||||
tui.start();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
// Shrink to single line
|
||||
component.lines = ["Only line"];
|
||||
tui.requestRender();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
const viewport = terminal.getViewport();
|
||||
assert.ok(viewport[0]?.includes("Only line"), "Single line rendered");
|
||||
|
|
@ -208,12 +208,12 @@ describe("TUI content shrinkage", () => {
|
|||
|
||||
component.lines = ["Line 0", "Line 1", "Line 2"];
|
||||
tui.start();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
// Shrink to empty
|
||||
component.lines = [];
|
||||
tui.requestRender();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
const viewport = terminal.getViewport();
|
||||
// All lines should be empty
|
||||
|
|
@ -234,18 +234,18 @@ describe("TUI differential rendering", () => {
|
|||
// Initial render: 5 identical lines
|
||||
component.lines = ["Line 0", "Line 1", "Line 2", "Line 3", "Line 4"];
|
||||
tui.start();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
// Shrink to 3 lines, all identical to before (no content changes in remaining lines)
|
||||
component.lines = ["Line 0", "Line 1", "Line 2"];
|
||||
tui.requestRender();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
// cursorRow should be 2 (last line of new content)
|
||||
// Verify by doing another render with a change on line 1
|
||||
component.lines = ["Line 0", "CHANGED", "Line 2"];
|
||||
tui.requestRender();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
const viewport = terminal.getViewport();
|
||||
// Line 1 should show "CHANGED", proving cursor tracking was correct
|
||||
|
|
@ -263,14 +263,14 @@ describe("TUI differential rendering", () => {
|
|||
// Initial render
|
||||
component.lines = ["Header", "Working...", "Footer"];
|
||||
tui.start();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
// Simulate spinner animation - only middle line changes
|
||||
const spinnerFrames = ["|", "/", "-", "\\"];
|
||||
for (const frame of spinnerFrames) {
|
||||
component.lines = ["Header", `Working ${frame}`, "Footer"];
|
||||
tui.requestRender();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
const viewport = terminal.getViewport();
|
||||
assert.ok(viewport[0]?.includes("Header"), `Header preserved: ${viewport[0]}`);
|
||||
|
|
@ -289,7 +289,7 @@ describe("TUI differential rendering", () => {
|
|||
|
||||
component.lines = ["\x1b[3mItalic", "Plain"];
|
||||
tui.start();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
assert.strictEqual(getCellItalic(terminal, 1, 0), 0);
|
||||
tui.stop();
|
||||
|
|
@ -303,12 +303,12 @@ describe("TUI differential rendering", () => {
|
|||
|
||||
component.lines = ["Line 0", "Line 1", "Line 2", "Line 3"];
|
||||
tui.start();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
// Change only first line
|
||||
component.lines = ["CHANGED", "Line 1", "Line 2", "Line 3"];
|
||||
tui.requestRender();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
const viewport = terminal.getViewport();
|
||||
assert.ok(viewport[0]?.includes("CHANGED"), `First line changed: ${viewport[0]}`);
|
||||
|
|
@ -327,12 +327,12 @@ describe("TUI differential rendering", () => {
|
|||
|
||||
component.lines = ["Line 0", "Line 1", "Line 2", "Line 3"];
|
||||
tui.start();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
// Change only last line
|
||||
component.lines = ["Line 0", "Line 1", "Line 2", "CHANGED"];
|
||||
tui.requestRender();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
const viewport = terminal.getViewport();
|
||||
assert.ok(viewport[0]?.includes("Line 0"), `Line 0 preserved: ${viewport[0]}`);
|
||||
|
|
@ -351,12 +351,12 @@ describe("TUI differential rendering", () => {
|
|||
|
||||
component.lines = ["Line 0", "Line 1", "Line 2", "Line 3", "Line 4"];
|
||||
tui.start();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
// Change lines 1 and 3, keep 0, 2, 4 the same
|
||||
component.lines = ["Line 0", "CHANGED 1", "Line 2", "CHANGED 3", "Line 4"];
|
||||
tui.requestRender();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
const viewport = terminal.getViewport();
|
||||
assert.ok(viewport[0]?.includes("Line 0"), `Line 0 preserved: ${viewport[0]}`);
|
||||
|
|
@ -377,7 +377,7 @@ describe("TUI differential rendering", () => {
|
|||
// Start with content
|
||||
component.lines = ["Line 0", "Line 1", "Line 2"];
|
||||
tui.start();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
let viewport = terminal.getViewport();
|
||||
assert.ok(viewport[0]?.includes("Line 0"), "Initial content rendered");
|
||||
|
|
@ -385,12 +385,12 @@ describe("TUI differential rendering", () => {
|
|||
// Clear to empty
|
||||
component.lines = [];
|
||||
tui.requestRender();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
// Add content back - this should work correctly even after empty state
|
||||
component.lines = ["New Line 0", "New Line 1"];
|
||||
tui.requestRender();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
viewport = terminal.getViewport();
|
||||
assert.ok(viewport[0]?.includes("New Line 0"), `New content rendered: ${viewport[0]}`);
|
||||
|
|
@ -407,13 +407,13 @@ describe("TUI differential rendering", () => {
|
|||
|
||||
component.lines = Array.from({ length: 12 }, (_, i) => `Line ${i}`);
|
||||
tui.start();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
const initialRedraws = tui.fullRedraws;
|
||||
|
||||
component.lines = Array.from({ length: 7 }, (_, i) => `Line ${i}`);
|
||||
tui.requestRender();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
assert.ok(tui.fullRedraws > initialRedraws, "Shrink should trigger a full redraw");
|
||||
assert.deepStrictEqual(terminal.getViewport(), ["Line 2", "Line 3", "Line 4", "Line 5", "Line 6"]);
|
||||
|
|
@ -429,20 +429,20 @@ describe("TUI differential rendering", () => {
|
|||
|
||||
component.lines = Array.from({ length: 8 }, (_, i) => `Line ${i}`);
|
||||
tui.start();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
const initialRedraws = tui.fullRedraws;
|
||||
|
||||
component.lines = ["Line 0", "Line 1"];
|
||||
tui.requestRender();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
assert.ok(tui.fullRedraws > initialRedraws, "Shrink should reset the viewport with a full redraw");
|
||||
const redrawsAfterShrink = tui.fullRedraws;
|
||||
|
||||
component.lines = ["Line 0", "Line 1", "Line 2"];
|
||||
tui.requestRender();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
assert.strictEqual(tui.fullRedraws, redrawsAfterShrink, "Append should stay on the differential path");
|
||||
assert.deepStrictEqual(terminal.getViewport(), ["Line 0", "Line 1", "Line 2", "", ""]);
|
||||
|
|
@ -466,20 +466,20 @@ describe("TUI differential rendering", () => {
|
|||
chat.lines = longChat;
|
||||
editor.lines = editorLines;
|
||||
tui.start();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
editor.lines = selectorLines;
|
||||
tui.requestRender();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
editor.lines = editorLines;
|
||||
tui.requestRender();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
const redrawsBeforeSwitch = tui.fullRedraws;
|
||||
chat.lines = shortChat;
|
||||
tui.requestRender();
|
||||
await terminal.flush();
|
||||
await terminal.waitForRender();
|
||||
|
||||
assert.ok(tui.fullRedraws > redrawsBeforeSwitch, "Branch switch should trigger a full redraw");
|
||||
|
||||
|
|
|
|||
|
|
@ -206,4 +206,11 @@ export class VirtualTerminal implements Terminal {
|
|||
y: buffer.cursorY,
|
||||
};
|
||||
}
|
||||
|
||||
/** Wait for TUI's throttled render pipeline to settle. */
|
||||
async waitForRender(): Promise<void> {
|
||||
await new Promise<void>((resolve) => process.nextTick(resolve));
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 20));
|
||||
await this.flush();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue