regress.sh — один вход: preflight (main = origin/main, чистые, нет чужих потребителей NATS), venv, lint+unit всех репозиториев и docs make check, фронт в чистой копии (npm ci --dry-run как проверка lock, установка без lock), интеграционные с циклом миграций, реальные данные парсера, образы, e2e ядра, изолированный стенд с нуля (compose-проект arm-ka-regress на своих портах) со сценарием по API, UI-смоук Playwright, парсер по doc-bank. Не останавливается на первой ошибке; итог — summary.md с SHA и статусом шагов. Контрольные цифры — expected.json (qa-guide §7.1–7.3, прогоны 08.10/14.10, golden шахматки); снимки свода, шахматки и форм сравниваются с прошлым прогоном. Ссылки из dev/checks.md и чек-листа синхронизации контура. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
96 lines
4.8 KiB
Python
96 lines
4.8 KiB
Python
"""UI-смоук фронта arm-core/web против стенда регрессии (regress.sh, этап ui; после сценария stand).
|
||
|
||
Обходит основные экраны под ka_go (токен — в sessionStorage, как после входа), на каждом ждёт затишья сети и
|
||
собирает: исключения страницы, ошибки консоли, ответы /api со статусом >= 400. Красный экран — исключение страницы,
|
||
ответ /api >= 500 или 4xx (кроме перечисленных в ALLOWED_4XX), пустая страница. Скриншоты — <out>/<экран>.png,
|
||
итог — <out>/report.json. Идентификаторы версий и документов берёт из report.json сценария стенда.
|
||
|
||
uv run --with playwright==1.55.0 python ui_smoke.py --web http://127.0.0.1:13000 --api http://127.0.0.1:18000 \\
|
||
--stand-report DIR/stand/report.json --out DIR/ui
|
||
"""
|
||
|
||
import argparse
|
||
import json
|
||
import sys
|
||
import urllib.request
|
||
from pathlib import Path
|
||
|
||
from playwright.sync_api import sync_playwright
|
||
|
||
# 4xx, которые фронт получает штатно (пусто — любой 4xx красный; дополнять с причиной)
|
||
ALLOWED_4XX: set[tuple[str, int]] = set()
|
||
|
||
|
||
def token(api: str, email: str) -> str:
|
||
req = urllib.request.Request(
|
||
api.rstrip("/") + "/api/v1/dev/token",
|
||
data=json.dumps({"email": email}).encode(),
|
||
headers={"Content-Type": "application/json"},
|
||
method="POST",
|
||
)
|
||
with urllib.request.urlopen(req, timeout=30) as r:
|
||
return json.loads(r.read())["access_token"]
|
||
|
||
|
||
def main() -> int:
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--web", required=True)
|
||
ap.add_argument("--api", required=True)
|
||
ap.add_argument("--stand-report", required=True, type=Path)
|
||
ap.add_argument("--out", required=True, type=Path)
|
||
ap.add_argument("--user", default="ka_go@armka.local")
|
||
a = ap.parse_args()
|
||
a.out.mkdir(parents=True, exist_ok=True)
|
||
ids = json.loads(a.stand_report.read_text()).get("ids", {})
|
||
g = ids.get("group_id")
|
||
pages = [("groups", "/groups"), ("catalogue-groups", "/catalogue/groups"), ("catalogue-le", "/catalogue/jur_l")]
|
||
if g:
|
||
pages.append(("group", f"/groups/{g}"))
|
||
for sid, vid in (ids.get("versions") or {}).items():
|
||
pages += [(f"{sid}-osv", f"/groups/{g}/versions/{vid}/osv"), (f"{sid}-chess", f"/groups/{g}/versions/{vid}/chess")]
|
||
for name, doc in (ids.get("documents") or {}).items():
|
||
pages.append((f"doc-{name}", f"/documents/{doc}"))
|
||
|
||
tok = token(a.api, a.user)
|
||
results = []
|
||
with sync_playwright() as p:
|
||
browser = p.chromium.launch()
|
||
ctx = browser.new_context(viewport={"width": 1600, "height": 1000}, locale="ru-RU")
|
||
ctx.add_init_script(f"try {{ sessionStorage.setItem('armka.token', {json.dumps(tok)}); }} catch (e) {{}}")
|
||
for name, path in pages:
|
||
page = ctx.new_page()
|
||
errors, console, bad_api = [], [], []
|
||
page.on("pageerror", lambda e, errors=errors: errors.append(str(e)[:300]))
|
||
page.on("console", lambda m, console=console: console.append(m.text[:300]) if m.type == "error" else None)
|
||
|
||
def on_response(r, bad_api=bad_api):
|
||
if "/api/" in r.url and r.status >= 400:
|
||
bad_api.append(f"{r.status} {r.request.method} {r.url.split('/api/', 1)[1][:150]}")
|
||
|
||
page.on("response", on_response)
|
||
try:
|
||
page.goto(a.web.rstrip("/") + path, wait_until="networkidle", timeout=60000)
|
||
page.wait_for_timeout(1500)
|
||
text = page.inner_text("body")
|
||
except Exception as e: # noqa: BLE001 — экран красный, обход продолжается
|
||
errors.append(f"{type(e).__name__}: {e}"[:300])
|
||
text = ""
|
||
page.screenshot(path=str(a.out / f"{name}.png"), full_page=False)
|
||
red_api = [
|
||
x for x in bad_api
|
||
if int(x.split()[0]) >= 500 or (x.split()[1], int(x.split()[0])) not in ALLOWED_4XX
|
||
]
|
||
ok = not errors and not red_api and len(text.strip()) > 20
|
||
results.append({"name": name, "path": path, "ok": ok, "page_errors": errors, "api_errors": bad_api, "console_errors": console[:10]})
|
||
print(("ok " if ok else "FAIL ") + f"{name} {path}" + ("" if ok else f": {errors or red_api or 'пустая страница'}"), flush=True)
|
||
page.close()
|
||
browser.close()
|
||
(a.out / "report.json").write_text(json.dumps(results, ensure_ascii=False, indent=1))
|
||
red = [r for r in results if not r["ok"]]
|
||
print(f"экранов {len(results)}, красных {len(red)}")
|
||
return 1 if red else 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|