Files
weval-l99/l99-playwright-visual.py.GOLD-20260422-003312-pre-shot-fix
2026-04-24 04:38:58 +02:00

352 lines
15 KiB
Python
Executable File

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