feat: Permettre au parent de consulter les devoirs de ses enfants
Les parents avaient accès à l'emploi du temps de leurs enfants mais pas à leurs devoirs. Sans cette visibilité, ils ne pouvaient pas accompagner efficacement le travail scolaire à la maison, notamment identifier les devoirs urgents ou contacter l'enseignant en cas de besoin. Le parent dispose désormais d'une vue consolidée multi-enfants avec filtrage par enfant et par matière, badges d'urgence différenciés (en retard / aujourd'hui / pour demain), lien de contact enseignant pré-rempli, et cache offline scopé par utilisateur.
This commit is contained in:
750
frontend/e2e/parent-homework.spec.ts
Normal file
750
frontend/e2e/parent-homework.spec.ts
Normal file
@@ -0,0 +1,750 @@
|
||||
import { test, expect, type Page } 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 ADMIN_EMAIL = 'e2e-parenthw-admin@example.com';
|
||||
const ADMIN_PASSWORD = 'AdminParentHW123';
|
||||
const PARENT_EMAIL = 'e2e-parenthw-parent@example.com';
|
||||
const PARENT_PASSWORD = 'ParentHomework123';
|
||||
const TEACHER_EMAIL = 'e2e-parenthw-teacher@example.com';
|
||||
const TEACHER_PASSWORD = 'TeacherParentHW123';
|
||||
const STUDENT1_EMAIL = 'e2e-parenthw-student1@example.com';
|
||||
const STUDENT1_PASSWORD = 'Student1ParentHW123';
|
||||
const STUDENT2_EMAIL = 'e2e-parenthw-student2@example.com';
|
||||
const STUDENT2_PASSWORD = 'Student2ParentHW123';
|
||||
const TENANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
|
||||
|
||||
const projectRoot = join(__dirname, '../..');
|
||||
const composeFile = join(projectRoot, 'compose.yaml');
|
||||
|
||||
let student1UserId: string;
|
||||
let student2UserId: string;
|
||||
|
||||
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 extractUserId(output: string): string {
|
||||
const match = output.match(/User ID\s+([a-f0-9-]{36})/i);
|
||||
if (!match) {
|
||||
throw new Error(`Could not extract User ID from command output:\n${output}`);
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
|
||||
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 getTomorrowWeekday(): string {
|
||||
return getNextWeekday(1);
|
||||
}
|
||||
|
||||
function getFormattedToday(): string {
|
||||
const date = new Date();
|
||||
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 getPastDate(daysAgo: number): string {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - daysAgo);
|
||||
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}`;
|
||||
}
|
||||
|
||||
async function loginAsAdmin(page: Page) {
|
||||
await page.goto(`${ALPHA_URL}/login`);
|
||||
await page.locator('#email').fill(ADMIN_EMAIL);
|
||||
await page.locator('#password').fill(ADMIN_PASSWORD);
|
||||
await Promise.all([
|
||||
page.waitForURL(/\/dashboard/, { timeout: 30000 }),
|
||||
page.getByRole('button', { name: /se connecter/i }).click()
|
||||
]);
|
||||
}
|
||||
|
||||
async function loginAsParent(page: Page) {
|
||||
await page.goto(`${ALPHA_URL}/login`);
|
||||
await page.locator('#email').fill(PARENT_EMAIL);
|
||||
await page.locator('#password').fill(PARENT_PASSWORD);
|
||||
await Promise.all([
|
||||
page.waitForURL(/\/dashboard/, { timeout: 30000 }),
|
||||
page.getByRole('button', { name: /se connecter/i }).click()
|
||||
]);
|
||||
}
|
||||
|
||||
async function addGuardianIfNotLinked(page: Page, studentId: string, parentSearchTerm: string, relationship: string) {
|
||||
await page.goto(`${ALPHA_URL}/admin/students/${studentId}`);
|
||||
await expect(page.locator('.guardian-section')).toBeVisible({ timeout: 10000 });
|
||||
await expect(
|
||||
page.getByText(/aucun parent\/tuteur/i).or(page.locator('.guardian-list'))
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
|
||||
const addButton = page.getByRole('button', { name: /ajouter un parent/i });
|
||||
if (!(await addButton.isVisible())) return;
|
||||
|
||||
const sectionText = await page.locator('.guardian-section').textContent();
|
||||
if (sectionText && sectionText.includes(parentSearchTerm)) return;
|
||||
|
||||
await addButton.click();
|
||||
const dialog = page.getByRole('dialog');
|
||||
await expect(dialog).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const searchInput = dialog.getByRole('combobox', { name: /rechercher/i });
|
||||
await searchInput.fill(parentSearchTerm);
|
||||
|
||||
const listbox = dialog.locator('#parent-search-listbox');
|
||||
await expect(listbox).toBeVisible({ timeout: 10000 });
|
||||
const option = listbox.locator('[role="option"]').first();
|
||||
await option.click();
|
||||
|
||||
await expect(dialog.getByText(/sélectionné/i)).toBeVisible();
|
||||
|
||||
await dialog.getByLabel(/type de relation/i).selectOption(relationship);
|
||||
await dialog.getByRole('button', { name: 'Ajouter' }).click();
|
||||
|
||||
await expect(
|
||||
page.locator('.alert-success').or(page.locator('.alert-error'))
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
}
|
||||
|
||||
test.describe('Parent Homework Consultation (Story 5.8)', () => {
|
||||
test.describe.configure({ mode: 'serial', timeout: 60000 });
|
||||
|
||||
const urgentDueDate = getTomorrowWeekday();
|
||||
const futureDueDate = getNextWeekday(10);
|
||||
const todayDueDate = getFormattedToday();
|
||||
const overdueDueDate = getPastDate(3);
|
||||
|
||||
test.beforeAll(async ({ browser }, testInfo) => {
|
||||
testInfo.setTimeout(120000);
|
||||
try {
|
||||
execSync(
|
||||
`docker compose -f "${composeFile}" exec -T php php bin/console cache:pool:clear cache.rate_limiter users.cache --env=dev 2>&1`,
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
} catch {
|
||||
// Cache pools may not exist
|
||||
}
|
||||
|
||||
// Create users
|
||||
execSync(
|
||||
`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${ADMIN_EMAIL} --password=${ADMIN_PASSWORD} --role=ROLE_ADMIN 2>&1`,
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
|
||||
execSync(
|
||||
`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${PARENT_EMAIL} --password=${PARENT_PASSWORD} --role=ROLE_PARENT --firstName=ParentHW --lastName=TestUser 2>&1`,
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
|
||||
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 student1Output = execSync(
|
||||
`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${STUDENT1_EMAIL} --password=${STUDENT1_PASSWORD} --role=ROLE_ELEVE --firstName=Emma --lastName=ParentHWTest 2>&1`,
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
student1UserId = extractUserId(student1Output);
|
||||
|
||||
const student2Output = execSync(
|
||||
`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${STUDENT2_EMAIL} --password=${STUDENT2_PASSWORD} --role=ROLE_ELEVE --firstName=Lucas --lastName=ParentHWTest 2>&1`,
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
student2UserId = extractUserId(student2Output);
|
||||
|
||||
const { schoolId, academicYearId } = resolveDeterministicIds();
|
||||
|
||||
// Ensure classes exist
|
||||
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-PHW-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-PHW-6B', '6ème', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
|
||||
);
|
||||
} catch {
|
||||
// May already exist
|
||||
}
|
||||
|
||||
// Ensure subjects exist
|
||||
try {
|
||||
runSql(
|
||||
`INSERT INTO subjects (id, tenant_id, school_id, name, code, color, status, created_at, updated_at) ` +
|
||||
`VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', 'E2E-PHW-Maths', 'E2EPHWMAT', '#3b82f6', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
|
||||
);
|
||||
runSql(
|
||||
`INSERT INTO subjects (id, tenant_id, school_id, name, code, color, status, created_at, updated_at) ` +
|
||||
`VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', 'E2E-PHW-Français', 'E2EPHWFRA', '#ef4444', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING`
|
||||
);
|
||||
} catch {
|
||||
// May already exist
|
||||
}
|
||||
|
||||
// Assign students to classes
|
||||
runSql(
|
||||
`INSERT INTO class_assignments (id, tenant_id, user_id, school_class_id, academic_year_id, assigned_at, created_at, updated_at) ` +
|
||||
`SELECT gen_random_uuid(), '${TENANT_ID}', u.id, c.id, '${academicYearId}', NOW(), NOW(), NOW() ` +
|
||||
`FROM users u, school_classes c ` +
|
||||
`WHERE u.email = '${STUDENT1_EMAIL}' AND u.tenant_id = '${TENANT_ID}' ` +
|
||||
`AND c.name = 'E2E-PHW-6A' AND c.tenant_id = '${TENANT_ID}' ` +
|
||||
`ON CONFLICT DO NOTHING`
|
||||
);
|
||||
|
||||
runSql(
|
||||
`INSERT INTO class_assignments (id, tenant_id, user_id, school_class_id, academic_year_id, assigned_at, created_at, updated_at) ` +
|
||||
`SELECT gen_random_uuid(), '${TENANT_ID}', u.id, c.id, '${academicYearId}', NOW(), NOW(), NOW() ` +
|
||||
`FROM users u, school_classes c ` +
|
||||
`WHERE u.email = '${STUDENT2_EMAIL}' AND u.tenant_id = '${TENANT_ID}' ` +
|
||||
`AND c.name = 'E2E-PHW-6B' AND c.tenant_id = '${TENANT_ID}' ` +
|
||||
`ON CONFLICT DO NOTHING`
|
||||
);
|
||||
|
||||
// Clean up stale homework from previous runs
|
||||
try {
|
||||
runSql(
|
||||
`DELETE FROM homework WHERE tenant_id = '${TENANT_ID}' AND class_id IN ` +
|
||||
`(SELECT id FROM school_classes WHERE name IN ('E2E-PHW-6A', 'E2E-PHW-6B') AND tenant_id = '${TENANT_ID}')`
|
||||
);
|
||||
} catch {
|
||||
// Table may not exist
|
||||
}
|
||||
|
||||
// Seed homework for both classes
|
||||
// Urgent homework (due tomorrow) for class 6A
|
||||
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, t.id, 'Devoir urgent maths', 'Exercices urgents', '${urgentDueDate}', 'published', NOW(), NOW() ` +
|
||||
`FROM school_classes c, ` +
|
||||
`(SELECT id FROM subjects WHERE code = 'E2EPHWMAT' AND tenant_id = '${TENANT_ID}' LIMIT 1) s, ` +
|
||||
`(SELECT id FROM users WHERE email = '${TEACHER_EMAIL}' AND tenant_id = '${TENANT_ID}') t ` +
|
||||
`WHERE c.name = 'E2E-PHW-6A' AND c.tenant_id = '${TENANT_ID}'`
|
||||
);
|
||||
|
||||
// Future homework for class 6A
|
||||
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, t.id, 'Rédaction français Emma', 'Écrire une rédaction', '${futureDueDate}', 'published', NOW(), NOW() ` +
|
||||
`FROM school_classes c, ` +
|
||||
`(SELECT id FROM subjects WHERE code = 'E2EPHWFRA' AND tenant_id = '${TENANT_ID}' LIMIT 1) s, ` +
|
||||
`(SELECT id FROM users WHERE email = '${TEACHER_EMAIL}' AND tenant_id = '${TENANT_ID}') t ` +
|
||||
`WHERE c.name = 'E2E-PHW-6A' AND c.tenant_id = '${TENANT_ID}'`
|
||||
);
|
||||
|
||||
// Homework for class 6B (Lucas)
|
||||
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, t.id, 'Exercices maths Lucas', 'Exercices chapitre 7', '${futureDueDate}', 'published', NOW(), NOW() ` +
|
||||
`FROM school_classes c, ` +
|
||||
`(SELECT id FROM subjects WHERE code = 'E2EPHWMAT' AND tenant_id = '${TENANT_ID}' LIMIT 1) s, ` +
|
||||
`(SELECT id FROM users WHERE email = '${TEACHER_EMAIL}' AND tenant_id = '${TENANT_ID}') t ` +
|
||||
`WHERE c.name = 'E2E-PHW-6B' AND c.tenant_id = '${TENANT_ID}'`
|
||||
);
|
||||
|
||||
// Homework due TODAY for class 6A ("Aujourd'hui" badge)
|
||||
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, t.id, 'Devoir maths aujourd''hui', 'Exercices pour aujourd''hui', '${todayDueDate}', 'published', NOW(), NOW() ` +
|
||||
`FROM school_classes c, ` +
|
||||
`(SELECT id FROM subjects WHERE code = 'E2EPHWMAT' AND tenant_id = '${TENANT_ID}' LIMIT 1) s, ` +
|
||||
`(SELECT id FROM users WHERE email = '${TEACHER_EMAIL}' AND tenant_id = '${TENANT_ID}') t ` +
|
||||
`WHERE c.name = 'E2E-PHW-6A' AND c.tenant_id = '${TENANT_ID}'`
|
||||
);
|
||||
|
||||
// Overdue homework for class 6A ("En retard" badge)
|
||||
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, t.id, 'Devoir français en retard', 'Exercices en retard', '${overdueDueDate}', 'published', NOW(), NOW() ` +
|
||||
`FROM school_classes c, ` +
|
||||
`(SELECT id FROM subjects WHERE code = 'E2EPHWFRA' AND tenant_id = '${TENANT_ID}' LIMIT 1) s, ` +
|
||||
`(SELECT id FROM users WHERE email = '${TEACHER_EMAIL}' AND tenant_id = '${TENANT_ID}') t ` +
|
||||
`WHERE c.name = 'E2E-PHW-6A' AND c.tenant_id = '${TENANT_ID}'`
|
||||
);
|
||||
|
||||
// Link parent to both students via admin UI
|
||||
const page = await browser.newPage();
|
||||
await loginAsAdmin(page);
|
||||
await addGuardianIfNotLinked(page, student1UserId, PARENT_EMAIL, 'tuteur');
|
||||
await addGuardianIfNotLinked(page, student2UserId, PARENT_EMAIL, 'tutrice');
|
||||
await page.close();
|
||||
|
||||
clearCache();
|
||||
});
|
||||
|
||||
// ======================================================================
|
||||
// AC1: Liste devoirs enfant
|
||||
// ======================================================================
|
||||
test.describe('AC1: Homework List', () => {
|
||||
test('parent can navigate to homework page via navigation', async ({ page }) => {
|
||||
await loginAsParent(page);
|
||||
|
||||
const nav = page.locator('.desktop-nav');
|
||||
await expect(nav.getByRole('link', { name: /devoirs/i })).toBeVisible({ timeout: 15000 });
|
||||
});
|
||||
|
||||
test('parent homework page shows homework list', async ({ page }) => {
|
||||
await loginAsParent(page);
|
||||
await page.goto(`${ALPHA_URL}/dashboard/parent-homework`);
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', { name: /devoirs des enfants/i })
|
||||
).toBeVisible({ timeout: 15000 });
|
||||
|
||||
// Homework cards should be visible
|
||||
const cards = page.locator('.homework-card');
|
||||
await expect(cards.first()).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
});
|
||||
|
||||
// ======================================================================
|
||||
// AC2: Vue identique élève (sans marquage "Fait")
|
||||
// ======================================================================
|
||||
test.describe('AC2: Student-like View Without Done Toggle', () => {
|
||||
test('homework cards do NOT show done toggle checkbox', async ({ page }) => {
|
||||
await loginAsParent(page);
|
||||
await page.goto(`${ALPHA_URL}/dashboard/parent-homework`);
|
||||
|
||||
const card = page.locator('.homework-card').first();
|
||||
await expect(card).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// No toggle-done button should exist (privacy)
|
||||
await expect(card.locator('.toggle-done')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('homework cards show title and due date', async ({ page }) => {
|
||||
await loginAsParent(page);
|
||||
await page.goto(`${ALPHA_URL}/dashboard/parent-homework`);
|
||||
|
||||
const card = page.locator('.homework-card').first();
|
||||
await expect(card).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Title visible
|
||||
await expect(card.locator('.card-title')).toBeVisible();
|
||||
// Due date visible
|
||||
await expect(card.locator('.due-date')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
// ======================================================================
|
||||
// AC3: Vue multi-enfants
|
||||
// ======================================================================
|
||||
test.describe('AC3: Multi-Child View', () => {
|
||||
test('parent with multiple children sees child selector', async ({ page }) => {
|
||||
await loginAsParent(page);
|
||||
await page.goto(`${ALPHA_URL}/dashboard/parent-homework`);
|
||||
|
||||
const childSelector = page.locator('.child-selector');
|
||||
await expect(childSelector).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Should have "Tous" + 2 children buttons
|
||||
const buttons = childSelector.locator('.child-button');
|
||||
await expect(buttons).toHaveCount(3);
|
||||
});
|
||||
|
||||
test('consolidated view shows homework grouped by child', async ({ page }) => {
|
||||
await loginAsParent(page);
|
||||
await page.goto(`${ALPHA_URL}/dashboard/parent-homework`);
|
||||
|
||||
// Wait for data to load
|
||||
const card = page.locator('[data-testid="homework-card"]').first();
|
||||
await expect(card).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Both children's names should appear as section headers
|
||||
const childNames = page.locator('[data-testid="child-name"]');
|
||||
await expect(childNames).toHaveCount(2, { timeout: 10000 });
|
||||
});
|
||||
|
||||
test('clicking a specific child filters to their homework', async ({ page }) => {
|
||||
await loginAsParent(page);
|
||||
await page.goto(`${ALPHA_URL}/dashboard/parent-homework`);
|
||||
|
||||
const childSelector = page.locator('.child-selector');
|
||||
await expect(childSelector).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Click on first child (Emma)
|
||||
const buttons = childSelector.locator('.child-button');
|
||||
await buttons.nth(1).click();
|
||||
|
||||
// Wait for data to reload
|
||||
const card = page.locator('.homework-card').first();
|
||||
await expect(card).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Should no longer show multiple child sections
|
||||
await expect(page.locator('.child-name')).toHaveCount(0, { timeout: 5000 });
|
||||
});
|
||||
});
|
||||
|
||||
// ======================================================================
|
||||
// AC4: Mise en évidence urgence
|
||||
// ======================================================================
|
||||
test.describe('AC4: Urgency Highlight', () => {
|
||||
test('homework due tomorrow shows urgent badge', async ({ page }) => {
|
||||
await loginAsParent(page);
|
||||
await page.goto(`${ALPHA_URL}/dashboard/parent-homework`);
|
||||
|
||||
await expect(page.locator('[data-testid="homework-card"]').first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Find urgent badge — text depends on when test runs relative to seeded date
|
||||
const urgentBadge = page.locator('[data-testid="urgent-badge"]');
|
||||
await expect(urgentBadge.first()).toBeVisible({ timeout: 5000 });
|
||||
await expect(urgentBadge.first()).toContainText(/pour demain|aujourd'hui|en retard/i);
|
||||
});
|
||||
|
||||
test('urgent homework card has red styling', async ({ page }) => {
|
||||
await loginAsParent(page);
|
||||
await page.goto(`${ALPHA_URL}/dashboard/parent-homework`);
|
||||
|
||||
await expect(page.locator('[data-testid="homework-card"]').first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Urgent card should have the urgent class
|
||||
const urgentCard = page.locator('[data-testid="homework-card"].urgent');
|
||||
await expect(urgentCard.first()).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('urgent homework shows contact teacher link', async ({ page }) => {
|
||||
await loginAsParent(page);
|
||||
await page.goto(`${ALPHA_URL}/dashboard/parent-homework`);
|
||||
|
||||
await expect(page.locator('[data-testid="homework-card"]').first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Contact teacher link should be visible on urgent homework
|
||||
const contactLink = page.locator('[data-testid="contact-teacher"]');
|
||||
await expect(contactLink.first()).toBeVisible({ timeout: 5000 });
|
||||
await expect(contactLink.first()).toContainText(/contacter l'enseignant/i);
|
||||
});
|
||||
|
||||
test('contact teacher link points to messaging page', async ({ page }) => {
|
||||
await loginAsParent(page);
|
||||
await page.goto(`${ALPHA_URL}/dashboard/parent-homework`);
|
||||
|
||||
await expect(page.locator('[data-testid="homework-card"]').first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
const contactLink = page.locator('[data-testid="contact-teacher"]').first();
|
||||
await expect(contactLink).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Verify href contains message creation path with proper encoding
|
||||
const href = await contactLink.getAttribute('href');
|
||||
expect(href).toContain('/messages/new');
|
||||
expect(href).toContain('to=');
|
||||
expect(href).toContain('subject=Devoir');
|
||||
});
|
||||
});
|
||||
|
||||
// ======================================================================
|
||||
// Homework detail
|
||||
// ======================================================================
|
||||
test.describe('Homework Detail', () => {
|
||||
test('clicking a homework card shows detail view', async ({ page }) => {
|
||||
await loginAsParent(page);
|
||||
await page.goto(`${ALPHA_URL}/dashboard/parent-homework`);
|
||||
|
||||
const card = page.locator('.homework-card').first();
|
||||
await expect(card).toBeVisible({ timeout: 10000 });
|
||||
await card.click();
|
||||
|
||||
await expect(page.locator('.homework-detail')).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator('.detail-title')).toBeVisible();
|
||||
});
|
||||
|
||||
test('back button returns to homework list', async ({ page }) => {
|
||||
await loginAsParent(page);
|
||||
await page.goto(`${ALPHA_URL}/dashboard/parent-homework`);
|
||||
|
||||
const card = page.locator('.homework-card').first();
|
||||
await expect(card).toBeVisible({ timeout: 10000 });
|
||||
await card.click();
|
||||
|
||||
await expect(page.locator('.homework-detail')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Click back
|
||||
await page.locator('.back-button').click();
|
||||
await expect(page.locator('.homework-card').first()).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
});
|
||||
|
||||
// ======================================================================
|
||||
// AC4 Extended: Urgency Badge Variants
|
||||
// ======================================================================
|
||||
test.describe('AC4 Extended: Urgency Badge Variants', () => {
|
||||
test('homework due today shows "Aujourd\'hui" badge', async ({ page }) => {
|
||||
await loginAsParent(page);
|
||||
await page.goto(`${ALPHA_URL}/dashboard/parent-homework`);
|
||||
|
||||
await expect(page.locator('[data-testid="homework-card"]').first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Find the card for "Devoir maths aujourd'hui"
|
||||
const todayCard = page.locator('[data-testid="homework-card"]', {
|
||||
has: page.locator('.card-title', { hasText: "Devoir maths aujourd'hui" })
|
||||
});
|
||||
await expect(todayCard).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify urgent badge shows "Aujourd'hui"
|
||||
const badge = todayCard.locator('[data-testid="urgent-badge"]');
|
||||
await expect(badge).toBeVisible({ timeout: 5000 });
|
||||
await expect(badge).toContainText("Aujourd'hui");
|
||||
|
||||
// Badge should NOT have the overdue class
|
||||
await expect(badge).not.toHaveClass(/overdue/);
|
||||
});
|
||||
|
||||
test('overdue homework shows "En retard" badge with overdue styling', async ({ page }) => {
|
||||
await loginAsParent(page);
|
||||
await page.goto(`${ALPHA_URL}/dashboard/parent-homework`);
|
||||
|
||||
await expect(page.locator('[data-testid="homework-card"]').first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Find the card for the overdue homework
|
||||
const overdueCard = page.locator('[data-testid="homework-card"]', {
|
||||
has: page.locator('.card-title', { hasText: 'Devoir français en retard' })
|
||||
});
|
||||
await expect(overdueCard).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify urgent badge shows "En retard"
|
||||
const badge = overdueCard.locator('[data-testid="urgent-badge"]');
|
||||
await expect(badge).toBeVisible({ timeout: 5000 });
|
||||
await expect(badge).toContainText('En retard');
|
||||
|
||||
// Badge should have the overdue class for stronger styling
|
||||
await expect(badge).toHaveClass(/overdue/);
|
||||
});
|
||||
|
||||
test('overdue homework card has urgent styling', async ({ page }) => {
|
||||
await loginAsParent(page);
|
||||
await page.goto(`${ALPHA_URL}/dashboard/parent-homework`);
|
||||
|
||||
await expect(page.locator('[data-testid="homework-card"]').first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// The overdue card should also have the .urgent class
|
||||
const overdueCard = page.locator('[data-testid="homework-card"]', {
|
||||
has: page.locator('.card-title', { hasText: 'Devoir français en retard' })
|
||||
});
|
||||
await expect(overdueCard).toHaveClass(/urgent/);
|
||||
});
|
||||
});
|
||||
|
||||
// ======================================================================
|
||||
// AC1 Extended: Subject Filter
|
||||
// ======================================================================
|
||||
test.describe('AC1 Extended: Subject Filter', () => {
|
||||
test('subject filter chips are visible when multiple subjects exist', async ({ page }) => {
|
||||
await loginAsParent(page);
|
||||
await page.goto(`${ALPHA_URL}/dashboard/parent-homework`);
|
||||
|
||||
await expect(page.locator('[data-testid="homework-card"]').first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Filter bar should be visible
|
||||
const filterBar = page.locator('.filter-bar');
|
||||
await expect(filterBar).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Should have "Tous" chip + subject chips (at least Maths and Français)
|
||||
const chips = filterBar.locator('.filter-chip');
|
||||
const chipCount = await chips.count();
|
||||
expect(chipCount).toBeGreaterThanOrEqual(3); // Tous + Maths + Français
|
||||
|
||||
// "Tous" chip should be active by default
|
||||
const tousChip = filterBar.locator('.filter-chip', { hasText: 'Tous' });
|
||||
await expect(tousChip).toBeVisible();
|
||||
await expect(tousChip).toHaveClass(/active/);
|
||||
});
|
||||
|
||||
test('clicking a subject filter shows only homework of that subject', async ({ page }) => {
|
||||
await loginAsParent(page);
|
||||
await page.goto(`${ALPHA_URL}/dashboard/parent-homework`);
|
||||
|
||||
await expect(page.locator('[data-testid="homework-card"]').first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Count all homework cards before filtering
|
||||
const allCardsCount = await page.locator('[data-testid="homework-card"]').count();
|
||||
expect(allCardsCount).toBeGreaterThanOrEqual(2);
|
||||
|
||||
// Click on the "E2E-PHW-Français" filter chip
|
||||
const filterBar = page.locator('.filter-bar');
|
||||
const francaisChip = filterBar.locator('.filter-chip', { hasText: /Français/i });
|
||||
await expect(francaisChip).toBeVisible({ timeout: 5000 });
|
||||
await francaisChip.click();
|
||||
|
||||
// Wait for the filter to be applied (chip becomes active)
|
||||
await expect(francaisChip).toHaveClass(/active/, { timeout: 5000 });
|
||||
|
||||
// Wait for homework cards to update
|
||||
await expect(page.locator('[data-testid="homework-card"]').first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// All visible cards should be Français homework
|
||||
const filteredCards = page.locator('[data-testid="homework-card"]');
|
||||
const filteredCount = await filteredCards.count();
|
||||
expect(filteredCount).toBeLessThan(allCardsCount);
|
||||
|
||||
// Each visible card should show the Français subject name
|
||||
for (let i = 0; i < filteredCount; i++) {
|
||||
await expect(filteredCards.nth(i).locator('.subject-name')).toContainText(/Français/i);
|
||||
}
|
||||
});
|
||||
|
||||
test('clicking "Tous" resets the subject filter', async ({ page }) => {
|
||||
await loginAsParent(page);
|
||||
await page.goto(`${ALPHA_URL}/dashboard/parent-homework`);
|
||||
|
||||
await expect(page.locator('[data-testid="homework-card"]').first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Apply a filter first
|
||||
const filterBar = page.locator('.filter-bar');
|
||||
const francaisChip = filterBar.locator('.filter-chip', { hasText: /Français/i });
|
||||
await francaisChip.click();
|
||||
await expect(francaisChip).toHaveClass(/active/, { timeout: 5000 });
|
||||
await expect(page.locator('[data-testid="homework-card"]').first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
const filteredCount = await page.locator('[data-testid="homework-card"]').count();
|
||||
|
||||
// Now click "Tous" to reset
|
||||
const tousChip = filterBar.locator('.filter-chip', { hasText: 'Tous' });
|
||||
await tousChip.click();
|
||||
await expect(tousChip).toHaveClass(/active/, { timeout: 5000 });
|
||||
await expect(page.locator('[data-testid="homework-card"]').first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Card count should be greater than filtered count
|
||||
const resetCount = await page.locator('[data-testid="homework-card"]').count();
|
||||
expect(resetCount).toBeGreaterThan(filteredCount);
|
||||
});
|
||||
});
|
||||
|
||||
// ======================================================================
|
||||
// Dashboard Widget
|
||||
// ======================================================================
|
||||
test.describe('Dashboard Widget', () => {
|
||||
test('dashboard shows homework widget with child names', async ({ page }) => {
|
||||
await loginAsParent(page);
|
||||
|
||||
// Dashboard should load
|
||||
await expect(page.locator('.dashboard-grid')).toBeVisible({ timeout: 15000 });
|
||||
|
||||
// Homework section should show items
|
||||
const homeworkSection = page.locator('.dashboard-grid').locator('section', { hasText: /devoirs à venir/i });
|
||||
await expect(homeworkSection).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Items should be clickable buttons
|
||||
const homeworkBtn = homeworkSection.locator('button.homework-item').first();
|
||||
await expect(homeworkBtn).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Child name should be visible on items
|
||||
await expect(homeworkSection.locator('.homework-child').first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('dashboard shows homework from multiple children sorted by date', async ({ page }) => {
|
||||
await loginAsParent(page);
|
||||
|
||||
await expect(page.locator('.dashboard-grid')).toBeVisible({ timeout: 15000 });
|
||||
|
||||
// Wait for homework buttons to load
|
||||
const homeworkBtn = page.locator('button.homework-item').first();
|
||||
await expect(homeworkBtn).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Should see homework from both Emma and Lucas
|
||||
const childLabels = page.locator('.homework-child');
|
||||
const count = await childLabels.count();
|
||||
const names = new Set<string>();
|
||||
for (let i = 0; i < count; i++) {
|
||||
const text = await childLabels.nth(i).textContent();
|
||||
if (text) names.add(text.trim());
|
||||
}
|
||||
expect(names.size).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
test('clicking a homework item opens detail modal', async ({ page }) => {
|
||||
await loginAsParent(page);
|
||||
|
||||
await expect(page.locator('.dashboard-grid')).toBeVisible({ timeout: 15000 });
|
||||
|
||||
const homeworkBtn = page.locator('button.homework-item').first();
|
||||
await expect(homeworkBtn).toBeVisible({ timeout: 10000 });
|
||||
await homeworkBtn.click();
|
||||
|
||||
// Modal with detail should appear
|
||||
const modal = page.locator('[role="dialog"]');
|
||||
await expect(modal).toBeVisible({ timeout: 10000 });
|
||||
await expect(modal.locator('.detail-title')).toBeVisible();
|
||||
await expect(modal.locator('.teacher-name')).toBeVisible();
|
||||
});
|
||||
|
||||
test('homework detail modal closes with X button', async ({ page }) => {
|
||||
await loginAsParent(page);
|
||||
|
||||
await expect(page.locator('.dashboard-grid')).toBeVisible({ timeout: 15000 });
|
||||
|
||||
const homeworkBtn = page.locator('button.homework-item').first();
|
||||
await expect(homeworkBtn).toBeVisible({ timeout: 10000 });
|
||||
await homeworkBtn.click();
|
||||
|
||||
const modal = page.locator('[role="dialog"]');
|
||||
await expect(modal).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Close modal
|
||||
await page.locator('.homework-modal-close').click();
|
||||
await expect(modal).not.toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('"Voir tous les devoirs" link navigates to homework page', async ({ page }) => {
|
||||
await loginAsParent(page);
|
||||
|
||||
await expect(page.locator('.dashboard-grid')).toBeVisible({ timeout: 15000 });
|
||||
|
||||
await page.getByText(/voir tous les devoirs/i).click();
|
||||
await expect(
|
||||
page.getByRole('heading', { name: /devoirs des enfants/i })
|
||||
).toBeVisible({ timeout: 15000 });
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user