Les helpers loginAs* utilisaient un pattern séquentiel (click → wait) qui crée une race condition : la navigation peut se terminer avant que le listener soit en place. Firefox sur CI est particulièrement sensible. Le fix remplace ce pattern par Promise.all([waitForURL, click]) dans les 14 fichiers E2E concernés, alignant le code sur le pattern robuste déjà utilisé dans login.spec.ts.
199 lines
7.6 KiB
TypeScript
199 lines
7.6 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 STUDENT1_EMAIL = 'e2e-childselector-student1@example.com';
|
|
const STUDENT1_PASSWORD = 'Student1Test123';
|
|
const STUDENT2_EMAIL = 'e2e-childselector-student2@example.com';
|
|
const STUDENT2_PASSWORD = 'Student2Test123';
|
|
|
|
let parentUserId: string;
|
|
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 Promise.all([
|
|
page.waitForURL(/\/dashboard/, { timeout: 10000 }),
|
|
page.getByRole('button', { name: /se connecter/i }).click()
|
|
]);
|
|
}
|
|
|
|
async function addGuardianIfNotLinked(page: Page, studentId: string, guardianId: 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;
|
|
|
|
await addButton.click();
|
|
const dialog = page.getByRole('dialog');
|
|
await expect(dialog).toBeVisible({ timeout: 5000 });
|
|
|
|
await dialog.getByLabel(/id du parent/i).fill(guardianId);
|
|
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
|
|
const parentOutput = 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 2>&1`,
|
|
{ encoding: 'utf-8' }
|
|
);
|
|
parentUserId = extractUserId(parentOutput);
|
|
|
|
// 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, parentUserId, 'tuteur');
|
|
await addGuardianIfNotLinked(page, student2UserId, parentUserId, '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 Promise.all([
|
|
page.waitForURL(/\/dashboard/, { timeout: 10000 }),
|
|
page.getByRole('button', { name: /se connecter/i }).click()
|
|
]);
|
|
}
|
|
|
|
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 2 child buttons
|
|
const buttons = childSelector.locator('.child-button');
|
|
await expect(buttons).toHaveCount(2);
|
|
|
|
// First child should be 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(2);
|
|
|
|
// First button should be selected initially
|
|
await expect(buttons.first()).toHaveClass(/selected/);
|
|
await expect(buttons.nth(1)).not.toHaveClass(/selected/);
|
|
|
|
// Click second button
|
|
await buttons.nth(1).click();
|
|
|
|
// Second button should now be selected, first should not
|
|
await expect(buttons.nth(1)).toHaveClass(/selected/);
|
|
await expect(buttons.first()).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, parentUserId, 'tutrice');
|
|
await restorePage.close();
|
|
});
|
|
});
|