SPB Git forge

spb/house-ka

Public
18commits 1branches 0releases
1.9 MBsize
maindefault branch
19 days agolast push
Python 67% TypeScript 18.2% CSS 14.4%
7.5 KB · 167 lines python
Raw Blame History
1# -----------------------------------------------------------------------------2# Immo-Ka — Agrégateur de maisons à vendre (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# scripts/valider_rendu.py : validation automatisée du rendu d'affichage.5#6#   Matrice largeurs × pages (Playwright/Chromium) : pour chaque combinaison —7#   · AUCUN débordement horizontal (scrollWidth <= largeur de la fenêtre)8#   · aucune image cassée parmi les images chargées (naturalWidth > 0)9#   · cibles tactiles : liens/boutons visibles < 40 px de haut signalés10#   · erreurs console JavaScript11#   · capture d'écran pleine page (régression visuelle : comparer les runs)12#13#   Usage :  python3 scripts/valider_rendu.py [BASE_URL] [DOSSIER_SORTIE]14#   Par défaut : http://localhost:18096 (tunnel SSH vers M4M64a:8096) et15#   docs/screenshots/validation-<date>/ . Code retour 1 si un échec bloquant.16# -----------------------------------------------------------------------------17from __future__ import annotations1819import json20import sys21import time22import urllib.request23from datetime import date24from pathlib import Path2526from playwright.sync_api import sync_playwright2728BASE = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:18096"29OUT = Path(sys.argv[2] if len(sys.argv) > 2 else30           f"docs/screenshots/validation-{date.today().isoformat()}")3132# matrice de largeurs : smartphones (portrait), paysage mobile, tablette, desktop33VIEWPORTS = [34    ("mobile-360", 360, 780, True),35    ("mobile-390", 390, 844, True),36    ("mobile-414", 414, 896, True),37    ("paysage-844", 844, 390, True),38    ("tablette-768", 768, 1024, True),39    ("desktop-1024", 1024, 768, False),40    ("desktop-1440", 1440, 900, False),41]424344def _api(path: str):45    with urllib.request.urlopen(BASE + path, timeout=30) as r:46        return json.loads(r.read().decode())474849def pick_cases() -> list[tuple[str, str]]:50    """Pages à tester — routes fixes + cas limites réels choisis dans l'API."""51    pages: list[tuple[str, str]] = [52        ("accueil", "/"),53        ("recherche-carte", "/?view=carte"),54        ("recherche-sans-resultat", "/?q=zzzz-introuvable-zzz"),55        ("agences", "/agences"),56        ("stats", "/stats"),57        ("contact", "/contact"),58    ]59    try:60        ls = _api("/api/listings?limit=500")["listings"]61        by_imgs = sorted(ls, key=lambda l: len(l.get("images") or []))62        cases = {63            "fiche-tres-photographiee": by_imgs[-1],64            "fiche-1-photo": next((l for l in by_imgs if len(l.get("images") or []) <= 1), None),65            "fiche-desc-longue": max(ls, key=lambda l: len(l.get("description") or "")),66            "fiche-prix-eleve": max(ls, key=lambda l: l.get("price") or 0),67            "fiche-sans-superficie": next((l for l in ls if l.get("area_sqft") is None), None),68        }69        from urllib.parse import quote70        for name, l in cases.items():71            if l:72                pages.append((name, f"/propriete/{quote(l['uid'], safe='')}"))73    except Exception as exc:74        print(f"⚠ cas limites non échantillonnés ({exc}) — routes fixes seulement")75    return pages767778def main() -> int:79    OUT.mkdir(parents=True, exist_ok=True)80    pages = pick_cases()81    rows = []82    failures = 083    with sync_playwright() as p:84        # --use-gl=angle : WebGL2 requis par la carte Mapbox (Ka Maps) en headless85        browser = p.chromium.launch(args=["--use-gl=angle"])86        for vp_name, w, h, mobile in VIEWPORTS:87            ctx = browser.new_context(88                viewport={"width": w, "height": h},89                is_mobile=mobile, has_touch=mobile,90                device_scale_factor=2 if mobile else 1,91                user_agent=("Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) "92                            "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 "93                            "Mobile/15E148 Safari/604.1") if mobile else None,94            )95            page = ctx.new_page()96            console_errors: list[str] = []97            page.on("console", lambda m: console_errors.append(m.text)98                    if m.type == "error" else None)99            for page_name, path in pages:100                console_errors.clear()101                try:102                    page.goto(BASE + path, wait_until="networkidle", timeout=45000)103                except Exception:104                    pass                       # networkidle peut expirer (carte) — on évalue quand même105                time.sleep(1.2)106                m = page.evaluate("""() => {107                  const d = document.documentElement;108                  const overflow = Math.max(d.scrollWidth, document.body.scrollWidth)109                                   - (window.innerWidth + 1);110                  let broken = 0, loaded = 0;111                  for (const img of document.images) {112                    if (!img.complete) continue;113                    loaded++;114                    if (img.naturalWidth === 0 && img.src.startsWith('http')) broken++;115                  }116                  let smallTargets = 0;117                  for (const el of document.querySelectorAll('a,button')) {118                    const r = el.getBoundingClientRect();119                    if (r.width > 0 && r.height > 0 && r.height < 40 && r.width < 40)120                      smallTargets++;121                  }122                  return {overflow: Math.max(0, overflow), broken, loaded, smallTargets};123                }""")124                shot = OUT / f"{page_name}--{vp_name}.png"125                try:126                    page.screenshot(path=str(shot), full_page=(page_name != "recherche-carte"))127                except Exception:128                    page.screenshot(path=str(shot))129                errs = [e for e in console_errors130                        if "favicon" not in e and "maplibre" not in e.lower()]131                ok = m["overflow"] == 0 and m["broken"] == 0132                if not ok:133                    failures += 1134                rows.append({135                    "page": page_name, "viewport": vp_name, "ok": ok,136                    "debordement_px": m["overflow"], "img_cassees": m["broken"],137                    "img_chargees": m["loaded"], "cibles_petites": m["smallTargets"],138                    "erreurs_js": len(errs),139                })140                flag = "OK " if ok else "ÉCHEC"141                print(f"[{flag}] {page_name:28s} {vp_name:12s} "142                      f"overflow={m['overflow']}px imgs cassées={m['broken']} "143                      f"petites cibles={m['smallTargets']} jsErr={len(errs)}")144            ctx.close()145        browser.close()146147    # rapport markdown148    md = ["# Rapport de validation du rendu — Immo-Ka",149          f"\nDate : {date.today().isoformat()} · Base : {BASE}",150          f"\nCombinaisons testées : {len(rows)} — échecs : {failures}\n",151          "| Page | Largeur | Statut | Débordement | Img cassées | Petites cibles | Erreurs JS |",152          "|---|---|---|---|---|---|---|"]153    for r in rows:154        md.append(f"| {r['page']} | {r['viewport']} | "155                  f"{'✅ OK' if r['ok'] else '❌ ÉCHEC'} | {r['debordement_px']} px | "156                  f"{r['img_cassees']}/{r['img_chargees']} | {r['cibles_petites']} | "157                  f"{r['erreurs_js']} |")158    (OUT / "rapport-validation.md").write_text("\n".join(md), encoding="utf-8")159    (OUT / "rapport-validation.json").write_text(json.dumps(rows, indent=1),160                                                 encoding="utf-8")161    print(f"\nRapport : {OUT}/rapport-validation.md — {failures} échec(s)")162    return 1 if failures else 0163164165if __name__ == "__main__":166    sys.exit(main())167