import { test, expect, type Page } 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-childselector-admin@example.com'; const ADMIN_PASSWORD = 'AdminCSTest123'; const PARENT_EMAIL = 'e2e-childselector-parent@example.com'; const PARENT_PASSWORD = 'ChildSelectorTest123'; const PARENT_FIRST_NAME = 'CSParent'; const PARENT_LAST_NAME = 'TestSelector'; const STUDENT1_EMAIL = 'e2e-childselector-student1@example.com'; const STUDENT1_PASSWORD = 'Student1Test123'; const STUDENT2_EMAIL = 'e2e-childselector-student2@example.com'; const STUDENT2_PASSWORD = 'Student2Test123'; let student1UserId: string; let student2UserId: string; function extractUserId(output: string): string { const match = output.match(/User ID\s+([a-f0-9-]{36})/i); if (!match) { throw new Error(`Could not extract User ID from command output:\n${output}`); } return match[1]; } async function loginAsAdmin(page: 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: 30000 }), page.getByRole('button', { name: /se connecter/i }).click() ]); } async function addGuardianIfNotLinked(page: Page, studentId: string, parentSearchTerm: string, relationship: string) { await page.goto(`${ALPHA_URL}/admin/students/${studentId}`); await expect(page.locator('.guardian-section')).toBeVisible({ timeout: 10000 }); await expect( page.getByText(/aucun parent\/tuteur/i).or(page.locator('.guardian-list')) ).toBeVisible({ timeout: 10000 }); // Skip if add button is not visible (max guardians already linked) const addButton = page.getByRole('button', { name: /ajouter un parent/i }); if (!(await addButton.isVisible())) return; // Skip if parent is already linked (email visible in guardian list) const sectionText = await page.locator('.guardian-section').textContent(); if (sectionText && sectionText.includes(parentSearchTerm)) return; await addButton.click(); const dialog = page.getByRole('dialog'); await expect(dialog).toBeVisible({ timeout: 5000 }); const searchInput = dialog.getByRole('combobox', { name: /rechercher/i }); await searchInput.fill(parentSearchTerm); const listbox = dialog.locator('#parent-search-listbox'); await expect(listbox).toBeVisible({ timeout: 10000 }); const option = listbox.locator('[role="option"]').first(); await option.click(); await expect(dialog.getByText(/sélectionné/i)).toBeVisible(); await dialog.getByLabel(/type de relation/i).selectOption(relationship); await dialog.getByRole('button', { name: 'Ajouter' }).click(); // Wait for either success (new link) or error (already linked → 409) await expect( page.locator('.alert-success').or(page.locator('.alert-error')) ).toBeVisible({ timeout: 10000 }); } async function removeFirstGuardian(page: Page, studentId: string) { await page.goto(`${ALPHA_URL}/admin/students/${studentId}`); await expect(page.locator('.guardian-section')).toBeVisible({ timeout: 10000 }); await expect( page.getByText(/aucun parent\/tuteur/i).or(page.locator('.guardian-list')) ).toBeVisible({ timeout: 10000 }); // Skip if no guardian to remove if (!(await page.locator('.guardian-item').first().isVisible())) return; const guardianItem = page.locator('.guardian-item').first(); await guardianItem.getByRole('button', { name: /retirer/i }).click(); await expect(guardianItem.getByText(/confirmer/i)).toBeVisible({ timeout: 5000 }); await guardianItem.getByRole('button', { name: /oui/i }).click(); await expect(page.locator('.alert-success')).toBeVisible({ timeout: 10000 }); } test.describe('Child Selector', () => { test.describe.configure({ mode: 'serial' }); test.beforeAll(async ({ browser }) => { 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 parent user execSync( `docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${PARENT_EMAIL} --password=${PARENT_PASSWORD} --role=ROLE_PARENT --firstName=${PARENT_FIRST_NAME} --lastName=${PARENT_LAST_NAME} 2>&1`, { encoding: 'utf-8' } ); // Create student 1 const student1Output = execSync( `docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${STUDENT1_EMAIL} --password=${STUDENT1_PASSWORD} --role=ROLE_ELEVE 2>&1`, { encoding: 'utf-8' } ); student1UserId = extractUserId(student1Output); // Create student 2 const student2Output = execSync( `docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${STUDENT2_EMAIL} --password=${STUDENT2_PASSWORD} --role=ROLE_ELEVE 2>&1`, { encoding: 'utf-8' } ); student2UserId = extractUserId(student2Output); // Use admin UI to link parent to both students const page = await browser.newPage(); await loginAsAdmin(page); await addGuardianIfNotLinked(page, student1UserId, PARENT_EMAIL, 'tuteur'); await addGuardianIfNotLinked(page, student2UserId, PARENT_EMAIL, 'tutrice'); await page.close(); }); async function loginAsParent(page: Page) { await page.goto(`${ALPHA_URL}/login`); await page.locator('#email').fill(PARENT_EMAIL); await page.locator('#password').fill(PARENT_PASSWORD); await Promise.all([ page.waitForURL(/\/dashboard/, { timeout: 30000 }), page.getByRole('button', { name: /se connecter/i }).click() ]); } test('[P1] parent with multiple children should see child selector', async ({ page }) => { await loginAsParent(page); // ChildSelector should be visible when parent has 2+ children const childSelector = page.locator('.child-selector'); await expect(childSelector).toBeVisible({ timeout: 10000 }); // Should display the label await expect(childSelector.locator('.child-selector-label')).toHaveText('Enfant :'); // Should have 3 buttons: "Tous" + 2 children const buttons = childSelector.locator('.child-button'); await expect(buttons).toHaveCount(3); // "Tous" button should be selected initially (no child auto-selected) await expect(buttons.first()).toHaveClass(/selected/); }); test('[P1] parent can switch between children', async ({ page }) => { await loginAsParent(page); const childSelector = page.locator('.child-selector'); await expect(childSelector).toBeVisible({ timeout: 10000 }); const buttons = childSelector.locator('.child-button'); await expect(buttons).toHaveCount(3); // "Tous" button (index 0) should be selected initially await expect(buttons.nth(0)).toHaveClass(/selected/); await expect(buttons.nth(1)).not.toHaveClass(/selected/); // Click first child button (index 1) await buttons.nth(1).click(); // First child should now be selected, "Tous" should not await expect(buttons.nth(1)).toHaveClass(/selected/); await expect(buttons.nth(0)).not.toHaveClass(/selected/); }); test('[P1] parent with single child should see static child name', async ({ browser, page }) => { // Remove one link via admin UI const adminPage = await browser.newPage(); await loginAsAdmin(adminPage); await removeFirstGuardian(adminPage, student2UserId); await adminPage.close(); await loginAsParent(page); // ChildSelector should be visible with 1 child (showing name, no buttons) await expect(page.locator('.child-selector')).toBeVisible({ timeout: 5000 }); await expect(page.locator('.child-button')).toHaveCount(0); // Restore the second link via admin UI for clean state const restorePage = await browser.newPage(); await loginAsAdmin(restorePage); await addGuardianIfNotLinked(restorePage, student2UserId, PARENT_EMAIL, 'tutrice'); await restorePage.close(); }); });