refactor(tui): extract standalone package (#31193)
This commit is contained in:
parent
7a2c49e762
commit
106f8e94d6
84 changed files with 1147 additions and 1918 deletions
59
packages/tui/test/app-lifecycle.test.tsx
Normal file
59
packages/tui/test/app-lifecycle.test.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { expect, mock, test } from "bun:test"
|
||||
import { createTestRenderer } from "@opentui/core/testing"
|
||||
import { Effect } from "effect"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { createTuiResolvedConfig } from "./fixture/tui-runtime"
|
||||
import { createEventSource, createFetch, directory } from "./fixture/tui-sdk"
|
||||
|
||||
test("SIGHUP clears title and disposes scoped resources once", async () => {
|
||||
const setup = await createTestRenderer({ width: 80, height: 24, useThread: false })
|
||||
const core = await import("@opentui/core")
|
||||
mock.module("@opentui/core", () => ({ ...core, createCliRenderer: async () => setup.renderer }))
|
||||
const titles: string[] = []
|
||||
const setTitle = setup.renderer.setTerminalTitle.bind(setup.renderer)
|
||||
setup.renderer.setTerminalTitle = (title) => {
|
||||
titles.push(title)
|
||||
setTitle(title)
|
||||
}
|
||||
const listeners = new Set(process.listeners("SIGHUP"))
|
||||
const events = createEventSource()
|
||||
const calls = createFetch()
|
||||
let started!: () => void
|
||||
const ready = new Promise<void>((resolve) => {
|
||||
started = resolve
|
||||
})
|
||||
let disposes = 0
|
||||
|
||||
try {
|
||||
const { run } = await import("../src/app")
|
||||
const task = Effect.runPromise(
|
||||
run({
|
||||
url: "http://test",
|
||||
directory,
|
||||
config: createTuiResolvedConfig({ plugin_enabled: {} }),
|
||||
fetch: calls.fetch,
|
||||
events: events.source,
|
||||
args: {},
|
||||
pluginHost: {
|
||||
async start() {
|
||||
started()
|
||||
},
|
||||
async dispose() {
|
||||
disposes++
|
||||
},
|
||||
},
|
||||
}).pipe(Effect.provide(Global.defaultLayer)),
|
||||
)
|
||||
await ready
|
||||
process.emit("SIGHUP")
|
||||
await task
|
||||
|
||||
expect(setup.renderer.isDestroyed).toBe(true)
|
||||
expect(titles.at(-1)).toBe("")
|
||||
expect(disposes).toBe(1)
|
||||
expect(process.listeners("SIGHUP").every((listener) => listeners.has(listener))).toBe(true)
|
||||
} finally {
|
||||
if (!setup.renderer.isDestroyed) setup.renderer.destroy()
|
||||
mock.restore()
|
||||
}
|
||||
})
|
||||
|
|
@ -2,13 +2,12 @@
|
|||
import { testRender } from "@opentui/solid"
|
||||
import { onMount } from "solid-js"
|
||||
import { ArgsProvider } from "../../../../src/context/args"
|
||||
import { createExit, ExitProvider } from "../../../../src/context/exit"
|
||||
import { KVProvider, useKV } from "../../../../src/context/kv"
|
||||
import { ProjectProvider, useProject } from "../../../../src/context/project"
|
||||
import { SDKProvider } from "../../../../src/context/sdk"
|
||||
import { SyncProvider, useSync } from "../../../../src/context/sync"
|
||||
import { createEventSource, createFetch, type FetchHandler, directory } from "../../../fixture/tui-sdk"
|
||||
import { TestTuiEnvironmentProvider } from "../../../fixture/tui-environment"
|
||||
import { TestTuiContexts } from "../../../fixture/tui-environment"
|
||||
export { createEventSource, createFetch, directory, eventSource, json, worktree } from "../../../fixture/tui-sdk"
|
||||
|
||||
export async function wait(fn: () => boolean, timeout = 2000) {
|
||||
|
|
@ -44,32 +43,22 @@ export async function mount(override?: FetchHandler, state?: string) {
|
|||
}
|
||||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiEnvironmentProvider paths={state ? { state } : undefined}>
|
||||
<TestTuiContexts paths={state ? { state } : undefined}>
|
||||
<ArgsProvider>
|
||||
<ExitProvider exit={createExit(async () => {})}>
|
||||
<KVProvider>
|
||||
<SDKProvider url="http://test" directory={directory} fetch={calls.fetch} events={events.source}>
|
||||
<ProjectProvider>
|
||||
<SyncFixtureProvider>
|
||||
<SyncProvider>
|
||||
<Probe />
|
||||
</SyncFixtureProvider>
|
||||
</SyncProvider>
|
||||
</ProjectProvider>
|
||||
</SDKProvider>
|
||||
</KVProvider>
|
||||
</ExitProvider>
|
||||
</ArgsProvider>
|
||||
</TestTuiEnvironmentProvider>
|
||||
</TestTuiContexts>
|
||||
))
|
||||
|
||||
await ready
|
||||
await wait(() => sync.status === "complete")
|
||||
return { app, emit: events.emit, kv, project, sync, session: calls.session }
|
||||
}
|
||||
|
||||
function SyncFixtureProvider(props: { children: import("solid-js").JSX.Element }) {
|
||||
return (
|
||||
<SyncProvider kv={useKV()} logger={{ error() {} }}>
|
||||
{props.children}
|
||||
</SyncProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { onCleanup } from "solid-js"
|
|||
import { tmpdir } from "../../fixture/fixture"
|
||||
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
|
||||
import type { TuiKeybind } from "../../../src/config/keybind"
|
||||
import { TestTuiEnvironmentProvider } from "../../fixture/tui-environment"
|
||||
import { TestTuiContexts } from "../../fixture/tui-environment"
|
||||
|
||||
async function wait(fn: () => boolean, timeout = 2000) {
|
||||
const start = Date.now()
|
||||
|
|
@ -57,7 +57,7 @@ async function mountPrompt(input: {
|
|||
onCleanup(off)
|
||||
|
||||
return (
|
||||
<TestTuiEnvironmentProvider
|
||||
<TestTuiContexts
|
||||
directory={input.root}
|
||||
paths={{
|
||||
home: input.root,
|
||||
|
|
@ -78,7 +78,7 @@ async function mountPrompt(input: {
|
|||
</KVProvider>
|
||||
</TuiConfigProvider>
|
||||
</OpencodeKeymapProvider>
|
||||
</TestTuiEnvironmentProvider>
|
||||
</TestTuiContexts>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ 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 { TestTuiEnvironmentProvider } from "../../fixture/tui-environment"
|
||||
import { TestTuiContexts } from "../../fixture/tui-environment"
|
||||
import {
|
||||
allExpandedFileTreeDirectories,
|
||||
buildFileTree,
|
||||
|
|
@ -180,13 +180,13 @@ async function captureSettledFrame(app: Awaited<ReturnType<typeof testRender>>)
|
|||
|
||||
function withTheme(component: () => JSX.Element) {
|
||||
return (
|
||||
<TestTuiEnvironmentProvider>
|
||||
<TestTuiContexts>
|
||||
<TuiConfigProvider config={createTuiResolvedConfig()}>
|
||||
<KVProvider>
|
||||
<ThemeProvider mode="dark">{component()}</ThemeProvider>
|
||||
</KVProvider>
|
||||
</TuiConfigProvider>
|
||||
</TestTuiEnvironmentProvider>
|
||||
</TestTuiContexts>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { OpencodeKeymapProvider } from "../../../src/keymap"
|
|||
import diffViewerPlugin from "../../../src/feature-plugins/system/diff-viewer"
|
||||
import { createTuiPluginApi } from "../../fixture/tui-plugin"
|
||||
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
|
||||
import { TestTuiEnvironmentProvider } from "../../fixture/tui-environment"
|
||||
import { TestTuiContexts } from "../../fixture/tui-environment"
|
||||
|
||||
test("closing the diff viewer returns to the route it opened from", async () => {
|
||||
const viewer = await renderDiffViewer([])
|
||||
|
|
@ -152,7 +152,7 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20) {
|
|||
commands.get("diff.open")?.run?.({} as never)
|
||||
|
||||
return (
|
||||
<TestTuiEnvironmentProvider>
|
||||
<TestTuiContexts>
|
||||
<OpencodeKeymapProvider keymap={keymap}>
|
||||
<TuiConfigProvider config={config}>
|
||||
<KVProvider>
|
||||
|
|
@ -162,7 +162,7 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20) {
|
|||
</KVProvider>
|
||||
</TuiConfigProvider>
|
||||
</OpencodeKeymapProvider>
|
||||
</TestTuiEnvironmentProvider>
|
||||
</TestTuiContexts>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { ProjectProvider } from "../../../src/context/project"
|
|||
import { SDKProvider } from "../../../src/context/sdk"
|
||||
import { SyncProviderV2, useSyncV2 } from "../../../src/context/sync-v2"
|
||||
import { createEventSource, createFetch, directory, json } from "../../fixture/tui-sdk"
|
||||
import { TestTuiEnvironmentProvider } from "../../fixture/tui-environment"
|
||||
import { TestTuiContexts } from "../../fixture/tui-environment"
|
||||
|
||||
async function wait(fn: () => boolean, timeout = 2000) {
|
||||
const start = Date.now()
|
||||
|
|
@ -43,7 +43,7 @@ test("sync v2 settles pending tools when a live failure arrives", async () => {
|
|||
}
|
||||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiEnvironmentProvider>
|
||||
<TestTuiContexts>
|
||||
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
||||
<ProjectProvider>
|
||||
<SyncProviderV2>
|
||||
|
|
@ -51,7 +51,7 @@ test("sync v2 settles pending tools when a live failure arrives", async () => {
|
|||
</SyncProviderV2>
|
||||
</ProjectProvider>
|
||||
</SDKProvider>
|
||||
</TestTuiEnvironmentProvider>
|
||||
</TestTuiContexts>
|
||||
))
|
||||
|
||||
try {
|
||||
|
|
@ -172,7 +172,7 @@ test("sync v2 renders admitted prompts only after promotion", async () => {
|
|||
}
|
||||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiEnvironmentProvider>
|
||||
<TestTuiContexts>
|
||||
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
||||
<ProjectProvider>
|
||||
<SyncProviderV2>
|
||||
|
|
@ -180,7 +180,7 @@ test("sync v2 renders admitted prompts only after promotion", async () => {
|
|||
</SyncProviderV2>
|
||||
</ProjectProvider>
|
||||
</SDKProvider>
|
||||
</TestTuiEnvironmentProvider>
|
||||
</TestTuiContexts>
|
||||
))
|
||||
|
||||
try {
|
||||
|
|
@ -236,7 +236,7 @@ test("sync v2 renders a promoted prompt when admission was missed", async () =>
|
|||
}
|
||||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiEnvironmentProvider>
|
||||
<TestTuiContexts>
|
||||
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
||||
<ProjectProvider>
|
||||
<SyncProviderV2>
|
||||
|
|
@ -244,7 +244,7 @@ test("sync v2 renders a promoted prompt when admission was missed", async () =>
|
|||
</SyncProviderV2>
|
||||
</ProjectProvider>
|
||||
</SDKProvider>
|
||||
</TestTuiEnvironmentProvider>
|
||||
</TestTuiContexts>
|
||||
))
|
||||
|
||||
try {
|
||||
|
|
@ -284,7 +284,7 @@ test("sync v2 projects live context updates with their message ID", async () =>
|
|||
}
|
||||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiEnvironmentProvider>
|
||||
<TestTuiContexts>
|
||||
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
||||
<ProjectProvider>
|
||||
<SyncProviderV2>
|
||||
|
|
@ -292,7 +292,7 @@ test("sync v2 projects live context updates with their message ID", async () =>
|
|||
</SyncProviderV2>
|
||||
</ProjectProvider>
|
||||
</SDKProvider>
|
||||
</TestTuiEnvironmentProvider>
|
||||
</TestTuiContexts>
|
||||
))
|
||||
|
||||
try {
|
||||
|
|
@ -339,7 +339,7 @@ test("sync v2 preserves live events while snapshot hydration is in flight", asyn
|
|||
}
|
||||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiEnvironmentProvider>
|
||||
<TestTuiContexts>
|
||||
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
||||
<ProjectProvider>
|
||||
<SyncProviderV2>
|
||||
|
|
@ -347,7 +347,7 @@ test("sync v2 preserves live events while snapshot hydration is in flight", asyn
|
|||
</SyncProviderV2>
|
||||
</ProjectProvider>
|
||||
</SDKProvider>
|
||||
</TestTuiEnvironmentProvider>
|
||||
</TestTuiContexts>
|
||||
))
|
||||
|
||||
try {
|
||||
|
|
@ -389,7 +389,7 @@ test("sync v2 replaces stale cached rows while preserving in-flight live rows",
|
|||
}
|
||||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiEnvironmentProvider>
|
||||
<TestTuiContexts>
|
||||
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
||||
<ProjectProvider>
|
||||
<SyncProviderV2>
|
||||
|
|
@ -397,7 +397,7 @@ test("sync v2 replaces stale cached rows while preserving in-flight live rows",
|
|||
</SyncProviderV2>
|
||||
</ProjectProvider>
|
||||
</SDKProvider>
|
||||
</TestTuiEnvironmentProvider>
|
||||
</TestTuiContexts>
|
||||
))
|
||||
|
||||
try {
|
||||
|
|
@ -458,7 +458,7 @@ test("sync v2 preserves snapshot order and metadata for in-flight updates", asyn
|
|||
}
|
||||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiEnvironmentProvider>
|
||||
<TestTuiContexts>
|
||||
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
||||
<ProjectProvider>
|
||||
<SyncProviderV2>
|
||||
|
|
@ -466,7 +466,7 @@ test("sync v2 preserves snapshot order and metadata for in-flight updates", asyn
|
|||
</SyncProviderV2>
|
||||
</ProjectProvider>
|
||||
</SDKProvider>
|
||||
</TestTuiEnvironmentProvider>
|
||||
</TestTuiContexts>
|
||||
))
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { ProjectProvider, useProject } from "../../../src/context/project"
|
|||
import { SDKProvider } from "../../../src/context/sdk"
|
||||
import { useEvent } from "../../../src/context/event"
|
||||
import { createEventSource, createFetch, directory } from "../../fixture/tui-sdk"
|
||||
import { TestTuiEnvironmentProvider } from "../../fixture/tui-environment"
|
||||
import { TestTuiContexts } from "../../fixture/tui-environment"
|
||||
|
||||
const projectID = "proj_test"
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ async function mount() {
|
|||
})
|
||||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiEnvironmentProvider>
|
||||
<TestTuiContexts>
|
||||
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
||||
<ProjectProvider>
|
||||
<Probe
|
||||
|
|
@ -74,7 +74,7 @@ async function mount() {
|
|||
/>
|
||||
</ProjectProvider>
|
||||
</SDKProvider>
|
||||
</TestTuiEnvironmentProvider>
|
||||
</TestTuiContexts>
|
||||
))
|
||||
|
||||
await ready
|
||||
|
|
|
|||
19
packages/tui/test/clipboard.test.ts
Normal file
19
packages/tui/test/clipboard.test.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { copyCommand } from "../src/clipboard"
|
||||
|
||||
test("prefers Wayland clipboard when available", () => {
|
||||
expect(copyCommand("linux", true, (name) => name === "wl-copy")).toEqual(["wl-copy"])
|
||||
})
|
||||
|
||||
test("uses osascript on macOS", () => {
|
||||
expect(copyCommand("darwin", false, (name) => name === "osascript")).toEqual(["osascript"])
|
||||
})
|
||||
|
||||
test("falls back through X11 clipboard commands", () => {
|
||||
expect(copyCommand("linux", true, (name) => name === "xclip")).toEqual(["xclip", "-selection", "clipboard"])
|
||||
expect(copyCommand("linux", false, (name) => name === "xsel")).toEqual(["xsel", "--clipboard", "--input"])
|
||||
})
|
||||
|
||||
test("returns undefined when native clipboard is unavailable", () => {
|
||||
expect(copyCommand("linux", false, () => false)).toBeUndefined()
|
||||
})
|
||||
23
packages/tui/test/editor.test.ts
Normal file
23
packages/tui/test/editor.test.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { afterEach, expect, test } from "bun:test"
|
||||
import { openEditor } from "../src/editor"
|
||||
|
||||
const editor = process.env.EDITOR
|
||||
const visual = process.env.VISUAL
|
||||
|
||||
afterEach(() => {
|
||||
process.env.EDITOR = editor
|
||||
process.env.VISUAL = visual
|
||||
})
|
||||
|
||||
test("rejects when the external editor cannot start", async () => {
|
||||
delete process.env.VISUAL
|
||||
process.env.EDITOR = "opencode-editor-that-does-not-exist"
|
||||
const renderer = {
|
||||
suspend() {},
|
||||
resume() {},
|
||||
requestRender() {},
|
||||
currentRenderBuffer: { clear() {} },
|
||||
}
|
||||
|
||||
await expect(openEditor({ value: "original", renderer: renderer as never })).rejects.toThrow()
|
||||
})
|
||||
|
|
@ -1,42 +1,32 @@
|
|||
/** @jsxImportSource @opentui/solid */
|
||||
import { createTuiEnvironment, TuiEnvironmentProvider, type TuiEnvironment } from "../../src/runtime"
|
||||
import {
|
||||
TuiPathsProvider,
|
||||
TuiStartupProvider,
|
||||
TuiTerminalEnvironmentProvider,
|
||||
type TuiPaths,
|
||||
} from "../../src/context/runtime"
|
||||
import type { ParentProps } from "solid-js"
|
||||
|
||||
export function TestTuiEnvironmentProvider(
|
||||
export function TestTuiContexts(
|
||||
props: ParentProps<{
|
||||
cwd?: string
|
||||
directory?: string
|
||||
paths?: Partial<TuiEnvironment["paths"]>
|
||||
capabilities?: Partial<TuiEnvironment["capabilities"]>
|
||||
editor?: Partial<TuiEnvironment["editor"]>
|
||||
paths?: Partial<TuiPaths>
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<TuiEnvironmentProvider
|
||||
value={createTuiEnvironment({
|
||||
<TuiPathsProvider
|
||||
value={{
|
||||
cwd: props.cwd ?? props.directory ?? "/tmp/opencode/packages/tui",
|
||||
platform: "linux",
|
||||
paths: {
|
||||
home: "/tmp/opencode/home",
|
||||
state: "/tmp/opencode/state",
|
||||
worktree: "/tmp/opencode",
|
||||
...props.paths,
|
||||
},
|
||||
capabilities: {
|
||||
mouse: true,
|
||||
copyOnSelect: true,
|
||||
terminalTitle: false,
|
||||
terminalSuspend: false,
|
||||
workspaces: false,
|
||||
showTimeToFirstDraw: false,
|
||||
...props.capabilities,
|
||||
},
|
||||
terminal: {},
|
||||
editor: { zedTerminal: false, ...props.editor },
|
||||
skipInitialLoading: false,
|
||||
})}
|
||||
home: "/tmp/opencode/home",
|
||||
state: "/tmp/opencode/state",
|
||||
worktree: "/tmp/opencode",
|
||||
...props.paths,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</TuiEnvironmentProvider>
|
||||
<TuiTerminalEnvironmentProvider value={{ platform: "linux" }}>
|
||||
<TuiStartupProvider value={{ skipInitialLoading: false }}>{props.children}</TuiStartupProvider>
|
||||
</TuiTerminalEnvironmentProvider>
|
||||
</TuiPathsProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { createRenderer, createTuiRenderer, mount, run, tui } from "../src"
|
||||
import { run } from "../src"
|
||||
|
||||
test("exports the canonical application lifecycle", () => {
|
||||
expect(run).toBe(tui)
|
||||
expect(createRenderer).toBe(createTuiRenderer)
|
||||
expect(typeof mount).toBe("function")
|
||||
expect(typeof run).toBe("function")
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { testRender } from "@opentui/solid"
|
||||
import { TuiPlatformProvider, useTuiPlatform, type TuiPlatform } from "../src/platform"
|
||||
|
||||
test("provides host platform operations", async () => {
|
||||
const platform: TuiPlatform = {
|
||||
files: {
|
||||
readText: async (path) => `text:${path}`,
|
||||
readBytes: async () => new Uint8Array([1, 2, 3]),
|
||||
mime: async () => "text/plain",
|
||||
},
|
||||
}
|
||||
|
||||
function Consumer() {
|
||||
const value = useTuiPlatform()
|
||||
return <text>{value.clipboard ? "clipboard" : "files-only"}</text>
|
||||
}
|
||||
|
||||
const app = await testRender(
|
||||
() => (
|
||||
<TuiPlatformProvider value={platform}>
|
||||
<Consumer />
|
||||
</TuiPlatformProvider>
|
||||
),
|
||||
{ width: 20, height: 3 },
|
||||
)
|
||||
|
||||
try {
|
||||
await app.renderOnce()
|
||||
expect(app.captureCharFrame()).toContain("files-only")
|
||||
expect(await platform.files.readText("file.txt")).toBe("text:file.txt")
|
||||
expect(await platform.files.readBytes("file.bin")).toEqual(new Uint8Array([1, 2, 3]))
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("requires a platform provider", () => {
|
||||
expect(() => useTuiPlatform()).toThrow("TuiPlatformProvider is missing")
|
||||
})
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { readLocalAttachment } from "../../src/component/prompt/local-attachment"
|
||||
import type { PlatformFiles } from "../../src/platform"
|
||||
import { readLocalAttachmentWith } from "../../src/component/prompt/local-attachment"
|
||||
import type { LocalFiles } from "../../src/component/prompt/local-attachment"
|
||||
|
||||
function files(input: { mime: string; text?: string; bytes?: Uint8Array }): PlatformFiles {
|
||||
function files(input: { mime: string; text?: string; bytes?: Uint8Array }): LocalFiles {
|
||||
return {
|
||||
mime: async () => input.mime,
|
||||
readText: async () => input.text ?? "",
|
||||
|
|
@ -12,7 +12,7 @@ function files(input: { mime: string; text?: string; bytes?: Uint8Array }): Plat
|
|||
|
||||
describe("prompt local attachments", () => {
|
||||
test("reads SVG attachments as text", async () => {
|
||||
expect(await readLocalAttachment(files({ mime: "image/svg+xml", text: "<svg />" }), "/tmp/image.svg")).toEqual({
|
||||
expect(await readLocalAttachmentWith(files({ mime: "image/svg+xml", text: "<svg />" }), "/tmp/image.svg")).toEqual({
|
||||
type: "text",
|
||||
mime: "image/svg+xml",
|
||||
content: "<svg />",
|
||||
|
|
@ -21,7 +21,7 @@ describe("prompt local attachments", () => {
|
|||
|
||||
test("reads image and PDF attachments as bytes", async () => {
|
||||
const content = new Uint8Array([1, 2, 3])
|
||||
expect(await readLocalAttachment(files({ mime: "application/pdf", bytes: content }), "/tmp/file.pdf")).toEqual({
|
||||
expect(await readLocalAttachmentWith(files({ mime: "application/pdf", bytes: content }), "/tmp/file.pdf")).toEqual({
|
||||
type: "binary",
|
||||
mime: "application/pdf",
|
||||
content,
|
||||
|
|
@ -29,9 +29,9 @@ describe("prompt local attachments", () => {
|
|||
})
|
||||
|
||||
test("ignores unsupported and unreadable local files", async () => {
|
||||
expect(await readLocalAttachment(files({ mime: "text/plain" }), "/tmp/file.txt")).toBeUndefined()
|
||||
expect(await readLocalAttachmentWith(files({ mime: "text/plain" }), "/tmp/file.txt")).toBeUndefined()
|
||||
expect(
|
||||
await readLocalAttachment(
|
||||
await readLocalAttachmentWith(
|
||||
{
|
||||
...files({ mime: "image/png" }),
|
||||
readBytes: async () => Promise.reject(new Error("missing")),
|
||||
|
|
|
|||
|
|
@ -1,14 +1,10 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { testRender } from "@opentui/solid"
|
||||
import { abbreviateHome } from "../src/runtime"
|
||||
import {
|
||||
abbreviateHome,
|
||||
createTuiBuildInfo,
|
||||
createTuiEnvironment,
|
||||
TuiBuildInfoProvider,
|
||||
TuiEnvironmentProvider,
|
||||
useTuiBuildInfo,
|
||||
useTuiEnvironment,
|
||||
} from "../src/runtime"
|
||||
TuiPathsProvider,
|
||||
useTuiPaths,
|
||||
} from "../src/context/runtime"
|
||||
|
||||
test("abbreviates paths within home boundaries", () => {
|
||||
expect(abbreviateHome("/home/test", "/home/test")).toBe("~")
|
||||
|
|
@ -17,48 +13,27 @@ test("abbreviates paths within home boundaries", () => {
|
|||
expect(abbreviateHome("/tmp/project", "/home/test")).toBe("/tmp/project")
|
||||
})
|
||||
|
||||
test("provides immutable runtime inputs", async () => {
|
||||
const environment = createTuiEnvironment({
|
||||
cwd: "/work",
|
||||
platform: "linux",
|
||||
paths: { home: "/home/test", state: "/state", worktree: "/data/worktree" },
|
||||
capabilities: {
|
||||
mouse: true,
|
||||
copyOnSelect: true,
|
||||
terminalTitle: true,
|
||||
terminalSuspend: true,
|
||||
workspaces: false,
|
||||
showTimeToFirstDraw: false,
|
||||
},
|
||||
terminal: { multiplexer: "tmux", displayServer: "wayland" },
|
||||
editor: { command: "vim", port: 4242, zedTerminal: false },
|
||||
skipInitialLoading: false,
|
||||
})
|
||||
const build = createTuiBuildInfo({ version: "1.2.3", channel: "beta" })
|
||||
test("provides focused immutable runtime inputs", async () => {
|
||||
let paths: ReturnType<typeof useTuiPaths>
|
||||
|
||||
function Runtime() {
|
||||
const runtime = useTuiEnvironment()
|
||||
const info = useTuiBuildInfo()
|
||||
return <text>{`${runtime.cwd} ${runtime.editor.command} ${info.version}`}</text>
|
||||
paths = useTuiPaths()
|
||||
return <text>{paths.cwd}</text>
|
||||
}
|
||||
|
||||
const app = await testRender(
|
||||
() => (
|
||||
<TuiEnvironmentProvider value={environment}>
|
||||
<TuiBuildInfoProvider value={build}>
|
||||
<Runtime />
|
||||
</TuiBuildInfoProvider>
|
||||
</TuiEnvironmentProvider>
|
||||
<TuiPathsProvider value={{ cwd: "/work", home: "/home/test", state: "/state", worktree: "/worktree" }}>
|
||||
<Runtime />
|
||||
</TuiPathsProvider>
|
||||
),
|
||||
{ width: 40, height: 3 },
|
||||
)
|
||||
|
||||
try {
|
||||
await app.renderOnce()
|
||||
expect(app.captureCharFrame()).toContain("/work vim 1.2.3")
|
||||
expect(Object.isFrozen(environment)).toBe(true)
|
||||
expect(Object.isFrozen(environment.paths)).toBe(true)
|
||||
expect(Object.isFrozen(build)).toBe(true)
|
||||
expect(app.captureCharFrame()).toContain("/work")
|
||||
expect(Object.isFrozen(paths!)).toBe(true)
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { mkdir, writeFile } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import type { TerminalColors } from "@opentui/core"
|
||||
import { DEFAULT_THEMES, addTheme, allThemes, hasTheme, resolveTheme, terminalMode } from "../src/theme"
|
||||
import { discoverThemes } from "../src/context/theme"
|
||||
import { tmpdir } from "./fixture/fixture"
|
||||
|
||||
test("addTheme writes into module theme store", () => {
|
||||
const name = `plugin-theme-${Date.now()}`
|
||||
|
|
@ -63,3 +67,15 @@ test("terminalMode derives mode from refreshed background", () => {
|
|||
test("terminalMode does not derive mode from ANSI slot zero", () => {
|
||||
expect(terminalMode(terminalColors(null, ["#000000"]))).toBeUndefined()
|
||||
})
|
||||
|
||||
test("custom theme precedence follows directory order", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const global = path.join(tmp.path, "global")
|
||||
const project = path.join(tmp.path, "project")
|
||||
await mkdir(path.join(global, "themes"), { recursive: true })
|
||||
await mkdir(path.join(project, "themes"), { recursive: true })
|
||||
await writeFile(path.join(global, "themes", "custom.json"), JSON.stringify({ source: "global" }))
|
||||
await writeFile(path.join(project, "themes", "custom.json"), JSON.stringify({ source: "project" }))
|
||||
|
||||
await expect(discoverThemes([global, project])).resolves.toEqual({ custom: { source: "project" } })
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { sessionExitSummary } from "../../src/util/presentation"
|
||||
import { expect, test } from "bun:test"
|
||||
import { sessionEpilogue } from "../../src/util/presentation"
|
||||
|
||||
describe("util.presentation", () => {
|
||||
test("formats the ANSI session exit summary", () => {
|
||||
const summary = sessionExitSummary({ title: "A session", sessionID: "ses_123" })
|
||||
expect(summary.split("\n")).toHaveLength(8)
|
||||
expect(summary).toContain("\x1b[90mSession \x1b[0m\x1b[1mA session\x1b[0m")
|
||||
expect(summary).toContain("\x1b[1mopencode -s ses_123\x1b[0m")
|
||||
})
|
||||
test("formats session continuation summary", () => {
|
||||
const epilogue = sessionEpilogue({ title: "A session", sessionID: "ses_123" })
|
||||
expect(epilogue).toContain("A session")
|
||||
expect(epilogue).toContain("opencode -s ses_123")
|
||||
})
|
||||
|
|
|
|||
30
packages/tui/test/util/renderer.test.ts
Normal file
30
packages/tui/test/util/renderer.test.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { destroyRenderer } from "../../src/util/renderer"
|
||||
|
||||
test("clears the terminal title before destroying the renderer", () => {
|
||||
const calls: string[] = []
|
||||
destroyRenderer({
|
||||
isDestroyed: false,
|
||||
setTerminalTitle(title) {
|
||||
calls.push(`title:${title}`)
|
||||
},
|
||||
destroy() {
|
||||
calls.push("destroy")
|
||||
},
|
||||
})
|
||||
expect(calls).toEqual(["title:", "destroy"])
|
||||
})
|
||||
|
||||
test("still clears the title after renderer destruction", () => {
|
||||
const calls: string[] = []
|
||||
destroyRenderer({
|
||||
isDestroyed: true,
|
||||
setTerminalTitle(title) {
|
||||
calls.push(`title:${title}`)
|
||||
},
|
||||
destroy() {
|
||||
calls.push("destroy")
|
||||
},
|
||||
})
|
||||
expect(calls).toEqual(["title:"])
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue