Skip to content

Commit dd66dad

Browse files
committed
Cover the slash-command picker's height bounding with tests
Investigation found the picker already routes through the shared list-viewport/resolveGeometry machinery: a 50-entry catalog stays bounded at 24, 16, 12, 8, and 6 terminal rows, the prompt box below it stays intact, and moving the selection scrolls the window so the active row is always visible. No sizing logic was missing, so this adds the regression coverage without a second windowing path.
1 parent 5e1d9d1 commit dd66dad

1 file changed

Lines changed: 62 additions & 0 deletions

File tree

src/tui-opentui/palette-paint.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import type { KeyEvent } from "@opentui/core"
88

99
import { withTestRenderer } from "./harness"
1010
import { openModelPickerOverlay } from "./overlays"
11+
import type { PaletteCommand } from "./palette"
1112
import {
1213
createAppShell,
1314
handlePaletteFilterKey,
15+
moveOverlaySelection,
1416
openPalette,
1517
type AppShell,
1618
} from "./shell"
@@ -256,3 +258,63 @@ describe("command palette selection colour", () => {
256258
})
257259
})
258260

261+
describe("command palette height cap", () => {
262+
const BIG_CATALOG: readonly PaletteCommand[] = Array.from(
263+
{ length: 50 },
264+
(_, i) => ({
265+
id: `cmd_${String(i)}`,
266+
label: `Fake command number ${String(i)} with a longish label`,
267+
dispatch: "command" as const,
268+
}),
269+
)
270+
271+
// Every plugin-inflated catalog and every terminal size gets a bounded
272+
// frame: the border-to-border row count above the prompt box never grows
273+
// past the terminal, and the box below stays intact and readable.
274+
for (const height of [24, 16, 12, 8, 6]) {
275+
test(`stays within a ${height}-row terminal and keeps the prompt box intact`, async () => {
276+
await withTestRenderer(
277+
async (h) => {
278+
const shell = createAppShell(h.renderer, {
279+
terminal: { columns: 80, rows: height },
280+
wireKeys: false,
281+
run: "idle",
282+
})
283+
openPalette(shell, { catalog: BIG_CATALOG, title: "commands · /" })
284+
await h.renderOnce()
285+
const lines = h.captureCharFrame().split("\n")
286+
// captureCharFrame's trailing newline yields one extra split
287+
// element — the frame itself must not exceed the terminal rows.
288+
expect(lines.length).toBeLessThanOrEqual(height + 1)
289+
expect(lines.some((l) => l.includes("message…"))).toBe(true)
290+
},
291+
{ width: 80, height },
292+
)
293+
})
294+
}
295+
296+
test("scrolling the selection keeps the active row inside the window", async () => {
297+
await withTestRenderer(
298+
async (h) => {
299+
const shell = createAppShell(h.renderer, {
300+
terminal: { columns: 80, rows: 12 },
301+
wireKeys: false,
302+
run: "idle",
303+
})
304+
openPalette(shell, { catalog: BIG_CATALOG, title: "commands · /" })
305+
await h.renderOnce()
306+
for (let i = 0; i < 20; i++) moveOverlaySelection(shell, 1)
307+
await h.renderOnce()
308+
expect(shell.overlayList?.activeIndex).toBe(20)
309+
const offset = shell.overlayList?.offset ?? 0
310+
const height = shell.overlayList?.height ?? 0
311+
expect(offset).toBeLessThanOrEqual(20)
312+
expect(offset + height).toBeGreaterThan(20)
313+
const frame = h.captureCharFrame()
314+
expect(frame).toContain(`Fake command number 20`)
315+
},
316+
{ width: 80, height: 12 },
317+
)
318+
})
319+
})
320+

0 commit comments

Comments
 (0)