Back to Repositories

Testing Terminal Component Integration in OpenHands

This test suite validates the Terminal component functionality in OpenHands, focusing on command input/output handling and terminal rendering. The tests verify terminal initialization, command processing, and proper display of terminal prompts using Jest and Testing Library.

Test Coverage Overview

The test suite provides comprehensive coverage of terminal functionality:

  • Terminal rendering and initialization
  • Command loading and execution
  • Input/output handling
  • Custom prompt symbol display
  • Terminal cleanup on unmount
Edge cases include custom symbol handling and multiple command processing. Integration points cover Redux store interactions and XTerm.js terminal implementation.

Implementation Analysis

The testing approach utilizes Jest and React Testing Library with mock implementations of XTerm.js terminal. The tests employ Redux store manipulation through act() wrapper and custom renderWithProviders utility.

Key patterns include:
  • Mock terminal implementation with vi.fn()
  • Redux state preloading
  • Async command dispatch testing
  • Component lifecycle validation

Technical Details

Testing tools and configuration:

  • Jest/Vitest as test runner
  • @testing-library/react for component testing
  • Redux store integration
  • XTerm.js terminal mocking
  • Custom test utilities for provider wrapping
  • ResizeObserver mock implementation

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices through isolation of component behavior and comprehensive coverage.

Notable practices include:
  • Proper cleanup with afterEach hooks
  • Isolated test cases with clear assertions
  • Thorough mock implementation
  • State management testing
  • Component lifecycle validation

all-hands-ai/openhands

frontend/__tests__/components/terminal/terminal.test.tsx

            
import { act, screen } from "@testing-library/react";
import { renderWithProviders } from "test-utils";
import { vi, describe, afterEach, it, expect } from "vitest";
import { Command, appendInput, appendOutput } from "#/state/command-slice";
import Terminal from "#/components/features/terminal/terminal";

const renderTerminal = (commands: Command[] = []) =>
  renderWithProviders(<Terminal secrets={[]} />, {
    preloadedState: {
      cmd: {
        commands,
      },
    },
  });

describe.skip("Terminal", () => {
  global.ResizeObserver = vi.fn().mockImplementation(() => ({
    observe: vi.fn(),
    disconnect: vi.fn(),
  }));

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

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

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

  it("should render a terminal", () => {
    renderTerminal();

    expect(screen.getByText("Terminal")).toBeInTheDocument();
    expect(mockTerminal.open).toHaveBeenCalledTimes(1);

    expect(mockTerminal.write).toHaveBeenCalledWith("$ ");
  });

  it("should load commands to the terminal", () => {
    renderTerminal([
      { type: "input", content: "INPUT" },
      { type: "output", content: "OUTPUT" },
    ]);

    expect(mockTerminal.writeln).toHaveBeenNthCalledWith(1, "INPUT");
    expect(mockTerminal.writeln).toHaveBeenNthCalledWith(2, "OUTPUT");
  });

  it("should write commands to the terminal", () => {
    const { store } = renderTerminal();

    act(() => {
      store.dispatch(appendInput("echo Hello"));
      store.dispatch(appendOutput("Hello"));
    });

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

    act(() => {
      store.dispatch(appendInput("echo World"));
    });

    expect(mockTerminal.writeln).toHaveBeenNthCalledWith(3, "echo World");
  });

  it("should load and write commands to the terminal", () => {
    const { store } = renderTerminal([
      { type: "input", content: "echo Hello" },
      { type: "output", content: "Hello" },
    ]);

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

    act(() => {
      store.dispatch(appendInput("echo Hello"));
    });

    expect(mockTerminal.writeln).toHaveBeenNthCalledWith(3, "echo Hello");
  });

  it("should end the line with a dollar sign after writing a command", () => {
    const { store } = renderTerminal();

    act(() => {
      store.dispatch(appendInput("echo Hello"));
    });

    expect(mockTerminal.writeln).toHaveBeenCalledWith("echo Hello");
    expect(mockTerminal.write).toHaveBeenCalledWith("$ ");
  });

  it("should display a custom symbol if output contains a custom symbol", () => {
    renderTerminal([
      { type: "input", content: "echo Hello" },
      {
        type: "output",
        content:
          "Hello\r
\r
[Python Interpreter: /openhands/poetry/openhands-5O4_aCHf-py3.12/bin/python]
openhands@659478cb008c:/workspace $ ",
      },
    ]);

    expect(mockTerminal.writeln).toHaveBeenNthCalledWith(1, "echo Hello");
    expect(mockTerminal.writeln).toHaveBeenNthCalledWith(2, "Hello");
    expect(mockTerminal.write).toHaveBeenCalledWith(
      "
openhands@659478cb008c:/workspace $ ",
    );
  });

  // This test fails because it expects `disposeMock` to have been called before the component is unmounted.
  it.skip("should dispose the terminal on unmount", () => {
    const { unmount } = renderWithProviders(<Terminal secrets={[]} />);

    expect(mockTerminal.dispose).not.toHaveBeenCalled();

    unmount();

    expect(mockTerminal.dispose).toHaveBeenCalledTimes(1);
  });
});