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); // Each browser project gets its own token to avoid conflicts let testToken: string | null = null; // eslint-disable-next-line no-empty-pattern test.beforeAll(async ({ }, testInfo) => { const browserName = testInfo.project.name; // Create a unique token for this browser project try { const projectRoot = join(__dirname, '../..'); const composeFile = join(projectRoot, 'compose.yaml'); const email = `e2e-${browserName}@example.com`; const result = execSync( `docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-activation-token --email=${email} 2>&1`, { encoding: 'utf-8' } ); const tokenMatch = result.match(/Token\s+([a-f0-9-]{36})/i); if (tokenMatch) { testToken = tokenMatch[1]; console.warn(`[${browserName}] Test token created: ${testToken}`); } else { console.error(`[${browserName}] Could not extract token from output:`, result); } } catch (error) { console.error(`[${browserName}] Failed to create test token:`, error); } }); function getToken(): string { if (!testToken) { throw new Error('No test token available. Make sure Docker is running.'); } return testToken; } test.describe('Account Activation Flow', () => { test.describe('Token Validation', () => { test('displays error for invalid token', async ({ page }) => { await page.goto('/activate/invalid-token-uuid-format'); // Wait for the error state await expect(page.getByRole('heading', { name: /lien invalide/i })).toBeVisible(); await expect(page.getByText(/contacter votre établissement/i)).toBeVisible(); }); test('displays error for non-existent token', async ({ page }) => { // Use a valid UUID format but non-existent token await page.goto('/activate/00000000-0000-0000-0000-000000000000'); // Shows error because token doesn't exist const heading = page.getByRole('heading', { name: /lien invalide/i }); await expect(heading).toBeVisible(); }); }); test.describe('Password Form', () => { test('validates password requirements in real-time', async ({ page }) => { const token = getToken(); await page.goto(`/activate/${token}`); // Wait for form to be visible (token must be valid) const form = page.locator('form'); await expect(form).toBeVisible({ timeout: 5000 }); const passwordInput = page.locator('#password'); // Test minimum length requirement - should NOT be valid yet await passwordInput.fill('Abc1'); const minLengthItem = page.locator('.password-requirements li').filter({ hasText: /8 caractères/ }); await expect(minLengthItem).not.toHaveClass(/valid/); // Test uppercase requirement - missing await passwordInput.fill('abcd1234'); const uppercaseItem = page.locator('.password-requirements li').filter({ hasText: /majuscule/ }); await expect(uppercaseItem).not.toHaveClass(/valid/); // Test digit requirement - missing await passwordInput.fill('Abcdefgh'); const digitItem = page.locator('.password-requirements li').filter({ hasText: /chiffre/ }); await expect(digitItem).not.toHaveClass(/valid/); // Valid password (without special char) should show 4/5 checkmarks // Requirements: minLength, uppercase, lowercase, digit, specialChar // "Abcdefgh1" satisfies: minLength(9>=8), uppercase(A), lowercase(bcdefgh), digit(1) await passwordInput.fill('Abcdefgh1'); const validItems = page.locator('.password-requirements li.valid'); await expect(validItems).toHaveCount(4); }); test('requires password confirmation to match', async ({ page }) => { const token = getToken(); await page.goto(`/activate/${token}`); const form = page.locator('form'); await expect(form).toBeVisible({ timeout: 5000 }); const passwordInput = page.locator('#password'); const confirmInput = page.locator('#passwordConfirmation'); await passwordInput.fill('SecurePass123!'); await confirmInput.fill('DifferentPass123!'); await expect(page.getByText(/mots de passe ne correspondent pas/i)).toBeVisible(); // Fix confirmation await confirmInput.fill('SecurePass123!'); await expect(page.getByText(/mots de passe ne correspondent pas/i)).not.toBeVisible(); }); test('submit button is disabled until form is valid', async ({ page }) => { const token = getToken(); await page.goto(`/activate/${token}`); const form = page.locator('form'); await expect(form).toBeVisible({ timeout: 5000 }); const submitButton = page.getByRole('button', { name: /activer mon compte/i }); // Initially disabled await expect(submitButton).toBeDisabled(); // Fill valid password (must include special char) await page.locator('#password').fill('SecurePass123!'); await page.locator('#passwordConfirmation').fill('SecurePass123!'); // Should now be enabled await expect(submitButton).toBeEnabled(); }); }); test.describe('Establishment Info Display', () => { test('shows establishment name and role when token is valid', async ({ page }) => { const token = getToken(); await page.goto(`/activate/${token}`); const form = page.locator('form'); await expect(form).toBeVisible({ timeout: 5000 }); // School info should be visible await expect(page.locator('.school-info')).toBeVisible(); await expect(page.locator('.school-name')).toBeVisible(); await expect(page.locator('.account-type')).toBeVisible(); }); }); test.describe('Password Visibility Toggle', () => { test('toggles password visibility', async ({ page }) => { const token = getToken(); await page.goto(`/activate/${token}`); const form = page.locator('form'); await expect(form).toBeVisible({ timeout: 5000 }); const passwordInput = page.locator('#password'); const toggleButton = page.locator('.toggle-password'); // Initially password type await expect(passwordInput).toHaveAttribute('type', 'password'); // Click toggle await toggleButton.click(); await expect(passwordInput).toHaveAttribute('type', 'text'); // Click again to hide await toggleButton.click(); await expect(passwordInput).toHaveAttribute('type', 'password'); }); }); test.describe('Full Activation Flow', () => { // TODO: Investigate CI timeout issue - activation works locally but times out in CI // The token is created successfully but the redirect to /login?activated=true doesn't happen // This might be a race condition or timing issue specific to the CI environment test.skip(!!process.env.CI, 'Activation flow times out in CI - needs investigation'); test('activates account and redirects to login', async ({ page }) => { const token = getToken(); await page.goto(`/activate/${token}`); const form = page.locator('form'); await expect(form).toBeVisible({ timeout: 5000 }); const submitButton = page.getByRole('button', { name: /activer mon compte/i }); // Button should be disabled initially (no password yet) await expect(submitButton).toBeDisabled(); // Fill valid password (must include special char) await page.locator('#password').fill('SecurePass123!'); await page.locator('#passwordConfirmation').fill('SecurePass123!'); // Wait for validation to complete - button should now be enabled await expect(submitButton).toBeEnabled({ timeout: 2000 }); // Submit and wait for navigation await Promise.all([ page.waitForURL(/\/login\?activated=true/, { timeout: 10000 }), submitButton.click() ]); // Verify success message await expect(page.getByText(/compte a été activé avec succès/i)).toBeVisible(); }); }); }); test.describe('Login Page After Activation', () => { test('shows success message when redirected after activation', async ({ page }) => { await page.goto('/login?activated=true'); await expect(page.getByText(/compte a été activé avec succès/i)).toBeVisible(); await expect(page.getByRole('heading', { name: /connexion/i })).toBeVisible(); }); test('does not show success message without query param', async ({ page }) => { await page.goto('/login'); await expect(page.getByText(/compte a été activé avec succès/i)).not.toBeVisible(); await expect(page.getByRole('heading', { name: /connexion/i })).toBeVisible(); }); }); test.describe('Parental Consent Flow (Minor User)', () => { // These tests would require seeded data for a minor user test.skip('shows consent required message for minor without consent', async () => { // Would navigate to activation page for a minor user token // and verify the consent required message is displayed }); test.skip('allows activation after parental consent is given', async () => { // Would verify the full flow: // 1. Minor receives activation link // 2. Parent gives consent // 3. Minor can then activate their account }); });