224 lines
8.9 KiB
Python
224 lines
8.9 KiB
Python
"""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()))
|
|
|