L'enseignant avait besoin d'un moyen rapide de saisir les notes après une évaluation. La grille inline permet de compléter 30 élèves en moins de 3 minutes grâce à la navigation clavier (Tab/Enter/Shift+Tab), la validation temps réel, l'auto-save debounced (500ms) et les raccourcis /abs et /disp pour marquer absents/dispensés. Les notes restent en brouillon jusqu'à publication explicite (avec confirmation modale). Une fois publiées, les élèves les voient immédiatement ; les parents après un délai de 24h (VisibiliteNotesPolicy). Le mode offline stocke les notes en IndexedDB et synchronise automatiquement au retour de la connexion. Chaque modification est auditée dans grade_events via un event subscriber qui écoute NoteSaisie/NoteModifiee sur le bus d'événements.
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: 30000 }),
|
|
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 });
|
|
});
|
|
});
|
|
});
|