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-search-admin@example.com'; const ADMIN_PASSWORD = 'SearchTest123'; const projectRoot = join(__dirname, '../..'); const composeFile = join(projectRoot, 'compose.yaml'); test.describe('Admin Search & Pagination (Story 2.8b)', () => { test.beforeAll(async () => { 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' } ); }); 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: 30000 }), page.getByRole('button', { name: /se connecter/i }).click() ]); } // ============================================================================ // USERS PAGE - Search & Pagination // ============================================================================ test.describe('Users Page', () => { test('displays search input on users page', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users`); await page.waitForLoadState('networkidle'); const searchInput = page.locator('input[type="search"]'); await expect(searchInput).toBeVisible({ timeout: 10000 }); await expect(searchInput).toHaveAttribute('placeholder', /rechercher/i); }); test('search filters users and shows results', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users`); await page.waitForLoadState('networkidle'); // Wait for initial load await expect( page.locator('.users-table, .empty-state') ).toBeVisible({ timeout: 10000 }); const searchInput = page.locator('input[type="search"]'); await searchInput.fill('e2e-search-admin'); // Wait for debounce + API response await page.waitForTimeout(500); await page.waitForLoadState('networkidle'); // Should find our test admin user await expect(page.locator('.users-table')).toBeVisible({ timeout: 10000 }); }); test('search with no results shows empty state', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users`); await page.waitForLoadState('networkidle'); await expect( page.locator('.users-table, .empty-state') ).toBeVisible({ timeout: 10000 }); const searchInput = page.locator('input[type="search"]'); await searchInput.fill('zzz-nonexistent-user-xyz'); // Wait for debounce + API response await page.waitForTimeout(500); await page.waitForLoadState('networkidle'); // Should show "Aucun résultat" empty state await expect(page.locator('.empty-state')).toBeVisible({ timeout: 10000 }); await expect(page.locator('.empty-state')).toContainText(/aucun résultat/i); }); test('search term is synced to URL', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users`); await page.waitForLoadState('networkidle'); await expect( page.locator('.users-table, .empty-state') ).toBeVisible({ timeout: 10000 }); const searchInput = page.locator('input[type="search"]'); await searchInput.fill('test-search'); // Wait for debounce await page.waitForTimeout(500); // URL should contain search param await expect(page).toHaveURL(/[?&]search=test-search/); }); test('search term from URL is restored on page load', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users?search=admin`); await page.waitForLoadState('networkidle'); const searchInput = page.locator('input[type="search"]'); await expect(searchInput).toHaveValue('admin'); }); test('clear search button resets results', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users`); await page.waitForLoadState('networkidle'); await expect( page.locator('.users-table, .empty-state') ).toBeVisible({ timeout: 10000 }); const searchInput = page.locator('input[type="search"]'); await searchInput.fill('test'); // Wait for debounce await page.waitForTimeout(500); // Clear button should appear const clearButton = page.locator('.search-clear'); await expect(clearButton).toBeVisible(); await clearButton.click(); // Search input should be empty await expect(searchInput).toHaveValue(''); }); test('Escape key clears search', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users`); await page.waitForLoadState('networkidle'); await expect( page.locator('.users-table, .empty-state') ).toBeVisible({ timeout: 10000 }); const searchInput = page.locator('input[type="search"]'); await searchInput.fill('test'); await page.waitForTimeout(500); await searchInput.press('Escape'); await expect(searchInput).toHaveValue(''); }); test('filters work together with search', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users`); await page.waitForLoadState('networkidle'); await expect( page.locator('.users-table, .empty-state') ).toBeVisible({ timeout: 10000 }); // Apply a role filter await page.locator('#filter-role').selectOption('ROLE_ADMIN'); await page.getByRole('button', { name: /filtrer/i }).click(); await page.waitForLoadState('networkidle'); // Then search const searchInput = page.locator('input[type="search"]'); await searchInput.fill('e2e-search'); await page.waitForTimeout(500); await page.waitForLoadState('networkidle'); // URL should have both params await expect(page).toHaveURL(/search=e2e-search/); }); }); // ============================================================================ // CLASSES PAGE - Search & Pagination // ============================================================================ test.describe('Classes Page', () => { test('displays search input on classes page', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/classes`); await page.waitForLoadState('networkidle'); const searchInput = page.locator('input[type="search"]'); await expect(searchInput).toBeVisible({ timeout: 10000 }); await expect(searchInput).toHaveAttribute('placeholder', /rechercher/i); }); test('search with no results shows empty state', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/classes`); await page.waitForLoadState('networkidle'); // Wait for initial load await expect( page.locator('.classes-grid, .empty-state, .loading-state') ).toBeVisible({ timeout: 10000 }); const searchInput = page.locator('input[type="search"]'); await searchInput.fill('zzz-nonexistent-class-xyz'); await page.waitForTimeout(500); await page.waitForLoadState('networkidle'); await expect(page.locator('.empty-state')).toBeVisible({ timeout: 10000 }); await expect(page.locator('.empty-state')).toContainText(/aucun résultat/i); }); test('search term is synced to URL', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/classes`); await page.waitForLoadState('networkidle'); await expect( page.locator('.classes-grid, .empty-state, .loading-state') ).toBeVisible({ timeout: 10000 }); const searchInput = page.locator('input[type="search"]'); await searchInput.fill('6eme'); await page.waitForTimeout(500); await expect(page).toHaveURL(/[?&]search=6eme/); }); }); // ============================================================================ // SUBJECTS PAGE - Search & Pagination // ============================================================================ test.describe('Subjects Page', () => { test('displays search input on subjects page', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/subjects`); await page.waitForLoadState('networkidle'); const searchInput = page.locator('input[type="search"]'); await expect(searchInput).toBeVisible({ timeout: 10000 }); await expect(searchInput).toHaveAttribute('placeholder', /rechercher/i); }); test('search with no results shows empty state', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/subjects`); await page.waitForLoadState('networkidle'); await expect( page.locator('.subjects-grid, .empty-state, .loading-state') ).toBeVisible({ timeout: 10000 }); const searchInput = page.locator('input[type="search"]'); await searchInput.fill('zzz-nonexistent-subject-xyz'); await page.waitForTimeout(500); await page.waitForLoadState('networkidle'); await expect(page.locator('.empty-state')).toBeVisible({ timeout: 10000 }); await expect(page.locator('.empty-state')).toContainText(/aucun résultat/i); }); test('search term is synced to URL', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/subjects`); await page.waitForLoadState('networkidle'); await expect( page.locator('.subjects-grid, .empty-state, .loading-state') ).toBeVisible({ timeout: 10000 }); const searchInput = page.locator('input[type="search"]'); await searchInput.fill('MATH'); await page.waitForTimeout(500); await expect(page).toHaveURL(/[?&]search=MATH/); }); }); // ============================================================================ // ASSIGNMENTS PAGE - Search & Pagination // ============================================================================ test.describe('Assignments Page', () => { test('displays search input on assignments page', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/assignments`); await page.waitForLoadState('networkidle'); const searchInput = page.locator('input[type="search"]'); await expect(searchInput).toBeVisible({ timeout: 15000 }); await expect(searchInput).toHaveAttribute('placeholder', /rechercher/i); }); test('search with no results shows empty state', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/assignments`); await page.waitForLoadState('networkidle'); // Wait for initial load await expect( page.locator('.table-container, .empty-state, .loading-state') ).toBeVisible({ timeout: 15000 }); const searchInput = page.locator('input[type="search"]'); await searchInput.fill('zzz-nonexistent-teacher-xyz'); await page.waitForTimeout(500); await page.waitForLoadState('networkidle'); await expect(page.locator('.empty-state')).toBeVisible({ timeout: 10000 }); await expect(page.locator('.empty-state')).toContainText(/aucun résultat/i); }); test('search term is synced to URL', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/assignments`); await page.waitForLoadState('networkidle'); await expect( page.locator('.table-container, .empty-state, .loading-state') ).toBeVisible({ timeout: 15000 }); const searchInput = page.locator('input[type="search"]'); await searchInput.fill('Dupont'); await page.waitForTimeout(500); await expect(page).toHaveURL(/[?&]search=Dupont/); }); }); });