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: 60000 }), 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: 60000 }), 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(); }); test.beforeEach(async () => { try { execSync( `docker compose -f "${composeFile}" exec -T php php bin/console cache:pool:clear cache.rate_limiter --env=dev 2>&1`, { encoding: 'utf-8' } ); } catch { // Cache pool may not exist } }); // ====================================================================== // 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 the card count to actually decrease (async data reload) await expect.poll( () => page.locator('[data-testid="homework-card"]').count(), { timeout: 15000, message: 'Filter should reduce homework card count' } ).toBeLessThan(allCardsCount); // All visible cards should be Français homework const filteredCards = page.locator('[data-testid="homework-card"]'); const filteredCount = await filteredCards.count(); // 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 Français filter and wait for it to take effect 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 }); // Wait for the filter to stabilize await page.waitForTimeout(2000); 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 }); // "Tous" should show at least as many cards as the filtered view await page.waitForTimeout(2000); await expect(page.locator('[data-testid="homework-card"]').first()).toBeVisible({ timeout: 10000 }); const resetCount = await page.locator('[data-testid="homework-card"]').count(); expect(resetCount).toBeGreaterThanOrEqual(filteredCount); // Verify "Tous" chip is active (filter was reset) await expect(tousChip).toHaveClass(/active/); }); }); // ====================================================================== // 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(); 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 }); }); }); });