Les listes admin (utilisateurs, classes, matières, affectations) chargeaient toutes les données d'un coup, ce qui dégradait l'expérience avec un volume croissant. La pagination côté serveur existait dans la config API Platform mais aucun Provider ne l'exploitait. Cette implémentation ajoute la pagination serveur (30 items/page, max 100) avec recherche textuelle sur toutes les sections, des composants frontend réutilisables (Pagination + SearchInput avec debounce), et la synchronisation URL pour le partage de liens filtrés. Les Query valident leurs paramètres (clamp page/limit, trim search) pour éviter les abus. Les affectations utilisent des lookup maps pour résoudre les noms sans N+1 queries. Les pages admin gèrent les race conditions via AbortController.
119 lines
4.5 KiB
TypeScript
119 lines
4.5 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-actlink-admin@example.com';
|
|
const ADMIN_PASSWORD = 'ActLinkTest123';
|
|
const STUDENT_EMAIL = 'e2e-actlink-student@example.com';
|
|
const STUDENT_PASSWORD = 'StudentTest123';
|
|
const UNIQUE_SUFFIX = Date.now();
|
|
const PARENT_EMAIL = `e2e-actlink-parent-${UNIQUE_SUFFIX}@example.com`;
|
|
const PARENT_PASSWORD = 'ParentActivation1!';
|
|
|
|
let studentUserId: string;
|
|
let activationToken: string;
|
|
|
|
function extractUserId(output: string): string {
|
|
const match = output.match(/User ID\s+([a-f0-9-]{36})/i);
|
|
if (!match) {
|
|
throw new Error(`Could not extract User ID from command output:\n${output}`);
|
|
}
|
|
return match[1];
|
|
}
|
|
|
|
test.describe('Activation with Parent-Child Auto-Link', () => {
|
|
test.describe.configure({ mode: 'serial' });
|
|
|
|
test.beforeAll(async () => {
|
|
const projectRoot = join(__dirname, '../..');
|
|
const composeFile = join(projectRoot, 'compose.yaml');
|
|
|
|
// Create admin user
|
|
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' }
|
|
);
|
|
|
|
// Create student user and capture userId
|
|
const studentOutput = execSync(
|
|
`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${STUDENT_EMAIL} --password=${STUDENT_PASSWORD} --role=ROLE_ELEVE 2>&1`,
|
|
{ encoding: 'utf-8' }
|
|
);
|
|
studentUserId = extractUserId(studentOutput);
|
|
|
|
// Clean up any existing guardian links for this student
|
|
try {
|
|
execSync(
|
|
`docker compose -f "${composeFile}" exec -T php php bin/console dbal:run-sql "DELETE FROM student_guardians WHERE student_id = '${studentUserId}'" 2>&1`,
|
|
{ encoding: 'utf-8' }
|
|
);
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
|
|
// Create activation token for parent WITH student-id for auto-linking
|
|
const tokenOutput = execSync(
|
|
`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-activation-token --email=${PARENT_EMAIL} --role=PARENT --tenant=ecole-alpha --student-id=${studentUserId} 2>&1`,
|
|
{ encoding: 'utf-8' }
|
|
);
|
|
|
|
const tokenMatch = tokenOutput.match(/Token\s+([a-f0-9-]{36})/i);
|
|
if (!tokenMatch) {
|
|
throw new Error(`Could not extract token from command output:\n${tokenOutput}`);
|
|
}
|
|
activationToken = tokenMatch[1];
|
|
});
|
|
|
|
test('[P1] should activate parent account and auto-link to student', async ({ page }) => {
|
|
// Navigate to the activation page
|
|
await page.goto(`${ALPHA_URL}/activate/${activationToken}`);
|
|
|
|
// Wait for the activation form to load
|
|
await expect(page.locator('#password')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Fill the password form
|
|
await page.locator('#password').fill(PARENT_PASSWORD);
|
|
await page.locator('#passwordConfirmation').fill(PARENT_PASSWORD);
|
|
|
|
// Wait for validation to pass and submit
|
|
const submitButton = page.getByRole('button', { name: /activer mon compte/i });
|
|
await expect(submitButton).toBeEnabled({ timeout: 5000 });
|
|
await submitButton.click();
|
|
|
|
// Should redirect to login with activated=true
|
|
await page.waitForURL(/\/login\?activated=true/, { timeout: 15000 });
|
|
|
|
// Now login as admin to verify the auto-link
|
|
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()
|
|
]);
|
|
|
|
// Navigate to the student's page to check guardian list
|
|
await page.goto(`${ALPHA_URL}/admin/students/${studentUserId}`);
|
|
|
|
// Wait for the guardian section to load
|
|
await expect(page.locator('.guardian-section')).toBeVisible({ timeout: 10000 });
|
|
await expect(
|
|
page.locator('.guardian-list')
|
|
).toBeVisible({ timeout: 10000 });
|
|
|
|
// The auto-linked parent should appear in the guardian list
|
|
const guardianItem = page.locator('.guardian-item').first();
|
|
await expect(guardianItem).toBeVisible();
|
|
// Auto-linking uses RelationshipType::OTHER → label "Autre"
|
|
await expect(guardianItem).toContainText('Autre');
|
|
});
|
|
});
|