// Entity-page QA screenshots → apps/web/qa/entities/-.png // node apps/web/qa/shoot-entities.mjs (all pages, 360/390/768/1280) // PAGES="facility,operator" WIDTHS=360,1280 THEME=dark node apps/web/qa/shoot-entities.mjs // EXTRA='name=/path,name2=/path2' adds ad-hoc pages. Reports horizontal overflow and HTTP status. import { createRequire } from "node:module"; import { mkdirSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const require = createRequire("/Users/simon-pierreboucher/Desktop/uqo-eval/package.json"); const { chromium } = require("playwright"); const out = resolve(dirname(fileURLToPath(import.meta.url)), "entities"); mkdirSync(out, { recursive: true }); const BASE = process.env.BASE ?? "http://localhost:8310"; const API = process.env.API ?? "http://127.0.0.1:8311/api/v1"; async function firstSlug(path, pick = (d) => d[0]?.slug) { try { const r = await fetch(`${API}${path}`); const j = await r.json(); return pick(Array.isArray(j.data) ? j.data : j.data?.items ?? []) ?? null; } catch { return null; } } const fac = process.env.FACILITY ?? (await firstSlug("/datacenters?per_page=1&sort=completeness&order=desc")); const op = process.env.OPERATOR ?? (await firstSlug("/operators?per_page=1&sort=facilities&order=desc")); const country = process.env.COUNTRY ?? "united-states"; const metro = process.env.METRO ?? (await firstSlug("/metros", (d) => [...d].sort((a, b) => b.facilityCount - a.facilityCount)[0]?.slug)); const project = process.env.PROJECT ?? (await firstSlug("/projects?per_page=1")); const ranking = process.env.RANKING ?? (await firstSlug("/rankings", (d) => (d.find((r) => r.total > 0) ?? d[0])?.key)); const cloud = process.env.CLOUD_REGION ?? (await firstSlug("/cloud-regions?per_page=1")); const ixp = process.env.IXP ?? (await firstSlug("/ixps?per_page=1")); const source = process.env.SOURCE ?? (await firstSlug("/sources", (d) => d[0]?.id)); const PAGES = [ ["facility", fac && `/datacenters/${fac}`], ["operators", "/operators"], ["operator", op && `/operators/${op}`], ["countries", "/countries"], ["country", `/countries/${country}`], ["markets", "/datacenters/markets"], ["market", metro && `/datacenters/markets/${metro}`], ["projects", "/projects"], ["project", project && `/projects/${project}`], ["rankings", "/rankings"], ["ranking", ranking && `/rankings/${ranking}`], ["cloud-regions", "/cloud-regions"], ["cloud-region", cloud && `/cloud-regions/${cloud}`], ["ixps", "/ixps"], ["ixp", ixp && `/ixps/${ixp}`], ["sources", "/sources"], ["source", source && `/sources/${source}`], ["methodology", "/methodology"], ["bot", "/bot"], ["notfound", "/datacenters/this-slug-does-not-exist"], ...(process.env.EXTRA ? process.env.EXTRA.split(",").map((s) => s.split("=")) : []), ].filter(([, p]) => p); const WIDTHS = (process.env.WIDTHS ?? "360,390,768,1280").split(",").map(Number); const THEME = process.env.THEME ?? "light"; const ONLY = process.env.PAGES ? new Set(process.env.PAGES.split(",")) : null; const suffix = THEME === "dark" ? "-dark" : ""; const browser = await chromium.launch(); const report = []; for (const w of WIDTHS) { const ctx = await browser.newContext({ viewport: { width: w, height: w < 700 ? 800 : 900 }, deviceScaleFactor: 1, isMobile: w < 700, hasTouch: w < 700 }); await ctx.addInitScript((theme) => { try { window.localStorage.setItem("theme", theme); } catch {} }, THEME); const page = await ctx.newPage(); for (const [name, path] of PAGES) { if (ONLY && !ONLY.has(name)) continue; let status = 0; const res = await page.goto(`${BASE}${path}`, { waitUntil: "load", timeout: 60000 }).catch((e) => console.log("goto failed", name, w, String(e).slice(0, 80))); if (res) status = res.status(); await page.waitForTimeout(1500); const overflow = await page.evaluate(() => { const doc = document.documentElement; const wide = []; for (const el of document.querySelectorAll("body *")) { const r = el.getBoundingClientRect(); if (r.right > doc.clientWidth + 1 && r.width > 0 && getComputedStyle(el).position !== "fixed") wide.push(`${el.tagName.toLowerCase()}.${(el.className && typeof el.className === "string" ? el.className : "").split(" ").slice(0, 2).join(".")}:${Math.round(r.right - doc.clientWidth)}`); } return { scrollWidth: doc.scrollWidth, clientWidth: doc.clientWidth, wide: wide.slice(0, 5) }; }); report.push({ w, name, status, overflow: overflow.scrollWidth > overflow.clientWidth ? overflow : "ok" }); await page.screenshot({ path: resolve(out, `${name}-${w}${suffix}.png`), fullPage: true }); } await ctx.close(); } await browser.close(); for (const r of report) console.log(r.w, r.name, r.status, typeof r.overflow === "string" ? r.overflow : JSON.stringify(r.overflow));