Lorsqu'un super-admin crée un établissement via l'interface, le système doit automatiquement créer la base tenant, exécuter les migrations, créer le premier utilisateur admin et envoyer l'invitation — le tout de manière asynchrone pour ne pas bloquer la réponse HTTP. Ce mécanisme rend chaque établissement opérationnel dès sa création sans intervention manuelle sur l'infrastructure.
338 lines
12 KiB
TypeScript
338 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 page.getByRole('button', { name: /se connecter/i }).click();
|
|
await page.waitForURL(/\/dashboard/, { timeout: 60000 });
|
|
}
|
|
|
|
// ============================================================================
|
|
// 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/, { timeout: 15000 });
|
|
});
|
|
|
|
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/);
|
|
});
|
|
});
|
|
});
|