// Mobile regression: hamburger drawer, wheel scroll, settings side-nav, help page. const { test, expect } = require('@playwright/test'); const FD_BASE = process.env.FD_BASE_URL || 'http://localhost:8080'; const USER = process.env.FD_USER || 'e2e@flowdeck.test'; const PASS = process.env.FD_PASS || 'e2e-pass-123'; // One login for the whole file (app rate limiter: 60 req/min per IP) — reused // as storageState by every test. let STATE = null; test.beforeAll(async ({ browser }) => { const ctx = await browser.newContext(); const page = await ctx.newPage(); await page.goto(`${FD_BASE}/auth/login?provider=local`, { waitUntil: 'domcontentloaded' }); await page.fill('#email', USER); await page.fill('#password', PASS); await page.click('.btn-primary'); const ok = await page.waitForURL('**/workspaces', { timeout: 10000 }).then(() => true).catch(() => false); if (!ok) { const resp = await page.request.post(`${FD_BASE}/auth/register`, { data: { email: USER, password: PASS, name: 'E2E Mobile' } }); if (!resp.ok() && resp.status() !== 409) throw new Error(`register ${resp.status()}`); await page.goto(`${FD_BASE}/auth/login?provider=local`, { waitUntil: 'domcontentloaded' }); await page.fill('#email', USER); await page.fill('#password', PASS); await page.click('.btn-primary'); await page.waitForURL('**/workspaces', { timeout: 10000 }); } STATE = await ctx.storageState(); await ctx.close(); }); async function login(page) { // Session reused from the single beforeAll login (rate limiter: 60 req/min/IP) await page.context().addCookies(STATE.cookies); } test.describe('mobile', () => { test.use({ viewport: { width: 390, height: 844 } }); test('hamburger opens mobile sidebar drawer', async ({ page }) => { const errs = []; page.on('pageerror', (e) => errs.push(e.message)); await login(page); await page.goto(`${FD_BASE}/library`, { waitUntil: 'domcontentloaded' }); const burger = page.locator('.hamburger-btn'); await expect(burger).toBeVisible(); // Sidebar starts off-canvas const sidebar = page.locator('#sidebar'); const boxBefore = await sidebar.boundingBox(); expect(boxBefore.x).toBeLessThan(0); await burger.click(); await expect(sidebar).toHaveClass(/mobile-open/, { timeout: 5000 }); const boxAfter = await sidebar.boundingBox(); expect(boxAfter.x).toBeGreaterThanOrEqual(0); expect(errs).toEqual([]); // Overlay tap closes it await page.locator('.sidebar-overlay').click({ position: { x: 350, y: 400 } }); await expect(sidebar).not.toHaveClass(/mobile-open/, { timeout: 5000 }); }); test('mobile sidebar drawer scrolls with mouse wheel', async ({ page }) => { await login(page); await page.goto(`${FD_BASE}/library`, { waitUntil: 'domcontentloaded' }); await page.locator('.hamburger-btn').click(); await page.waitForSelector('#sidebar.mobile-open'); const sc = page.locator('.sidebar-scroll'); await expect.poll(async () => sc.evaluate((el) => el.scrollHeight > el.clientHeight), { timeout: 3000 }).toBe(true); const box = await sc.boundingBox(); await page.mouse.move(box.x + box.width / 2, box.y + Math.min(box.height / 2, 400)); await page.mouse.wheel(0, 300); await expect.poll(async () => sc.evaluate((el) => el.scrollTop), { timeout: 3000 }).toBeGreaterThan(0); }); test('settings page fits mobile viewport', async ({ page }) => { await login(page); await page.goto(`${FD_BASE}/settings`, { waitUntil: 'domcontentloaded' }); const panel = page.locator('.settings-panel'); await expect(panel).toBeVisible(); const box = await panel.boundingBox(); expect(box.width).toBeLessThanOrEqual(390); expect(box.height).toBeLessThanOrEqual(844); // No horizontal page overflow const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth); expect(overflow).toBeLessThanOrEqual(0); }); test('settings mobile nav is a hamburger side drawer', async ({ page }) => { await login(page); await page.goto(`${FD_BASE}/settings`, { waitUntil: 'domcontentloaded' }); const burger = page.locator('.settings-menu-btn'); await expect(burger).toBeVisible(); const nav = page.locator('.settings-nav'); const before = await nav.boundingBox(); expect(before.x + before.width).toBeLessThanOrEqual(1); await burger.click(); await expect(nav).toHaveClass(/nav-open/); // Nav item switches section AND closes the drawer await page.locator('.settings-nav-item', { hasText: 'Tags' }).click(); await expect(nav).not.toHaveClass(/nav-open/); await expect(page.locator('h2', { hasText: 'Tags' }).first()).toBeVisible(); // Re-open and dismiss via backdrop await burger.click(); await expect(nav).toHaveClass(/nav-open/); await page.locator('.settings-nav-backdrop').click({ position: { x: 375, y: 500 } }); await expect(nav).not.toHaveClass(/nav-open/); }); test('help page uses settings-style side navigation', async ({ page }) => { const errs = []; page.on('pageerror', (e) => errs.push(e.message)); await login(page); await page.goto(`${FD_BASE}/help`, { waitUntil: 'domcontentloaded' }); // Same panel skeleton as settings await expect(page.locator('.settings-panel')).toBeVisible(); await expect(page.locator('.settings-menu-btn')).toBeVisible(); // Mobile: nav drawer behind hamburger const nav = page.locator('.settings-nav'); await expect(nav).not.toHaveClass(/nav-open/); await page.locator('.settings-menu-btn').click(); await expect(nav).toHaveClass(/nav-open/); await page.locator('.settings-nav-item', { hasText: 'Databases' }).click(); await expect(nav).not.toHaveClass(/nav-open/); await expect(page.locator('h2', { hasText: 'Databases & Views' })).toBeVisible(); // App-wide coverage: all sections exist in the nav const sections = ['Getting Started', 'Pages & Editor', 'Databases & Views', 'Tasks & Dependencies', 'Workspaces & Forges', 'Collaboration', 'Library, Trash & Search', 'AI Agent & Automations', 'Integrations & API', 'Offline & PWA', 'Accounts & SSO', 'Keyboard Shortcuts', 'Tips']; const labels = (await nav.locator('.settings-nav-item').allInnerTexts()).join(' '); for (const s of sections) expect(labels, s).toContain(s); expect(errs).toEqual([]); }); }); test.describe('desktop', () => { test.use({ viewport: { width: 1440, height: 900 } }); test('settings and help keep the 2-column layout on desktop', async ({ page }) => { await login(page); await page.goto(`${FD_BASE}/settings`, { waitUntil: 'domcontentloaded' }); await expect(page.locator('.settings-menu-btn')).toBeHidden(); const nav = page.locator('.settings-nav'); await expect(nav).toBeVisible(); const navBox = await nav.boundingBox(); expect(navBox.height).toBeGreaterThan(navBox.width); await page.goto(`${FD_BASE}/help`, { waitUntil: 'domcontentloaded' }); await expect(page.locator('.settings-menu-btn')).toBeHidden(); await page.locator('.settings-nav-item', { hasText: 'SSO' }).click(); await expect(page.locator('h2', { hasText: 'Accounts & SSO' })).toBeVisible(); await expect(page.locator('.settings-content')).toContainText('IndexedDB'); }); });