Files
Classeo/frontend/e2e/admin-responsive-nav.spec.ts
Mathias STRASSER ce05207c64 feat: Réorganiser la navigation admin en catégories pour améliorer l'UX mobile-first
Le menu d'administration contenait 13 liens à plat dans le header, ce qui
débordait sur desktop et rendait le drawer mobile trop long à scanner.

Les liens sont maintenant regroupés en 4 catégories (Personnes, Organisation,
Année scolaire, Paramètres) avec des dropdowns au survol sur desktop et des
accordéons repliables dans le drawer mobile. Le nombre d'éléments visibles
passe de 13 à 5 (1 lien direct + 4 catégories), la catégorie active
s'auto-déplie dans le menu mobile.
2026-02-28 16:37:10 +01:00

278 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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: 30000 }),
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();
});
});
});