""" Playwright E2E test fixtures. Starts the FastAPI app on a random port with test SQLite, no Oracle. Includes console error collector and screenshot capture. """ import os import sys import tempfile import pytest import subprocess import time import socket from pathlib import Path # --- Screenshots directory --- QA_REPORTS_DIR = Path(__file__).parents[3] / "qa-reports" SCREENSHOTS_DIR = QA_REPORTS_DIR / "screenshots" def _free_port(): with socket.socket() as s: s.bind(('', 0)) return s.getsockname()[1] def _app_is_running(url): """Check if app is already running at the given URL.""" try: import urllib.request urllib.request.urlopen(f"{url}/health", timeout=2) return True except Exception: return False @pytest.fixture(scope="session") def app_url(request): """Use a running app if available (e.g. started by test.sh), otherwise start a subprocess. When --base-url is provided or app is already running on :5003, use the live app. This allows E2E tests to run against the real Oracle-backed app in ./test.sh full. """ # Check if --base-url was provided via pytest-playwright base_url = request.config.getoption("--base-url", default=None) # Try live app on :5003 first live_url = base_url or "http://localhost:5003" if _app_is_running(live_url): yield live_url return # No live app — start subprocess with dummy Oracle (structure-only tests) port = _free_port() tmpdir = tempfile.mkdtemp() sqlite_path = os.path.join(tmpdir, "e2e_test.db") env = os.environ.copy() env.update({ "FORCE_THIN_MODE": "true", "SQLITE_DB_PATH": sqlite_path, "ORACLE_DSN": "dummy", "ORACLE_USER": "dummy", "ORACLE_PASSWORD": "dummy", "JSON_OUTPUT_DIR": tmpdir, }) api_dir = os.path.join(os.path.dirname(__file__), "..", "..") proc = subprocess.Popen( [sys.executable, "-m", "uvicorn", "app.main:app", "--host", "127.0.0.1", "--port", str(port)], cwd=api_dir, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) # Wait for startup (up to 15 seconds) url = f"http://127.0.0.1:{port}" for _ in range(30): try: import urllib.request urllib.request.urlopen(f"{url}/health", timeout=1) break except Exception: time.sleep(0.5) else: proc.kill() stdout, stderr = proc.communicate() raise RuntimeError( f"App failed to start on port {port}.\n" f"STDOUT: {stdout.decode()[-2000:]}\n" f"STDERR: {stderr.decode()[-2000:]}" ) yield url proc.terminate() try: proc.wait(timeout=5) except subprocess.TimeoutExpired: proc.kill() @pytest.fixture(scope="session") def seed_test_data(app_url): """ Seed SQLite with test data via API calls. Oracle is unavailable in E2E tests — only SQLite-backed pages are fully functional. This fixture exists as a hook for future seeding; for now E2E tests validate UI structure on empty-state pages. """ return app_url # --------------------------------------------------------------------------- # Console & Network Error Collectors # --------------------------------------------------------------------------- @pytest.fixture(scope="session") def console_errors(): """Session-scoped list collecting JS console errors across all tests.""" return [] @pytest.fixture(scope="session") def network_errors(): """Session-scoped list collecting HTTP 4xx/5xx responses across all tests.""" return [] @pytest.fixture(autouse=True) def _attach_collectors(page, console_errors, network_errors, request): """Auto-attach console and network listeners to every test's page.""" test_errors = [] test_network = [] def on_console(msg): if msg.type == "error": entry = {"test": request.node.name, "text": msg.text, "type": "console.error"} console_errors.append(entry) test_errors.append(entry) def on_pageerror(exc): entry = {"test": request.node.name, "text": str(exc), "type": "pageerror"} console_errors.append(entry) test_errors.append(entry) def on_response(response): if response.status >= 400: entry = { "test": request.node.name, "url": response.url, "status": response.status, "type": "network_error", } network_errors.append(entry) test_network.append(entry) page.on("console", on_console) page.on("pageerror", on_pageerror) page.on("response", on_response) yield # Remove listeners to avoid leaks page.remove_listener("console", on_console) page.remove_listener("pageerror", on_pageerror) page.remove_listener("response", on_response) # --------------------------------------------------------------------------- # Screenshot on failure # --------------------------------------------------------------------------- @pytest.fixture(autouse=True) def _screenshot_on_failure(page, request): """Take a screenshot when a test fails.""" yield if request.node.rep_call and request.node.rep_call.failed: SCREENSHOTS_DIR.mkdir(parents=True, exist_ok=True) name = request.node.name.replace("/", "_").replace("::", "_") path = SCREENSHOTS_DIR / f"FAIL-{name}.png" try: page.screenshot(path=str(path)) except Exception: pass # page may be closed @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): """Store test result on the item for _screenshot_on_failure.""" outcome = yield rep = outcome.get_result() setattr(item, f"rep_{rep.when}", rep)