Back to Repositories

Testing Terminal Spinner Animations in Textualize/rich

This test suite validates the Spinner component functionality in the Rich library, focusing on creation, rendering, and state management of spinner animations. The tests ensure proper spinner initialization, text updates, and measurement calculations for terminal display.

Test Coverage Overview

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

  • Spinner creation with valid and invalid spinner types
  • Rendering spinner frames with text
  • Dynamic text updates during animation
  • Measurement calculations for terminal display
  • Markup text handling in spinner content

Implementation Analysis

The testing approach utilizes pytest fixtures and mocked time functions to control spinner animation states. The implementation demonstrates proper isolation of time-dependent behavior and console output capture for verification.

  • Custom time function injection for deterministic testing
  • Console capture mechanisms for output validation
  • Assertion-based verification of rendered content

Technical Details

  • Testing Framework: pytest
  • Key Components: Console, Measurement, Rule, Spinner, Text
  • Mock Objects: Custom time function
  • Test Environment: Controlled terminal simulation
  • Output Validation: String comparison and measurement assertions

Best Practices Demonstrated

The test suite exemplifies several testing best practices for terminal-based applications.

  • Isolation of time-dependent behavior
  • Comprehensive error case handling
  • Controlled environment setup
  • Clear test case organization
  • Explicit expected vs actual output comparison

textualize/rich

tests/test_spinner.py

            
import pytest

from rich.console import Console
from rich.measure import Measurement
from rich.rule import Rule
from rich.spinner import Spinner
from rich.text import Text


def test_spinner_create():
    Spinner("dots")
    with pytest.raises(KeyError):
        Spinner("foobar")


def test_spinner_render():
    time = 0.0

    def get_time():
        nonlocal time
        return time

    console = Console(
        width=80, color_system=None, force_terminal=True, get_time=get_time
    )
    console.begin_capture()
    spinner = Spinner("dots", "Foo")
    console.print(spinner)
    time += 80 / 1000
    console.print(spinner)
    result = console.end_capture()
    print(repr(result))
    expected = "⠋ Foo
⠙ Foo
"
    assert result == expected


def test_spinner_update():
    time = 0.0

    def get_time():
        nonlocal time
        return time

    console = Console(width=20, force_terminal=True, get_time=get_time, _environ={})
    console.begin_capture()
    spinner = Spinner("dots")
    console.print(spinner)

    rule = Rule("Bar")

    spinner.update(text=rule)
    time += 80 / 1000
    console.print(spinner)

    result = console.end_capture()
    print(repr(result))
    expected = "⠋
⠙ \x1b[92m─\x1b[0m
"
    assert result == expected


def test_rich_measure():
    console = Console(width=80, color_system=None, force_terminal=True)
    spinner = Spinner("dots", "Foo")
    min_width, max_width = Measurement.get(console, console.options, spinner)
    assert min_width == 3
    assert max_width == 5


def test_spinner_markup():
    spinner = Spinner("dots", "[bold]spinning[/bold]")
    assert isinstance(spinner.text, Text)
    assert str(spinner.text) == "spinning"