feat: Paralléliser les appels API de la page devoirs
La page devoirs enchaînait séquentiellement les appels classes, subjects, assignments et homework, produisant un waterfall de ~2.5s. En les lançant dans un seul Promise.all, le temps de chargement correspond désormais au plus lent des appels (~600ms) au lieu de leur somme. Pour résoudre la dépendance de assignments sur le userId (nécessaire dans l'URL), un nouveau helper getAuthenticatedUserId() encapsule le mécanisme de token refresh côté module auth, évitant aux pages d'importer refreshToken directement. Chaque branche side-effect (loadAssignments, loadHomeworks) gère ses erreurs via .catch() local pour éviter l'état partiel si l'une échoue pendant que les autres réussissent.
This commit is contained in:
@@ -109,13 +109,17 @@ test.describe('Homework Management (Story 5.1)', () => {
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
|
||||
// Ensure class and subject exist
|
||||
// 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-HW-6A', '6ème', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
|
||||
);
|
||||
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-6B', '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`
|
||||
@@ -143,6 +147,10 @@ test.describe('Homework Management (Story 5.1)', () => {
|
||||
`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 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-6B', '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`
|
||||
@@ -422,9 +430,17 @@ test.describe('Homework Management (Story 5.1)', () => {
|
||||
});
|
||||
|
||||
async function createHomework(page: import('@playwright/test').Page, title: string) {
|
||||
await createHomeworkInClass(page, title, { index: 1 });
|
||||
}
|
||||
|
||||
async function createHomeworkInClass(
|
||||
page: import('@playwright/test').Page,
|
||||
title: string,
|
||||
classOption: { index: number } | { label: 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-class').selectOption(classOption);
|
||||
await page.locator('#hw-subject').selectOption({ index: 1 });
|
||||
await page.locator('#hw-title').fill(title);
|
||||
await page.locator('#hw-due-date').fill(getNextWeekday(5));
|
||||
@@ -576,24 +592,81 @@ test.describe('Homework Management (Story 5.1)', () => {
|
||||
await expect(page.getByText('Devoir date custom')).toHaveCount(2, { timeout: 10000 });
|
||||
});
|
||||
|
||||
test('shows validation errors per class when duplication rules fail', async ({ page }) => {
|
||||
await loginAsTeacher(page);
|
||||
await navigateToHomework(page);
|
||||
await createHomework(page, 'Devoir validation règles');
|
||||
|
||||
await page.getByRole('button', { name: /dupliquer/i }).first().click();
|
||||
const dialog = page.getByRole('dialog');
|
||||
await expect(dialog).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Select first target class
|
||||
const firstCheckbox = dialog.locator('input[type="checkbox"]').first();
|
||||
await firstCheckbox.check();
|
||||
|
||||
// Intercept the POST duplicate API call to simulate a validation error
|
||||
await page.route(
|
||||
'**/api/homework/*/duplicate',
|
||||
async (route, request) => {
|
||||
// Extract targetClassIds from the request body
|
||||
const body = request.postDataJSON();
|
||||
const targetClassIds: string[] = body.targetClassIds ?? [];
|
||||
|
||||
await route.fulfill({
|
||||
status: 400,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
error: 'Validation échouée pour certaines classes.',
|
||||
results: targetClassIds.map((classId: string) => ({
|
||||
classId,
|
||||
valid: false,
|
||||
error: "L'enseignant n'est pas affecté à cette classe pour cette matière.",
|
||||
})),
|
||||
}),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Click duplicate
|
||||
await page.getByRole('button', { name: /dupliquer \(1 classe\)/i }).click();
|
||||
|
||||
// Verify: error alert appears in the modal
|
||||
await expect(dialog.locator('.alert-error')).toBeVisible({ timeout: 10000 });
|
||||
await expect(dialog.getByText(/certaines classes ne passent pas la validation/i)).toBeVisible();
|
||||
|
||||
// Verify: validation error message appears next to the class
|
||||
await expect(dialog.locator('.validation-error')).toBeVisible();
|
||||
await expect(
|
||||
dialog.getByText(/l'enseignant n'est pas affecté à cette classe/i),
|
||||
).toBeVisible();
|
||||
|
||||
// Verify: modal stays open (not closed on validation error)
|
||||
await expect(dialog).toBeVisible();
|
||||
});
|
||||
|
||||
test('can filter homework list by class', async ({ page }) => {
|
||||
await loginAsTeacher(page);
|
||||
await navigateToHomework(page);
|
||||
await createHomework(page, 'Devoir filtre classe');
|
||||
|
||||
// Class filter should be visible
|
||||
// Create homework in two different classes
|
||||
await createHomeworkInClass(page, 'Devoir classe A', { label: 'E2E-HW-6A' });
|
||||
await createHomeworkInClass(page, 'Devoir classe B', { label: 'E2E-HW-6B' });
|
||||
|
||||
// Both visible initially
|
||||
await expect(page.getByText('Devoir classe A')).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText('Devoir classe B')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Filter by class A — class B homework must disappear
|
||||
const classFilter = page.locator('select[aria-label="Filtrer par classe"]');
|
||||
await expect(classFilter).toBeVisible();
|
||||
await classFilter.selectOption({ label: 'E2E-HW-6A' });
|
||||
await expect(page.getByText('Devoir classe A')).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText('Devoir classe B')).not.toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Select a class to filter
|
||||
await classFilter.selectOption({ index: 1 });
|
||||
|
||||
// Should still show homework (it matches the filter)
|
||||
await expect(page.getByText('Devoir filtre classe')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Select "Toutes les classes" to reset
|
||||
// Reset — both visible again
|
||||
await classFilter.selectOption({ index: 0 });
|
||||
await expect(page.getByText('Devoir filtre classe')).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText('Devoir classe A')).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText('Devoir classe B')).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user