Back to Repositories

Testing Terminal Hook Implementation in OpenHands

This test suite validates the useTerminal React hook implementation, focusing on terminal rendering, command handling, and secret management functionality. The tests ensure proper terminal initialization, command display, and secure handling of sensitive information.

Test Coverage Overview

The test suite provides comprehensive coverage of the useTerminal hook functionality:

  • Basic terminal rendering and initialization
  • Command rendering with input/output verification
  • Secret masking and sensitive data handling
  • Terminal mock implementation and cleanup

Implementation Analysis

The testing approach utilizes Jest and React Testing Library to validate the terminal hook behavior. It implements mock patterns for XTerm terminal functionality and ResizeObserver, ensuring isolated testing of the component’s logic.

Key patterns include vi.hoisted for mock definitions and component wrapper implementation for consistent rendering context.

Technical Details

Testing stack includes:

  • Vitest for test running and mocking
  • React Testing Library for component rendering
  • XTerm terminal mocking
  • Custom TestTerminalComponent for isolation
  • ResizeObserver polyfill implementation

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Proper test isolation through mocking
  • Comprehensive edge case handling
  • Clear test case organization
  • Effective mock cleanup between tests
  • Secure testing of sensitive data handling

all-hands-ai/openhands

frontend/__tests__/hooks/use-terminal.test.tsx

            
import { beforeAll, describe, expect, it, vi } from "vitest";
import { render } from "@testing-library/react";
import { afterEach } from "node:test";
import { ReactNode } from "react";
import { useTerminal } from "#/hooks/use-terminal";
import { Command } from "#/state/command-slice";

interface TestTerminalComponentProps {
  commands: Command[];
  secrets: string[];
}

function TestTerminalComponent({
  commands,
  secrets,
}: TestTerminalComponentProps) {
  const ref = useTerminal({ commands, secrets, disabled: false });
  return <div ref={ref} />;
}

interface WrapperProps {
  children: ReactNode;
}

function Wrapper({ children }: WrapperProps) {
  return <div>{children}</div>;
}

describe("useTerminal", () => {
  const mockTerminal = vi.hoisted(() => ({
    loadAddon: vi.fn(),
    open: vi.fn(),
    write: vi.fn(),
    writeln: vi.fn(),
    onKey: vi.fn(),
    attachCustomKeyEventHandler: vi.fn(),
    dispose: vi.fn(),
  }));

  beforeAll(() => {
    // mock ResizeObserver
    window.ResizeObserver = vi.fn().mockImplementation(() => ({
      observe: vi.fn(),
      unobserve: vi.fn(),
      disconnect: vi.fn(),
    }));

    // mock Terminal
    vi.mock("@xterm/xterm", async (importOriginal) => ({
      ...(await importOriginal<typeof import("@xterm/xterm")>()),
      Terminal: vi.fn().mockImplementation(() => mockTerminal),
    }));
  });

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

  it("should render", () => {
    render(<TestTerminalComponent commands={[]} secrets={[]} />, {
      wrapper: Wrapper,
    });
  });

  it("should render the commands in the terminal", () => {
    const commands: Command[] = [
      { content: "echo hello", type: "input" },
      { content: "hello", type: "output" },
    ];

    render(<TestTerminalComponent commands={commands} secrets={[]} />, {
      wrapper: Wrapper,
    });

    expect(mockTerminal.writeln).toHaveBeenNthCalledWith(1, "echo hello");
    expect(mockTerminal.writeln).toHaveBeenNthCalledWith(2, "hello");
  });

  it("should hide secrets in the terminal", () => {
    const secret = "super_secret_github_token";
    const anotherSecret = "super_secret_another_token";
    const commands: Command[] = [
      {
        content: `export GITHUB_TOKEN=${secret},${anotherSecret},${secret}`,
        type: "input",
      },
      { content: secret, type: "output" },
    ];

    render(
      <TestTerminalComponent
        commands={commands}
        secrets={[secret, anotherSecret]}
      />,
      {
        wrapper: Wrapper,
      },
    );

    // BUG: `vi.clearAllMocks()` does not clear the number of calls
    // therefore, we need to assume the order of the calls based
    // on the test order
    expect(mockTerminal.writeln).toHaveBeenNthCalledWith(
      3,
      `export GITHUB_TOKEN=${"*".repeat(10)},${"*".repeat(10)},${"*".repeat(10)}`,
    );
    expect(mockTerminal.writeln).toHaveBeenNthCalledWith(4, "*".repeat(10));
  });
});