Files
webterm/tests/conftest.py
T
GitHub Copilot 126a4bc712 Fix screenshot affecting terminal state in open sessions
- Add get_screen_snapshot() method that doesn't mutate terminal state
- Use change counter for reliable activity detection instead of dirty flag
- Update screenshot handler to use non-mutating snapshot method
- Refactor tests to use shared fixtures and reduce duplication
- Update copilot-instructions.md with detailed Makefile usage
2026-01-28 20:15:51 +00:00

167 lines
4.0 KiB
Python

"""Pytest configuration and fixtures for webterm tests."""
from __future__ import annotations
import asyncio
from typing import TYPE_CHECKING
from unittest.mock import AsyncMock, MagicMock
import pytest
from webterm.config import App, Config
from webterm.local_server import LocalServer
from webterm.poller import Poller
from webterm.session_manager import SessionManager
if TYPE_CHECKING:
from collections.abc import AsyncGenerator, Generator
from pathlib import Path
@pytest.fixture
def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]:
"""Create an event loop for async tests."""
loop = asyncio.new_event_loop()
yield loop
loop.close()
@pytest.fixture
def sample_terminal_app() -> App:
"""Create a sample terminal app configuration."""
return App(
name="Test Terminal",
slug="test-terminal",
terminal=True,
command="echo hello",
)
@pytest.fixture
def sample_config(sample_terminal_app: App) -> Config:
"""Create a sample configuration with a terminal app."""
return Config(apps=[sample_terminal_app])
@pytest.fixture
def tmp_config_path(tmp_path: Path) -> Path:
"""Create a temporary config path."""
return tmp_path / "config"
@pytest.fixture
def mock_request() -> MagicMock:
"""Create a mock request with common attributes."""
request = MagicMock()
request.headers = {}
request.secure = False
request.query = {}
return request
@pytest.fixture
def screen_buffer_factory():
def _make(rows: list[str], width: int = 80):
return [
[
{
"data": c,
"fg": "default",
"bg": "default",
"bold": False,
"italics": False,
"underscore": False,
"reverse": False,
}
for c in (row + " " * width)[:width]
]
for row in rows
]
return _make
@pytest.fixture
def mock_session():
session = MagicMock()
session.get_screen_has_changes = AsyncMock(return_value=False)
session.get_screen_state = AsyncMock(return_value=(80, 24, [], True))
session.get_screen_snapshot = AsyncMock(return_value=(80, 24, [], True))
return session
@pytest.fixture
def poller() -> Poller:
"""Create a Poller instance."""
return Poller()
@pytest.fixture
def mock_poller() -> MagicMock:
"""Create a mock Poller for unit tests."""
return MagicMock()
class DummyAsyncLock:
"""A dummy async context manager for replacing locks in tests."""
async def __aenter__(self):
return None
async def __aexit__(self, exc_type, exc, tb):
return False
@pytest.fixture
def dummy_lock() -> DummyAsyncLock:
"""Create a dummy async lock for tests."""
return DummyAsyncLock()
@pytest.fixture
def mock_screen_char():
"""Factory for creating mock pyte screen characters."""
def _make(
data: str = " ",
fg: int = 0,
bg: int = 0,
bold: bool = False,
italics: bool = False,
underscore: bool = False,
reverse: bool = False,
) -> MagicMock:
char = MagicMock()
char.data = data
char.fg = fg
char.bg = bg
char.bold = bold
char.italics = italics
char.underscore = underscore
char.reverse = reverse
return char
return _make
@pytest.fixture
def session_manager(poller: Poller, tmp_path: Path, sample_terminal_app: App) -> SessionManager:
"""Create a SessionManager instance."""
return SessionManager(poller, tmp_path, [sample_terminal_app])
@pytest.fixture
async def local_server(
tmp_config_path: Path, sample_config: Config
) -> AsyncGenerator[LocalServer, None]:
"""Create a LocalServer instance for testing."""
server = LocalServer(
str(tmp_config_path),
sample_config,
host="127.0.0.1",
port=0, # Use random available port
)
yield server
# Cleanup
server.force_exit()