Files
Classeo/frontend/e2e/admin-responsive-nav.spec.ts
Mathias STRASSER aedde6707e
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled
feat: Calculer automatiquement les moyennes après chaque saisie de notes
Les enseignants ont besoin de moyennes à jour immédiatement après la
publication ou modification des notes, sans attendre un batch nocturne.

Le système recalcule via Domain Events synchrones : statistiques
d'évaluation (min/max/moyenne/médiane), moyennes matières pondérées
(normalisation /20), et moyenne générale par élève. Les résultats sont
stockés dans des tables dénormalisées avec cache Redis (TTL 5 min).

Trois endpoints API exposent les données avec contrôle d'accès par rôle.
Une commande console permet le backfill des données historiques au
déploiement.
2026-03-31 16:43:10 +02: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: 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();
});
});
});