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-roles-admin@example.com'; const ADMIN_PASSWORD = 'RolesAdmin123'; const TARGET_EMAIL = `e2e-roles-target-${Date.now()}@example.com`; const TARGET_PASSWORD = 'RolesTarget123'; test.describe('Multi-Role Assignment (FR5) [P2]', () => { test.describe.configure({ mode: 'serial' }); test.beforeAll(async () => { const projectRoot = join(__dirname, '../..'); const composeFile = join(projectRoot, 'compose.yaml'); // Create admin user 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' } ); // Create target user with single role (PROF) execSync( `docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${TARGET_EMAIL} --password=${TARGET_PASSWORD} --role=ROLE_PROF 2>&1`, { encoding: 'utf-8' } ); }); async function loginAsAdmin(page: import('@playwright/test').Page) { await page.goto(`${ALPHA_URL}/login`); 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() ]); } async function openRolesModalForTarget(page: import('@playwright/test').Page) { await page.goto(`${ALPHA_URL}/admin/users`); await expect(page.locator('.users-table')).toBeVisible({ timeout: 10000 }); // Search for the target user (paginated list may not show them on page 1) await page.getByRole('searchbox').fill(TARGET_EMAIL); await page.waitForTimeout(500); // debounce // Find the target user row and click "Rôles" button const targetRow = page.locator('tr', { has: page.locator(`text=${TARGET_EMAIL}`) }); await expect(targetRow).toBeVisible({ timeout: 10000 }); await targetRow.getByRole('button', { name: 'Rôles' }).click(); // Modal should appear await expect(page.getByRole('dialog')).toBeVisible(); await expect(page.locator('#roles-modal-title')).toHaveText('Modifier les rôles'); } test('[P2] admin can open role modal showing current roles', async ({ page }) => { await loginAsAdmin(page); await openRolesModalForTarget(page); // Target user email should be displayed in modal await expect(page.locator('.roles-modal-user')).toContainText(TARGET_EMAIL); // ROLE_PROF should be checked (current role) const profCheckbox = page.locator('.role-checkbox-label', { hasText: 'Enseignant' }).locator('input[type="checkbox"]'); await expect(profCheckbox).toBeChecked(); // Other roles should be unchecked const adminCheckbox = page.locator('.role-checkbox-label', { hasText: 'Directeur' }).locator('input[type="checkbox"]'); await expect(adminCheckbox).not.toBeChecked(); }); test('[P2] admin can assign multiple roles to a user', async ({ page }) => { await loginAsAdmin(page); await openRolesModalForTarget(page); // Add Vie Scolaire role in addition to PROF const vieScolaireLabel = page.locator('.role-checkbox-label', { hasText: 'Vie Scolaire' }); await vieScolaireLabel.locator('input[type="checkbox"]').check(); // Both should now be checked const profCheckbox = page.locator('.role-checkbox-label', { hasText: 'Enseignant' }).locator('input[type="checkbox"]'); await expect(profCheckbox).toBeChecked(); await expect(vieScolaireLabel.locator('input[type="checkbox"]')).toBeChecked(); // Save const saveResponsePromise = page.waitForResponse( (resp) => resp.url().includes('/roles') && resp.request().method() === 'PUT' ); await page.getByRole('button', { name: 'Enregistrer' }).click(); const saveResponse = await saveResponsePromise; expect(saveResponse.status()).toBeLessThan(400); // Success message should appear await expect(page.locator('.alert-success')).toContainText(/rôles.*mis à jour/i, { timeout: 5000 }); }); test('[P2] assigned roles persist after page reload', async ({ page }) => { await loginAsAdmin(page); await openRolesModalForTarget(page); // Both PROF and VIE_SCOLAIRE should still be checked after reload const profCheckbox = page.locator('.role-checkbox-label', { hasText: 'Enseignant' }).locator('input[type="checkbox"]'); const vieScolaireCheckbox = page.locator('.role-checkbox-label', { hasText: 'Vie Scolaire' }).locator('input[type="checkbox"]'); await expect(profCheckbox).toBeChecked(); await expect(vieScolaireCheckbox).toBeChecked(); }); test('[P2] admin can remove a role while keeping at least one', async ({ page }) => { await loginAsAdmin(page); await openRolesModalForTarget(page); // Uncheck Vie Scolaire (added in previous test) const vieScolaireCheckbox = page.locator('.role-checkbox-label', { hasText: 'Vie Scolaire' }).locator('input[type="checkbox"]'); await vieScolaireCheckbox.uncheck(); // PROF should still be checked const profCheckbox = page.locator('.role-checkbox-label', { hasText: 'Enseignant' }).locator('input[type="checkbox"]'); await expect(profCheckbox).toBeChecked(); await expect(vieScolaireCheckbox).not.toBeChecked(); // Save const saveResponsePromise = page.waitForResponse( (resp) => resp.url().includes('/roles') && resp.request().method() === 'PUT' ); await page.getByRole('button', { name: 'Enregistrer' }).click(); await saveResponsePromise; await expect(page.locator('.alert-success')).toContainText(/rôles.*mis à jour/i, { timeout: 5000 }); }); test('[P2] last role checkbox is disabled to prevent removal', async ({ page }) => { await loginAsAdmin(page); await openRolesModalForTarget(page); // Only PROF should be checked now (after previous test removed VIE_SCOLAIRE) const profCheckbox = page.locator('.role-checkbox-label', { hasText: 'Enseignant' }).locator('input[type="checkbox"]'); await expect(profCheckbox).toBeChecked(); // Last role checkbox should be disabled await expect(profCheckbox).toBeDisabled(); // "(dernier rôle)" hint should be visible await expect( page.locator('.role-checkbox-label', { hasText: 'Enseignant' }).locator('.role-checkbox-hint') ).toContainText('dernier rôle'); }); test('[P2] role modal can be closed with Escape', async ({ page }) => { await loginAsAdmin(page); await openRolesModalForTarget(page); await page.getByRole('dialog').press('Escape'); await expect(page.getByRole('dialog')).not.toBeVisible(); }); });