// Mobile audit at iPhone 13 width (390x844). Visit every scene; assert // no horizontal scroll, no element clipping past viewport edge, no // stacked-on-top layout breakage. The user said "a regional store // manager could use every system effectively" — most aren't on desktop. import { chromium } from "playwright"; const args = [ "--host-resolver-rules=MAP canvas.flow-master.ai 65.21.71.186", "--ignore-certificate-errors", "--window-size=390,844", ]; const b = await chromium.launch({ headless: true, args }); const ctx = await b.newContext({ viewport: { width: 390, height: 844 }, userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15", isMobile: true, hasTouch: true, deviceScaleFactor: 3, }); const p = await ctx.newPage(); const results = []; function record(name, ok, detail = "") { results.push({ name, ok, detail }); console.log(`${ok ? "PASS" : "FAIL"} ${name}${detail ? " — " + detail : ""}`); } async function noHorizontalScroll(scene) { const { sw, vw } = await p.evaluate(() => ({ sw: document.documentElement.scrollWidth, vw: window.innerWidth })); record(`mobile_${scene}_no_horizontal_overflow`, sw <= vw + 2, `scroll=${sw}, viewport=${vw}`); } async function noOverflowingElements(scene) { const offending = await p.evaluate(() => { const out = []; // Walk every element; report any whose computed width is >= 600 (much // wider than the 390 viewport) AND whose offsetParent is the document. document.querySelectorAll("*").forEach((el) => { const r = el.getBoundingClientRect(); if (r.width >= 600 && r.right > window.innerWidth + 2) { out.push({ tag: el.tagName, cls: (el.className || "").toString().slice(0, 40), width: Math.round(r.width), right: Math.round(r.right), }); } }); return out.slice(0, 10); }); record( `mobile_${scene}_no_element_clipping`, offending.length === 0, offending.length ? offending.map((o) => `${o.tag}.${o.cls}@w${o.width}`).join(", ") : "clean" ); } // Pre-seed dev-login token + a CEO session into localStorage / sessionStorage // before we ever navigate. That avoids the auth-guard navigation that resets // the viewport on some Playwright versions. const tokenRes = await ctx.request.post("https://canvas.flow-master.ai/api/v1/auth/dev-login", { data: { email: "ceo-head@flow-master.ai" }, }); const tokenBody = await tokenRes.json(); const accessToken = tokenBody?.access_token; console.log(`[seed token]: ${accessToken ? "OK" : "FAIL"}`); await ctx.addInitScript((tok) => { sessionStorage.setItem("fm.mc.token.v1", tok); localStorage.setItem("fm.mc.prefs.v1", JSON.stringify({ email: "ceo-head@flow-master.ai" })); }, accessToken); await p.goto("https://canvas.flow-master.ai/", { waitUntil: "networkidle" }); await p.waitForTimeout(1500); await noHorizontalScroll("landing_pre_login"); await noOverflowingElements("landing_pre_login"); for (const [chip, scene] of [ [/Approvals queue/, "approvals"], [/Procurement Hub/, "procurement_hub"], [/Attendance Map/, "geo"], [/Command Assistant/, "assistant"], [/Team Chat/, "chat"], [/What is FlowMaster/, "explainer"], ]) { const found = await p.locator(".hub-chip", { hasText: chip }).count(); if (found === 0) { record(`mobile_${scene}_no_horizontal_overflow`, false, "chip not visible — auth state lost or landing not hydrated"); continue; } await p.locator(".hub-chip", { hasText: chip }).click(); await p.waitForTimeout(1500); await noHorizontalScroll(scene); await noOverflowingElements(scene); await p.goBack().catch(() => {}); await p.waitForTimeout(600); } const fails = results.filter((r) => !r.ok); console.log(`\n=== mobile audit: ${results.length - fails.length}/${results.length} passed ===`); if (fails.length) { console.log("FAILED:"); fails.forEach((f) => console.log(` - ${f.name}: ${f.detail}`)); } await b.close(); process.exit(fails.length === 0 ? 0 : 1);