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)
})
})