Files
guide/tests/conftest.py
2025-11-22 10:51:25 +00:00

153 lines
4.3 KiB
Python

"""Pytest configuration and shared fixtures for all tests."""
from __future__ import annotations
import pytest
from unittest.mock import AsyncMock, MagicMock
from typing import TYPE_CHECKING
@pytest.fixture
def mock_browser_hosts() -> dict[str, object]:
"""Provide mock browser host configurations for testing."""
from guide.app.core.config import BrowserHostConfig, HostKind
return {
"demo-headless": BrowserHostConfig(
id="demo-headless",
kind=HostKind.HEADLESS,
browser="chromium",
),
"demo-cdp": BrowserHostConfig(
id="demo-cdp",
kind=HostKind.CDP,
host="localhost",
port=9222,
),
}
@pytest.fixture
def mock_personas() -> dict[str, object]:
"""Provide mock persona configurations for testing."""
from guide.app.models.personas.models import (
DemoPersona,
PersonaRole,
LoginMethod,
)
return {
"buyer1": DemoPersona(
id="buyer1",
role=PersonaRole.BUYER,
email="buyer1@example.com",
login_method=LoginMethod.MFA_EMAIL,
browser_host_id="demo-headless",
),
"supplier1": DemoPersona(
id="supplier1",
role=PersonaRole.SUPPLIER,
email="supplier1@example.com",
login_method=LoginMethod.MFA_EMAIL,
browser_host_id="demo-cdp",
),
}
@pytest.fixture
def app_settings(
mock_browser_hosts: dict[str, object],
mock_personas: dict[str, object],
) -> object:
"""Provide application settings with mock configuration.
Note: Fixtures are typed as object to avoid circular import issues.
Runtime casting is necessary because dict invariance prevents direct assignment.
"""
from typing import cast as type_cast
from guide.app.core.config import AppSettings
return AppSettings(
raindrop_base_url="https://app.raindrop.com",
raindrop_graphql_url="https://app.raindrop.com/graphql",
default_browser_host_id="demo-headless",
browser_hosts=type_cast(dict[str, object], mock_browser_hosts), # type: ignore[arg-type]
personas=type_cast(dict[str, object], mock_personas), # type: ignore[arg-type]
)
@pytest.fixture
def persona_store(app_settings: object) -> object:
"""Provide PersonaStore instance with mock settings.
Note: app_settings is AppSettings but typed as object to avoid circular imports.
"""
from guide.app.models.personas.store import PersonaStore
# app_settings is AppSettings at runtime but typed as object for circular import avoidance
return PersonaStore(app_settings) # type: ignore[arg-type]
@pytest.fixture
def action_registry(persona_store: object) -> object:
"""Provide ActionRegistry instance with DI context."""
from guide.app.actions.registry import ActionRegistry
registry = ActionRegistry(
di_context={
"persona_store": persona_store,
"login_url": "https://app.raindrop.com",
}
)
return registry
@pytest.fixture
def action_context(_persona_store: object) -> object:
"""Provide ActionContext instance for testing action execution.
Args:
_persona_store: Unused, but required for fixture dependency order.
"""
from guide.app.models.domain.models import ActionContext
return ActionContext(
action_id="test-action",
persona_id="buyer1",
browser_host_id="demo-headless",
correlation_id="test-correlation-123",
shared_state={},
)
@pytest.fixture
def mock_page() -> AsyncMock:
"""Provide mock Playwright page instance."""
page = AsyncMock()
page.goto = AsyncMock()
page.wait_for_selector = AsyncMock()
page.fill = AsyncMock()
page.click = AsyncMock()
page.screenshot = AsyncMock(return_value=b"fake-image-data")
page.content = AsyncMock(return_value="<html><body>Mock</body></html>")
page.on = MagicMock()
return page
@pytest.fixture
def mock_browser_client() -> AsyncMock:
"""Provide mock BrowserClient instance."""
client = AsyncMock()
client.open_page = AsyncMock()
return client
@pytest.fixture
def mock_browser_pool() -> AsyncMock:
"""Provide mock BrowserPool instance."""
pool = AsyncMock()
pool.initialize = AsyncMock()
pool.close = AsyncMock()
return pool