Files
Classeo/frontend/e2e/admin-search-pagination.spec.ts
Mathias STRASSER aedde6707e
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled
feat: Calculer automatiquement les moyennes après chaque saisie de notes
Les enseignants ont besoin de moyennes à jour immédiatement après la
publication ou modification des notes, sans attendre un batch nocturne.

Le système recalcule via Domain Events synchrones : statistiques
d'évaluation (min/max/moyenne/médiane), moyennes matières pondérées
(normalisation /20), et moyenne générale par élève. Les résultats sont
stockés dans des tables dénormalisées avec cache Redis (TTL 5 min).

Trois endpoints API exposent les données avec contrôle d'accès par rôle.
Une commande console permet le backfill des données historiques au
déploiement.
2026-03-31 16:43:10 +02:00

340 lines
12 KiB
TypeScript

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: 60000 }),
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/);
});
});
});