Files
weval-l99/wevia-ux-agent.py
2026-04-13 12:43:21 +02:00

156 lines
7.8 KiB
Python

import asyncio, json, os
from playwright.async_api import async_playwright
from datetime import datetime
RESULTS = "/var/www/html/api/wevia-ux-audit.json"
async def run():
results = {"date": datetime.now().isoformat(), "checks": [], "passed": 0, "failed": 0, "warnings": 0}
async with async_playwright() as p:
b = await p.chromium.launch(headless=True, args=["--no-sandbox"])
for viewport, label in [
({"width": 1400, "height": 900}, "desktop"),
({"width": 375, "height": 812}, "mobile"),
({"width": 768, "height": 1024}, "tablet"),
]:
pg = await b.new_page(viewport=viewport)
errs = []
pg.on("pageerror", lambda e: errs.append(str(e)[:60]))
await pg.goto("https://weval-consulting.com/wevia-master.html", timeout=15000)
await pg.wait_for_timeout(5000)
# === 1. OVERFLOW / HORIZONTAL SCROLL ===
has_hscroll = await pg.evaluate("document.documentElement.scrollWidth > document.documentElement.clientWidth")
add(results, f"{label}:no_hscroll", not has_hscroll, "horizontal overflow" if has_hscroll else "ok")
# === 2. ELEMENT OVERLAP DETECTION ===
overlaps = await pg.evaluate("""(function(){
var els = document.querySelectorAll('.wcard,.sb-item,.msg,.input-wrap,button,.send-btn,.voice-btn,textarea,h1,h2,h3');
var rects = [];
els.forEach(function(el){
var r = el.getBoundingClientRect();
if(r.width>0 && r.height>0) rects.push({tag:el.tagName+'.'+el.className.split(' ')[0],x:r.x,y:r.y,w:r.width,h:r.height});
});
var overlaps = [];
for(var i=0;i<rects.length;i++){
for(var j=i+1;j<rects.length;j++){
var a=rects[i], b=rects[j];
if(!(a.x+a.w<b.x || b.x+b.w<a.x || a.y+a.h<b.y || b.y+b.h<a.y)){
var ox = Math.min(a.x+a.w,b.x+b.w)-Math.max(a.x,b.x);
var oy = Math.min(a.y+a.h,b.y+b.h)-Math.max(a.y,b.y);
if(ox>5 && oy>5) overlaps.push(a.tag+' x '+b.tag);
}
}
}
return overlaps.slice(0,5);
})()""")
add(results, f"{label}:no_overlaps", len(overlaps) == 0, str(overlaps) if overlaps else "ok")
# === 3. CONTRAST CHECK (text vs background) ===
contrast_issues = await pg.evaluate("""(function(){
var issues = [];
var els = document.querySelectorAll('h1,h2,h3,p,span,li,td,th,code,strong,a,.sb-item,.wcard');
els.forEach(function(el){
var cs = getComputedStyle(el);
var color = cs.color;
var bg = cs.backgroundColor;
if(!color || !bg || bg==='rgba(0, 0, 0, 0)' || bg==='transparent') return;
// Parse rgb values
function parse(c){var m=c.match(/(\d+)/g);return m?m.map(Number):[0,0,0];}
var fg = parse(color);
var bgc = parse(bg);
// Luminance
function lum(r,g,b){var a=[r,g,b].map(function(v){v/=255;return v<=0.03928?v/12.92:Math.pow((v+0.055)/1.055,2.4);});return 0.2126*a[0]+0.7152*a[1]+0.0722*a[2];}
var l1 = lum(fg[0],fg[1],fg[2]);
var l2 = lum(bgc[0],bgc[1],bgc[2]);
var ratio = (Math.max(l1,l2)+0.05)/(Math.min(l1,l2)+0.05);
if(ratio < 3) issues.push(el.tagName+': ratio '+ratio.toFixed(1)+' ('+color+' on '+bg+')');
});
return issues.slice(0,5);
})()""")
add(results, f"{label}:contrast>=3", len(contrast_issues) == 0, str(contrast_issues) if contrast_issues else "ok")
# === 4. TEXT VISIBILITY (no 0-size, no hidden) ===
hidden_text = await pg.evaluate("""(function(){
var issues = [];
document.querySelectorAll('h1,h2,h3,p,span,li,.sb-item').forEach(function(el){
var cs = getComputedStyle(el);
var r = el.getBoundingClientRect();
if(el.textContent.trim().length>0 && (r.width<2 || r.height<2 || cs.opacity==='0' || cs.visibility==='hidden'))
issues.push(el.tagName+': invisible ('+r.width+'x'+r.height+')');
});
return issues.slice(0,5);
})()""")
add(results, f"{label}:text_visible", len(hidden_text) == 0, str(hidden_text) if hidden_text else "ok")
# === 5. CENTERING CHECKS ===
centers = await pg.evaluate("""(function(){
var checks = {};
var main = document.querySelector('.main');
if(!main) return {error:'no .main'};
var mB = main.getBoundingClientRect();
var mc = mB.x + mB.width/2;
['.input-wrap','.welcome','.wx-grid'].forEach(function(sel){
var el = document.querySelector(sel);
if(el){
var eB = el.getBoundingClientRect();
checks[sel] = Math.round(Math.abs((eB.x+eB.width/2) - mc));
}
});
return checks;
})()""")
for sel, offset in (centers if isinstance(centers, dict) else {}).items():
if sel != 'error':
add(results, f"{label}:center:{sel}", offset < 20, f"{offset}px")
# === 6. FONT CONSISTENCY ===
fonts = await pg.evaluate("""(function(){
var fonts = {};
document.querySelectorAll('h1,h2,p,.sb-item,.wcard,textarea,button').forEach(function(el){
var f = getComputedStyle(el).fontFamily.split(',')[0].trim().replace(/['"]/g,'');
fonts[f] = (fonts[f]||0)+1;
});
return fonts;
})()""")
add(results, f"{label}:font_consistency", len(fonts) <= 3, f"{len(fonts)} fonts: {json.dumps(fonts)[:80]}")
# === 7. JS ERRORS ===
add(results, f"{label}:js_errors=0", len(errs) == 0, f"{len(errs)} errors")
# === 8. CLICKABLE ELEMENTS ACCESSIBLE ===
if label == "mobile":
small_btns = await pg.evaluate("""(function(){
var issues = [];
document.querySelectorAll('button,a,.sb-item,.wcard').forEach(function(el){
var r = el.getBoundingClientRect();
if(r.width>0 && r.height>0 && (r.width<30 || r.height<30))
issues.push(el.tagName+'.'+el.className.split(' ')[0]+': '+Math.round(r.width)+'x'+Math.round(r.height));
});
return issues.slice(0,5);
})()""")
add(results, f"{label}:touch_targets>=30px", len(small_btns) == 0, str(small_btns) if small_btns else "ok")
await pg.close()
await b.close()
results["total"] = results["passed"] + results["failed"] + results["warnings"]
results["score"] = f"{results['passed']}/{results['total']}"
with open(RESULTS, "w") as f:
json.dump(results, f, indent=2, ensure_ascii=False)
print(f"Score: {results['score']}")
for c in results["checks"]:
s = "" if c["ok"] else ("⚠️" if c.get("warn") else "")
print(f" {s} {c['name']:35s} {c['detail'][:60]}")
def add(results, name, ok, detail, warn=False):
results["checks"].append({"name": name, "ok": ok, "detail": detail, "warn": warn})
if ok: results["passed"] += 1
elif warn: results["warnings"] += 1
else: results["failed"] += 1
asyncio.run(run())