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-pedagogy-admin@example.com'; const ADMIN_PASSWORD = 'PedagogyTest123'; test.describe('Pedagogy - Grading Mode Configuration (Story 2.4)', () => { test.beforeAll(async () => { const projectRoot = join(__dirname, '../..'); const composeFile = join(projectRoot, 'compose.yaml'); 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' } ); // Reset grading mode to default (numeric_20) to ensure clean state try { execSync( `docker compose -f "${composeFile}" exec -T php php -r " require 'vendor/autoload.php'; \\$kernel = new App\\Kernel('dev', true); \\$kernel->boot(); \\$conn = \\$kernel->getContainer()->get('doctrine')->getConnection(); \\$conn->executeStatement('DELETE FROM school_grading_configurations'); " 2>&1`, { encoding: 'utf-8' } ); } catch { // Table might not exist yet, that's OK — default mode applies } }); 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: 10000 }), page.getByRole('button', { name: /se connecter/i }).click() ]); } // ========================================================================= // Navigation // ========================================================================= test('pedagogy link is visible in admin navigation', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/classes`); const pedagogyLink = page.getByRole('link', { name: /pédagogie/i }); await expect(pedagogyLink).toBeVisible(); }); test('pedagogy link navigates to pedagogy page', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/classes`); await page.getByRole('link', { name: /pédagogie/i }).click(); await expect(page).toHaveURL(/\/admin\/pedagogy/, { timeout: 10000 }); }); test('pedagogy card is visible on admin dashboard', async ({ page }) => { await loginAsAdmin(page); // Authenticated admin sees admin dashboard directly via role context await page.goto(`${ALPHA_URL}/dashboard`); await expect(page.getByRole('heading', { name: /administration/i })).toBeVisible({ timeout: 10000 }); const pedagogyCard = page.getByRole('link', { name: /pédagogie/i }); await expect(pedagogyCard).toBeVisible(); }); // ========================================================================= // AC1: Display grading mode options // ========================================================================= test('shows page title and subtitle', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/pedagogy`); await expect(page.getByRole('heading', { name: /mode de notation/i })).toBeVisible(); await expect(page.getByText(/système d'évaluation/i)).toBeVisible(); }); test('shows current mode banner', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/pedagogy`); // Wait for loading to finish await page.waitForLoadState('networkidle'); // Should show "Mode actuel" banner await expect(page.getByText(/mode actuel/i)).toBeVisible({ timeout: 10000 }); }); test('displays all five grading modes', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/pedagogy`); await page.waitForLoadState('networkidle'); // All 5 mode cards should be visible (scope to .mode-card to avoid ambiguous matches) const modeCards = page.locator('.mode-card'); await expect(modeCards).toHaveCount(5, { timeout: 10000 }); await expect(modeCards.filter({ hasText: /notes \/20/i })).toBeVisible(); await expect(modeCards.filter({ hasText: /notes \/10/i })).toBeVisible(); await expect(modeCards.filter({ hasText: /lettres/i })).toBeVisible(); await expect(modeCards.filter({ hasText: /compétences/i })).toBeVisible(); await expect(modeCards.filter({ hasText: /sans notes/i })).toBeVisible(); }); test('default mode is Notes sur 20', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/pedagogy`); await page.waitForLoadState('networkidle'); // The "Notes /20" mode card should be selected (has mode-selected class) const selectedCard = page.locator('.mode-card.mode-selected'); await expect(selectedCard).toBeVisible({ timeout: 10000 }); await expect(selectedCard).toContainText(/notes \/20/i); }); // ========================================================================= // AC1: Year selector // ========================================================================= test('can switch between year tabs', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/pedagogy`); const tabs = page.getByRole('tab'); // Wait for page to load await expect(tabs.nth(1)).toHaveAttribute('aria-selected', 'true', { timeout: 10000 }); await page.waitForLoadState('networkidle'); // Click next year tab await tabs.nth(2).click(); await expect(tabs.nth(2)).toHaveAttribute('aria-selected', 'true', { timeout: 10000 }); // Click previous year tab await tabs.nth(0).click(); await expect(tabs.nth(0)).toHaveAttribute('aria-selected', 'true', { timeout: 10000 }); }); // ========================================================================= // AC2-4: Mode selection and preview // ========================================================================= test('selecting a different mode shows save button', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/pedagogy`); await page.waitForLoadState('networkidle'); // Save button should not be visible initially await expect(page.getByRole('button', { name: /enregistrer/i })).not.toBeVisible(); // Click a different mode const competenciesCard = page.locator('.mode-card').filter({ hasText: /compétences/i }); await competenciesCard.click(); // Save and cancel buttons should appear await expect(page.getByRole('button', { name: /enregistrer/i })).toBeVisible(); await expect(page.getByRole('button', { name: /annuler/i })).toBeVisible(); }); test('cancel button reverts mode selection', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/pedagogy`); await page.waitForLoadState('networkidle'); // Select a different mode const lettersCard = page.locator('.mode-card').filter({ hasText: /lettres/i }); await lettersCard.click(); // Click cancel await page.getByRole('button', { name: /annuler/i }).click(); // Save button should disappear await expect(page.getByRole('button', { name: /enregistrer/i })).not.toBeVisible(); }); test('selecting competencies mode shows competency-specific description', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/pedagogy`); await page.waitForLoadState('networkidle'); await expect(page.getByText(/mode actuel/i)).toBeVisible({ timeout: 10000 }); const competenciesCard = page.locator('.mode-card').filter({ hasText: /compétences/i }); await competenciesCard.click(); // Check description inside the card (scoped to avoid matching bulletin impact text) await expect(competenciesCard.locator('.mode-description')).toContainText( /acquis.*en cours.*non acquis/i ); }); test('selecting sans notes mode shows no-grades description', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/pedagogy`); await page.waitForLoadState('networkidle'); await expect(page.getByText(/mode actuel/i)).toBeVisible({ timeout: 10000 }); const noGradesCard = page.locator('.mode-card').filter({ hasText: /sans notes/i }); await noGradesCard.click(); // Check description inside the card (scoped to avoid matching bulletin impact text) await expect(noGradesCard.locator('.mode-description')).toContainText( /appréciations textuelles/i ); }); test('shows bulletin impact preview when mode is selected', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/pedagogy`); await page.waitForLoadState('networkidle'); // Impact section should be visible await expect(page.getByText(/impact sur les bulletins/i)).toBeVisible({ timeout: 10000 }); }); // ========================================================================= // AC2: Can change to a different mode // ========================================================================= test('can save a new grading mode', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/pedagogy`); await page.waitForLoadState('networkidle'); await expect(page.getByText(/mode actuel/i)).toBeVisible({ timeout: 10000 }); // Select "Notes sur 10" const numeric10Card = page.locator('.mode-card').filter({ hasText: /notes \/10/i }); await numeric10Card.click(); // Save await page.getByRole('button', { name: /enregistrer/i }).click(); // Success message should appear await expect(page.getByText(/mis à jour avec succès/i)).toBeVisible({ timeout: 10000 }); // The selected mode should now be "Notes /10" const selectedCard = page.locator('.mode-card.mode-selected'); await expect(selectedCard).toContainText(/notes \/10/i); // Reload the page to verify server-side persistence await page.reload(); await page.waitForLoadState('networkidle'); await expect(page.locator('.mode-card.mode-selected')).toContainText(/notes \/10/i, { timeout: 10000 }); // Restore default mode for other tests const numeric20Card = page.locator('.mode-card').filter({ hasText: /notes \/20/i }); await numeric20Card.click(); await page.getByRole('button', { name: /enregistrer/i }).click(); await expect(page.getByText(/mis à jour avec succès/i)).toBeVisible({ timeout: 10000 }); }); });