Back to Repositories

Testing User Actions Component Integration in OpenHands

This test suite evaluates the UserActions component’s functionality in the OpenHands frontend, focusing on user interaction with avatar clicks and menu operations. The tests verify menu toggling, account settings navigation, and logout functionality, ensuring proper user flow and component state management.

Test Coverage Overview

The test suite provides comprehensive coverage of the UserActions component’s core functionalities:

  • Component rendering and presence of essential elements
  • User avatar click behavior and menu toggle functionality
  • Account settings navigation and callback handling
  • Logout functionality with proper state management
  • Edge cases for logged-out user states

Implementation Analysis

The testing approach utilizes React Testing Library and Vitest for component testing, emphasizing user interactions and event handling. The implementation leverages userEvent for simulating user actions and mock functions for callback verification, following the component-driven testing pattern.

Key patterns include async/await testing for user interactions, mock function verification, and DOM presence assertions.

Technical Details

  • Testing Framework: Vitest
  • UI Testing Library: React Testing Library
  • User Event Simulation: @testing-library/user-event
  • Mock Functions: vi.fn()
  • Component Rendering: render from @testing-library/react
  • DOM Queries: screen utilities

Best Practices Demonstrated

The test suite exemplifies several testing best practices in React component testing:

  • Isolation of tests with proper cleanup using afterEach
  • User-centric testing approach using data-testid attributes
  • Async testing patterns for user interactions
  • Comprehensive edge case coverage
  • Clear test descriptions and organized test structure

all-hands-ai/openhands

frontend/__tests__/components/user-actions.test.tsx

            
import { render, screen } from "@testing-library/react";
import { describe, expect, it, test, vi, afterEach } from "vitest";
import userEvent from "@testing-library/user-event";
import { UserActions } from "#/components/features/sidebar/user-actions";

describe("UserActions", () => {
  const user = userEvent.setup();
  const onClickAccountSettingsMock = vi.fn();
  const onLogoutMock = vi.fn();

  afterEach(() => {
    onClickAccountSettingsMock.mockClear();
    onLogoutMock.mockClear();
  });

  it("should render", () => {
    render(
      <UserActions
        onClickAccountSettings={onClickAccountSettingsMock}
        onLogout={onLogoutMock}
      />,
    );

    expect(screen.getByTestId("user-actions")).toBeInTheDocument();
    expect(screen.getByTestId("user-avatar")).toBeInTheDocument();
  });

  it("should toggle the user menu when the user avatar is clicked", async () => {
    render(
      <UserActions
        onClickAccountSettings={onClickAccountSettingsMock}
        onLogout={onLogoutMock}
      />,
    );

    const userAvatar = screen.getByTestId("user-avatar");
    await user.click(userAvatar);

    expect(
      screen.getByTestId("account-settings-context-menu"),
    ).toBeInTheDocument();

    await user.click(userAvatar);

    expect(
      screen.queryByTestId("account-settings-context-menu"),
    ).not.toBeInTheDocument();
  });

  it("should call onClickAccountSettings and close the menu when the account settings option is clicked", async () => {
    render(
      <UserActions
        onClickAccountSettings={onClickAccountSettingsMock}
        onLogout={onLogoutMock}
      />,
    );

    const userAvatar = screen.getByTestId("user-avatar");
    await user.click(userAvatar);

    const accountSettingsOption = screen.getByText("Account Settings");
    await user.click(accountSettingsOption);

    expect(onClickAccountSettingsMock).toHaveBeenCalledOnce();
    expect(
      screen.queryByTestId("account-settings-context-menu"),
    ).not.toBeInTheDocument();
  });

  it("should call onLogout and close the menu when the logout option is clicked", async () => {
    render(
      <UserActions
        onClickAccountSettings={onClickAccountSettingsMock}
        onLogout={onLogoutMock}
        user={{ avatar_url: "https://example.com/avatar.png" }}
      />,
    );

    const userAvatar = screen.getByTestId("user-avatar");
    await user.click(userAvatar);

    const logoutOption = screen.getByText("Logout");
    await user.click(logoutOption);

    expect(onLogoutMock).toHaveBeenCalledOnce();
    expect(
      screen.queryByTestId("account-settings-context-menu"),
    ).not.toBeInTheDocument();
  });

  test("onLogout should not be called when the user is not logged in", async () => {
    render(
      <UserActions
        onClickAccountSettings={onClickAccountSettingsMock}
        onLogout={onLogoutMock}
      />,
    );

    const userAvatar = screen.getByTestId("user-avatar");
    await user.click(userAvatar);

    const logoutOption = screen.getByText("Logout");
    await user.click(logoutOption);

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

  // FIXME: Spinner now provided through useQuery
  it.skip("should display the loading spinner", () => {
    render(
      <UserActions
        onClickAccountSettings={onClickAccountSettingsMock}
        onLogout={onLogoutMock}
        user={{ avatar_url: "https://example.com/avatar.png" }}
      />,
    );

    const userAvatar = screen.getByTestId("user-avatar");
    user.click(userAvatar);

    expect(screen.getByTestId("loading-spinner")).toBeInTheDocument();
    expect(screen.queryByAltText("user avatar")).not.toBeInTheDocument();
  });
});