Files
Classeo/frontend/e2e/parent-grades.spec.ts
Mathias STRASSER bec211ebf0
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled
feat: Permettre à l'élève de consulter ses notes et moyennes
L'élève avait accès à ses compétences mais pas à ses notes numériques.
Cette fonctionnalité lui donne une vue complète de sa progression scolaire
avec moyennes par matière, détail par évaluation, statistiques de classe,
et un mode "découverte" pour révéler ses notes à son rythme (FR14, FR15).

Les notes ne sont visibles qu'après publication par l'enseignant, ce qui
garantit que l'élève les découvre avant ses parents (délai 24h story 6.7).
2026-04-07 14:43:38 +02:00

242 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 Promise.all([
page.waitForURL(/\/dashboard/, { timeout: 60000 }),
page.getByRole('button', { name: /se connecter/i }).click()
]);
}
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 });
});
});