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); // Extract port from PLAYWRIGHT_BASE_URL or use default (4173 matches playwright.config.ts) 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}`; // Test credentials const ADMIN_EMAIL = 'e2e-class-detail-admin@example.com'; const ADMIN_PASSWORD = 'ClassDetail123'; test.describe('Admin Class Detail Page [P1]', () => { test.describe.configure({ mode: 'serial' }); // Class name used for the test class (shared across serial tests) const CLASS_NAME = `DetailTest-${Date.now()}`; test.beforeAll(async () => { const projectRoot = join(__dirname, '../..'); const composeFile = join(projectRoot, 'compose.yaml'); // Clear caches to prevent stale data and rate limiter issues try { execSync( `docker compose -f "${composeFile}" exec -T php php bin/console cache:pool:clear cache.rate_limiter --env=dev 2>&1`, { encoding: 'utf-8' } ); } catch { // Cache pool may not exist } // Create admin user (or reuse existing) 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' } ); }); test.beforeEach(async () => { const projectRoot = join(__dirname, '../..'); const composeFile = join(projectRoot, 'compose.yaml'); try { execSync( `docker compose -f "${composeFile}" exec -T php php bin/console cache:pool:clear cache.rate_limiter --env=dev 2>&1`, { encoding: 'utf-8' } ); } catch { // Cache pool may not exist } }); 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() ]); } // Helper to open "Nouvelle classe" dialog with proper wait async function openNewClassDialog(page: import('@playwright/test').Page) { const button = page.getByRole('button', { name: /nouvelle classe/i }); await button.waitFor({ state: 'visible' }); await page.waitForLoadState('networkidle'); await button.click(); const dialog = page.getByRole('dialog'); try { await expect(dialog).toBeVisible({ timeout: 5000 }); } catch { // Retry once - webkit sometimes needs a second click await button.click(); await expect(dialog).toBeVisible({ timeout: 10000 }); } } // Helper to create a class and navigate to its detail page async function createClassAndNavigateToDetail(page: import('@playwright/test').Page, name: string) { await page.goto(`${ALPHA_URL}/admin/classes`); await expect(page.getByRole('heading', { name: /gestion des classes/i })).toBeVisible(); await openNewClassDialog(page); await page.locator('#class-name').fill(name); await page.locator('#class-level').selectOption('CM1'); await page.locator('#class-capacity').fill('25'); await page.getByRole('button', { name: /créer la classe/i }).click(); await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 }); // Use search to find the class (pagination may push it off the first page) const searchInput = page.locator('input[type="search"]'); if (await searchInput.isVisible()) { await searchInput.fill(name); await page.waitForTimeout(500); await page.waitForLoadState('networkidle'); } // Click modify to go to detail page const classCard = page.locator('.class-card', { hasText: name }); await expect(classCard).toBeVisible({ timeout: 10000 }); await classCard.getByRole('button', { name: /modifier/i }).click(); // Verify we are on the edit page await expect(page).toHaveURL(/\/admin\/classes\/[\w-]+/); } // ============================================================================ // Navigate to class detail page and see edit form // ============================================================================ test('[P1] navigates to class detail page and sees edit form', async ({ page }) => { await loginAsAdmin(page); await createClassAndNavigateToDetail(page, CLASS_NAME); // Should show the edit form heading await expect( page.getByRole('heading', { name: /modifier la classe/i }) ).toBeVisible(); // Form fields should be pre-populated await expect(page.locator('#class-name')).toHaveValue(CLASS_NAME); await expect(page.locator('#class-level')).toHaveValue('CM1'); await expect(page.locator('#class-capacity')).toHaveValue('25'); // Breadcrumb should be visible await expect(page.locator('.breadcrumb')).toBeVisible(); await expect( page.getByRole('main').getByRole('link', { name: 'Classes' }) ).toBeVisible(); }); // ============================================================================ // Modify class name and save successfully // ============================================================================ test('[P1] modifies class name and saves successfully', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/classes`); // Create a fresh class for this test const originalName = `ModName-${Date.now()}`; await openNewClassDialog(page); await page.locator('#class-name').fill(originalName); await page.getByRole('button', { name: /créer la classe/i }).click(); await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 }); // Search for the newly created class to handle pagination const searchModify = page.locator('input[type="search"]'); if (await searchModify.isVisible()) { await searchModify.fill(originalName); await page.waitForLoadState('networkidle'); } // Navigate to edit page const classCard = page.locator('.class-card', { hasText: originalName }); await expect(classCard).toBeVisible({ timeout: 15000 }); await classCard.getByRole('button', { name: /modifier/i }).click(); await expect(page).toHaveURL(/\/admin\/classes\/[\w-]+/); // Modify the name const newName = `Renamed-${Date.now()}`; await page.locator('#class-name').fill(newName); // Save await page.getByRole('button', { name: /enregistrer/i }).click(); // Should show success message await expect(page.getByText(/modifiée avec succès/i)).toBeVisible({ timeout: 10000 }); // Go back to list and verify the new name appears (use search for pagination) await page.goto(`${ALPHA_URL}/admin/classes`); await page.waitForLoadState('networkidle'); const searchInput = page.locator('input[type="search"]'); if (await searchInput.isVisible()) { await searchInput.fill(newName); await page.waitForLoadState('networkidle'); } await expect(page.getByText(newName)).toBeVisible({ timeout: 15000 }); }); // ============================================================================ // Modify class level and save // ============================================================================ test('[P1] modifies class level and saves successfully', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/classes`); // Create a class with specific level const className = `ModLevel-${Date.now()}`; await openNewClassDialog(page); await page.locator('#class-name').fill(className); await page.locator('#class-level').selectOption('CE1'); await page.getByRole('button', { name: /créer la classe/i }).click(); await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 }); // Navigate to edit page const classCard = page.locator('.class-card', { hasText: className }); await classCard.getByRole('button', { name: /modifier/i }).click(); await expect(page).toHaveURL(/\/admin\/classes\/[\w-]+/); // Change level from CE1 to CM2 await page.locator('#class-level').selectOption('CM2'); // Save await page.getByRole('button', { name: /enregistrer/i }).click(); // Should show success message await expect(page.getByText(/modifiée avec succès/i)).toBeVisible({ timeout: 10000 }); // Go back and verify the level changed in the card // Search for the class by name to find it regardless of pagination await page.goto(`${ALPHA_URL}/admin/classes`); const searchInput = page.locator('input[type="search"]'); await searchInput.fill(className); await page.waitForTimeout(500); await page.waitForLoadState('networkidle'); const updatedCard = page.locator('.class-card', { hasText: className }); await expect(updatedCard).toBeVisible({ timeout: 10000 }); await expect(updatedCard.getByText('CM2')).toBeVisible({ timeout: 5000 }); }); // ============================================================================ // Cancel modification preserves original values // ============================================================================ test('[P1] cancelling modification preserves original values', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/classes`); // Create a class const originalName = `NoCancel-${Date.now()}`; await openNewClassDialog(page); await page.locator('#class-name').fill(originalName); await page.locator('#class-level').selectOption('6ème'); await page.getByRole('button', { name: /créer la classe/i }).click(); await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 }); // Search for the newly created class to handle pagination const searchCancel = page.locator('input[type="search"]'); if (await searchCancel.isVisible()) { await searchCancel.fill(originalName); await page.waitForLoadState('networkidle'); } // Navigate to edit page const classCard = page.locator('.class-card', { hasText: originalName }); await expect(classCard).toBeVisible({ timeout: 15000 }); await classCard.getByRole('button', { name: /modifier/i }).click(); await expect(page).toHaveURL(/\/admin\/classes\/[\w-]+/); // Modify the name but cancel await page.locator('#class-name').fill('Should-Not-Persist'); await page.locator('#class-level').selectOption('CM2'); // Cancel await page.getByRole('button', { name: /annuler/i }).click(); // Should go back to the classes list await expect(page).toHaveURL(/\/admin\/classes$/, { timeout: 10000 }); // Search for the class to handle pagination const searchAfterCancel = page.locator('input[type="search"]'); if (await searchAfterCancel.isVisible()) { await searchAfterCancel.fill(originalName); await page.waitForLoadState('networkidle'); } // The original name should still be visible, modified name should not await expect(page.getByText(originalName)).toBeVisible({ timeout: 10000 }); await expect(page.getByText('Should-Not-Persist')).not.toBeVisible(); }); // ============================================================================ // Breadcrumb navigation back to classes list // ============================================================================ test('[P1] breadcrumb navigates back to classes list', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/classes`); // Create a class const className = `Breadcrumb-${Date.now()}`; await openNewClassDialog(page); await page.locator('#class-name').fill(className); await page.getByRole('button', { name: /créer la classe/i }).click(); await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 }); // Navigate to edit page const classCard = page.locator('.class-card', { hasText: className }); await classCard.getByRole('button', { name: /modifier/i }).click(); await expect(page).toHaveURL(/\/admin\/classes\/[\w-]+/); // Click breadcrumb "Classes" link (scoped to main to avoid nav link) await page.getByRole('main').getByRole('link', { name: 'Classes' }).click(); // Should navigate back to the classes list await expect(page).toHaveURL(/\/admin\/classes$/); await expect( page.getByRole('heading', { name: /gestion des classes/i }) ).toBeVisible(); }); // ============================================================================ // Empty required field (name) prevents submission // ============================================================================ test('[P1] empty required field (name) prevents submission', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/classes`); // Create a class const className = `EmptyField-${Date.now()}`; await openNewClassDialog(page); await page.locator('#class-name').fill(className); await page.getByRole('button', { name: /créer la classe/i }).click(); await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 }); // Navigate to edit page const classCard = page.locator('.class-card', { hasText: className }); await classCard.getByRole('button', { name: /modifier/i }).click(); await expect(page).toHaveURL(/\/admin\/classes\/[\w-]+/); // Clear the required name field await page.locator('#class-name').fill(''); // Submit button should be disabled when name is empty const submitButton = page.getByRole('button', { name: /enregistrer/i }); await expect(submitButton).toBeDisabled(); }); });