"""Isolated localhost application and Chromium support for browser_forms.py. Only generated test credentials/data are used. Fixture databases, server logs, and screenshots are written below the repository's ignored .cache/ui-browser. The server process and browser are always terminated after each scenario group. """ from contextlib import contextmanager import json import os from pathlib import Path import socket import subprocess import sys import tempfile import time import httpx from playwright.sync_api import sync_playwright ROOT = Path(__file__).resolve().parents[1] TEST_PASSWORD = "Browser-fixture-admin-2026!" @contextmanager def local_browser(viewport=None): with socket.socket() as listener: listener.bind(("127.0.0.1", 0)) port = listener.getsockname()[1] base = f"http://127.0.0.1:{port}" artifacts = ROOT / ".cache" / "ui-browser" artifacts.mkdir(parents=True, exist_ok=True) tempdir = Path(tempfile.mkdtemp(prefix="fixture-", dir=artifacts)) env = { **os.environ, "PYTHONPATH": str(ROOT), "DATA_DIR": str(tempdir / "data"), "MASTER_KEY_FILE": str(tempdir / "keys" / "master.key"), "PUBLIC_URL": base, "SECURE_COOKIES": "false", "BOOTSTRAP_USERNAME": "admin", "BOOTSTRAP_PASSWORD": TEST_PASSWORD, "TESTING": "true", "FOUR_EYES": "false", } log = (tempdir / "server.log").open("w", encoding="utf-8") server = subprocess.Popen( [sys.executable, "-m", "provisioner.cli", "serve", "--host", "127.0.0.1", "--port", str(port)], cwd=ROOT, env=env, stdout=log, stderr=subprocess.STDOUT, creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0, ) try: for _ in range(80): if server.poll() is not None: raise RuntimeError((tempdir / "server.log").read_text(encoding="utf-8")) try: if httpx.get(base + "/health/ready", timeout=1).status_code == 200: break except httpx.HTTPError: pass time.sleep(0.15) else: raise TimeoutError("Local application did not become ready") with sync_playwright() as playwright: browser = playwright.chromium.launch(headless=True) context = browser.new_context(viewport=viewport or {"width": 1440, "height": 1000}) page = context.new_page() errors = [] page.on("pageerror", lambda error: errors.append(str(error))) page.goto(base + "/") page.get_by_label("Benutzername").fill("admin") page.get_by_label("Passwort", exact=True).fill(TEST_PASSWORD) page.get_by_role("button", name="Anmelden").click() page.wait_for_selector("#main .page-header") csrf = context.request.get(base + "/api/v1/me").json()["csrf_token"] def api(path, body=None): response = context.request.get(base + "/api/v1" + path) if body is None else context.request.post( base + "/api/v1" + path, data=body, headers={"X-CSRF-Token": csrf}) assert response.ok, f"{path}: {response.status} {response.text()}" return response.json() yield page, api, {"base": base, "artifacts": artifacts, "fixture": tempdir, "errors": errors} assert not errors, errors context.close() browser.close() finally: server.terminate() try: server.wait(timeout=5) except subprocess.TimeoutExpired: server.kill() server.wait(timeout=5) log.close() if __name__ == "__main__": with local_browser() as (page, api, context): print(json.dumps({"title": page.title(), "url": page.url, "me": api("/me")["username"], "errors": context["errors"]}))