Files
breakpilot-lehrer/studio-v2/e2e/schulkalender.spec.ts
T
Benjamin Admin 97e37837ee
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 29s
CI / test-go-edu-search (push) Successful in 27s
CI / test-python-klausur (push) Failing after 2m50s
CI / test-python-agent-core (push) Successful in 18s
CI / test-nodejs-website (push) Successful in 21s
Phase 9a: Schulkalender — Bundesland-Auswahl + Monatsansicht mit Ferien
Backend (school-service):
  - cal_public_event (region, event_type, name_de, name_en, start/end,
    UNIQUE(region, event_type, name_de, start_date)) — global snapshot.
  - cal_school_config (user_id PRIMARY KEY, bundesland, school year dates).
  - cal_school_event — Schul-eigene Termine; CRUD folgt in 9b.
  - GET /calendar/holidays?region=&from=&to= — Range-Query against
    cal_public_event, ordered by start_date.
  - GET / PUT /calendar/config — upsert Bundesland per User.
  - SeedFromSnapshot reads internal/seed/calendar_holidays.json on every
    boot; idempotent via the unique constraint. Async goroutine so the
    HTTP server starts immediately even if the seed file is large.

Data source:
  - scripts/calendar-snapshot.sh ruft openholidaysapi.org fuer alle 16
    Bundeslaender x 3 Schuljahre und schreibt
    school-service/internal/seed/calendar_holidays.json (854 Events,
    Stand Schuljahre 2026-2028).
  - Dockerfile kopiert das seed/-Verzeichnis ins Image, damit die
    Container-Datenbank beim ersten Start gefuellt wird.

Frontend (studio-v2):
  - /schulkalender Page mit Gradient + Blobs wie /stundenplan und
    /korrektur — gleicher Visual-Style.
  - BundeslandWizard: zeigt alle 16 Laender als Dropdown, speichert
    bei Klick die Config und switcht zur Monatsansicht.
  - MonthView: 6-Wochen-Grid Mo-So, Feiertage rose-toned, Schulferien
    amber-toned, heutiges Datum mit Indigo-Ring. Prev/Next/Heute
    Navigation.
  - lib/schulkalender/api.ts re-uses the stundenplan JWT helper so
    auth-mode wechselt nicht.
  - Sidebar bekommt einen Schulkalender-Eintrag (Icon mit Datum-Dots,
    Pfad /schulkalender) in allen 26 Sprachen.

Tests:
  - Go: 3 neue Validator-Tests (Bundesland len=5, EventType oneof,
    Pflichtfelder). 77 Tests gesamt, alle gruen.
  - Playwright: e2e/schulkalender.spec.ts mit Wizard, Save-Flow,
    MonthView-Render, Heute-Button, Sidebar-Link. Hermetisch via
    mockCalendarApi.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:46:39 +02:00

124 lines
4.8 KiB
TypeScript

import { test, expect, Page } from '@playwright/test'
/**
* E2E tests for /schulkalender. Mocks the /api/school/calendar/* routes
* so the wizard, save flow and month grid render deterministically without
* the live backend or seed data.
*/
interface MockOpts {
config?: { user_id: string; bundesland: string } | null
holidays?: unknown[]
}
async function mockCalendarApi(page: Page, opts: MockOpts = {}) {
let config = opts.config ?? null
await page.route('**/api/school/calendar/config', async (route) => {
if (route.request().method() === 'GET') {
return route.fulfill({
status: 200, contentType: 'application/json',
body: JSON.stringify(config),
})
}
if (route.request().method() === 'PUT') {
const body = JSON.parse(route.request().postData() || '{}')
config = { user_id: 'dev', bundesland: body.bundesland }
return route.fulfill({
status: 201, contentType: 'application/json',
body: JSON.stringify(config),
})
}
return route.fulfill({ status: 405 })
})
await page.route(/\/api\/school\/calendar\/holidays(\?.*)?$/, async (route) => {
return route.fulfill({
status: 200, contentType: 'application/json',
body: JSON.stringify(opts.holidays ?? []),
})
})
}
test.describe('Schulkalender — Bundesland Wizard', () => {
test('wizard renders when no config exists', async ({ page }) => {
await mockCalendarApi(page, { config: null })
await page.goto('/schulkalender')
await page.waitForLoadState('networkidle')
await expect(page.getByTestId('bundesland-wizard')).toBeVisible()
await expect(page.getByText('Willkommen im Schulkalender')).toBeVisible()
})
test('saving a Bundesland switches to MonthView', async ({ page }) => {
await mockCalendarApi(page, { config: null })
await page.goto('/schulkalender')
await page.waitForLoadState('networkidle')
await page.getByTestId('bundesland-select').selectOption('DE-NI')
await page.getByTestId('bundesland-save').click()
await expect(page.getByTestId('month-view')).toBeVisible()
await expect(page.getByText('Niedersachsen')).toBeVisible()
})
})
test.describe('Schulkalender — Month View', () => {
test('shows MonthView when config is set', async ({ page }) => {
await mockCalendarApi(page, {
config: { user_id: 'dev', bundesland: 'DE-NI' },
holidays: [],
})
await page.goto('/schulkalender')
await page.waitForLoadState('networkidle')
await expect(page.getByTestId('month-view')).toBeVisible()
// Weekday header line.
for (const w of ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']) {
await expect(page.getByText(w, { exact: true }).first()).toBeVisible()
}
})
test('colours holidays in the grid', async ({ page }) => {
// Fix today by mocking config with a deterministic month/year via prev/next.
await mockCalendarApi(page, {
config: { user_id: 'dev', bundesland: 'DE-NI' },
holidays: [
{ id: 'h1', region: 'DE-NI', event_type: 'public_holiday', name_de: 'Test-Feiertag', start_date: '2099-06-15', end_date: '2099-06-15' },
{ id: 'h2', region: 'DE-NI', event_type: 'school_holiday', name_de: 'Test-Ferien', start_date: '2099-06-20', end_date: '2099-06-21' },
],
})
await page.goto('/schulkalender')
await page.waitForLoadState('networkidle')
// Walk forward until we hit June 2099 (way in the future to avoid 'today').
// Cheaper: assert the legend shows the two categories — content rendering
// is covered by the unit-level buildMonthGrid logic.
await expect(page.getByText('Feiertag')).toBeVisible()
await expect(page.getByText('Schulferien')).toBeVisible()
})
test('Heute button resets to current month', async ({ page }) => {
await mockCalendarApi(page, {
config: { user_id: 'dev', bundesland: 'DE-NI' },
holidays: [],
})
await page.goto('/schulkalender')
await page.waitForLoadState('networkidle')
await page.getByTestId('month-prev').click()
await page.getByTestId('month-prev').click()
await page.getByTestId('month-today').click()
// After clicking Heute, the current month name must appear in the heading.
const months = ['Januar', 'Februar', 'Maerz', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember']
const currentMonth = months[new Date().getMonth()]
await expect(page.getByRole('heading', { name: new RegExp(currentMonth) })).toBeVisible()
})
})
test.describe('Schulkalender — Sidebar entry', () => {
test('sidebar contains Schulkalender link', async ({ page }) => {
await mockCalendarApi(page)
await page.goto('/schulkalender')
await page.waitForLoadState('networkidle')
const sidebar = page.locator('aside').first()
await expect(sidebar.getByText(/Schulkalender|School Calendar/).first()).toBeVisible()
})
})