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.
340 lines
12 KiB
TypeScript
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/);
|
|
});
|
|
});
|
|
});
|