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.
208 lines
8.2 KiB
TypeScript
208 lines
8.2 KiB
TypeScript
import { test, expect, type Page } 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-childselector-admin@example.com';
|
|
const ADMIN_PASSWORD = 'AdminCSTest123';
|
|
const PARENT_EMAIL = 'e2e-childselector-parent@example.com';
|
|
const PARENT_PASSWORD = 'ChildSelectorTest123';
|
|
const PARENT_FIRST_NAME = 'CSParent';
|
|
const PARENT_LAST_NAME = 'TestSelector';
|
|
const STUDENT1_EMAIL = 'e2e-childselector-student1@example.com';
|
|
const STUDENT1_PASSWORD = 'Student1Test123';
|
|
const STUDENT2_EMAIL = 'e2e-childselector-student2@example.com';
|
|
const STUDENT2_PASSWORD = 'Student2Test123';
|
|
|
|
let student1UserId: string;
|
|
let student2UserId: 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];
|
|
}
|
|
|
|
async function loginAsAdmin(page: 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 });
|
|
}
|
|
|
|
async function addGuardianIfNotLinked(page: Page, studentId: string, parentSearchTerm: string, relationship: string) {
|
|
await page.goto(`${ALPHA_URL}/admin/students/${studentId}`);
|
|
await expect(page.locator('.guardian-section')).toBeVisible({ timeout: 10000 });
|
|
await expect(
|
|
page.getByText(/aucun parent\/tuteur/i).or(page.locator('.guardian-list'))
|
|
).toBeVisible({ timeout: 10000 });
|
|
|
|
// Skip if add button is not visible (max guardians already linked)
|
|
const addButton = page.getByRole('button', { name: /ajouter un parent/i });
|
|
if (!(await addButton.isVisible())) return;
|
|
|
|
// Skip if parent is already linked (email visible in guardian list)
|
|
const sectionText = await page.locator('.guardian-section').textContent();
|
|
if (sectionText && sectionText.includes(parentSearchTerm)) return;
|
|
|
|
await addButton.click();
|
|
const dialog = page.getByRole('dialog');
|
|
await expect(dialog).toBeVisible({ timeout: 5000 });
|
|
|
|
const searchInput = dialog.getByRole('combobox', { name: /rechercher/i });
|
|
await searchInput.fill(parentSearchTerm);
|
|
|
|
const listbox = dialog.locator('#parent-search-listbox');
|
|
await expect(listbox).toBeVisible({ timeout: 10000 });
|
|
const option = listbox.locator('[role="option"]').first();
|
|
await option.click();
|
|
|
|
await expect(dialog.getByText(/sélectionné/i)).toBeVisible();
|
|
|
|
await dialog.getByLabel(/type de relation/i).selectOption(relationship);
|
|
await dialog.getByRole('button', { name: 'Ajouter' }).click();
|
|
|
|
// Wait for either success (new link) or error (already linked → 409)
|
|
await expect(
|
|
page.locator('.alert-success').or(page.locator('.alert-error'))
|
|
).toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
async function removeFirstGuardian(page: Page, studentId: string) {
|
|
await page.goto(`${ALPHA_URL}/admin/students/${studentId}`);
|
|
await expect(page.locator('.guardian-section')).toBeVisible({ timeout: 10000 });
|
|
await expect(
|
|
page.getByText(/aucun parent\/tuteur/i).or(page.locator('.guardian-list'))
|
|
).toBeVisible({ timeout: 10000 });
|
|
|
|
// Skip if no guardian to remove
|
|
if (!(await page.locator('.guardian-item').first().isVisible())) return;
|
|
|
|
const guardianItem = page.locator('.guardian-item').first();
|
|
await guardianItem.getByRole('button', { name: /retirer/i }).click();
|
|
await expect(guardianItem.getByText(/confirmer/i)).toBeVisible({ timeout: 5000 });
|
|
await guardianItem.getByRole('button', { name: /oui/i }).click();
|
|
|
|
await expect(page.locator('.alert-success')).toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
test.describe('Child Selector', () => {
|
|
test.describe.configure({ mode: 'serial' });
|
|
|
|
test.beforeAll(async ({ browser }) => {
|
|
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 parent user
|
|
execSync(
|
|
`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${PARENT_EMAIL} --password=${PARENT_PASSWORD} --role=ROLE_PARENT --firstName=${PARENT_FIRST_NAME} --lastName=${PARENT_LAST_NAME} 2>&1`,
|
|
{ encoding: 'utf-8' }
|
|
);
|
|
|
|
// Create student 1
|
|
const student1Output = execSync(
|
|
`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${STUDENT1_EMAIL} --password=${STUDENT1_PASSWORD} --role=ROLE_ELEVE 2>&1`,
|
|
{ encoding: 'utf-8' }
|
|
);
|
|
student1UserId = extractUserId(student1Output);
|
|
|
|
// Create student 2
|
|
const student2Output = execSync(
|
|
`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${STUDENT2_EMAIL} --password=${STUDENT2_PASSWORD} --role=ROLE_ELEVE 2>&1`,
|
|
{ encoding: 'utf-8' }
|
|
);
|
|
student2UserId = extractUserId(student2Output);
|
|
|
|
// Use admin UI to link parent to both students
|
|
const page = await browser.newPage();
|
|
await loginAsAdmin(page);
|
|
await addGuardianIfNotLinked(page, student1UserId, PARENT_EMAIL, 'tuteur');
|
|
await addGuardianIfNotLinked(page, student2UserId, PARENT_EMAIL, 'tutrice');
|
|
await page.close();
|
|
});
|
|
|
|
async function loginAsParent(page: Page) {
|
|
await page.goto(`${ALPHA_URL}/login`);
|
|
await page.locator('#email').fill(PARENT_EMAIL);
|
|
await page.locator('#password').fill(PARENT_PASSWORD);
|
|
await page.getByRole('button', { name: /se connecter/i }).click();
|
|
await page.waitForURL(/\/dashboard/, { timeout: 60000 });
|
|
}
|
|
|
|
test('[P1] parent with multiple children should see child selector', async ({ page }) => {
|
|
await loginAsParent(page);
|
|
|
|
// ChildSelector should be visible when parent has 2+ children
|
|
const childSelector = page.locator('.child-selector');
|
|
await expect(childSelector).toBeVisible({ timeout: 10000 });
|
|
|
|
// Should display the label
|
|
await expect(childSelector.locator('.child-selector-label')).toHaveText('Enfant :');
|
|
|
|
// Should have 3 buttons: "Tous" + 2 children
|
|
const buttons = childSelector.locator('.child-button');
|
|
await expect(buttons).toHaveCount(3);
|
|
|
|
// "Tous" button should be selected initially (no child auto-selected)
|
|
await expect(buttons.first()).toHaveClass(/selected/);
|
|
});
|
|
|
|
test('[P1] parent can switch between children', async ({ page }) => {
|
|
await loginAsParent(page);
|
|
|
|
const childSelector = page.locator('.child-selector');
|
|
await expect(childSelector).toBeVisible({ timeout: 10000 });
|
|
|
|
const buttons = childSelector.locator('.child-button');
|
|
await expect(buttons).toHaveCount(3);
|
|
|
|
// "Tous" button (index 0) should be selected initially
|
|
await expect(buttons.nth(0)).toHaveClass(/selected/);
|
|
await expect(buttons.nth(1)).not.toHaveClass(/selected/);
|
|
|
|
// Click first child button (index 1)
|
|
await buttons.nth(1).click();
|
|
|
|
// First child should now be selected, "Tous" should not
|
|
await expect(buttons.nth(1)).toHaveClass(/selected/);
|
|
await expect(buttons.nth(0)).not.toHaveClass(/selected/);
|
|
});
|
|
|
|
test('[P1] parent with single child should see static child name', async ({ browser, page }) => {
|
|
// Remove one link via admin UI
|
|
const adminPage = await browser.newPage();
|
|
await loginAsAdmin(adminPage);
|
|
await removeFirstGuardian(adminPage, student2UserId);
|
|
await adminPage.close();
|
|
|
|
await loginAsParent(page);
|
|
|
|
// ChildSelector should be visible with 1 child (showing name, no buttons)
|
|
await expect(page.locator('.child-selector')).toBeVisible({ timeout: 5000 });
|
|
await expect(page.locator('.child-button')).toHaveCount(0);
|
|
|
|
// Restore the second link via admin UI for clean state
|
|
const restorePage = await browser.newPage();
|
|
await loginAsAdmin(restorePage);
|
|
await addGuardianIfNotLinked(restorePage, student2UserId, PARENT_EMAIL, 'tutrice');
|
|
await restorePage.close();
|
|
});
|
|
});
|