test(pi-tui): add regression tests for slash command TUI interactions
Add 3 new tests covering editor↔selector/input component swaps that happen during /gsd prefs, /gsd migrate, and /gsd setup: - editor-to-selector swap: verifies cursor tracking when editor with CURSOR_MARKER is replaced by a selector without one - selector-to-editor swap: verifies cursor restores to CURSOR_MARKER position when editor returns after selector dismissal - input component swap: verifies typing in prefs wizard text input produces correct cursor movement without jumps All tests confirm hardwareCursorRow baseline computes correct movement deltas for these interactive component transitions.
This commit is contained in:
parent
d29d086f6a
commit
d98456cad7
1 changed files with 176 additions and 0 deletions
|
|
@ -101,6 +101,182 @@ describe("TUI cursor tracking regression (#3764)", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("handles editor-to-selector swap without cursor corruption", () => {
|
||||
// Simulates /gsd prefs: editor with CURSOR_MARKER is replaced by
|
||||
// a selector component (no CURSOR_MARKER) that has different line count.
|
||||
const terminal = new MockTTYTerminal();
|
||||
const tui = new TUI(terminal, false);
|
||||
|
||||
// Initial state: chat + editor with cursor marker (typical idle state)
|
||||
const chatLines = Array.from({ length: 15 }, (_, i) => `chat line ${i}`);
|
||||
const editorComponent = new DynamicLinesComponent([
|
||||
...chatLines,
|
||||
`> ${CURSOR_MARKER}`, // editor input line with cursor
|
||||
]);
|
||||
|
||||
tui.addChild(editorComponent);
|
||||
(tui as any).doRender();
|
||||
|
||||
// Cursor should be at the CURSOR_MARKER line (row 15)
|
||||
assert.strictEqual(
|
||||
(tui as any).hardwareCursorRow,
|
||||
15,
|
||||
"hardwareCursorRow should be at editor cursor position (row 15)",
|
||||
);
|
||||
|
||||
// Now swap editor for selector (simulating showExtensionSelector)
|
||||
terminal.writtenData = [];
|
||||
editorComponent.lines = [
|
||||
...chatLines,
|
||||
"─── Select preference ───",
|
||||
"→ Model routing",
|
||||
" Timeouts",
|
||||
" Budget",
|
||||
" Cancel",
|
||||
"─────────────────────────",
|
||||
];
|
||||
|
||||
(tui as any).doRender();
|
||||
|
||||
assert.ok(terminal.writtenData.length >= 1, "selector render should produce output");
|
||||
|
||||
const buffer = terminal.writtenData[0];
|
||||
// Verify no extremely large cursor jumps that would cause visual corruption
|
||||
const hugeJump = buffer.match(/\x1b\[(\d{2,})A/);
|
||||
if (hugeJump) {
|
||||
const jumpSize = parseInt(hugeJump[1], 10);
|
||||
assert.ok(
|
||||
jumpSize < 20,
|
||||
`cursor jump of ${jumpSize} rows is too large — likely a baseline desync, got: ${JSON.stringify(buffer.slice(0, 200))}`,
|
||||
);
|
||||
}
|
||||
|
||||
// hardwareCursorRow should NOT be at old IME position
|
||||
// since there's no CURSOR_MARKER in the selector
|
||||
const hwRow = (tui as any).hardwareCursorRow;
|
||||
assert.ok(
|
||||
hwRow >= 15 && hwRow <= 20,
|
||||
`hardwareCursorRow should be at rendered content (${hwRow}), not stuck at old IME position`,
|
||||
);
|
||||
|
||||
// Now simulate user pressing ↓ in selector (one line changes)
|
||||
terminal.writtenData = [];
|
||||
editorComponent.lines = [
|
||||
...chatLines,
|
||||
"─── Select preference ───",
|
||||
" Model routing",
|
||||
"→ Timeouts",
|
||||
" Budget",
|
||||
" Cancel",
|
||||
"─────────────────────────",
|
||||
];
|
||||
|
||||
(tui as any).doRender();
|
||||
|
||||
if (terminal.writtenData.length > 0) {
|
||||
const navBuffer = terminal.writtenData[0];
|
||||
// The differential render should only update the 2 changed lines (16 and 17)
|
||||
// Verify no large upward jumps from wrong baseline
|
||||
const navJump = navBuffer.match(/\x1b\[(\d{2,})A/);
|
||||
if (navJump) {
|
||||
const jumpSize = parseInt(navJump[1], 10);
|
||||
assert.ok(
|
||||
jumpSize < 20,
|
||||
`navigation caused jump of ${jumpSize} rows — cursor baseline may be wrong`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("handles selector-to-editor swap restoring cursor correctly", () => {
|
||||
// After dismissing a selector, the editor returns with CURSOR_MARKER.
|
||||
// The cursor must move to the new marker position without corruption.
|
||||
const terminal = new MockTTYTerminal();
|
||||
const tui = new TUI(terminal, false);
|
||||
|
||||
const chatLines = Array.from({ length: 10 }, (_, i) => `chat ${i}`);
|
||||
const component = new DynamicLinesComponent([
|
||||
...chatLines,
|
||||
"─── Selector ───",
|
||||
"→ Option A",
|
||||
" Option B",
|
||||
"────────────────",
|
||||
]);
|
||||
|
||||
tui.addChild(component);
|
||||
(tui as any).doRender();
|
||||
|
||||
// No CURSOR_MARKER → cursor stays at last rendered line
|
||||
const hwRowAfterSelector = (tui as any).hardwareCursorRow;
|
||||
|
||||
// Swap back to editor with CURSOR_MARKER
|
||||
terminal.writtenData = [];
|
||||
component.lines = [
|
||||
...chatLines,
|
||||
`> ${CURSOR_MARKER}`,
|
||||
];
|
||||
|
||||
(tui as any).doRender();
|
||||
|
||||
// CURSOR_MARKER is at row 10 — cursor should be positioned there
|
||||
assert.strictEqual(
|
||||
(tui as any).hardwareCursorRow,
|
||||
10,
|
||||
"hardwareCursorRow should move to editor cursor after selector dismiss",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles input component swap (prefs wizard text input)", () => {
|
||||
// Simulates /gsd prefs input step: selector replaced by text input with cursor
|
||||
const terminal = new MockTTYTerminal();
|
||||
const tui = new TUI(terminal, false);
|
||||
|
||||
const chatLines = Array.from({ length: 8 }, (_, i) => `msg ${i}`);
|
||||
const component = new DynamicLinesComponent([
|
||||
...chatLines,
|
||||
"─── Enter value ───",
|
||||
`Value: ${CURSOR_MARKER}`,
|
||||
"───────────────────",
|
||||
]);
|
||||
|
||||
tui.addChild(component);
|
||||
(tui as any).doRender();
|
||||
|
||||
assert.strictEqual(
|
||||
(tui as any).hardwareCursorRow,
|
||||
9,
|
||||
"hardwareCursorRow should be at input cursor (row 9)",
|
||||
);
|
||||
|
||||
// Simulate typing in the input
|
||||
terminal.writtenData = [];
|
||||
component.lines = [
|
||||
...chatLines,
|
||||
"─── Enter value ───",
|
||||
`Value: hello${CURSOR_MARKER}`,
|
||||
"───────────────────",
|
||||
];
|
||||
|
||||
(tui as any).doRender();
|
||||
|
||||
assert.ok(terminal.writtenData.length >= 1, "typing should trigger render");
|
||||
|
||||
const buffer = terminal.writtenData[0];
|
||||
// Should not jump to wrong row — only line 9 changed
|
||||
const upJump = buffer.match(/\x1b\[(\d+)A/);
|
||||
if (upJump) {
|
||||
const jumpSize = parseInt(upJump[1], 10);
|
||||
// Cursor was at row 9 (IME), need to go to row 9 (changed line) = no jump needed
|
||||
assert.ok(jumpSize <= 1, `typing in input caused unexpected up-jump of ${jumpSize}`);
|
||||
}
|
||||
|
||||
assert.strictEqual(
|
||||
(tui as any).hardwareCursorRow,
|
||||
9,
|
||||
"hardwareCursorRow should stay at input cursor after typing",
|
||||
);
|
||||
});
|
||||
|
||||
it("hardwareCursorRow tracks actual terminal position through IME and shrink", () => {
|
||||
const terminal = new MockTTYTerminal();
|
||||
const tui = new TUI(terminal, false);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue