Spaces:
Runtime error
Runtime error
File size: 6,460 Bytes
d44ff09 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | // ============================================================================
// 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;
})();
|