| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200 |
- /** @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/context/kv"
- import { ThemeProvider } from "../../../src/context/theme"
- import { TuiConfigProvider } from "../../../src/config"
- import { DiffViewerFileTree } from "../../../src/feature-plugins/system/diff-viewer-file-tree"
- import { TestTuiContexts } from "../../fixture/tui-environment"
- import {
- allExpandedFileTreeDirectories,
- buildFileTree,
- } from "../../../src/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(() => (
- <DiffViewerFileTree
- width={32}
- 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 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(() => (
- <DiffViewerFileTree width={32} files={[]} loading={true} error={undefined} theme={theme} />
- ))
- const failed = await renderFrame(() => (
- <DiffViewerFileTree width={32} files={[]} loading={false} error={new Error("nope")} theme={theme} />
- ))
- const empty = await renderFrame(() => (
- <DiffViewerFileTree width={32} 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
- width={32}
- files={files}
- loading={false}
- error={undefined}
- theme={theme}
- focused
- highlightedNode={src.id}
- />
- )),
- )
- const unfocused = visibleLines(
- await renderFrame(() => (
- <DiffViewerFileTree width={32} files={files} loading={false} error={undefined} theme={theme} />
- )),
- )
- 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(() => (
- <DiffViewerFileTree
- width={32}
- files={files}
- loading={false}
- error={undefined}
- theme={theme}
- expandedNodes={collapsed}
- />
- )),
- ),
- ).toEqual(["▸ src/config"])
- expect(
- visibleLines(
- await renderFrame(() => (
- <DiffViewerFileTree
- files={files}
- width={32}
- loading={false}
- error={undefined}
- theme={theme}
- expandedNodes={allExpandedFileTreeDirectories(tree)}
- />
- )),
- ),
- ).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<ReturnType<typeof testRender>>) {
- await app.renderOnce()
- await new Promise((resolve) => setTimeout(resolve, 25))
- await app.renderOnce()
- }
- async function captureSettledFrame(app: Awaited<ReturnType<typeof testRender>>) {
- 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 (
- <TestTuiContexts>
- <TuiConfigProvider config={createTuiResolvedConfig()}>
- <KVProvider>
- <ThemeProvider mode="dark">{component()}</ThemeProvider>
- </KVProvider>
- </TuiConfigProvider>
- </TestTuiContexts>
- )
- }
- 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))
- }
|