/** * Responsive design detector — captures page at multiple breakpoints * and detects layout differences between desktop and mobile viewports. * * Breakpoints: mobile(375), tablet(768), desktop(1280), wide(1920) * Detects: viewport-specific content visibility, menu collapse state, * font size changes, hidden/shown elements. */ import type { Page } from 'playwright'; export interface ViewportCapture { width: number; height: number; label: 'mobile' | 'tablet' | 'desktop' | 'wide'; screenshotBuffer?: Buffer; visibleTextLength: number; hasHamburgerMenu: boolean; hiddenElements: number; // display:none element count scrollHeight: number; } export interface ResponsiveReport { url: string; captures: ViewportCapture[]; isResponsive: boolean; // true if layout changes between viewports hasMobileMenu: boolean; issues: string[]; } const BREAKPOINTS: Array<{ width: number; height: number; label: ViewportCapture['label'] }> = [ { width: 375, height: 812, label: 'mobile' }, { width: 768, height: 1024, label: 'tablet' }, { width: 1280, height: 800, label: 'desktop' }, ]; export async function detectResponsive( page: Page, url: string, opts: { captureScreenshots?: boolean } = {}, ): Promise { const captures: ViewportCapture[] = []; const issues: string[] = []; for (const bp of BREAKPOINTS) { try { await page.setViewportSize({ width: bp.width, height: bp.height }); await page.waitForTimeout(500); // allow CSS reflow const data = await page.evaluate(() => ({ visibleTextLength: (document.body.innerText ?? '').length, hasHamburgerMenu: !!( document.querySelector('[class*="hamburger"],[class*="menu-toggle"],[class*="nav-toggle"],[aria-label*="menu"],[aria-label*="Menu"]') ), hiddenElements: Array.from(document.querySelectorAll('*')).filter( (el: any) => window.getComputedStyle(el).display === 'none' ).length, scrollHeight: document.body.scrollHeight, })).catch(() => ({ visibleTextLength: 0, hasHamburgerMenu: false, hiddenElements: 0, scrollHeight: 0 })); let screenshotBuffer: Buffer | undefined; if (opts.captureScreenshots) { screenshotBuffer = await page.screenshot({ type: 'jpeg', quality: 60 }).catch(() => undefined); } captures.push({ ...bp, ...data, screenshotBuffer }); } catch { /* viewport failed — skip */ } } // Restore to desktop await page.setViewportSize({ width: 1280, height: 800 }).catch(() => {}); const mobile = captures.find(c => c.label === 'mobile'); const desktop = captures.find(c => c.label === 'desktop'); const isResponsive = !!(mobile && desktop && ( mobile.hiddenElements !== desktop.hiddenElements || mobile.scrollHeight !== desktop.scrollHeight || mobile.hasHamburgerMenu )); const hasMobileMenu = captures.some(c => c.label === 'mobile' && c.hasHamburgerMenu); if (mobile && desktop && mobile.visibleTextLength > desktop.visibleTextLength * 1.3) { issues.push('Mobile shows significantly more text than desktop — possible responsive issue'); } if (!isResponsive && captures.length > 1) { issues.push('Layout appears identical across breakpoints — may not be responsive'); } return { url, captures, isResponsive, hasMobileMenu, issues }; }