Le handler du hamburger était une expression Alpine avec un effet de bord dans un ternaire (sidebarCollapsed ? toggleSidebar() : (mobileSidebarOpen = true, ...)). Évaluée sans eval dans le build CSP d'Alpine, elle ne produisait aucun effet visible sur mobile: le bouton ne faisait rien (prouvé via Alpine.evaluate@390px, open=false). Remplacée par la méthode fdHamburgerClick() (matchMedia 768px: drawer mobile, sinon toggle desktop). Settings: panel 1050px + nav verticale 200px inutilisable sur 390px → @media 768px: plein écran (dvh), nav en onglets horizontaux scrollables, rows en colonne, prov-grid 1 colonne, close 44px. + gate e2e mobile_regression.spec.js (drawer open/close + viewport settings)
75 lines
3.1 KiB
JavaScript
75 lines
3.1 KiB
JavaScript
// Mobile regression: hamburger opens the sidebar drawer, settings page fits viewport.
|
|
const { test, expect } = require('@playwright/test');
|
|
const FD_BASE = process.env.FD_BASE_URL || 'http://localhost:8080';
|
|
const USER = process.env.FD_USER || '[email protected]';
|
|
const PASS = process.env.FD_PASS || 'e2e-pass-123';
|
|
|
|
async function login(page) {
|
|
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 });
|
|
}
|
|
}
|
|
|
|
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('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);
|
|
|
|
// Nav becomes horizontal tabs: nav wider than tall
|
|
const navBox = await page.locator('.settings-nav').boundingBox();
|
|
expect(navBox.width).toBeGreaterThan(navBox.height);
|
|
|
|
// Tapping a nav item still switches sections
|
|
await page.locator('.settings-nav-item', { hasText: 'Tags' }).click();
|
|
await expect(page.locator('h2', { hasText: 'Tags' }).first()).toBeVisible();
|
|
|
|
// No horizontal page overflow
|
|
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
|
expect(overflow).toBeLessThanOrEqual(0);
|
|
});
|
|
});
|