from __future__ import annotations import os import socket import subprocess import sys import time from collections.abc import Iterator from pathlib import Path import pytest PROJECT_ROOT = Path(__file__).resolve().parents[2] def _find_free_port() -> int: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.bind(("127.0.0.1", 0)) return int(sock.getsockname()[1]) def _wait_for_server_ready(base_url: str, timeout_seconds: float = 20.0) -> None: import urllib.error import urllib.request health_url = f"{base_url}/health" deadline = time.monotonic() + timeout_seconds while time.monotonic() < deadline: try: with urllib.request.urlopen(health_url, timeout=1.0) as response: if response.status == 200: return except (urllib.error.URLError, TimeoutError): time.sleep(0.2) raise RuntimeError(f"Timed out waiting for test server at {health_url}") @pytest.fixture(scope="session") def e2e_environment(tmp_path_factory: pytest.TempPathFactory) -> Iterator[dict[str, str]]: workspace = tmp_path_factory.mktemp("hw-e2e") db_path = workspace / "happywedding-e2e.db" port = _find_free_port() base_url = f"http://127.0.0.1:{port}" env = os.environ.copy() env["DATABASE_URL"] = f"sqlite:///{db_path}" env["FLASK_APP"] = "run.py" env["PYTHONUNBUFFERED"] = "1" for command in ( [sys.executable, "-m", "flask", "db", "upgrade"], [sys.executable, "-m", "flask", "seed-all"], [sys.executable, "-m", "flask", "seed-demo"], ): subprocess.run(command, cwd=PROJECT_ROOT, env=env, check=True) server_process = subprocess.Popen( [ sys.executable, "-m", "flask", "run", "--host", "127.0.0.1", "--port", str(port), "--no-debugger", "--no-reload", ], cwd=PROJECT_ROOT, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) try: _wait_for_server_ready(base_url) yield { "base_url": base_url, "db_path": os.fspath(db_path), } finally: server_process.terminate() try: server_process.wait(timeout=10) except subprocess.TimeoutExpired: server_process.kill() server_process.wait(timeout=5) @pytest.fixture(scope="session") def base_url(e2e_environment: dict[str, str]) -> str: return e2e_environment["base_url"] @pytest.fixture(scope="function") def browser_context_args(browser_context_args: dict[str, object]) -> dict[str, object]: return { **browser_context_args, "locale": "zh-CN", "timezone_id": "Asia/Shanghai", "viewport": {"width": 1440, "height": 1080}, } @pytest.fixture(scope="function") def admin_credentials() -> dict[str, str]: return {"username": "admin", "password": "ChangeMe123!"} @pytest.fixture(scope="function") def entry_credentials() -> dict[str, str]: return {"username": "entry-demo", "password": "EntryDemo123!"} @pytest.fixture(scope="function") def quick_editor_credentials() -> dict[str, str]: return {"username": "quick-editor-demo", "password": "QuickEditor123!"}