Skip to content

Testing

Metaseed uses pytest for testing with a multi-layered approach covering unit tests, integration tests, and end-to-end UI tests.

Running Tests

# Run all tests
uv run pytest

# Run tests with coverage
uv run pytest --cov=metaseed

# Run only UI tests
uv run pytest tests/test_ui/ -v

# Run Selenium tests (visible browser)
uv run pytest tests/test_ui/test_selenium.py -v

Test Structure

tests/
├── test_api/           # REST API endpoint tests
├── test_cli/           # CLI command tests
├── test_facade.py      # ProfileFacade tests
├── test_models/        # Pydantic model factory tests
├── test_specs/         # YAML spec loading tests
├── test_storage/       # JSON/YAML storage tests
├── test_validators/    # Validation rule tests
├── test_ui/            # UI tests
│   ├── test_htmx.py    # HTMX route tests (FastAPI TestClient)
│   └── test_selenium.py # Selenium E2E tests (visible browser)
└── test_version.py

Unit Tests

Unit tests cover individual components in isolation:

  • Models: Test Pydantic model generation from specs
  • Validators: Test validation rules (required fields, date ranges, entity references)
  • Storage: Test JSON/YAML serialization and loading
  • Specs: Test YAML spec parsing and entity loading

Integration Tests

Integration tests verify component interactions:

  • CLI: Test command execution with real file I/O
  • API: Test REST endpoints with TestClient
  • Facade: Test entity creation through the facade pattern

UI Tests

The UI is built with HTMX + FastAPI + Jinja2 templates. Two test approaches are used:

HTMX Route Tests (TestClient)

Fast, headless tests using FastAPI's TestClient to verify route behavior and HTML responses.

from fastapi.testclient import TestClient
from metaseed.ui.routes import AppState, create_app

def test_create_entity(client):
    response = client.post(
        "/entity",
        data={
            "_entity_type": "Investigation",
            "unique_id": "INV-001",
            "title": "Test Investigation",
        },
    )
    assert response.status_code == 200
    assert "Created Investigation" in response.text

Selenium E2E Tests (Visible Browser)

End-to-end tests with a visible Chrome browser for demonstrating and verifying UI interactions.

Prerequisites

  • Chrome browser installed
  • ChromeDriver (managed by selenium)

Running Selenium Tests

# Run all Selenium tests
uv run pytest tests/test_ui/test_selenium.py -v

# Stop on first failure
uv run pytest tests/test_ui/test_selenium.py -v -x --tb=short

Test Configuration

Tests use a module-scoped server fixture that starts uvicorn on port 8081:

@pytest.fixture(scope="module")
def server():
    """Start the Metaseed server for testing."""
    proc = subprocess.Popen(
        ["uv", "run", "uvicorn", "metaseed.ui.routes:app", "--port", "8081"],
        ...
    )
    # Wait for server to be ready
    yield proc
    proc.terminate()

@pytest.fixture
def browser(_server):
    """Create a visible Chrome browser for testing."""
    options = Options()
    options.add_argument("--window-size=1280,900")
    driver = webdriver.Chrome(options=options)
    yield driver
    driver.quit()

Test ID Convention

All interactive UI elements have data-testid attributes for reliable selection:

Element Pattern Example
Create entity button btn-create-{EntityType} btn-create-Investigation
Tree node tree-node-{node_id} tree-node-abc123
Delete node button btn-delete-{node_id} btn-delete-abc123
Entity form form-entity form-entity
Input field input-{field-name} input-unique-id
Create button btn-create btn-create
Update button btn-update btn-update
Optional fields toggle section-optional-toggle section-optional-toggle
Nested field button btn-nested-{field-name} btn-nested-contacts
Table add row table-add-row table-add-row
Table save table-save table-save
Table count table-count table-count
Table cell cell-{row}-{column} cell-0-name
Table row (clickable) row-{idx} row-0
Breadcrumb breadcrumb breadcrumb
Back button btn-back btn-back
Notification notification notification

Helper Functions

The test file provides helper functions for common operations:

def fill_field(driver, testid: str, value: str, trigger_change: bool = False):
    """Fill a form field by data-testid."""

def click_button(driver, testid: str):
    """Click a button by data-testid and wait."""

def element_exists(driver, testid: str) -> bool:
    """Check if an element with given data-testid exists."""

def expand_optional_fields(driver):
    """Expand the optional fields section if collapsed."""

Example Test

@pytest.mark.ui
class TestCreateInvestigation:
    def test_create_investigation(self, browser):
        browser.get("http://127.0.0.1:8081")

        # Click create button
        click_button(browser, "btn-create-Investigation")

        # Fill required fields
        fill_field(browser, "input-unique-id", "INV-001")
        fill_field(browser, "input-title", "Test Investigation")

        # Submit
        click_button(browser, "btn-create")

        # Verify entity appears in sidebar
        sidebar = browser.find_element(By.ID, "sidebar")
        assert "Test Investigation" in sidebar.text

Date Input Handling

For date inputs, JavaScript is used to set values directly (avoids locale-specific formatting issues):

if element.get_attribute("type") == "date":
    driver.execute_script("arguments[0].value = arguments[1]", element, "2024-03-15")

Triggering HTMX Events

When filling table cells that have HTMX handlers, trigger the change event:

fill_field(browser, "cell-0-name", "Dr. Smith", trigger_change=True)

Nested Entity Editing

The UI supports deep nesting of entities. When viewing a table of nested entities (e.g., Studies within an Investigation), users can click on a table row to edit that nested entity in a full form view.

Behavior

  1. Click row to edit: Clicking a table row opens a form for editing that nested entity
  2. Breadcrumb navigation: A breadcrumb trail shows the navigation path (e.g., "Investigation > studies > Study[0]")
  3. Deep nesting: Nested entities can contain their own nested entities (e.g., Study > biological_materials > BiologicalMaterial)
  4. Save & Back: Saves changes and returns to the parent table view
  5. Cancel: Returns to parent without saving

Test IDs for Nested Editing

Element Pattern Example
Table row (clickable) row-{idx} row-0
Breadcrumb breadcrumb breadcrumb
Back button btn-back btn-back

Example Flow

Investigation (root form)
  └─ Click "studies" button → Table view
      └─ Click row 0 → Study form
          └─ Click "biological_materials" button → Table view
              └─ Click row 0 → BiologicalMaterial form

Selenium Test Coverage

The following end-to-end tests are implemented:

Test Class Test Method Description
TestCreateInvestigationRequiredFields test_create_investigation_required_fields Create Investigation with only required fields
TestCreateInvestigationAllFields test_create_investigation_all_fields Create Investigation with all optional fields
TestAddNestedContacts test_add_nested_contacts Add contacts through nested table
TestEditInvestigation test_edit_investigation Edit existing entity and verify persistence
TestDeleteInvestigation test_delete_investigation Delete entity and verify removal
TestAddNestedStudy test_add_nested_study Add Study through nested table
TestValidationError test_validation_error_missing_required Verify HTML5 validation on required fields
TestProfileSwitch test_switch_profile_clears_state Switch profile and verify state cleared
TestNestedEntityEditing test_edit_nested_study Click Study row to edit, verify form shows
TestNestedEntityEditing test_deep_nesting_navigation Navigate Investigation > Study > BiologicalMaterial

Markers

Tests are marked for selective execution:

  • @pytest.mark.ui - UI tests (Selenium)

Run specific markers:

uv run pytest -m ui           # Only UI tests
uv run pytest -m "not ui"     # Exclude UI tests