feat(ui): replace configuration editors with graphical forms
CI / javascript-check (push) Successful in 14s
CI / container-policy (push) Successful in 4s
CI / python-tests (push) Successful in 1m48s
CI / container-verify (push) Skipped
CI / container-publish (push) Successful in 49s

This commit is contained in:
BartelLuis
2026-09-14 21:52:39 +02:00
parent 9a6dea34c4
commit 7b979e6243
22 changed files with 1603 additions and 81 deletions
+98
View File
@@ -0,0 +1,98 @@
"""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"]}))