/** * pane-resize.test.tsx — dragging the center column's borders actually * resizes the panes in a rendered PaneRow. * * Each pane renders a long run of a unique character (P / C / V). A line * where all three meet encodes the boundary columns directly: the current * pane carries the only borders (cols `leftPx` and `rightPx - 1`), so its * content starts one column in — `leftPx + 1`. Hence * `leftPx = firstC - 1`, `rightPx = firstV`. * * The drag strips overlay the border cells (left strip at [left, left+2), * right strip at [right-2, right)). The test presses inside a strip and * drags across the row — the drag bubbles to the row container which moves * the split, so the panes must re-render at the new columns. */ import { test, expect, afterAll } from "bun:test"; import { testRender } from "@opentui/solid"; import { ThemeProvider } from "../src/context/ThemeContext"; import { PaneRow } from "../src/components/PaneRow"; import { usePaneLayout } from "../src/stores/pane-layout"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; // Point the config dir at a throwaway directory BEFORE importing the store // (module-level init reads it). const configHome = mkdtempSync(join(tmpdir(), "podtui-paneresize-")); process.env.XDG_CONFIG_HOME = configHome; type Span = { text: string }; type Frame = { cols: number; lines: { spans: Span[] }[] }; interface BoundCols { left: number; right: number; } function readBounds(frame: Frame): BoundCols { const line = frame.lines .map((l) => l.spans.map((s) => s.text).join("")) .find((l) => l.includes("C")); if (!line) throw new Error("pane row did not render"); return { left: line.indexOf("C") - 1, right: line.indexOf("V") }; } async function renderRow(panes: 2 | 3 = 3) { const setup = (await testRender( () => ( {"P".repeat(300)}} current={{"C".repeat(600)}} preview={{"V".repeat(300)}} currentLabel="" panes={panes} /> ), { width: 100, height: 10, useThread: false }, )) as unknown as { renderOnce: () => Promise; captureSpans: () => Frame; mockMouse: { drag: (a: number, b: number, c: number, d: number) => Promise; click: (a: number, b: number) => Promise; }; renderer: { destroy: () => void }; }; for (let i = 0; i < 10; i++) await setup.renderOnce(); return setup; } /** Reset the shared store to the default split for a deterministic start. */ function resetSplits() { usePaneLayout().setLeft(20, 100); usePaneLayout().setRight(70, 100); } const cleanups: (() => void)[] = []; afterAll(() => { for (const c of cleanups) c(); // Restore the default split so a later file sharing this process (bun // test reuses the module registry) renders the default layout. usePaneLayout().setLeft(20, 100); usePaneLayout().setRight(70, 100); rmSync(configHome, { recursive: true, force: true }); }); test("panes render at the default 20/70 split", async () => { const setup = await renderRow(3); cleanups.push(() => setup.renderer.destroy()); resetSplits(); await setup.renderOnce(); const { left, right } = readBounds(setup.captureSpans()); expect(left).toBe(20); expect(right).toBe(70); }); test("dragging the left border resizes parent vs current", async () => { const setup = await renderRow(3); cleanups.push(() => setup.renderer.destroy()); resetSplits(); await setup.renderOnce(); // Press on the left strip (border at 20 → strip covers 20) and drag // toward the middle of the row. await setup.mockMouse.drag(20, 5, 45, 5); for (let i = 0; i < 10; i++) await setup.renderOnce(); const after = readBounds(setup.captureSpans()); expect(after.left).toBeGreaterThanOrEqual(44); expect(after.left).toBeLessThanOrEqual(46); // Pushing the left border to 45 would shrink the current pane below its // 30-col minimum (45..70 = 25), so the right border is forced right to // 75, keeping the current pane at exactly 30 and absorbing the overflow // in the preview. expect(after.right).toBe(75); }); test("dragging the right border resizes current vs preview", async () => { const setup = await renderRow(3); cleanups.push(() => setup.renderer.destroy()); resetSplits(); await setup.renderOnce(); // Press on the right strip (border at 69 → strip covers 69) and drag // toward the right edge of the row. await setup.mockMouse.drag(69, 5, 90, 5); for (let i = 0; i < 10; i++) await setup.renderOnce(); const after = readBounds(setup.captureSpans()); expect(after.right).toBeGreaterThanOrEqual(84); expect(after.right).toBeLessThanOrEqual(85); // clamped at preview min 15 expect(after.left).toBe(20); }); test("a plain click away from the borders does not resize", async () => { const setup = await renderRow(3); cleanups.push(() => setup.renderer.destroy()); resetSplits(); await setup.renderOnce(); await setup.mockMouse.click(5, 5); await setup.renderOnce(); const { left, right } = readBounds(setup.captureSpans()); expect(left).toBe(20); expect(right).toBe(70); }); test("2-pane rows offer no right border (current fills the row)", async () => { const setup = await renderRow(2); cleanups.push(() => setup.renderer.destroy()); resetSplits(); for (let i = 0; i < 10; i++) await setup.renderOnce(); // No 'V' pane: the line runs to the screen edge. const frame = setup.captureSpans(); const { left } = readBounds(frame); expect(left).toBe(20); const line = frame.lines .map((l) => l.spans.map((s) => s.text).join("")) .find((l) => l.includes("C")); expect(line?.includes("V")).toBe(false); });