"""Pytest configuration for Playwright tests. This conftest creates module-scoped fixtures that start the FastAPI server before running Playwright tests and stop it after all tests complete. """ from __future__ import annotations import logging import os import socket import sys import threading import time from collections.abc import Generator from pathlib import Path import pytest import uvicorn # Suppress NiceGUI banner noise logging.getLogger("nicegui").setLevel(logging.WARNING) def find_free_port() -> int: """Find a free port on localhost.""" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(("", 0)) s.listen(1) port = s.getsockname()[1] return port class ServerManager: """Manages a NiceGUI/FastAPI server for testing.""" _instance: "ServerManager | None" = None _lock = threading.Lock() def __init__(self, port: int) -> None: self.port = port self.url = f"http://localhost:{port}" self._thread: threading.Thread | None = None self._server: uvicorn.Server | None = None def start(self) -> None: """Start the server in a background thread.""" self._thread = threading.Thread(target=self._run_server, daemon=True) self._thread.start() # Wait for server to be ready if not self._wait_for_connection(timeout=30): raise RuntimeError("Server did not start within 30 seconds") def _run_server(self) -> None: """Run the FastAPI server with uvicorn.""" # Ensure project root is on sys.path project_root = str(Path(__file__).parent.parent) if project_root not in sys.path: sys.path.insert(0, project_root) os.environ["APP_ENV"] = "test" os.environ["NICEGUI_STORAGE_SECRET"] = "test-secret-key" # Import after environment is set from app.main import app # Configure uvicorn config = uvicorn.Config( app, host="127.0.0.1", port=self.port, log_level="warning", access_log=False, ) self._server = uvicorn.Server(config) self._server.run() def _wait_for_connection(self, timeout: float = 10.0) -> bool: """Wait for server to accept connections.""" deadline = time.time() + timeout while time.time() < deadline: try: with socket.create_connection(("127.0.0.1", self.port), timeout=1): return True except OSError: time.sleep(0.1) return False def stop(self) -> None: """Stop the server.""" if self._server: self._server.should_exit = True if self._thread and self._thread.is_alive(): self._thread.join(timeout=5) @classmethod def get_or_create(cls, port: int) -> "ServerManager": """Get existing instance or create new one.""" with cls._lock: if cls._instance is None: cls._instance = cls(port) return cls._instance @pytest.fixture(scope="module") def server_url() -> Generator[str, None, None]: """Start the server once per module and yield its URL.""" port = find_free_port() server = ServerManager(port) # Start server server.start() yield server.url # Cleanup server.stop() ServerManager._instance = None @pytest.fixture(scope="module") def base_url(server_url: str) -> str: """Alias for server_url for naming consistency with Playwright conventions.""" return server_url