Spaces:
Runtime error
Runtime error
| // ============================================================================ | |
| // RESPONSIVE E2E β verifies the app adapts across 8 viewport widths with NO | |
| // horizontal overflow at any size. Captures a screenshot per breakpoint. | |
| // ============================================================================ | |
| const { chromium } = require('playwright'); | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const FRONTEND = 'http://localhost:5173'; | |
| const SHOTS = path.join(__dirname, 'screenshots', 'responsive'); | |
| fs.mkdirSync(SHOTS, { recursive: true }); | |
| const email = `resp_${Date.now()}@example.com`; | |
| const password = 'RealPass123!'; | |
| const licenseKey = 'ALK-DEMO-TEST-0001-0001'; | |
| // Viewports to test: small phone -> ultra-wide desktop | |
| const VIEWPORTS = [ | |
| { name: 'iphone-se', w: 320, h: 568 }, // smallest common phone | |
| { name: 'iphone-12', w: 390, h: 844 }, // modern phone | |
| { name: 'pixel-large', w: 414, h: 896 }, // large android | |
| { name: 'ipad-mini', w: 768, h: 1024 }, // portrait tablet | |
| { name: 'ipad-landscape',w: 1024, h: 768 }, // landscape tablet / small laptop | |
| { name: 'laptop', w: 1280, h: 800 }, // standard laptop | |
| { name: 'desktop', w: 1536, h: 864 }, // desktop | |
| { name: 'ultrawide', w: 1920, h: 1080 }, // large monitor | |
| ]; | |
| const results = []; | |
| function check(name, cond, detail) { | |
| const ok = !!cond; | |
| results.push({ name, ok, detail: detail || '' }); | |
| console.log(`${ok ? 'β ' : 'β'} ${name}${detail ? ' β ' + detail : ''}`); | |
| } | |
| const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); | |
| // Measures horizontal overflow: how many pixels of content exceed the viewport. | |
| async function overflowPx(page) { | |
| return page.evaluate(() => ({ | |
| scrollWidth: document.documentElement.scrollWidth, | |
| clientWidth: document.documentElement.clientWidth, | |
| bodyScroll: document.body.scrollWidth, | |
| })); | |
| } | |
| (async () => { | |
| const browser = await chromium.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] }); | |
| const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } }); | |
| const page = await ctx.newPage(); | |
| // ---- One-time setup: register + activate license + create a campaign ---- | |
| console.log('βββ setup: account + campaign βββ'); | |
| await page.goto(FRONTEND + '/login', { waitUntil: 'networkidle' }); | |
| await page.getByRole('button', { name: 'Register', exact: true }).click(); | |
| await page.fill('input[type="email"]', email); | |
| await page.fill('input[type="password"]', password); | |
| await Promise.all([page.waitForURL('**/', { timeout: 15000 }), page.getByTestId('auth-submit').click()]); | |
| await page.goto(FRONTEND + '/account', { waitUntil: 'networkidle' }); | |
| await page.fill('input[placeholder*="ALK"]', licenseKey); | |
| await page.getByTestId('activate-btn').click(); | |
| await sleep(1000); | |
| await page.goto(FRONTEND + '/campaigns/new', { waitUntil: 'networkidle' }); | |
| await page.fill('#pn', 'Responsive Demo Product'); | |
| await page.fill('#aud', 'marketers'); | |
| await Promise.all([page.waitForURL(/\/campaigns\/[a-f0-9-]+$/, { timeout: 60000 }), page.getByTestId('generate-btn').click()]); | |
| await page.waitForSelector('.tabs', { timeout: 15000 }); | |
| await sleep(2000); | |
| const campaignId = page.url().split('/campaigns/')[1]; | |
| console.log(' campaign ready:', campaignId, '\n'); | |
| // Screens to validate at each viewport | |
| const SCREENS = [ | |
| { label: 'login', url: FRONTEND + '/login' }, | |
| { label: 'dashboard',url: FRONTEND + '/' }, | |
| { label: 'editor', url: FRONTEND + '/campaigns/' + campaignId }, | |
| { label: 'export', url: FRONTEND + '/campaigns/' + campaignId + '/export' }, | |
| { label: 'history', url: FRONTEND + '/history' }, | |
| ]; | |
| for (const vp of VIEWPORTS) { | |
| console.log(`\nβββ ${vp.name} (${vp.w}Γ${vp.h}) βββ`); | |
| await ctx.newPage().then(async (np) => { await np.close(); }); // noop safety | |
| await page.setViewportSize({ width: vp.w, height: vp.h }); | |
| for (const screen of SCREENS) { | |
| await page.goto(screen.url, { waitUntil: 'networkidle' }); | |
| await sleep(700); | |
| const ov = await overflowPx(page); | |
| const overflow = ov.scrollWidth - ov.clientWidth; | |
| const ok = overflow <= 2; // tolerate 2px rounding | |
| check(`${vp.name}/${screen.label}: no horizontal overflow`, ok, | |
| !ok ? `overflow=${overflow}px (scroll=${ov.scrollWidth} client=${ov.clientWidth})` : ''); | |
| await page.screenshot({ path: `${SHOTS}/${vp.name}-${screen.label}.png`, fullPage: false }); | |
| } | |
| // Device-class-specific behavioral checks | |
| if (vp.w < 768) { | |
| // Mobile: hamburger must be visible, sidebar hidden until toggled | |
| await page.goto(FRONTEND + '/', { waitUntil: 'networkidle' }); | |
| await sleep(500); | |
| const menuVisible = await page.locator('.menu-toggle').isVisible().catch(() => false); | |
| check(`${vp.name}: mobile hamburger visible`, menuVisible); | |
| // Editor: pane toggle visible (Edit/Preview switch) | |
| await page.goto(FRONTEND + '/campaigns/' + campaignId, { waitUntil: 'networkidle' }); | |
| await page.waitForSelector('.editor-wrap'); | |
| await sleep(600); | |
| const toggleVisible = await page.locator('.pane-toggle:visible').count(); | |
| check(`${vp.name}: editor Edit/Preview toggle visible`, toggleVisible > 0, `toggle=${toggleVisible}`); | |
| } else { | |
| // Tablet/desktop: sidebar persistent (no hamburger), editor dual-pane | |
| await page.goto(FRONTEND + '/', { waitUntil: 'networkidle' }); | |
| await sleep(500); | |
| const menuHidden = !(await page.locator('.menu-toggle').isVisible().catch(() => false)); | |
| check(`${vp.name}: desktop β no hamburger (sidebar persistent)`, menuHidden); | |
| await page.goto(FRONTEND + '/campaigns/' + campaignId, { waitUntil: 'networkidle' }); | |
| await page.waitForSelector('.editor-wrap'); | |
| await sleep(600); | |
| const cols = await page.evaluate(() => window.getComputedStyle(document.querySelector('.editor-wrap')).gridTemplateColumns); | |
| const dual = cols.trim().indexOf(' ') > 0; | |
| check(`${vp.name}: editor dual-pane (side by side)`, dual, `cols="${cols}"`); | |
| } | |
| } | |
| await browser.close(); | |
| const passed = results.filter((r) => r.ok).length; | |
| const total = results.length; | |
| console.log(`\n${'='.repeat(60)}\nRESPONSIVE E2E: ${passed}/${total} PASSED across ${VIEWPORTS.length} devices\n${'='.repeat(60)}`); | |
| console.log(`Screenshots: ${SHOTS}/`); | |
| process.exitCode = passed === total ? 0 : 1; | |
| })(); | |