#!/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)