feat: +18 eventos escandalo/momento LATAM, premios nuevos, fix export ELO, e2e tests
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
"""E2E smoke test for 1v9 — Convertite en Leyenda.
|
||||
|
||||
Juega carreras completas con un bot y verifica:
|
||||
- 0 errores de consola / 0 requests fallidos
|
||||
- resumen alcanzado en 100% de las carreras
|
||||
- sin overflow horizontal (desktop + mobile)
|
||||
- timeline colapsada en mobile
|
||||
- design tokens correctos
|
||||
- gating: 2ª división no ve competencias internacionales
|
||||
- fichajes solo desde split >= 4
|
||||
- stats sin NaN ni claves fantasma
|
||||
- diversidad: las 5 competencias aparecen en N carreras
|
||||
- export de imagen 1080x1920 válido (PIL)
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
import random
|
||||
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
BASE = "http://127.0.0.1:8899/"
|
||||
|
||||
|
||||
async def pick_region_role(page):
|
||||
cards = page.locator(".region-card")
|
||||
n = await cards.count()
|
||||
idx = random.randrange(n)
|
||||
await cards.nth(idx).click()
|
||||
cards = page.locator(".role-card")
|
||||
n = await cards.count()
|
||||
await cards.nth(random.randrange(n)).click()
|
||||
|
||||
|
||||
async def play_career(page, want_export=False, max_rounds=60):
|
||||
"""Juega una carrera completa. Devuelve dict con datos de la partida."""
|
||||
data = {"decisions": [], "intl_seen": [], "categories": [], "transfer_splits": [], "errors": [], "intl_divs": []}
|
||||
page.on("pageerror", lambda e: data["errors"].append(f"pageerror: {e}"))
|
||||
page.on("console", lambda m: data["errors"].append(f"console: {m.text}") if m.type == "error" else None)
|
||||
page.on("requestfailed", lambda r: data["errors"].append(f"requestfailed: {r.url}"))
|
||||
|
||||
await page.goto(BASE, wait_until="networkidle")
|
||||
await page.click("#btnStart")
|
||||
await pick_region_role(page)
|
||||
await page.fill("#inpNick", "BotPro")
|
||||
await page.click("#btnBegin")
|
||||
|
||||
await page.click(".opt-card:visible >> nth=0") # oferta de academia
|
||||
|
||||
rounds = 0
|
||||
while rounds < max_rounds:
|
||||
rounds += 1
|
||||
summary_active = await page.evaluate("document.getElementById('summary').classList.contains('active')")
|
||||
if summary_active:
|
||||
break
|
||||
overlay = await page.evaluate("document.getElementById('resultOverlay').classList.contains('show')")
|
||||
if overlay:
|
||||
cont = page.locator("#btnContinue")
|
||||
if await cont.count():
|
||||
await cont.click()
|
||||
continue
|
||||
# nueva decisión renderizada
|
||||
cat = await page.locator(".dc-cat").first.text_content()
|
||||
if cat:
|
||||
data["categories"].append(cat.strip())
|
||||
if "Fichaje" in cat:
|
||||
split = await page.evaluate("G.split")
|
||||
data["transfer_splits"].append(split)
|
||||
for comp in ["First Stand", "MSI", "EWC", "Asian Games", "Worlds"]:
|
||||
if comp in cat and comp not in data["intl_seen"]:
|
||||
data["intl_seen"].append(comp)
|
||||
if any(c in cat for c in ["First Stand", "MSI", "EWC", "Asian Games", "Worlds"]):
|
||||
div = await page.evaluate("G.div")
|
||||
data["intl_divs"].append(div)
|
||||
opts = page.locator(".opt-card:visible")
|
||||
n = await opts.count()
|
||||
if not n:
|
||||
await page.wait_for_timeout(300)
|
||||
continue
|
||||
# 40% de retirarse cuando esté disponible (split >= 6) para acelerar
|
||||
retire = page.locator('.opt-card[data-i="retire"]:visible')
|
||||
if await retire.count() and random.random() < 0.4:
|
||||
await retire.click()
|
||||
else:
|
||||
await opts.nth(random.randrange(n)).click()
|
||||
await page.wait_for_timeout(120)
|
||||
|
||||
state = await page.evaluate("""() => ({
|
||||
split: G.split, totalSplits: G.totalSplits, retired: G.retired, div: G.div,
|
||||
stats: G.stats, teamId: G.teamId, intl: G.intl, teamTitles: G.teamTitles,
|
||||
indTitles: G.indTitles, history: G.history, games: G.games,
|
||||
kills: G.kills, assists: G.assists, deaths: G.deaths, penta: G.penta,
|
||||
maxValue: G.maxValue, ratingMax: G.ratingMax, regSelect: G.regSelect
|
||||
})""")
|
||||
data["state"] = state
|
||||
|
||||
if want_export:
|
||||
async with page.expect_download(timeout=10000) as dl_info:
|
||||
await page.click("#btnExport")
|
||||
dl = await dl_info.value
|
||||
path = await dl.path()
|
||||
data["download"] = path
|
||||
|
||||
return data
|
||||
|
||||
|
||||
async def check_layout(page, is_mobile):
|
||||
"""Chequeos de layout programáticos."""
|
||||
res = {}
|
||||
res["overflow"] = await page.evaluate(
|
||||
"document.documentElement.scrollWidth > window.innerWidth + 1"
|
||||
)
|
||||
res["tl_closed_mobile"] = await page.evaluate(
|
||||
"""() => {
|
||||
const b = document.getElementById('tlBody');
|
||||
return !b || b.classList.contains('closed');
|
||||
}"""
|
||||
) if is_mobile else None
|
||||
res["tokens"] = await page.evaluate(
|
||||
"""() => {
|
||||
const s = getComputedStyle(document.documentElement);
|
||||
return {
|
||||
accent: s.getPropertyValue('--accent-ovr').trim(),
|
||||
success: s.getPropertyValue('--success').trim(),
|
||||
bg: s.getPropertyValue('--bg-base').trim()
|
||||
};
|
||||
}"""
|
||||
)
|
||||
return res
|
||||
|
||||
|
||||
def check_state(data):
|
||||
"""Verifica mecánicas sobre el estado final de una carrera."""
|
||||
issues = []
|
||||
st = data["state"]
|
||||
for k, v in st["stats"].items():
|
||||
if v != v or v is None: # NaN check
|
||||
issues.append(f"stat NaN: {k}")
|
||||
ghost = [k for k in st["stats"] if k in ("valor",)]
|
||||
if ghost:
|
||||
issues.append(f"ghost stats: {ghost}")
|
||||
# gating internacionales: evento intl solo con div 1
|
||||
for cat in data["categories"]:
|
||||
if any(c in cat for c in ["First Stand", "MSI", "EWC", "Asian Games", "Worlds"]):
|
||||
pass # no podemos saber el div en el momento; se chequea en vivo abajo
|
||||
# fichajes >= split 4
|
||||
for s in data["transfer_splits"]:
|
||||
if s < 4:
|
||||
issues.append(f"transfer en split {s} (< 4)")
|
||||
# gating: toda competencia vista con div 1
|
||||
for d in data["intl_divs"]:
|
||||
if d != 1:
|
||||
issues.append(f"competencia internacional vista en div {d}")
|
||||
return issues
|
||||
|
||||
|
||||
def validate_export(path):
|
||||
"""Valida el PNG exportado con PIL."""
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
return "PIL no instalado (skip)"
|
||||
img = Image.open(path)
|
||||
size = img.size
|
||||
mode = img.mode
|
||||
px = img.convert("RGB").getpixel((100, 100))
|
||||
luma = img.convert("L").getextrema()
|
||||
ok = size == (1080, 1920) and luma[1] > 60
|
||||
return f"OK size={size} mode={mode} bg={px} luma={luma}" if ok else f"FAIL size={size} mode={mode} bg={px} luma={luma}"
|
||||
|
||||
|
||||
async def main():
|
||||
results = {"errors_total": 0, "careers": [], "intl_all": set(), "export_ok": None}
|
||||
n_careers = int(sys.argv[1]) if len(sys.argv) > 1 else 6
|
||||
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch()
|
||||
|
||||
# --- desktop ---
|
||||
page = await browser.new_page(viewport={"width": 1280, "height": 800})
|
||||
data = await play_career(page, want_export=True)
|
||||
layout = await check_layout(page, is_mobile=False)
|
||||
print("DESKTOP layout:", json.dumps(layout, ensure_ascii=False))
|
||||
print("DESKTOP errors:", data["errors"])
|
||||
print("DESKTOP finished:", data["state"]["split"] >= data["state"]["totalSplits"] or data["state"]["retired"])
|
||||
if data.get("download"):
|
||||
results["export_ok"] = validate_export(data["download"])
|
||||
print("DESKTOP export:", results["export_ok"])
|
||||
await page.close()
|
||||
|
||||
# --- mobile (390x844) + N carreras ---
|
||||
for i in range(n_careers):
|
||||
page = await browser.new_page(viewport={"width": 390, "height": 844})
|
||||
data = await play_career(page, want_export=False)
|
||||
issues = check_state(data)
|
||||
finished = data["state"]["split"] >= data["state"]["totalSplits"] or data["state"]["retired"]
|
||||
print(f"--- carrera {i+1}: errors={len(data['errors'])} finished={finished} div={data['state']['div']} intl={data['intl_seen']} transfers={data['transfer_splits']} issues={issues}")
|
||||
results["errors_total"] += len(data["errors"])
|
||||
results["careers"].append({"finished": finished, "issues": issues})
|
||||
results["intl_all"] |= set(data["intl_seen"])
|
||||
await page.close()
|
||||
|
||||
# layout mobile una vez
|
||||
page = await browser.new_page(viewport={"width": 390, "height": 844})
|
||||
data = await play_career(page)
|
||||
layout = await check_layout(page, is_mobile=True)
|
||||
print("MOBILE layout:", json.dumps(layout, ensure_ascii=False))
|
||||
await page.close()
|
||||
await browser.close()
|
||||
|
||||
ok = results["errors_total"] == 0 and all(c["finished"] for c in results["careers"])
|
||||
print("\n=== RESUMEN ===")
|
||||
print("errores totales:", results["errors_total"])
|
||||
print("carreras completadas:", sum(1 for c in results["careers"] if c["finished"]), "/", len(results["careers"]))
|
||||
print("issue por carrera:", [c["issues"] for c in results["careers"]])
|
||||
print("competencias vistas:", sorted(results["intl_all"]))
|
||||
print("export:", results["export_ok"])
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(asyncio.run(main()))
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 356 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
Reference in New Issue
Block a user