156 lines
7.8 KiB
Python
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())
|