import { test, expect } from '@playwright/test'; import { execSync } from 'child_process'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const baseUrl = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:4173'; const urlMatch = baseUrl.match(/:(\d+)$/); const PORT = urlMatch ? urlMatch[1] : '4173'; const ALPHA_URL = `http://ecole-alpha.classeo.local:${PORT}`; const ADMIN_EMAIL = 'e2e-responsive-nav@example.com'; const ADMIN_PASSWORD = 'ResponsiveNav123'; const projectRoot = join(__dirname, '../..'); const composeFile = join(projectRoot, 'compose.yaml'); test.describe('Admin Responsive Navigation', () => { test.beforeAll(async () => { execSync( `docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${ADMIN_EMAIL} --password=${ADMIN_PASSWORD} --role=ROLE_ADMIN 2>&1`, { encoding: 'utf-8' } ); }); async function loginAsAdmin(page: import('@playwright/test').Page) { await page.goto(`${ALPHA_URL}/login`); await page.waitForLoadState('networkidle'); await page.locator('#email').fill(ADMIN_EMAIL); await page.locator('#password').fill(ADMIN_PASSWORD); await Promise.all([ page.waitForURL(/\/dashboard/, { timeout: 60000 }), page.getByRole('button', { name: /se connecter/i }).click() ]); } // ========================================================================= // MOBILE (375×667) // ========================================================================= test.describe('Mobile (375×667)', () => { test.use({ viewport: { width: 375, height: 667 } }); test('shows hamburger button and hides desktop nav', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users`); await page.waitForLoadState('networkidle'); const hamburger = page.getByRole('button', { name: /ouvrir le menu/i }); await expect(hamburger).toBeVisible(); const desktopNav = page.locator('.desktop-nav'); await expect(desktopNav).not.toBeVisible(); }); test('displays current section label', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users`); await page.waitForLoadState('networkidle'); const label = page.locator('.mobile-section-label'); await expect(label).toBeVisible(); await expect(label).toHaveText('Utilisateurs'); }); test('opens and closes menu via hamburger button', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users`); await page.waitForLoadState('networkidle'); const hamburger = page.getByRole('button', { name: /ouvrir le menu/i }); // Open await hamburger.click(); const drawer = page.locator('[role="dialog"][aria-modal="true"]'); await expect(drawer).toBeVisible(); // Close via × button const closeButton = page.getByRole('button', { name: /fermer le menu/i }); await closeButton.click(); await expect(drawer).not.toBeVisible(); }); test('closes menu on overlay click', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users`); await page.waitForLoadState('networkidle'); await page.getByRole('button', { name: /ouvrir le menu/i }).click(); const drawer = page.locator('[role="dialog"][aria-modal="true"]'); await expect(drawer).toBeVisible(); // Click overlay (outside drawer) const overlay = page.locator('.mobile-overlay'); await overlay.click({ position: { x: 350, y: 300 } }); await expect(drawer).not.toBeVisible(); }); test('closes menu on Escape key', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users`); await page.waitForLoadState('networkidle'); await page.getByRole('button', { name: /ouvrir le menu/i }).click(); const drawer = page.locator('[role="dialog"][aria-modal="true"]'); await expect(drawer).toBeVisible(); await page.keyboard.press('Escape'); await expect(drawer).not.toBeVisible(); }); test('shows active state for current section in mobile menu', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users`); await page.waitForLoadState('networkidle'); await page.getByRole('button', { name: /ouvrir le menu/i }).click(); const drawer = page.locator('[role="dialog"][aria-modal="true"]'); await expect(drawer).toBeVisible(); // Active category "Personnes" should be auto-expanded const activeLink = drawer.locator('.mobile-nav-link.active'); await expect(activeLink).toHaveText('Utilisateurs'); }); test('navigates via mobile menu and closes it', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users`); await page.waitForLoadState('networkidle'); // Open menu and expand "Organisation" section to find "Classes" await page.getByRole('button', { name: /ouvrir le menu/i }).click(); const drawer = page.locator('[role="dialog"][aria-modal="true"]'); await expect(drawer).toBeVisible(); // Expand "Organisation" accordion await drawer.getByRole('button', { name: 'Organisation' }).click(); await drawer.getByRole('link', { name: 'Classes' }).click(); // Menu should close and page should navigate await expect(drawer).not.toBeVisible(); await expect(page).toHaveURL(/\/admin\/classes/); // Section label should update const label = page.locator('.mobile-section-label'); await expect(label).toHaveText('Classes'); }); test('accordion sections expand and collapse', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users`); await page.waitForLoadState('networkidle'); await page.getByRole('button', { name: /ouvrir le menu/i }).click(); const drawer = page.locator('[role="dialog"][aria-modal="true"]'); await expect(drawer).toBeVisible(); // "Personnes" should be auto-expanded (active category) await expect(drawer.getByRole('link', { name: 'Utilisateurs' })).toBeVisible(); // "Organisation" should be collapsed initially await expect(drawer.getByRole('link', { name: 'Classes' })).not.toBeVisible(); // Expand "Organisation" await drawer.getByRole('button', { name: 'Organisation' }).click(); await expect(drawer.getByRole('link', { name: 'Classes' })).toBeVisible(); // Collapse "Organisation" await drawer.getByRole('button', { name: 'Organisation' }).click(); await expect(drawer.getByRole('link', { name: 'Classes' })).not.toBeVisible(); }); }); // ========================================================================= // TABLET (768×1024) // ========================================================================= test.describe('Tablet (768×1024)', () => { test.use({ viewport: { width: 768, height: 1024 } }); test('shows hamburger button (below 1200px)', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users`); await page.waitForLoadState('networkidle'); const hamburger = page.getByRole('button', { name: /ouvrir le menu/i }); await expect(hamburger).toBeVisible(); const desktopNav = page.locator('.desktop-nav'); await expect(desktopNav).not.toBeVisible(); }); test('drawer opens and shows grouped nav', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users`); await page.waitForLoadState('networkidle'); await page.getByRole('button', { name: /ouvrir le menu/i }).click(); const drawer = page.locator('[role="dialog"][aria-modal="true"]'); await expect(drawer).toBeVisible(); // "Personnes" auto-expanded (contains active link Utilisateurs) await expect(drawer.getByRole('link', { name: 'Utilisateurs' })).toBeVisible(); // Expand "Organisation" to see its links await drawer.getByRole('button', { name: 'Organisation' }).click(); await expect(drawer.getByRole('link', { name: 'Classes' })).toBeVisible(); await expect(drawer.getByRole('link', { name: 'Matières' })).toBeVisible(); }); }); // ========================================================================= // DESKTOP (1280×800) // ========================================================================= test.describe('Desktop (1280×800)', () => { test.use({ viewport: { width: 1280, height: 800 } }); test('hides hamburger and shows desktop nav', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users`); await page.waitForLoadState('networkidle'); const hamburger = page.getByRole('button', { name: /ouvrir le menu/i }); await expect(hamburger).not.toBeVisible(); const desktopNav = page.locator('.desktop-nav'); await expect(desktopNav).toBeVisible(); }); test('desktop nav shows category dropdowns', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users`); await page.waitForLoadState('networkidle'); const nav = page.locator('.desktop-nav'); // Category triggers should be visible await expect(nav.getByRole('button', { name: /personnes/i })).toBeVisible(); await expect(nav.getByRole('button', { name: /organisation/i })).toBeVisible(); await expect(nav.getByRole('button', { name: /année scolaire/i })).toBeVisible(); await expect(nav.getByRole('button', { name: /paramètres/i })).toBeVisible(); // Hover "Personnes" to reveal dropdown await nav.getByRole('button', { name: /personnes/i }).hover(); const dropdown = nav.locator('.dropdown-panel').first(); await expect(dropdown).toBeVisible(); await expect(dropdown.getByRole('menuitem', { name: 'Utilisateurs' })).toBeVisible(); await expect(dropdown.getByRole('menuitem', { name: 'Élèves' })).toBeVisible(); // Hover "Organisation" await nav.getByRole('button', { name: /organisation/i }).hover(); const orgDropdown = nav.locator('.dropdown-panel').first(); await expect(orgDropdown.getByRole('menuitem', { name: 'Classes' })).toBeVisible(); await expect(orgDropdown.getByRole('menuitem', { name: 'Matières' })).toBeVisible(); await expect(orgDropdown.getByRole('menuitem', { name: 'Affectations' })).toBeVisible(); }); test('active category trigger is highlighted', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users`); await page.waitForLoadState('networkidle'); const nav = page.locator('.desktop-nav'); const personnesTrigger = nav.getByRole('button', { name: /personnes/i }); await expect(personnesTrigger).toHaveClass(/active/); }); test('hides mobile section label', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users`); await page.waitForLoadState('networkidle'); const label = page.locator('.mobile-section-label'); await expect(label).not.toBeVisible(); }); }); });