Les enseignants ont besoin de moyennes à jour immédiatement après la publication ou modification des notes, sans attendre un batch nocturne. Le système recalcule via Domain Events synchrones : statistiques d'évaluation (min/max/moyenne/médiane), moyennes matières pondérées (normalisation /20), et moyenne générale par élève. Les résultats sont stockés dans des tables dénormalisées avec cache Redis (TTL 5 min). Trois endpoints API exposent les données avec contrôle d'accès par rôle. Une commande console permet le backfill des données historiques au déploiement.
487 lines
22 KiB
TypeScript
487 lines
22 KiB
TypeScript
import { test, expect } 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 TEACHER_EMAIL = 'e2e-eval-teacher@example.com';
|
|
const TEACHER_PASSWORD = 'EvalTest123';
|
|
const TENANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
|
|
|
|
const projectRoot = join(__dirname, '../..');
|
|
const composeFile = join(projectRoot, 'compose.yaml');
|
|
|
|
function runSql(sql: string) {
|
|
execSync(
|
|
`docker compose -f "${composeFile}" exec -T php php bin/console dbal:run-sql "${sql}" 2>&1`,
|
|
{ encoding: 'utf-8' }
|
|
);
|
|
}
|
|
|
|
function clearCache() {
|
|
try {
|
|
execSync(
|
|
`docker compose -f "${composeFile}" exec -T php php bin/console cache:pool:clear paginated_queries.cache 2>&1`,
|
|
{ encoding: 'utf-8' }
|
|
);
|
|
} catch {
|
|
// Cache pool may not exist
|
|
}
|
|
}
|
|
|
|
function resolveDeterministicIds(): { schoolId: string; academicYearId: string } {
|
|
const output = execSync(
|
|
`docker compose -f "${composeFile}" exec -T php php -r '` +
|
|
`require "/app/vendor/autoload.php"; ` +
|
|
`$t="${TENANT_ID}"; $ns="6ba7b814-9dad-11d1-80b4-00c04fd430c8"; ` +
|
|
`echo Ramsey\\Uuid\\Uuid::uuid5($ns,"school-$t")->toString()."\\n"; ` +
|
|
`$m=(int)date("n"); $s=$m>=9?(int)date("Y"):(int)date("Y")-1; $e=$s+1; ` +
|
|
`echo Ramsey\\Uuid\\Uuid::uuid5($ns,"$t:$s-$e")->toString();` +
|
|
`' 2>&1`,
|
|
{ encoding: 'utf-8' }
|
|
).trim();
|
|
const [schoolId, academicYearId] = output.split('\n');
|
|
return { schoolId: schoolId!, academicYearId: academicYearId! };
|
|
}
|
|
|
|
async function loginAsTeacher(page: import('@playwright/test').Page) {
|
|
await page.goto(`${ALPHA_URL}/login`);
|
|
await page.locator('#email').fill(TEACHER_EMAIL);
|
|
await page.locator('#password').fill(TEACHER_PASSWORD);
|
|
await Promise.all([
|
|
page.waitForURL(/\/dashboard/, { timeout: 60000 }),
|
|
page.getByRole('button', { name: /se connecter/i }).click()
|
|
]);
|
|
}
|
|
|
|
async function navigateToEvaluations(page: import('@playwright/test').Page) {
|
|
await page.goto(`${ALPHA_URL}/dashboard/teacher/evaluations`);
|
|
await expect(page.getByRole('heading', { name: /mes évaluations/i })).toBeVisible({ timeout: 15000 });
|
|
}
|
|
|
|
async function selectClassAndSubject(page: import('@playwright/test').Page) {
|
|
const classSelect = page.locator('#ev-class');
|
|
await expect(classSelect).toBeVisible();
|
|
await classSelect.selectOption({ index: 1 });
|
|
|
|
// Wait for subject options to appear after class selection
|
|
const subjectSelect = page.locator('#ev-subject');
|
|
await expect(subjectSelect).toBeEnabled({ timeout: 5000 });
|
|
await expect(subjectSelect.locator('option')).not.toHaveCount(1, { timeout: 10000 });
|
|
await subjectSelect.selectOption({ index: 1 });
|
|
}
|
|
|
|
function seedTeacherAssignments() {
|
|
const { academicYearId } = resolveDeterministicIds();
|
|
try {
|
|
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, c.id, s.id, '${academicYearId}', 'active', NOW(), NOW(), NOW() ` +
|
|
`FROM users u CROSS JOIN school_classes c CROSS JOIN subjects s ` +
|
|
`WHERE u.email = '${TEACHER_EMAIL}' AND u.tenant_id = '${TENANT_ID}' ` +
|
|
`AND c.tenant_id = '${TENANT_ID}' ` +
|
|
`AND s.tenant_id = '${TENANT_ID}' ` +
|
|
`ON CONFLICT DO NOTHING`
|
|
);
|
|
} catch {
|
|
// Table may not exist
|
|
}
|
|
}
|
|
|
|
test.describe('Evaluation Management (Story 6.1)', () => {
|
|
test.beforeAll(async () => {
|
|
// Create teacher user
|
|
execSync(
|
|
`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${TEACHER_EMAIL} --password=${TEACHER_PASSWORD} --role=ROLE_PROF 2>&1`,
|
|
{ encoding: 'utf-8' }
|
|
);
|
|
|
|
// Ensure classes and subject exist
|
|
const { schoolId, academicYearId } = resolveDeterministicIds();
|
|
try {
|
|
runSql(
|
|
`INSERT INTO school_classes (id, tenant_id, school_id, academic_year_id, name, level, status, created_at, updated_at) ` +
|
|
`VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', '${academicYearId}', 'E2E-EVAL-6A', '6ème', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
|
|
);
|
|
runSql(
|
|
`INSERT INTO subjects (id, tenant_id, school_id, name, code, status, created_at, updated_at) ` +
|
|
`VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', 'E2E-EVAL-Maths', 'E2EVALM', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
|
|
);
|
|
} catch {
|
|
// May already exist
|
|
}
|
|
|
|
seedTeacherAssignments();
|
|
clearCache();
|
|
});
|
|
|
|
test.beforeEach(async () => {
|
|
// Clean up ALL evaluations for this teacher (not just by tenant, to avoid
|
|
// stale data from parallel test files with different teachers)
|
|
try {
|
|
runSql(`DELETE FROM grade_events WHERE grade_id IN (SELECT g.id FROM grades g JOIN evaluations e ON g.evaluation_id = e.id WHERE e.tenant_id = '${TENANT_ID}' AND e.teacher_id IN (SELECT id FROM users WHERE email = '${TEACHER_EMAIL}' AND tenant_id = '${TENANT_ID}'))`);
|
|
runSql(`DELETE FROM grades WHERE evaluation_id IN (SELECT id FROM evaluations WHERE tenant_id = '${TENANT_ID}' AND teacher_id IN (SELECT id FROM users WHERE email = '${TEACHER_EMAIL}' AND tenant_id = '${TENANT_ID}'))`);
|
|
runSql(`DELETE FROM evaluations WHERE tenant_id = '${TENANT_ID}' AND teacher_id IN (SELECT id FROM users WHERE email = '${TEACHER_EMAIL}' AND tenant_id = '${TENANT_ID}')`);
|
|
} catch {
|
|
// Table may not exist
|
|
}
|
|
|
|
const { schoolId, academicYearId } = resolveDeterministicIds();
|
|
try {
|
|
runSql(
|
|
`INSERT INTO school_classes (id, tenant_id, school_id, academic_year_id, name, level, status, created_at, updated_at) ` +
|
|
`VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', '${academicYearId}', 'E2E-EVAL-6A', '6ème', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
|
|
);
|
|
runSql(
|
|
`INSERT INTO subjects (id, tenant_id, school_id, name, code, status, created_at, updated_at) ` +
|
|
`VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', 'E2E-EVAL-Maths', 'E2EVALM', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
|
|
);
|
|
} catch {
|
|
// May already exist
|
|
}
|
|
|
|
seedTeacherAssignments();
|
|
clearCache();
|
|
});
|
|
|
|
// ============================================================================
|
|
// Navigation
|
|
// ============================================================================
|
|
test.describe('Navigation', () => {
|
|
test('evaluations link appears in teacher navigation', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
const nav = page.locator('.desktop-nav');
|
|
await expect(nav.getByRole('link', { name: /évaluations/i })).toBeVisible({ timeout: 15000 });
|
|
});
|
|
|
|
test('can navigate to evaluations page', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToEvaluations(page);
|
|
await expect(page.getByRole('heading', { name: /mes évaluations/i })).toBeVisible();
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Empty State
|
|
// ============================================================================
|
|
test.describe('Empty State', () => {
|
|
test('shows empty state when no evaluations exist', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToEvaluations(page);
|
|
await expect(page.getByText(/aucune évaluation/i)).toBeVisible({ timeout: 10000 });
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// AC1-AC5: Create Evaluation
|
|
// ============================================================================
|
|
test.describe('AC1-AC5: Create Evaluation', () => {
|
|
test('can create a new evaluation with default grade scale and coefficient', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToEvaluations(page);
|
|
|
|
// Open create modal
|
|
await page.getByRole('button', { name: /nouvelle évaluation/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Select class and subject
|
|
await selectClassAndSubject(page);
|
|
|
|
// Fill title
|
|
await page.locator('#ev-title').fill('Contrôle chapitre 5');
|
|
|
|
// Fill date
|
|
await page.locator('#ev-date').fill('2026-06-15');
|
|
|
|
// Submit
|
|
await page.getByRole('button', { name: 'Créer', exact: true }).click();
|
|
|
|
// Wait for modal to close (creation succeeded)
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 15000 });
|
|
|
|
// Verify evaluation appears in list
|
|
await expect(page.getByText('Contrôle chapitre 5')).toBeVisible({ timeout: 10000 });
|
|
// Verify default grade scale and coefficient badges
|
|
await expect(page.getByText('/20')).toBeVisible();
|
|
await expect(page.getByText('x1')).toBeVisible();
|
|
});
|
|
|
|
test('can create evaluation with custom grade scale and coefficient', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToEvaluations(page);
|
|
|
|
await page.getByRole('button', { name: /nouvelle évaluation/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Select class and subject
|
|
await selectClassAndSubject(page);
|
|
|
|
// Fill form
|
|
await page.locator('#ev-title').fill('QCM rapide');
|
|
await page.locator('#ev-date').fill('2026-06-20');
|
|
|
|
// Set custom grade scale /10
|
|
await page.locator('#ev-scale').fill('10');
|
|
|
|
// Set coefficient to 0.5
|
|
await page.locator('#ev-coeff').fill('0.5');
|
|
|
|
// Submit
|
|
await page.getByRole('button', { name: 'Créer', exact: true }).click();
|
|
|
|
// Verify evaluation with custom values
|
|
await expect(page.getByText('QCM rapide')).toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByText('/10')).toBeVisible();
|
|
await expect(page.getByText('x0.5')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// AC6: Edit Evaluation
|
|
// ============================================================================
|
|
test.describe('AC6: Edit Evaluation', () => {
|
|
test('can modify title, description, and coefficient', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToEvaluations(page);
|
|
|
|
// Create an evaluation first
|
|
await page.getByRole('button', { name: /nouvelle évaluation/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
await selectClassAndSubject(page);
|
|
await page.locator('#ev-title').fill('Évaluation originale');
|
|
await page.locator('#ev-date').fill('2026-06-15');
|
|
await page.getByRole('button', { name: 'Créer', exact: true }).click();
|
|
await expect(page.getByText('Évaluation originale')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Open edit modal
|
|
await page.getByRole('button', { name: /modifier/i }).first().click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Modify title
|
|
await page.locator('#edit-title').fill('Évaluation modifiée');
|
|
|
|
// Modify coefficient
|
|
await page.locator('#edit-coeff').fill('2');
|
|
|
|
// Submit
|
|
await page.getByRole('button', { name: /enregistrer/i }).click();
|
|
|
|
// Verify changes
|
|
await expect(page.getByText('Évaluation modifiée')).toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByText('x2')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Delete Evaluation
|
|
// ============================================================================
|
|
test.describe('Delete Evaluation', () => {
|
|
test('can delete an evaluation', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToEvaluations(page);
|
|
|
|
// Create an evaluation first
|
|
await page.getByRole('button', { name: /nouvelle évaluation/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
await selectClassAndSubject(page);
|
|
await page.locator('#ev-title').fill('Évaluation à supprimer');
|
|
await page.locator('#ev-date').fill('2026-06-15');
|
|
await page.getByRole('button', { name: 'Créer', exact: true }).click();
|
|
await expect(page.getByText('Évaluation à supprimer')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Open delete modal
|
|
await page.getByRole('button', { name: /supprimer/i }).first().click();
|
|
await expect(page.getByRole('alertdialog')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Confirm deletion
|
|
await page.getByRole('alertdialog').getByRole('button', { name: /supprimer/i }).click();
|
|
|
|
// Verify evaluation is removed
|
|
await expect(page.getByText(/aucune évaluation/i)).toBeVisible({ timeout: 10000 });
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// T1: Search evaluations by title (P2)
|
|
// ============================================================================
|
|
test.describe('Search evaluations', () => {
|
|
test('filters evaluations when searching by title', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToEvaluations(page);
|
|
|
|
// Create first evaluation: "Contrôle géométrie"
|
|
await page.getByRole('button', { name: /nouvelle évaluation/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
await selectClassAndSubject(page);
|
|
await page.locator('#ev-title').fill('Contrôle géométrie');
|
|
await page.locator('#ev-date').fill('2026-06-15');
|
|
await page.getByRole('button', { name: 'Créer', exact: true }).click();
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 15000 });
|
|
await expect(page.getByText('Contrôle géométrie')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Create second evaluation: "QCM algèbre"
|
|
await page.getByRole('button', { name: /nouvelle évaluation/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
await selectClassAndSubject(page);
|
|
await page.locator('#ev-title').fill('QCM algèbre');
|
|
await page.locator('#ev-date').fill('2026-06-20');
|
|
await page.getByRole('button', { name: 'Créer', exact: true }).click();
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 15000 });
|
|
await expect(page.getByText('QCM algèbre')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Both evaluations should be visible
|
|
await expect(page.getByText('Contrôle géométrie')).toBeVisible();
|
|
await expect(page.getByText('QCM algèbre')).toBeVisible();
|
|
|
|
// Search for "géométrie"
|
|
const searchInput = page.getByRole('searchbox', { name: /rechercher par titre/i });
|
|
await searchInput.fill('géométrie');
|
|
|
|
// Wait for debounced search to trigger and results to update
|
|
await expect(page.getByText('Contrôle géométrie')).toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByText('QCM algèbre')).not.toBeVisible({ timeout: 10000 });
|
|
|
|
// Clear search and verify both reappear
|
|
await page.getByRole('button', { name: /effacer la recherche/i }).click();
|
|
await expect(page.getByText('Contrôle géométrie')).toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByText('QCM algèbre')).toBeVisible({ timeout: 10000 });
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// T2: Filter evaluations by class (P2)
|
|
// ============================================================================
|
|
test.describe('Filter by class', () => {
|
|
test('class filter dropdown filters the evaluation list', async ({ page }) => {
|
|
// Seed a second class and assignment for this test
|
|
const { schoolId, academicYearId } = resolveDeterministicIds();
|
|
try {
|
|
runSql(
|
|
`INSERT INTO school_classes (id, tenant_id, school_id, academic_year_id, name, level, status, created_at, updated_at) ` +
|
|
`VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', '${academicYearId}', 'E2E-EVAL-5B', '5ème', 'active', NOW(), NOW()) 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, c.id, s.id, '${academicYearId}', 'active', NOW(), NOW(), NOW() ` +
|
|
`FROM users u CROSS JOIN school_classes c CROSS JOIN subjects s ` +
|
|
`WHERE u.email = '${TEACHER_EMAIL}' AND u.tenant_id = '${TENANT_ID}' ` +
|
|
`AND c.name = 'E2E-EVAL-5B' AND c.tenant_id = '${TENANT_ID}' ` +
|
|
`AND s.tenant_id = '${TENANT_ID}' ` +
|
|
`ON CONFLICT DO NOTHING`
|
|
);
|
|
} catch {
|
|
// May already exist
|
|
}
|
|
clearCache();
|
|
|
|
await loginAsTeacher(page);
|
|
await navigateToEvaluations(page);
|
|
|
|
// Create evaluation in first class (E2E-EVAL-6A)
|
|
await page.getByRole('button', { name: /nouvelle évaluation/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
const classSelect = page.locator('#ev-class');
|
|
await expect(classSelect).toBeVisible();
|
|
await classSelect.selectOption({ label: 'E2E-EVAL-6A' });
|
|
const subjectSelect = page.locator('#ev-subject');
|
|
await expect(subjectSelect).toBeEnabled({ timeout: 5000 });
|
|
await expect(subjectSelect.locator('option')).not.toHaveCount(1, { timeout: 10000 });
|
|
await subjectSelect.selectOption({ index: 1 });
|
|
await page.locator('#ev-title').fill('Eval classe 6A');
|
|
await page.locator('#ev-date').fill('2026-06-15');
|
|
await page.getByRole('button', { name: 'Créer', exact: true }).click();
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 15000 });
|
|
await expect(page.getByText('Eval classe 6A')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Create evaluation in second class (E2E-EVAL-5B)
|
|
await page.getByRole('button', { name: /nouvelle évaluation/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
const classSelect2 = page.locator('#ev-class');
|
|
await classSelect2.selectOption({ label: 'E2E-EVAL-5B' });
|
|
const subjectSelect2 = page.locator('#ev-subject');
|
|
await expect(subjectSelect2).toBeEnabled({ timeout: 5000 });
|
|
await expect(subjectSelect2.locator('option')).not.toHaveCount(1, { timeout: 10000 });
|
|
await subjectSelect2.selectOption({ index: 1 });
|
|
await page.locator('#ev-title').fill('Eval classe 5B');
|
|
await page.locator('#ev-date').fill('2026-06-20');
|
|
await page.getByRole('button', { name: 'Créer', exact: true }).click();
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 15000 });
|
|
await expect(page.getByText('Eval classe 5B')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Both evaluations visible initially
|
|
await expect(page.getByText('Eval classe 6A')).toBeVisible();
|
|
await expect(page.getByText('Eval classe 5B')).toBeVisible();
|
|
|
|
// Filter by E2E-EVAL-6A
|
|
const filterSelect = page.getByRole('combobox', { name: /filtrer par classe/i });
|
|
await expect(filterSelect).toBeVisible();
|
|
await filterSelect.selectOption({ label: 'E2E-EVAL-6A' });
|
|
|
|
// Wait for filtered results
|
|
await expect(page.getByText('Eval classe 6A')).toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByText('Eval classe 5B')).not.toBeVisible({ timeout: 10000 });
|
|
|
|
// Reset filter to "Toutes les classes"
|
|
await filterSelect.selectOption({ label: 'Toutes les classes' });
|
|
|
|
// Both should reappear
|
|
await expect(page.getByText('Eval classe 6A')).toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByText('Eval classe 5B')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Cleanup: remove the second class data
|
|
try {
|
|
runSql(`DELETE FROM grade_events WHERE grade_id IN (SELECT g.id FROM grades g JOIN evaluations e ON g.evaluation_id = e.id WHERE e.teacher_id IN (SELECT id FROM users WHERE email = '${TEACHER_EMAIL}' AND tenant_id = '${TENANT_ID}') AND e.class_id IN (SELECT id FROM school_classes WHERE name = 'E2E-EVAL-5B' AND tenant_id = '${TENANT_ID}'))`);
|
|
runSql(`DELETE FROM grades WHERE evaluation_id IN (SELECT id FROM evaluations WHERE teacher_id IN (SELECT id FROM users WHERE email = '${TEACHER_EMAIL}' AND tenant_id = '${TENANT_ID}') AND class_id IN (SELECT id FROM school_classes WHERE name = 'E2E-EVAL-5B' AND tenant_id = '${TENANT_ID}'))`);
|
|
runSql(`DELETE FROM evaluations WHERE teacher_id IN (SELECT id FROM users WHERE email = '${TEACHER_EMAIL}' AND tenant_id = '${TENANT_ID}') AND class_id IN (SELECT id FROM school_classes WHERE name = 'E2E-EVAL-5B' AND tenant_id = '${TENANT_ID}')`);
|
|
runSql(`DELETE FROM teacher_assignments WHERE school_class_id IN (SELECT id FROM school_classes WHERE name = 'E2E-EVAL-5B' AND tenant_id = '${TENANT_ID}')`);
|
|
runSql(`DELETE FROM school_classes WHERE name = 'E2E-EVAL-5B' AND tenant_id = '${TENANT_ID}'`);
|
|
} catch {
|
|
// Cleanup is best-effort
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// T3: Grade scale equivalence preview (P2)
|
|
// ============================================================================
|
|
test.describe('Grade scale preview', () => {
|
|
test('shows equivalence preview when barème is not 20', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToEvaluations(page);
|
|
|
|
// Open create modal
|
|
await page.getByRole('button', { name: /nouvelle évaluation/i }).click();
|
|
const dialog = page.getByRole('dialog');
|
|
await expect(dialog).toBeVisible({ timeout: 10000 });
|
|
|
|
// Default barème is 20 - no preview should appear
|
|
const scaleInput = dialog.locator('#ev-scale');
|
|
await expect(scaleInput).toHaveValue('20');
|
|
const previewHint = dialog.locator('#ev-scale ~ .form-hint');
|
|
await expect(previewHint).not.toBeVisible();
|
|
|
|
// Change barème to 10
|
|
await scaleInput.fill('10');
|
|
|
|
// Verify equivalence preview appears: (10/10 = 20.0/20)
|
|
await expect(previewHint).toBeVisible({ timeout: 5000 });
|
|
await expect(previewHint).toHaveText('(10/10 = 20.0/20)');
|
|
|
|
// Change barème to 5 -> (10/5 = 40.0/20)
|
|
await scaleInput.fill('5');
|
|
await expect(previewHint).toHaveText('(10/5 = 40.0/20)');
|
|
|
|
// Change back to 20 -> preview should disappear
|
|
await scaleInput.fill('20');
|
|
await expect(previewHint).not.toBeVisible({ timeout: 5000 });
|
|
});
|
|
});
|
|
});
|