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.
492 lines
20 KiB
TypeScript
492 lines
20 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
import { execSync } from 'child_process';
|
|
import { join, dirname } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import { writeFileSync, mkdirSync, unlinkSync } from 'fs';
|
|
|
|
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}`;
|
|
|
|
// Réutilise le même enseignant que homework.spec.ts pour partager le setup
|
|
const TEACHER_EMAIL = 'e2e-homework-teacher@example.com';
|
|
const TEACHER_PASSWORD = 'HomeworkTest123';
|
|
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! };
|
|
}
|
|
|
|
function getNextWeekday(daysFromNow: number): string {
|
|
const date = new Date();
|
|
date.setDate(date.getDate() + daysFromNow);
|
|
const day = date.getDay();
|
|
if (day === 0) date.setDate(date.getDate() + 1);
|
|
if (day === 6) date.setDate(date.getDate() + 2);
|
|
const y = date.getFullYear();
|
|
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
const d = String(date.getDate()).padStart(2, '0');
|
|
return `${y}-${m}-${d}`;
|
|
}
|
|
|
|
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 {
|
|
// May already exist
|
|
}
|
|
}
|
|
|
|
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 navigateToHomework(page: import('@playwright/test').Page) {
|
|
await page.goto(`${ALPHA_URL}/dashboard/teacher/homework`);
|
|
await expect(page.getByRole('heading', { name: /mes devoirs/i })).toBeVisible({ timeout: 15000 });
|
|
}
|
|
|
|
async function createHomework(page: import('@playwright/test').Page, title: string) {
|
|
await page.getByRole('button', { name: /nouveau devoir/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
|
|
await page.locator('#hw-class').selectOption({ index: 1 });
|
|
await page.locator('#hw-subject').selectOption({ index: 1 });
|
|
await page.locator('#hw-title').fill(title);
|
|
|
|
// Type in WYSIWYG editor (TipTap initializes asynchronously)
|
|
const editorContent = page.locator('.modal .rich-text-content');
|
|
await expect(editorContent).toBeVisible({ timeout: 10000 });
|
|
await editorContent.click();
|
|
await page.keyboard.type('Consignes du devoir');
|
|
|
|
await page.locator('#hw-due-date').fill(getNextWeekday(5));
|
|
await page.getByRole('button', { name: /créer le devoir/i }).click();
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByText(title)).toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
function createTempPdf(): string {
|
|
const tmpDir = join(__dirname, '..', 'tmp-test-files');
|
|
mkdirSync(tmpDir, { recursive: true });
|
|
const filePath = join(tmpDir, 'test-attachment.pdf');
|
|
const pdfContent = `%PDF-1.4
|
|
1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
|
|
2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj
|
|
3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj
|
|
xref
|
|
0 4
|
|
0000000000 65535 f
|
|
0000000009 00000 n
|
|
0000000058 00000 n
|
|
0000000115 00000 n
|
|
trailer<</Size 4/Root 1 0 R>>
|
|
startxref
|
|
190
|
|
%%EOF`;
|
|
writeFileSync(filePath, pdfContent);
|
|
return filePath;
|
|
}
|
|
|
|
function createTempTxt(): string {
|
|
const tmpDir = join(__dirname, '..', 'tmp-test-files');
|
|
mkdirSync(tmpDir, { recursive: true });
|
|
const filePath = join(tmpDir, 'test-invalid.txt');
|
|
writeFileSync(filePath, 'This is a plain text file that should be rejected.');
|
|
return filePath;
|
|
}
|
|
|
|
function cleanupTempFiles() {
|
|
const tmpDir = join(__dirname, '..', 'tmp-test-files');
|
|
for (const name of ['test-attachment.pdf', 'test-invalid.txt']) {
|
|
try {
|
|
unlinkSync(join(tmpDir, name));
|
|
} catch {
|
|
// May not exist
|
|
}
|
|
}
|
|
}
|
|
|
|
test.describe('Rich Text & Attachments (Story 5.9)', () => {
|
|
test.beforeAll(async () => {
|
|
// Ensure teacher user exists (same as homework.spec.ts)
|
|
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' }
|
|
);
|
|
|
|
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-HW-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-HW-Maths', 'E2EMAT', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
|
|
);
|
|
} catch {
|
|
// May already exist
|
|
}
|
|
|
|
seedTeacherAssignments();
|
|
clearCache();
|
|
});
|
|
|
|
test.afterAll(() => {
|
|
cleanupTempFiles();
|
|
});
|
|
|
|
test.beforeEach(async () => {
|
|
// homework_submissions has NO CASCADE on homework_id — delete submissions first
|
|
try {
|
|
runSql(`DELETE FROM submission_attachments WHERE submission_id IN (SELECT hs.id FROM homework_submissions hs JOIN homework h ON hs.homework_id = h.id WHERE h.tenant_id = '${TENANT_ID}' AND h.teacher_id IN (SELECT id FROM users WHERE email = '${TEACHER_EMAIL}' AND tenant_id = '${TENANT_ID}'))`);
|
|
} catch { /* Table may not exist */ }
|
|
try {
|
|
runSql(`DELETE FROM homework_submissions WHERE homework_id IN (SELECT id FROM homework 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 */ }
|
|
try {
|
|
runSql(`DELETE FROM homework 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
|
|
}
|
|
|
|
// Disable any homework rules left by other test files (homework-rules-warning,
|
|
// homework-rules-hard) to prevent rule warnings blocking homework creation.
|
|
try {
|
|
runSql(`UPDATE homework_rules SET enabled = false, updated_at = NOW() WHERE tenant_id = '${TENANT_ID}'`);
|
|
} catch { /* Table may not exist */ }
|
|
|
|
// Clear school calendar entries that may block dates (Vacances de Printemps, etc.)
|
|
try {
|
|
runSql(`DELETE FROM school_calendar_entries WHERE tenant_id = '${TENANT_ID}'`);
|
|
} catch { /* Table may not exist */ }
|
|
|
|
clearCache();
|
|
});
|
|
|
|
// ============================================================================
|
|
// T4.1 : WYSIWYG Editor
|
|
// ============================================================================
|
|
test.describe('WYSIWYG Editor', () => {
|
|
test('create form shows rich text editor with toolbar', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
|
|
await page.getByRole('button', { name: /nouveau devoir/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Rich text editor with toolbar should be visible
|
|
const editor = page.locator('.rich-text-editor');
|
|
await expect(editor).toBeVisible({ timeout: 5000 });
|
|
await expect(page.locator('.toolbar')).toBeVisible();
|
|
|
|
// Toolbar buttons
|
|
await expect(page.getByRole('button', { name: 'Gras' })).toBeVisible();
|
|
await expect(page.getByRole('button', { name: 'Italique' })).toBeVisible();
|
|
await expect(page.getByRole('button', { name: 'Liste à puces' })).toBeVisible();
|
|
await expect(page.getByRole('button', { name: 'Liste numérotée' })).toBeVisible();
|
|
await expect(page.getByRole('button', { name: 'Lien' })).toBeVisible();
|
|
});
|
|
|
|
test('can create homework with rich text description', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
|
|
await page.getByRole('button', { name: /nouveau devoir/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
|
|
await page.locator('#hw-class').selectOption({ index: 1 });
|
|
await page.locator('#hw-subject').selectOption({ index: 1 });
|
|
await page.locator('#hw-title').fill('Devoir texte riche');
|
|
|
|
// Type in rich text editor (TipTap initializes asynchronously)
|
|
const editorContent = page.locator('.modal .rich-text-content');
|
|
await expect(editorContent).toBeVisible({ timeout: 10000 });
|
|
await editorContent.click();
|
|
await page.keyboard.type('Consignes importantes');
|
|
|
|
await page.locator('#hw-due-date').fill(getNextWeekday(5));
|
|
await page.getByRole('button', { name: /créer le devoir/i }).click();
|
|
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByText('Devoir texte riche')).toBeVisible({ timeout: 10000 });
|
|
});
|
|
|
|
test('bold formatting works in editor', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
|
|
await page.getByRole('button', { name: /nouveau devoir/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
|
|
await page.locator('#hw-class').selectOption({ index: 1 });
|
|
await page.locator('#hw-subject').selectOption({ index: 1 });
|
|
await page.locator('#hw-title').fill('Devoir gras test');
|
|
|
|
const editorContent = page.locator('.modal .rich-text-content');
|
|
await expect(editorContent).toBeVisible({ timeout: 10000 });
|
|
await editorContent.click();
|
|
await page.keyboard.type('Normal ');
|
|
|
|
// Apply bold via keyboard shortcut (more reliable than toolbar click)
|
|
await page.keyboard.press('Control+b');
|
|
await page.keyboard.type('en gras');
|
|
|
|
await page.locator('#hw-due-date').fill(getNextWeekday(5));
|
|
await page.getByRole('button', { name: /créer le devoir/i }).click();
|
|
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByText('Devoir gras test')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Verify bold is rendered in the description
|
|
const description = page.locator('.homework-description');
|
|
await expect(description.locator('strong')).toContainText('en gras');
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// T4.2 : Upload attachment
|
|
// ============================================================================
|
|
test.describe('Attachments', () => {
|
|
test('can upload a PDF attachment to homework via edit modal', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
|
|
// Create homework
|
|
await createHomework(page, 'Devoir avec PJ');
|
|
|
|
// Open edit modal
|
|
const hwCard = page.locator('.homework-card', { hasText: 'Devoir avec PJ' });
|
|
await hwCard.getByRole('button', { name: /modifier/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Upload file
|
|
const pdfPath = createTempPdf();
|
|
const fileInput = page.locator('.file-input-hidden');
|
|
await fileInput.setInputFiles(pdfPath);
|
|
|
|
// File appears in list
|
|
await expect(page.getByText('test-attachment.pdf')).toBeVisible({ timeout: 15000 });
|
|
});
|
|
|
|
// T4.3 : Delete attachment
|
|
test('can delete an uploaded attachment', async ({ page }) => {
|
|
test.slow(); // upload + delete needs more than 30s
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
|
|
await createHomework(page, 'Devoir suppr PJ');
|
|
|
|
// Open edit modal and upload
|
|
const hwCard = page.locator('.homework-card', { hasText: 'Devoir suppr PJ' });
|
|
await hwCard.getByRole('button', { name: /modifier/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
|
|
const pdfPath = createTempPdf();
|
|
await page.locator('.file-input-hidden').setInputFiles(pdfPath);
|
|
await expect(page.getByText('test-attachment.pdf')).toBeVisible({ timeout: 15000 });
|
|
|
|
// Delete the attachment
|
|
await page.getByRole('button', { name: /supprimer test-attachment.pdf/i }).click();
|
|
await expect(page.getByText('test-attachment.pdf')).not.toBeVisible({ timeout: 5000 });
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// T5.9.1 : Invalid file type rejection (P1)
|
|
// ============================================================================
|
|
test.describe('Invalid File Type Rejection', () => {
|
|
test('rejects a .txt file with an error message', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
|
|
await createHomework(page, 'Devoir rejet fichier');
|
|
|
|
// Open edit modal
|
|
const hwCard = page.locator('.homework-card', { hasText: 'Devoir rejet fichier' });
|
|
await hwCard.getByRole('button', { name: /modifier/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Try to upload a .txt file
|
|
const txtPath = createTempTxt();
|
|
const fileInput = page.locator('.file-input-hidden');
|
|
await fileInput.setInputFiles(txtPath);
|
|
|
|
// Error message should appear
|
|
const errorAlert = page.locator('[role="alert"]');
|
|
await expect(errorAlert).toBeVisible({ timeout: 5000 });
|
|
await expect(errorAlert).toContainText('Type de fichier non accepté');
|
|
|
|
// The .txt file should NOT appear in the file list
|
|
await expect(page.getByText('test-invalid.txt')).not.toBeVisible();
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// T5.9.2 : Attachment persistence after save (P1)
|
|
// ============================================================================
|
|
test.describe('Attachment Persistence', () => {
|
|
test('uploaded attachment persists after saving and reopening edit modal', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
|
|
await createHomework(page, 'Devoir persistance PJ');
|
|
|
|
// Open edit modal
|
|
const hwCard = page.locator('.homework-card', { hasText: 'Devoir persistance PJ' });
|
|
await hwCard.getByRole('button', { name: /modifier/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Upload a PDF
|
|
const pdfPath = createTempPdf();
|
|
const fileInput = page.locator('.file-input-hidden');
|
|
await fileInput.setInputFiles(pdfPath);
|
|
await expect(page.getByText('test-attachment.pdf')).toBeVisible({ timeout: 15000 });
|
|
|
|
// Save the changes
|
|
await page.getByRole('button', { name: /enregistrer/i }).click();
|
|
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10000 });
|
|
|
|
// Reopen the edit modal
|
|
const hwCardAfterSave = page.locator('.homework-card', { hasText: 'Devoir persistance PJ' });
|
|
await hwCardAfterSave.getByRole('button', { name: /modifier/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
|
|
// The attachment should still be there
|
|
await expect(page.getByText('test-attachment.pdf')).toBeVisible({ timeout: 15000 });
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// T5.9.3 : File size display after upload (P2)
|
|
// ============================================================================
|
|
test.describe('File Size Display', () => {
|
|
test('shows formatted file size after uploading a PDF', async ({ page }) => {
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
|
|
await createHomework(page, 'Devoir taille fichier');
|
|
|
|
// Open edit modal
|
|
const hwCard = page.locator('.homework-card', { hasText: 'Devoir taille fichier' });
|
|
await hwCard.getByRole('button', { name: /modifier/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Upload a PDF
|
|
const pdfPath = createTempPdf();
|
|
const fileInput = page.locator('.file-input-hidden');
|
|
await fileInput.setInputFiles(pdfPath);
|
|
await expect(page.getByText('test-attachment.pdf')).toBeVisible({ timeout: 15000 });
|
|
|
|
// The file size element should be visible and show a formatted size (e.g., "xxx o" or "xxx Ko")
|
|
const fileSize = page.locator('.file-size');
|
|
await expect(fileSize).toBeVisible({ timeout: 5000 });
|
|
await expect(fileSize).toHaveText(/\d+(\.\d+)?\s*(o|Ko|Mo)/);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// T4.4 : Backward compatibility
|
|
// ============================================================================
|
|
test.describe('Backward Compatibility', () => {
|
|
test('existing plain text homework displays correctly', async ({ page }) => {
|
|
// Create homework with plain text description via SQL
|
|
runSql(
|
|
`INSERT INTO homework (id, tenant_id, class_id, subject_id, teacher_id, title, description, due_date, status, created_at, updated_at) ` +
|
|
`SELECT gen_random_uuid(), '${TENANT_ID}', c.id, s.id, u.id, 'Devoir texte brut E2E', 'Description simple sans balise HTML', '${getNextWeekday(10)}', 'published', NOW(), NOW() ` +
|
|
`FROM users u, school_classes c, subjects s ` +
|
|
`WHERE u.email = '${TEACHER_EMAIL}' AND u.tenant_id = '${TENANT_ID}' ` +
|
|
`AND c.tenant_id = '${TENANT_ID}' AND c.name = 'E2E-HW-6A' ` +
|
|
`AND s.tenant_id = '${TENANT_ID}' AND s.name = 'E2E-HW-Maths' ` +
|
|
`LIMIT 1`
|
|
);
|
|
clearCache();
|
|
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
|
|
// Plain text description displays correctly
|
|
await expect(page.getByText('Devoir texte brut E2E')).toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByText('Description simple sans balise HTML')).toBeVisible();
|
|
});
|
|
|
|
test('edit modal loads plain text in WYSIWYG editor', async ({ page }) => {
|
|
runSql(
|
|
`INSERT INTO homework (id, tenant_id, class_id, subject_id, teacher_id, title, description, due_date, status, created_at, updated_at) ` +
|
|
`SELECT gen_random_uuid(), '${TENANT_ID}', c.id, s.id, u.id, 'Devoir edit brut E2E', 'Ancienne description', '${getNextWeekday(10)}', 'published', NOW(), NOW() ` +
|
|
`FROM users u, school_classes c, subjects s ` +
|
|
`WHERE u.email = '${TEACHER_EMAIL}' AND u.tenant_id = '${TENANT_ID}' ` +
|
|
`AND c.tenant_id = '${TENANT_ID}' AND c.name = 'E2E-HW-6A' ` +
|
|
`AND s.tenant_id = '${TENANT_ID}' AND s.name = 'E2E-HW-Maths' ` +
|
|
`LIMIT 1`
|
|
);
|
|
clearCache();
|
|
|
|
await loginAsTeacher(page);
|
|
await navigateToHomework(page);
|
|
|
|
// Open edit modal
|
|
const hwCard = page.locator('.homework-card', { hasText: 'Devoir edit brut E2E' });
|
|
await hwCard.getByRole('button', { name: /modifier/i }).click();
|
|
await expect(page.getByRole('dialog')).toBeVisible({ timeout: 10000 });
|
|
|
|
// WYSIWYG editor contains the old text (TipTap initializes asynchronously)
|
|
const editorContent = page.locator('.modal .rich-text-content');
|
|
await expect(editorContent).toBeVisible({ timeout: 10000 });
|
|
await expect(editorContent).toContainText('Ancienne description', { timeout: 5000 });
|
|
});
|
|
});
|
|
});
|