import { describe, test, expect } from "bun:test" import React from "react" import { render } from "ink-testing-library" import { Text, Box } from "ink" import { computeConversationLayout, APP_BORDER_ROWS, APP_BORDER_COLS } from "../views/layout.js" const SIDEBAR_WIDTH = 26 function ConversationLayoutHarness({ termHeight, termWidth, headerText, footerText, messageCount, isLive, }: { termHeight: number termWidth: number headerText: string footerText: string messageCount: number isLive: boolean }) { const { innerHeight, effectiveWidth, bodyHeight } = computeConversationLayout(termHeight, termWidth) const fullDivider = "─".repeat(effectiveWidth) const contentWidth = Math.max(1, effectiveWidth - SIDEBAR_WIDTH - 1) const divider = "─".repeat(contentWidth) return ( {/* Header */} {headerText} {fullDivider} {/* Body row — explicit height */} {/* Sidebar — explicit height */} sessions item 1 item 2 {/* Message pane */} {Array.from({ length: messageCount }, (_, i) => ( Message line {i + 1} ))} {isLive && ( <> {divider} › Press i to type )} {/* Footer */} {footerText} ) } describe("computeConversationLayout formula", () => { test("innerHeight accounts for App border", () => { const layout = computeConversationLayout(24, 80) expect(layout.innerHeight).toBe(24 - APP_BORDER_ROWS) }) test("effectiveWidth accounts for App border", () => { const layout = computeConversationLayout(24, 80) expect(layout.effectiveWidth).toBe(80 - APP_BORDER_COLS) }) test("header + body + footer fit within innerHeight", () => { for (const h of [12, 15, 20, 24, 30, 40, 50]) { const layout = computeConversationLayout(h, 80) const total = layout.bodyHeight + layout.HEADER_ROWS + layout.FOOTER_ROWS expect(total).toBeLessThanOrEqual(layout.innerHeight) } }) test("header + body + footer + killConfirm fit within innerHeight", () => { for (const h of [15, 20, 24, 30]) { const layout = computeConversationLayout(h, 80, { killConfirm: true }) const total = layout.bodyHeight + layout.HEADER_ROWS + layout.FOOTER_ROWS + 3 expect(total).toBeLessThanOrEqual(layout.innerHeight) } }) test("bodyHeight never goes below minimum of 5", () => { const layout = computeConversationLayout(10, 80) expect(layout.bodyHeight).toBeGreaterThanOrEqual(5) }) test("very small terminal still produces valid layout", () => { const layout = computeConversationLayout(8, 40) expect(layout.innerHeight).toBe(6) expect(layout.bodyHeight).toBeGreaterThanOrEqual(5) expect(layout.effectiveWidth).toBe(38) }) test("bodyHeight has room for spinner + input area + messages at typical terminal sizes", () => { for (const h of [20, 24, 30, 40, 50]) { const layout = computeConversationLayout(h, 80) const INNER_OVERHEAD = 3 // divider + prefix line + input line const SPINNER_ROW = 1 const msgAreaHeight = layout.bodyHeight - INNER_OVERHEAD - SPINNER_ROW expect(msgAreaHeight).toBeGreaterThanOrEqual(5) } }) }) describe("conversation layout", () => { test("header is visible with few messages", () => { const { lastFrame } = render( ) const lines = lastFrame()!.split("\n") expect(lines[0]).toContain("myrepo / my session") }) test("header is visible with many messages", () => { const { lastFrame } = render( ) const lines = lastFrame()!.split("\n") expect(lines[0]).toContain("myrepo / my session") }) test("footer is visible", () => { const { lastFrame } = render( ) const output = lastFrame()! expect(output).toContain("q: back i: insert") }) test("header visible for non-live sessions", () => { const { lastFrame } = render( ) const lines = lastFrame()!.split("\n") expect(lines[0]).toContain("myrepo / read only session") }) test("header visible on small terminal", () => { const { lastFrame } = render( ) const lines = lastFrame()!.split("\n") expect(lines[0]).toContain("repo / session") }) test("output does not exceed termHeight lines", () => { const termHeight = 24 const { lastFrame } = render( ) const lines = lastFrame()!.split("\n") expect(lines.length).toBeLessThanOrEqual(termHeight) }) })