SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
3 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
5.5 KB · 108 lines javascript
Raw Blame History
1// Admin console sweep: node apps/web/qa/admin-sweep.mjs   (Playwright from ~/Desktop/uqo-eval)2// env: BASE (default http://localhost:8314), TOKEN (dev-admin-token), OUT (default qa/sweep-admin), PAGES=name,name, WIDTHS=390,12803// Logs in through /admin/login (httpOnly cookie), then screenshots every admin page and reports page-level horizontal4// overflow (wide tables must scroll inside their own container, never the page).5import { createRequire } from "node:module";6import { mkdirSync, writeFileSync } from "node:fs";7import { dirname, resolve } from "node:path";8import { fileURLToPath } from "node:url";910const require = createRequire("/Users/simon-pierreboucher/Desktop/uqo-eval/package.json");11const { chromium } = require("playwright");12const here = dirname(fileURLToPath(import.meta.url));13const OUT = process.env.OUT ?? resolve(here, "sweep-admin");14mkdirSync(OUT, { recursive: true });15const BASE = process.env.BASE ?? "http://localhost:8314";16const TOKEN = process.env.TOKEN ?? "dev-admin-token";17const DOC = process.env.DOC ?? "doc_8b4ef3d4844a957b";18const RUN = process.env.RUN ?? "run_4ke18ugp5yd3";1920const PAGES = [21  ["connectors", "/admin/connectors"],22  ["connectors-full", "/admin/connectors?cols=full"],23  ["quality", "/admin/quality"],24  ["data-gaps", "/admin/data-gaps"],25  ["matches", "/admin/matches"],26  ["projects", "/admin/projects"],27  ["document-trace", `/admin/documents/${DOC}?tab=trace`],28  ["document-fixture", `/admin/documents/${DOC}?fixture=1`],29  ["run", `/admin/runs/${RUN}`],30];31const ONLY = process.env.PAGES ? new Set(process.env.PAGES.split(",")) : null;32const WIDTHS = (process.env.WIDTHS ?? "390,1280").split(",").map(Number);3334function audit() {35  const doc = document.documentElement;36  const vw = doc.clientWidth;37  const wide = [];38  for (const el of document.querySelectorAll("body *")) {39    const r = el.getBoundingClientRect();40    if (r.width > 0 && r.right > vw + 1 && getComputedStyle(el).position !== "fixed") {41      // inside a horizontally scrolling container is fine42      let p = el.parentElement;43      let scrolls = false;44      while (p && p !== document.body) {45        const cs = getComputedStyle(p);46        if ((cs.overflowX === "auto" || cs.overflowX === "scroll") && p.scrollWidth > p.clientWidth + 1) {47          scrolls = true;48          break;49        }50        p = p.parentElement;51      }52      if (!scrolls) wide.push(`${el.tagName.toLowerCase()}.${(typeof el.className === "string" ? el.className : "").split(" ").filter(Boolean).slice(0, 3).join(".")}:+${Math.round(r.right - vw)}`);53    }54  }55  const overflow = doc.scrollWidth > doc.clientWidth ? { scrollWidth: doc.scrollWidth, clientWidth: doc.clientWidth, wide: wide.slice(0, 6) } : null;56  const h1 = document.querySelectorAll("h1").length;57  const alerts = Array.from(document.querySelectorAll("[role=alert]")).map((a) => a.textContent.trim().slice(0, 120));58  return { overflow, h1, alerts, height: doc.scrollHeight, title: document.title };59}6061const browser = await chromium.launch();62const results = [];63for (const w of WIDTHS) {64  const mobile = w < 700;65  const ctx = await browser.newContext({ viewport: { width: w, height: mobile ? 844 : 900 }, deviceScaleFactor: 1, isMobile: mobile, hasTouch: mobile, locale: "en-CA", timezoneId: "America/Toronto" });66  const page = await ctx.newPage();67  // login68  await page.goto(`${BASE}/admin/login`, { waitUntil: "load", timeout: 90000 });69  await page.fill('input[name="token"]', TOKEN);70  await page.click('button[type="submit"]');71  await page.waitForURL((u) => u.pathname.startsWith("/admin") && !u.pathname.startsWith("/admin/login"), { timeout: 60000 }).catch((e) => console.log("login failed", String(e).slice(0, 100)));72  for (const [name, path] of PAGES) {73    if (ONLY && !ONLY.has(name)) continue;74    const errors = [];75    const onConsole = (m) => {76      if (m.type() === "error") errors.push(m.text().slice(0, 200));77    };78    page.on("console", onConsole);79    const row = { name, path, w };80    try {81      const resp = await page.goto(`${BASE}${path}`, { waitUntil: "load", timeout: 90000 });82      row.status = resp?.status();83      await page.waitForLoadState("networkidle", { timeout: 20000 }).catch(() => {});84      await page.waitForTimeout(name.startsWith("connectors") ? 6000 : 1200);85      await page.waitForFunction(() => !document.querySelector(".animate-pulse-soft"), null, { timeout: 40000 }).catch(() => {});86      Object.assign(row, await page.evaluate(audit));87      await page.screenshot({ path: resolve(OUT, `${name}-${w}.png`), fullPage: row.height < 16000, timeout: 90000 });88    } catch (e) {89      row.error = String(e).slice(0, 200);90    }91    page.off("console", onConsole);92    row.errors = [...new Set(errors)];93    results.push(row);94    const flags = [];95    if (row.overflow) flags.push(`OVERFLOW ${JSON.stringify(row.overflow.wide)}`);96    if (row.h1 !== 1) flags.push(`h1×${row.h1}`);97    if (row.alerts?.length) flags.push(`alerts:${row.alerts.length}`);98    if (row.errors.length) flags.push(`console:${row.errors.length}`);99    if (row.error) flags.push("ERROR");100    console.log(`${String(w).padStart(4)} ${name.padEnd(18)} ${String(row.status ?? "").padEnd(3)} h=${String(row.height ?? "").padStart(5)} ${flags.length ? flags.join(" ") : "ok"}`);101    if (row.alerts?.length) for (const a of row.alerts) console.log(`       alert: ${a}`);102  }103  await ctx.close();104}105await browser.close();106writeFileSync(resolve(OUT, "results.json"), JSON.stringify({ base: BASE, at: new Date().toISOString(), results }, null, 1));107console.log(`wrote ${OUT}/results.json`);108