/** @jsxImportSource @opentui/solid */ import { describe, expect, test } from "bun:test" import { RGBA } from "@opentui/core" import { testRender } from "@opentui/solid" import type { JSX } from "solid-js" import { createTuiResolvedConfig } from "../../fixture/tui-runtime" import { KVProvider } from "../../../src/cli/cmd/tui/context/kv" import { ThemeProvider } from "../../../src/cli/cmd/tui/context/theme" import { TuiConfigProvider } from "../../../src/cli/cmd/tui/context/tui-config" import { DiffViewerFileTree } from "../../../src/cli/cmd/tui/feature-plugins/system/diff-viewer-file-tree" import { allExpandedFileTreeDirectories, buildFileTree, } from "../../../src/cli/cmd/tui/feature-plugins/system/diff-viewer-file-tree-utils" const theme = { background: RGBA.fromHex("#000000"), backgroundPanel: RGBA.fromHex("#111111"), backgroundElement: RGBA.fromHex("#333333"), primary: RGBA.fromHex("#00ffff"), secondary: RGBA.fromHex("#0088ff"), selectedListItemText: RGBA.fromHex("#ffffff"), text: RGBA.fromHex("#ffffff"), textMuted: RGBA.fromHex("#888888"), error: RGBA.fromHex("#ff0000"), } describe("DiffViewerFileTree", () => { test.skip("renders sorted hierarchical file rows", async () => { const app = await testRender( () => withTheme(() => ( )), { width: 40, height: 20 }, ) try { await renderOnceSettled(app) const lines = visibleLines(app.captureCharFrame()) expect(lines).toEqual([ "▾ a", "│ ├─ alpha.ts ?", "│ └─ zeta.ts ?", "├─ ▾ b", "│ ├─ alpha.ts ?", "│ └─ file.ts ?", ]) } finally { app.renderer.destroy() } }) test("keeps loading and error quiet while rendering an empty settled state", async () => { const loading = await renderFrame(() => ( )) const failed = await renderFrame(() => ( )) const empty = await renderFrame(() => ( )) expect(loading).not.toContain("Loading diff...") expect(loading).not.toContain("No files") expect(failed).not.toContain("Failed to load diff") expect(failed).not.toContain("No files") expect(empty).toContain("No files") }) test("does not render text markers for highlighted rows", async () => { const files = [{ file: "src/config/tui.ts" }, { file: "README.md" }] const src = buildFileTree(files).nodes.find((node) => node.kind === "directory" && node.name === "src")! const focused = visibleLines( await renderFrame(() => ( )), ) const unfocused = visibleLines( await renderFrame(() => ( )), ) expect(focused).toContain("▾ src/config") expect(unfocused).toContain("▾ src/config") expect(focused.some((line) => line.includes("*"))).toBe(false) expect(unfocused.some((line) => line.includes("*"))).toBe(false) }) test("renders collapsed and expanded directory rows", async () => { const files = [{ file: "src/config/tui.ts" }, { file: "README.md" }] const tree = buildFileTree(files) const src = tree.nodes.find((node) => node.kind === "directory" && node.name === "src")! const collapsed = allExpandedFileTreeDirectories(tree) collapsed.delete(src.id) expect( visibleLines( await renderFrame(() => ( )), ), ).toEqual(["▸ src/config"]) expect( visibleLines( await renderFrame(() => ( )), ), ).toEqual(["▾ src/config", "│ └─ tui.ts ?"]) }) }) async function renderFrame(component: () => JSX.Element) { const app = await testRender(() => withTheme(component), { width: 40, height: 10 }) try { await renderOnceSettled(app) return await captureSettledFrame(app) } finally { app.renderer.destroy() } } async function renderOnceSettled(app: Awaited>) { await app.renderOnce() await new Promise((resolve) => setTimeout(resolve, 25)) await app.renderOnce() } async function captureSettledFrame(app: Awaited>) { for (let attempt = 0; attempt < 5; attempt++) { const frame = app.captureCharFrame() if (frame.trim().length > 0) return frame await new Promise((resolve) => setTimeout(resolve, 25)) await app.renderOnce() } return app.captureCharFrame() } function withTheme(component: () => JSX.Element) { return ( {component()} ) } function visibleLines(frame: string) { return frame .split("\n") .map((line) => line.trimEnd()) .map((line) => line.replace(/^ ?│ ?/, "").replace(/[ │]*$/, "")) .map((line) => (line.startsWith(" ") ? line.slice(1) : line)) .filter((line) => line.length > 0 && !/^┌|^└|^─+$/.test(line)) }