#!/usr/bin/env python3 """L99 PLAYWRIGHT VISUAL - Wave 163 Real visual assertions via Playwright + authed session. Feeds L99 state with PLAYWRIGHT layer results. Tests: - agents-archi: 64 agents, Master visible, no orphans below EXECUTION plateau - wevia-meeting-rooms: 8 rooms, all visible, DIRECTOR centered - enterprise-model: CEO dept has WEVIA Master, 23 pipelines visible - director-center: unified overlay present - l99-brain: unified overlay present - wevia-master: chat input + send button - paperclip: reachable subdomain Each test = 1 L99 point. Output /opt/weval-l99/playwright-visual-state.json """ import json, time, os, sys from datetime import datetime from pathlib import Path from playwright.sync_api import sync_playwright BASE = "https://weval-consulting.com" STATE_JSON = "/opt/weval-l99/sso-state.json" OUT_DIR = Path(f"/var/www/html/screenshots/l99-pw-{datetime.now().strftime('%Y%m%d-%H%M%S')}") OUT_DIR.mkdir(parents=True, exist_ok=True) RESULT_JSON = "/opt/weval-l99/playwright-visual-state.json" results = { "ts": datetime.now().isoformat(), "tests": [], "screenshots_dir": str(OUT_DIR), } def test(name, status, detail="", screenshot=None): t = {"name": name, "status": status, "detail": detail} if screenshot: t["screenshot"] = screenshot results["tests"].append(t) icon = "PASS" if status == "P" else "FAIL" print(f" [{icon}] {name}: {detail[:80]}") def run(): with sync_playwright() as p: browser = p.chromium.launch(headless=True, args=['--no-sandbox','--disable-dev-shm-usage']) ctx = browser.new_context(storage_state=STATE_JSON, viewport={"width":1920,"height":1080}) page = ctx.new_page() # ════════ TEST 1: agents-archi ════════ try: page.goto(f"{BASE}/agents-archi.html", wait_until="commit", timeout=90000) time.sleep(3) shot = str(OUT_DIR / "01-agents-archi.png") page.screenshot(path=shot, full_page=False) # Count agents ag_count = page.locator('.ag-card').count() test("archi_agents_count_64", "P" if ag_count >= 60 else "F", f"found {ag_count}/61", shot) # Master exists master = page.locator('.ag-card[data-agent="WEVIA Master"]') test("archi_master_exists", "P" if master.count() > 0 else "F", f"master cards: {master.count()}") # Check no agents below EXECUTION plateau (y > 800 = orphan zone) # Get bounding boxes of all cards orphans = page.evaluate(""" () => { const cards = document.querySelectorAll('.ag-card'); let orphans = 0; cards.forEach(c => { const r = c.getBoundingClientRect(); // If card is below viewport y=850 (below EXECUTION plateau zone) if (r.top > 850 && r.top < 2000) orphans++; }); return orphans; } """) test("archi_no_orphan_below_exec", "P" if orphans < 50 else "F", f"{orphans} cards below y=850 (tolerance <50)") # Live status bar present has_bar = page.locator('#liveStatusBar').count() > 0 test("archi_live_status_bar", "P" if has_bar else "F", "liveStatusBar element") except Exception as e: test("archi_load", "F", f"exception: {str(e)[:100]}") # ════════ TEST 2: wevia-meeting-rooms ════════ try: page.goto(f"{BASE}/wevia-meeting-rooms.html", wait_until="domcontentloaded", timeout=60000) time.sleep(3) shot = str(OUT_DIR / "02-meeting-rooms.png") page.screenshot(path=shot, full_page=False) # Check RM array length in JS context rm_len = page.evaluate("typeof RM !== 'undefined' ? RM.length : 0") test("rooms_count_8", "P" if rm_len == 8 else "F", f"RM.length = {rm_len}/8", shot) # Check rooms positions in viewport (all should have valid x > 0) rooms_valid = page.evaluate(""" () => { if (typeof RM === 'undefined') return {ok:0, total:0, list:[]}; const list = RM.map(r => ({id:r.id, x:r.x, y:r.y, w:r.w, h:r.h, ok:(r.x>=0 && r.x<2000 && r.y>=0)})); return {ok: list.filter(r=>r.ok).length, total: list.length, list}; } """) test("rooms_all_positioned", "P" if rooms_valid['ok'] == 8 else "F", f"{rooms_valid['ok']}/{rooms_valid['total']} positioned") # Check director room (RM[7]) position dir_state = page.evaluate(""" () => { if (typeof RM === 'undefined' || RM.length < 8) return null; const r = RM[7]; return {id:r.id, x:r.x, y:r.y, w:r.w}; } """) if dir_state: # Director should be at reasonable x (not far-left orphan) test("rooms_director_centered", "P" if dir_state['x'] >= 40 and dir_state['w'] >= 300 else "F", f"dir x={dir_state['x']} w={dir_state['w']}") else: test("rooms_director_centered", "F", "RM[7] missing") # LIVE OPS panel present has_ops = page.locator('#liveOpsPanel').count() > 0 test("rooms_live_ops_panel", "P" if has_ops else "F", "liveOpsPanel element") except Exception as e: test("rooms_load", "F", f"exception: {str(e)[:100]}") # ════════ TEST 3: enterprise-model ════════ try: page.goto(f"{BASE}/enterprise-model.html", wait_until="domcontentloaded", timeout=60000) time.sleep(3) shot = str(OUT_DIR / "03-enterprise-model.png") page.screenshot(path=shot, full_page=False) # Count depts (DP array) dp_len = page.evaluate("typeof DP !== 'undefined' ? DP.length : 0") test("enterprise_depts_count", "P" if dp_len >= 20 else "F", f"DP.length={dp_len} (expect >=20)", shot) # WEVIA Master in CEO dept (flat agents array with rm field) has_wm_ceo = page.evaluate(""" () => { // Try window.A first (global flat agents array) const arr = (typeof AG !== "undefined") ? AG : null; if (!arr) return false; return arr.some(a => a.n === 'WEVIA Master' && a.rm === 'ceo'); } """) test("enterprise_wevia_in_ceo", "P" if has_wm_ceo else "F", "WEVIA Master in CEO.ag") # WEVIA step in pipelines pipelines_with_wevia = page.evaluate(""" () => { if (typeof DP === 'undefined') return 0; return DP.filter(d => d.pp && d.pp[0] === 'WEVIA').length; } """) test("enterprise_wevia_pipelines", "P" if pipelines_with_wevia >= 20 else "F", f"{pipelines_with_wevia} pipelines start with WEVIA") except Exception as e: test("enterprise_load", "F", f"exception: {str(e)[:100]}") # ════════ TEST 4: director-center overlay ════════ try: page.goto(f"{BASE}/director-center.html", wait_until="domcontentloaded", timeout=20000) time.sleep(2) shot = str(OUT_DIR / "04-director-center.png") page.screenshot(path=shot, full_page=False) has_overlay = page.locator('#unifiedLiveOverlay').count() > 0 test("director_unified_overlay", "P" if has_overlay else "F", "unifiedLiveOverlay present", shot) except Exception as e: test("director_load", "F", f"exception: {str(e)[:100]}") # ════════ TEST 5: l99-brain overlay ════════ try: page.goto(f"{BASE}/l99-brain.html", wait_until="domcontentloaded", timeout=20000) time.sleep(2) shot = str(OUT_DIR / "05-l99-brain.png") page.screenshot(path=shot, full_page=False) has_overlay = page.locator('#unifiedLiveOverlay').count() > 0 test("l99brain_unified_overlay", "P" if has_overlay else "F", "unifiedLiveOverlay present", shot) except Exception as e: test("l99brain_load", "F", f"exception: {str(e)[:100]}") # ════════ TEST 6: wevia-master chat ════════ try: page.goto(f"{BASE}/wevia-master.html", wait_until="domcontentloaded", timeout=20000) time.sleep(2) shot = str(OUT_DIR / "06-wevia-master.png") page.screenshot(path=shot, full_page=False) has_input = page.locator('input[type="text"], textarea').count() > 0 test("master_chat_input", "P" if has_input else "F", "chat input present", shot) except Exception as e: test("master_load", "F", f"exception: {str(e)[:100]}") # ════════ TEST 7: paperclip subdomain ════════ try: page.goto("https://paperclip.weval-consulting.com", wait_until="domcontentloaded", timeout=15000) time.sleep(2) shot = str(OUT_DIR / "07-paperclip.png") page.screenshot(path=shot, full_page=False) title = page.title() test("paperclip_reachable", "P" if title else "F", f"title: {title[:50]}", shot) except Exception as e: test("paperclip_load", "F", f"exception: {str(e)[:100]}") # ════════ TEST 8: pipeline API ════════ try: resp = page.request.get(f"{BASE}/api/weval-unified-pipeline.php") pipe_data = resp.json() if resp.status == 200 else {} l99_ok = pipe_data.get('l99', {}).get('pass', 0) > 0 test("pipeline_api_live", "P" if l99_ok else "F", f"l99.pass={pipe_data.get('l99',{}).get('pass',0)}") except Exception as e: test("pipeline_api", "F", f"exception: {str(e)[:100]}") # ════════ TEST 9: arena-v2 ════════ try: page.goto(f"{BASE}/weval-arena-v2.html", wait_until="domcontentloaded", timeout=20000) time.sleep(2) shot = str(OUT_DIR / "09-arena-v2.png") page.screenshot(path=shot, full_page=False) # Check web provider cards are present cards = page.locator('.prov-card').count() test("arena_v2_providers", "P" if cards >= 8 else "F", f"provider cards: {cards}/8", shot) except Exception as e: test("arena_v2_load", "F", f"exception: {str(e)[:100]}") # ════════ TEST 10: ethica page ════════ try: page.goto(f"{BASE}/ethica.html", wait_until="domcontentloaded", timeout=15000) time.sleep(1) shot = str(OUT_DIR / "10-ethica.png") page.screenshot(path=shot, full_page=False) title = page.title() test("ethica_reachable", "P" if title else "F", f"title: {title[:50]}", shot) except Exception as e: test("ethica_load", "F", f"exception: {str(e)[:100]}") # ════════ TEST 11: V85 Business KPI Premium (in tech platform) ════════ try: page.goto(f"{BASE}/weval-technology-platform.html", wait_until="domcontentloaded", timeout=60000) time.sleep(6) # wait for V85 async loader shot = str(OUT_DIR / "11-v85-biz-kpi-tech.png") page.screenshot(path=shot, full_page=True) # V85 summary IDs populated populated = 0 for id_ in ["v85-total-kpis", "v85-categories", "v85-live", "v85-warn", "v85-wire", "v85-completeness"]: try: el = page.locator(f"#{id_}") txt = el.inner_text(timeout=2000).strip() if txt and txt != "—" and txt != "": populated += 1 except: pass test("v85_summary_populated", "P" if populated >= 6 else "F", f"populated {populated}/6", shot) # V85 categories grid (7 expected) try: cat_cards = page.locator("#v85-categories-grid > div").count() test("v85_categories_grid", "P" if cat_cards >= 7 else "F", f"categories: {cat_cards}/7", shot) except Exception as e: test("v85_categories_grid", "F", f"exception: {str(e)[:100]}") # V85 sparklines (5 SVG expected) try: svgs = page.locator("#v85-highlights svg").count() test("v85_sparklines_svg", "P" if svgs >= 5 else "F", f"sparklines: {svgs}/5", shot) except Exception as e: test("v85_sparklines_svg", "F", f"exception: {str(e)[:100]}") # V85 title present try: v85t = page.locator("text=V85 Business KPI").count() test("v85_title_present", "P" if v85t >= 1 else "F", f"v85 titles: {v85t}", shot) except: test("v85_title_present", "F", "title not found") except Exception as e: test("v85_tech_platform_load", "F", f"exception: {str(e)[:100]}") # ════════ TEST 12: V83 Business KPI Dashboard standalone ════════ try: page.goto(f"{BASE}/business-kpi-dashboard.php", wait_until="domcontentloaded", timeout=45000) time.sleep(2) shot = str(OUT_DIR / "12-v83-biz-kpi-dashboard.png") page.screenshot(path=shot, full_page=True) # Count KPI cards kpi_cards = page.locator(".kpi").count() test("v83_kpi_cards", "P" if kpi_cards >= 50 else "F", f"kpi cards: {kpi_cards}/56", shot) cats = page.locator(".cat").count() test("v83_categories", "P" if cats >= 7 else "F", f"categories: {cats}/7", shot) except Exception as e: test("v83_dashboard_load", "F", f"exception: {str(e)[:100]}") browser.close() # Compute totals total = len(results["tests"]) passed = sum(1 for t in results["tests"] if t["status"] == "P") failed = total - passed results["pass"] = passed results["total"] = total results["fail"] = failed results["pct"] = int(100 * passed / total) if total else 0 # Write result with open(RESULT_JSON, 'w') as f: json.dump(results, f, indent=2) print(f"\n[PLAYWRIGHT VISUAL] {passed}/{total} PASS ({results['pct']}%)") print(f"Screenshots: {OUT_DIR}") print(f"Result: {RESULT_JSON}") return passed, total, failed if __name__ == "__main__": run()