SPB Git

spb/ka-ui

Public
5.4 KB · 115 lines javascript
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 out = { overflow: Math.max(document.documentElement.scrollWidth - iw,37    document.body ? document.body.scrollWidth - iw : 0), smallInputs: 0, viewport: null };38  const mv = document.querySelector('meta[name="viewport"]');39  out.viewport = mv ? mv.getAttribute("content") : null;40  for (const el of document.querySelectorAll("input, select, textarea")) {41    if (el.type === "hidden" || el.type === "checkbox" || el.type === "radio") continue;42    const r = el.getBoundingClientRect();43    if (r.width < 4 || r.height < 4) continue;44    if (parseFloat(getComputedStyle(el).fontSize) < 16) out.smallInputs++;45  }46  return out;47})()`;4849const SCROLLBAR = `(async () => {50  const iw = window.innerWidth, vh = window.innerHeight;51  let bar = null;52  for (const el of document.querySelectorAll("body *")) {53    const cs = getComputedStyle(el);54    if (cs.position !== "fixed" || cs.display === "none") continue;55    const r = el.getBoundingClientRect();56    if (r.height >= 8 && r.bottom > vh - 120 && r.top > vh * 0.4 && r.width >= iw * 0.6) { bar = el; break; }57  }58  if (!bar) return null;59  const pos = [];60  for (let i = 0; i < 3; i++) {61    window.scrollBy(0, 700);62    await new Promise((r) => setTimeout(r, 320));63    const r = bar.getBoundingClientRect();64    pos.push({ off: Math.round(innerHeight - r.bottom), vis: r.height > 0 && getComputedStyle(bar).display !== "none" });65  }66  window.scrollTo(0, 0);67  return pos;68})()`;6970const launcher = engineName === "webkit" ? webkit : chromium;71const browser = await launcher.launch();72let failures = 0;73const rows = [];74for (const site of sites) {75  for (const c of cases) {76    const ctx = await browser.newContext({77      viewport: { width: c.w, height: c.h },78      isMobile: c.w < 800, hasTouch: true,79      userAgent: engineName === "webkit"80        ? undefined81        : "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Mobile Safari/537.36",82    });83    const page = await ctx.newPage();84    const label = `${site.id} ${c.w}×${c.h} (${c.tag})`;85    const errs = [];86    try {87      const resp = await page.goto(`https://${site.domain}/`, { waitUntil: "domcontentloaded", timeout: 40000 });88      if (!resp || resp.status() !== 200) errs.push(`HTTP ${resp ? resp.status() : "??"}`);89      await page.waitForLoadState("networkidle", { timeout: 15000 }).catch(() => {});90      await page.waitForTimeout(900);91      const m = await page.evaluate(CHECK);92      if (m.overflow > 1) errs.push(`débordement +${m.overflow}px`);93      if (!m.viewport || !/width=device-width/.test(m.viewport)) errs.push("meta viewport invalide");94      if (m.smallInputs > 0) errs.push(`${m.smallInputs} champ(s) <16px`);95      const track = await page.evaluate(SCROLLBAR).catch(() => null);96      if (track) {97        const offs = track.map((p) => p.off);98        if (Math.max(...offs) - Math.min(...offs) > 2) errs.push(`barre basse instable (${Math.max(...offs) - Math.min(...offs)}px)`);99        if (track.some((p) => !p.vis)) errs.push("barre basse disparaît au scroll");100      }101      await page.screenshot({ path: join(outDir, `${site.id}-${c.w}x${c.h}.png`) });102    } catch (e) {103      errs.push(String(e).split("\n")[0].slice(0, 90));104    }105    if (errs.length) failures++;106    rows.push({ site: site.id, case: `${c.w}x${c.h}`, tag: c.tag, errs });107    console.log(`${errs.length ? "❌" : "✅"} ${label}${errs.length ? " — " + errs.join(" · ") : ""}`);108    await ctx.close();109  }110}111await browser.close();112writeFileSync(join(outDir, "last-run.json"), JSON.stringify({ date: new Date().toISOString(), engine: engineName, rows }, null, 2));113console.log(`\n${rows.length} cas, ${failures} échec(s). Captures + last-run.json : ${outDir}`);114process.exit(failures ? 1 : 0);115
Files · 56447b82d77ec66415fa3458ec72294bcaf7ac5e
Menu

Theme