Files
storkit/frontend/src/components/ChatInput.test.tsx

280 lines
6.8 KiB
TypeScript
Raw Normal View History

import { act, fireEvent, render, screen } from "@testing-library/react";
import * as React from "react";
import { describe, expect, it, vi } from "vitest";
import type { ChatInputHandle } from "./ChatInput";
import { ChatInput } from "./ChatInput";
describe("ChatInput component (Story 178 AC1)", () => {
it("renders a textarea with Send a message... placeholder", () => {
render(
<ChatInput
loading={false}
queuedMessages={[]}
onSubmit={vi.fn()}
onCancel={vi.fn()}
onRemoveQueuedMessage={vi.fn()}
/>,
);
const textarea = screen.getByPlaceholderText("Send a message...");
expect(textarea.tagName.toLowerCase()).toBe("textarea");
});
it("manages input state internally — typing updates value without calling onSubmit", async () => {
const onSubmit = vi.fn();
render(
<ChatInput
loading={false}
queuedMessages={[]}
onSubmit={onSubmit}
onCancel={vi.fn()}
onRemoveQueuedMessage={vi.fn()}
/>,
);
const textarea = screen.getByPlaceholderText("Send a message...");
await act(async () => {
fireEvent.change(textarea, { target: { value: "hello world" } });
});
expect((textarea as HTMLTextAreaElement).value).toBe("hello world");
expect(onSubmit).not.toHaveBeenCalled();
});
it("calls onSubmit with the input text on Enter key press", async () => {
const onSubmit = vi.fn();
render(
<ChatInput
loading={false}
queuedMessages={[]}
onSubmit={onSubmit}
onCancel={vi.fn()}
onRemoveQueuedMessage={vi.fn()}
/>,
);
const textarea = screen.getByPlaceholderText("Send a message...");
await act(async () => {
fireEvent.change(textarea, { target: { value: "test message" } });
});
await act(async () => {
fireEvent.keyDown(textarea, { key: "Enter", shiftKey: false });
});
expect(onSubmit).toHaveBeenCalledWith("test message");
});
it("clears input after submitting", async () => {
render(
<ChatInput
loading={false}
queuedMessages={[]}
onSubmit={vi.fn()}
onCancel={vi.fn()}
onRemoveQueuedMessage={vi.fn()}
/>,
);
const textarea = screen.getByPlaceholderText("Send a message...");
await act(async () => {
fireEvent.change(textarea, { target: { value: "hello" } });
});
await act(async () => {
fireEvent.keyDown(textarea, { key: "Enter", shiftKey: false });
});
expect((textarea as HTMLTextAreaElement).value).toBe("");
});
it("does not submit on Shift+Enter", async () => {
const onSubmit = vi.fn();
render(
<ChatInput
loading={false}
queuedMessages={[]}
onSubmit={onSubmit}
onCancel={vi.fn()}
onRemoveQueuedMessage={vi.fn()}
/>,
);
const textarea = screen.getByPlaceholderText("Send a message...");
await act(async () => {
fireEvent.change(textarea, { target: { value: "multiline" } });
});
await act(async () => {
fireEvent.keyDown(textarea, { key: "Enter", shiftKey: true });
});
expect(onSubmit).not.toHaveBeenCalled();
expect((textarea as HTMLTextAreaElement).value).toBe("multiline");
});
it("calls onCancel when stop button is clicked while loading with empty input", async () => {
const onCancel = vi.fn();
render(
<ChatInput
loading={true}
queuedMessages={[]}
onSubmit={vi.fn()}
onCancel={onCancel}
onRemoveQueuedMessage={vi.fn()}
/>,
);
const stopButton = screen.getByRole("button", { name: "■" });
await act(async () => {
fireEvent.click(stopButton);
});
expect(onCancel).toHaveBeenCalled();
});
it("renders queued message indicators", () => {
render(
<ChatInput
loading={true}
queuedMessages={[
{ id: "1", text: "first message" },
{ id: "2", text: "second message" },
]}
onSubmit={vi.fn()}
onCancel={vi.fn()}
onRemoveQueuedMessage={vi.fn()}
/>,
);
const indicators = screen.getAllByTestId("queued-message-indicator");
expect(indicators).toHaveLength(2);
expect(indicators[0]).toHaveTextContent("first message");
expect(indicators[1]).toHaveTextContent("second message");
});
it("calls onRemoveQueuedMessage when cancel button is clicked", async () => {
const onRemove = vi.fn();
render(
<ChatInput
loading={true}
queuedMessages={[{ id: "q1", text: "to remove" }]}
onSubmit={vi.fn()}
onCancel={vi.fn()}
onRemoveQueuedMessage={onRemove}
/>,
);
const cancelBtn = screen.getByTitle("Cancel queued message");
await act(async () => {
fireEvent.click(cancelBtn);
});
expect(onRemove).toHaveBeenCalledWith("q1");
});
it("edit button restores queued message text to input and removes from queue", async () => {
const onRemove = vi.fn();
render(
<ChatInput
loading={true}
queuedMessages={[{ id: "q1", text: "edit me back" }]}
onSubmit={vi.fn()}
onCancel={vi.fn()}
onRemoveQueuedMessage={onRemove}
/>,
);
const editBtn = screen.getByTitle("Edit queued message");
await act(async () => {
fireEvent.click(editBtn);
});
const textarea = screen.getByPlaceholderText("Send a message...");
expect((textarea as HTMLTextAreaElement).value).toBe("edit me back");
expect(onRemove).toHaveBeenCalledWith("q1");
});
});
describe("ChatInput appendToInput (Bug 215 regression)", () => {
it("appendToInput sets text into an empty input", async () => {
const ref = React.createRef<ChatInputHandle>();
render(
<ChatInput
ref={ref}
loading={false}
queuedMessages={[]}
onSubmit={vi.fn()}
onCancel={vi.fn()}
onRemoveQueuedMessage={vi.fn()}
/>,
);
await act(async () => {
ref.current?.appendToInput("queued message");
});
const textarea = screen.getByPlaceholderText("Send a message...");
expect((textarea as HTMLTextAreaElement).value).toBe("queued message");
});
it("appendToInput appends to existing input content with a newline separator", async () => {
const ref = React.createRef<ChatInputHandle>();
render(
<ChatInput
ref={ref}
loading={false}
queuedMessages={[]}
onSubmit={vi.fn()}
onCancel={vi.fn()}
onRemoveQueuedMessage={vi.fn()}
/>,
);
const textarea = screen.getByPlaceholderText("Send a message...");
await act(async () => {
fireEvent.change(textarea, { target: { value: "existing text" } });
});
await act(async () => {
ref.current?.appendToInput("appended text");
});
expect((textarea as HTMLTextAreaElement).value).toBe(
"existing text\nappended text",
);
});
it("multiple queued messages joined with newlines are appended on cancel", async () => {
const ref = React.createRef<ChatInputHandle>();
render(
<ChatInput
ref={ref}
loading={false}
queuedMessages={[]}
onSubmit={vi.fn()}
onCancel={vi.fn()}
onRemoveQueuedMessage={vi.fn()}
/>,
);
await act(async () => {
ref.current?.appendToInput("msg one\nmsg two");
});
const textarea = screen.getByPlaceholderText("Send a message...");
expect((textarea as HTMLTextAreaElement).value).toBe("msg one\nmsg two");
});
});