Back to Repositories

Testing ChatInput Component Interactions in OpenHands

This test suite provides comprehensive coverage of the ChatInput component, focusing on user interaction handling, input validation, and event management. The tests verify core chat functionality including message submission, input clearing, and special input handling like image paste events.

Test Coverage Overview

The test suite achieves thorough coverage of ChatInput functionality through 18 distinct test cases. Key areas include:

  • Basic input rendering and placeholder text verification
  • Message submission via button click and Enter key
  • Empty and whitespace message validation
  • Disabled state handling
  • Special key combinations (Shift+Enter)
  • Image paste functionality

Implementation Analysis

The testing approach utilizes React Testing Library’s user-event for simulating user interactions and fireEvent for direct event triggering. The implementation follows component isolation principles with mocked callbacks and employs vitest’s modern testing patterns.

Key patterns include:
  • User event simulation for keyboard and mouse interactions
  • Mock function verification for callback testing
  • DOM queries using role-based selectors
  • Clipboard event simulation for paste handling

Technical Details

Testing tools and setup:

  • @testing-library/react for component rendering
  • @testing-library/user-event for user interaction simulation
  • vitest for test running and assertions
  • Mock implementations for file handling and clipboard operations
  • Custom event triggers for paste operations

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Isolation of component testing with mock functions
  • Comprehensive edge case coverage
  • Accessibility-focused selectors (role-based queries)
  • Clear test case organization and naming
  • Proper cleanup between tests using afterEach
  • Realistic user interaction simulation

all-hands-ai/openhands

frontend/__tests__/components/chat/chat-input.test.tsx

            
import userEvent from "@testing-library/user-event";
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, afterEach, vi, it, expect } from "vitest";
import { ChatInput } from "#/components/features/chat/chat-input";

describe("ChatInput", () => {
  const onSubmitMock = vi.fn();

  afterEach(() => {
    vi.clearAllMocks();
  });

  it("should render a textarea", () => {
    render(<ChatInput onSubmit={onSubmitMock} />);
    expect(screen.getByTestId("chat-input")).toBeInTheDocument();
    expect(screen.getByRole("textbox")).toBeInTheDocument();
  });

  it("should call onSubmit when the user types and presses enter", async () => {
    const user = userEvent.setup();
    render(<ChatInput onSubmit={onSubmitMock} />);
    const textarea = screen.getByRole("textbox");

    await user.type(textarea, "Hello, world!");
    await user.keyboard("{Enter}");

    expect(onSubmitMock).toHaveBeenCalledWith("Hello, world!");
  });

  it("should call onSubmit when pressing the submit button", async () => {
    const user = userEvent.setup();
    render(<ChatInput onSubmit={onSubmitMock} />);
    const textarea = screen.getByRole("textbox");
    const button = screen.getByRole("button");

    await user.type(textarea, "Hello, world!");
    await user.click(button);

    expect(onSubmitMock).toHaveBeenCalledWith("Hello, world!");
  });

  it("should not call onSubmit when the message is empty", async () => {
    const user = userEvent.setup();
    render(<ChatInput onSubmit={onSubmitMock} />);
    const button = screen.getByRole("button");

    await user.click(button);
    expect(onSubmitMock).not.toHaveBeenCalled();

    await user.keyboard("{Enter}");
    expect(onSubmitMock).not.toHaveBeenCalled();
  });

  it("should not call onSubmit when the message is only whitespace", async () => {
    const user = userEvent.setup();
    render(<ChatInput onSubmit={onSubmitMock} />);
    const textarea = screen.getByRole("textbox");

    await user.type(textarea, "   ");
    await user.keyboard("{Enter}");

    expect(onSubmitMock).not.toHaveBeenCalled();

    await user.type(textarea, " \t
");
    await user.keyboard("{Enter}");

    expect(onSubmitMock).not.toHaveBeenCalled();
  });

  it("should disable submit", async () => {
    const user = userEvent.setup();
    render(<ChatInput disabled onSubmit={onSubmitMock} />);

    const button = screen.getByRole("button");
    const textarea = screen.getByRole("textbox");

    await user.type(textarea, "Hello, world!");

    expect(button).toBeDisabled();
    await user.click(button);
    expect(onSubmitMock).not.toHaveBeenCalled();

    await user.keyboard("{Enter}");
    expect(onSubmitMock).not.toHaveBeenCalled();
  });

  it("should render a placeholder", () => {
    render(
      <ChatInput placeholder="Enter your message" onSubmit={onSubmitMock} />,
    );

    const textarea = screen.getByPlaceholderText("Enter your message");
    expect(textarea).toBeInTheDocument();
  });

  it("should create a newline instead of submitting when shift + enter is pressed", async () => {
    const user = userEvent.setup();
    render(<ChatInput onSubmit={onSubmitMock} />);
    const textarea = screen.getByRole("textbox");

    await user.type(textarea, "Hello, world!");
    await user.keyboard("{Shift>} {Enter}"); // Shift + Enter

    expect(onSubmitMock).not.toHaveBeenCalled();
    // expect(textarea).toHaveValue("Hello, world!
");
  });

  it("should clear the input message after sending a message", async () => {
    const user = userEvent.setup();
    render(<ChatInput onSubmit={onSubmitMock} />);
    const textarea = screen.getByRole("textbox");
    const button = screen.getByRole("button");

    await user.type(textarea, "Hello, world!");
    await user.keyboard("{Enter}");
    expect(textarea).toHaveValue("");

    await user.type(textarea, "Hello, world!");
    await user.click(button);
    expect(textarea).toHaveValue("");
  });

  it("should hide the submit button", () => {
    render(<ChatInput onSubmit={onSubmitMock} showButton={false} />);
    expect(screen.queryByRole("button")).not.toBeInTheDocument();
  });

  it("should call onChange when the user types", async () => {
    const user = userEvent.setup();
    const onChangeMock = vi.fn();
    render(<ChatInput onSubmit={onSubmitMock} onChange={onChangeMock} />);
    const textarea = screen.getByRole("textbox");

    await user.type(textarea, "Hello, world!");

    expect(onChangeMock).toHaveBeenCalledTimes("Hello, world!".length);
  });

  it("should have set the passed value", () => {
    render(<ChatInput value="Hello, world!" onSubmit={onSubmitMock} />);
    const textarea = screen.getByRole("textbox");

    expect(textarea).toHaveValue("Hello, world!");
  });

  it("should display the stop button and trigger the callback", async () => {
    const user = userEvent.setup();
    const onStopMock = vi.fn();
    render(
      <ChatInput onSubmit={onSubmitMock} button="stop" onStop={onStopMock} />,
    );
    const stopButton = screen.getByTestId("stop-button");

    await user.click(stopButton);
    expect(onStopMock).toHaveBeenCalledOnce();
  });

  it("should call onFocus and onBlur when the textarea is focused and blurred", async () => {
    const user = userEvent.setup();
    const onFocusMock = vi.fn();
    const onBlurMock = vi.fn();
    render(
      <ChatInput
        onSubmit={onSubmitMock}
        onFocus={onFocusMock}
        onBlur={onBlurMock}
      />,
    );
    const textarea = screen.getByRole("textbox");

    await user.click(textarea);
    expect(onFocusMock).toHaveBeenCalledOnce();

    await user.tab();
    expect(onBlurMock).toHaveBeenCalledOnce();
  });

  it("should handle text paste correctly", () => {
    const onSubmit = vi.fn();
    const onChange = vi.fn();

    render(<ChatInput onSubmit={onSubmit} onChange={onChange} />);

    const input = screen.getByTestId("chat-input").querySelector("textarea");
    expect(input).toBeTruthy();

    // Fire paste event with text data
    fireEvent.paste(input!, {
      clipboardData: {
        getData: (type: string) => (type === "text/plain" ? "test paste" : ""),
        files: [],
      },
    });
  });

  it("should handle image paste correctly", () => {
    const onSubmit = vi.fn();
    const onImagePaste = vi.fn();

    render(<ChatInput onSubmit={onSubmit} onImagePaste={onImagePaste} />);

    const input = screen.getByTestId("chat-input").querySelector("textarea");
    expect(input).toBeTruthy();

    // Create a paste event with an image file
    const file = new File(["dummy content"], "image.png", {
      type: "image/png",
    });

    // Fire paste event with image data
    fireEvent.paste(input!, {
      clipboardData: {
        getData: () => "",
        files: [file],
      },
    });

    // Verify image paste was handled
    expect(onImagePaste).toHaveBeenCalledWith([file]);
  });
});