Files
kandev/apps/web/components/task/chat/use-chat-input-state.test.ts
T
2026-06-16 13:58:48 +01:00

130 lines
3.8 KiB
TypeScript

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<typeof useChatInputState>[0]["onSubmit"];
function renderInputState(onSubmit: SubmitHandler) {
return renderHook(() =>
useChatInputState({
sessionId: "session-1",
isSending: false,
contextItems: [],
showRequestChangesTooltip: false,
onSubmit,
}),
);
}
function attachInputHandle(
inputRef: React.RefObject<TipTapInputHandle | null>,
clear: ReturnType<typeof vi.fn>,
) {
(inputRef as React.MutableRefObject<Partial<TipTapInputHandle> | null>).current = {
clear,
getMentions: () => [],
getTaskMentions: () => [],
};
}
function deferred<T>() {
let resolve!: (value: T) => void;
const promise = new Promise<T>((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<Parameters<SubmitHandler>, ReturnType<SubmitHandler>>()
.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<Parameters<SubmitHandler>, ReturnType<SubmitHandler>>()
.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<boolean>();
const onSubmit = vi
.fn<Parameters<SubmitHandler>, ReturnType<SubmitHandler>>()
.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();
});
});