Lorsqu'un super-admin crée un établissement via l'interface, le système doit automatiquement créer la base tenant, exécuter les migrations, créer le premier utilisateur admin et envoyer l'invitation — le tout de manière asynchrone pour ne pas bloquer la réponse HTTP. Ce mécanisme rend chaque établissement opérationnel dès sa création sans intervention manuelle sur l'infrastructure.
276 lines
11 KiB
TypeScript
276 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 page.getByRole('button', { name: /se connecter/i }).click();
|
||
await page.waitForURL(/\/dashboard/, { timeout: 60000 });
|
||
}
|
||
|
||
// =========================================================================
|
||
// 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();
|
||
});
|
||
});
|
||
});
|