147 lines
4.6 KiB
TypeScript
147 lines
4.6 KiB
TypeScript
/** @jsxImportSource @opentui/solid */
|
|
import { TextareaRenderable } from "@opentui/core"
|
|
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
|
|
import { testRender, useRenderer } from "@opentui/solid"
|
|
import { expect, test } from "bun:test"
|
|
import { mkdir } from "node:fs/promises"
|
|
import path from "node:path"
|
|
import { onCleanup } from "solid-js"
|
|
import { tmpdir } from "../../fixture/fixture"
|
|
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
|
|
import type { TuiKeybind } from "../../../src/cli/cmd/tui/config/keybind"
|
|
|
|
async function wait(fn: () => boolean, timeout = 2000) {
|
|
const start = Date.now()
|
|
while (!fn()) {
|
|
if (Date.now() - start > timeout) throw new Error("timed out waiting for condition")
|
|
await Bun.sleep(10)
|
|
}
|
|
}
|
|
|
|
async function mountPrompt(input: {
|
|
root: string
|
|
keybinds: Partial<TuiKeybind.Keybinds>
|
|
onConfirm: (value: string) => void
|
|
}) {
|
|
const { Global } = await import("@opencode-ai/core/global")
|
|
const previous = {
|
|
config: Global.Path.config,
|
|
state: Global.Path.state,
|
|
}
|
|
Global.Path.config = path.join(input.root, "config")
|
|
Global.Path.state = path.join(input.root, "state")
|
|
await mkdir(Global.Path.config, { recursive: true })
|
|
await mkdir(Global.Path.state, { recursive: true })
|
|
await Bun.write(path.join(Global.Path.state, "kv.json"), "{}")
|
|
|
|
const [
|
|
{ DialogProvider },
|
|
{ DialogPrompt },
|
|
{ KVProvider },
|
|
{ ThemeProvider },
|
|
{ TuiConfigProvider },
|
|
{ ToastProvider },
|
|
{ OpencodeKeymapProvider, registerOpencodeKeymap },
|
|
] = await Promise.all([
|
|
import("../../../src/cli/cmd/tui/ui/dialog"),
|
|
import("../../../src/cli/cmd/tui/ui/dialog-prompt"),
|
|
import("../../../src/cli/cmd/tui/context/kv"),
|
|
import("../../../src/cli/cmd/tui/context/theme"),
|
|
import("../../../src/cli/cmd/tui/context/tui-config"),
|
|
import("../../../src/cli/cmd/tui/ui/toast"),
|
|
import("../../../src/cli/cmd/tui/keymap"),
|
|
])
|
|
|
|
function Harness() {
|
|
const renderer = useRenderer()
|
|
const keymap = createDefaultOpenTuiKeymap(renderer)
|
|
const resolvedConfig = createTuiResolvedConfig({
|
|
keybinds: input.keybinds,
|
|
leader_timeout: 1000,
|
|
})
|
|
const off = registerOpencodeKeymap(keymap, renderer, resolvedConfig)
|
|
onCleanup(off)
|
|
|
|
return (
|
|
<OpencodeKeymapProvider keymap={keymap}>
|
|
<TuiConfigProvider config={resolvedConfig}>
|
|
<KVProvider>
|
|
<ThemeProvider mode="dark">
|
|
<ToastProvider>
|
|
<DialogProvider>
|
|
<DialogPrompt title="Rename Session" value="draft" onConfirm={input.onConfirm} />
|
|
</DialogProvider>
|
|
</ToastProvider>
|
|
</ThemeProvider>
|
|
</KVProvider>
|
|
</TuiConfigProvider>
|
|
</OpencodeKeymapProvider>
|
|
)
|
|
}
|
|
|
|
const app = await testRender(() => <Harness />, { kittyKeyboard: true })
|
|
return {
|
|
app,
|
|
async cleanup() {
|
|
app.renderer.destroy()
|
|
Global.Path.config = previous.config
|
|
Global.Path.state = previous.state
|
|
},
|
|
}
|
|
}
|
|
|
|
test("dialog prompt submit wins when return is also input newline", async () => {
|
|
await using tmp = await tmpdir()
|
|
const confirmed: string[] = []
|
|
const prompt = await mountPrompt({
|
|
root: tmp.path,
|
|
keybinds: {
|
|
input_submit: "super+return",
|
|
input_newline: "return,shift+return,alt+return,ctrl+j",
|
|
},
|
|
onConfirm: (value) => confirmed.push(value),
|
|
})
|
|
|
|
try {
|
|
await wait(() => prompt.app.renderer.currentFocusedEditor instanceof TextareaRenderable)
|
|
const textarea = prompt.app.renderer.currentFocusedEditor
|
|
if (!(textarea instanceof TextareaRenderable)) throw new Error("expected focused dialog textarea")
|
|
|
|
prompt.app.mockInput.pressEnter()
|
|
|
|
expect(confirmed).toEqual(["draft"])
|
|
expect(textarea.plainText).toBe("draft")
|
|
} finally {
|
|
await prompt.cleanup()
|
|
}
|
|
})
|
|
|
|
test("dialog prompt submit can be rebound separately from input submit", async () => {
|
|
await using tmp = await tmpdir()
|
|
const confirmed: string[] = []
|
|
const prompt = await mountPrompt({
|
|
root: tmp.path,
|
|
keybinds: {
|
|
input_submit: "return",
|
|
"dialog.prompt.submit": "ctrl+y",
|
|
},
|
|
onConfirm: (value) => confirmed.push(value),
|
|
})
|
|
|
|
try {
|
|
await wait(() => prompt.app.renderer.currentFocusedEditor instanceof TextareaRenderable)
|
|
const textarea = prompt.app.renderer.currentFocusedEditor
|
|
if (!(textarea instanceof TextareaRenderable)) throw new Error("expected focused dialog textarea")
|
|
|
|
prompt.app.mockInput.pressEnter()
|
|
expect(confirmed).toEqual([])
|
|
expect(textarea.plainText).toBe("draft")
|
|
|
|
prompt.app.mockInput.pressKey("y", { ctrl: true })
|
|
|
|
expect(confirmed).toEqual(["draft"])
|
|
} finally {
|
|
await prompt.cleanup()
|
|
}
|
|
})
|