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.
278 lines
11 KiB
TypeScript
278 lines
11 KiB
TypeScript
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();
|
||
});
|
||
});
|
||
});
|