SPB Git forge

spb/ka-ui

Public
30commits 1branches 0releases
145.7 MBsize
maindefault branch
27 days agolast push
Python 33.5% JavaScript 30.1% TypeScript 25% CSS 10% Shell 1.4%
5.6 KB · 123 lines javascript
Raw Blame History
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// ka-ui/regression-mobile.mjs — RÉGRESSION mobile des 13 sites Groupe KA.3// À lancer à CHAQUE mise à jour d'un site (rapide : ~3 min) :4//   cd ~/Desktop/ka-ui && node regression-mobile.mjs [site…] [--engine=webkit]5// Matrice : 360 / 390 / 414 / 768 px, portrait ET paysage. Pour chaque cas :6//   · capture d'écran (regression/<site>-<l>x<h>.png)7//   · ÉCHEC si débordement horizontal (> 1 px)8//   · ÉCHEC si meta viewport absente ou sans width=device-width9//   · ÉCHEC si champ visible < 16 px (zoom iOS)10//   · ÉCHEC si une barre basse fixe (pleine largeur) dérive ou disparaît au scroll11// Code de sortie ≠ 0 s'il y a au moins un échec → utilisable en CI/cron.12import { chromium, webkit } from "playwright";13import { mkdirSync, readFileSync, writeFileSync } from "fs";14import { dirname, join } from "path";15import { fileURLToPath } from "url";1617const here = dirname(fileURLToPath(import.meta.url));18const eco = JSON.parse(readFileSync(join(here, "ecosystem.json"), "utf8"));19const outDir = join(here, "regression");20mkdirSync(outDir, { recursive: true });2122const args = process.argv.slice(2);23const engineName = (args.find((a) => a.startsWith("--engine=")) || "--engine=chromium").split("=")[1];24const only = args.filter((a) => !a.startsWith("--"));25const sites = eco.sites.filter((s) => !only.length || only.includes(s.id));2627const widths = [360, 390, 414, 768];28const cases = [];29for (const w of widths) {30  cases.push({ w, h: Math.round(w * 2.05), tag: "portrait" });31  if (w === 390 || w === 768) cases.push({ w: Math.round(w * 2.05), h: w, tag: "paysage" });32}3334const CHECK = `(() => {35  const iw = window.innerWidth;36  const raw = Math.max(document.documentElement.scrollWidth - iw,37    document.body ? document.body.scrollWidth - iw : 0);38  let overflow = 0;39  if (raw > 1) { // scrollWidth ment quand overflow-x:clip — tester le défilement réel40    const x0 = window.scrollX;41    window.scrollBy(raw, 0);42    if (window.scrollX - x0 > 1) overflow = raw;43    window.scrollTo(x0, 0);44  }45  const out = { overflow, smallInputs: 0, viewport: null };46  const mv = document.querySelector('meta[name="viewport"]');47  out.viewport = mv ? mv.getAttribute("content") : null;48  for (const el of document.querySelectorAll("input, select, textarea")) {49    if (el.type === "hidden" || el.type === "checkbox" || el.type === "radio") continue;50    const r = el.getBoundingClientRect();51    if (r.width < 4 || r.height < 4) continue;52    if (parseFloat(getComputedStyle(el).fontSize) < 16) out.smallInputs++;53  }54  return out;55})()`;5657const SCROLLBAR = `(async () => {58  const iw = window.innerWidth, vh = window.innerHeight;59  let bar = null;60  for (const el of document.querySelectorAll("body *")) {61    const cs = getComputedStyle(el);62    if (cs.position !== "fixed" || cs.display === "none") continue;63    const r = el.getBoundingClientRect();64    if (r.height >= 8 && r.bottom > vh - 120 && r.top > vh * 0.4 && r.width >= iw * 0.6) { bar = el; break; }65  }66  if (!bar) return null;67  const pos = [];68  for (let i = 0; i < 3; i++) {69    window.scrollBy(0, 700);70    await new Promise((r) => setTimeout(r, 320));71    const r = bar.getBoundingClientRect();72    pos.push({ off: Math.round(innerHeight - r.bottom), vis: r.height > 0 && getComputedStyle(bar).display !== "none" });73  }74  window.scrollTo(0, 0);75  return pos;76})()`;7778const launcher = engineName === "webkit" ? webkit : chromium;79const browser = await launcher.launch();80let failures = 0;81const rows = [];82for (const site of sites) {83  for (const c of cases) {84    const ctx = await browser.newContext({85      viewport: { width: c.w, height: c.h },86      isMobile: c.w < 800, hasTouch: true,87      userAgent: engineName === "webkit"88        ? undefined89        : "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Mobile Safari/537.36",90    });91    const page = await ctx.newPage();92    const label = `${site.id} ${c.w}×${c.h} (${c.tag})`;93    const errs = [];94    try {95      const resp = await page.goto(`https://${site.domain}/`, { waitUntil: "domcontentloaded", timeout: 40000 });96      if (!resp || resp.status() !== 200) errs.push(`HTTP ${resp ? resp.status() : "??"}`);97      await page.waitForLoadState("networkidle", { timeout: 15000 }).catch(() => {});98      await page.waitForTimeout(900);99      const m = await page.evaluate(CHECK);100      if (m.overflow > 1) errs.push(`débordement +${m.overflow}px`);101      if (!m.viewport || !/width=device-width/.test(m.viewport)) errs.push("meta viewport invalide");102      if (m.smallInputs > 0) errs.push(`${m.smallInputs} champ(s) <16px`);103      const track = await page.evaluate(SCROLLBAR).catch(() => null);104      if (track) {105        const offs = track.map((p) => p.off);106        if (Math.max(...offs) - Math.min(...offs) > 2) errs.push(`barre basse instable (${Math.max(...offs) - Math.min(...offs)}px)`);107        if (track.some((p) => !p.vis)) errs.push("barre basse disparaît au scroll");108      }109      await page.screenshot({ path: join(outDir, `${site.id}-${c.w}x${c.h}.png`) });110    } catch (e) {111      errs.push(String(e).split("\n")[0].slice(0, 90));112    }113    if (errs.length) failures++;114    rows.push({ site: site.id, case: `${c.w}x${c.h}`, tag: c.tag, errs });115    console.log(`${errs.length ? "❌" : "✅"} ${label}${errs.length ? " — " + errs.join(" · ") : ""}`);116    await ctx.close();117  }118}119await browser.close();120writeFileSync(join(outDir, "last-run.json"), JSON.stringify({ date: new Date().toISOString(), engine: engineName, rows }, null, 2));121console.log(`\n${rows.length} cas, ${failures} échec(s). Captures + last-run.json : ${outDir}`);122process.exit(failures ? 1 : 0);123