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).
582 lines
24 KiB
TypeScript
582 lines
24 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 STUDENT_EMAIL = 'e2e-student-grades@example.com';
|
|
const STUDENT_PASSWORD = 'StudentGrades123';
|
|
const TEACHER_EMAIL = 'e2e-sg-teacher@example.com';
|
|
const TEACHER_PASSWORD = 'TeacherGrades123';
|
|
const TENANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
|
|
|
|
let classId: string;
|
|
let subjectId: string;
|
|
let subject2Id: string;
|
|
let studentId: string;
|
|
let evalId1: string;
|
|
let evalId2: 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 loginAsStudent(page: import('@playwright/test').Page) {
|
|
await page.goto(`${ALPHA_URL}/login`);
|
|
await page.locator('#email').fill(STUDENT_EMAIL);
|
|
await page.locator('#password').fill(STUDENT_PASSWORD);
|
|
await Promise.all([
|
|
page.waitForURL(/\/dashboard/, { timeout: 60000 }),
|
|
page.getByRole('button', { name: /se connecter/i }).click()
|
|
]);
|
|
}
|
|
|
|
test.describe('Student Grade Consultation (Story 6.6)', () => {
|
|
test.describe.configure({ mode: 'serial' });
|
|
|
|
test.beforeAll(async () => {
|
|
// Create users
|
|
createTestUser('ecole-alpha', STUDENT_EMAIL, STUDENT_PASSWORD, 'ROLE_ELEVE --firstName=Émilie --lastName=Dubois');
|
|
createTestUser('ecole-alpha', TEACHER_EMAIL, TEACHER_PASSWORD, 'ROLE_PROF');
|
|
|
|
const { schoolId, academicYearId } = resolveDeterministicIds(TENANT_ID);
|
|
|
|
// Resolve student ID
|
|
const idOutput = 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`
|
|
);
|
|
const idMatch = idOutput.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/);
|
|
studentId = idMatch![0]!;
|
|
|
|
// Create deterministic IDs
|
|
classId = uuid5(`sg-class-${TENANT_ID}`);
|
|
subjectId = uuid5(`sg-subject1-${TENANT_ID}`);
|
|
subject2Id = uuid5(`sg-subject2-${TENANT_ID}`);
|
|
evalId1 = uuid5(`sg-eval1-${TENANT_ID}`);
|
|
evalId2 = uuid5(`sg-eval2-${TENANT_ID}`);
|
|
periodId = uuid5(`sg-period-${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-SG-4A', '4ème', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
|
|
);
|
|
|
|
// Create subjects
|
|
runSql(
|
|
`INSERT INTO subjects (id, tenant_id, school_id, name, code, status, created_at, updated_at) ` +
|
|
`VALUES ('${subjectId}', '${TENANT_ID}', '${schoolId}', 'E2E-SG-Mathématiques', 'E2ESGMATH', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
|
|
);
|
|
runSql(
|
|
`INSERT INTO subjects (id, tenant_id, school_id, name, code, status, created_at, updated_at) ` +
|
|
`VALUES ('${subject2Id}', '${TENANT_ID}', '${schoolId}', 'E2E-SG-Français', 'E2ESGFRA', '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`
|
|
);
|
|
|
|
// 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`
|
|
);
|
|
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}', '${subject2Id}', '${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 1 (Maths - older)
|
|
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 '${evalId1}', '${TENANT_ID}', '${classId}', '${subjectId}', u.id, 'DS Mathématiques', '2026-03-01', 20, 2.0, 'published', NOW(), NOW(), NOW() ` +
|
|
`FROM users u WHERE u.email='${TEACHER_EMAIL}' AND u.tenant_id='${TENANT_ID}' ` +
|
|
`ON CONFLICT (id) DO NOTHING`
|
|
);
|
|
|
|
// Create published evaluation 2 (Français - more recent)
|
|
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 '${evalId2}', '${TENANT_ID}', '${classId}', '${subject2Id}', u.id, 'Dictée', '2026-03-15', 20, 1.0, 'published', NOW(), NOW(), NOW() ` +
|
|
`FROM users u WHERE u.email='${TEACHER_EMAIL}' AND u.tenant_id='${TENANT_ID}' ` +
|
|
`ON CONFLICT (id) DO NOTHING`
|
|
);
|
|
|
|
// Insert grades
|
|
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}', '${evalId1}', '${studentId}', 16.5, 'graded', u.id, NOW(), NOW(), 'Très bon travail' ` +
|
|
`FROM users u WHERE u.email='${TEACHER_EMAIL}' AND u.tenant_id='${TENANT_ID}' ` +
|
|
`ON CONFLICT (evaluation_id, student_id) DO NOTHING`
|
|
);
|
|
runSql(
|
|
`INSERT INTO grades (id, tenant_id, evaluation_id, student_id, value, status, created_by, created_at, updated_at) ` +
|
|
`SELECT gen_random_uuid(), '${TENANT_ID}', '${evalId2}', '${studentId}', 14.0, 'graded', u.id, NOW(), NOW() ` +
|
|
`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 ('${evalId1}', 14.2, 8.0, 18.5, 14.5, 25, NOW()) ON CONFLICT (evaluation_id) DO NOTHING`
|
|
);
|
|
runSql(
|
|
`INSERT INTO evaluation_statistics (evaluation_id, average, min_grade, max_grade, median_grade, graded_count, updated_at) ` +
|
|
`VALUES ('${evalId2}', 12.8, 6.0, 17.0, 13.0, 25, NOW()) ON CONFLICT (evaluation_id) DO NOTHING`
|
|
);
|
|
|
|
// Find the academic period covering the current date (needed for /me/averages auto-detection)
|
|
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(`sg-period-${TENANT_ID}`);
|
|
|
|
// Insert student averages (subject + general)
|
|
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}', 16.5, 1, NOW()) ` +
|
|
`ON CONFLICT (student_id, subject_id, period_id) DO NOTHING`
|
|
);
|
|
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}', '${subject2Id}', '${periodId}', 14.0, 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.25, NOW()) ` +
|
|
`ON CONFLICT (student_id, period_id) DO NOTHING`
|
|
);
|
|
|
|
clearCache();
|
|
});
|
|
|
|
// =========================================================================
|
|
// AC2: Dashboard notes — grades and averages visible
|
|
// =========================================================================
|
|
|
|
test('AC2: student sees recent grades on dashboard', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
|
|
// Dashboard should show grades widget
|
|
const gradesSection = page.locator('.grades-list');
|
|
await expect(gradesSection).toBeVisible({ timeout: 15000 });
|
|
|
|
// Should show at least one grade
|
|
const gradeItems = page.locator('.grade-item');
|
|
await expect(gradeItems.first()).toBeVisible({ timeout: 10000 });
|
|
});
|
|
|
|
test('AC2: student navigates to full grades page', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/student-grades`);
|
|
|
|
// Page title
|
|
await expect(page.getByRole('heading', { name: 'Mes notes' })).toBeVisible({ timeout: 15000 });
|
|
|
|
// Should show grade cards
|
|
const gradeCards = page.locator('.grade-card');
|
|
await expect(gradeCards.first()).toBeVisible({ timeout: 10000 });
|
|
|
|
// Should show both grades (Dictée more recent first)
|
|
await expect(gradeCards).toHaveCount(2);
|
|
});
|
|
|
|
test('AC2: grades show value, subject, and evaluation title', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/student-grades`);
|
|
|
|
await expect(page.locator('.grade-card').first()).toBeVisible({ timeout: 15000 });
|
|
|
|
// Check first grade (Dictée - more recent)
|
|
const firstCard = page.locator('.grade-card').first();
|
|
await expect(firstCard.locator('.grade-subject')).toContainText('E2E-SG-Français');
|
|
await expect(firstCard.locator('.grade-eval-title')).toContainText('Dictée');
|
|
await expect(firstCard.locator('.grade-value')).toContainText('14/20');
|
|
|
|
// Check second grade (DS Maths)
|
|
const secondCard = page.locator('.grade-card').nth(1);
|
|
await expect(secondCard.locator('.grade-subject')).toContainText('E2E-SG-Mathématiques');
|
|
await expect(secondCard.locator('.grade-value')).toContainText('16.5/20');
|
|
});
|
|
|
|
test('AC2: class statistics visible on grades', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/student-grades`);
|
|
|
|
await expect(page.locator('.grade-card').first()).toBeVisible({ timeout: 15000 });
|
|
|
|
const firstStats = page.locator('.grade-card').first().locator('.grade-card-stats');
|
|
await expect(firstStats).toContainText('Moy. classe');
|
|
});
|
|
|
|
test('AC2: appreciation visible on grade', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/student-grades`);
|
|
|
|
await expect(page.locator('.grade-card').first()).toBeVisible({ timeout: 15000 });
|
|
|
|
// The Maths grade has an appreciation
|
|
await expect(page.locator('.grade-appreciation').first()).toContainText('Très bon travail');
|
|
});
|
|
|
|
// =========================================================================
|
|
// AC3: Subject detail — click on subject shows all evaluations
|
|
// =========================================================================
|
|
|
|
test('AC3: click on subject shows detail modal', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/student-grades`);
|
|
|
|
// Wait for averages section
|
|
const avgCard = page.locator('.average-card').first();
|
|
await expect(avgCard).toBeVisible({ timeout: 15000 });
|
|
|
|
// Click on first subject average card
|
|
await avgCard.click();
|
|
|
|
// Modal should appear
|
|
const modal = page.getByRole('dialog');
|
|
await expect(modal).toBeVisible({ timeout: 5000 });
|
|
|
|
// Modal should show grade details
|
|
await expect(modal.locator('.detail-item')).toHaveCount(1);
|
|
});
|
|
|
|
test('AC3: subject detail modal closes with Escape', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/student-grades`);
|
|
|
|
const avgCard = page.locator('.average-card').first();
|
|
await expect(avgCard).toBeVisible({ timeout: 15000 });
|
|
|
|
await avgCard.click();
|
|
|
|
const modal = page.getByRole('dialog');
|
|
await expect(modal).toBeVisible({ timeout: 5000 });
|
|
|
|
await page.keyboard.press('Escape');
|
|
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
|
});
|
|
|
|
// =========================================================================
|
|
// AC4: Discover mode — notes hidden by default, click to reveal
|
|
// =========================================================================
|
|
|
|
test('AC4: discover mode toggle exists', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/student-grades`);
|
|
|
|
await expect(page.locator('.discover-toggle')).toBeVisible({ timeout: 15000 });
|
|
await expect(page.locator('.toggle-label')).toContainText('Mode découverte');
|
|
});
|
|
|
|
test('AC4: enabling discover mode hides grade values', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/student-grades`);
|
|
|
|
await expect(page.locator('.grade-card').first()).toBeVisible({ timeout: 15000 });
|
|
|
|
// Enable discover mode
|
|
const toggle = page.locator('.discover-toggle input');
|
|
await toggle.check();
|
|
|
|
// Grade values should be blurred and reveal hint visible
|
|
await expect(page.locator('.grade-blur').first()).toBeVisible({ timeout: 5000 });
|
|
await expect(page.locator('.reveal-hint').first()).toContainText('Cliquer pour révéler');
|
|
});
|
|
|
|
test('AC4: clicking card in discover mode reveals the grade', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/student-grades`);
|
|
|
|
await expect(page.locator('.grade-card').first()).toBeVisible({ timeout: 15000 });
|
|
|
|
// Enable discover mode
|
|
await page.locator('.discover-toggle input').check();
|
|
|
|
await expect(page.locator('.grade-blur').first()).toBeVisible({ timeout: 5000 });
|
|
|
|
// Click the card to reveal
|
|
await page.locator('.grade-card-btn').first().click();
|
|
|
|
// Grade value should now be visible (no longer blurred)
|
|
const firstCard = page.locator('.grade-card').first();
|
|
await expect(firstCard.locator('.grade-value:not(.grade-blur)')).toBeVisible({ timeout: 5000 });
|
|
});
|
|
|
|
// =========================================================================
|
|
// AC5: Badge "Nouveau" on recent grades
|
|
// =========================================================================
|
|
|
|
test('AC5: new grades show Nouveau badge', async ({ page }) => {
|
|
// Clear localStorage to simulate fresh session
|
|
await page.goto(`${ALPHA_URL}/login`);
|
|
await page.evaluate(() => {
|
|
localStorage.removeItem('classeo_grades_seen');
|
|
localStorage.removeItem('classeo_grade_preferences');
|
|
localStorage.removeItem('classeo_grades_revealed');
|
|
});
|
|
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/student-grades`);
|
|
|
|
await expect(page.locator('.grade-card').first()).toBeVisible({ timeout: 15000 });
|
|
|
|
// Badges should be visible on new grades
|
|
await expect(page.locator('.badge-new').first()).toBeVisible({ timeout: 5000 });
|
|
});
|
|
|
|
// =========================================================================
|
|
// AC2: Averages section visible
|
|
// =========================================================================
|
|
|
|
test('AC2: subject averages section displays correctly', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/student-grades`);
|
|
|
|
// Wait for averages section
|
|
const avgSection = page.locator('.averages-section');
|
|
await expect(avgSection).toBeVisible({ timeout: 15000 });
|
|
|
|
// Should show heading
|
|
await expect(avgSection.getByRole('heading', { name: 'Moyennes par matière' })).toBeVisible();
|
|
|
|
// Should show at least one average card
|
|
const avgCards = page.locator('.average-card');
|
|
await expect(avgCards.first()).toBeVisible({ timeout: 10000 });
|
|
});
|
|
|
|
test('AC2: general average visible on grades page', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/student-grades`);
|
|
|
|
const generalAvg = page.locator('.general-average');
|
|
await expect(generalAvg).toBeVisible({ timeout: 15000 });
|
|
await expect(generalAvg).toContainText('Moyenne générale');
|
|
await expect(generalAvg.locator('.avg-value')).toContainText('/20');
|
|
});
|
|
|
|
// =========================================================================
|
|
// AC4: Discover mode persistence
|
|
// =========================================================================
|
|
|
|
test('AC4: discover mode persists after page reload', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/student-grades`);
|
|
|
|
await expect(page.locator('.grade-card').first()).toBeVisible({ timeout: 15000 });
|
|
|
|
// Enable discover mode
|
|
await page.locator('.discover-toggle input').check();
|
|
await expect(page.locator('.grade-blur').first()).toBeVisible({ timeout: 5000 });
|
|
|
|
// Reload the page
|
|
await page.reload();
|
|
|
|
// Discover mode should still be active after reload
|
|
await expect(page.locator('.grade-card').first()).toBeVisible({ timeout: 15000 });
|
|
await expect(page.locator('.grade-blur').first()).toBeVisible({ timeout: 5000 });
|
|
|
|
// Disable discover mode for cleanup
|
|
await page.locator('.discover-toggle input').uncheck();
|
|
});
|
|
|
|
// =========================================================================
|
|
// Dashboard: grade card pop-in
|
|
// =========================================================================
|
|
|
|
test('dashboard: clicking a grade card opens detail pop-in', async ({ page }) => {
|
|
// Ensure discover mode is off
|
|
await page.goto(`${ALPHA_URL}/login`);
|
|
await page.evaluate(() => localStorage.setItem('classeo_grade_preferences', '{"revealMode":"immediate"}'));
|
|
|
|
await loginAsStudent(page);
|
|
|
|
const gradeBtn = page.locator('.grade-item-btn').first();
|
|
await expect(gradeBtn).toBeVisible({ timeout: 15000 });
|
|
|
|
await gradeBtn.click();
|
|
|
|
// Detail modal should appear
|
|
const modal = page.locator('.grade-detail-modal');
|
|
await expect(modal).toBeVisible({ timeout: 5000 });
|
|
|
|
// Modal shows evaluation title
|
|
await expect(modal.locator('.grade-detail-title')).toBeVisible();
|
|
|
|
// Modal shows grade value
|
|
await expect(modal.locator('.grade-detail-value')).toBeVisible();
|
|
});
|
|
|
|
test('dashboard: grade pop-in shows appreciation', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
|
|
const gradeItems = page.locator('.grade-item-btn');
|
|
await expect(gradeItems.first()).toBeVisible({ timeout: 15000 });
|
|
|
|
// Click the Maths grade which has an appreciation
|
|
// Maths is second in the list (Dictée/Français is more recent)
|
|
await gradeItems.nth(1).click();
|
|
|
|
const modal = page.locator('.grade-detail-modal');
|
|
await expect(modal).toBeVisible({ timeout: 5000 });
|
|
await expect(modal.locator('.grade-detail-appreciation')).toContainText('Très bon travail');
|
|
});
|
|
|
|
test('dashboard: grade pop-in shows class statistics', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
|
|
const gradeBtn = page.locator('.grade-item-btn').first();
|
|
await expect(gradeBtn).toBeVisible({ timeout: 15000 });
|
|
|
|
await gradeBtn.click();
|
|
|
|
const modal = page.locator('.grade-detail-modal');
|
|
await expect(modal).toBeVisible({ timeout: 5000 });
|
|
|
|
// Stats grid with Moyenne, Min, Max
|
|
await expect(modal.locator('.grade-detail-stats')).toBeVisible();
|
|
await expect(modal.locator('.stat-label').first()).toBeVisible();
|
|
});
|
|
|
|
test('dashboard: grade pop-in closes with Escape', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
|
|
const gradeBtn = page.locator('.grade-item-btn').first();
|
|
await expect(gradeBtn).toBeVisible({ timeout: 15000 });
|
|
|
|
await gradeBtn.click();
|
|
|
|
const modal = page.locator('.grade-detail-modal');
|
|
await expect(modal).toBeVisible({ timeout: 5000 });
|
|
|
|
await page.keyboard.press('Escape');
|
|
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
|
});
|
|
|
|
// =========================================================================
|
|
// Dashboard: discover mode
|
|
// =========================================================================
|
|
|
|
test('dashboard: discover mode toggle exists', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
|
|
const toggle = page.locator('.widget-discover-toggle');
|
|
await expect(toggle).toBeVisible({ timeout: 15000 });
|
|
await expect(toggle).toContainText('Mode découverte');
|
|
});
|
|
|
|
test('dashboard: discover mode blurs grades', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
|
|
const toggle = page.locator('.widget-discover-toggle input');
|
|
await expect(toggle).toBeVisible({ timeout: 15000 });
|
|
|
|
await toggle.check();
|
|
|
|
// Grades should be blurred
|
|
await expect(page.locator('.grade-item-btn .grade-blur').first()).toBeVisible({ timeout: 5000 });
|
|
await expect(page.locator('.grade-reveal-hint').first()).toBeVisible();
|
|
|
|
// Cleanup
|
|
await toggle.uncheck();
|
|
});
|
|
|
|
test('dashboard: clicking blurred card reveals grade', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
|
|
// Enable discover mode
|
|
const toggle = page.locator('.widget-discover-toggle input');
|
|
await expect(toggle).toBeVisible({ timeout: 15000 });
|
|
await toggle.check();
|
|
|
|
await expect(page.locator('.grade-item-btn .grade-blur').first()).toBeVisible({ timeout: 5000 });
|
|
|
|
// Click to reveal
|
|
await page.locator('.grade-item-btn').first().click();
|
|
|
|
// Grade value should now be visible (no blur), and no pop-in should open
|
|
const firstItem = page.locator('.grade-item-btn').first();
|
|
await expect(firstItem.locator('.grade-value:not(.grade-blur)')).toBeVisible({ timeout: 5000 });
|
|
await expect(page.locator('.grade-detail-modal')).not.toBeVisible();
|
|
|
|
// Cleanup
|
|
await toggle.uncheck();
|
|
});
|
|
|
|
// =========================================================================
|
|
// Full page: clickable grade cards
|
|
// =========================================================================
|
|
|
|
test('grades page: clicking a grade card opens subject detail modal', async ({ page }) => {
|
|
// Ensure discover mode is off
|
|
await page.goto(`${ALPHA_URL}/login`);
|
|
await page.evaluate(() => localStorage.setItem('classeo_grade_preferences', '{"revealMode":"immediate"}'));
|
|
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/student-grades`);
|
|
|
|
const gradeCard = page.locator('.grade-card-btn').first();
|
|
await expect(gradeCard).toBeVisible({ timeout: 15000 });
|
|
|
|
await gradeCard.click();
|
|
|
|
// Subject detail modal should appear
|
|
const modal = page.getByRole('dialog');
|
|
await expect(modal).toBeVisible({ timeout: 5000 });
|
|
|
|
// Should show detail items
|
|
await expect(modal.locator('.detail-item')).toHaveCount(1);
|
|
});
|
|
|
|
test('grades page: clicking second card opens correct subject modal', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
await page.goto(`${ALPHA_URL}/dashboard/student-grades`);
|
|
|
|
const secondCard = page.locator('.grade-card-btn').nth(1);
|
|
await expect(secondCard).toBeVisible({ timeout: 15000 });
|
|
|
|
await secondCard.click();
|
|
|
|
const modal = page.getByRole('dialog');
|
|
await expect(modal).toBeVisible({ timeout: 5000 });
|
|
|
|
// Should show the Maths subject detail
|
|
await expect(modal.locator('.detail-item')).toHaveCount(1);
|
|
await expect(modal.locator('.detail-title')).toContainText('DS Mathématiques');
|
|
});
|
|
|
|
// =========================================================================
|
|
// Navigation
|
|
// =========================================================================
|
|
|
|
test('student can navigate to grades page from nav bar', async ({ page }) => {
|
|
await loginAsStudent(page);
|
|
|
|
const navLink = page.getByRole('link', { name: /mes notes/i });
|
|
await expect(navLink).toBeVisible({ timeout: 15000 });
|
|
|
|
await navLink.click();
|
|
await page.waitForURL(/student-grades/, { timeout: 10000 });
|
|
|
|
await expect(page.getByRole('heading', { name: 'Mes notes' })).toBeVisible({ timeout: 10000 });
|
|
});
|
|
});
|