import { act, renderHook, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type React from "react"; import { useChatInputState } from "./use-chat-input-state"; import type { TipTapInputHandle } from "./tiptap-input"; type SubmitHandler = Parameters[0]["onSubmit"]; function renderInputState(onSubmit: SubmitHandler) { return renderHook(() => useChatInputState({ sessionId: "session-1", isSending: false, contextItems: [], showRequestChangesTooltip: false, onSubmit, }), ); } function attachInputHandle( inputRef: React.RefObject, clear: ReturnType, ) { (inputRef as React.MutableRefObject | null>).current = { clear, getMentions: () => [], getTaskMentions: () => [], }; } function deferred() { let resolve!: (value: T) => void; const promise = new Promise((res) => { resolve = res; }); return { promise, resolve }; } describe("useChatInputState", () => { beforeEach(() => { localStorage.clear(); }); it("keeps the draft when async submit reports failure", async () => { const onSubmit = vi .fn, ReturnType>() .mockResolvedValue(false); const clear = vi.fn(); const { result } = renderInputState(onSubmit); act(() => { result.current.handleChange("hello"); attachInputHandle(result.current.inputRef, clear); }); await waitFor(() => expect(result.current.value).toBe("hello")); act(() => { result.current.handleSubmit(vi.fn()); }); await waitFor(() => expect(onSubmit).toHaveBeenCalledWith("hello", undefined, undefined, undefined, undefined), ); expect(result.current.value).toBe("hello"); expect(clear).not.toHaveBeenCalled(); }); it("clears the draft when async submit succeeds", async () => { const onSubmit = vi .fn, ReturnType>() .mockResolvedValue(true); const clear = vi.fn(); const resetHeight = vi.fn(); const { result } = renderInputState(onSubmit); act(() => { result.current.handleChange("hello"); attachInputHandle(result.current.inputRef, clear); }); await waitFor(() => expect(result.current.value).toBe("hello")); act(() => { result.current.handleSubmit(resetHeight); }); await waitFor(() => expect(result.current.value).toBe("")); expect(clear).toHaveBeenCalled(); expect(resetHeight).toHaveBeenCalled(); }); it("keeps newer attachments when async submit succeeds after attachments change", async () => { const submit = deferred(); const onSubmit = vi .fn, ReturnType>() .mockReturnValue(submit.promise); const clear = vi.fn(); const resetHeight = vi.fn(); const { result } = renderInputState(onSubmit); act(() => { result.current.handleChange("hello"); attachInputHandle(result.current.inputRef, clear); }); await waitFor(() => expect(result.current.value).toBe("hello")); act(() => { result.current.handleSubmit(resetHeight); }); await waitFor(() => expect(onSubmit).toHaveBeenCalled()); await act(async () => { await result.current.addFiles([ new File(["new attachment"], "later.txt", { type: "text/plain" }), ]); }); await waitFor(() => expect(result.current.allItems).toHaveLength(1)); await act(async () => { submit.resolve(true); await submit.promise; }); await waitFor(() => expect(result.current.value).toBe("")); expect(result.current.allItems).toHaveLength(1); expect(clear).toHaveBeenCalled(); expect(resetHeight).toHaveBeenCalled(); }); });