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-actlink-admin@example.com'; const ADMIN_PASSWORD = 'ActLinkTest123'; const STUDENT_EMAIL = 'e2e-actlink-student@example.com'; const STUDENT_PASSWORD = 'StudentTest123'; const UNIQUE_SUFFIX = Date.now(); const PARENT_EMAIL = `e2e-actlink-parent-${UNIQUE_SUFFIX}@example.com`; const PARENT_PASSWORD = 'ParentActivation1!'; let studentUserId: string; let activationToken: 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]; } test.describe('Activation with Parent-Child Auto-Link', () => { test.describe.configure({ mode: 'serial' }); test.beforeAll(async () => { const projectRoot = join(__dirname, '../..'); const composeFile = join(projectRoot, 'compose.yaml'); const run = (cmd: string) => { for (let attempt = 0; attempt < 3; attempt++) { try { return execSync(cmd, { encoding: 'utf-8' }); } catch (e) { if (attempt === 2) throw e; execSync('sleep 2'); } } return ''; }; // Create admin user run( `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` ); // Create student user and capture userId const studentOutput = run( `docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${STUDENT_EMAIL} --password=${STUDENT_PASSWORD} --role=ROLE_ELEVE 2>&1` ); studentUserId = extractUserId(studentOutput); // Clean up any existing guardian links for this student try { execSync( `docker compose -f "${composeFile}" exec -T php php bin/console dbal:run-sql "DELETE FROM student_guardians WHERE student_id = '${studentUserId}'" 2>&1`, { encoding: 'utf-8' } ); } catch { // Ignore cleanup errors } // Create activation token for parent WITH student-id for auto-linking const tokenOutput = execSync( `docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-activation-token --email=${PARENT_EMAIL} --role=PARENT --tenant=ecole-alpha --student-id=${studentUserId} 2>&1`, { encoding: 'utf-8' } ); const tokenMatch = tokenOutput.match(/Token\s+([a-f0-9-]{36})/i); if (!tokenMatch) { throw new Error(`Could not extract token from command output:\n${tokenOutput}`); } activationToken = tokenMatch[1]; }); test('[P1] should activate parent account and auto-link to student', async ({ page }) => { // Navigate to the activation page await page.goto(`${ALPHA_URL}/activate/${activationToken}`); // Wait for the activation form to load await expect(page.locator('#password')).toBeVisible({ timeout: 10000 }); // Fill the password form await page.locator('#password').fill(PARENT_PASSWORD); await page.locator('#passwordConfirmation').fill(PARENT_PASSWORD); // Wait for validation to pass and submit const submitButton = page.getByRole('button', { name: /activer mon compte/i }); await expect(submitButton).toBeEnabled({ timeout: 5000 }); await submitButton.click(); // Should redirect to login with activated=true await page.waitForURL(/\/login\?activated=true/, { timeout: 15000 }); // Now login as admin to verify the auto-link 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() ]); // Navigate to the student's page to check guardian list await page.goto(`${ALPHA_URL}/admin/students/${studentUserId}`); // Wait for the guardian section to load await expect(page.locator('.guardian-section')).toBeVisible({ timeout: 10000 }); await expect( page.locator('.guardian-list') ).toBeVisible({ timeout: 10000 }); // The auto-linked parent should appear in the guardian list const guardianItem = page.locator('.guardian-item').first(); await expect(guardianItem).toBeVisible(); // Auto-linking uses RelationshipType::OTHER → label "Autre" await expect(guardianItem).toContainText('Autre'); }); });