1import { EnvironmentId, RuntimeRequestId, ThreadId } from "@t3tools/contracts";
2import { renderToStaticMarkup } from "react-dom/server";
3import { describe, expect, it } from "vite-plus/test";
4
5import { ComposerPendingUserInputPanel } from "./ComposerPendingUserInputPanel";
6
7const threadRef = { environmentId: EnvironmentId.make("env"), threadId: ThreadId.make("thread") };
8import type { PendingUserInput } from "../../session-logic";
9
10const prompt: PendingUserInput = {
11 requestId: RuntimeRequestId.make("request-1"),
12 responseCapability: "live" as const,
13 createdAt: "2026-08-15T00:00:00.000Z",
14 questions: [
15 {
16 id: "question-1",
17 header: "Approach",
18 question: "Which approach should the migration take?",
19 options: [
20 { label: "Incremental", description: "Move one module at a time" },
21 { label: "Big bang", description: "Move everything in one release" },
22 ],
23 multiSelect: false,
24 },
25 ],
26 dismissible: true,
27};
28
29function renderPanel(pendingUserInput: PendingUserInput = prompt) {
30 return renderToStaticMarkup(
31 <ComposerPendingUserInputPanel
32 pendingUserInputs={[pendingUserInput]}
33 cwd="/repo"
34 threadRef={threadRef}
35 respondingRequestIds={[]}
36 answers={{}}
37 questionIndex={0}
38 onToggleOption={() => {}}
39 onAdvance={() => {}}
40 onDismiss={() => {}}
41 />,
42 );
43}
44
45describe("ComposerPendingUserInputPanel", () => {
46 it("renders the header as a disclosure control for the question body", () => {
47 const markup = renderPanel();
48
49 const toggle = markup.match(/<button[^>]*data-pending-user-input-toggle="[^"]*"[^>]*>/)?.[0];
50 expect(toggle).toBeDefined();
51 expect(toggle).toContain('data-pending-user-input-toggle="expanded"');
52 expect(toggle).toContain('aria-expanded="true"');
53 expect(toggle).toContain('type="button"');
54
55 const controlledId = toggle?.match(/aria-controls="([^"]+)"/)?.[1];
56 expect(controlledId).toBeDefined();
57 expect(markup).toMatch(new RegExp(`<div[^>]*\\sid="${controlledId}"`));
58 });
59
60 it("offers dismiss only for async questions", () => {
61 expect(renderPanel()).toContain("data-pending-user-input-dismiss");
62 expect(renderPanel({ ...prompt, dismissible: false })).not.toContain(
63 "data-pending-user-input-dismiss",
64 );
65 });
66
67 it("starts expanded so the question and its options are visible", () => {
68 const markup = renderPanel();
69
70 expect(markup).toContain("Approach");
71 expect(markup).toContain("Which approach should the migration take?");
72 expect(markup).toContain("Incremental");
73 expect(markup).toContain("Big bang");
74 });
75
76 it("links URLs in the question text", () => {
77 const url = "https://accounts.t3.codes/device?user_code=RTJS-TQGS";
78 const question = prompt.questions[0]!;
79 const markup = renderPanel({
80 ...prompt,
81 questions: [{ ...question, question: `Authorize BeeBox at ${url} and confirm.` }],
82 });
83
84 expect(markup).toMatch(new RegExp(`<a[^>]*href="${url.replace(/[?.]/g, "\\$&")}"`));
85 });
86});