Lorsqu'un super-admin crée un établissement via l'interface, le système doit automatiquement créer la base tenant, exécuter les migrations, créer le premier utilisateur admin et envoyer l'invitation — le tout de manière asynchrone pour ne pas bloquer la réponse HTTP. Ce mécanisme rend chaque établissement opérationnel dès sa création sans intervention manuelle sur l'infrastructure.
315 lines
13 KiB
TypeScript
315 lines
13 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-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 page.getByRole('button', { name: /se connecter/i }).click();
|
|
await page.waitForURL(/\/dashboard/, { timeout: 60000 });
|
|
}
|
|
|
|
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 });
|
|
}
|
|
|
|
test.describe('WYSIWYG Editor & Backward Compatibility (Story 5.9/5.11)', () => {
|
|
test.beforeAll(async () => {
|
|
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.beforeEach(async () => {
|
|
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 */ }
|
|
try {
|
|
runSql(`UPDATE homework_rules SET enabled = false, updated_at = NOW() WHERE tenant_id = '${TENANT_ID}'`);
|
|
} catch { /* Table may not exist */ }
|
|
try {
|
|
runSql(`DELETE FROM school_calendar_entries WHERE tenant_id = '${TENANT_ID}'`);
|
|
} catch { /* Table may not exist */ }
|
|
clearCache();
|
|
});
|
|
|
|
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 });
|
|
|
|
const editor = page.locator('.rich-text-editor');
|
|
await expect(editor).toBeVisible({ timeout: 5000 });
|
|
await expect(page.locator('.toolbar')).toBeVisible();
|
|
|
|
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');
|
|
|
|
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 ');
|
|
|
|
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 });
|
|
|
|
const description = page.locator('.homework-description');
|
|
await expect(description.locator('strong')).toContainText('en gras');
|
|
});
|
|
|
|
test('italic 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 italique test');
|
|
|
|
const editorContent = page.locator('.modal .rich-text-content');
|
|
await expect(editorContent).toBeVisible({ timeout: 10000 });
|
|
await editorContent.click();
|
|
await page.keyboard.type('Normal ');
|
|
|
|
await page.keyboard.press('Control+i');
|
|
await page.keyboard.type('en italique');
|
|
|
|
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 italique test')).toBeVisible({ timeout: 10000 });
|
|
|
|
const description = page.locator('.homework-description');
|
|
await expect(description.locator('em')).toContainText('en italique');
|
|
});
|
|
|
|
test('bullet list 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 liste test');
|
|
|
|
const editorContent = page.locator('.modal .rich-text-content');
|
|
await expect(editorContent).toBeVisible({ timeout: 10000 });
|
|
await editorContent.click();
|
|
|
|
await editorContent.press('Control+Shift+8');
|
|
await page.keyboard.type('Premier élément');
|
|
|
|
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 liste test')).toBeVisible({ timeout: 10000 });
|
|
|
|
const description = page.locator('.homework-description');
|
|
await expect(description.locator('ul')).toBeVisible();
|
|
await expect(description.locator('li')).toContainText('Premier élément');
|
|
});
|
|
});
|
|
|
|
test.describe('Backward Compatibility', () => {
|
|
test('existing plain text homework displays correctly', 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 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);
|
|
|
|
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);
|
|
|
|
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 });
|
|
|
|
const editorContent = page.locator('.modal .rich-text-content');
|
|
await expect(editorContent).toBeVisible({ timeout: 10000 });
|
|
await expect(editorContent).toContainText('Ancienne description', { timeout: 5000 });
|
|
});
|
|
});
|
|
});
|