import { test, expect, Page } from '@playwright/test' /** * E2E for the Phase 9c parent-side: ParentManager on /schulkalender (teacher * UI) and the /eltern login + timetable view. Backend calls are intercepted * so the suite doesn't need a real teacher → parent invitation cycle. */ async function mockTeacherCalendar(page: Page, opts: { classes?: unknown[]; parents?: unknown[]; invite?: unknown } = {}) { // Existing schulkalender mocks the page already needs. await page.route('**/api/school/calendar/config', async (route) => { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ user_id: 'dev', bundesland: 'DE-NI' }) }) }) await page.route(/\/api\/school\/calendar\/holidays(\?.*)?$/, async (route) => route.fulfill({ status: 200, contentType: 'application/json', body: '[]' })) await page.route(/\/api\/school\/calendar\/events(\?.*)?$/, async (route) => route.fulfill({ status: 200, contentType: 'application/json', body: '[]' })) // ParentManager loads classes via the stundenplan API. await page.route('**/api/school/timetable/classes', async (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(opts.classes ?? []) })) await page.route('**/api/school/calendar/parents', async (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(opts.parents ?? []) })) await page.route('**/api/school/calendar/parents/invite', async (route) => { if (route.request().method() !== 'POST') return route.fulfill({ status: 405 }) return route.fulfill({ status: 201, contentType: 'application/json', body: JSON.stringify(opts.invite ?? { parent: { id: 'p1', email: 'mama@example.de', preferred_language: 'tr' }, child: { id: 'c1', parent_id: 'p1', tt_class_id: 'class-1', first_name: 'Max', last_name: 'Mueller' }, magic_token: 'abc123', magic_url: '/eltern/login?token=abc123', expires_at: new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString(), }), }) }) } test.describe('Schulkalender — ParentManager', () => { test('renders empty state when no parents invited', async ({ page }) => { await mockTeacherCalendar(page, { classes: [{ id: 'class-1', name: '5a', grade_level: 5, student_count: 24, created_by_user_id: 'u', created_at: '' }] }) await page.goto('/schulkalender') await page.waitForLoadState('networkidle') const manager = page.getByTestId('parent-manager') await expect(manager).toBeVisible() await expect(manager.getByText('Keine eingeladenen Eltern.')).toBeVisible() }) test('+ Eltern einladen opens the form when classes exist', async ({ page }) => { await mockTeacherCalendar(page, { classes: [{ id: 'class-1', name: '5a', grade_level: 5, student_count: 24, created_by_user_id: 'u', created_at: '' }] }) await page.goto('/schulkalender') await page.waitForLoadState('networkidle') await page.getByTestId('parent-invite-toggle').click() await expect(page.getByTestId('parent-email')).toBeVisible() }) test('submitting invite shows the magic link to copy', async ({ page }) => { await mockTeacherCalendar(page, { classes: [{ id: 'class-1', name: '5a', grade_level: 5, student_count: 24, created_by_user_id: 'u', created_at: '' }] }) await page.goto('/schulkalender') await page.waitForLoadState('networkidle') await page.getByTestId('parent-invite-toggle').click() await page.getByTestId('parent-email').fill('mama@example.de') await page.getByTestId('parent-child-first').fill('Max') await page.getByTestId('parent-child-last').fill('Mueller') await page.getByTestId('parent-class').selectOption('class-1') await page.getByTestId('parent-invite-submit').click() await expect(page.getByTestId('parent-invite-link')).toBeVisible() await expect(page.getByText('Einladungs-Link fuer mama@example.de')).toBeVisible() }) }) async function mockParentApi(page: Page, opts: { redeemOk?: boolean; me?: unknown; lessons?: unknown[] } = {}) { const redeemOk = opts.redeemOk ?? true await page.route('**/api/parent/auth/redeem', async (route) => { if (!redeemOk) return route.fulfill({ status: 401, contentType: 'application/json', body: '{"error":"invalid"}' }) return route.fulfill({ status: 200, contentType: 'application/json', headers: { 'set-cookie': 'bp_parent_session=test; Path=/; HttpOnly' }, body: JSON.stringify({ id: 'p1', email: 'mama@example.de', preferred_language: 'tr' }), }) }) await page.route('**/api/parent/me', async (route) => { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(opts.me ?? { parent: { id: 'p1', email: 'mama@example.de', preferred_language: 'tr' }, children: [{ id: 'c1', parent_id: 'p1', tt_class_id: 'class-1', first_name: 'Max', last_name: 'Mueller', class_name: '5a' }], }), }) }) await page.route(/\/api\/parent\/me\/timetable(\?.*)?$/, async (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(opts.lessons ?? []) })) await page.route('**/api/parent/auth/logout', async (route) => route.fulfill({ status: 200, contentType: 'application/json', body: '{"message":"ok"}' })) } test.describe('Eltern — Login + Wochengrid', () => { test('login page shows error when no token in URL', async ({ page }) => { await mockParentApi(page) await page.goto('/eltern/login') await expect(page.getByTestId('eltern-login')).toBeVisible() await expect(page.getByText('Kein Token in der URL')).toBeVisible() }) test('valid token redirects to the parent overview', async ({ page }) => { await mockParentApi(page, {}) await page.goto('/eltern/login?token=abc123') await page.waitForURL('**/eltern', { timeout: 3000 }) await expect(page.getByTestId('eltern-page')).toBeVisible() }) test('shows greeting and child class on /eltern', async ({ page }) => { await mockParentApi(page) await page.goto('/eltern/login?token=abc123') await page.waitForURL('**/eltern') // Turkish greeting because preferred_language=tr. await expect(page.getByText('Hoş geldiniz, mama@example.de')).toBeVisible() await expect(page.getByText('Max Mueller · 5a')).toBeVisible() }) test('translates subject names into the parent language', async ({ page }) => { await mockParentApi(page, { lessons: [ { DayOfWeek: 1, PeriodIndex: 1, StartTime: '08:00', EndTime: '08:45', ClassName: '5a', SubjectName: 'Mathematik', SubjectCode: 'M', TeacherName: 'Schmidt, Anna', RoomName: 'A101', Pinned: false }, ], }) await page.goto('/eltern/login?token=abc123') await page.waitForURL('**/eltern') // Turkish target = Matematik. await expect(page.getByTestId('eltern-cell-1-1').getByText('Matematik')).toBeVisible() }) })