280 lines
6.8 KiB
TypeScript
280 lines
6.8 KiB
TypeScript
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");
|
|
});
|
|
});
|