| 1 | import { EnvironmentId, RuntimeRequestId, ThreadId } from "@t3tools/contracts"; |
| 2 | import { act, type ReactNode } from "react"; |
| 3 | import { create, type ReactTestRenderer } from "react-test-renderer"; |
| 4 | import { expect, it, vi } from "vite-plus/test"; |
| 5 | |
| 6 | vi.mock("../ui/collapsible", () => { |
| 7 | const Children = ({ children }: { children: ReactNode }) => <>{children}</>; |
| 8 | return { |
| 9 | Collapsible: Children, |
| 10 | CollapsiblePanel: Children, |
| 11 | CollapsibleTrigger: Children, |
| 12 | }; |
| 13 | }); |
| 14 | |
| 15 | vi.mock("../ChatMarkdown", () => ({ default: ({ text }: { text: string }) => <p>{text}</p> })); |
| 16 | |
| 17 | import { ComposerPendingUserInputPanel } from "./ComposerPendingUserInputPanel"; |
| 18 | |
| 19 | const threadRef = { environmentId: EnvironmentId.make("env"), threadId: ThreadId.make("thread") }; |
| 20 | |
| 21 | it("cancels an answer's pending auto-submit on revocation and resumes after a new grant", async () => { |
| 22 | vi.useFakeTimers(); |
| 23 | vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); |
| 24 | vi.stubGlobal("window", { |
| 25 | setTimeout: globalThis.setTimeout, |
| 26 | clearTimeout: globalThis.clearTimeout, |
| 27 | }); |
| 28 | vi.stubGlobal("document", { addEventListener: vi.fn(), removeEventListener: vi.fn() }); |
| 29 | const onToggleOption = vi.fn(); |
| 30 | const onAdvance = vi.fn(); |
| 31 | const onDismiss = vi.fn(); |
| 32 | const prompt = { |
| 33 | requestId: RuntimeRequestId.make("scoped-answer"), |
| 34 | createdAt: "2026-09-05T00:00:00.000Z", |
| 35 | dismissible: false, |
| 36 | responseCapability: "live" as const, |
| 37 | questions: [ |
| 38 | { |
| 39 | id: "approach", |
| 40 | header: "Approach", |
| 41 | question: "Which approach?", |
| 42 | options: [{ label: "Incremental", description: "One module at a time" }], |
| 43 | multiSelect: false, |
| 44 | }, |
| 45 | ], |
| 46 | }; |
| 47 | const panel = (disabled: boolean) => ( |
| 48 | <ComposerPendingUserInputPanel |
| 49 | pendingUserInputs={[prompt]} |
| 50 | cwd="/repo" |
| 51 | threadRef={threadRef} |
| 52 | disabled={disabled} |
| 53 | respondingRequestIds={[]} |
| 54 | answers={{}} |
| 55 | questionIndex={0} |
| 56 | onToggleOption={onToggleOption} |
| 57 | onAdvance={onAdvance} |
| 58 | onDismiss={onDismiss} |
| 59 | /> |
| 60 | ); |
| 61 | let renderer: ReactTestRenderer | undefined; |
| 62 | const chooseOption = () => { |
| 63 | const option = renderer!.root |
| 64 | .findAllByType("button") |
| 65 | .find((button) => |
| 66 | button.findAllByType("span").some((span) => span.children.includes("Incremental")), |
| 67 | ); |
| 68 | expect(option).toBeDefined(); |
| 69 | option!.props.onClick(); |
| 70 | }; |
| 71 | try { |
| 72 | await act(async () => { |
| 73 | renderer = create(panel(false)); |
| 74 | }); |
| 75 | await act(async () => chooseOption()); |
| 76 | expect(onToggleOption).toHaveBeenCalledWith("approach", "Incremental"); |
| 77 | expect(onAdvance).not.toHaveBeenCalled(); |
| 78 | |
| 79 | await act(async () => renderer!.update(panel(true))); |
| 80 | await act(async () => { |
| 81 | chooseOption(); |
| 82 | vi.runAllTimers(); |
| 83 | }); |
| 84 | expect(onToggleOption).toHaveBeenCalledTimes(1); |
| 85 | expect(onAdvance).not.toHaveBeenCalled(); |
| 86 | |
| 87 | await act(async () => renderer!.update(panel(false))); |
| 88 | await act(async () => { |
| 89 | chooseOption(); |
| 90 | vi.runAllTimers(); |
| 91 | }); |
| 92 | expect(onToggleOption).toHaveBeenCalledTimes(2); |
| 93 | expect(onAdvance).toHaveBeenCalledTimes(1); |
| 94 | } finally { |
| 95 | await act(async () => renderer?.unmount()); |
| 96 | vi.unstubAllGlobals(); |
| 97 | vi.useRealTimers(); |
| 98 | } |
| 99 | }); |