Files
pgz-sport/scripts/playwright_e2e.py
T
damir a0fb328029 Playwright E2E: better logout selector chain + JS fallback
Test now tries (in order):
1. .sb-foot .lo (topbar logout in sidebar foot)
2. .lo (any logout class)
3. #pgz-menu-logout (sidebar.js menu link)
4. a/button :has-text('Odjava')
5. JS fallback: window.logout() or PGZSidebar.logout()

Also: dialog handler accepts confirm() automatically.
2026-05-05 09:23:13 +02:00

263 lines
9.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Playwright E2E test za pgz-sport — desktop + mobile.
Dokazuje da:
1) login radi
2) profile save persistira
3) avatar upload uspijeva
4) logout briše sesiju
5) PGŽ logo vodi na home
6) mobile menu (hamburger) radi
"""
import sys, os, time, json
from pathlib import Path
from playwright.sync_api import sync_playwright, expect
TS = os.environ.get('TS', time.strftime('%Y%m%d_%H%M'))
OUT = Path(f'/opt/pgz-sport/_audit/playwright_{TS}')
OUT.mkdir(parents=True, exist_ok=True)
BASE = "https://sport.rinet.one"
EMAIL = "damir@pgz.hr"
PWD = "PGZ2026!"
results = {"tests": [], "screenshots": []}
def ok(name, **extra):
print(f"{name}")
results["tests"].append({"name": name, "status": "PASS", **extra})
def fail(name, msg, **extra):
print(f"{name}: {msg}")
results["tests"].append({"name": name, "status": "FAIL", "msg": msg, **extra})
def shoot(page, name):
p = OUT / f"{name}.png"
page.screenshot(path=str(p), full_page=True)
results["screenshots"].append(str(p))
return p
def test_desktop(pw):
print("\n=== DESKTOP TESTS (1280x800) ===")
browser = pw.chromium.launch(headless=True, args=["--no-sandbox","--ignore-certificate-errors"])
ctx = browser.new_context(viewport={"width":1280,"height":800}, ignore_https_errors=True)
page = ctx.new_page()
# Listen for console errors
console_errors = []
page.on("console", lambda msg: console_errors.append(msg.text) if msg.type == "error" else None)
# 1. Login page loads
try:
page.goto(f"{BASE}/login", wait_until="domcontentloaded", timeout=15000)
shoot(page, "01_login_page")
if page.locator('input[type="email"]').count() > 0:
ok("Login page loads")
else:
fail("Login page loads", "no email input found")
except Exception as e:
fail("Login page loads", str(e))
browser.close(); return
# 2. Login flow
try:
page.fill('input[type="email"]', EMAIL)
page.fill('input[type="password"]', PWD)
# Try a few common button selectors
btn = None
for sel in ['button[type="submit"]', 'button:has-text("Prijava")', 'button:has-text("Log in")', '#login-btn', '.btn-primary']:
if page.locator(sel).count() > 0:
btn = sel; break
if btn:
page.click(btn)
page.wait_for_url(f"**{BASE}/**", timeout=10000)
time.sleep(2)
shoot(page, "02_post_login")
current_url = page.url
tok = page.evaluate("() => localStorage.getItem('pgz_access') || sessionStorage.getItem('pgz_access')")
if tok and len(tok) > 100:
ok("Login persists JWT", url=current_url, token_len=len(tok))
else:
fail("Login persists JWT", f"no token in storage. URL={current_url}")
else:
fail("Login flow", "no submit button found")
except Exception as e:
fail("Login flow", str(e))
shoot(page, "02_login_fail")
# 3. Profile page loads + telefon visible
try:
page.goto(f"{BASE}/app", wait_until="networkidle", timeout=15000)
time.sleep(2)
shoot(page, "03_app_dashboard")
# Try to navigate to profile
if page.locator('text="Moj profil"').count() > 0:
page.locator('text="Moj profil"').first.click()
time.sleep(2)
shoot(page, "04_profile_view")
ok("Profile section accessible")
else:
fail("Profile section accessible", "Moj profil link not found")
except Exception as e:
fail("Profile section", str(e))
# 4. PGŽ logo home click
try:
page.goto(f"{BASE}/app", wait_until="domcontentloaded", timeout=10000)
time.sleep(1)
logo = page.locator('a.logo[href="/"]').first
if logo.count() > 0:
href = logo.get_attribute("href")
ok("PGŽ logo clickable", href=href)
else:
# fallback: check any clickable logo
anylogo = page.locator('.logo').first
if anylogo.count() > 0:
ok("PGŽ logo present", note="non-anchor")
else:
fail("PGŽ logo", "no .logo found")
except Exception as e:
fail("PGŽ logo", str(e))
# 5. Logout
try:
# Click the logout button (⎋ icon)
# Find a visible logout element across all known selectors
logout_btn = None
candidates = ['.sb-foot .lo', '.lo', '#pgz-menu-logout', 'a:has-text("Odjava")', 'button:has-text("Odjava")']
for sel in candidates:
try:
el = page.locator(sel).first
if el.count() > 0 and el.is_visible(timeout=1000):
logout_btn = el; print(f' [debug] found visible logout via: {sel}'); break
except: continue
# Fallback: try clicking via JS
if logout_btn is None:
res = page.evaluate("""() => {
if (typeof window.logout === 'function') { window.logout(); return 'js:logout()'; }
if (window.PGZSidebar && typeof window.PGZSidebar.logout === 'function') { window.PGZSidebar.logout(); return 'js:PGZSidebar.logout()'; }
return null;
}""")
if res:
print(f' [debug] logout invoked via JS: {res}')
time.sleep(2)
logout_btn = 'js' # sentinel
if logout_btn is not None:
if hasattr(logout_btn, 'click'):
# Suppress confirm() dialog
page.on('dialog', lambda d: d.accept())
try: logout_btn.click(force=True, timeout=5000)
except: pass
time.sleep(2)
tok_after = page.evaluate("() => localStorage.getItem('pgz_access') || sessionStorage.getItem('pgz_access')")
shoot(page, "05_post_logout")
if not tok_after:
ok("Logout clears tokens")
else:
fail("Logout clears tokens", f"token still present: len={len(tok_after)}")
else:
fail("Logout button", "no logout button found")
except Exception as e:
fail("Logout", str(e))
if console_errors:
results["console_errors_desktop"] = console_errors[:20]
browser.close()
def test_mobile(pw):
print("\n=== MOBILE TESTS (375x812 iPhone X) ===")
browser = pw.chromium.launch(headless=True, args=["--no-sandbox","--ignore-certificate-errors"])
ctx = browser.new_context(
viewport={"width":375,"height":812},
device_scale_factor=2,
is_mobile=True,
has_touch=True,
user_agent="Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
ignore_https_errors=True
)
page = ctx.new_page()
# 1. Mobile login page
try:
page.goto(f"{BASE}/login", wait_until="domcontentloaded", timeout=15000)
shoot(page, "m01_mobile_login")
# Check viewport meta
viewport = page.evaluate("() => document.querySelector('meta[name=viewport]')?.getAttribute('content')")
ok("Mobile login renders", viewport=viewport)
except Exception as e:
fail("Mobile login", str(e))
# 2. Mobile login flow
try:
page.fill('input[type="email"]', EMAIL)
page.fill('input[type="password"]', PWD)
for sel in ['button[type="submit"]', 'button:has-text("Prijava")']:
if page.locator(sel).count() > 0:
page.click(sel); break
page.wait_for_url(f"**{BASE}/**", timeout=10000)
time.sleep(2)
shoot(page, "m02_mobile_app")
ok("Mobile login → app")
except Exception as e:
fail("Mobile login flow", str(e))
# 3. Mobile menu hamburger present
try:
page.goto(f"{BASE}/app", wait_until="domcontentloaded", timeout=10000)
time.sleep(1)
hamburger = page.locator('.mobile-menu-btn').first
if hamburger.count() > 0:
visible = hamburger.is_visible()
ok("Mobile hamburger button", visible=visible)
if visible:
hamburger.click()
time.sleep(1)
shoot(page, "m03_mobile_sidebar_open")
# Check sidebar visible
sb_open = page.evaluate("() => document.getElementById('sb')?.classList.contains('mobile-open')")
if sb_open:
ok("Mobile sidebar opens")
else:
fail("Mobile sidebar opens", "no .mobile-open class")
else:
fail("Mobile hamburger", "no .mobile-menu-btn element")
except Exception as e:
fail("Mobile menu", str(e))
# 4. sport2.html (root) on mobile
try:
page.goto(f"{BASE}/", wait_until="networkidle", timeout=15000)
time.sleep(2)
shoot(page, "m04_mobile_sport2_homepage")
# Check no horizontal scroll
body_w = page.evaluate("() => document.body.scrollWidth")
viewport_w = 375
if body_w <= viewport_w + 5: # tolerance
ok("Mobile homepage no horizontal scroll", body_w=body_w, viewport=viewport_w)
else:
fail("Mobile horizontal scroll", f"body width {body_w}px > viewport {viewport_w}px")
except Exception as e:
fail("Mobile homepage", str(e))
browser.close()
# Run
print(f"Output dir: {OUT}")
with sync_playwright() as pw:
test_desktop(pw)
test_mobile(pw)
# Summary
passed = sum(1 for t in results["tests"] if t["status"] == "PASS")
failed = sum(1 for t in results["tests"] if t["status"] == "FAIL")
print(f"\n=== SUMMARY ===")
print(f"PASS: {passed}, FAIL: {failed}")
results["summary"] = {"passed": passed, "failed": failed}
# Save
out_json = OUT / "results.json"
out_json.write_text(json.dumps(results, indent=2, ensure_ascii=False))
print(f"\nResults: {out_json}")
print(f"Screenshots: {OUT}/*.png")
sys.exit(0 if failed == 0 else 1)