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.
240 lines
9.6 KiB
TypeScript
240 lines
9.6 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
import {
|
|
execWithRetry,
|
|
runSql,
|
|
clearCache,
|
|
resolveDeterministicIds,
|
|
createTestUser,
|
|
composeFile
|
|
} from './helpers';
|
|
|
|
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 PARENT_EMAIL = 'e2e-parent-grades@example.com';
|
|
const PARENT_PASSWORD = 'ParentGrades123';
|
|
const TEACHER_EMAIL = 'e2e-pg-teacher@example.com';
|
|
const TEACHER_PASSWORD = 'TeacherPG123';
|
|
const STUDENT_EMAIL = 'e2e-pg-student@example.com';
|
|
const STUDENT_PASSWORD = 'StudentPG123';
|
|
const TENANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
|
|
|
|
let parentId: string;
|
|
let studentId: string;
|
|
let classId: string;
|
|
let subjectId: string;
|
|
let evalId: string;
|
|
let periodId: string;
|
|
|
|
function uuid5(name: string): string {
|
|
return execWithRetry(
|
|
`docker compose -f "${composeFile}" exec -T php php -r '` +
|
|
`require "/app/vendor/autoload.php"; ` +
|
|
`echo Ramsey\\Uuid\\Uuid::uuid5("6ba7b814-9dad-11d1-80b4-00c04fd430c8","${name}")->toString();` +
|
|
`' 2>&1`
|
|
).trim();
|
|
}
|
|
|
|
async function loginAsParent(page: import('@playwright/test').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.describe('Parent Grade Consultation (Story 6.7)', () => {
|
|
test.describe.configure({ mode: 'serial' });
|
|
|
|
test.beforeAll(async () => {
|
|
// Create users
|
|
createTestUser(
|
|
'ecole-alpha',
|
|
PARENT_EMAIL,
|
|
PARENT_PASSWORD,
|
|
'ROLE_PARENT --firstName=Marie --lastName=Dupont'
|
|
);
|
|
createTestUser(
|
|
'ecole-alpha',
|
|
TEACHER_EMAIL,
|
|
TEACHER_PASSWORD,
|
|
'ROLE_PROF --firstName=Jean --lastName=Martin'
|
|
);
|
|
createTestUser(
|
|
'ecole-alpha',
|
|
STUDENT_EMAIL,
|
|
STUDENT_PASSWORD,
|
|
'ROLE_ELEVE --firstName=Emma --lastName=Dupont'
|
|
);
|
|
|
|
const { schoolId, academicYearId } = resolveDeterministicIds(TENANT_ID);
|
|
|
|
// Resolve user IDs
|
|
const parentOutput = execWithRetry(
|
|
`docker compose -f "${composeFile}" exec -T php php bin/console dbal:run-sql "SELECT id FROM users WHERE email='${PARENT_EMAIL}' AND tenant_id='${TENANT_ID}'" 2>&1`
|
|
);
|
|
parentId = parentOutput.match(
|
|
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/
|
|
)![0]!;
|
|
|
|
const studentOutput = execWithRetry(
|
|
`docker compose -f "${composeFile}" exec -T php php bin/console dbal:run-sql "SELECT id FROM users WHERE email='${STUDENT_EMAIL}' AND tenant_id='${TENANT_ID}'" 2>&1`
|
|
);
|
|
studentId = studentOutput.match(
|
|
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/
|
|
)![0]!;
|
|
|
|
// Create deterministic IDs
|
|
classId = uuid5(`pg-class-${TENANT_ID}`);
|
|
subjectId = uuid5(`pg-subject-${TENANT_ID}`);
|
|
evalId = uuid5(`pg-eval-${TENANT_ID}`);
|
|
|
|
// Create class
|
|
runSql(
|
|
`INSERT INTO school_classes (id, tenant_id, school_id, academic_year_id, name, level, status, created_at, updated_at) ` +
|
|
`VALUES ('${classId}', '${TENANT_ID}', '${schoolId}', '${academicYearId}', 'E2E-PG-6A', '6ème', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
|
|
);
|
|
|
|
// Create subject
|
|
runSql(
|
|
`INSERT INTO subjects (id, tenant_id, school_id, name, code, status, created_at, updated_at) ` +
|
|
`VALUES ('${subjectId}', '${TENANT_ID}', '${schoolId}', 'E2E-PG-Mathématiques', 'E2EPGMATH', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
|
|
);
|
|
|
|
// Assign student to class
|
|
runSql(
|
|
`INSERT INTO class_assignments (id, tenant_id, user_id, school_class_id, academic_year_id, assigned_at, created_at, updated_at) ` +
|
|
`VALUES (gen_random_uuid(), '${TENANT_ID}', '${studentId}', '${classId}', '${academicYearId}', NOW(), NOW(), NOW()) ON CONFLICT (user_id, academic_year_id) DO NOTHING`
|
|
);
|
|
|
|
// Link parent to student
|
|
runSql(
|
|
`INSERT INTO student_guardians (id, tenant_id, student_id, guardian_id, relationship_type, created_at) ` +
|
|
`VALUES (gen_random_uuid(), '${TENANT_ID}', '${studentId}', '${parentId}', 'mère', NOW()) ON CONFLICT DO NOTHING`
|
|
);
|
|
|
|
// Create teacher assignment
|
|
runSql(
|
|
`INSERT INTO teacher_assignments (id, tenant_id, teacher_id, school_class_id, subject_id, academic_year_id, status, start_date, created_at, updated_at) ` +
|
|
`SELECT gen_random_uuid(), '${TENANT_ID}', u.id, '${classId}', '${subjectId}', '${academicYearId}', 'active', NOW(), NOW(), NOW() ` +
|
|
`FROM users u WHERE u.email = '${TEACHER_EMAIL}' AND u.tenant_id = '${TENANT_ID}' ` +
|
|
`ON CONFLICT DO NOTHING`
|
|
);
|
|
|
|
// Create published evaluation (published 48h ago so delay is passed)
|
|
runSql(
|
|
`INSERT INTO evaluations (id, tenant_id, class_id, subject_id, teacher_id, title, evaluation_date, grade_scale, coefficient, status, grades_published_at, created_at, updated_at) ` +
|
|
`SELECT '${evalId}', '${TENANT_ID}', '${classId}', '${subjectId}', u.id, 'DS Maths Parent', '2026-03-01', 20, 2.0, 'published', NOW() - INTERVAL '48 hours', NOW() - INTERVAL '48 hours', NOW() ` +
|
|
`FROM users u WHERE u.email='${TEACHER_EMAIL}' AND u.tenant_id='${TENANT_ID}' ` +
|
|
`ON CONFLICT (id) DO NOTHING`
|
|
);
|
|
|
|
// Insert grade for student
|
|
runSql(
|
|
`INSERT INTO grades (id, tenant_id, evaluation_id, student_id, value, status, created_by, created_at, updated_at, appreciation) ` +
|
|
`SELECT gen_random_uuid(), '${TENANT_ID}', '${evalId}', '${studentId}', 15.5, 'graded', u.id, NOW(), NOW(), 'Bon travail' ` +
|
|
`FROM users u WHERE u.email='${TEACHER_EMAIL}' AND u.tenant_id='${TENANT_ID}' ` +
|
|
`ON CONFLICT (evaluation_id, student_id) DO NOTHING`
|
|
);
|
|
|
|
// Insert class statistics
|
|
runSql(
|
|
`INSERT INTO evaluation_statistics (evaluation_id, average, min_grade, max_grade, median_grade, graded_count, updated_at) ` +
|
|
`VALUES ('${evalId}', 13.5, 7.0, 18.0, 13.5, 25, NOW()) ON CONFLICT (evaluation_id) DO NOTHING`
|
|
);
|
|
|
|
// Find academic period
|
|
const periodOutput = execWithRetry(
|
|
`docker compose -f "${composeFile}" exec -T php php bin/console dbal:run-sql "SELECT id FROM academic_periods WHERE tenant_id='${TENANT_ID}' AND start_date <= CURRENT_DATE AND end_date >= CURRENT_DATE LIMIT 1" 2>&1`
|
|
);
|
|
const periodMatch = periodOutput.match(
|
|
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/
|
|
);
|
|
periodId = periodMatch ? periodMatch[0]! : uuid5(`pg-period-${TENANT_ID}`);
|
|
|
|
// Insert student averages
|
|
runSql(
|
|
`INSERT INTO student_averages (id, tenant_id, student_id, subject_id, period_id, average, grade_count, updated_at) ` +
|
|
`VALUES (gen_random_uuid(), '${TENANT_ID}', '${studentId}', '${subjectId}', '${periodId}', 15.5, 1, NOW()) ` +
|
|
`ON CONFLICT (student_id, subject_id, period_id) DO NOTHING`
|
|
);
|
|
runSql(
|
|
`INSERT INTO student_general_averages (id, tenant_id, student_id, period_id, average, updated_at) ` +
|
|
`VALUES (gen_random_uuid(), '${TENANT_ID}', '${studentId}', '${periodId}', 15.5, NOW()) ` +
|
|
`ON CONFLICT (student_id, period_id) DO NOTHING`
|
|
);
|
|
|
|
clearCache();
|
|
});
|
|
|
|
// =========================================================================
|
|
// AC2: Parent can see child's grades and averages
|
|
// =========================================================================
|
|
|
|
test('AC2: parent navigates to grades page', async ({ page }) => {
|
|
await loginAsParent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/parent-grades`);
|
|
|
|
await expect(page.getByRole('heading', { name: 'Notes des enfants' })).toBeVisible({
|
|
timeout: 15000
|
|
});
|
|
});
|
|
|
|
test('AC2: parent sees child selector', async ({ page }) => {
|
|
await loginAsParent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/parent-grades`);
|
|
|
|
// Child selector should be visible with the child's name
|
|
await expect(page.getByText('Emma Dupont')).toBeVisible({ timeout: 15000 });
|
|
});
|
|
|
|
test("AC2: parent sees child's grade card", async ({ page }) => {
|
|
await loginAsParent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/parent-grades`);
|
|
|
|
// Wait for grades to load (single child auto-selected)
|
|
await expect(page.getByText('DS Maths Parent')).toBeVisible({ timeout: 15000 });
|
|
await expect(page.locator('.grade-value', { hasText: '15.5/20' }).first()).toBeVisible();
|
|
});
|
|
|
|
// =========================================================================
|
|
// AC4: Subject detail with class statistics
|
|
// =========================================================================
|
|
|
|
test('AC4: parent sees class statistics on grade card', async ({ page }) => {
|
|
await loginAsParent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/parent-grades`);
|
|
|
|
await expect(page.getByText('DS Maths Parent')).toBeVisible({ timeout: 15000 });
|
|
await expect(page.getByText(/Moy\. classe/)).toBeVisible();
|
|
});
|
|
|
|
test('AC4: parent opens subject detail modal', async ({ page }) => {
|
|
await loginAsParent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/parent-grades`);
|
|
|
|
// Wait for grade cards to appear
|
|
await expect(page.getByText('DS Maths Parent')).toBeVisible({ timeout: 15000 });
|
|
|
|
// Click on a grade card to open subject detail
|
|
await page.getByRole('button', { name: /DS Maths Parent/ }).click();
|
|
|
|
// Modal should appear with subject name and grade details
|
|
const modal = page.getByRole('dialog');
|
|
await expect(modal).toBeVisible({ timeout: 5000 });
|
|
await expect(modal.locator('.grade-value', { hasText: '15.5/20' }).first()).toBeVisible();
|
|
});
|
|
|
|
// =========================================================================
|
|
// Navigation
|
|
// =========================================================================
|
|
|
|
test('navigation: parent sees Notes link in nav', async ({ page }) => {
|
|
await loginAsParent(page);
|
|
|
|
await expect(page.getByRole('link', { name: 'Notes' })).toBeVisible({ timeout: 15000 });
|
|
});
|
|
});
|