refactor(form): model links as fields (#36129)
This commit is contained in:
parent
6a85f0d3db
commit
a6449cb45c
24 changed files with 2409 additions and 1430 deletions
|
|
@ -77,16 +77,12 @@ function question(id: string, sessionID = "session"): QuestionRequest {
|
|||
}
|
||||
}
|
||||
|
||||
function form(
|
||||
id: string,
|
||||
sessionID = "session",
|
||||
): Extract<OpenCodeEvent, { type: "form.created" }>["data"]["form"] {
|
||||
function form(id: string, sessionID = "session"): Extract<OpenCodeEvent, { type: "form.created" }>["data"]["form"] {
|
||||
return {
|
||||
id,
|
||||
sessionID,
|
||||
title: "Input requested",
|
||||
mode: "form",
|
||||
fields: [],
|
||||
fields: [{ key: "authorization", type: "external", url: "https://example.com" }],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,14 @@ import { createSessionRows, type SessionRow } from "../../../src/routes/session/
|
|||
import { createApi, createClient, createEventStream, createFetch, directory, json } from "../../fixture/tui-sdk"
|
||||
import { TestTuiContexts } from "../../fixture/tui-environment"
|
||||
|
||||
const formFields = [{ key: "authorization", type: "external", url: "https://example.com" }] satisfies [
|
||||
{
|
||||
key: string
|
||||
type: "external"
|
||||
url: string
|
||||
},
|
||||
]
|
||||
|
||||
async function wait(fn: () => boolean, timeout = 2000) {
|
||||
const start = Date.now()
|
||||
while (!fn()) {
|
||||
|
|
@ -1510,7 +1518,7 @@ test("adds, dismisses, and refreshes form requests", async () => {
|
|||
const calls = createFetch((url) => {
|
||||
if (url.pathname !== "/api/session/ses_1/form") return
|
||||
return json({
|
||||
data: [{ id: "frm_remote", sessionID: "ses_1", title: "Input requested", mode: "form", fields: [] }],
|
||||
data: [{ id: "frm_remote", sessionID: "ses_1", title: "Input requested", fields: formFields }],
|
||||
})
|
||||
}, events)
|
||||
let data!: ReturnType<typeof useData>
|
||||
|
|
@ -1540,13 +1548,13 @@ test("adds, dismisses, and refreshes form requests", async () => {
|
|||
id: "evt_form_created_1",
|
||||
created: 0,
|
||||
type: "form.created",
|
||||
data: { form: { id: "frm_1", sessionID: "ses_1", title: "Input requested", mode: "form", fields: [] } },
|
||||
data: { form: { id: "frm_1", sessionID: "ses_1", title: "Input requested", fields: formFields } },
|
||||
})
|
||||
emitEvent(events, {
|
||||
id: "evt_form_created_duplicate",
|
||||
created: 1,
|
||||
type: "form.created",
|
||||
data: { form: { id: "frm_1", sessionID: "ses_1", title: "Input requested", mode: "form", fields: [] } },
|
||||
data: { form: { id: "frm_1", sessionID: "ses_1", title: "Input requested", fields: formFields } },
|
||||
})
|
||||
await wait(() => data.session.form.list("ses_1")?.length === 1)
|
||||
|
||||
|
|
@ -1562,7 +1570,7 @@ test("adds, dismisses, and refreshes form requests", async () => {
|
|||
id: "evt_form_created_2",
|
||||
created: 3,
|
||||
type: "form.created",
|
||||
data: { form: { id: "frm_2", sessionID: "ses_1", title: "Input requested", mode: "form", fields: [] } },
|
||||
data: { form: { id: "frm_2", sessionID: "ses_1", title: "Input requested", fields: formFields } },
|
||||
})
|
||||
emitEvent(events, {
|
||||
id: "evt_form_cancelled_2",
|
||||
|
|
@ -1612,7 +1620,7 @@ test("tracks global forms by location", async () => {
|
|||
location: other,
|
||||
type: "form.created",
|
||||
data: {
|
||||
form: { id: "frm_other", sessionID: "global", title: "Input requested", mode: "form", fields: [] },
|
||||
form: { id: "frm_other", sessionID: "global", title: "Input requested", fields: formFields },
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -1625,7 +1633,7 @@ test("tracks global forms by location", async () => {
|
|||
location: { directory },
|
||||
type: "form.created",
|
||||
data: {
|
||||
form: { id: "frm_default", sessionID: "global", title: "Input requested", mode: "form", fields: [] },
|
||||
form: { id: "frm_default", sessionID: "global", title: "Input requested", fields: formFields },
|
||||
},
|
||||
})
|
||||
await wait(() => data.session.form.list("global", { directory })?.length === 1)
|
||||
|
|
@ -1664,8 +1672,7 @@ test("refreshes global forms for the requested location", async () => {
|
|||
id: requestedDirectory === other.directory ? "frm_other" : "frm_default",
|
||||
sessionID: "global",
|
||||
title: "Input requested",
|
||||
mode: "form",
|
||||
fields: [],
|
||||
fields: formFields,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
|
@ -1743,8 +1750,7 @@ test("refreshes global forms once per loaded location after reconnect", async ()
|
|||
id: `frm_${requestedDirectory === other.directory ? "other" : "default"}_${count}`,
|
||||
sessionID: "global",
|
||||
title: "Input requested",
|
||||
mode: "form",
|
||||
fields: [],
|
||||
fields: formFields,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
|
@ -1803,13 +1809,12 @@ test("refreshes global forms once per loaded location after reconnect", async ()
|
|||
test("reconciles all pending form requests when the event stream reconnects", async () => {
|
||||
const events = createEventStream()
|
||||
let requests = [
|
||||
{ id: "frm_old", sessionID: "ses_old", title: "Input requested", mode: "form" as const, fields: [] },
|
||||
{ id: "frm_old", sessionID: "ses_old", title: "Input requested", fields: formFields },
|
||||
{
|
||||
id: "frm_keep",
|
||||
sessionID: "ses_keep",
|
||||
title: "Input requested",
|
||||
mode: "url" as const,
|
||||
url: "https://example.com",
|
||||
fields: [{ key: "authorization", type: "external" as const, url: "https://example.com" }],
|
||||
},
|
||||
]
|
||||
let calls = 0
|
||||
|
|
@ -1841,7 +1846,7 @@ test("reconciles all pending form requests when the event stream reconnects", as
|
|||
await wait(() => data.session.form.list("ses_old")?.[0]?.id === "frm_old")
|
||||
expect(data.session.form.list("ses_keep")?.[0]?.id).toBe("frm_keep")
|
||||
|
||||
requests = [{ id: "frm_new", sessionID: "ses_new", title: "Input requested", mode: "form" as const, fields: [] }]
|
||||
requests = [{ id: "frm_new", sessionID: "ses_new", title: "Input requested", fields: formFields }]
|
||||
events.disconnect()
|
||||
|
||||
await wait(() => calls === 2 && data.session.form.list("ses_new")?.[0]?.id === "frm_new")
|
||||
|
|
|
|||
139
packages/tui/test/cli/tui/form.test.tsx
Normal file
139
packages/tui/test/cli/tui/form.test.tsx
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
/** @jsxImportSource @opentui/solid */
|
||||
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 { ClipboardProvider } from "../../../src/context/clipboard"
|
||||
import type { FormWithLocation } from "../../../src/context/data"
|
||||
import { KVProvider } from "../../../src/context/kv"
|
||||
import { SDKProvider } from "../../../src/context/sdk"
|
||||
import { ThemeProvider } from "../../../src/context/theme"
|
||||
import { TuiConfigProvider } from "../../../src/config"
|
||||
import { OpencodeKeymapProvider, registerOpencodeKeymap } from "../../../src/keymap"
|
||||
import { ToastProvider } from "../../../src/ui/toast"
|
||||
import { tmpdir } from "../../fixture/fixture"
|
||||
import { TestTuiContexts } from "../../fixture/tui-environment"
|
||||
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
|
||||
import { createApi, createClient, createEventStream, createFetch } from "../../fixture/tui-sdk"
|
||||
|
||||
async function mountForm(root: string, width = 80) {
|
||||
const state = path.join(root, "state")
|
||||
await mkdir(state, { recursive: true })
|
||||
await Bun.write(path.join(state, "kv.json"), "{}")
|
||||
|
||||
const replies: unknown[] = []
|
||||
const copied: string[] = []
|
||||
const events = createEventStream()
|
||||
const transport = createFetch(
|
||||
(url, request) =>
|
||||
url.pathname === "/api/session/ses_test/form/frm_test/reply"
|
||||
? request.json().then((answer) => {
|
||||
replies.push(answer)
|
||||
return new Response(null, { status: 204 })
|
||||
})
|
||||
: undefined,
|
||||
events,
|
||||
)
|
||||
const config = createTuiResolvedConfig()
|
||||
const form = {
|
||||
id: "frm_test",
|
||||
sessionID: "ses_test",
|
||||
title: "Authorization required",
|
||||
fields: [
|
||||
{
|
||||
key: "authorization",
|
||||
type: "external",
|
||||
url: "https://example.com/authorize",
|
||||
title: "Authorize access",
|
||||
},
|
||||
],
|
||||
} satisfies FormWithLocation
|
||||
const { FormPrompt } = await import("../../../src/routes/session/form")
|
||||
|
||||
function Harness() {
|
||||
const renderer = useRenderer()
|
||||
const keymap = createDefaultOpenTuiKeymap(renderer)
|
||||
const off = registerOpencodeKeymap(keymap, renderer, config)
|
||||
onCleanup(off)
|
||||
|
||||
return (
|
||||
<TestTuiContexts
|
||||
directory={root}
|
||||
paths={{
|
||||
home: root,
|
||||
state,
|
||||
worktree: root,
|
||||
}}
|
||||
>
|
||||
<ClipboardProvider
|
||||
value={{
|
||||
write(text) {
|
||||
copied.push(text)
|
||||
return Promise.resolve()
|
||||
},
|
||||
}}
|
||||
>
|
||||
<OpencodeKeymapProvider keymap={keymap}>
|
||||
<TuiConfigProvider config={config}>
|
||||
<SDKProvider client={createClient(transport.fetch)} api={createApi(transport.fetch)}>
|
||||
<KVProvider>
|
||||
<ThemeProvider mode="dark" source={{ discover: () => Promise.resolve({}) }}>
|
||||
<ToastProvider>
|
||||
<FormPrompt form={form} />
|
||||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
</KVProvider>
|
||||
</SDKProvider>
|
||||
</TuiConfigProvider>
|
||||
</OpencodeKeymapProvider>
|
||||
</ClipboardProvider>
|
||||
</TestTuiContexts>
|
||||
)
|
||||
}
|
||||
|
||||
const app = await testRender(() => <Harness />, { width, height: 20, kittyKeyboard: true })
|
||||
app.renderer.start()
|
||||
await app.waitForFrame((frame) => frame.includes("Authorization required"))
|
||||
return { app, copied, replies }
|
||||
}
|
||||
|
||||
test("requires explicit acknowledgement before submitting an external field", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const prompt = await mountForm(tmp.path)
|
||||
try {
|
||||
prompt.app.mockInput.pressKey("right")
|
||||
await prompt.app.waitForFrame((frame) => frame.includes("(acknowledgement required)"))
|
||||
prompt.app.mockInput.pressEnter()
|
||||
await prompt.app.waitForFrame((frame) => frame.includes("External action must be acknowledged"))
|
||||
expect(prompt.replies).toEqual([])
|
||||
|
||||
prompt.app.mockInput.pressKey("left")
|
||||
prompt.app.mockInput.pressKey("c")
|
||||
await prompt.app.waitForFrame((frame) => frame.includes("press enter to confirm"))
|
||||
expect(prompt.copied).toEqual(["https://example.com/authorize"])
|
||||
expect(prompt.replies).toEqual([])
|
||||
|
||||
prompt.app.mockInput.pressEnter()
|
||||
await prompt.app.waitForFrame((frame) => frame.includes("Acknowledged"))
|
||||
expect(prompt.replies).toEqual([])
|
||||
|
||||
prompt.app.mockInput.pressEnter()
|
||||
await prompt.app.waitFor(() => prompt.replies.length === 1)
|
||||
expect(prompt.replies).toEqual([{ answer: { authorization: true } }])
|
||||
} finally {
|
||||
prompt.app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("includes external acknowledgements in progress", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const prompt = await mountForm(tmp.path, 32)
|
||||
try {
|
||||
expect(prompt.app.captureCharFrame()).toContain("0/1")
|
||||
expect(prompt.replies).toEqual([])
|
||||
} finally {
|
||||
prompt.app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue