/* * Sideways scroll check for phones. Not part of the gate, run by hand on the local stack (see README, "Overflow check"). * A Playwright function, run with the Playwright MCP tool browser_run_code_unsafe (filename = this file) or * `playwright-cli run-code`. Log in as an admin in that browser first. A logged-in browser redirects the logged-out * pages, so check those in a second run with LOGGED_OUT = true in a clean browser (no session). * A hit is: scrollWidth != clientWidth, an element sticking out on the left (never raises scrollWidth), or a * redirect away from the page. * Content clipped inside an overflow-hidden ancestor is not checked (same rule as scrollWidth). */ async (page) => { const BASE = 'http://localhost:5173'; const WIDTHS = [360, 390, 412, 414]; const FONT_PERCENT = 100; // set 130 for the large system font run const DARK = false; // set true for dark mode const SKIN = ''; // e.g. 'pastel' to check a skin const LOGGED_OUT = false; // true: only the logged-out pages, run in a browser without a session const GAME_ID = 620; // any appid in the library const PROFILE_NAME = 'Dev'; // display name of the logged-in user, the profile must be public const PAGES = LOGGED_OUT ? ['/', '/login', '/register', '/reset-password', '/impressum', `/u/${encodeURIComponent(PROFILE_NAME)}`] : [ '/dashboard', '/library', `/library/${GAME_ID}`, '/wishlist', '/play-next', '/stats', '/import', '/points', '/settings', '/whats-new', '/welcome', '/audit/games', '/audit/links', '/audit/log', '/admin/users', '/admin/hints', '/admin/news', '/admin/broadcasts', `/u/${encodeURIComponent(PROFILE_NAME)}`, ]; const hits = []; let checked = 0; for (const width of WIDTHS) { await page.setViewportSize({width, height: 800}); for (const path of PAGES) { await page.goto(BASE + path); // no networkidle: some pages poll await page.waitForTimeout(1500); if (FONT_PERCENT !== 100) await page.addStyleTag({content: `html{font-size:${FONT_PERCENT}%}`}); await page.evaluate(([dark, skin]) => { document.documentElement.classList.toggle('dark', dark); if (skin) document.documentElement.dataset.skin = skin; }, [DARK, SKIN]); await page.waitForTimeout(300); const result = await page.evaluate(() => { const root = document.documentElement; const describe = (element) => `${element.tagName.toLowerCase()}.${String(element.className).slice(0, 60)}`; const all = [...document.querySelectorAll('body *')].map((element) => [element, element.getBoundingClientRect()]); const right = all.filter(([, box]) => box.right > root.clientWidth).sort((a, b) => b[1].right - a[1].right); // Clipped by an overflow:hidden/auto ancestor (an effect preview, a scroller): not visible, not a hit. const clipped = (element) => { for (let parent = element.parentElement; parent && parent !== document.body; parent = parent.parentElement) { if (getComputedStyle(parent).overflowX !== 'visible') return true; } return false; }; const left = all.filter(([element, box]) => box.width > 0 && box.left < 0 && !clipped(element)); if (root.scrollWidth === root.clientWidth && left.length === 0) return null; return { scrollWidth: root.scrollWidth, clientWidth: root.clientWidth, widest: right.slice(0, 5).map(([element]) => describe(element)), offLeft: left.slice(0, 5).map(([element]) => describe(element)), }; }); checked++; const landed = await page.evaluate(() => location.pathname); if (decodeURIComponent(landed) !== decodeURIComponent(path)) hits.push({path, width, redirectedTo: landed}); if (result) hits.push({path, width, ...result}); } } return {checked, hits}; }