У фичи AURORA-1540/1541 (корректировки) и AURORA-1751 (vgo_assignment),
слитых 02.11.2026, не было ни одной проверки в регрессии: ui_smoke.py не
заходил на вкладку корректировок, stand_scenario.py не делал ни одного
запроса к /corrections или /vgo-assignments.
- stand_scenario.py: happy-path GET /vgo-assignments/distribute и
GET /corrections на каждый сценарий; сохранение корректировки первого
агрегата и повтор с тем же expected_version_no — 409
CORRECTION_VERSION_CONFLICT. rb-2024-q3 не имеет подтверждённых форм ББ
(только ОСВ) — сохранение там штатно получает 422 INVALID_LINE_CODE,
это не регрессия и репортится как пропуск, не красным.
- ui_smoke.py: страница /groups/{g}/versions/{v}/correction на каждую
версию сценария.
Проверено: python3 -I scripts/regression/regress.sh --only stand,ui —
зелёный, оба сценария (rb-2024-q3, rb-2025-q1), включая реальный 409 на
rb-2025-q1 и скриншот вкладки «Таблица корректировки» с данными.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
100 lines
5.0 KiB
Python
100 lines
5.0 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"),
|
||
(f"{sid}-correction", f"/groups/{g}/versions/{vid}/correction"),
|
||
]
|
||
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())
|