"""E2E teljes flow Playwright + AI-validáció.
A `prototype-agentic/docs/prototype-agentic-tesztek/` 72 manuális screenshot-os
tesztet automatizáljuk. 4 demo-eset (audit_demo, dd_demo, compliance_demo,
multi_doc) + minden tab full-page screenshot + chat-szekvencia + AI-validáció.
Futtatás:
pytest tests/e2e_screenshot/ -v -s
A `streamlit_server` session-fixture indítja a portot a 8520-on. A
`ai_validator.py` Claude vision-API-val validál a screenshotok alapján.
"""
from __future__ import annotations
import json
import time
from pathlib import Path
import pytest
from tests.e2e_screenshot.ai_validator import (
ValidationResult,
validate_screenshot,
write_validation_report,
)
from tests.e2e_screenshot.conftest import SNAPSHOTS_DIR
# ---------------------------------------------------------------------------
# Várt findingek a `prototype-agentic/test_data/EXPECTED_FINDINGS.md`-ből
# ---------------------------------------------------------------------------
EXPECTED_AUDIT_DEMO = [
"Magas kerekített összeg arány",
"50% árnövekedés a márciusi számlán",
"Hiányzó kötelező számlaelem (cím vagy fizetési mód)",
"Csomag-szintű cross-doc anomália",
]
EXPECTED_DD_DEMO = [
"Change-of-control klauzula",
"Non-compete (versenytilalom) klauzula",
"Automatikus megújulás",
"Top red flags lista (3+)",
"Per-szerződés kockázati szint",
"Havi kötelezettségek aggregálva",
]
EXPECTED_COMPLIANCE_DEMO = [
"GDPR 28. cikk hiányzó elemek (kritikus)",
"Kontraszt: a-szerz teljes vs b-szerz hiányos",
"Csomag-szintű compliance aszimmetria",
"Személyes adatok feldolgozása PII-indikátor",
]
EXPECTED_MULTI_DOC = [
"Three-way matching mennyiségi eltérés",
"Critical/warning a keresztellenőrzésben",
"HI-100 cikkszám említése",
]
# ---------------------------------------------------------------------------
# Helper-ek
# ---------------------------------------------------------------------------
def _click_tab(page, tab_name: str) -> None:
"""Streamlit tab-kattintás (a tab-szöveg alapján).
A Streamlit tab-jai `role="tab"` szerepben vannak — pontos szelektor,
hogy a sidebar gombokat (pl. "Chat előzmények törlése") NE találja el.
"""
# Elsődleges: pontos role+név egyezés a tablist-en belül
tab = page.get_by_role("tab", name=tab_name, exact=True).first
if tab.count() > 0:
tab.scroll_into_view_if_needed()
tab.click()
else:
# Fallback: explicit data-testid alapú szelektor (Streamlit st.tabs)
candidates = page.locator(f"[data-baseweb='tab']:has-text('{tab_name}')").all()
if candidates:
candidates[0].click()
else:
# Régi fallback (kockázatos, de jobb mint semmi)
page.locator(f"button:has-text('{tab_name}')").first.click()
page.wait_for_load_state("networkidle", timeout=10000)
time.sleep(1.5) # Streamlit re-render
def _full_page_screenshot(page, path: Path) -> None:
"""Teljes oldal screenshot (görgetett tartalom is).
A Streamlit shadow DOM-ja miatt a Playwright `full_page=True` csak a
viewport-ot rögzíti. Trükk: dinamikusan a tartalom magasságához állítjuk
a viewport-ot, scrollozunk az aljáig és vissza (lazy render trigger),
majd kérünk full_page screenshot-ot.
"""
path.parent.mkdir(parents=True, exist_ok=True)
try:
# 1. Görgetés aljáig hogy a virtual scroll alatt is mountolódjon
page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
time.sleep(0.6)
page.evaluate("window.scrollTo(0, 0)")
time.sleep(0.4)
# 2. Tartalom magasság detektálás (a max-ot vesszük a body és main között)
height = page.evaluate(
"""() => Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight,
document.body.offsetHeight,
document.documentElement.offsetHeight,
document.querySelector('main')?.scrollHeight || 0,
document.querySelector('section[data-testid=\\"stMain\\"]')?.scrollHeight || 0
)"""
)
height = max(int(height or 0), 1000)
# Maximalizáljuk: ne legyen hatalmas ha a content kicsi, de fedjen le mindent
target = min(height + 200, 12000)
page.set_viewport_size({"width": 1600, "height": target})
time.sleep(0.6)
except Exception:
pass
page.screenshot(path=str(path), full_page=True)
# Visszaállítás az alapviewport-ra (a következő művelet kompatibilitásához)
try:
page.set_viewport_size({"width": 1600, "height": 1000})
time.sleep(0.3)
except Exception:
pass
def _wait_for_demo_complete(page, timeout: float = 600.0) -> None:
"""Megvárja amíg a demo-pipeline befejeződik.
A `st.success("...betöltve...")` üzenet a `st.rerun()` után eltűnik —
helyette a sidebar **"Feldolgozott dokumentumok: N"** zöld dobozra várunk,
mert ez a `st.session_state.pipeline_state` jelenlétét tükrözi.
A Claude API hívásokra elég idő: 3 doksi × ~6 LLM hívás + package_insights
+ DD synthesizer = 25-30 LLM hívás Haiku-val ≈ 4-7 perc.
"""
deadline = time.time() + timeout
while time.time() < deadline:
# A sidebar success-doboz "Feldolgozott dokumentumok: N" → pipeline_state kész
if page.locator("text=Feldolgozott dokumentumok").count() > 0:
time.sleep(3.0)
return
# Backup: ha a Feltöltés tabon megjelenik a "Jelenleg N feldolgozott" üzenet
if page.locator("text=feldolgozott dokumentum van").count() > 0:
time.sleep(3.0)
return
# Az Alkalmazott szabványok footer is csak a pipeline-state után renderelődik
if page.locator("text=Alkalmazott szabványok").count() > 0:
time.sleep(3.0)
return
time.sleep(1.5)
raise TimeoutError(f"Demo nem fejeződött be {timeout}s alatt")
def _click_demo_button(page, label: str) -> None:
"""Demo gomb kattintás. A `Indítás` gomb a `label` alatti card-ban van.
A 3 demo card mindegyikében pontosan egyetlen "Indítás" feliratú gomb van —
a `Feldolgozás indítása` upload-gomb tág match miatt nem rontja el a
sorrendet, mert exact-name szelektort használunk.
"""
label_to_idx = {
"Audit Demo": 0,
"Due Diligence Demo": 1,
"Compliance Demo": 2,
}
idx = label_to_idx[label]
# Pontos szöveg-egyezés: csak az "Indítás" gomb (NEM "Feldolgozás indítása")
buttons = page.get_by_role("button", name="Indítás", exact=True).all()
if not buttons:
# Fallback: regex-pattern-rel pontosan az "Indítás" szöveggel
import re as _re
buttons = page.get_by_role("button", name=_re.compile(r"^Indítás$")).all()
if len(buttons) <= idx:
raise RuntimeError(
f"Csak {len(buttons)} db 'Indítás' gomb van, de a {idx}. (label={label}) kéne"
)
buttons[idx].scroll_into_view_if_needed()
buttons[idx].click()
def _manual_upload_files(page, file_paths: list[Path]) -> None:
"""Streamlit `st.file_uploader` programmatikus fájl-feltöltés.
A `app/main.py:feltoltes_tab`-ban `accept_multiple_files=True` van — egyszerre
többfájlos átadás OK. A feltöltés UTÁN megjelenik a "Feldolgozás indítása"
gomb (csak ha van fájl), arra kattintunk.
Args:
page: Playwright page objektum
file_paths: lista a feltöltendő fájlok abszolút útvonalairól
"""
# `st.file_uploader` egy hidden `` egy stXxxx wrapper-ben
file_input = page.locator("input[type='file']").first
file_input.set_input_files([str(p) for p in file_paths])
time.sleep(2.0) # Streamlit re-render hogy a "Feldolgozás indítása" megjelenjen
upload_btn = page.get_by_role("button", name="Feldolgozás indítása", exact=True).first
upload_btn.scroll_into_view_if_needed()
upload_btn.click()
def _open_all_expanders(page, max_count: int = 20) -> None:
"""Minden Streamlit expander-t kinyit (DD/Riport tabokon hasznos)."""
expanders = page.locator("button[aria-expanded='false']").all()
for exp in expanders[:max_count]:
try:
exp.click(timeout=2000)
time.sleep(0.3)
except Exception:
pass
time.sleep(0.5)
def _capture_5_tabs_and_chat(
page,
case_dir: Path,
questions: list[str],
) -> list[dict]:
"""A pipeline befejezése UTÁN: 5 tab full-page screenshot + chat-szekvencia.
Returns:
chat_responses lista a JSON mentéshez (és AI-validáció kontextushoz).
"""
# 03. Eredmények tab
_click_tab(page, "Eredmények")
time.sleep(2.0)
_full_page_screenshot(page, case_dir / "03_eredmenyek_full.png")
# 04. Chat tab — szekvencia kérdésekkel (kérdésenként külön screenshot)
_click_tab(page, "Chat")
time.sleep(2.0)
chat_responses: list[dict] = []
for i, q in enumerate(questions, start=1):
try:
answer = _ask_chat_question(page, q)
except Exception as exc:
answer = f"[HIBA: {type(exc).__name__}: {exc}]"
chat_responses.append({"question": q, "answer": answer})
_full_page_screenshot(page, case_dir / f"04_chat_q{i:02d}.png")
(case_dir / "chat_responses.json").write_text(
json.dumps(chat_responses, ensure_ascii=False, indent=2),
encoding="utf-8",
)
# 05. DD Asszisztens tab
_click_tab(page, "DD Asszisztens")
time.sleep(2.0)
_open_all_expanders(page)
_full_page_screenshot(page, case_dir / "05_dd_full.png")
# 06. Riport tab
_click_tab(page, "Riport")
time.sleep(2.0)
json_exp = page.locator("button:has-text('JSON nézet')").first
if json_exp.count() > 0:
try:
json_exp.click(timeout=2000)
time.sleep(1.0)
except Exception:
pass
_full_page_screenshot(page, case_dir / "06_riport_full.png")
return chat_responses
def _run_ai_validation(
case_dir: Path,
label: str,
expected: list[str],
chat_responses: list[dict],
) -> list[ValidationResult]:
"""AI-validáció a 3 fő screenshot-on (Eredmények + Chat 1. válasz + Riport)."""
chat_text = "\n\n".join(
f"Q: {r['question']}\nA: {r['answer']}" for r in chat_responses
)
results: list[ValidationResult] = []
results.append(validate_screenshot(
case_dir / "03_eredmenyek_full.png",
f"{label} / Eredmények tab",
expected,
))
if (case_dir / "04_chat_q01.png").exists():
results.append(validate_screenshot(
case_dir / "04_chat_q01.png",
f"{label} / Chat (1. válasz)",
expected,
raw_text_context=chat_text,
))
results.append(validate_screenshot(
case_dir / "06_riport_full.png",
f"{label} / Riport tab",
expected,
))
write_validation_report(case_dir, results)
return results
def _ask_chat_question(page, question: str) -> str:
"""Chat-input kitöltés + várás a válaszra. Visszaadja a válasz nyers szövegét."""
# Görgessünk az oldal aljáig hogy a chat_input mountolódjon (Streamlit lazy)
try:
page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
time.sleep(0.7)
except Exception:
pass
chat_input = page.locator("textarea[data-testid='stChatInputTextArea'], textarea[placeholder*='Kérdezz']").first
# Várjuk meg hogy láthatóvá váljon — Streamlit chat_input fixed pozícióban van
try:
chat_input.wait_for(state="visible", timeout=15000)
except Exception:
# Második próba: scroll_into_view_if_needed + várás
try:
chat_input.scroll_into_view_if_needed(timeout=5000)
except Exception:
pass
chat_input.fill(question)
chat_input.press("Enter")
# 15 másodperces fix várás. A Claude rövid válaszai 3-5s alatt kész, a hosszabb
# multi-doc/multi-szerződés kérdések 10-15s. A 15s középút: minden gyakori chat
# válasz kész, és csak +3 perc plusz idő a 4-scenario futáshoz.
time.sleep(15.0)
# Az utolsó assistant üzenet szövege
msgs = page.locator("[data-testid='stChatMessage']").all()
if not msgs:
return ""
return msgs[-1].inner_text()
# ---------------------------------------------------------------------------
# Tesztek
# ---------------------------------------------------------------------------
@pytest.mark.e2e
@pytest.mark.parametrize("demo,expected,questions", [
(
"audit_demo",
EXPECTED_AUDIT_DEMO,
[
"Mit lehet tudni ezekről a számlákról és mi az összefüggés köztük?",
"Hány százalékkal drágább a legutolsó számla a legelsőhöz képest?",
"Van matematikai hiba vagy hiányzó kötelező mező a számlákon?",
],
),
(
"dd_demo",
EXPECTED_DD_DEMO,
[
"Milyen DD-szempontból kritikus klauzulák szerepelnek a szerződésekben?",
"Mekkora az aggregált havi kötelezettség?",
"Van change-of-control vagy non-compete klauzula bárhol?",
],
),
(
"compliance_demo",
EXPECTED_COMPLIANCE_DEMO,
[
"Megfelel-e a két szerződés a GDPR 28. cikknek?",
"Hasonlítsd össze a két szerződést compliance szempontból.",
"Van olyan szerződés, ami személyes adatot dolgoz fel adatvédelmi záradék nélkül?",
],
),
])
def test_demo_full_flow(streamlit_server, browser, demo, expected, questions):
"""Demo gomb kattintás → 5 tab végig + chat-szekvencia + AI-validáció."""
case_dir = SNAPSHOTS_DIR / demo
case_dir.mkdir(parents=True, exist_ok=True)
page = browser.new_page()
page.goto(streamlit_server)
page.wait_for_load_state("networkidle", timeout=30000)
# Streamlit komplet renderelést várjuk: a "Gyors demo" h2 megjelenik
page.wait_for_selector("text=Gyors demo", timeout=30000)
time.sleep(2)
# 01. Feltöltés tab — alap állapot (teljes UI render után)
_full_page_screenshot(page, case_dir / "01_feltoltes_alap.png")
# 02. Demo gomb kattintás
label_map = {
"audit_demo": "Audit Demo",
"dd_demo": "Due Diligence Demo",
"compliance_demo": "Compliance Demo",
}
_click_demo_button(page, label_map[demo])
time.sleep(3.0)
_full_page_screenshot(page, case_dir / "02_demo_gomb_kattintva.png")
# Várás a feldolgozás befejeződésére (3 doksi × ~6 LLM hívás + package + DD ≈ 5-7 perc)
try:
_wait_for_demo_complete(page, timeout=600.0)
except TimeoutError:
_full_page_screenshot(page, case_dir / "ERROR_timeout.png")
raise
# 03. Eredmények tab full-page
_click_tab(page, "Eredmények")
time.sleep(2.0)
_full_page_screenshot(page, case_dir / "03_eredmenyek_full.png")
# 04. Chat tab — szekvencia kérdésekkel
_click_tab(page, "Chat")
time.sleep(2.0)
chat_responses: list[dict] = []
for i, q in enumerate(questions, start=1):
try:
answer = _ask_chat_question(page, q)
except Exception as exc:
answer = f"[HIBA: {type(exc).__name__}: {exc}]"
chat_responses.append({"question": q, "answer": answer})
_full_page_screenshot(page, case_dir / f"04_chat_q{i:02d}.png")
# Mentsük el a chat válaszokat JSON-be
(case_dir / "chat_responses.json").write_text(
json.dumps(chat_responses, ensure_ascii=False, indent=2),
encoding="utf-8",
)
# 05. DD Asszisztens tab full-page
_click_tab(page, "DD Asszisztens")
time.sleep(2.0)
# Minden expander nyitva legyen — minden expander gombra kattintunk
expanders = page.locator("button[aria-expanded='false']").all()
for exp in expanders[:20]: # max 20 a végtelen ciklus elkerüléséhez
try:
exp.click(timeout=2000)
time.sleep(0.3)
except Exception:
pass
time.sleep(1.0)
_full_page_screenshot(page, case_dir / "05_dd_full.png")
# 06. Riport tab full-page
_click_tab(page, "Riport")
time.sleep(2.0)
# JSON-expander nyitva
json_exp = page.locator("button:has-text('JSON nézet')").first
if json_exp.count() > 0:
try:
json_exp.click(timeout=2000)
time.sleep(1.0)
except Exception:
pass
_full_page_screenshot(page, case_dir / "06_riport_full.png")
# 07. AI-validáció — minden screenshot + chat-válasz alapján
chat_text = "\n\n".join(f"Q: {r['question']}\nA: {r['answer']}" for r in chat_responses)
results: list[ValidationResult] = []
eredmenyek_validation = validate_screenshot(
case_dir / "03_eredmenyek_full.png",
f"{demo} / Eredmények tab",
expected,
)
results.append(eredmenyek_validation)
chat_validation = validate_screenshot(
case_dir / "04_chat_q01.png",
f"{demo} / Chat (1. válasz)",
expected,
raw_text_context=chat_text,
)
results.append(chat_validation)
riport_validation = validate_screenshot(
case_dir / "06_riport_full.png",
f"{demo} / Riport tab",
expected,
)
results.append(riport_validation)
write_validation_report(case_dir, results)
page.close()
# Asszertálás — a végén legalább 1 "pass" vagy "partial" legyen
overall_states = {r.overall for r in results}
assert "pass" in overall_states or "partial" in overall_states, (
f"AI-validáció FAIL minden szekcióra: {[r.summary for r in results]}"
)
# ---------------------------------------------------------------------------
# (b) — Manuális upload szimuláció (4 forgatókönyv) ALAP TESZTI ARZENÁLLAL
# ---------------------------------------------------------------------------
# Várt findingek a manuális forgatókönyvekhez (paritás a tests/e2e_api/expected_findings.py-pel)
EXPECTED_MANUAL_SZAMLAK = [
"5 számla feldolgozva (HU + EN + DE)",
"Helyes nyelv-detekció (magyar/english/deutsch)",
"Classify confidence ≥ 90% mind",
"0 hamis-pozitív (NEM flag-eli a 0% VAT-ot, 27% ÁFA-t, 19% MwSt-et)",
"Max KOZEPES finding (Hiányzó Fizetési mód a HU számlákon)",
]
EXPECTED_MANUAL_SZERZODESEK = [
"4 szerződés feldolgozva (NDA + MSSA + IT support + leasing)",
"Felmondási feltételek mező kitöltve (legalább 2 szerz)",
"Irányadó jog mező kitöltve (legalább 2 szerz)",
"Change-of-control klauzula MSSA-ban detektálva",
"GDPR 28. cikk finding az IT-supporton vagy lízingen",
]
EXPECTED_MANUAL_MULTI_DOC = [
"3-utas keresztellenőrzés (megrendelés + szállítólevél + számla)",
"KRITIKUS HI-100 mennyiségi eltérés (40 vs 38)",
"I-gerenda 6m cikkszám említése",
"Comparison overall_status: critical",
]
EXPECTED_MANUAL_ADVERSARIAL = [
"Math-error detektálva: nettó+ÁFA != bruttó (50 000 Ft eltérés)",
"Hiányos szerződés finding: Felmondási feltételek hiánya MAGAS",
"Bilingual HU/EN szerződés Incoterms CIP detektálva",
"Dátum-logikai ellentmondás finding",
"3+ MAGAS severity összesen a 4 doksin",
]
@pytest.mark.e2e
@pytest.mark.parametrize("scenario,subdir,glob_pattern,expected,questions", [
(
"manual_szamlak",
"szamlak",
"*.pdf",
EXPECTED_MANUAL_SZAMLAK,
[
"Hány számla van feltöltve és milyen nyelvűek?",
"Van matematikai hiba vagy hiányzó kötelező mező a számlákon?",
"Hasonlítsd össze az ÁFA-kulcsokat a számlákon. Van valami szokatlan?",
],
),
(
"manual_szerzodesek",
"szerzodesek",
"*.pdf",
EXPECTED_MANUAL_SZERZODESEK,
[
"Mely szerződésekben van change-of-control vagy non-compete klauzula?",
"Mi az irányadó jog a szerződésekben?",
"Van automatikus megújulási klauzula bárhol?",
],
),
(
"manual_multi_doc",
"multi_doc",
"*.pdf",
EXPECTED_MANUAL_MULTI_DOC,
[
"Mekkora a HI-100 I-gerenda mennyisége a megrendelésen vs szállítólevélen vs számlán?",
"Mennyi a HI-100 hiány nettó értéke?",
"És bruttóban mennyibe kerül az előző hiány?",
],
),
(
"manual_adversarial",
"adversarial",
"*.pdf",
EXPECTED_MANUAL_ADVERSARIAL,
[
"Van matematikai hiba valamelyik dokumentumban?",
"Van olyan szerződés, amiben hiányoznak kötelező elemek?",
"Van olyan dokumentum, amiben dátum-logikai ellentmondás van?",
],
),
])
def test_manual_upload_full_flow(
streamlit_server, browser,
scenario, subdir, glob_pattern, expected, questions,
):
"""Manuális fájl-feltöltés az `st.file_uploader`-be → 5 tab + chat-szekvencia + AI-validáció.
Eltérés a `test_demo_full_flow`-hoz képest:
* A 3 demo-gomb HELYETT a Feltöltés tab `st.file_uploader`-ébe töltjük a fájlokat
* A teljes test_data//*.pdf készletet egyszerre adjuk át (5/4/3/4 fájl)
* A "Feldolgozás indítása" gomb futtatja a pipeline-t (UI-szintű, NEM demo-flow)
* Per-scenario teljes 5 tab + 3 chat kérdés
"""
from tests.e2e_screenshot.conftest import PROJECT_ROOT
case_dir = SNAPSHOTS_DIR / scenario
case_dir.mkdir(parents=True, exist_ok=True)
# Fájlok betöltése a test_data-ból
file_paths = sorted((PROJECT_ROOT / "test_data" / subdir).glob(glob_pattern))
assert file_paths, f"Nincs fájl: test_data/{subdir}/{glob_pattern}"
page = browser.new_page()
page.goto(streamlit_server)
page.wait_for_load_state("networkidle", timeout=30000)
page.wait_for_selector("text=Gyors demo", timeout=30000)
time.sleep(2)
# 01. Feltöltés tab — alapállapot
_full_page_screenshot(page, case_dir / "01_feltoltes_alap.png")
# 02. Manuális upload + Feldolgozás indítása
_manual_upload_files(page, file_paths)
time.sleep(3.0)
_full_page_screenshot(page, case_dir / "02_upload_indul.png")
# Várás: Claude pipeline + esetleg DD report (csak szerződésnél). Idő: 3-7 perc
try:
_wait_for_demo_complete(page, timeout=600.0)
except TimeoutError:
_full_page_screenshot(page, case_dir / "ERROR_timeout.png")
page.close()
raise
# 03-06. Tabok + chat
chat_responses = _capture_5_tabs_and_chat(page, case_dir, questions)
# 07. AI-validáció
results = _run_ai_validation(case_dir, scenario, expected, chat_responses)
page.close()
overall_states = {r.overall for r in results}
assert "pass" in overall_states or "partial" in overall_states, (
f"AI-validáció FAIL minden szekcióra: {[r.summary for r in results]}"
)