Back to Repositories

Testing React BaseModal Component Functionality in OpenHands

This test suite validates the functionality of a BaseModal component in a React application, covering modal visibility, actions, and interaction behaviors. The tests ensure proper rendering, state management, and user interactions with the modal dialog.

Test Coverage Overview

The test suite provides comprehensive coverage of the BaseModal component’s core functionality.

  • Modal visibility and state management
  • Optional subtitle rendering
  • Action button functionality and callbacks
  • Modal dismissal behaviors
  • Child component rendering
  • Conditional button disabling

Implementation Analysis

The testing approach utilizes React Testing Library and Vitest for component testing. It employs act() for handling state updates and userEvent for simulating user interactions. The tests follow component isolation principles while verifying both rendered output and behavior.

Technical Details

  • Testing Framework: Vitest
  • UI Testing: @testing-library/react
  • User Event Simulation: @testing-library/user-event
  • Mocking: vi.fn() for callback functions
  • Component: React BaseModal implementation

Best Practices Demonstrated

The test suite exemplifies modern React testing best practices with clear test organization and comprehensive coverage.

  • Isolated component testing
  • Async action handling
  • Mock function implementation
  • State change verification
  • Accessibility considerations

all-hands-ai/openhands

frontend/__tests__/components/modals/base-modal/base-modal.test.tsx

            
import { render, screen, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, vi, expect } from "vitest";
import { BaseModal } from "#/components/shared/modals/base-modal/base-modal";

describe("BaseModal", () => {
  it("should render if the modal is open", () => {
    const { rerender } = render(
      <BaseModal isOpen={false} onOpenChange={vi.fn} title="Settings" />,
    );
    expect(screen.queryByText("Settings")).not.toBeInTheDocument();

    rerender(<BaseModal title="Settings" onOpenChange={vi.fn} isOpen />);
    expect(screen.getByText("Settings")).toBeInTheDocument();
  });

  it("should render an optional subtitle", () => {
    render(
      <BaseModal
        isOpen
        onOpenChange={vi.fn}
        title="Settings"
        subtitle="Subtitle"
      />,
    );
    expect(screen.getByText("Subtitle")).toBeInTheDocument();
  });

  it("should render actions", async () => {
    const onPrimaryClickMock = vi.fn();
    const onSecondaryClickMock = vi.fn();

    const primaryAction = {
      action: onPrimaryClickMock,
      label: "Save",
    };

    const secondaryAction = {
      action: onSecondaryClickMock,
      label: "Cancel",
    };

    render(
      <BaseModal
        isOpen
        onOpenChange={vi.fn}
        title="Settings"
        actions={[primaryAction, secondaryAction]}
      />,
    );

    expect(screen.getByText("Save")).toBeInTheDocument();
    expect(screen.getByText("Cancel")).toBeInTheDocument();

    await act(async () => {
      await userEvent.click(screen.getByText("Save"));
    });
    expect(onPrimaryClickMock).toHaveBeenCalledTimes(1);

    await act(async () => {
      await userEvent.click(screen.getByText("Cancel"));
    });
    expect(onSecondaryClickMock).toHaveBeenCalledTimes(1);
  });

  it("should close the modal after an action is performed", async () => {
    const onOpenChangeMock = vi.fn();
    render(
      <BaseModal
        isOpen
        onOpenChange={onOpenChangeMock}
        title="Settings"
        actions={[
          {
            label: "Save",
            action: () => {},
            closeAfterAction: true,
          },
        ]}
      />,
    );

    await act(async () => {
      await userEvent.click(screen.getByText("Save"));
    });
    expect(onOpenChangeMock).toHaveBeenCalledTimes(1);
  });

  it("should render children", () => {
    render(
      <BaseModal isOpen onOpenChange={vi.fn} title="Settings">
        <div>Children</div>
      </BaseModal>,
    );
    expect(screen.getByText("Children")).toBeInTheDocument();
  });

  it("should disable the action given the condition", () => {
    const { rerender } = render(
      <BaseModal
        isOpen
        onOpenChange={vi.fn}
        title="Settings"
        actions={[
          {
            label: "Save",
            action: () => {},
            isDisabled: true,
          },
        ]}
      />,
    );

    expect(screen.getByText("Save")).toBeDisabled();

    rerender(
      <BaseModal
        isOpen
        onOpenChange={vi.fn}
        title="Settings"
        actions={[
          {
            label: "Save",
            action: () => {},
            isDisabled: false,
          },
        ]}
      />,
    );

    expect(screen.getByText("Save")).not.toBeDisabled();
  });

  it.skip("should not close if the backdrop or escape key is pressed", () => {
    const onOpenChangeMock = vi.fn();
    render(
      <BaseModal
        isOpen
        onOpenChange={onOpenChangeMock}
        title="Settings"
        isDismissable={false}
      />,
    );

    act(() => {
      userEvent.keyboard("{esc}");
    });
    // fails because the nextui component wraps the modal content in an aria-hidden div
    expect(screen.getByRole("dialog")).toBeVisible();
  });
});