|
|
|
|
@@ -0,0 +1,581 @@
|
|
|
|
|
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 });
|
|
|
|
|
});
|
|
|
|
|
});
|