feat(tui): initial impl of diff viewer (#28476)
This commit is contained in:
143
packages/opencode/test/cli/tui/diff-viewer-file-tree.test.tsx
Normal file
143
packages/opencode/test/cli/tui/diff-viewer-file-tree.test.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
/** @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 { 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"),
|
||||
selectedListItemText: RGBA.fromHex("#ffffff"),
|
||||
text: RGBA.fromHex("#ffffff"),
|
||||
textMuted: RGBA.fromHex("#888888"),
|
||||
error: RGBA.fromHex("#ff0000"),
|
||||
}
|
||||
|
||||
describe("DiffViewerFileTree", () => {
|
||||
test("renders sorted hierarchical file rows", async () => {
|
||||
const app = await testRender(
|
||||
() => (
|
||||
<DiffViewerFileTree
|
||||
files={[
|
||||
{ file: "z-file.ts" },
|
||||
{ file: "b/file.ts" },
|
||||
{ file: "a/zeta.ts" },
|
||||
{ file: "b/alpha.ts" },
|
||||
{ file: "a/alpha.ts" },
|
||||
]}
|
||||
loading={false}
|
||||
error={undefined}
|
||||
theme={theme}
|
||||
focused={true}
|
||||
/>
|
||||
),
|
||||
{ width: 40, height: 20 },
|
||||
)
|
||||
|
||||
try {
|
||||
await app.renderOnce()
|
||||
const lines = visibleLines(app.captureCharFrame())
|
||||
|
||||
expect(lines).toEqual([
|
||||
"▾ a",
|
||||
" alpha.ts",
|
||||
" zeta.ts",
|
||||
"▾ b",
|
||||
" alpha.ts",
|
||||
" file.ts",
|
||||
" z-file.ts",
|
||||
])
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("keeps loading and error quiet while rendering an empty settled state", async () => {
|
||||
const loading = await renderFrame(() => <DiffViewerFileTree files={[]} loading={true} error={undefined} theme={theme} />)
|
||||
const failed = await renderFrame(() => (
|
||||
<DiffViewerFileTree files={[]} loading={false} error={new Error("nope")} theme={theme} />
|
||||
))
|
||||
const empty = await renderFrame(() => <DiffViewerFileTree files={[]} loading={false} error={undefined} theme={theme} />)
|
||||
|
||||
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(() => (
|
||||
<DiffViewerFileTree files={files} loading={false} error={undefined} theme={theme} focused highlightedNode={src.id} />
|
||||
)),
|
||||
)
|
||||
const unfocused = visibleLines(
|
||||
await renderFrame(() => <DiffViewerFileTree files={files} loading={false} error={undefined} theme={theme} />),
|
||||
)
|
||||
|
||||
expect(focused).toContain("▾ src")
|
||||
expect(unfocused).toContain("▾ src")
|
||||
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(() => (
|
||||
<DiffViewerFileTree files={files} loading={false} error={undefined} theme={theme} expandedNodes={collapsed} />
|
||||
)),
|
||||
),
|
||||
).toEqual(["▸ src", " README.md"])
|
||||
|
||||
expect(
|
||||
visibleLines(
|
||||
await renderFrame(() => (
|
||||
<DiffViewerFileTree
|
||||
files={files}
|
||||
loading={false}
|
||||
error={undefined}
|
||||
theme={theme}
|
||||
expandedNodes={allExpandedFileTreeDirectories(tree)}
|
||||
/>
|
||||
)),
|
||||
),
|
||||
).toEqual(["▾ src", " ▾ config", " tui.ts", " README.md"])
|
||||
})
|
||||
})
|
||||
|
||||
async function renderFrame(component: () => JSX.Element) {
|
||||
const app = await testRender(component, { width: 40, height: 10 })
|
||||
try {
|
||||
await app.renderOnce()
|
||||
return app.captureCharFrame()
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user