const { chromium } = require("playwright"); const fs = require("node:fs"); const path = require("node:path"); const origin = "http://audit.local"; const mime = { ".js": "application/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png", ".html": "text/html", ".rsc": "text/x-component", ".json": "application/json" }; const routes = ["/streams", "/compose", "/inbox", "/approvals", "/connections", "/team", "/video", "/audit", "/onboarding", "/analytics", "/calendar", "/settings", "/more", "/search"]; async function run() { const browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 1, isMobile: true, hasTouch: true }); await page.route("**/*", async (route) => { const request = route.request(); const url = new URL(request.url()); if (url.hostname !== "audit.local") return route.abort(); let file; if (url.pathname.startsWith("/_next/")) file = path.join(process.cwd(), ".next", url.pathname.slice(7)); else if (url.pathname.startsWith("/brand/")) file = path.join(process.cwd(), "public", url.pathname); else { const name = url.pathname === "/" ? "index" : url.pathname.slice(1); const isRsc = request.headers().rsc === "1" || url.searchParams.has("_rsc"); file = path.join(process.cwd(), ".next/server/app", `${name}.${isRsc ? "rsc" : "html"}`); } if (!fs.existsSync(file)) return route.fulfill({ status: 404, body: "Not found" }); return route.fulfill({ status: 200, contentType: mime[path.extname(file)] || "application/octet-stream", body: fs.readFileSync(file) }); }); const results = []; for (const route of routes) { await page.goto(`${origin}${route}`, { waitUntil: "networkidle" }); await page.waitForTimeout(200); const result = await page.evaluate(() => { const width = document.documentElement.clientWidth; const all = Array.from(document.querySelectorAll("button, a[href], input, select, textarea, [role='tab']")); const undersized = all.filter((el) => { const rect = el.getBoundingClientRect(); const style = getComputedStyle(el); return rect.width > 0 && rect.height > 0 && rect.left < innerWidth && rect.right > 0 && rect.top < innerHeight && rect.bottom > 0 && style.visibility !== "hidden" && style.display !== "none" && (rect.width < 43.5 || rect.height < 43.5); }).map((el) => ({ tag: el.tagName, text: (el.getAttribute("aria-label") || el.textContent || "").trim().slice(0, 40), width: Math.round(el.getBoundingClientRect().width), height: Math.round(el.getBoundingClientRect().height) })); return { title: document.title, width, scrollWidth: document.documentElement.scrollWidth, bodyScrollWidth: document.body.scrollWidth, nav: !!document.querySelector("nav[aria-label='Primary navigation']"), undersized }; }); results.push({ route, ...result }); } await page.goto(`${origin}/more`, { waitUntil: "networkidle" }); const scroller = page.locator("[data-mobile-main]"); await scroller.evaluate((el) => { el.scrollTop = 300; }); const before = await scroller.evaluate((el) => el.scrollTop); await page.getByRole("navigation", { name: "Primary navigation" }).getByRole("link", { name: "Home" }).click(); await page.waitForURL(`${origin}/streams`); await page.getByRole("navigation", { name: "Primary navigation" }).getByRole("link", { name: "More" }).click(); await page.waitForURL(`${origin}/more`); await page.waitForTimeout(100); const after = await scroller.evaluate((el) => el.scrollTop); console.log(JSON.stringify({ routes: results, scrollPreservation: { before, after } }, null, 2)); await browser.close(); if (results.some((r) => r.scrollWidth > r.width || r.bodyScrollWidth > r.width || r.undersized.length > 0 || !r.nav) || before < 1 || Math.abs(before - after) > 1) process.exitCode = 1; } run().catch((error) => { console.error(error); process.exitCode = 1; });